Skip to content

Commit bdc3486

Browse files
authored
Merge pull request #127 from openUC2/mergemaster
enable sending camera triggers from board
2 parents 13747d2 + 858836d commit bdc3486

3 files changed

Lines changed: 208 additions & 4 deletions

File tree

uc2rest/UC2Client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from .message import Message
2929
from .can import CAN
3030
from .canota import CANOTA
31+
from .camera_trigger import CameraTrigger
3132
try:
3233
import requests
3334
except:
@@ -151,6 +152,9 @@ def __init__(self, host=None, port=31950, serialport=None, identity="UC2_Feather
151152

152153
# initialize messaging
153154
self.message = Message(self)
155+
156+
# initialize camera trigger callback handler
157+
self.camera_trigger = CameraTrigger(self)
154158

155159
# initialize module controller
156160
self.modules = Modules(parent=self)

uc2rest/camera_trigger.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
"""
2+
Camera trigger callback module for UC2-REST.
3+
4+
This module handles camera trigger signals from the firmware ({"cam":1})
5+
to enable software triggering based on hardware events during stage scanning.
6+
"""
7+
8+
import numpy as np
9+
import time
10+
import json
11+
12+
13+
gTIMEOUT = 1 # seconds to wait for a response from the ESP32
14+
15+
16+
class CameraTrigger(object):
17+
"""
18+
This class parses incoming camera trigger signals from the ESP32 firmware.
19+
20+
When the firmware sends {"cam":1}, this module triggers registered callbacks
21+
which can be used for software-triggered image acquisition during stage scanning.
22+
23+
Example usage:
24+
import uc2rest
25+
26+
ESP32 = uc2rest.UC2Client(serialport=port, baudrate=500000)
27+
28+
# Register callback for camera trigger
29+
def my_camera_callback(data):
30+
print(f"Camera trigger received: {data}")
31+
# Trigger image acquisition here
32+
33+
ESP32.camera_trigger.register_callback(0, my_camera_callback)
34+
"""
35+
36+
def __init__(self, parent=None, nCallbacks=10):
37+
"""
38+
Initialize camera trigger handler.
39+
40+
Args:
41+
parent: Parent UC2Client instance
42+
nCallbacks: Maximum number of callback functions to register
43+
"""
44+
self._parent = parent
45+
self.nCallbacks = nCallbacks
46+
47+
# Track trigger count for diagnostics
48+
self._trigger_count = 0
49+
self._last_trigger_time = None
50+
51+
# Initialize callback functions
52+
self._callbackPerKey = {}
53+
self.init_callback_functions(self.nCallbacks)
54+
55+
# Register callback for camera trigger on serial loop
56+
if hasattr(self._parent, "serial"):
57+
self._parent.serial.register_callback(self._callback_camera_trigger, pattern="cam")
58+
59+
def _callback_camera_trigger(self, data):
60+
"""
61+
Parse camera trigger message from firmware.
62+
63+
Expected JSON format:
64+
{
65+
"cam": 1 # Trigger signal
66+
}
67+
68+
or with additional data:
69+
{
70+
"cam": {
71+
"trigger": 1,
72+
"frame_id": 123,
73+
"illumination": 0
74+
}
75+
}
76+
77+
Args:
78+
data: JSON data dictionary from firmware
79+
"""
80+
try:
81+
# Update trigger statistics
82+
self._trigger_count += 1
83+
self._last_trigger_time = time.time()
84+
85+
# Extract trigger information
86+
cam_data = data.get("cam", {})
87+
88+
# Handle simple trigger ({"cam": 1})
89+
if isinstance(cam_data, (int, float)):
90+
trigger_info = {
91+
"trigger": int(cam_data),
92+
"frame_id": self._trigger_count,
93+
"timestamp": self._last_trigger_time
94+
}
95+
self._parent.logger.debug(f"Camera trigger received: {trigger_info}")
96+
else:
97+
# Handle extended trigger data
98+
trigger_info = {
99+
"trigger": cam_data.get("trigger", 1),
100+
"frame_id": cam_data.get("frame_id", self._trigger_count),
101+
"illumination": cam_data.get("illumination", -1),
102+
"timestamp": self._last_trigger_time
103+
}
104+
self._parent.logger.debug(f"Camera trigger with data received: {trigger_info}")
105+
106+
# Call all registered callbacks
107+
for key, callback in self._callbackPerKey.items():
108+
if callback is not None and callable(callback):
109+
try:
110+
callback(trigger_info)
111+
except Exception as callback_error:
112+
print(f"Error in camera trigger callback {key}: {callback_error}")
113+
114+
except Exception as e:
115+
print(f"Error in _callback_camera_trigger: {e}")
116+
117+
def init_callback_functions(self, nCallbacks=10):
118+
"""
119+
Initialize callback function dictionary.
120+
121+
Args:
122+
nCallbacks: Number of callback slots to create
123+
"""
124+
self._callbackPerKey = {}
125+
self.nCallbacks = nCallbacks
126+
for i in range(nCallbacks):
127+
self._callbackPerKey[i] = None
128+
129+
def register_callback(self, key, callback):
130+
"""
131+
Register a callback function for camera trigger events.
132+
133+
Args:
134+
key: Integer key (0 to nCallbacks-1) for this callback
135+
callback: Function to call when trigger is received.
136+
Function signature: callback(trigger_info: dict)
137+
138+
Example:
139+
def on_camera_trigger(info):
140+
print(f"Frame {info['frame_id']} triggered at {info['timestamp']}")
141+
camera.snap_image()
142+
143+
ESP32.camera_trigger.register_callback(0, on_camera_trigger)
144+
"""
145+
if key < 0 or key >= self.nCallbacks:
146+
raise ValueError(f"Callback key must be between 0 and {self.nCallbacks-1}")
147+
self._callbackPerKey[key] = callback
148+
149+
def unregister_callback(self, key):
150+
"""
151+
Remove a registered callback.
152+
153+
Args:
154+
key: Integer key of callback to remove
155+
"""
156+
if key in self._callbackPerKey:
157+
self._callbackPerKey[key] = None
158+
159+
def clear_all_callbacks(self):
160+
"""Remove all registered callbacks."""
161+
self.init_callback_functions(self.nCallbacks)
162+
163+
def get_trigger_count(self):
164+
"""
165+
Get the total number of triggers received since initialization.
166+
167+
Returns:
168+
int: Number of camera triggers received
169+
"""
170+
return self._trigger_count
171+
172+
def get_last_trigger_time(self):
173+
"""
174+
Get the timestamp of the last trigger received.
175+
176+
Returns:
177+
float or None: Unix timestamp of last trigger, or None if no triggers received
178+
"""
179+
return self._last_trigger_time
180+
181+
def reset_trigger_count(self):
182+
"""Reset the trigger counter to zero."""
183+
self._trigger_count = 0
184+
self._last_trigger_time = None
185+
186+
def get_trigger_stats(self):
187+
"""
188+
Get statistics about trigger events.
189+
190+
Returns:
191+
dict: Dictionary with trigger statistics
192+
"""
193+
return {
194+
"total_triggers": self._trigger_count,
195+
"last_trigger_time": self._last_trigger_time,
196+
"callbacks_registered": sum(1 for cb in self._callbackPerKey.values() if cb is not None)
197+
}

uc2rest/motor.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -950,8 +950,8 @@ def stop_stage_scanning(self):
950950

951951

952952

953-
def start_stage_scanning(self, xstart=0, xstep=1000, nx=20, ystart=0, ystep=1000, ny=10, tsettle=5, tExposure=50, illumination=(0,0,0,0), led=0, speed=20000, acceleration=None):
954-
# {"task": "/motor_act", "stagescan": {"xStart": 0, "yStart": 0, "xStep": 500, "yStep": 500, "nX": 10, "nY": 10, "tPre": 50, "tPost": 50, "illumination": [0, 1, 0, 0], "led": 255}}
953+
def start_stage_scanning(self, xstart=0, xstep=1000, nx=20, ystart=0, ystep=1000, ny=10, zstart=0, zstep=1000, nz=10, tsettle=5, tExposure=50, illumination=(0,0,0,0), led=0, speed=20000, acceleration=None):
954+
# {"task": "/motor_act", "stagescan": {"xStart": 0, "yStart": 0, "zStart": 0, "xStep": 500, "yStep": 500, "zStep": 500, "nX": 10, "nY": 10, "nZ": 10, "tPre": 50, "tPost": 50, "illumination": [0, 1, 0, 0], "led": 255}}
955955
if acceleration is None:
956956
acceleration = self.DEFAULT_ACCELERATION
957957
path = "/motor_act"
@@ -970,16 +970,19 @@ def start_stage_scanning(self, xstart=0, xstep=1000, nx=20, ystart=0, ystep=1000
970970
"led": led,
971971
"accel": self.DEFAULT_ACCELERATION, # default acceleration
972972
"speed": speed, # default speed
973+
"zStart": zstart / self.stepSizeZ,
974+
"zStep": zstep / self.stepSizeZ,
975+
"nZ": nz,
973976
}
974977
}
975978
r = self._parent.post_json(path, payload)
976979
return r
977980

978981
def start_stage_scanning_by_coordinates(self, coordinates, tPre=50, tPost=50, led=100, illumination=[50, 75, 100, 125], stopped=0):
979982
'''
980-
Example: {"task": "/motor_act", "stagescan": {"coordinates": [{"x": 100, "y": 200}, {"x": 300, "y": 400}, {"x": 500, "y": 600}], "tPre": 50, "tPost": 50, "led": 100, "illumination": [50, 75, 100, 125], "stopped": 0}}
983+
Example: {"task": "/motor_act", "stagescan": {"coordinates": [{"x": 100, "y": 200, "z": 0}, {"x": 300, "y": 400, "z": 0}, {"x": 500, "y": 600, "z": 0}], "tPre": 50, "tPost": 50, "led": 100, "illumination": [50, 75, 100, 125], "stopped": 0}}
981984
982-
coordinates: list of dictionaries with x and y coordinates
985+
coordinates: list of dictionaries with x, y and z coordinates
983986
tPre: time before exposure in ms
984987
tPost: exposure time - time after action
985988
led: led value for illumination

0 commit comments

Comments
 (0)