Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions config/hsfei/hsfei.yaml
Original file line number Diff line number Diff line change
@@ -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]
17 changes: 17 additions & 0 deletions config/hsfei/hsfei_yjpiaagim.yaml
Original file line number Diff line number Diff line change
@@ -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
262 changes: 262 additions & 0 deletions daemons/hsfei/atcfwheel
Original file line number Diff line number Diff line change
@@ -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()
Loading
Loading