From d062dcf5749f2b0bc695a880f61c256c3967cc60 Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Sat, 29 Nov 2025 17:13:06 -0800 Subject: [PATCH 1/5] Revised iteration of PIAA gimbal mount daemon after baseclass and libby developement --- daemons/hsfei/hsfei.config | 25 ++++ daemons/hsfei/yjpiaagim | 231 +++++++++++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 daemons/hsfei/hsfei.config create mode 100644 daemons/hsfei/yjpiaagim diff --git a/daemons/hsfei/hsfei.config b/daemons/hsfei/hsfei.config new file mode 100644 index 0000000..1bffa79 --- /dev/null +++ b/daemons/hsfei/hsfei.config @@ -0,0 +1,25 @@ +# Config file for daemon references across FEI daemons + +[Device Control] +atcpress_host = feiinficon +atcpress_port = 8000 + +[atcfwheel] +host = 192.168.29.100 +port = 10010 + +[atcfwheel-named_pos] +clear = 1 +nd2 = 2 +nd3 = 3 +nd4 = 4 +nd5 = 5 +nd6 = 6 + +[yjpiaagim] +host = 192.168.29.100 +port = 10013 + +[yjpiaagim-named_pos] +center = 0.0, 0.0 +top_left = -5.0, 5.0 \ No newline at end of file diff --git a/daemons/hsfei/yjpiaagim b/daemons/hsfei/yjpiaagim new file mode 100644 index 0000000..f58dd59 --- /dev/null +++ b/daemons/hsfei/yjpiaagim @@ -0,0 +1,231 @@ +#!/usr/bin/python3.12 +'''Module for the BLUE YJ PIAA Gimbal Daemon''' +import logging +import configparser +from libby.daemon import LibbyDaemon # pyright: ignore[reportMissingImports] +from hispec.util.thorlabs.ppc102 import Ppc102Controller #pylint: disable = E0401,E0611 +#from ppc102 import Ppc102Controller # Assuming ppc102.py is in the same directory + +class Yjpiaagim(LibbyDaemon): #pylint: disable = W0223 + '''Daemon for controlling the ATC Filter Wheel via Thorlabs FW102C controller''' + peer_id = "yjpiaagim" + transport = "rabbitmq" + discovery_enabled = False + discovery_interval_s = 5.0 + rabbitmq_url = "amqp://localhost" # RabbitMQ on hispec + daemon_desc = "YJ Piaa Gimbal" + named_positions = {} + + # pub/sub topics + topics = {} + + def __init__(self): + """Initialize the pickoff daemon. + + Args: come from the hsfei configuration file + """ + #on start set up the daemon from config and initialize device + config = configparser.ConfigParser() + config.read('hsfei.config') + self.host = config["yjpiaagim"]["host"] + self.port = int(config["yjpiaagim"]["port"]) + + self.dev = Ppc102Controller() + + # Set up logging (temporary UNTIL LIBBY LOGGING IS IMPLEMENTED) + self.logger = logging.getLogger(__name__) + self.logger.setLevel(logging.DEBUG) + ch = logging.StreamHandler() + ch.setLevel(logging.DEBUG) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + ch.setFormatter(formatter) + self.logger.addHandler(ch) + + + # Daemon state + self.state = { + 'connected': False, + 'error': '' + } + + # Call parent __init__ first + super().__init__() + + 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(), + "is_loops_closed": lambda p: self.is_loops_closed(), + "close_loops": lambda p: self.close_loops(), + "open_loops": lambda p: self.open_loops(), + "position.get": lambda p: self.get_pos(), + "position.set": lambda p: self.set_pos(axis = p.get("axis"), + 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")) + }) + try: + self.connect() + self.logger.info("Connected to %s", self.daemon_desc) + self.initialize() + self.logger.info("Initialized %s", self.daemon_desc) + self.load_named_pos() + #publish to libby + libby.publish("yjpiaagim", {"Daemon Startup": "Success"}) + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error Connecting and Initializing %s: %s", self.daemon_desc, e) + #publish failure + libby.publish("yjpiaagim", {"Daemon Startup": "Failed", "Connection Error": f"{e}"}) + + 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("yjpiaagim", {"Daemon Shutdown": "Success"}) + except Exception as e: # pylint: disable=W0718 + libby.publish("yjpiaagim", {"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) + self.logger.info("Connected %s", self.daemon_desc) + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"Connect": "Failed", "Error": f"{e}"} + return {"Connect": "Success"} + + def disconnect(self): + """handles disconnection""" + try: + self.dev.disconnect() + self.logger.info("Disconnected from %s", self.daemon_desc) + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"Disconnect": "Failed", "Error": f"{e}"} + return {"Disconnect": "Success"} + + def initialize(self): + """handles initialization""" + # for PPC102_Coms, this involves setting the enable + try: + self.dev.set_enable(channel = 0, enable = 1) + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"Initialize": "Failed", "Error": f"{e}"} + return {"Initialize": "Success"} + + def load_named_pos(self): + """loads named positions into the device from config file""" + config = configparser.ConfigParser() + config.read('hsfei.config') + try: + for name, pos in config["yjpiaagim-named_pos"].items(): + self.named_positions[name] = tuple(map(float, pos.split(","))) + self.logger.info("Loaded named positions for %s", self.daemon_desc) + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"Load Named Positions": "Failed", "Error": f"{e}"} + return {"Load Named Positions": "Success"} + + def status(self): #TODO: Check to make sure this is correct for PPC102 + """handles status""" + try: + status = self.dev.get_status_update() + self.logger.debug("status: %s",status) + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"status": "Failed", "Error": f"{e}"} + return {"status": status} + + def is_loops_closed(self): + '''checks if loops are closed''' + try: + closed = self.dev.is_loop_closed() + self.logger.debug("is_loops_closed: %s",closed) + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"is_loops_closed": "Failed", "Error": f"{e}"} + return {"loops_closed": str(closed)} + + def close_loops(self): + '''closes control loops''' + try: + self.dev.set_loop(channel=0, loop=2) + self.logger.debug("close_loops called") + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"close_loops": "Failed", "Error": f"{e}"} + return {"close_loops": "Success"} + + def open_loops(self): + '''opens control loops''' + try: + self.dev.set_loop(channel=0, loop=1) + self.logger.debug("open_loops called") + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"open_loops": "Failed", "Error": f"{e}"} + return {"open_loops": "Success"} + + def get_pos(self): + '''gets current position''' + try: + xpos = self.dev.get_pos(channel=1) + ypos = self.dev.get_pos(channel=2) + position = (xpos, ypos) + self.logger.debug("get_pos: %s",position) + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"get_pos": "Failed", "Error": f"{e}"} + return {"position": str(position)} + + def set_pos(self, axis, pos): + '''sets current position''' + try: + pos = float(pos) + axis = int(axis) + if axis not in [0,1]: + self.logger.error("Axis must be 0 (X) or 1 (Y)") + return {"Move": "Failed", "Error": "Axis must be 0 (X) or 1 (Y)"} + chan = axis + 1 + self.dev.set_pos(channel=chan, pos=pos) + self.logger.debug("set_pos: %d",pos) + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"Move": "Failed", "Error": f"{e}"} + return {"Move": "Success"} + + def goto_named_pos(self, name): + '''moves to named position''' + try: + goal = self.named_positions.get(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 {"Move to Named Position": "Failed", "Error": f"{e}"} + return {"move": "Success"} + + def get_named_position(self): + '''returns current named position''' + current = ( + round(self.dev.get_pos(channel=1), 3), + round(self.dev.get_pos(channel=2), 3), + ) + for name, pos in self.named_positions.items(): + if pos == current: + return {"named_pos": name} + return {"named_pos": "Unknown"} + +if __name__ == "__main__": + Yjpiaagim().serve() + From dab843f7f3bb9e82672cc975045152f47e01dc19 Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Mon, 5 Jan 2026 17:04:58 -0800 Subject: [PATCH 2/5] Stopped at is_loops_closed, need to finish converting the rest to fit HispecDaemon --- daemons/hsfei/atcfwheel | 262 ++++++++++++++++++++++++++++++++++++++++ daemons/hsfei/yjpiaagim | 100 +++++++++------ 2 files changed, 323 insertions(+), 39 deletions(-) create mode 100644 daemons/hsfei/atcfwheel diff --git a/daemons/hsfei/atcfwheel b/daemons/hsfei/atcfwheel new file mode 100644 index 0000000..6c61582 --- /dev/null +++ b/daemons/hsfei/atcfwheel @@ -0,0 +1,262 @@ +#!/usr/bin/python3.12 +'''Module for the ATC Filter Wheel Daemon''' +import argparse +import sys +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 +#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 ATC Filter Wheel 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", 10010) + self.device_key = None + self.dev = FilterWheelController(log = True) + + # Daemon state + self.state = { + 'connected': False, + 'error': '' + } + + 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 cur_named_position(self): + """Get the name of the current position, if it matches a named position.""" + current_pos = self.dev.get_pos() + 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} #pylint: disable = C0301 + + 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.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(), + "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 not connection.get("ok"): + 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: + 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) + 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_pos": name, "position": goal} + +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() diff --git a/daemons/hsfei/yjpiaagim b/daemons/hsfei/yjpiaagim index e99d011..5ae9e12 100644 --- a/daemons/hsfei/yjpiaagim +++ b/daemons/hsfei/yjpiaagim @@ -34,7 +34,6 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 self.host = self.get_config("hardware.host", "192.168.29.100") self.port = self.get_config("hardware.port", 10010) self.device_key = None - self.dev = Ppc102Controller(log = True) # Daemon state @@ -42,6 +41,23 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 'connected': False, 'error': '' } + + 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 cur_named_position(self): + """Get the name of the current position, if it matches a named position.""" + current_pos = self.dev.get_pos() + 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} #pylint: disable = C0301 + def on_start(self, libby): '''Starts up daemon and initializies the hardware device''' @@ -52,26 +68,32 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 "initialize": lambda p: self.initialize(), "status": lambda p: self.status(), "is_loops_closed": lambda p: self.is_loops_closed(), - "close_loops": lambda p: self.close_loops(), - "open_loops": lambda p: self.open_loops(), + "loops.close": lambda p: self.close_loops(), + "loops.open": lambda p: self.open_loops(), "position.get": lambda p: self.get_pos(), "position.set": lambda p: self.set_pos(axis = p.get("axis"), 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")) }) - try: - self.connect() - self.logger.info("Connected to %s", self.daemon_desc) - self.initialize() - self.logger.info("Initialized %s", self.daemon_desc) - self.load_named_pos() - #publish to libby - libby.publish("yjpiaagim", {"Daemon Startup": "Success"}) - except Exception as e: # pylint: disable=W0718 - self.logger.error("Error Connecting and Initializing %s: %s", self.daemon_desc, e) - #publish failure - libby.publish("yjpiaagim", {"Daemon Startup": "Failed", "Connection Error": f"{e}"}) + # Initialize hardware connection + if self.host is None or self.port is None: + self.logger.error("No IP address or port specified for Blue Gimbal Mount controller") + self.state['error'] = 'No IP address or port specified' + else: + try: + connection = self.connect() + if not connection.get("ok"): + 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 def on_stop(self, libby) -> None: #pylint: disable=W0222 '''Stops the daemon and disconnects from hardware device''' @@ -88,64 +110,64 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 """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("Error: %s",e) - return {"Connect": "Failed", "Error": f"{e}"} - return {"Connect": "Success"} + 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 {"Disconnect": "Failed", "Error": f"{e}"} - return {"Disconnect": "Success"} + return {"ok": False, "error": str(e)} + return {"ok": True, "message": "Disconnected from hardware"} def initialize(self): """handles initialization""" # for PPC102_Coms, this involves setting the enable + if not self.state['connected']: + return {"ok": False, "error": "Not connected to hardware"} + try: self.dev.set_enable(channel = 0, enable = 1) except Exception as e: # pylint: disable=W0718 self.logger.error("Error: %s",e) - return {"Initialize": "Failed", "Error": f"{e}"} - return {"Initialize": "Success"} - - def load_named_pos(self): - """loads named positions into the device from config file""" - config = configparser.ConfigParser() - config.read('hsfei.config') - try: - for name, pos in config["yjpiaagim-named_pos"].items(): - self.named_positions[name] = tuple(map(float, pos.split(","))) - self.logger.info("Loaded named positions for %s", self.daemon_desc) - except Exception as e: # pylint: disable=W0718 - self.logger.error("Error: %s",e) - return {"Load Named Positions": "Failed", "Error": f"{e}"} - return {"Load Named Positions": "Success"} + return {"ok":False , "error": str(e)} + return {"ok": True} def status(self): #TODO: Check to make sure this is correct for PPC102 """handles status""" + if not self.state['connected']: + return {"ok": False, "error": "Not connected to hardware"} + try: status = self.dev.get_status_update() self.logger.debug("status: %s",status) except Exception as e: # pylint: disable=W0718 self.logger.error("Error: %s",e) - return {"status": "Failed", "Error": f"{e}"} - return {"status": status} + return {"ok": False, "error": str(e)} + return {"ok": True, "status": status} def is_loops_closed(self): '''checks if loops are closed''' + if not self.state['connected']: + return {"ok": False, "error": "Not connected to hardware"} + try: closed = self.dev.is_loop_closed() self.logger.debug("is_loops_closed: %s",closed) except Exception as e: # pylint: disable=W0718 self.logger.error("Error: %s",e) - return {"is_loops_closed": "Failed", "Error": f"{e}"} - return {"loops_closed": str(closed)} + return {"ok": False, "error": str(e)} + return {"ok":True, "loops_closed": closed} def close_loops(self): '''closes control loops''' From e04fd8ec6d862fa6c0a8269eaec99fb20e74861e Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Tue, 6 Jan 2026 11:43:59 -0800 Subject: [PATCH 3/5] Completed Gimbal mount refactor for HispecDaemon, ready for testing --- daemons/hsfei/yjpiaagim | 185 ++++++++++++++++++++++++++++++---------- 1 file changed, 138 insertions(+), 47 deletions(-) diff --git a/daemons/hsfei/yjpiaagim b/daemons/hsfei/yjpiaagim index 5ae9e12..6766cee 100644 --- a/daemons/hsfei/yjpiaagim +++ b/daemons/hsfei/yjpiaagim @@ -39,9 +39,11 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 # Daemon state self.state = { 'connected': False, - 'error': '' + 'error': '', + 'enabled': False, + 'loops_closed': False } - + def get_named_positions(self): """Get named positions from config (e.g., home, deployed, science).""" return self._config.get("named_positions", {}) @@ -52,9 +54,9 @@ class Yjpiaagim(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.dev.get_pos() + current_pos = self.dev.get_pos(channel=1), self.dev.get_pos(channel=2) for name, pos in self.get_named_positions().items(): - if str(pos) == str(current_pos): + if abs(float(pos) - float(current_pos)) <= 0.001: return {"ok": True, "named_pos": name, "position": current_pos} return {"ok": False, "error": "Current position does not match any named position", "position": current_pos} #pylint: disable = C0301 @@ -73,7 +75,7 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 "position.get": lambda p: self.get_pos(), "position.set": lambda p: self.set_pos(axis = p.get("axis"), 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 @@ -84,7 +86,7 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 try: connection = self.connect() if not connection.get("ok"): - raise ConnectionError(connection.get("Error")) + raise ConnectionError(connection.get("error")) self.state['connected'] = True self.logger.info("Daemon started successfully and connected to hardware") self.initialize() @@ -102,7 +104,7 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 self.logger.info("Disconnected %s", self.daemon_desc) libby.publish("yjpiaagim", {"Daemon Shutdown": "Success"}) except Exception as e: # pylint: disable=W0718 - libby.publish("yjpiaagim", {"Daemon Startup": "Failed", "Error":f"{e}"}) + libby.publish("yjpiaagim", {"Daemon Startup": "Failed", "error":f"{e}"}) self.logger.error("Disconnect %s:: Failed ", self.daemon_desc) @@ -126,33 +128,48 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 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) + self.logger.error("error: %s",e) return {"ok": False, "error": str(e)} - return {"ok": True, "message": "Disconnected from hardware"} + return {"ok": True, "message": "Disconnected from hardware"} def initialize(self): """handles initialization""" # for PPC102_Coms, this involves setting the enable if not self.state['connected']: return {"ok": False, "error": "Not connected to hardware"} - + try: self.dev.set_enable(channel = 0, enable = 1) + self.state['enabled'] = True + self.logger.debug("Initialized %s", self.daemon_desc) except Exception as e: # pylint: disable=W0718 - self.logger.error("Error: %s",e) + self.logger.error("error: %s",e) return {"ok":False , "error": str(e)} return {"ok": True} - def status(self): #TODO: Check to make sure this is correct for PPC102 + def status(self): """handles status""" if not self.state['connected']: return {"ok": False, "error": "Not connected to hardware"} try: - status = self.dev.get_status_update() + res_x = self.dev.get_status_update(channel = 1) + res_y = self.dev.get_status_update(channel = 2) + enabled = self.dev.get_enable(channel = 0) + status = { + "is_connected": self.dev.is_connected(), + "position_x": res_x[1], + "position_y": res_y[1], + "voltage_x": res_x[0], + "voltage_y": res_y[0], + "flag_x": res_x[2], + "flag_y": res_y[2], + "enabled": enabled, + "loops_closed": self.dev.is_loop_closed() + } self.logger.debug("status: %s",status) except Exception as e: # pylint: disable=W0718 - self.logger.error("Error: %s",e) + self.logger.error("error: %s",e) return {"ok": False, "error": str(e)} return {"ok": True, "status": status} @@ -165,81 +182,155 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 closed = self.dev.is_loop_closed() self.logger.debug("is_loops_closed: %s",closed) except Exception as e: # pylint: disable=W0718 - self.logger.error("Error: %s",e) + self.logger.error("error: %s",e) return {"ok": False, "error": str(e)} return {"ok":True, "loops_closed": closed} - + def close_loops(self): '''closes control loops''' try: self.dev.set_loop(channel=0, loop=2) self.logger.debug("close_loops called") + closed = self.dev.is_loop_closed() + if not closed: + raise RuntimeError("Failed to close loops") + self.state['loops_closed'] = True except Exception as e: # pylint: disable=W0718 - self.logger.error("Error: %s",e) - return {"close_loops": "Failed", "Error": f"{e}"} - return {"close_loops": "Success"} + self.logger.error("error: %s",e) + return {"ok": False, "error": str(e)} + return {"ok":True, "loops_closed": closed} def open_loops(self): '''opens control loops''' try: self.dev.set_loop(channel=0, loop=1) self.logger.debug("open_loops called") + closed = self.dev.is_loop_closed() + if closed: + raise RuntimeError("Failed to open loops") + self.state['loops_closed'] = False except Exception as e: # pylint: disable=W0718 - self.logger.error("Error: %s",e) - return {"open_loops": "Failed", "Error": f"{e}"} - return {"open_loops": "Success"} + self.logger.error("error: %s",e) + return {"ok": False, "error": str(e)} + return {"ok":True, "loops_closed": closed} def get_pos(self): '''gets current position''' + if not self.state['connected']: + return {"ok": False, "error": "Not connected to hardware"} + if not self.state['enabled']: + return {"ok": False, "error": "Device not enabled"} + if not self.state['loops_closed']: + return {"ok": False, "error": "Control loops are not closed"} try: xpos = self.dev.get_pos(channel=1) ypos = self.dev.get_pos(channel=2) position = (xpos, ypos) self.logger.debug("get_pos: %s",position) except Exception as e: # pylint: disable=W0718 - self.logger.error("Error: %s",e) - return {"get_pos": "Failed", "Error": f"{e}"} - return {"position": str(position)} + self.logger.error("error: %s",e) + return {"ok": False, "error": str(e)} + return {"ok":True, "position": str(position)} def set_pos(self, axis, pos): '''sets current position''' + if not self.state['connected']: + return {"ok": False, "error": "Not connected to hardware"} + if not self.state['enabled']: + return {"ok": False, "error": "Device not enabled"} + if not self.state['loops_closed']: + return {"ok": False, "error": "Control loops are not closed"} try: pos = float(pos) axis = int(axis) if axis not in [0,1]: self.logger.error("Axis must be 0 (X) or 1 (Y)") - return {"Move": "Failed", "Error": "Axis must be 0 (X) or 1 (Y)"} + return {"ok": False, "error": "Axis must be 0 (X) or 1 (Y)"} chan = axis + 1 self.dev.set_pos(channel=chan, pos=pos) self.logger.debug("set_pos: %d",pos) + position = self.dev.get_pos(channel=chan) except Exception as e: # pylint: disable=W0718 - self.logger.error("Error: %s",e) - return {"Move": "Failed", "Error": f"{e}"} - return {"Move": "Success"} + self.logger.error("error: %s",e) + return {"ok": False, "error": str(e)} + return {"ok":True, "position": str(position)} 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.named_positions.get(name.lower()) + goal = self.get_named_position(name.lower()) if goal is not None: - self.dev.set_pos(int(goal)) + self.dev.set_pos(channel=1, pos=float(goal[0])) + self.dev.set_pos(channel=2, pos=float(goal[1])) self.logger.debug("goto_named_pos: %s -> %s",name,goal) + cur_pos = (self.dev.get_pos(channel=1), self.dev.get_pos(channel=2)) except Exception as e: # pylint: disable=W0718 self.logger.error("Error: %s",e) - return {"Move to Named Position": "Failed", "Error": f"{e}"} - return {"move": "Success"} - - def get_named_position(self): - '''returns current named position''' - current = ( - round(self.dev.get_pos(channel=1), 3), - round(self.dev.get_pos(channel=2), 3), - ) - for name, pos in self.named_positions.items(): - if pos == current: - return {"named_pos": name} - return {"named_pos": "Unknown"} - -if __name__ == "__main__": - Yjpiaagim().serve() + return {"ok": False, "error": str(e)} + return {"ok": True, "named_pos": name, "position": cur_pos} + +def main(): + """Main entry point for the daemon.""" + parser = argparse.ArgumentParser( + description='HSFEI Blue Gimbal Mount 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=10013, + help='Port for Blue Gimbal Mount (default: 10013)' + ) + + args = parser.parse_args() + + # Create and run daemon + try: + if args.config: + # Load from config file + daemon = Yjpiaagim.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 = Yjpiaagim.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 616cc2f531809d813122a30036f30937320a498b Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Wed, 7 Jan 2026 15:05:11 -0800 Subject: [PATCH 4/5] Gimbal mount changes. -Needs to be tested --- daemons/hsfei/yjpiaagim | 44 ++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/daemons/hsfei/yjpiaagim b/daemons/hsfei/yjpiaagim index 6766cee..5acd589 100644 --- a/daemons/hsfei/yjpiaagim +++ b/daemons/hsfei/yjpiaagim @@ -9,7 +9,7 @@ from hispec.util.thorlabs.ppc102 import Ppc102Controller #pylint: disable = E040 #from ppc102 import Ppc102Controller # Assuming ppc102.py is in the same directory class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 - '''Daemon for controlling the ATC Filter Wheel via Thorlabs FW102C controller''' + '''Daemon for controlling the Blue Piaa Gimbal Mount via Thorlabs PPC102 controller''' # Defaults peer_id = "yjpiaagim" @@ -54,11 +54,20 @@ class Yjpiaagim(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.dev.get_pos(channel=1), self.dev.get_pos(channel=2) - for name, pos in self.get_named_positions().items(): - if abs(float(pos) - float(current_pos)) <= 0.001: - return {"ok": True, "named_pos": name, "position": current_pos} - return {"ok": False, "error": "Current position does not match any named position", "position": current_pos} #pylint: disable = C0301 + if not self.state['connected']: + return {"ok": False, "error": "Not connected to hardware"} + + try: + current_pos = self.dev.get_pos(channel=1), self.dev.get_pos(channel=2) + for name, pos in self.get_named_positions().items(): + dx = abs(float(pos[0]) - float(current_pos[0])) + dy = abs(float(pos[1]) - float(current_pos[1])) + if dx <= 0.001 and dy <= 0.001: + return {"ok": True, "named_pos": name, "position": current_pos} + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error: %s",e) + return {"ok": False, "error": str(e)} + return {"ok": True, "named_pos": "unknown", "position": current_pos} #pylint: disable = C0301 def on_start(self, libby): @@ -76,7 +85,8 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 "position.set": lambda p: self.set_pos(axis = p.get("axis"), pos = p.get("position")), "position.get_named": lambda p: self.cur_named_position(), - "position.set_named": lambda p: self.goto_named_pos(name = p.get("named_pos")) + "position.set_named": lambda p: self.goto_named_pos(name = p.get("named_pos")), + "cleanup": lambda p: self.clean_up_gimbal() }) # Initialize hardware connection if self.host is None or self.port is None: @@ -107,6 +117,18 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 libby.publish("yjpiaagim", {"Daemon Startup": "Failed", "error":f"{e}"}) self.logger.error("Disconnect %s:: Failed ", self.daemon_desc) + def clean_up_gimbal(self): + '''Cleans up gimbal settings''' + try: + self.dev.set_loop(channel=0, loop=1) # Open loops + self.dev.set_enable(channel=0, enable=1) # Device can stay enabled + self.dev.set_output_volts(channel=1, volts=0) # Set output voltages to 0 + self.dev.set_output_volts(channel=2, volts=0) + self.logger.info("Cleaned up %s", self.daemon_desc) + except Exception as e: # pylint: disable=W0718 + self.logger.error("error during cleanup: %s",e) + return {"ok": False, "error": str(e)} + return {"ok": True, "message": "Gimbal cleaned up"} def connect(self): """handles connection""" @@ -263,8 +285,8 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 try: goal = self.get_named_position(name.lower()) if goal is not None: - self.dev.set_pos(channel=1, pos=float(goal[0])) - self.dev.set_pos(channel=2, pos=float(goal[1])) + self.set_pos(axis=0, pos=float(goal[0])) + self.set_pos(axis=1, pos=float(goal[1])) self.logger.debug("goto_named_pos: %s -> %s",name,goal) cur_pos = (self.dev.get_pos(channel=1), self.dev.get_pos(channel=2)) except Exception as e: # pylint: disable=W0718 @@ -292,7 +314,7 @@ def main(): '-H', '--host', type=str, default='192.168.29.100', - help='Host address of the ATC Filter Wheel' + help='Host address of the Blue Piaa Gimbal Mount controller (default:192.168.29.100)' ) parser.add_argument( '-p', '--port', @@ -314,7 +336,7 @@ def main(): else: # Use CLI args - build config dict and use from_config config = { - "peer_id": "atcfwheel", + "peer_id": "yjpiaagim", "group_id": "hsfei", "transport": "rabbitmq", "hardware": { From 354c3b26f3c4cd242fa1bf8d346fa4f713db54b4 Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Tue, 13 Jan 2026 00:40:09 -0800 Subject: [PATCH 5/5] Hardware tested and working PIAA gimbal mount daemon --- config/hsfei/hsfei.yaml | 69 +++++++++++++++++++++++++++++++ config/hsfei/hsfei_yjpiaagim.yaml | 17 ++++++++ daemons/hsfei/yjpiaagim | 29 +++++++++---- 3 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 config/hsfei/hsfei.yaml create mode 100644 config/hsfei/hsfei_yjpiaagim.yaml mode change 100644 => 100755 daemons/hsfei/yjpiaagim diff --git a/config/hsfei/hsfei.yaml b/config/hsfei/hsfei.yaml new file mode 100644 index 0000000..dd3309f --- /dev/null +++ b/config/hsfei/hsfei.yaml @@ -0,0 +1,69 @@ +# 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 + named_positions: + clear: 1 + nd1: 2 + nd2: 3 + nd3: 4 + nd4: 5 + nd5: 6 + + yjpiaagim: + hardware: + host: 192.168.29.100 + port: 10013 + named_positions: + center: [0.0, 0.0] + offset1: [1.0, 1.0] + offset2: [-1.0, -1.0] \ No newline at end of file diff --git a/config/hsfei/hsfei_yjpiaagim.yaml b/config/hsfei/hsfei_yjpiaagim.yaml new file mode 100644 index 0000000..3e70304 --- /dev/null +++ b/config/hsfei/hsfei_yjpiaagim.yaml @@ -0,0 +1,17 @@ +peer_id: yjpiaagim +group_id: hsfei +transport: rabbitmq +rabbitmq_url: amqp://localhost +discovery_enabled: false + +hardware: + host: 192.168.29.100 + port: 10013 + +named_positions: + center: [0.0, 0.0] + offset1: [1.0, 1.0] + offset2: [-1.0, -1.0] + +logging: + level: INFO \ No newline at end of file diff --git a/daemons/hsfei/yjpiaagim b/daemons/hsfei/yjpiaagim old mode 100644 new mode 100755 index 5acd589..5b2f8ed --- a/daemons/hsfei/yjpiaagim +++ b/daemons/hsfei/yjpiaagim @@ -32,7 +32,7 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 #load configuration from hsfei.yaml self.host = self.get_config("hardware.host", "192.168.29.100") - self.port = self.get_config("hardware.port", 10010) + self.port = self.get_config("hardware.port", 10013) self.device_key = None self.dev = Ppc102Controller(log = True) @@ -58,11 +58,15 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 return {"ok": False, "error": "Not connected to hardware"} try: - current_pos = self.dev.get_pos(channel=1), self.dev.get_pos(channel=2) + current_pos = ( + float(self.dev.get_pos(channel=1)), + float(self.dev.get_pos(channel=2)) + ) + for name, pos in self.get_named_positions().items(): dx = abs(float(pos[0]) - float(current_pos[0])) dy = abs(float(pos[1]) - float(current_pos[1])) - if dx <= 0.001 and dy <= 0.001: + if dx <= 0.01 and dy <= 0.01: return {"ok": True, "named_pos": name, "position": current_pos} except Exception as e: # pylint: disable=W0718 self.logger.error("Error: %s",e) @@ -107,9 +111,13 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 self.state['error'] = str(e) self.state['connected'] = False + # Publish initial status + libby.publish("yjpiaagim.status", self.state) + def on_stop(self, libby) -> None: #pylint: disable=W0222 '''Stops the daemon and disconnects from hardware device''' try: + self.clean_up_gimbal() self.disconnect() self.logger.info("Disconnected %s", self.daemon_desc) libby.publish("yjpiaagim", {"Daemon Shutdown": "Success"}) @@ -161,7 +169,8 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 return {"ok": False, "error": "Not connected to hardware"} try: - self.dev.set_enable(channel = 0, enable = 1) + self.dev.set_enable(channel = 1, enable = 1) + self.dev.set_enable(channel = 2, enable = 1) self.state['enabled'] = True self.logger.debug("Initialized %s", self.daemon_desc) except Exception as e: # pylint: disable=W0718 @@ -177,7 +186,9 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 try: res_x = self.dev.get_status_update(channel = 1) res_y = self.dev.get_status_update(channel = 2) - enabled = self.dev.get_enable(channel = 0) + enabled_x = self.dev.get_enable(channel = 1) + enabled_y = self.dev.get_enable(channel = 2) + enabled = enabled_x == 1 and enabled_y == 1 status = { "is_connected": self.dev.is_connected(), "position_x": res_x[1], @@ -247,12 +258,12 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 try: xpos = self.dev.get_pos(channel=1) ypos = self.dev.get_pos(channel=2) - position = (xpos, ypos) + position = [float(xpos), float(ypos)] 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)} + return {"ok":True, "position": position} def set_pos(self, axis, pos): '''sets current position''' @@ -270,12 +281,12 @@ class Yjpiaagim(HispecDaemon): #pylint: disable = W0223 return {"ok": False, "error": "Axis must be 0 (X) or 1 (Y)"} chan = axis + 1 self.dev.set_pos(channel=chan, pos=pos) - self.logger.debug("set_pos: %d",pos) + self.logger.debug("set_pos: %s",pos) position = self.dev.get_pos(channel=chan) 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)} + return {"ok":True, "position": position} def goto_named_pos(self, name): '''moves to named position'''