Skip to content

Commit

Permalink
Split handlers and add signal handling
Browse files Browse the repository at this point in the history
  • Loading branch information
albireox committed Nov 26, 2023
1 parent c3efb00 commit 1721681
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 264 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ select = ["E", "F", "I"]
unfixable = ["F841"]

[tool.ruff.per-file-ignores]
"__init__.py" = ["F403", "E402"]
"__init__.py" = ["F403", "E402", "F401"]

[tool.ruff.isort]
known-first-party = ["ln2fill"]
Expand Down
24 changes: 23 additions & 1 deletion src/ln2fill/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@

from __future__ import annotations

import asyncio
import datetime
import logging
import pathlib
import signal
import sys
import warnings

from typing import Unpack
Expand Down Expand Up @@ -166,7 +169,7 @@ def update_options(
async def handle_fill(**options: Unpack[OptionsType]):
"""Handles the purge/fill process."""

from ln2fill.core import LN2Handler
from ln2fill.handlers import LN2Handler

if options["quiet"]:
log.sh.setLevel(logging.ERROR)
Expand All @@ -193,6 +196,18 @@ async def handle_fill(**options: Unpack[OptionsType]):

await handler.check()

async def signal_handler():
log.error("User aborted the process. Closing all valves before exiting.")
await handler.abort(only_active=False)
log.error("All valves closed. Exiting.")
asyncio.get_running_loop().call_soon(sys.exit, 0)

for signame in ("SIGINT", "SIGTERM"):
asyncio.get_running_loop().add_signal_handler(
getattr(signal, signame),
lambda: asyncio.create_task(signal_handler()),
)

max_purge_time = options["purge_time"] or options["max_purge_time"]
await handler.purge(
use_thermistor=options["use_thermistors"],
Expand All @@ -201,6 +216,13 @@ async def handle_fill(**options: Unpack[OptionsType]):
prompt=not options["no_prompt"],
)

max_fill_time = options["fill_time"] or options["max_fill_time"]
await handler.fill(
use_thermistors=options["use_thermistors"],
min_fill_time=options["min_fill_time"],
max_fill_time=max_fill_time,
prompt=not options["no_prompt"],
)

@click.command(name="ln2fill")
@click.argument(
Expand Down
13 changes: 13 additions & 0 deletions src/ln2fill/handlers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @Author: José Sánchez-Gallego ([email protected])
# @Date: 2023-11-26
# @Filename: __init__.py
# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause)

from __future__ import annotations

from .ln2 import LN2Handler
from .thermistor import ThermistorHandler
from .valve import ValveHandler
266 changes: 4 additions & 262 deletions src/ln2fill/core.py → src/ln2fill/handlers/ln2.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,287 +2,29 @@
# -*- coding: utf-8 -*-
#
# @Author: José Sánchez-Gallego ([email protected])
# @Date: 2023-11-11
# @Filename: ln2fill.py
# @Date: 2023-11-26
# @Filename: ln2.py
# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause)

from __future__ import annotations

import asyncio
import dataclasses
from time import time

from typing import Coroutine, Optional
from typing import Coroutine

import sshkeyboard
from pydantic import Field, model_validator
from pydantic.dataclasses import dataclass
from rich.progress import TaskID

from sdsstools.configuration import RecursiveDict

from ln2fill import config, log
from ln2fill.handlers.valve import ValveHandler
from ln2fill.tools import (
TimerProgressBar,
cancel_nps_threads,
cancel_task,
get_spectrograph_status,
read_thermistors,
valve_on_off,
)


PROGRESS_BAR = TimerProgressBar(console=log.rich_console)


@dataclass
class ThermistorHandler:
"""Reads a thermistor, potentially closing the valve if active.
Parameters
----------
valve_handler
The `.ValveHandler` instance associated with this thermistor.
interval
The interval in seconds between thermistor reads.
min_open_time
The minimum valve open time. The thermistor will not be read until
the minimum time has been reached.
close_valve
If ``True``, closes the valve once the thermistor is active.
min_active_time
Number of seconds the thermistor must be active before the valve is
closed.
"""

valve_handler: ValveHandler
interval: float = 1
min_open_time: float = Field(default=0.0, ge=0.0)
close_valve: bool = True
min_active_time: float = Field(default=10.0, ge=0.0)

def __init__(self, *args, **kwargs):
self.valve_handler = self.valve_handler

# Unix time at which we started to monitor.
self._start_time: float = -1

# Unix time at which the thermistor became active.
self._active_time: float = -1

thermistor_channel = self.valve_handler.thermistor_channel
assert thermistor_channel

self.thermistor_channel: str = thermistor_channel

async def read_thermistor(self):
"""Reads the thermistor and returns its state."""

thermistors = await read_thermistors()
return thermistors[self.thermistor_channel]

async def start_monitoring(self):
"""Monitors the thermistor and potentially closes the valve."""

await asyncio.sleep(self.min_open_time)

while True:
thermistor_status = await self.read_thermistor()
if thermistor_status is True:
if self._active_time < 0:
self._active_time = time.time()
if time.time() - self._active_time > self.min_active_time:
break

await asyncio.sleep(self.interval)

if self.close_valve:
await self.valve_handler.finish_fill()


@dataclass
class ValveHandler:
"""Handles a valve, including opening and closing, timeouts, and thermistors.
Parameters
----------
valve
The name of the valve. Additional information such as thermistor channel,
actor names, etc. are determined from the configuration file if not
explicitely provided.
thermistor_channel
The channel of the thermistor associated with the valve.
nps_actor
The NPS actor that command the valve.
show_progress_bar
Whether to show a progress bar during fills.
"""

valve: str
thermistor_channel: Optional[str] = dataclasses.field(default=None, repr=False)
nps_actor: Optional[str] = dataclasses.field(default=None, repr=False)
show_progress_bar: Optional[bool] = dataclasses.field(default=True, repr=False)

@model_validator(mode="after")
def validate_fields(self):
if self.valve not in config["valves"]:
raise ValueError(f"Unknown valve {self.valve!r}.")

if self.thermistor_channel is None:
thermistor_key = f"valves.{self.valve}.thermistor"
self.thermistor_channel = config.get(thermistor_key, self.valve)

if self.nps_actor is None:
self.nps_actor = config.get(f"valves.{self.valve}.actor")
if self.nps_actor is None:
raise ValueError(f"Cannot find NPS actor for valve {self.valve!r}.")

return self

def __post_init__(self):
self.thermistor = ThermistorHandler(self)

self._thread_id: int | None = None
self._progress_bar_id: TaskID | None = None

self._monitor_task: asyncio.Task | None = None
self._timeout_task: asyncio.Task | None = None

self.event = asyncio.Event()
self.active: bool = False

async def start_fill(
self,
min_open_time: float = 0.0,
timeout: float | None = None,
use_thermistor: bool = True,
):
"""Starts a fill.
Parameters
----------
min_open_time
Minimum time to keep the valve open.
timeout
The maximumtime to keep the valve open. If ``None``, defaults to
the ``max_open_time`` value.
use_thermistor
Whether to use the thermistor to close the valve. If ``True`` and
``fill_time`` is not ``None``, ``fill_time`` become the maximum
open time.
"""

if timeout is None:
# Some hardcoded hard limits.
if self.valve == "purge":
timeout = 2000
else:
timeout = 600

await self._set_state(True, timeout=timeout, use_script=True)

if use_thermistor:
self.thermistor.min_open_time = min_open_time
self._monitor_task = asyncio.create_task(self.thermistor.start_monitoring())

if self.show_progress_bar:
if self.valve.lower() == "purge":
initial_description = "Purge in progress ..."
complete_description = "Purge complete"
else:
initial_description = "Fill in progress ..."
complete_description = "Fill complete"

self._progress_bar_id = await PROGRESS_BAR.add_timer(
timeout,
label=self.valve,
initial_description=initial_description,
complete_description=complete_description,
)

await asyncio.sleep(2)
self._timeout_task = asyncio.create_task(self._schedule_timeout_task(timeout))

self.active = True

self.event.clear()
await self.event.wait()

async def _schedule_timeout_task(self, timeout: float):
"""Schedules a task to cancel the fill after a timeout."""

await asyncio.sleep(timeout)
await self.finish_fill()

async def finish_fill(self):
"""Finishes the fill, closing the valve."""

await self._set_state(False)

await cancel_task(self._monitor_task)
self._monitor_task = None

if self._progress_bar_id is not None:
await PROGRESS_BAR.stop_timer(self._progress_bar_id)
self._progress_bar_id = None

await cancel_task(self._timeout_task)
self._timeout_task = None

if not self.event.is_set():
self.event.set()

self.active = False

async def _set_state(
self,
on: bool,
use_script: bool = True,
timeout: float | None = None,
):
"""Sets the state of the valve.
Parameters
----------
on
Whether to open or close the valve.
use_script
Whether to use the ``cycle_with_timeout`` script to set a timeout
after which the valve will be closed.
timeout
Maximum open valve time.
"""

assert self.nps_actor is not None

# If there's already a thread running for this valve, we cancel it.
if self._thread_id is not None:
await cancel_nps_threads(self.nps_actor, self._thread_id)

thread_id = await valve_on_off(
self.valve,
on,
timeout=timeout,
use_script=use_script,
)

if thread_id is not None:
self._thread_id = thread_id

if on:
if thread_id is not None:
log.debug(
f"Valve {self.valve!r} was opened with timeout={timeout} "
f"(thread_id={thread_id})."
)
else:
log.debug(f"Valve {self.valve!r} was closed.")


@dataclass
class LN2Handler:
"""The main LN2 purge/fill handlerclass.
Expand Down
Loading

0 comments on commit 1721681

Please sign in to comment.