diff --git a/config/hscal/hscal_hketatten.yaml b/config/hscal/hscal_hketatten.yaml index c5f0a0f..a654666 100644 --- a/config/hscal/hscal_hketatten.yaml +++ b/config/hscal/hscal_hketatten.yaml @@ -1,4 +1,4 @@ -# HISPEC RED CAL Etalon Attenuator — Standa SMC8 contoller instance +# HISPEC RED CAL Etalon Attenuator — Standa SMC8 controller instance # # Usage: # daemons/hscal/smc8_attenuator -c config/hscal/hscal_hketatten.yaml diff --git a/config/hscal/hscal_yjetatten.yaml b/config/hscal/hscal_yjetatten.yaml new file mode 100644 index 0000000..6af690b --- /dev/null +++ b/config/hscal/hscal_yjetatten.yaml @@ -0,0 +1,15 @@ +# HISPEC BLUE CAL Etalon Attenuator — OZOptics DD100MC controller instance +# +# Usage: +# daemons/hscal/ozopt_attenuator -c config/hscal/hscal_yjetatten.yaml + +peer_id: yjetatten +group_id: hscal + +hardware: + ip_address: 192.168.29.153 + tcp_port: 10003 + +logging: + level: INFO + file: /tmp/hscal_hketatten.log \ No newline at end of file diff --git a/daemons/hscal/ozopt_attenuator b/daemons/hscal/ozopt_attenuator new file mode 100644 index 0000000..f975549 --- /dev/null +++ b/daemons/hscal/ozopt_attenuator @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +""" +OZOptics Attenuator Daemon + +Exposes the OZOptics attenuator functions as libby keywords. +""" + +import argparse +import sys + +from hispec import HispecDriverDaemon +from hispec.driver.ozoptics.dd100mc import OZController + +# StageController.home() polls to completion, so the RPC waits out the move +HOME_TIMEOUT_S = 60.0 + + +class OZDaemon(HispecDriverDaemon): # pylint: disable=W0223 + """Daemon for controlling the OZ Optics attenuator via keywords.""" + + def __init__(self): + super().__init__() + + self.ip_address = None + self.tcp_port = None + self.controller = OZController(log=True) + self.units = "steps" # units of the position keyword + self.atten_units = "dB" # fixed by the driver's conversion coefficients + + def on_start(self): + """Called when the daemon starts - register keywords and connect.""" + # from_config populates _config after __init__, so read it here + self.ip_address = self.get_config("hardware.ip_address") + self.tcp_port = self.get_config("hardware.tcp_port") + + self.logger.info("Starting %s daemon", self.peer_id) + + self.keyword_registry.bool("isconnected", + getter=self.controller.is_connected, + setter=self._set_connected, + description="ADC rotator controller is connected.") + self.keyword_registry.string("status", + getter=self._status, + description="Status of the device driver as a " + "(code, message) string.") + self.keyword_registry.trigger("shutdown", + action=self._shutdown, + description="Gracefully stop this daemon; " + "refused while a stage is moving.") + self.keyword_registry.bool("isreferenced", + getter=self.is_referenced, + setter=self.controller.home(), + timeout_s=HOME_TIMEOUT_S, + description="OZOptic attenuator stage is referenced; " + "write true to home.") + self.keyword_registry.float(f"position", + getter=self.controller.get_pos, + setter=self.controller.set_pos, + units=self.units, + description="OZOptics stage position in steps.") + self.keyword_registry.float(f"attenuation", + getter=self.controller.get_attenuation, + setter=self.controller.set_attenuation, + units=self.atten_units, + description="OZOptics attenuation in dB.") + self.keyword_registry.string("configuration", + getter=self.controller.get_params, + description="OZOptics attenuator configuration parameters.") + + if not (self.ip_address and self.tcp_port): + self.logger.error("No IP address or port specified") + return + + try: + self._connect_hardware() + self.logger.info("Daemon started successfully and connected to hardware") + except Exception as e: # pylint: disable=W0718 + self.logger.error("Failed to connect to hardware: %s", e) + self.logger.warning("Daemon will start but hardware is not available") + + def on_stop(self): + """Cleanup when the daemon shuts down.""" + self.logger.info("Shutting down %s daemon", self.peer_id) + try: + self.controller.disconnect() + self.logger.info("Disconnected from ADC controller") + except Exception as e: # pylint: disable=W0718 + self.logger.error("Error disconnecting: %s", e) + + def _connect_hardware(self): + self.logger.info("Connecting to OZOptics controller at %s:%s", + self.ip_address, self.tcp_port) + self.controller.connect(host=self.ip_address, port=int(self.tcp_port)) + # The drivers signal failure by staying disconnected rather than raising + if not self.controller.is_connected(): + raise RuntimeError( + f"no connection to {self.ip_address}:{self.tcp_port}") + self.controller.initialize() + + def _set_connected(self, value: bool) -> None: + if value: + self._connect_hardware() + else: + self.controller.disconnect() + + def is_referenced(self) -> bool: + """Return True if the stage has been referenced (homed).""" + return bool(self.controller.is_homed()) + + +def main(): + """Main entry point for the daemon.""" + parser = argparse.ArgumentParser(description='OZOptics Attenuator Daemon') + parser.add_argument('-c', '--config', type=str, + help='Path to config file (YAML or JSON)') + + args = parser.parse_args() + + if not args.config: + print("--config is required", file=sys.stderr) + sys.exit(2) + + try: + daemon = OZDaemon.from_config_file(args.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/systemd/instances/hscal_yjetatten.env b/systemd/instances/hscal_yjetatten.env new file mode 100644 index 0000000..8d04d12 --- /dev/null +++ b/systemd/instances/hscal_yjetatten.env @@ -0,0 +1,9 @@ +# HISPEC BLUE CAL Etalon Attenuator — OZOptics DD100MC controller instance (hscal) +# +# hispec deploy hscal_yjetatten +# +# copies this file to /etc/hispec/instances/hscal_yjetatten.env and +# config/hscal/hscal_yjetatten.yaml to /etc/hispec/hscal_yjetatten.yaml, then +# enables and starts the daemon. +HISPEC_DAEMON=hscal/ozopt_attenuator +HISPEC_HOST=hispecserver