From cc0426beeb12805ca6830e8aa7cbcabe2863c795 Mon Sep 17 00:00:00 2001 From: elijahab Date: Fri, 22 May 2026 11:23:05 -0700 Subject: [PATCH 1/8] Initial refactor of gimbal mount daemon to fit new design --- config/hsfei/hsfei_yjpiaagim.yaml | 25 ++ daemons/hsfei/piaa-gimbalmount | 383 ++++++++++++++++++++++++++++++ src/hispec/driver/thorlabs | 2 +- 3 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 config/hsfei/hsfei_yjpiaagim.yaml create mode 100644 daemons/hsfei/piaa-gimbalmount diff --git a/config/hsfei/hsfei_yjpiaagim.yaml b/config/hsfei/hsfei_yjpiaagim.yaml new file mode 100644 index 0000000..2d89a72 --- /dev/null +++ b/config/hsfei/hsfei_yjpiaagim.yaml @@ -0,0 +1,25 @@ +# +# Usage: +# daemons/hsfei/piaa-gimbalmount -c config/hsfei/hsfei_yjpiaagim.yaml + +peer_id: yjpiaagim +group_id: hsfei + +hardware: + ip_address: 192.168.29.100 + tcp_port: 10013 + axis: 2 + units: urad + timeout: 30.0 + retry_count: 3 + hardlimits: + min: -10.0 + max: 10.0 + +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/piaa-gimbalmount b/daemons/hsfei/piaa-gimbalmount new file mode 100644 index 0000000..2e39b17 --- /dev/null +++ b/daemons/hsfei/piaa-gimbalmount @@ -0,0 +1,383 @@ +#!/usr/bin/python3.12 +'''Module for the BLUE YJ PIAA Gimbal Daemon''' +import argparse +import sys +from typing import Dict, Any #pylint: disable = W0611 + +from hispec.daemon import HispecDaemon #pylint: disable = E0401,E0611 +from hispec.driver.thorlabs.ppc102 import Ppc102Controller #pylint: disable = E0401,E0611 +#from ppc102 import Ppc102Controller # Assuming ppc102.py is in the same directory + +class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 + '''Daemon for controlling the Blue Piaa Gimbal Mount via Thorlabs PPC102 controller''' + + # Defaults + group_id = "hsfei" + + # pub/sub topics + topics = {} + + def __init__(self): + """Initialize the Gimbal daemon.""" + super().__init__() + + #Defaults + self.host = None + self.port = None + self.dev = Ppc102Controller(log = True) + self.daemon_desc = self.get_config("peer_id") + self.units = self.get_config("units") + self._soft_min = None # No limit by default + self._soft_max = None # No limit by default + self._hard_min = self.get_config("hardware.hard_limits.min", default=None) + self._hard_max = self.get_config("hardware.hard_limits.max", default=None) + + # Daemon state + self.state = { + 'connected': False, + 'error': '', + 'enabled': False, + 'loops_closed': False + } + + def on_start(self, libby): + '''Starts up daemon and initializies the hardware device''' + + #load configuration from hsfei.yaml + self.host = self.get_config("hardware.ip_address") + self.port = self.get_config("hardware.tcp_port") + + # Initialize hardware connection + if not(self.host and self.port): + self.logger.error("No IP address or port specified for Filter Wheel controller") + self.state['error'] = 'No IP address or port specified' + return + + try: + connection = self.connect() + if not connection.get("ok"): + raise ConnectionError(connection.get("error")) + self.state['connected'] = True + self.initialize() + self.keyword_registry.bool("isconnected", + getter=self.dev.is_connected, + description="Check if daemon can talk to the GimbalMount controller.") + self.keyword_registry.bool("isloopsclosed", + getter=self.dev.is_loop_closed, + description="Check if gimbal loops are closed.") + self.keyword_registry.bool("closeloops", + setter=self.keyword_wrapper(self.close_loops), + description="Close gimbal control loops.") + self.keyword_registry.bool("openloops", + setter=self.keyword_wrapper(self.open_loops), + description="Open gimbal control loops.") + self.keyword_registry.int("position", + getter=self.keyword_wrapper(self.get_pos, key="position"), + setter=self.keyword_wrapper(self.set_pos, key="position"), + validator=self._check_soft_limits, + units=self.units, + description="Set and get current position of GimbalMount.") + self.keyword_registry.string("namedposition", + getter=self.keyword_wrapper(self.cur_named_position, key="named_pos"), + setter=self.keyword_wrapper(self.goto_named_pos, key="named_pos"), + validator=self._check_soft_limits, + description="Set and get named position of GimbalMount.") + self.keyword_registry.int("softmin", + getter=lambda: self._soft_min, + setter=lambda v: setattr(self, "_soft_min", int(v)), + units=self.units, + description="Software lower limit for gimbal position.") + self.keyword_registry.int("softmax", + getter=lambda: self._soft_max, + setter=lambda v: setattr(self, "_soft_max", int(v)), + units=self.units, + description="Software upper limit for gimbal position.") + self.keyword_registry.int("hardmin", + getter=lambda: self._hard_min, + setter=lambda v: setattr(self, "_hard_min", int(v)), + units=self.units, + description="Hardware lower limit for gimbal position.") + self.keyword_registry.int("hardmax", + getter=lambda: self._hard_max, + setter=lambda v: setattr(self, "_hard_max", int(v)), + units=self.units, + description="Hardware upper limit for gimbal position.") + + self.logger.info("Daemon started successfully and connected to hardware") + 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 + + 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(), + # "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.cur_named_position(), + # "position.set_named": lambda p: self.goto_named_pos(name = p.get("named_pos")), + # "cleanup": lambda p: self.clean_up_gimbal() + #}) + + # Publish initial status + libby.publish("yjpiaagim.status", self.state) + + 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 = 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 + self.logger.error("error: %s",e) + return {"ok":False , "error": str(e)} + return {"ok": True} + + 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.""" + if not self.state['connected']: + return {"ok": False, "error": "Not connected to hardware"} + + try: + 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.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) + return {"ok": False, "error": str(e)} + return {"ok": True, "named_pos": "unknown", "position": current_pos} #pylint: disable = C0301 + + 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"}) + 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 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""" + 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 status(self): + """handles status""" + if not self.state['connected']: + return {"ok": False, "error": "Not connected to hardware"} + + try: + res_x = self.dev.get_status_update(channel = 1) + res_y = self.dev.get_status_update(channel = 2) + 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], + "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) + 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 {"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 {"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 {"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 = [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": 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 {"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: %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": 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.get_named_position(name.lower()) + if goal is not None: + 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 + self.logger.error("Error: %s",e) + 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='FilterWheel Daemon' + ) + parser.add_argument('-c', '--config', type=str, + help='Path to config file (YAML or JSON)') + parser.add_argument('-d', '--daemon-id', type=str, + help='Daemon ID (required for subsystem configs with multiple daemons)') + + args = parser.parse_args() + + if not args.config: + print("--config is required", file=sys.stderr) + sys.exit(2) + + try: + daemon = PiaaGimbalmount.from_config_file(args.config, daemon_id=args.daemon_id) + 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/src/hispec/driver/thorlabs b/src/hispec/driver/thorlabs index 0c39034..89cee42 160000 --- a/src/hispec/driver/thorlabs +++ b/src/hispec/driver/thorlabs @@ -1 +1 @@ -Subproject commit 0c390342f07fe06f0f92469a6a082af28e4a82e1 +Subproject commit 89cee42fb943e88fc17c192e3197396735a904e5 From 72330e6e8123b2f8631b88c2386c9f899fdc92aa Mon Sep 17 00:00:00 2001 From: elijahab Date: Fri, 22 May 2026 11:57:19 -0700 Subject: [PATCH 2/8] commit to change branches momentarily --- daemons/hsfei/piaa-gimbalmount | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemons/hsfei/piaa-gimbalmount b/daemons/hsfei/piaa-gimbalmount index 2e39b17..f394809 100644 --- a/daemons/hsfei/piaa-gimbalmount +++ b/daemons/hsfei/piaa-gimbalmount @@ -66,10 +66,10 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 getter=self.dev.is_loop_closed, description="Check if gimbal loops are closed.") self.keyword_registry.bool("closeloops", - setter=self.keyword_wrapper(self.close_loops), + setter=self.dev.close_loop, description="Close gimbal control loops.") self.keyword_registry.bool("openloops", - setter=self.keyword_wrapper(self.open_loops), + setter=self.dev.set_loop(loop=1), description="Open gimbal control loops.") self.keyword_registry.int("position", getter=self.keyword_wrapper(self.get_pos, key="position"), From 0a31c477df923a773a4e49d11cddd01168c1c3b6 Mon Sep 17 00:00:00 2001 From: elijahab Date: Tue, 26 May 2026 18:54:17 -0700 Subject: [PATCH 3/8] Second iteration, changing branches to fix something else --- daemons/hsfei/piaa-gimbalmount | 159 +++++++++++++++++---------------- 1 file changed, 82 insertions(+), 77 deletions(-) diff --git a/daemons/hsfei/piaa-gimbalmount b/daemons/hsfei/piaa-gimbalmount index f394809..0f81028 100644 --- a/daemons/hsfei/piaa-gimbalmount +++ b/daemons/hsfei/piaa-gimbalmount @@ -25,19 +25,20 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 self.host = None self.port = None self.dev = Ppc102Controller(log = True) - self.daemon_desc = self.get_config("peer_id") - self.units = self.get_config("units") - self._soft_min = None # No limit by default - self._soft_max = None # No limit by default - self._hard_min = self.get_config("hardware.hard_limits.min", default=None) - self._hard_max = self.get_config("hardware.hard_limits.max", default=None) + self.daemon_desc = None + self.units = None + self._soft_min = None + self._soft_max = None + self._hard_min = None + self._hard_max = None + self.named_positions = None # Daemon state self.state = { 'connected': False, 'error': '', 'enabled': False, - 'loops_closed': False + 'isloopsclosed': False } def on_start(self, libby): @@ -46,6 +47,13 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 #load configuration from hsfei.yaml self.host = self.get_config("hardware.ip_address") self.port = self.get_config("hardware.tcp_port") + self.daemon_desc = self.get_config("peer_id") + self.units = self.get_config("units") + self._soft_min = self.get_config("limits.soft_min") + self._soft_max = self.get_config("limits.soft_max") + self._hard_min = self.get_config("limits.hard_min") + self._hard_max = self.get_config("limits.hard_max") + self.named_positions = self.get_config("named_positions") # Initialize hardware connection if not(self.host and self.port): @@ -61,23 +69,34 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 self.initialize() self.keyword_registry.bool("isconnected", getter=self.dev.is_connected, + setter=self.keyword_wrapper(self.connect, key="isconnected"), description="Check if daemon can talk to the GimbalMount controller.") self.keyword_registry.bool("isloopsclosed", getter=self.dev.is_loop_closed, + setter=self.keyword_wrapper(self.set_loops, key="loops"), description="Check if gimbal loops are closed.") - self.keyword_registry.bool("closeloops", - setter=self.dev.close_loop, - description="Close gimbal control loops.") - self.keyword_registry.bool("openloops", - setter=self.dev.set_loop(loop=1), - description="Open gimbal control loops.") - self.keyword_registry.int("position", + self.keyword_registry.bool("ismoving", + getter=, + description=) + self.keyword_registry.bool("isreferenced", + getter=, + description=) + self.keyword_registry.trigger("halt", + action=, + description=) + self.keyword_registry.int("positionvaluex", getter=self.keyword_wrapper(self.get_pos, key="position"), setter=self.keyword_wrapper(self.set_pos, key="position"), validator=self._check_soft_limits, units=self.units, description="Set and get current position of GimbalMount.") - self.keyword_registry.string("namedposition", + self.keyword_registry.int("positionvaluey", + getter=self.keyword_wrapper(self.get_pos, key="position"), + setter=self.keyword_wrapper(self.set_pos, key="position"), + validator=self._check_soft_limits, + units=self.units, + description="Set and get current position of GimbalMount.") + self.keyword_registry.string("positionnamed", getter=self.keyword_wrapper(self.cur_named_position, key="named_pos"), setter=self.keyword_wrapper(self.goto_named_pos, key="named_pos"), validator=self._check_soft_limits, @@ -94,14 +113,18 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 description="Software upper limit for gimbal position.") self.keyword_registry.int("hardmin", getter=lambda: self._hard_min, - setter=lambda v: setattr(self, "_hard_min", int(v)), units=self.units, description="Hardware lower limit for gimbal position.") self.keyword_registry.int("hardmax", getter=lambda: self._hard_max, - setter=lambda v: setattr(self, "_hard_max", int(v)), units=self.units, description="Hardware upper limit for gimbal position.") + self.keyword_registry.string("status", + getter=self.keyword_wrapper(self.status,key="status"), + description="Grabs status of gimbal mount") + self.keyword_registry.trigger("cleanupgimbal", + action=, + description=) self.logger.info("Daemon started successfully and connected to hardware") self.logger.info("Initialized %s", self.daemon_desc) @@ -112,21 +135,6 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 self.state['connected'] = False 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(), - # "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.cur_named_position(), - # "position.set_named": lambda p: self.goto_named_pos(name = p.get("named_pos")), - # "cleanup": lambda p: self.clean_up_gimbal() - #}) # Publish initial status libby.publish("yjpiaagim.status", self.state) @@ -200,29 +208,20 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 return {"ok": False, "error": str(e)} return {"ok": True, "message": "Gimbal cleaned up"} - def connect(self): + def connect(self, connect): """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") + if connect: + self.dev.connect(host = self.host, port = self.port) + self.logger.info("") + else: + self.dev.disconnect() + result = self.dev.is_connected() 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) + self.logger.error("Failed to Connect or Disconnect with Hardware: %s",e) return {"ok": False, "error": str(e)} - return {"ok": True, "message": "Disconnected from hardware"} + return {"ok": True, "isconnected": result} def status(self): """handles status""" @@ -244,7 +243,7 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 "flag_x": res_x[2], "flag_y": res_y[2], "enabled": enabled, - "loops_closed": self.dev.is_loop_closed() + "isloopsclosed": self.dev.is_loop_closed() } self.logger.debug("status: %s",status) except Exception as e: # pylint: disable=W0718 @@ -263,35 +262,25 @@ class PiaaGimbalmount(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, "loops_closed": closed} + return {"ok":True, "isloopsclosed": closed} - def close_loops(self): - '''closes control loops''' + def set_loops(self, loops): + '''sets 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 + if loops: + self.dev.set_loop(channel=0, loop=2) + self.logger.debug("close loops sent") + else: + self.dev.set_loop(channel=0, loop=1) + self.logger.debug("open loops sent") + result = self.dev.is_loop_closed() + if result != loops: + raise RuntimeError("Failed to execute set loops") + self.state['isloopsclosed'] = result except Exception as e: # pylint: disable=W0718 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 {"ok": False, "error": str(e)} - return {"ok":True, "loops_closed": closed} + return {"ok":True, "isloopsclosed": result} def get_pos(self): '''gets current position''' @@ -299,7 +288,7 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 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']: + if not self.state['isloopsclosed']: return {"ok": False, "error": "Control loops are not closed"} try: xpos = self.dev.get_pos(channel=1) @@ -317,7 +306,7 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 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']: + if not self.state['isloopsclosed']: return {"ok": False, "error": "Control loops are not closed"} try: pos = float(pos) @@ -351,6 +340,22 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 return {"ok": False, "error": str(e)} return {"ok": True, "named_pos": name, "position": cur_pos} + @staticmethod + def keyword_wrapper(func, key=None): + """Wrap a daemon method for use as a keyword getter/setter.""" + def wrapper(*args, **kwargs): + try: + result = func(*args, **kwargs) + except Exception as e: + print(f"DEBUG keyword_wrapper [{func.__name__}]: exception={e}") + raise + if not result.get("ok"): + raise RuntimeError(result.get("error", f"Unknown error in {func.__name__}")) + if key: + return result[key] + return {k: v for k, v in result.items() if k != "ok"} + return wrapper + def main(): """Main entry point for the daemon.""" From 106610624c77d735ebae5b239c3c78569e9f7840 Mon Sep 17 00:00:00 2001 From: elijahab Date: Tue, 26 May 2026 19:11:08 -0700 Subject: [PATCH 4/8] Save point for current iteration --- daemons/hsfei/piaa-gimbalmount | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/daemons/hsfei/piaa-gimbalmount b/daemons/hsfei/piaa-gimbalmount index 0f81028..7941a8b 100644 --- a/daemons/hsfei/piaa-gimbalmount +++ b/daemons/hsfei/piaa-gimbalmount @@ -1,5 +1,5 @@ #!/usr/bin/python3.12 -'''Module for the BLUE YJ PIAA Gimbal Daemon''' +'''Module for Gimbal mount Daemon''' import argparse import sys from typing import Dict, Any #pylint: disable = W0611 @@ -62,7 +62,7 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 return try: - connection = self.connect() + connection = self.connect(True) if not connection.get("ok"): raise ConnectionError(connection.get("error")) self.state['connected'] = True @@ -77,13 +77,13 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 description="Check if gimbal loops are closed.") self.keyword_registry.bool("ismoving", getter=, - description=) + description="checks if stage is currently moving") self.keyword_registry.bool("isreferenced", getter=, - description=) + description="check if stage is referenced") self.keyword_registry.trigger("halt", action=, - description=) + description="Halts stage from further movement") self.keyword_registry.int("positionvaluex", getter=self.keyword_wrapper(self.get_pos, key="position"), setter=self.keyword_wrapper(self.set_pos, key="position"), @@ -123,8 +123,8 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 getter=self.keyword_wrapper(self.status,key="status"), description="Grabs status of gimbal mount") self.keyword_registry.trigger("cleanupgimbal", - action=, - description=) + action=self.clean_up_gimbal, + description="Clean up Gimbal, Open loops and set voltage to 0") self.logger.info("Daemon started successfully and connected to hardware") self.logger.info("Initialized %s", self.daemon_desc) @@ -137,7 +137,7 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 self.logger.info("Starting %s Daemon", self.daemon_desc) # Publish initial status - libby.publish("yjpiaagim.status", self.state) + libby.publish(self.peer_id, "status: %s", self.state) def initialize(self): """handles initialization""" @@ -187,12 +187,11 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 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.connect(False) self.logger.info("Disconnected %s", self.daemon_desc) - libby.publish("yjpiaagim", {"Daemon Shutdown": "Success"}) + libby.publish(self.peer_id, {"Daemon Shutdown": "Success"}) except Exception as e: # pylint: disable=W0718 - libby.publish("yjpiaagim", {"Daemon Startup": "Failed", "error":f"{e}"}) + libby.publish(self.peer_id, {"Daemon Startup": "Failed", "Error":f"{e}"}) self.logger.error("Disconnect %s:: Failed ", self.daemon_desc) def clean_up_gimbal(self): From 9458c55173576927ae97643cfdf10e164d13fd71 Mon Sep 17 00:00:00 2001 From: elijahab Date: Wed, 27 May 2026 10:28:39 -0700 Subject: [PATCH 5/8] limit checks --- daemons/hsfei/piaa-gimbalmount | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/daemons/hsfei/piaa-gimbalmount b/daemons/hsfei/piaa-gimbalmount index 7941a8b..76e12a9 100644 --- a/daemons/hsfei/piaa-gimbalmount +++ b/daemons/hsfei/piaa-gimbalmount @@ -2,7 +2,7 @@ '''Module for Gimbal mount Daemon''' import argparse import sys -from typing import Dict, Any #pylint: disable = W0611 +from typing import Dict, Any, Optional #pylint: disable = W0611 from hispec.daemon import HispecDaemon #pylint: disable = E0401,E0611 from hispec.driver.thorlabs.ppc102 import Ppc102Controller #pylint: disable = E0401,E0611 @@ -75,22 +75,13 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 getter=self.dev.is_loop_closed, setter=self.keyword_wrapper(self.set_loops, key="loops"), description="Check if gimbal loops are closed.") - self.keyword_registry.bool("ismoving", - getter=, - description="checks if stage is currently moving") - self.keyword_registry.bool("isreferenced", - getter=, - description="check if stage is referenced") - self.keyword_registry.trigger("halt", - action=, - description="Halts stage from further movement") - self.keyword_registry.int("positionvaluex", + self.keyword_registry.int("positionx", getter=self.keyword_wrapper(self.get_pos, key="position"), setter=self.keyword_wrapper(self.set_pos, key="position"), validator=self._check_soft_limits, units=self.units, description="Set and get current position of GimbalMount.") - self.keyword_registry.int("positionvaluey", + self.keyword_registry.int("positiony", getter=self.keyword_wrapper(self.get_pos, key="position"), setter=self.keyword_wrapper(self.set_pos, key="position"), validator=self._check_soft_limits, @@ -99,7 +90,7 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 self.keyword_registry.string("positionnamed", getter=self.keyword_wrapper(self.cur_named_position, key="named_pos"), setter=self.keyword_wrapper(self.goto_named_pos, key="named_pos"), - validator=self._check_soft_limits, + validator=self._check_named, description="Set and get named position of GimbalMount.") self.keyword_registry.int("softmin", getter=lambda: self._soft_min, @@ -339,6 +330,22 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 return {"ok": False, "error": str(e)} return {"ok": True, "named_pos": name, "position": cur_pos} + def _check_soft_limits(self, pos: int) -> bool: + """Returns True if position is within soft limits.""" + if self._soft_min is not None and pos < self._soft_min: + raise ValueError(f"Position {pos} below soft min {self._soft_min}") + if self._soft_max is not None and pos > self._soft_max: + raise ValueError(f"Position {pos} above soft max {self._soft_max}") + return None + + def _check_named(self, name: str) -> Optional[str]: + if not name: + return "value must be a non-empty string" + if name not in self.named_positions: + return (f"unknown named position '{name}'; " + f"available: {list(self.named_positions)}") + return None + @staticmethod def keyword_wrapper(func, key=None): """Wrap a daemon method for use as a keyword getter/setter.""" From 3f1e6472e3868f000fb72f4e01426d496ddf9f2a Mon Sep 17 00:00:00 2001 From: elijahab Date: Wed, 27 May 2026 10:43:08 -0700 Subject: [PATCH 6/8] yaml files --- config/hsfei/hsfei_hkpiaagim.yaml | 28 ++++++++++++++++++++++++++++ config/hsfei/hsfei_yjpiaagim.yaml | 29 ++++++++++++++++------------- daemons/hsfei/piaa-gimbalmount | 2 +- 3 files changed, 45 insertions(+), 14 deletions(-) create mode 100644 config/hsfei/hsfei_hkpiaagim.yaml mode change 100644 => 100755 daemons/hsfei/piaa-gimbalmount diff --git a/config/hsfei/hsfei_hkpiaagim.yaml b/config/hsfei/hsfei_hkpiaagim.yaml new file mode 100644 index 0000000..c07f3ec --- /dev/null +++ b/config/hsfei/hsfei_hkpiaagim.yaml @@ -0,0 +1,28 @@ +# +# Usage: +# daemons/hsfei/piaa-gimbalmount -c config/hsfei/hsfei_hkpiaagim.yaml + +peer_id: hsfei_hkpiaagim +group_id: hsfei + +hardware: + ip_address: 192.168.29.100 + tcp_port: 10012 + axis: 2 + units: urad + timeout: 30.0 + retry_count: 3 + +limits: + soft_min: -9.5 + soft_max: 9.5 + hard_min: -10.0 + hard_max: 10.0 + +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/config/hsfei/hsfei_yjpiaagim.yaml b/config/hsfei/hsfei_yjpiaagim.yaml index 2d89a72..d9f896f 100644 --- a/config/hsfei/hsfei_yjpiaagim.yaml +++ b/config/hsfei/hsfei_yjpiaagim.yaml @@ -2,24 +2,27 @@ # Usage: # daemons/hsfei/piaa-gimbalmount -c config/hsfei/hsfei_yjpiaagim.yaml -peer_id: yjpiaagim +peer_id: hsfei_yjpiaagim group_id: hsfei hardware: - ip_address: 192.168.29.100 - tcp_port: 10013 - axis: 2 - units: urad - timeout: 30.0 - retry_count: 3 - hardlimits: - min: -10.0 - max: 10.0 + ip_address: 192.168.29.100 + tcp_port: 10013 + axis: 2 + units: urad + timeout: 30.0 + retry_count: 3 + +limits: + soft_min: -9.5 + soft_max: 9.5 + hard_min: -10.0 + hard_max: 10.0 named_positions: - center: [0.0, 0.0] - offset1: [1.0, 1.0] - offset2: [-1.0, -1.0] + 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/piaa-gimbalmount b/daemons/hsfei/piaa-gimbalmount old mode 100644 new mode 100755 index 76e12a9..064e834 --- a/daemons/hsfei/piaa-gimbalmount +++ b/daemons/hsfei/piaa-gimbalmount @@ -1,4 +1,4 @@ -#!/usr/bin/python3.12 +#!/usr/bin/env python3 '''Module for Gimbal mount Daemon''' import argparse import sys From 3277f24e628f834d4e44b3da3fdbb55ea5eac349 Mon Sep 17 00:00:00 2001 From: elijahab Date: Wed, 27 May 2026 15:50:28 -0700 Subject: [PATCH 7/8] 99% done, one issue with the positionnamed acting CORRECTLY but not printing correctly. Usable for Dan at the moment so wanting to push --- daemons/hsfei/piaa-gimbalmount | 133 +++++++++++++++++---------------- 1 file changed, 69 insertions(+), 64 deletions(-) diff --git a/daemons/hsfei/piaa-gimbalmount b/daemons/hsfei/piaa-gimbalmount index 064e834..42e1af8 100755 --- a/daemons/hsfei/piaa-gimbalmount +++ b/daemons/hsfei/piaa-gimbalmount @@ -4,8 +4,8 @@ import argparse import sys from typing import Dict, Any, Optional #pylint: disable = W0611 -from hispec.daemon import HispecDaemon #pylint: disable = E0401,E0611 -from hispec.driver.thorlabs.ppc102 import Ppc102Controller #pylint: disable = E0401,E0611 +from hispec.daemon import HispecDaemon #pylint: disable = E0611 +from hispec.driver.thorlabs.ppc102 import Ppc102Controller #pylint: disable = E0611 #from ppc102 import Ppc102Controller # Assuming ppc102.py is in the same directory class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 @@ -73,17 +73,17 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 description="Check if daemon can talk to the GimbalMount controller.") self.keyword_registry.bool("isloopsclosed", getter=self.dev.is_loop_closed, - setter=self.keyword_wrapper(self.set_loops, key="loops"), + setter=self.keyword_wrapper(self.set_loops, key="isloopsclosed"), description="Check if gimbal loops are closed.") - self.keyword_registry.int("positionx", - getter=self.keyword_wrapper(self.get_pos, key="position"), - setter=self.keyword_wrapper(self.set_pos, key="position"), + self.keyword_registry.float("positionvaluex", + getter=self.keyword_wrapper(self.get_xpos, key="position"), + setter=self.keyword_wrapper(self.set_xpos, key="position"), validator=self._check_soft_limits, units=self.units, description="Set and get current position of GimbalMount.") - self.keyword_registry.int("positiony", - getter=self.keyword_wrapper(self.get_pos, key="position"), - setter=self.keyword_wrapper(self.set_pos, key="position"), + self.keyword_registry.float("positionvaluey", + getter=self.keyword_wrapper(self.get_ypos, key="position"), + setter=self.keyword_wrapper(self.set_ypos, key="position"), validator=self._check_soft_limits, units=self.units, description="Set and get current position of GimbalMount.") @@ -113,7 +113,7 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 self.keyword_registry.string("status", getter=self.keyword_wrapper(self.status,key="status"), description="Grabs status of gimbal mount") - self.keyword_registry.trigger("cleanupgimbal", + self.keyword_registry.trigger("cleanup", action=self.clean_up_gimbal, description="Clean up Gimbal, Open loops and set voltage to 0") @@ -127,9 +127,6 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 self.logger.info("Starting %s Daemon", self.daemon_desc) - # Publish initial status - libby.publish(self.peer_id, "status: %s", self.state) - def initialize(self): """handles initialization""" # for PPC102_Coms, this involves setting the enable @@ -146,57 +143,14 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 return {"ok":False , "error": str(e)} return {"ok": True} - 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.""" - if not self.state['connected']: - return {"ok": False, "error": "Not connected to hardware"} - - try: - 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.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) - return {"ok": False, "error": str(e)} - return {"ok": True, "named_pos": "unknown", "position": current_pos} #pylint: disable = C0301 - def on_stop(self, libby) -> None: #pylint: disable=W0222 '''Stops the daemon and disconnects from hardware device''' try: self.connect(False) self.logger.info("Disconnected %s", self.daemon_desc) - libby.publish(self.peer_id, {"Daemon Shutdown": "Success"}) except Exception as e: # pylint: disable=W0718 - libby.publish(self.peer_id, {"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"} + self.logger.error("Error: %s",e) def connect(self, connect): """handles connection""" @@ -213,6 +167,19 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 return {"ok": False, "error": str(e)} return {"ok": True, "isconnected": result} + 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 status(self): """handles status""" if not self.state['connected']: @@ -254,7 +221,7 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 return {"ok": False, "error": str(e)} return {"ok":True, "isloopsclosed": closed} - def set_loops(self, loops): + def set_loops(self, loops: bool): '''sets control loops''' try: if loops: @@ -272,7 +239,15 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 return {"ok": False, "error": str(e)} return {"ok":True, "isloopsclosed": result} - def get_pos(self): + def get_xpos(self): + '''gets current X position''' + return self.get_pos(axis=0) + + def get_ypos(self): + '''gets current Y position''' + return self.get_pos(axis=1) + + def get_pos(self, axis : int): '''gets current position''' if not self.state['connected']: return {"ok": False, "error": "Not connected to hardware"} @@ -281,15 +256,22 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 if not self.state['isloopsclosed']: 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 = [float(xpos), float(ypos)] + pos = self.dev.get_pos(channel=axis+1) + position = float(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": position} + def set_xpos(self, pos: float): + '''sets current X position''' + return self.set_pos(axis=0, pos=pos) + + def set_ypos(self, pos: float): + '''sets current Y position''' + return self.set_pos(axis=1, pos=pos) + def set_pos(self, axis, pos): '''sets current position''' if not self.state['connected']: @@ -319,7 +301,8 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 return {"ok": False, "error": "Not connected to hardware"} try: - goal = self.get_named_position(name.lower()) + str_goal = name.lower() + goal = self.named_positions.get(str_goal) if goal is not None: self.set_pos(axis=0, pos=float(goal[0])) self.set_pos(axis=1, pos=float(goal[1])) @@ -328,7 +311,29 @@ class PiaaGimbalmount(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_pos": name, "position": cur_pos} + return {"ok": True, "named_pos": str_goal, "position": cur_pos} + + def cur_named_position(self): + """Get the name of the current position, if it matches a named position.""" + if not self.state['connected']: + return {"ok": False, "error": "Not connected to hardware"} + + try: + current_pos = ( + float(self.dev.get_pos(channel=1)), + float(self.dev.get_pos(channel=2)) + ) + + for name, pos in self.named_positions.items(): + dx = abs(float(pos[0]) - float(current_pos[0])) + dy = abs(float(pos[1]) - float(current_pos[1])) + 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) + return {"ok": False, "error": str(e)} + return {"ok": True, "named_pos": "unknown", "position": current_pos} #pylint: disable = C0301 + def _check_soft_limits(self, pos: int) -> bool: """Returns True if position is within soft limits.""" From dcfa7487aaea92c1f83a5297870180d11658c3da Mon Sep 17 00:00:00 2001 From: elijahab Date: Thu, 28 May 2026 11:20:26 -0700 Subject: [PATCH 8/8] adding voltage control for open loop operations --- config/hsfei/hsfei_hkpiaagim.yaml | 17 +++++--- config/hsfei/hsfei_yjpiaagim.yaml | 17 +++++--- daemons/hsfei/piaa-gimbalmount | 64 +++++++++++++++++++++++++++---- 3 files changed, 81 insertions(+), 17 deletions(-) diff --git a/config/hsfei/hsfei_hkpiaagim.yaml b/config/hsfei/hsfei_hkpiaagim.yaml index c07f3ec..524042d 100644 --- a/config/hsfei/hsfei_hkpiaagim.yaml +++ b/config/hsfei/hsfei_hkpiaagim.yaml @@ -9,15 +9,22 @@ hardware: ip_address: 192.168.29.100 tcp_port: 10012 axis: 2 - units: urad + closed_loop_units: mrad + open_loop_units: volts timeout: 30.0 retry_count: 3 limits: - soft_min: -9.5 - soft_max: 9.5 - hard_min: -10.0 - hard_max: 10.0 + open_loop: + soft_min: -20.0 + soft_max: 145.0 + hard_min: -25.0 + hard_max: 150.0 + closed_loop: + soft_min: -9.5 + soft_max: 9.5 + hard_min: -10.0 + hard_max: 10.0 named_positions: center: [0.0, 0.0] diff --git a/config/hsfei/hsfei_yjpiaagim.yaml b/config/hsfei/hsfei_yjpiaagim.yaml index d9f896f..339a33d 100644 --- a/config/hsfei/hsfei_yjpiaagim.yaml +++ b/config/hsfei/hsfei_yjpiaagim.yaml @@ -9,15 +9,22 @@ hardware: ip_address: 192.168.29.100 tcp_port: 10013 axis: 2 - units: urad + closed_loop_units: mrad + open_loop_units: volts timeout: 30.0 retry_count: 3 limits: - soft_min: -9.5 - soft_max: 9.5 - hard_min: -10.0 - hard_max: 10.0 + open_loop: + soft_min: -20.0 + soft_max: 145.0 + hard_min: -25.0 + hard_max: 150.0 + closed_loop: + soft_min: -9.5 + soft_max: 9.5 + hard_min: -10.0 + hard_max: 10.0 named_positions: center: [0.0, 0.0] diff --git a/daemons/hsfei/piaa-gimbalmount b/daemons/hsfei/piaa-gimbalmount index 42e1af8..b8fbe07 100755 --- a/daemons/hsfei/piaa-gimbalmount +++ b/daemons/hsfei/piaa-gimbalmount @@ -17,6 +17,12 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 # pub/sub topics topics = {} + #voltage control parameters + V_MIN = -20 + V_MAX = 150 + DIGITAL_MIN = -32767 + DIGITAL_MAX = 32767 + def __init__(self): """Initialize the Gimbal daemon.""" super().__init__() @@ -48,11 +54,11 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 self.host = self.get_config("hardware.ip_address") self.port = self.get_config("hardware.tcp_port") self.daemon_desc = self.get_config("peer_id") - self.units = self.get_config("units") - self._soft_min = self.get_config("limits.soft_min") - self._soft_max = self.get_config("limits.soft_max") - self._hard_min = self.get_config("limits.hard_min") - self._hard_max = self.get_config("limits.hard_max") + self.units = self.get_config("hardware.open_loop_units") + self._soft_min = self.get_config("limits.open_loop.soft_min") + self._soft_max = self.get_config("limits.open_loop.soft_max") + self._hard_min = self.get_config("limits.open_loop.hard_min") + self._hard_max = self.get_config("limits.open_loop.hard_max") self.named_positions = self.get_config("named_positions") # Initialize hardware connection @@ -231,6 +237,20 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 self.dev.set_loop(channel=0, loop=1) self.logger.debug("open loops sent") result = self.dev.is_loop_closed() + if result: + self.logger.debug("loops are closed") + self.units = self.get_config("hardware.closed_loop_units") + self._soft_min = self.get_config("limits.closed_loop.soft_min") + self._soft_max = self.get_config("limits.closed_loop.soft_max") + self._hard_min = self.get_config("limits.closed_loop.hard_min") + self._hard_max = self.get_config("limits.closed_loop.hard_max") + else: + self.logger.debug("loops are open") + self.units = self.get_config("hardware.open_loop_units") + self._soft_min = self.get_config("limits.open_loop.soft_min") + self._soft_max = self.get_config("limits.open_loop.soft_max") + self._hard_min = self.get_config("limits.open_loop.hard_min") + self._hard_max = self.get_config("limits.open_loop.hard_max") if result != loops: raise RuntimeError("Failed to execute set loops") self.state['isloopsclosed'] = result @@ -266,11 +286,41 @@ class PiaaGimbalmount(HispecDaemon): #pylint: disable = W0223 def set_xpos(self, pos: float): '''sets current X position''' - return self.set_pos(axis=0, pos=pos) + if self.state['isloopsclosed']: + return self.set_pos(axis=0, pos=pos) + return self.set_volts(axis=0, volts=pos) def set_ypos(self, pos: float): '''sets current Y position''' - return self.set_pos(axis=1, pos=pos) + if self.state['isloopsclosed']: + return self.set_pos(axis=1, pos=pos) + return self.set_volts(axis=1, volts=pos) + + def set_volts(self, axis: int, volts: float): + '''sets output voltage for open loop control''' + 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 self.state['isloopsclosed']: + return {"ok": False, "error": "Control loops are closed; cannot set volts"} + try: + volts = float(volts) + axis = int(axis) + if axis not in [0,1]: + self.logger.error("Axis must be 0 (X) or 1 (Y)") + return {"ok": False, "error": "Axis must be 0 (X) or 1 (Y)"} + chan = axis + 1 + #convert volts to int between -32768 and 32,767 for PPC102 + voltage_ratio = (volts - self.V_MIN) / (self.V_MAX - self.V_MIN) + volts_int = int(voltage_ratio*(self.DIGITAL_MAX-self.DIGITAL_MIN)+self.DIGITAL_MIN) + self.dev.set_output_volts(channel=chan, volts=volts_int) + self.logger.debug("set_volts: %s",volts) + voltage = self.dev.get_status_update(channel=chan)[0] + except Exception as e: # pylint: disable=W0718 + self.logger.error("error: %s",e) + return {"ok": False, "error": str(e)} + return {"ok":True, "voltage": voltage} def set_pos(self, axis, pos): '''sets current position'''