From 6fd056cbec5dab89290ea276fb8932955ba3540c Mon Sep 17 00:00:00 2001 From: Ian OHara Date: Mon, 25 Aug 2025 14:56:45 -0700 Subject: [PATCH 1/6] test: fast hardware acqui testing via embedded --- .../main_controller_teensy41.ino | 139 ++++++++------ software/control/_def.py | 1 + software/control/camera_toupcam.py | 1 + software/control/microcontroller.py | 8 + software/squid/abc.py | 1 + .../tools/hardware_acuisition_capture_test.py | 177 ++++++++++++++++++ 6 files changed, 273 insertions(+), 54 deletions(-) create mode 100644 software/tools/hardware_acuisition_capture_test.py diff --git a/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino b/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino index 7433b72bd..71f712ba6 100644 --- a/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino +++ b/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino @@ -68,6 +68,7 @@ static const int SEND_HARDWARE_TRIGGER = 30; static const int SET_STROBE_DELAY = 31; static const int SET_AXIS_DISABLE_ENABLE = 32; static const int SET_PIN_LEVEL = 41; +static const int SET_CONTINUOUS_HARDWARE_TRIGGERING = 42; static const int INITFILTERWHEEL = 253; static const int INITIALIZE = 254; static const int RESET = 255; @@ -572,38 +573,81 @@ void set_illumination_led_matrix(int source, uint8_t r, uint8_t g, uint8_t b) turn_on_illumination(); //update the illumination } +/*********************** Continuous hardware triggering (debug / test setup only!) *************************/ +struct continuous_hardware_triggering_state { + bool enabled; + uint8_t low_ms; + uint8_t high_ms; + + uint32_t last_transition_us; + bool last_was_high; +}; + +static struct continuous_hardware_triggering_state cont_trig_state = { + .enabled = false, + .low_ms = 50, + .high_ms = 50, + + .last_transition_us = 0, + .last_was_high = false +}; + void ISR_strobeTimer() { for (int camera_channel = 0; camera_channel < 6; camera_channel++) { - // strobe pulse - if (control_strobe[camera_channel]) - { - if (illumination_on_time[camera_channel] <= 30000) - { - // if the illumination on time is smaller than 30 ms, use delayMicroseconds to control the pulse length to avoid pulse length jitter - if ( ((micros() - timestamp_trigger_rising_edge[camera_channel]) >= strobe_delay[camera_channel]) && strobe_output_level[camera_channel] == LOW ) - { + uint32_t now_us = micros(); // Capture now once so we don't leak time below. This means we pretend all events below happen at this captured instant. + // If we're in continuous triggering mode, ignore other strobing requests and control the illumination ourselves. + if (cont_trig_state.enabled) { + uint8_t next_level = cont_trig_state.last_was_high ? LOW : HIGH; + bool need_transition = false; + if (next_level == LOW && cont_trig_state.last_transition_us + cont_trig_state.high_ms >= now_us) { + need_transition = true; + } else if (next_level == HIGH && cont_trig_state.last_transition_us + cont_trig_state.low_ms >= now_us) { + need_transition = true; + } + + if (need_transition) { + // If this is a transition to low, we also want to turn on illumination. Otherwise turn it off. + if (next_level == LOW) { turn_on_illumination(); - delayMicroseconds(illumination_on_time[camera_channel]); + } else { turn_off_illumination(); - control_strobe[camera_channel] = false; } + digitalWrite(camera_trigger_pins[camera_channel], next_level); + cont_trig_state.last_was_high = next_level != HIGH; + cont_trig_state.last_transition_us = now_us; } - else + } else { + // strobe pulse + if (control_strobe[camera_channel]) { - // start the strobe - if ( ((micros() - timestamp_trigger_rising_edge[camera_channel]) >= strobe_delay[camera_channel]) && strobe_output_level[camera_channel] == LOW ) + if (illumination_on_time[camera_channel] <= 30000) { - turn_on_illumination(); - strobe_output_level[camera_channel] = HIGH; + // if the illumination on time is smaller than 30 ms, use delayMicroseconds to control the pulse length to avoid pulse length jitter + if ( ((now_us - timestamp_trigger_rising_edge[camera_channel]) >= strobe_delay[camera_channel]) && strobe_output_level[camera_channel] == LOW ) + { + turn_on_illumination(); + delayMicroseconds(illumination_on_time[camera_channel]); + turn_off_illumination(); + control_strobe[camera_channel] = false; + } } - // end the strobe - if (((micros() - timestamp_trigger_rising_edge[camera_channel]) >= strobe_delay[camera_channel] + illumination_on_time[camera_channel]) && strobe_output_level[camera_channel] == HIGH) + else { - turn_off_illumination(); - strobe_output_level[camera_channel] = LOW; - control_strobe[camera_channel] = false; + // start the strobe + if ( ((now_us - timestamp_trigger_rising_edge[camera_channel]) >= strobe_delay[camera_channel]) && strobe_output_level[camera_channel] == LOW ) + { + turn_on_illumination(); + strobe_output_level[camera_channel] = HIGH; + } + // end the strobe + if (((now_us - timestamp_trigger_rising_edge[camera_channel]) >= strobe_delay[camera_channel] + illumination_on_time[camera_channel]) && strobe_output_level[camera_channel] == HIGH) + { + turn_off_illumination(); + strobe_output_level[camera_channel] = LOW; + control_strobe[camera_channel] = false; + } } } } @@ -1602,6 +1646,27 @@ void loop() { digitalWrite(pin, level); break; } + case SET_CONTINUOUS_HARDWARE_TRIGGERING: + { + uint8_t channel = buffer_rx[2]; + bool enabled = buffer_rx[3] > 0; + uint8_t low_ms = buffer_rx[4]; + uint8_t high_ms = buffer_rx[5]; + + noInterrupts(); + cont_trig_state.enabled = enabled; + cont_trig_state.low_ms = low_ms; + cont_trig_state.high_ms = high_ms; + + digitalWrite(camera_trigger_pins[channel], LOW); + trigger_output_level[channel] = LOW; + cont_trig_state.last_transition_us = micros(); + cont_trig_state.last_was_high = false; + // This assumes low is the exposure state. If high is the trigger state then this needs to change. + illumination_on_time[channel] = low_ms; + interrupts(); + + } case CONFIGURE_STAGE_PID: { int axis = buffer_rx[2]; @@ -1759,40 +1824,6 @@ void loop() { digitalWrite(camera_trigger_pins[camera_channel], HIGH); trigger_output_level[camera_channel] = HIGH; } - - /* - // strobe pulse - if(control_strobe[camera_channel]) - { - if(illumination_on_time[camera_channel] <= 30000) - { - // if the illumination on time is smaller than 30 ms, use delayMicroseconds to control the pulse length to avoid pulse length jitter (can be up to 20 us if using the code in the else branch) - if( ((micros()-timestamp_trigger_rising_edge[camera_channel])>=strobe_delay[camera_channel]) && strobe_output_level[camera_channel]==LOW ) - { - turn_on_illumination(); - delayMicroseconds(illumination_on_time[camera_channel]); - turn_off_illumination(); - control_strobe[camera_channel] = false; - } - } - else - { - // start the strobe - if( ((micros()-timestamp_trigger_rising_edge[camera_channel])>=strobe_delay[camera_channel]) && strobe_output_level[camera_channel]==LOW ) - { - turn_on_illumination(); - strobe_output_level[camera_channel] = HIGH; - } - // end the strobe - if(((micros()-timestamp_trigger_rising_edge[camera_channel])>=strobe_delay[camera_channel]+illumination_on_time[camera_channel]) && strobe_output_level[camera_channel]==HIGH) - { - turn_off_illumination(); - strobe_output_level[camera_channel] = LOW; - control_strobe[camera_channel] = false; - } - } - } - */ } // homing - preparing for homing diff --git a/software/control/_def.py b/software/control/_def.py index 5ccc7ddbb..142ba9e41 100644 --- a/software/control/_def.py +++ b/software/control/_def.py @@ -160,6 +160,7 @@ class CMD_SET: SET_STROBE_DELAY = 31 SET_AXIS_DISABLE_ENABLE = 32 SET_PIN_LEVEL = 41 + SET_CONTINUOUS_HARDWARE_TRIGGERING = 42 INITFILTERWHEEL = 253 INITIALIZE = 254 RESET = 255 diff --git a/software/control/camera_toupcam.py b/software/control/camera_toupcam.py index 845e70a30..6886bc967 100644 --- a/software/control/camera_toupcam.py +++ b/software/control/camera_toupcam.py @@ -822,6 +822,7 @@ def _set_acquisition_mode_imp(self, acquisition_mode: CameraAcquisitionMode): trigger_option_value = 1 elif acquisition_mode == CameraAcquisitionMode.HARDWARE_TRIGGER: trigger_option_value = 2 + elif acquisition_mode == CameraAcquisitionMode.LEVEL_TRIGGER: else: raise ValueError(f"Do not know how to handle {acquisition_mode=}") self._camera.put_Option(toupcam.TOUPCAM_OPTION_TRIGGER, trigger_option_value) diff --git a/software/control/microcontroller.py b/software/control/microcontroller.py index 02f47ad76..bac844b67 100644 --- a/software/control/microcontroller.py +++ b/software/control/microcontroller.py @@ -606,6 +606,14 @@ def set_illumination_led_matrix(self, illumination_source, r, g, b): cmd[5] = min(int(b * 255), 255) self.send_command(cmd) + def set_continuous_triggering(self, enabled: bool, low_ms: int, high_ms: int, trigger_output_ch=0): + cmd = bytearray(self.tx_buffer_length) + cmd[1] = CMD_SET.SET_CONTINUOUS_HARDWARE_TRIGGERING + cmd[2] = trigger_output_ch + cmd[3] = 1 if enabled else 0 + cmd[4] = low_ms + cmd[5] = high_ms + def send_hardware_trigger(self, control_illumination=False, illumination_on_time_us=0, trigger_output_ch=0): illumination_on_time_us = int(illumination_on_time_us) cmd = bytearray(self.tx_buffer_length) diff --git a/software/squid/abc.py b/software/squid/abc.py index 54e54a899..2801e7857 100644 --- a/software/squid/abc.py +++ b/software/squid/abc.py @@ -224,6 +224,7 @@ class CameraAcquisitionMode(enum.Enum): SOFTWARE_TRIGGER = "Software Trigger" HARDWARE_TRIGGER = "Hardware Trigger" CONTINUOUS = "Continuous Acquisition" + LEVEL_TRIGGER = "Level Trigger" class CameraFrameFormat(enum.Enum): diff --git a/software/tools/hardware_acuisition_capture_test.py b/software/tools/hardware_acuisition_capture_test.py new file mode 100644 index 000000000..16aa0c3bc --- /dev/null +++ b/software/tools/hardware_acuisition_capture_test.py @@ -0,0 +1,177 @@ +import logging +import threading +import time + +import control.microcontroller +import squid.camera.utils +import squid.config +import squid.logging +from squid.abc import CameraFrame, CameraAcquisitionMode + +log = squid.logging.get_logger("camera stress test") + + +class Stats: + def __init__(self): + self.callback_frame_count = 0 + self.last_callback_frame_time = time.time() + + self.read_frame_count = 0 + self.last_read_frame_time = time.time() + + self.start_time = time.time() + self._update_lock = threading.Lock() + + def start(self): + with self._update_lock: + self.callback_frame_count = 0 + self.last_callback_frame_time = time.time() + + self.read_frame_count = 0 + self.last_read_frame_time = time.time() + + self.start_time = time.time() + + def callback_frame(self): + with self._update_lock: + self.callback_frame_count += 1 + self.last_callback_frame_time = time.time() + + def read_frame(self): + with self._update_lock: + self.read_frame_count += 1 + self.last_read_frame_time = time.time() + + def _summary_line(self, label, count, last_frame): + elapsed = last_frame - self.start_time + return f"{label}: {count} in {elapsed:.3f} [s] ({count / elapsed:.3f} [Hz])\n" + + def report_if_on_interval(self, interval): + if self.read_frame_count % interval == 0: + log.info(self) + + def __str__(self): + return ( + f"Stats (elapsed = {time.time() - self.start_time} [s]:\n" + f" {self._summary_line('callback', self.callback_frame_count, self.last_callback_frame_time)}" + f" {self._summary_line('read frame', self.read_frame_count, self.last_read_frame_time)}" + ) + + +def main(args): + if args.verbose: + squid.logging.set_stdout_log_level(logging.DEBUG) + + microcontroller = control.microcontroller.Microcontroller( + serial_device=control.microcontroller.get_microcontroller_serial_device() + ) + + def hw_trigger(illum_time: float) -> bool: + microcontroller.send_hardware_trigger(False) + + return True + + def strobe_delay_fn(strobe_time_ms: float): + microcontroller.set_strobe_delay_us(int(strobe_time_ms * 1000)) + + return True + + # Special case for simulated camera + if args.camera.lower() == "simulated": + log.info("Using simulated camera!") + camera_type = squid.config.CameraVariant.GXIPY # Not actually used + simulated = True + else: + camera_type = squid.config.CameraVariant.from_string(args.camera) + simulated = False + + if not camera_type: + log.error(f"Invalid camera type '{args.camera}'") + return 1 + + default_config = squid.config.get_camera_config() + force_this_camera_config = default_config.model_copy(update={"camera_type": camera_type}) + + cam = squid.camera.utils.get_camera( + force_this_camera_config, simulated, hw_trigger_fn=hw_trigger, hw_set_strobe_delay_ms_fn=strobe_delay_fn + ) + + stats = Stats() + + def frame_callback(frame: CameraFrame): + stats.callback_frame() + + log.info("Registering frame callback...") + callback_id = cam.add_frame_callback(frame_callback) + + cam.set_exposure_time(args.exposure) + + if software_trigger: + cam.set_acquisition_mode(CameraAcquisitionMode.SOFTWARE_TRIGGER) + elif args.hardware_trigger: + cam.set_acquisition_mode(CameraAcquisitionMode.HARDWARE_TRIGGER) + + log.info("Starting streaming...") + cam.start_streaming() + stats.start() + + end_time = time.time() + args.runtime + + log.info( + ( + f"Camera Info:\n" + f" Type: {args.camera}\n" + f" Resolution: {cam.get_resolution()}\n" + f" Exposure Time: {cam.get_exposure_time()} [ms]\n" + f" Strobe Time: {cam.get_strobe_time()} [ms]\n" + ) + ) + + try: + while time.time() < end_time: + if use_trigger and cam.get_ready_for_trigger(): + log.debug("Sending trigger...") + cam.send_trigger() + log.debug("Trigger sent....") + + read_frame = cam.read_camera_frame() + log.debug(f"Read frame with id={read_frame.frame_id}") + stats.read_frame() + + stats.report_if_on_interval(args.report_interval) + + finally: + log.info("Stopping streaming...") + cam.stop_streaming() + return 0 + + +if __name__ == "__main__": + import argparse + import sys + + ap = argparse.ArgumentParser(description="hammer a camera to test it.") + + ap.add_argument("--runtime", type=float, help="Time, in s, to run the test for.", default=60) + ap.add_argument( + "--continuous", action="store_true", help="Use continuous (internal to cam) triggering, not software trigger." + ) + ap.add_argument( + "--hardware_trigger", + action="store_true", + help="Use the hardware trigger, not software trigger (requires microcontroller)", + ) + ap.add_argument("--exposure", type=float, help="The exposure time in ms", default=1) + ap.add_argument("--report_interval", type=int, help="Report every this many frames captured.", default=100) + ap.add_argument("--verbose", action="store_true", help="Turn on debug logging") + ap.add_argument( + "--camera", + type=str, + required=True, + choices=["hamamatsu", "toupcam", "gxipy", "simulated"], + help="The type of camera to create and use for this test.", + ) + + args = ap.parse_args() + + sys.exit(main(args)) From 98294d3f4f02d41ad90d060a5204a08e42bca6e2 Mon Sep 17 00:00:00 2001 From: Ian OHara Date: Mon, 1 Sep 2025 13:11:13 -0700 Subject: [PATCH 2/6] script for testing continuous capture --- .../main_controller_teensy41.ino | 43 ++++++++++++---- software/control/_def.py | 3 +- software/control/microcontroller.py | 49 ++++++++++++------ .../tools/hardware_acuisition_capture_test.py | 51 ++++++++----------- 4 files changed, 89 insertions(+), 57 deletions(-) diff --git a/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino b/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino index 71f712ba6..dd39bf9e2 100644 --- a/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino +++ b/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino @@ -69,6 +69,7 @@ static const int SET_STROBE_DELAY = 31; static const int SET_AXIS_DISABLE_ENABLE = 32; static const int SET_PIN_LEVEL = 41; static const int SET_CONTINUOUS_HARDWARE_TRIGGERING = 42; +static const int CANCEL_CONTINUOUS_HARDWARE_TRIGGERING = 43; static const int INITFILTERWHEEL = 253; static const int INITIALIZE = 254; static const int RESET = 255; @@ -91,6 +92,7 @@ static const int AXES_XY = 4; static const int AXIS_W = 5; static const int BIT_POS_JOYSTICK_BUTTON = 0; +static const int BIT_POS_CONTINUOUS_TRIGGER_ENABLED = 1; static const int LIM_CODE_X_POSITIVE = 0; static const int LIM_CODE_X_NEGATIVE = 1; @@ -578,20 +580,24 @@ struct continuous_hardware_triggering_state { bool enabled; uint8_t low_ms; uint8_t high_ms; + bool illuminate_on_low; uint32_t last_transition_us; bool last_was_high; }; -static struct continuous_hardware_triggering_state cont_trig_state = { +static struct continuous_hardware_triggering_state DEFAULT_CONTINUOUS_TRIGGERING_STATE = { .enabled = false, .low_ms = 50, .high_ms = 50, + .illuminate_on_low = true, .last_transition_us = 0, .last_was_high = false }; +static struct continuous_hardware_triggering_state cont_trig_state = DEFAULT_CONTINUOUS_TRIGGERING_STATE; + void ISR_strobeTimer() { for (int camera_channel = 0; camera_channel < 6; camera_channel++) @@ -607,9 +613,11 @@ void ISR_strobeTimer() need_transition = true; } + uint8_t illumination_on_level = cont_trig_state.illuminate_on_low ? LOW : HIGH; + if (need_transition) { // If this is a transition to low, we also want to turn on illumination. Otherwise turn it off. - if (next_level == LOW) { + if (next_level == illumination_on_level) { turn_on_illumination(); } else { turn_off_illumination(); @@ -1649,23 +1657,32 @@ void loop() { case SET_CONTINUOUS_HARDWARE_TRIGGERING: { uint8_t channel = buffer_rx[2]; - bool enabled = buffer_rx[3] > 0; - uint8_t low_ms = buffer_rx[4]; - uint8_t high_ms = buffer_rx[5]; + uint8_t low_ms = buffer_rx[3]; + uint8_t high_ms = buffer_rx[4]; + uint8_t illuminate_on_low = buffer_rx[5] > 0; + uint16_t frame_count = (uint32_t)(buffer_rx[6] << 8 | buffer_rx[7]); noInterrupts(); - cont_trig_state.enabled = enabled; + cont_trig_state.enabled = true; cont_trig_state.low_ms = low_ms; cont_trig_state.high_ms = high_ms; + cont_trig_state.illuminate_on_low = illuminate_on_low; digitalWrite(camera_trigger_pins[channel], LOW); trigger_output_level[channel] = LOW; cont_trig_state.last_transition_us = micros(); cont_trig_state.last_was_high = false; - // This assumes low is the exposure state. If high is the trigger state then this needs to change. - illumination_on_time[channel] = low_ms; + long cont_illum_time = 1000 * (illuminate_on_low ? low_ms : high_ms); + illumination_on_time[channel] = cont_illum_time; interrupts(); - + break; + } + case CANCEL_CONTINUOUS_HARDWARE_TRIGGERING: + { + noInterrupts(); + cont_trig_state.enabled = false; + interrupts(); + break; } case CONFIGURE_STAGE_PID: { @@ -1806,6 +1823,7 @@ void loop() { is_preparing_for_homing_Z = false; is_preparing_for_homing_W = false; cmd_id = 0; + cont_trig_state = DEFAULT_CONTINUOUS_TRIGGERING_STATE; break; } default: @@ -2209,8 +2227,11 @@ void loop() { if (joystick_button_pressed && millis() - joystick_button_pressed_timestamp > 1000) joystick_button_pressed = false; - buffer_tx[18] &= ~ (1 << BIT_POS_JOYSTICK_BUTTON); // clear the joystick button bit - buffer_tx[18] = buffer_tx[18] | joystick_button_pressed << BIT_POS_JOYSTICK_BUTTON; + // Entry 18 is a bunch of state bits. We reset them every time, so just clear to 0 here. + buffer_tx[18] = 0; + noInterrupts(); // Hold interrupts for the cont_trigger_state check + buffer_tx[18] = (joystick_button_pressed << BIT_POS_JOYSTICK_BUTTON) | (cont_trigger_state.enabled << BIT_POS_CONTINUOUS_TRIGGER_ENABLED); + interrupts(); // Calculate and fill out the checksum. NOTE: This must be after all other buffer_tx modifications are done! uint8_t checksum = crc8ccitt(buffer_tx, MSG_LENGTH - 1); diff --git a/software/control/_def.py b/software/control/_def.py index 142ba9e41..ca6c7ddda 100644 --- a/software/control/_def.py +++ b/software/control/_def.py @@ -161,6 +161,7 @@ class CMD_SET: SET_AXIS_DISABLE_ENABLE = 32 SET_PIN_LEVEL = 41 SET_CONTINUOUS_HARDWARE_TRIGGERING = 42 + CANCEL_CONTINUOUS_TRIGGERING = 43 INITFILTERWHEEL = 253 INITIALIZE = 254 RESET = 255 @@ -174,7 +175,7 @@ class CMD_SET2: BIT_POS_JOYSTICK_BUTTON = 0 -BIT_POS_SWITCH = 1 +BIT_POS_CONTINUOUS_TRIGGERING = 1 class HOME_OR_ZERO: diff --git a/software/control/microcontroller.py b/software/control/microcontroller.py index bac844b67..5bf474da6 100644 --- a/software/control/microcontroller.py +++ b/software/control/microcontroller.py @@ -130,7 +130,7 @@ def reconnect(self, attempts: int) -> bool: class SimSerial(AbstractCephlaMicroSerial): @staticmethod - def response_bytes_for(command_id, execution_status, x, y, z, theta, joystick_button, switch) -> bytes: + def response_bytes_for(command_id, execution_status, x, y, z, theta, joystick_button, continuous_triggering) -> bytes: """ - command ID (1 byte) - execution status (1 byte) @@ -144,7 +144,7 @@ def response_bytes_for(command_id, execution_status, x, y, z, theta, joystick_bu """ crc_calculator = CrcCalculator(Crc8.CCITT, table_based=True) - button_state = joystick_button << BIT_POS_JOYSTICK_BUTTON | switch << BIT_POS_SWITCH + button_state = joystick_button << BIT_POS_JOYSTICK_BUTTON | continuous_triggering << BIT_POS_CONTINUOUS_TRIGGERING reserved_state = 0 # This is just filler for the 4 reserved bytes. response = bytearray( struct.pack(">BBiiiiBi", command_id, execution_status, x, y, z, theta, button_state, reserved_state) @@ -165,7 +165,7 @@ def __init__(self): self.z = 0 self.theta = 0 self.joystick_button = False - self.switch = False + self.continuous_triggering = False self._closed = False @@ -221,7 +221,7 @@ def _respond_to(self, write_bytes): self.z, self.theta, self.joystick_button, - self.switch, + self.continuous_triggering, ) ) @@ -476,7 +476,7 @@ def __init__(self, serial_device: AbstractCephlaMicroSerial, reset_and_initializ self.z_pos = 0 # unit: microstep or encoder resolution self.w_pos = 0 # unit: microstep or encoder resolution self.theta_pos = 0 # unit: microstep or encoder resolution - self.button_and_switch_state = 0 + self.button_switch_and_other_bits_state = 0 self.joystick_button_pressed = 0 # This is used to keep track of whether or not we should emit joystick events to the joystick listeners, # and can be changed with enable_joystick(...) @@ -491,6 +491,8 @@ def __init__(self, serial_device: AbstractCephlaMicroSerial, reset_and_initializ self.joystick_event_listeners = [] self.switch_state = 0 + self.continuous_triggering_enabled: bool = False + self.last_command = None self.last_command_send_timestamp = time.time() self.last_command_aborted_error = None @@ -606,13 +608,29 @@ def set_illumination_led_matrix(self, illumination_source, r, g, b): cmd[5] = min(int(b * 255), 255) self.send_command(cmd) - def set_continuous_triggering(self, enabled: bool, low_ms: int, high_ms: int, trigger_output_ch=0): + def set_continuous_triggering(self, frame_count: int, low_ms: int, high_ms: int, trigger_output_ch: int = 0, illuminate_on_low: bool = True): + if frame_count <= 0: + raise ValueError(f"frame_count must be >0 but is: {frame_count}") + + if low_ms <= 0 or low_ms > 255: + raise ValueError(f"low_ms must be >0 and <255, but is: {low_ms}") + + if high_ms <= 0 or high_ms > 255: + raise ValueError(f"high_ms must be >0 and <255, but is: {high_ms}") + cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.SET_CONTINUOUS_HARDWARE_TRIGGERING cmd[2] = trigger_output_ch - cmd[3] = 1 if enabled else 0 - cmd[4] = low_ms - cmd[5] = high_ms + cmd[3] = low_ms + cmd[4] = high_ms + cmd[5] = 1 if illuminate_on_low else 0 + payload = self._int_to_payload(frame_count, 2) + cmd[6] = (payload >> 8) & 0xFF + cmd[7] = payload & 0xFF + + def cancel_continuous_triggering(self): + cmd = bytearray(self.tx_buffer_length) + cmd[1] = CMD_SET.CANCEL_CONTINUOUS_TRIGGERING def send_hardware_trigger(self, control_illumination=False, illumination_on_time_us=0, trigger_output_ch=0): illumination_on_time_us = int(illumination_on_time_us) @@ -1118,7 +1136,7 @@ def get_msg_with_good_checksum(): - Y pos (4 bytes) - Z pos (4 bytes) - Theta (4 bytes) - - buttons and switches (1 byte) + - buttons, switches, and other bit fields (1 byte) - reserved (4 bytes) - CRC (1 byte) """ @@ -1169,9 +1187,9 @@ def get_msg_with_good_checksum(): msg[14:18], MicrocontrollerDef.N_BYTES_POS ) # unit: microstep or encoder resolution - self.button_and_switch_state = msg[18] + self.button_switch_and_other_bits_state = msg[18] # joystick button - tmp = self.button_and_switch_state & (1 << BIT_POS_JOYSTICK_BUTTON) + tmp = self.button_switch_and_other_bits_state & (1 << BIT_POS_JOYSTICK_BUTTON) joystick_button_pressed = tmp > 0 if self.joystick_button_pressed != joystick_button_pressed: if self.joystick_listener_events_enabled: @@ -1184,9 +1202,8 @@ def get_msg_with_good_checksum(): self.ack_joystick_button_pressed() self.joystick_button_pressed = joystick_button_pressed - # switch - tmp = self.button_and_switch_state & (1 << BIT_POS_SWITCH) - self.switch_state = tmp > 0 + tmp = self.button_switch_and_other_bits_state & (1 << BIT_POS_CONTINUOUS_TRIGGERING) + self.continuous_triggering_enabled = tmp > 0 with self._received_packet_cv: self._received_packet_cv.notify_all() @@ -1200,7 +1217,7 @@ def get_pos(self): return self.x_pos, self.y_pos, self.z_pos, self.theta_pos def get_button_and_switch_state(self): - return self.button_and_switch_state + return self.button_switch_and_other_bits_state def is_busy(self): return self.mcu_cmd_execution_in_progress diff --git a/software/tools/hardware_acuisition_capture_test.py b/software/tools/hardware_acuisition_capture_test.py index 16aa0c3bc..24461d1c1 100644 --- a/software/tools/hardware_acuisition_capture_test.py +++ b/software/tools/hardware_acuisition_capture_test.py @@ -16,9 +16,6 @@ def __init__(self): self.callback_frame_count = 0 self.last_callback_frame_time = time.time() - self.read_frame_count = 0 - self.last_read_frame_time = time.time() - self.start_time = time.time() self._update_lock = threading.Lock() @@ -37,11 +34,6 @@ def callback_frame(self): self.callback_frame_count += 1 self.last_callback_frame_time = time.time() - def read_frame(self): - with self._update_lock: - self.read_frame_count += 1 - self.last_read_frame_time = time.time() - def _summary_line(self, label, count, last_frame): elapsed = last_frame - self.start_time return f"{label}: {count} in {elapsed:.3f} [s] ({count / elapsed:.3f} [Hz])\n" @@ -54,7 +46,6 @@ def __str__(self): return ( f"Stats (elapsed = {time.time() - self.start_time} [s]:\n" f" {self._summary_line('callback', self.callback_frame_count, self.last_callback_frame_time)}" - f" {self._summary_line('read frame', self.read_frame_count, self.last_read_frame_time)}" ) @@ -102,20 +93,19 @@ def frame_callback(frame: CameraFrame): stats.callback_frame() log.info("Registering frame callback...") - callback_id = cam.add_frame_callback(frame_callback) - + cam.add_frame_callback(frame_callback) cam.set_exposure_time(args.exposure) - if software_trigger: - cam.set_acquisition_mode(CameraAcquisitionMode.SOFTWARE_TRIGGER) - elif args.hardware_trigger: - cam.set_acquisition_mode(CameraAcquisitionMode.HARDWARE_TRIGGER) + + # TODO(imo): When cameras officially support LEVEL_TRIGGER we need to add and implement that in the cameras. For + # now, always use HARDWARE_TRIGGER and figure it out behind the scenes. + cam.set_acquisition_mode(CameraAcquisitionMode.HARDWARE_TRIGGER) log.info("Starting streaming...") cam.start_streaming() stats.start() - end_time = time.time() + args.runtime + end_time = time.time() + args.max_runtime log.info( ( @@ -128,19 +118,26 @@ def frame_callback(frame: CameraFrame): ) try: - while time.time() < end_time: - if use_trigger and cam.get_ready_for_trigger(): + + if args.batch_mode: + frame_time = cam.get_total_frame_time() + high_ms = 1 + low_ms = int(frame_time - high_ms) + + log.debug(f"Sending continuous triggering request to micro for: {args.frame_count=}, {low_ms=} [ms], {high_ms=} [ms]") + microcontroller.set_continuous_triggering(args.frame_count, low_ms, high_ms, 0, False) + while time.time() < end_time and stats.callback_frame_count < args.frame_count: + if not args.batch_mode and cam.get_ready_for_trigger(): log.debug("Sending trigger...") cam.send_trigger() log.debug("Trigger sent....") - read_frame = cam.read_camera_frame() - log.debug(f"Read frame with id={read_frame.frame_id}") - stats.read_frame() - stats.report_if_on_interval(args.report_interval) + time.sleep(0.0001) finally: + if args.batch_mode: + microcontroller.cancel_continuous_triggering() log.info("Stopping streaming...") cam.stop_streaming() return 0 @@ -152,14 +149,9 @@ def frame_callback(frame: CameraFrame): ap = argparse.ArgumentParser(description="hammer a camera to test it.") - ap.add_argument("--runtime", type=float, help="Time, in s, to run the test for.", default=60) - ap.add_argument( - "--continuous", action="store_true", help="Use continuous (internal to cam) triggering, not software trigger." - ) + ap.add_argument("--frame_count", type=float, help="The number of frames to try to capture", default=100) ap.add_argument( - "--hardware_trigger", - action="store_true", - help="Use the hardware trigger, not software trigger (requires microcontroller)", + "--batch_mode", action="store_true", help="Ask the microcontroller to trigger all the frames (in a row) for us." ) ap.add_argument("--exposure", type=float, help="The exposure time in ms", default=1) ap.add_argument("--report_interval", type=int, help="Report every this many frames captured.", default=100) @@ -171,6 +163,7 @@ def frame_callback(frame: CameraFrame): choices=["hamamatsu", "toupcam", "gxipy", "simulated"], help="The type of camera to create and use for this test.", ) + ap.add_argument("--max_runtime", type=float, help="The maximum runtime before timing out.", default=60) args = ap.parse_args() From 900bc0f728c29ff4e4e47ab73143734332377a18 Mon Sep 17 00:00:00 2001 From: Ian OHara Date: Mon, 1 Sep 2025 15:38:49 -0700 Subject: [PATCH 3/6] get basic full setup working --- .../main_controller_teensy41.ino | 101 +++++++++++------- software/control/camera_toupcam.py | 1 - software/control/microcontroller.py | 30 +++--- software/squid/abc.py | 1 - .../tools/hardware_acuisition_capture_test.py | 29 +++-- 5 files changed, 99 insertions(+), 63 deletions(-) diff --git a/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino b/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino index dd39bf9e2..d5f5c0260 100644 --- a/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino +++ b/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino @@ -578,22 +578,24 @@ void set_illumination_led_matrix(int source, uint8_t r, uint8_t g, uint8_t b) /*********************** Continuous hardware triggering (debug / test setup only!) *************************/ struct continuous_hardware_triggering_state { bool enabled; - uint8_t low_ms; - uint8_t high_ms; - bool illuminate_on_low; + uint32_t triggered_us; + uint32_t not_triggered_us; + uint8_t trigger_channel; + uint16_t requested_frames; - uint32_t last_transition_us; - bool last_was_high; + elapsedMicros us_since_last_transition; + uint32_t frames_so_far; }; static struct continuous_hardware_triggering_state DEFAULT_CONTINUOUS_TRIGGERING_STATE = { .enabled = false, - .low_ms = 50, - .high_ms = 50, - .illuminate_on_low = true, + .triggered_us = 50000, + .not_triggered_us = 50000, + .trigger_channel = 0, + .requested_frames = 0, - .last_transition_us = 0, - .last_was_high = false + .us_since_last_transition = 0, + .frames_so_far = 0 }; static struct continuous_hardware_triggering_state cont_trig_state = DEFAULT_CONTINUOUS_TRIGGERING_STATE; @@ -602,29 +604,42 @@ void ISR_strobeTimer() { for (int camera_channel = 0; camera_channel < 6; camera_channel++) { + // NOTE(imo): micros is not safe for general usage. It overflows every ~70 mins. uint32_t now_us = micros(); // Capture now once so we don't leak time below. This means we pretend all events below happen at this captured instant. + // If we're in continuous triggering mode, ignore other strobing requests and control the illumination ourselves. if (cont_trig_state.enabled) { - uint8_t next_level = cont_trig_state.last_was_high ? LOW : HIGH; - bool need_transition = false; - if (next_level == LOW && cont_trig_state.last_transition_us + cont_trig_state.high_ms >= now_us) { - need_transition = true; - } else if (next_level == HIGH && cont_trig_state.last_transition_us + cont_trig_state.low_ms >= now_us) { - need_transition = true; + if (cont_trig_state.trigger_channel != camera_channel) { + continue; } - uint8_t illumination_on_level = cont_trig_state.illuminate_on_low ? LOW : HIGH; + + + uint8_t current_level = trigger_output_level[camera_channel]; + uint8_t next_level = current_level == HIGH ? LOW : HIGH; + + uint32_t this_interval = current_level == HIGH ? cont_trig_state.not_triggered_us : cont_trig_state.triggered_us; + unsigned long elapsed_us = cont_trig_state.us_since_last_transition; + bool need_transition = elapsed_us > (unsigned long)(this_interval); if (need_transition) { // If this is a transition to low, we also want to turn on illumination. Otherwise turn it off. - if (next_level == illumination_on_level) { + // NOTE(imo): LOW corresponds to 5V, HIGH to 0V + if (next_level == LOW) { turn_on_illumination(); + cont_trig_state.frames_so_far++; } else { turn_off_illumination(); } + digitalWrite(camera_trigger_pins[camera_channel], next_level); - cont_trig_state.last_was_high = next_level != HIGH; - cont_trig_state.last_transition_us = now_us; + trigger_output_level[camera_channel] = next_level; + cont_trig_state.us_since_last_transition = 0; + + // We finished our last trigger if we just went down to 0V (HIGH), and we've seen as many frames as requested. + if (cont_trig_state.frames_so_far >= cont_trig_state.requested_frames && next_level == HIGH) { + cont_trig_state.enabled = false; + } } } else { // strobe pulse @@ -1656,23 +1671,24 @@ void loop() { } case SET_CONTINUOUS_HARDWARE_TRIGGERING: { - uint8_t channel = buffer_rx[2]; - uint8_t low_ms = buffer_rx[3]; - uint8_t high_ms = buffer_rx[4]; - uint8_t illuminate_on_low = buffer_rx[5] > 0; - uint16_t frame_count = (uint32_t)(buffer_rx[6] << 8 | buffer_rx[7]); + uint8_t channel = buffer_rx[2] & 0x0f; + uint8_t triggered_ms = buffer_rx[3]; + uint8_t not_triggered_ms = buffer_rx[4]; + uint16_t frame_count = (uint32_t)(buffer_rx[5] << 8 | buffer_rx[6]); noInterrupts(); cont_trig_state.enabled = true; - cont_trig_state.low_ms = low_ms; - cont_trig_state.high_ms = high_ms; - cont_trig_state.illuminate_on_low = illuminate_on_low; - - digitalWrite(camera_trigger_pins[channel], LOW); - trigger_output_level[channel] = LOW; - cont_trig_state.last_transition_us = micros(); - cont_trig_state.last_was_high = false; - long cont_illum_time = 1000 * (illuminate_on_low ? low_ms : high_ms); + cont_trig_state.triggered_us = 1000 * (uint32_t)(6); + cont_trig_state.not_triggered_us = 1000 * (uint32_t)(11); + cont_trig_state.trigger_channel = channel; + cont_trig_state.requested_frames = frame_count; + + // NOTE(imo): HIGH pulls the line down to 0V, LOW lets it float to 5V. + digitalWrite(camera_trigger_pins[channel], HIGH); + cont_trig_state.frames_so_far = 0; + trigger_output_level[channel] = HIGH; + cont_trig_state.us_since_last_transition = 0; + long cont_illum_time = cont_trig_state.triggered_us + cont_trig_state.not_triggered_us; illumination_on_time[channel] = cont_illum_time; interrupts(); break; @@ -1837,10 +1853,15 @@ void loop() { for (int camera_channel = 0; camera_channel < 6; camera_channel++) { // end the trigger pulse - if (trigger_output_level[camera_channel] == LOW && (micros() - timestamp_trigger_rising_edge[camera_channel]) >= TRIGGER_PULSE_LENGTH_us ) - { - digitalWrite(camera_trigger_pins[camera_channel], HIGH); - trigger_output_level[camera_channel] = HIGH; + noInterrupts(); + bool continuous_triggering = cont_trig_state.enabled && cont_trig_state.trigger_channel == camera_channel; + interrupts(); + if (!continuous_triggering) { + if (trigger_output_level[camera_channel] == LOW && (micros() - timestamp_trigger_rising_edge[camera_channel]) >= TRIGGER_PULSE_LENGTH_us ) + { + digitalWrite(camera_trigger_pins[camera_channel], HIGH); + trigger_output_level[camera_channel] = HIGH; + } } } @@ -2229,8 +2250,8 @@ void loop() { // Entry 18 is a bunch of state bits. We reset them every time, so just clear to 0 here. buffer_tx[18] = 0; - noInterrupts(); // Hold interrupts for the cont_trigger_state check - buffer_tx[18] = (joystick_button_pressed << BIT_POS_JOYSTICK_BUTTON) | (cont_trigger_state.enabled << BIT_POS_CONTINUOUS_TRIGGER_ENABLED); + noInterrupts(); // Hold interrupts for the cont_trig_state check + buffer_tx[18] = (joystick_button_pressed << BIT_POS_JOYSTICK_BUTTON) | (cont_trig_state.enabled << BIT_POS_CONTINUOUS_TRIGGER_ENABLED); interrupts(); // Calculate and fill out the checksum. NOTE: This must be after all other buffer_tx modifications are done! diff --git a/software/control/camera_toupcam.py b/software/control/camera_toupcam.py index 6886bc967..845e70a30 100644 --- a/software/control/camera_toupcam.py +++ b/software/control/camera_toupcam.py @@ -822,7 +822,6 @@ def _set_acquisition_mode_imp(self, acquisition_mode: CameraAcquisitionMode): trigger_option_value = 1 elif acquisition_mode == CameraAcquisitionMode.HARDWARE_TRIGGER: trigger_option_value = 2 - elif acquisition_mode == CameraAcquisitionMode.LEVEL_TRIGGER: else: raise ValueError(f"Do not know how to handle {acquisition_mode=}") self._camera.put_Option(toupcam.TOUPCAM_OPTION_TRIGGER, trigger_option_value) diff --git a/software/control/microcontroller.py b/software/control/microcontroller.py index 5bf474da6..e2c423b52 100644 --- a/software/control/microcontroller.py +++ b/software/control/microcontroller.py @@ -608,29 +608,34 @@ def set_illumination_led_matrix(self, illumination_source, r, g, b): cmd[5] = min(int(b * 255), 255) self.send_command(cmd) - def set_continuous_triggering(self, frame_count: int, low_ms: int, high_ms: int, trigger_output_ch: int = 0, illuminate_on_low: bool = True): + def set_continuous_triggering(self, frame_count: int, triggered_ms: int, not_triggered_ms: int, trigger_output_ch: int = 0): if frame_count <= 0: raise ValueError(f"frame_count must be >0 but is: {frame_count}") - if low_ms <= 0 or low_ms > 255: - raise ValueError(f"low_ms must be >0 and <255, but is: {low_ms}") + if triggered_ms <= 0 or triggered_ms > 255: + raise ValueError(f"triggered_ms must be >0 and <255, but is: {triggered_ms}") - if high_ms <= 0 or high_ms > 255: - raise ValueError(f"high_ms must be >0 and <255, but is: {high_ms}") + if not_triggered_ms <= 0 or not_triggered_ms > 255: + raise ValueError(f"not_triggered_ms must be >0 and <255, but is: {not_triggered_ms}") cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.SET_CONTINUOUS_HARDWARE_TRIGGERING - cmd[2] = trigger_output_ch - cmd[3] = low_ms - cmd[4] = high_ms - cmd[5] = 1 if illuminate_on_low else 0 + cmd[2] = trigger_output_ch & 0xFF + cmd[3] = triggered_ms & 0xFF + cmd[4] = not_triggered_ms & 0xFF payload = self._int_to_payload(frame_count, 2) - cmd[6] = (payload >> 8) & 0xFF - cmd[7] = payload & 0xFF + cmd[5] = (payload >> 8) & 0xFF + cmd[6] = payload & 0xFF + + self.log.debug( + f"Sending continuous triggering request to micro for: {frame_count=}, {triggered_ms=} [ms], {not_triggered_ms=} [ms], {trigger_output_ch=}") + self.send_command(cmd) def cancel_continuous_triggering(self): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.CANCEL_CONTINUOUS_TRIGGERING + self.log.debug("Canceling continuous triggering...") + self.send_command(cmd) def send_hardware_trigger(self, control_illumination=False, illumination_on_time_us=0, trigger_output_ch=0): illumination_on_time_us = int(illumination_on_time_us) @@ -1249,7 +1254,8 @@ def _int_to_payload(signed_int, number_of_bytes): payload = signed_int else: payload = 2 ** (8 * number_of_bytes) + signed_int # find two's completement - return payload + return int(payload) + @staticmethod def _payload_to_int(payload, number_of_bytes): diff --git a/software/squid/abc.py b/software/squid/abc.py index 2801e7857..54e54a899 100644 --- a/software/squid/abc.py +++ b/software/squid/abc.py @@ -224,7 +224,6 @@ class CameraAcquisitionMode(enum.Enum): SOFTWARE_TRIGGER = "Software Trigger" HARDWARE_TRIGGER = "Hardware Trigger" CONTINUOUS = "Continuous Acquisition" - LEVEL_TRIGGER = "Level Trigger" class CameraFrameFormat(enum.Enum): diff --git a/software/tools/hardware_acuisition_capture_test.py b/software/tools/hardware_acuisition_capture_test.py index 24461d1c1..02a8225f9 100644 --- a/software/tools/hardware_acuisition_capture_test.py +++ b/software/tools/hardware_acuisition_capture_test.py @@ -1,4 +1,5 @@ import logging +import math import threading import time @@ -15,6 +16,7 @@ class Stats: def __init__(self): self.callback_frame_count = 0 self.last_callback_frame_time = time.time() + self._last_report_frame_count = -1 self.start_time = time.time() self._update_lock = threading.Lock() @@ -39,12 +41,17 @@ def _summary_line(self, label, count, last_frame): return f"{label}: {count} in {elapsed:.3f} [s] ({count / elapsed:.3f} [Hz])\n" def report_if_on_interval(self, interval): - if self.read_frame_count % interval == 0: - log.info(self) + with self._update_lock: + if self.callback_frame_count % interval == 0 and self._last_report_frame_count != self.callback_frame_count: + self._last_report_frame_count = self.callback_frame_count + self.report() + + def report(self): + log.info(self) def __str__(self): return ( - f"Stats (elapsed = {time.time() - self.start_time} [s]:\n" + f"Stats (elapsed = {time.time() - self.start_time} [s]):\n" f" {self._summary_line('callback', self.callback_frame_count, self.last_callback_frame_time)}" ) @@ -101,6 +108,9 @@ def frame_callback(frame: CameraFrame): # now, always use HARDWARE_TRIGGER and figure it out behind the scenes. cam.set_acquisition_mode(CameraAcquisitionMode.HARDWARE_TRIGGER) + # We just want some illumination enabled so we can see if the firmware is doing its job + microcontroller.set_illumination_led_matrix(0, r=0, g=0, b=100) + log.info("Starting streaming...") cam.start_streaming() stats.start() @@ -121,11 +131,10 @@ def frame_callback(frame: CameraFrame): if args.batch_mode: frame_time = cam.get_total_frame_time() - high_ms = 1 - low_ms = int(frame_time - high_ms) + triggered_ms = 10 + not_triggered_ms = int(math.ceil(frame_time) - math.ceil(triggered_ms) + round(args.extra_not_triggered_ms)) - log.debug(f"Sending continuous triggering request to micro for: {args.frame_count=}, {low_ms=} [ms], {high_ms=} [ms]") - microcontroller.set_continuous_triggering(args.frame_count, low_ms, high_ms, 0, False) + microcontroller.set_continuous_triggering(args.frame_count, triggered_ms, not_triggered_ms, 0) while time.time() < end_time and stats.callback_frame_count < args.frame_count: if not args.batch_mode and cam.get_ready_for_trigger(): log.debug("Sending trigger...") @@ -140,7 +149,9 @@ def frame_callback(frame: CameraFrame): microcontroller.cancel_continuous_triggering() log.info("Stopping streaming...") cam.stop_streaming() - return 0 + + stats.report() + return 0 if __name__ == "__main__": @@ -164,7 +175,7 @@ def frame_callback(frame: CameraFrame): help="The type of camera to create and use for this test.", ) ap.add_argument("--max_runtime", type=float, help="The maximum runtime before timing out.", default=60) - + ap.add_argument("--extra_not_triggered_ms", type=float, help="Extra time, in ms, to add between triggers.", default=0) args = ap.parse_args() sys.exit(main(args)) From 95615a3d22e8e72455b452c50c5882be6c6a0862 Mon Sep 17 00:00:00 2001 From: Ian OHara Date: Mon, 1 Sep 2025 16:15:46 -0700 Subject: [PATCH 4/6] finish and formatting --- .../main_controller_teensy41.ino | 25 ++++++++----------- software/control/microcontroller.py | 16 ++++++++---- .../tools/hardware_acuisition_capture_test.py | 5 ++-- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino b/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino index d5f5c0260..8aa06abba 100644 --- a/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino +++ b/firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino @@ -604,32 +604,27 @@ void ISR_strobeTimer() { for (int camera_channel = 0; camera_channel < 6; camera_channel++) { - // NOTE(imo): micros is not safe for general usage. It overflows every ~70 mins. - uint32_t now_us = micros(); // Capture now once so we don't leak time below. This means we pretend all events below happen at this captured instant. - // If we're in continuous triggering mode, ignore other strobing requests and control the illumination ourselves. if (cont_trig_state.enabled) { if (cont_trig_state.trigger_channel != camera_channel) { continue; } - - - uint8_t current_level = trigger_output_level[camera_channel]; + uint8_t current_level = digitalRead(camera_trigger_pins[camera_channel]); uint8_t next_level = current_level == HIGH ? LOW : HIGH; - uint32_t this_interval = current_level == HIGH ? cont_trig_state.not_triggered_us : cont_trig_state.triggered_us; + unsigned long this_interval = current_level == HIGH ? cont_trig_state.not_triggered_us : cont_trig_state.triggered_us; unsigned long elapsed_us = cont_trig_state.us_since_last_transition; - bool need_transition = elapsed_us > (unsigned long)(this_interval); + bool need_transition = elapsed_us >= this_interval; if (need_transition) { // If this is a transition to low, we also want to turn on illumination. Otherwise turn it off. // NOTE(imo): LOW corresponds to 5V, HIGH to 0V if (next_level == LOW) { - turn_on_illumination(); + //turn_on_illumination(); cont_trig_state.frames_so_far++; } else { - turn_off_illumination(); + //turn_off_illumination(); } digitalWrite(camera_trigger_pins[camera_channel], next_level); @@ -648,7 +643,7 @@ void ISR_strobeTimer() if (illumination_on_time[camera_channel] <= 30000) { // if the illumination on time is smaller than 30 ms, use delayMicroseconds to control the pulse length to avoid pulse length jitter - if ( ((now_us - timestamp_trigger_rising_edge[camera_channel]) >= strobe_delay[camera_channel]) && strobe_output_level[camera_channel] == LOW ) + if ( ((micros() - timestamp_trigger_rising_edge[camera_channel]) >= strobe_delay[camera_channel]) && strobe_output_level[camera_channel] == LOW ) { turn_on_illumination(); delayMicroseconds(illumination_on_time[camera_channel]); @@ -659,13 +654,13 @@ void ISR_strobeTimer() else { // start the strobe - if ( ((now_us - timestamp_trigger_rising_edge[camera_channel]) >= strobe_delay[camera_channel]) && strobe_output_level[camera_channel] == LOW ) + if ( ((micros() - timestamp_trigger_rising_edge[camera_channel]) >= strobe_delay[camera_channel]) && strobe_output_level[camera_channel] == LOW ) { turn_on_illumination(); strobe_output_level[camera_channel] = HIGH; } // end the strobe - if (((now_us - timestamp_trigger_rising_edge[camera_channel]) >= strobe_delay[camera_channel] + illumination_on_time[camera_channel]) && strobe_output_level[camera_channel] == HIGH) + if (((micros() - timestamp_trigger_rising_edge[camera_channel]) >= strobe_delay[camera_channel] + illumination_on_time[camera_channel]) && strobe_output_level[camera_channel] == HIGH) { turn_off_illumination(); strobe_output_level[camera_channel] = LOW; @@ -1678,8 +1673,8 @@ void loop() { noInterrupts(); cont_trig_state.enabled = true; - cont_trig_state.triggered_us = 1000 * (uint32_t)(6); - cont_trig_state.not_triggered_us = 1000 * (uint32_t)(11); + cont_trig_state.triggered_us = 1000 * (uint32_t)(triggered_ms); + cont_trig_state.not_triggered_us = 1000 * (uint32_t)(not_triggered_ms); cont_trig_state.trigger_channel = channel; cont_trig_state.requested_frames = frame_count; diff --git a/software/control/microcontroller.py b/software/control/microcontroller.py index e2c423b52..d77e6aa86 100644 --- a/software/control/microcontroller.py +++ b/software/control/microcontroller.py @@ -130,7 +130,9 @@ def reconnect(self, attempts: int) -> bool: class SimSerial(AbstractCephlaMicroSerial): @staticmethod - def response_bytes_for(command_id, execution_status, x, y, z, theta, joystick_button, continuous_triggering) -> bytes: + def response_bytes_for( + command_id, execution_status, x, y, z, theta, joystick_button, continuous_triggering + ) -> bytes: """ - command ID (1 byte) - execution status (1 byte) @@ -144,7 +146,9 @@ def response_bytes_for(command_id, execution_status, x, y, z, theta, joystick_bu """ crc_calculator = CrcCalculator(Crc8.CCITT, table_based=True) - button_state = joystick_button << BIT_POS_JOYSTICK_BUTTON | continuous_triggering << BIT_POS_CONTINUOUS_TRIGGERING + button_state = ( + joystick_button << BIT_POS_JOYSTICK_BUTTON | continuous_triggering << BIT_POS_CONTINUOUS_TRIGGERING + ) reserved_state = 0 # This is just filler for the 4 reserved bytes. response = bytearray( struct.pack(">BBiiiiBi", command_id, execution_status, x, y, z, theta, button_state, reserved_state) @@ -608,7 +612,9 @@ def set_illumination_led_matrix(self, illumination_source, r, g, b): cmd[5] = min(int(b * 255), 255) self.send_command(cmd) - def set_continuous_triggering(self, frame_count: int, triggered_ms: int, not_triggered_ms: int, trigger_output_ch: int = 0): + def set_continuous_triggering( + self, frame_count: int, triggered_ms: int, not_triggered_ms: int, trigger_output_ch: int = 0 + ): if frame_count <= 0: raise ValueError(f"frame_count must be >0 but is: {frame_count}") @@ -628,7 +634,8 @@ def set_continuous_triggering(self, frame_count: int, triggered_ms: int, not_tri cmd[6] = payload & 0xFF self.log.debug( - f"Sending continuous triggering request to micro for: {frame_count=}, {triggered_ms=} [ms], {not_triggered_ms=} [ms], {trigger_output_ch=}") + f"Sending continuous triggering request to micro for: {frame_count=}, {triggered_ms=} [ms], {not_triggered_ms=} [ms], {trigger_output_ch=}" + ) self.send_command(cmd) def cancel_continuous_triggering(self): @@ -1256,7 +1263,6 @@ def _int_to_payload(signed_int, number_of_bytes): payload = 2 ** (8 * number_of_bytes) + signed_int # find two's completement return int(payload) - @staticmethod def _payload_to_int(payload, number_of_bytes): signed = 0 diff --git a/software/tools/hardware_acuisition_capture_test.py b/software/tools/hardware_acuisition_capture_test.py index 02a8225f9..e18374aa1 100644 --- a/software/tools/hardware_acuisition_capture_test.py +++ b/software/tools/hardware_acuisition_capture_test.py @@ -103,7 +103,6 @@ def frame_callback(frame: CameraFrame): cam.add_frame_callback(frame_callback) cam.set_exposure_time(args.exposure) - # TODO(imo): When cameras officially support LEVEL_TRIGGER we need to add and implement that in the cameras. For # now, always use HARDWARE_TRIGGER and figure it out behind the scenes. cam.set_acquisition_mode(CameraAcquisitionMode.HARDWARE_TRIGGER) @@ -175,7 +174,9 @@ def frame_callback(frame: CameraFrame): help="The type of camera to create and use for this test.", ) ap.add_argument("--max_runtime", type=float, help="The maximum runtime before timing out.", default=60) - ap.add_argument("--extra_not_triggered_ms", type=float, help="Extra time, in ms, to add between triggers.", default=0) + ap.add_argument( + "--extra_not_triggered_ms", type=float, help="Extra time, in ms, to add between triggers.", default=0 + ) args = ap.parse_args() sys.exit(main(args)) From 291886c1a2ac580110a406eee193e11aaad46d53 Mon Sep 17 00:00:00 2001 From: Ian OHara Date: Mon, 1 Sep 2025 16:20:18 -0700 Subject: [PATCH 5/6] fix mispelling --- ...ition_capture_test.py => hardware_acquisition_capture_test.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename software/tools/{hardware_acuisition_capture_test.py => hardware_acquisition_capture_test.py} (100%) diff --git a/software/tools/hardware_acuisition_capture_test.py b/software/tools/hardware_acquisition_capture_test.py similarity index 100% rename from software/tools/hardware_acuisition_capture_test.py rename to software/tools/hardware_acquisition_capture_test.py From 57b405b041371bcfe8e61c4689f48d0f7c6f4df0 Mon Sep 17 00:00:00 2001 From: Ian OHara Date: Mon, 1 Sep 2025 16:28:10 -0700 Subject: [PATCH 6/6] allow specifying trigger ms --- software/tools/hardware_acquisition_capture_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/software/tools/hardware_acquisition_capture_test.py b/software/tools/hardware_acquisition_capture_test.py index e18374aa1..654628323 100644 --- a/software/tools/hardware_acquisition_capture_test.py +++ b/software/tools/hardware_acquisition_capture_test.py @@ -130,7 +130,7 @@ def frame_callback(frame: CameraFrame): if args.batch_mode: frame_time = cam.get_total_frame_time() - triggered_ms = 10 + triggered_ms = args.trigger_ms not_triggered_ms = int(math.ceil(frame_time) - math.ceil(triggered_ms) + round(args.extra_not_triggered_ms)) microcontroller.set_continuous_triggering(args.frame_count, triggered_ms, not_triggered_ms, 0) @@ -175,8 +175,9 @@ def frame_callback(frame: CameraFrame): ) ap.add_argument("--max_runtime", type=float, help="The maximum runtime before timing out.", default=60) ap.add_argument( - "--extra_not_triggered_ms", type=float, help="Extra time, in ms, to add between triggers.", default=0 + "--extra_not_triggered_ms", type=int, help="Extra time, in ms, to add between triggers.", default=0 ) + ap.add_argument("--trigger_ms", type=int, help="The time to spend in trigger state", default=1) args = ap.parse_args() sys.exit(main(args))