From deaff0edf5e7d4cfcff0b08a653d171cef028254 Mon Sep 17 00:00:00 2001 From: Brian Brondel Date: Tue, 3 Dec 2024 18:36:57 -0300 Subject: [PATCH 1/7] Make sure the loop runs infinitely. --- python/lsst/ts/vent/controller/run_dispatcher.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python/lsst/ts/vent/controller/run_dispatcher.py b/python/lsst/ts/vent/controller/run_dispatcher.py index 3b54842..6d902a0 100644 --- a/python/lsst/ts/vent/controller/run_dispatcher.py +++ b/python/lsst/ts/vent/controller/run_dispatcher.py @@ -151,7 +151,11 @@ async def async_main() -> None: ) # Keep the event loop running indefinitely. - await asyncio.Event().wait() + try: + while True: + await asyncio.sleep(60) + except asyncio.CancelledError: + log.info("Event loop is stopping.") def main() -> None: From 6b2e6626a29da3554eddd99c8a3efbcdafa5759e Mon Sep 17 00:00:00 2001 From: Brian Brondel Date: Wed, 4 Dec 2024 16:00:57 -0300 Subject: [PATCH 2/7] Set frequency to zero when changing control mode --- python/lsst/ts/vent/controller/controller.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/lsst/ts/vent/controller/controller.py b/python/lsst/ts/vent/controller/controller.py index ed0129b..5becb82 100644 --- a/python/lsst/ts/vent/controller/controller.py +++ b/python/lsst/ts/vent/controller/controller.py @@ -137,6 +137,9 @@ async def fan_manual_control(self, manual: bool) -> None: self.log.debug("set vfd_manual_control") assert self.connected assert self.vfd_client is not None + + await self.set_fan_frequency(0.0) + settings = vf_drive.MANUAL if manual else vf_drive.AUTO for address, value in zip(vf_drive.CFG_REGISTERS, settings): await self.vfd_client.write_register( From e425235145f6de59a21bb1505c075d757ec57dc7 Mon Sep 17 00:00:00 2001 From: Brian Brondel Date: Wed, 4 Dec 2024 16:06:25 -0300 Subject: [PATCH 3/7] Reverse order of last 8 faults --- python/lsst/ts/vent/controller/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lsst/ts/vent/controller/controller.py b/python/lsst/ts/vent/controller/controller.py index 5becb82..46e2502 100644 --- a/python/lsst/ts/vent/controller/controller.py +++ b/python/lsst/ts/vent/controller/controller.py @@ -330,7 +330,7 @@ async def last8faults(self) -> list[tuple[int, str]]: address=vf_drive.Registers.FAULT_REGISTER, count=8, ) - return [(r, vf_drive.FAULTS[r]) for r in rvals.registers] + return [(r, vf_drive.FAULTS[r]) for r in reversed(rvals.registers)] def vent_open(self, vent_number: int) -> None: """Opens the specified vent. From c19bef23b98e1d41bae964922862e80cfa91099c Mon Sep 17 00:00:00 2001 From: Brian Brondel Date: Wed, 15 Jan 2025 12:14:52 -0300 Subject: [PATCH 4/7] Reported frequency should work in manual or auto mode --- python/lsst/ts/vent/controller/controller.py | 17 ++++++----------- python/lsst/ts/vent/controller/vf_drive.py | 1 + 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/python/lsst/ts/vent/controller/controller.py b/python/lsst/ts/vent/controller/controller.py index 46e2502..45e22bc 100644 --- a/python/lsst/ts/vent/controller/controller.py +++ b/python/lsst/ts/vent/controller/controller.py @@ -191,20 +191,15 @@ async def get_fan_frequency(self) -> float: self.log.debug("get fan_frequency") assert self.connected assert self.vfd_client is not None - cmd = ( - await self.vfd_client.read_holding_registers( - slave=self.config.device_id, address=vf_drive.Registers.CMD_REGISTER - ) - ).registers[0] - if cmd == 0: - return 0.0 - lfr = ( + output_frequency = ( await self.vfd_client.read_holding_registers( - slave=self.config.device_id, address=vf_drive.Registers.LFR_REGISTER + slave=self.config.device_id, address=vf_drive.Registers.RFR_REGISTER ) - ).registers[0] - return 0.1 * lfr + ).registers[ + 0 + ] * 0.1 # RFR register holds frequency in units of 0.1 Hz + return output_frequency async def set_fan_frequency(self, frequency: float) -> None: """Sets the target frequency for the dome exhaust fan. The frequency diff --git a/python/lsst/ts/vent/controller/vf_drive.py b/python/lsst/ts/vent/controller/vf_drive.py index b16a69b..2ea8408 100644 --- a/python/lsst/ts/vent/controller/vf_drive.py +++ b/python/lsst/ts/vent/controller/vf_drive.py @@ -23,6 +23,7 @@ class Registers(IntEnum): + RFR_REGISTER = 3202 SLL_REGISTER = 7010 RSF_REGISTER = 7124 FAULT_REGISTER = 7201 From fe2d3bf28cee5481651251a3bd1084ba9aec1b40 Mon Sep 17 00:00:00 2001 From: Brian Brondel Date: Wed, 15 Jan 2025 14:28:33 -0300 Subject: [PATCH 5/7] Add the RFR register into the simulator --- python/lsst/ts/vent/controller/controller.py | 5 +-- .../vent/controller/dome_vents_simulator.py | 39 +++++++++++++++++++ .../ts/vent/controller/simulator_setup.json | 3 +- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/python/lsst/ts/vent/controller/controller.py b/python/lsst/ts/vent/controller/controller.py index 45e22bc..e10eb6b 100644 --- a/python/lsst/ts/vent/controller/controller.py +++ b/python/lsst/ts/vent/controller/controller.py @@ -196,9 +196,8 @@ async def get_fan_frequency(self) -> float: await self.vfd_client.read_holding_registers( slave=self.config.device_id, address=vf_drive.Registers.RFR_REGISTER ) - ).registers[ - 0 - ] * 0.1 # RFR register holds frequency in units of 0.1 Hz + ).registers[0] + output_frequency *= 0.1 # RFR register holds frequency in units of 0.1 Hz return output_frequency async def set_fan_frequency(self, frequency: float) -> None: diff --git a/python/lsst/ts/vent/controller/dome_vents_simulator.py b/python/lsst/ts/vent/controller/dome_vents_simulator.py index ce46e68..9d3070b 100644 --- a/python/lsst/ts/vent/controller/dome_vents_simulator.py +++ b/python/lsst/ts/vent/controller/dome_vents_simulator.py @@ -22,9 +22,11 @@ import os import random +from pymodbus.datastore.simulator import Cell from pymodbus.server import ModbusSimulatorServer from .config import Config +from .vf_drive import Registers class DomeVentsSimulator: @@ -39,6 +41,7 @@ def __init__(self, config: Config): http_host="localhost", http_port=self.http_port, json_file=os.path.dirname(__file__) + "/simulator_setup.json", + custom_actions_module=__name__, ) async def start(self) -> None: @@ -138,3 +141,39 @@ def set_bits(self, input_bits: tuple[int]) -> None: """ assert len(input_bits) == 16 self.input_bits = list(input_bits) + + +def mirror_lfr_action( + registers: list[Cell], + inx: int, + cell: Cell, + minval: int | None = None, + maxval: int | None = None, +) -> None: + """Custom action for the modbus simulator. + + The RFR register should read the same value as what was written to the + LFR register. This custom action is tied to the RFR register + to ensure that it behaves as expected. + + Parameters + ---------- + registers: list[Cell] + An array of all cells in the simulated modbus server. + + inx: int + The index of the cell being read. + + cell: Cell + An object representing the cell being read. + + minval: int + The minimum allowed value for the cell. + + maxval: int + The maximum allowed value for the cell. + """ + cell.value = registers[Registers.LFR_REGISTER].value + + +custom_actions_dict = {"mirror_lfr": mirror_lfr_action} diff --git a/python/lsst/ts/vent/controller/simulator_setup.json b/python/lsst/ts/vent/controller/simulator_setup.json index d188caf..70740f7 100644 --- a/python/lsst/ts/vent/controller/simulator_setup.json +++ b/python/lsst/ts/vent/controller/simulator_setup.json @@ -34,9 +34,10 @@ } }, "invalid": [], - "write": [ 7010, 7124, 8401, 8413, 8423, 8501, 8502, 8602, 64279], + "write": [ 3202, 7010, 7124, 8401, 8413, 8423, 8501, 8502, 8602, 64279], "bits": [], "uint16": [ + { "addr": 3202, "value": 0, "action": "mirror_lfr" }, { "addr": 7010, "value": 1 }, { "addr": 7124, "value": 0 }, { "addr": [ 7201, 7211 ], "value": 22 }, From ff56efbb2c503b0c20579e92392938b211870fdb Mon Sep 17 00:00:00 2001 From: Brian Brondel Date: Wed, 15 Jan 2025 16:38:27 -0300 Subject: [PATCH 6/7] Add drive voltage to the telemetry. --- python/lsst/ts/vent/controller/controller.py | 24 +++++++++++++++++++ python/lsst/ts/vent/controller/dispatcher.py | 10 +++++++- .../ts/vent/controller/simulator_setup.json | 3 ++- python/lsst/ts/vent/controller/vf_drive.py | 1 + 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/python/lsst/ts/vent/controller/controller.py b/python/lsst/ts/vent/controller/controller.py index e10eb6b..c1d23d8 100644 --- a/python/lsst/ts/vent/controller/controller.py +++ b/python/lsst/ts/vent/controller/controller.py @@ -297,6 +297,30 @@ async def get_drive_state(self) -> FanDriveState: return FanDriveState.OPERATING return FanDriveState.FAULT + async def get_drive_voltage(self) -> float: + """Returns the target frequency configured in the drive. + + Raises + ------ + AssertionError + If the controller is not connected. + + ModbusException + If a communications error occurs. + """ + + self.log.debug("get drive_voltage") + assert self.connected + assert self.vfd_client is not None + + drive_voltage = ( + await self.vfd_client.read_holding_registers( + slave=self.config.device_id, address=vf_drive.Registers.ULN_REGISTER + ) + ).registers[0] + drive_voltage *= 0.1 # ULN register holds voltage in units of 0.1 V + return drive_voltage + async def last8faults(self) -> list[tuple[int, str]]: """Returns the last eight fault conditions recorded by the drive. diff --git a/python/lsst/ts/vent/controller/dispatcher.py b/python/lsst/ts/vent/controller/dispatcher.py index aeb3fd0..e9cbc60 100644 --- a/python/lsst/ts/vent/controller/dispatcher.py +++ b/python/lsst/ts/vent/controller/dispatcher.py @@ -243,6 +243,7 @@ async def monitor_status(self) -> None: last_fault = None fan_drive_state = None fan_frequency = None + drive_voltage = None while self.connected: try: @@ -320,11 +321,18 @@ async def monitor_status(self) -> None: # Send telemetry every TELEMETRY_INTERVAL times through the loop self.telemetry_count -= 1 new_fan_frequency = await self.controller.get_fan_frequency() - if self.telemetry_count < 0 or new_fan_frequency != fan_frequency: + new_drive_voltage = await self.controller.get_drive_voltage() + if ( + self.telemetry_count < 0 + or new_fan_frequency != fan_frequency + or new_drive_voltage != drive_voltage + ): fan_frequency = new_fan_frequency + drive_voltage = new_drive_voltage self.telemetry_count = self.TELEMETRY_INTERVAL telemetry = { "tel_extraction_fan": new_fan_frequency, + "tel_drive_voltage": new_drive_voltage, } await self.respond( json.dumps( diff --git a/python/lsst/ts/vent/controller/simulator_setup.json b/python/lsst/ts/vent/controller/simulator_setup.json index 70740f7..7f829b6 100644 --- a/python/lsst/ts/vent/controller/simulator_setup.json +++ b/python/lsst/ts/vent/controller/simulator_setup.json @@ -34,10 +34,11 @@ } }, "invalid": [], - "write": [ 3202, 7010, 7124, 8401, 8413, 8423, 8501, 8502, 8602, 64279], + "write": [ 3202, 3207, 7010, 7124, 8401, 8413, 8423, 8501, 8502, 8602, 64279], "bits": [], "uint16": [ { "addr": 3202, "value": 0, "action": "mirror_lfr" }, + { "addr": 3207, "value": 3829 }, { "addr": 7010, "value": 1 }, { "addr": 7124, "value": 0 }, { "addr": [ 7201, 7211 ], "value": 22 }, diff --git a/python/lsst/ts/vent/controller/vf_drive.py b/python/lsst/ts/vent/controller/vf_drive.py index 2ea8408..f949d66 100644 --- a/python/lsst/ts/vent/controller/vf_drive.py +++ b/python/lsst/ts/vent/controller/vf_drive.py @@ -24,6 +24,7 @@ class Registers(IntEnum): RFR_REGISTER = 3202 + ULN_REGISTER = 3207 SLL_REGISTER = 7010 RSF_REGISTER = 7124 FAULT_REGISTER = 7201 From 3e28f6b7b374c724deca47045ce701e00050cc09 Mon Sep 17 00:00:00 2001 From: Brian Brondel Date: Thu, 16 Jan 2025 19:38:31 -0300 Subject: [PATCH 7/7] Add a command get_fan_drive_max_frequency. --- python/lsst/ts/vent/controller/config.py | 2 +- python/lsst/ts/vent/controller/controller.py | 8 ++++++++ python/lsst/ts/vent/controller/dispatcher.py | 7 ++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/python/lsst/ts/vent/controller/config.py b/python/lsst/ts/vent/controller/config.py index 1e0a609..8c4d8e1 100644 --- a/python/lsst/ts/vent/controller/config.py +++ b/python/lsst/ts/vent/controller/config.py @@ -30,7 +30,7 @@ class Config: device_id = 1 """The default modbus device ID for the variable frequency drive.""" - max_freq = 50.0 + max_freq = 25.0 """Default maximum frequency for the dome fans.""" megaind_bus = 1 diff --git a/python/lsst/ts/vent/controller/controller.py b/python/lsst/ts/vent/controller/controller.py index c1d23d8..3f57428 100644 --- a/python/lsst/ts/vent/controller/controller.py +++ b/python/lsst/ts/vent/controller/controller.py @@ -200,6 +200,14 @@ async def get_fan_frequency(self) -> float: output_frequency *= 0.1 # RFR register holds frequency in units of 0.1 Hz return output_frequency + def get_max_frequency(self) -> float: + """Returns the maximum allowed frequency. + + Calls to `set_fan_frequency` may not have an argument exceeding + this value. + """ + return self.config.max_freq + async def set_fan_frequency(self, frequency: float) -> None: """Sets the target frequency for the dome exhaust fan. The frequency must be between zero and MAX_FREQ. diff --git a/python/lsst/ts/vent/controller/dispatcher.py b/python/lsst/ts/vent/controller/dispatcher.py index e9cbc60..be61c16 100644 --- a/python/lsst/ts/vent/controller/dispatcher.py +++ b/python/lsst/ts/vent/controller/dispatcher.py @@ -89,6 +89,7 @@ def __init__( self.dispatch_dict: Final[dict[str, list[type]]] = { "close_vent_gate": [int, int, int, int], "open_vent_gate": [int, int, int, int], + "get_fan_drive_max_frequency": [], "reset_extraction_fan_drive": [], "set_extraction_fan_drive_freq": [float], "set_extraction_fan_manual_control_mode": [bool], @@ -164,12 +165,13 @@ async def read_and_dispatch(self) -> None: # Convert the arguments to their expected type. args = [cast_string_to_type(t, arg) for t, arg in zip(types, args)] # Call the method with the specified arguments. - await getattr(self, command)(*args) + return_value = await getattr(self, command)(*args) # Send back a success response. await self.respond( json.dumps( dict( command=command, + return_value=return_value, error=0, exception_name="", message="", @@ -211,6 +213,9 @@ async def open_vent_gate( if gate != -1: raise ValueError(f"Invalid vent ({gate}) must be between 0 and 3.") + async def get_fan_drive_max_frequency(self) -> float: + return self.controller.get_max_frequency() + async def reset_extraction_fan_drive(self) -> None: await self.controller.vfd_fault_reset()