From b8ef3206123550bb6fe0a4e96e30205fea09cc6b Mon Sep 17 00:00:00 2001 From: Reed Riddle Date: Wed, 17 Dec 2025 17:38:07 -0800 Subject: [PATCH 1/8] atcfwheel with updated hispec daemon --- config/hsfei/hsfei.yaml | 61 ++++++++++ config/hsfei/hsfei_atcfwheel.yaml | 28 +++++ daemons/hsfei/atcfwheel | 190 ++++++++++++++++++++++++++++++ 3 files changed, 279 insertions(+) create mode 100644 config/hsfei/hsfei.yaml create mode 100644 config/hsfei/hsfei_atcfwheel.yaml create mode 100644 daemons/hsfei/atcfwheel diff --git a/config/hsfei/hsfei.yaml b/config/hsfei/hsfei.yaml new file mode 100644 index 0000000..2e2d2a0 --- /dev/null +++ b/config/hsfei/hsfei.yaml @@ -0,0 +1,61 @@ +# HSFEI Subsystem Configuration +group_id: hsfei +transport: rabbitmq +rabbitmq_url: amqp://hispec-rabbitmq +discovery_enabled: false + +daemons: + pickoff1: + hardware: + ip_address: 192.168.29.100 + tcp_port: 10001 + axis: "1" + named_positions: + home: 0.0 + science: 12.5 + calibration: 25.0 + engineering: 37.5 + + pickoff2: + hardware: + ip_address: 192.168.29.101 + tcp_port: 10001 + axis: "1" + named_positions: + home: 0.0 + flat: 10.0 + arc: 20.0 + dark: 30.0 + + pickoff3: + hardware: + ip_address: 192.168.29.102 + tcp_port: 10001 + axis: "1" + named_positions: + home: 0.0 + science: 12.5 + calibration: 25.0 + engineering: 37.5 + + pickoff4: + hardware: + ip_address: 192.168.29.103 + tcp_port: 10001 + axis: "1" + named_positions: + home: 0.0 + test_pos_1: 10.0 + + atcfwheel: + hardware: + host: 192.168.29.100 + port: 10010 + axis: "1" + named_positions: + open: 1 + nd_filter_1: 2 + nd_filter_2: 3 + nd_filter_3: 4 + nd_filter_4: 5 + nd_filter_5: 6 \ No newline at end of file diff --git a/config/hsfei/hsfei_atcfwheel.yaml b/config/hsfei/hsfei_atcfwheel.yaml new file mode 100644 index 0000000..1cc835f --- /dev/null +++ b/config/hsfei/hsfei_atcfwheel.yaml @@ -0,0 +1,28 @@ +# Single Daemon Configuration Example +# For standalone daemon deployment or development/testing +# +# Usage: +# daemon = HsfeiPickoffDaemon.from_config_file("config/pickoff_single.yaml") + +peer_id: atcfwheel +group_id: hsfei +transport: rabbitmq +rabbitmq_url: amqp://localhost +discovery_enabled: false + +hardware: + host: 192.168.29.100 + port: 10010 + axis: "1" + +named_positions: + open: 1 + nd_filter_1: 2 + nd_filter_2: 3 + nd_filter_3: 4 + nd_filter_4: 5 + nd_filter_5: 6 + +logging: + level: INFO + # file: /var/log/hispec/atcfwheel.log diff --git a/daemons/hsfei/atcfwheel b/daemons/hsfei/atcfwheel new file mode 100644 index 0000000..af13987 --- /dev/null +++ b/daemons/hsfei/atcfwheel @@ -0,0 +1,190 @@ +#!/usr/bin/python3.12 +'''Module for the ATC Filter Wheel Daemon''' +import argparse +import sys +from typing import Dict, Any + +from hispec.daemon import HispecDaemon #pylint: disable = E0401,E0611 +from hispec.util.thorlabs.fw102c import FilterWheelController #pylint: disable = E0401,E0611 +#from fw102c import FilterWheelController # Assuming fw102c.py is in the same directory + +class Atcfwheel(HispecDaemon): #pylint: disable = W0223 + '''Daemon for controlling the ATC Filter Wheel via Thorlabs FW102C controller''' + + # Defaults + peer_id = "atcfwheel" + group_id = "hsfei" + transport = "rabbitmq" + discovery_enabled = False + daemon_desc = "ATC FWheel" + + # pub/sub topics + topics = {} + + def __init__(self): + """Initialize the pickoff daemon. + + Args: come from the hsfei configuration file + """ + super().__init__() + + self.host = self.get_config("hardware.host", "192.168.29.100") + self.port = self.get_config("hardware.port", 10001) + self.device_key = None + self.dev = FilterWheelController() + self.daemon_desc = "ATC Filter Wheel Daemon" + + # Daemon state + self.state = { + 'connected': False, + 'error': '' + } + + # Call parent __init__ first + super().__init__() + + def get_named_positions(self): + """Get named positions from config (e.g., home, deployed, science).""" + return self._config.get("named_positions", {}) + + def get_named_position(self, name: str): + """Get a specific named position value, or None if not found.""" + return self.get_named_positions().get(name) + + def on_start(self, libby): + '''Starts up daemon and initializies the hardware device''' + self.logger.info("Starting %s Daemon", self.daemon_desc) + self.add_services({ + "connect": lambda p: self.connect(), + "disconnect": lambda p: self.disconnect(), + "initialize": lambda p: self.initialize(), + "status": lambda p: self.status(), + "position.get": lambda p: self.get_pos(), + "position.set": lambda p: self.set_pos(pos = p.get("position")), + "position.get_named": lambda p: self.get_named_position(), + "position.set_named": lambda p: self.goto_named_pos(name = p.get("named_pos")) + }) + # Initialize hardware connection + if self.host is None or self.port is None: + self.logger.error("No IP address or port specified for ATC Filter Wheel controller") + self.state['error'] = 'No IP address or port specified' + else: + try: + connection = self.connect() + if connection.get("Connect") != "Success": + raise ConnectionError(connection.get("Error")) + self.state['connected'] = True + self.logger.info("Daemon started successfully and connected to hardware") + self.initialize() + self.logger.info("Initialized %s", self.daemon_desc) + except ConnectionRefusedError as e: + self.logger.error("Failed to connect to hardware: %s", e) + self.logger.warning("Daemon will start but hardware is not available") + self.state['error'] = str(e) + self.state['connected'] = False + + # Publish initial status + libby.publish("atcfwheel.status", self.state) + + def on_stop(self, libby) -> None: #pylint: disable=W0222 + '''Stops the daemon and disconnects from hardware device''' + try: + self.disconnect() + self.logger.info("Disconnected %s", self.daemon_desc) + libby.publish("atcfwheel", {"Daemon Shutdown": "Success"}) + except Exception as e: # pylint: disable=W0718 + libby.publish("atcfwheel", {"Daemon Startup": "Failed", "Error":f"{e}"}) + self.logger.error("Disconnect %s:: Failed ", self.daemon_desc) + + + def connect(self): + """handles connection""" + try: + self.dev.connect(host = self.host, port = self.port) + if not self.dev.is_connected(): + raise ConnectionError("Failed to connect to device") + self.logger.info("Connected %s", self.daemon_desc) + except Exception as e: # pylint: disable=W0718 + self.logger.error("Failed to Connect to Hardware: %s",e) + return {"ok": False, "error": str(e)} + return {"ok": True, "message": "Connected to hardware"} + + def disconnect(self): + """handles disconnection""" + try: + self.dev.disconnect() + if self.dev.is_connected(): + raise ConnectionAbortedError("Failed to disconnect to device") + self.logger.info("Disconnected from %s", self.daemon_desc) + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"ok": False, "error": str(e)} + return {"ok": True, "message": "Disconnected from hardware"} + + def initialize(self): + """handles initialization""" + if not self.state['connected']: + return {"ok": False, "error": "Not connected to hardware"} + + # for PPC102_Coms, this involves setting the enabled status + try: + self.dev.initialize() + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"ok":False , "error": str(e)} + return {"ok": True} + + def status(self): + """handles status""" + try: + status = self.dev.get_status() + self.logger.debug("status: %s",status) + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"ok": False, "error": str(e)} + return {"ok": True, "status": status} + + def get_pos(self): + '''gets current position''' + if not self.state['connected']: + return {"ok": False, "error": "Not connected to hardware"} + + try: + position = self.dev.get_pos() + self.logger.debug("get_pos: %s",position) + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"ok": False, "error": str(e)} + return {"ok":True, "position": str(position)} + + def set_pos(self, pos): + '''sets current position''' + if not self.state['connected']: + return {"ok": False, "error": "Not connected to hardware"} + + try: + pos = int(pos) + self.dev.set_pos(pos) + self.logger.debug("set_pos: %d",pos) + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"ok": False, "error": str(e)} + return {"ok": True, "position": pos} + + def goto_named_pos(self, name): + '''moves to named position''' + if not self.state['connected']: + return {"ok": False, "error": "Not connected to hardware"} + + try: + goal = self.get_named_position(name.lower()) + if goal is not None: + self.dev.set_pos(int(goal)) + self.logger.debug("goto_named_pos: %s -> %s",name,goal) + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"ok": False, "error": str(e)} + return {"ok": True, "named_position": name, "position": goal} + +if __name__ == "__main__": + Atcfwheel().serve() From 7484c4cbf1dd501b3753360797d86698d8057eb0 Mon Sep 17 00:00:00 2001 From: Reed Riddle Date: Wed, 17 Dec 2025 18:22:32 -0800 Subject: [PATCH 2/8] Finished filter wheel daemon, untested -Elijah.ab --- daemons/hsfei/atcfwheel | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/daemons/hsfei/atcfwheel b/daemons/hsfei/atcfwheel index af13987..157d119 100644 --- a/daemons/hsfei/atcfwheel +++ b/daemons/hsfei/atcfwheel @@ -51,6 +51,14 @@ class Atcfwheel(HispecDaemon): #pylint: disable = W0223 """Get a specific named position value, or None if not found.""" return self.get_named_positions().get(name) + def cur_named_position(self): + """Get the name of the current position, if it matches a named position.""" + current_pos = self.get_pos().get("position") + for name, pos in self.get_named_positions().items(): + if str(pos) == str(current_pos): + return name + return None + def on_start(self, libby): '''Starts up daemon and initializies the hardware device''' self.logger.info("Starting %s Daemon", self.daemon_desc) @@ -61,7 +69,7 @@ class Atcfwheel(HispecDaemon): #pylint: disable = W0223 "status": lambda p: self.status(), "position.get": lambda p: self.get_pos(), "position.set": lambda p: self.set_pos(pos = p.get("position")), - "position.get_named": lambda p: self.get_named_position(), + "position.get_named": lambda p: self.cur_named_position(), "position.set_named": lambda p: self.goto_named_pos(name = p.get("named_pos")) }) # Initialize hardware connection From c1a9a767ea44fe3fed455e2a78744de478a45533 Mon Sep 17 00:00:00 2001 From: Reed Riddle Date: Thu, 18 Dec 2025 19:45:57 -0800 Subject: [PATCH 3/8] atcfwheel with updated hispec daemon --- daemons/hsfei/atcfwheel | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 daemons/hsfei/atcfwheel diff --git a/daemons/hsfei/atcfwheel b/daemons/hsfei/atcfwheel old mode 100644 new mode 100755 From a9352c4ea62ec86237dd8dabe9bf8fbdcbc3bf83 Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Mon, 5 Jan 2026 14:16:20 -0800 Subject: [PATCH 4/8] atcfwheel with updated hispec daemon --- config/hsfei/hsfei.yaml | 13 +++-- config/hsfei/hsfei_atcfwheel.yaml | 15 +++--- daemons/hsfei/atcfwheel | 90 +++++++++++++++++++++++++++---- 3 files changed, 92 insertions(+), 26 deletions(-) diff --git a/config/hsfei/hsfei.yaml b/config/hsfei/hsfei.yaml index 2e2d2a0..75efe23 100644 --- a/config/hsfei/hsfei.yaml +++ b/config/hsfei/hsfei.yaml @@ -51,11 +51,10 @@ daemons: hardware: host: 192.168.29.100 port: 10010 - axis: "1" named_positions: - open: 1 - nd_filter_1: 2 - nd_filter_2: 3 - nd_filter_3: 4 - nd_filter_4: 5 - nd_filter_5: 6 \ No newline at end of file + clear: 1 + nd1: 2 + nd2: 3 + nd3: 4 + nd4: 5 + nd5: 6 \ No newline at end of file diff --git a/config/hsfei/hsfei_atcfwheel.yaml b/config/hsfei/hsfei_atcfwheel.yaml index 1cc835f..62aa631 100644 --- a/config/hsfei/hsfei_atcfwheel.yaml +++ b/config/hsfei/hsfei_atcfwheel.yaml @@ -2,7 +2,7 @@ # For standalone daemon deployment or development/testing # # Usage: -# daemon = HsfeiPickoffDaemon.from_config_file("config/pickoff_single.yaml") +# daemon = Atcfwheel.from_config_file("config/hsfei_atcfwheel.yaml") peer_id: atcfwheel group_id: hsfei @@ -13,15 +13,14 @@ discovery_enabled: false hardware: host: 192.168.29.100 port: 10010 - axis: "1" named_positions: - open: 1 - nd_filter_1: 2 - nd_filter_2: 3 - nd_filter_3: 4 - nd_filter_4: 5 - nd_filter_5: 6 + clear: 1 + nd1: 2 + nd2: 3 + nd3: 4 + nd4: 5 + nd5: 6 logging: level: INFO diff --git a/daemons/hsfei/atcfwheel b/daemons/hsfei/atcfwheel index 157d119..63e164f 100755 --- a/daemons/hsfei/atcfwheel +++ b/daemons/hsfei/atcfwheel @@ -29,9 +29,9 @@ class Atcfwheel(HispecDaemon): #pylint: disable = W0223 super().__init__() self.host = self.get_config("hardware.host", "192.168.29.100") - self.port = self.get_config("hardware.port", 10001) + self.port = self.get_config("hardware.port", 10010) self.device_key = None - self.dev = FilterWheelController() + self.dev = FilterWheelController(log = True) self.daemon_desc = "ATC Filter Wheel Daemon" # Daemon state @@ -53,11 +53,11 @@ class Atcfwheel(HispecDaemon): #pylint: disable = W0223 def cur_named_position(self): """Get the name of the current position, if it matches a named position.""" - current_pos = self.get_pos().get("position") + current_pos = self.dev.get_pos() for name, pos in self.get_named_positions().items(): if str(pos) == str(current_pos): - return name - return None + return {"ok": True, "named_pos": name, "position": current_pos} + return {"ok": False, "error": "Current position does not match any named position", "position": current_pos} def on_start(self, libby): '''Starts up daemon and initializies the hardware device''' @@ -66,7 +66,7 @@ class Atcfwheel(HispecDaemon): #pylint: disable = W0223 "connect": lambda p: self.connect(), "disconnect": lambda p: self.disconnect(), "initialize": lambda p: self.initialize(), - "status": lambda p: self.status(), + "status.get": lambda p: self.status(), "position.get": lambda p: self.get_pos(), "position.set": lambda p: self.set_pos(pos = p.get("position")), "position.get_named": lambda p: self.cur_named_position(), @@ -79,7 +79,7 @@ class Atcfwheel(HispecDaemon): #pylint: disable = W0223 else: try: connection = self.connect() - if connection.get("Connect") != "Success": + if not connection.get("ok"): raise ConnectionError(connection.get("Error")) self.state['connected'] = True self.logger.info("Daemon started successfully and connected to hardware") @@ -145,7 +145,15 @@ class Atcfwheel(HispecDaemon): #pylint: disable = W0223 def status(self): """handles status""" try: - status = self.dev.get_status() + limits = self.dev.get_limits() + position = self.cur_named_position() + status = { + "connected": self.dev.is_connected(), + "position": position.get("position"), + "named_pos": position.get("named_pos"), + "min_limit": limits.get("1")[0], + "max_limit": limits.get("1")[1], + } self.logger.debug("status: %s",status) except Exception as e: # pylint: disable=W0718 self.logger.error("Error: %s",e) @@ -192,7 +200,67 @@ class Atcfwheel(HispecDaemon): #pylint: disable = W0223 except Exception as e: # pylint: disable=W0718 self.logger.error("Error: %s",e) return {"ok": False, "error": str(e)} - return {"ok": True, "named_position": name, "position": goal} + return {"ok": True, "named_pos": name, "position": goal} -if __name__ == "__main__": - Atcfwheel().serve() +def main(): + """Main entry point for the daemon.""" + parser = argparse.ArgumentParser( + description='HSFEI ATC Filter Wheel Daemon' + ) + parser.add_argument( + '-c', '--config', + type=str, + help='Path to config file (YAML or JSON)' + ) + parser.add_argument( + '-d', '--daemon-id', + type=str, + default='', + help='Daemon ID (required for subsystem configs with multiple daemons)' + ) + parser.add_argument( + '-H', '--host', + type=str, + default='192.168.29.100', + help='Host address of the ATC Filter Wheel' + ) + parser.add_argument( + '-p', '--port', + type=int, + default=10010, + help='Port for ATC Filter Wheel (default: 10010)' + ) + + args = parser.parse_args() + + # Create and run daemon + try: + if args.config: + # Load from config file + daemon = Atcfwheel.from_config_file( + args.config, + daemon_id=args.daemon_id, + ) + else: + # Use CLI args - build config dict and use from_config + config = { + "peer_id": "atcfwheel", + "group_id": "hsfei", + "transport": "rabbitmq", + "hardware": { + "host": args.host, + "port": args.port, + } + } + daemon = Atcfwheel.from_config(config) + daemon.serve() + except KeyboardInterrupt: + print("\nDaemon interrupted by user") + sys.exit(0) + except Exception as e: # pylint: disable=W0718 + print(f"Error running daemon: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == '__main__': + main() From ee8bd247e6bfca539355e06dd0a8ad4cc31c04d4 Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Mon, 5 Jan 2026 15:19:48 -0800 Subject: [PATCH 5/8] atcfwheel with updated hispec daemon- removed pylint errors --- daemons/hsfei/atcfwheel | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemons/hsfei/atcfwheel b/daemons/hsfei/atcfwheel index 63e164f..0d2532a 100755 --- a/daemons/hsfei/atcfwheel +++ b/daemons/hsfei/atcfwheel @@ -2,7 +2,7 @@ '''Module for the ATC Filter Wheel Daemon''' import argparse import sys -from typing import Dict, Any +from typing import Dict, Any #pylint: disable = W0611 from hispec.daemon import HispecDaemon #pylint: disable = E0401,E0611 from hispec.util.thorlabs.fw102c import FilterWheelController #pylint: disable = E0401,E0611 @@ -57,7 +57,7 @@ class Atcfwheel(HispecDaemon): #pylint: disable = W0223 for name, pos in self.get_named_positions().items(): if str(pos) == str(current_pos): return {"ok": True, "named_pos": name, "position": current_pos} - return {"ok": False, "error": "Current position does not match any named position", "position": current_pos} + return {"ok": False, "error": "Current position does not match any named position", "position": current_pos} #pylint: disable = C0301 def on_start(self, libby): '''Starts up daemon and initializies the hardware device''' From eb55df90631a3e954d39d20930e11d458c1b0c12 Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Mon, 5 Jan 2026 16:45:03 -0800 Subject: [PATCH 6/8] Some redundant calls and whrong class string name --- daemons/hsfei/atcfwheel | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/daemons/hsfei/atcfwheel b/daemons/hsfei/atcfwheel index 0d2532a..34fccc5 100755 --- a/daemons/hsfei/atcfwheel +++ b/daemons/hsfei/atcfwheel @@ -22,7 +22,7 @@ class Atcfwheel(HispecDaemon): #pylint: disable = W0223 topics = {} def __init__(self): - """Initialize the pickoff daemon. + """Initialize the ATC Filter Wheel daemon. Args: come from the hsfei configuration file """ @@ -40,9 +40,6 @@ class Atcfwheel(HispecDaemon): #pylint: disable = W0223 'error': '' } - # Call parent __init__ first - super().__init__() - def get_named_positions(self): """Get named positions from config (e.g., home, deployed, science).""" return self._config.get("named_positions", {}) From 65981983727f570b736d1ab318bcb6d07faa1282 Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Mon, 5 Jan 2026 16:47:03 -0800 Subject: [PATCH 7/8] Another redundant variable --- daemons/hsfei/atcfwheel | 1 - 1 file changed, 1 deletion(-) diff --git a/daemons/hsfei/atcfwheel b/daemons/hsfei/atcfwheel index 34fccc5..6c61582 100755 --- a/daemons/hsfei/atcfwheel +++ b/daemons/hsfei/atcfwheel @@ -32,7 +32,6 @@ class Atcfwheel(HispecDaemon): #pylint: disable = W0223 self.port = self.get_config("hardware.port", 10010) self.device_key = None self.dev = FilterWheelController(log = True) - self.daemon_desc = "ATC Filter Wheel Daemon" # Daemon state self.state = { From 276300f939bbbee4a7918d8fdf6ddada9261ff3a Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Tue, 6 Jan 2026 11:56:21 -0800 Subject: [PATCH 8/8] Daemon tested and Ready -elijah.ab --- daemons/hsfei/atcfwheel | 1 - 1 file changed, 1 deletion(-) diff --git a/daemons/hsfei/atcfwheel b/daemons/hsfei/atcfwheel index 6c61582..12c53c9 100755 --- a/daemons/hsfei/atcfwheel +++ b/daemons/hsfei/atcfwheel @@ -130,7 +130,6 @@ class Atcfwheel(HispecDaemon): #pylint: disable = W0223 if not self.state['connected']: return {"ok": False, "error": "Not connected to hardware"} - # for PPC102_Coms, this involves setting the enabled status try: self.dev.initialize() except Exception as e: # pylint: disable=W0718