fixed app to use new thorlabs driver

This commit is contained in:
Thomas Ales [M S E]
2026-02-09 16:28:47 -06:00
parent 23f6331ba2
commit 57613b7aca
12 changed files with 1474 additions and 3333 deletions
+139 -93
View File
@@ -21,7 +21,7 @@ from hardware.helios_laser import HeliosLaser, PulseMode
from hardware.uc480_camera import UC480Camera, CameraStreamThread
# Import stage controller and motion worker
from hardware.bbd202 import MotionController
from hardware.pybbd202 import ThorlabsServoDriver, AXIS_X, AXIS_Y, TriggerBitsServo
from scanengine.motion_worker import MotionWorker
# Import scan planning tool
@@ -54,33 +54,7 @@ class ScanWorker(QtCore.QObject):
@QtCore.pyqtSlot()
def run_scan(self):
"""
Execute the full scanning process.
TODO: Implement your own motion control logic here.
Available data:
self.scan_params - dict containing:
'scan_boxes' - list of scan box dicts with:
'angle_degrees' - rotation angle
'start' - (x_start, y_start) in mm
'end' - (x_end, y_end) in mm
'row_spacing' - spacing between rows in mm
self.motion_worker.controller - MotionController instance (if connected)
self.should_stop - set to True when user requests abort
Signals to emit:
self.scan_started.emit() - at start
self.angle_started.emit(angle_idx, total_angles) - when starting each angle
self.line_started.emit(line_idx, total_lines, y_pos) - when starting each line
self.current_progress.emit(percent) - progress for current angle
self.overall_progress.emit(percent) - overall progress
self.status_message.emit(message) - status updates
self.scan_completed.emit() - on successful completion
self.scan_failed.emit(error_msg) - on failure
"""
"""Execute the full scanning process."""
# Pause motion worker polling during scan to avoid conflicts
if self.motion_worker:
self.motion_worker.scanning_active = True
@@ -88,40 +62,114 @@ class ScanWorker(QtCore.QObject):
try:
self.scan_started.emit()
# Extract parameters
scan_boxes = self.scan_params.get('scan_boxes', [])
num_angles = len(scan_boxes)
row_spacing = self.scan_params.get('row_spacing', 0.1)
scan_velocity = self.scan_params.get('scan_velocity_mm_s', 200.0)
scan_accel = self.scan_params.get('scan_acceleration_mm_s2', 500.0)
print(f"Scan worker: Starting scan with {num_angles} angles")
print(f"Row spacing: {row_spacing} mm")
print(f"Row spacing: {row_spacing} mm, velocity: {scan_velocity} mm/s")
controller = self.motion_worker.controller if self.motion_worker else None
if not controller:
self.scan_failed.emit("No motion controller connected")
return
# TODO: Implement your motion control logic here
#
# For each angle in scan_boxes:
# - Move to start position (x_start, y_start)
# - For each row from y_start to y_end with row_spacing:
# - Move X from x_start to x_end (flying scan)
# - Move to next row (X back to x_start, Y to next row)
#
# The MotionController provides these methods:
# controller.move_to_fast(x=mm, y=mm) - send move commands
# controller.get_position(dest, timeout) - get current position
# controller.poll_until_idle(tolerance, timeout) - poll until settled
# controller.set_velocity_params(dest, min_vel, accel, max_vel)
# controller.start_update_messages() - enable status updates
# etc.
# Store original velocity params for restoration
orig_x_velocity = controller.max_velocities[0]
orig_x_accel = controller.max_accels[0]
orig_y_velocity = controller.max_velocities[1]
orig_y_accel = controller.max_accels[1]
self.scan_failed.emit("Motion control not implemented - please implement run_scan()")
# Configure stage for high-speed scanning
controller.set_velocity_params(AXIS_X, max_velocity=scan_velocity, acceleration=scan_accel)
controller.set_velocity_params(AXIS_Y, max_velocity=scan_velocity, acceleration=scan_accel)
# Set X-axis trigger output HIGH during motion (for oscilloscope sync)
controller.set_trigger(AXIS_X, TriggerBitsServo.TRIGOUT_INMOTION)
self.status_message.emit("Scan configured, starting raster...")
# Main scan loop
for angle_idx, scan_box in enumerate(scan_boxes):
if self.should_stop:
self.scan_failed.emit("Scan aborted by user")
return
self.angle_started.emit(angle_idx, num_angles)
angle_deg = scan_box.get('angle_degrees', 0)
self.status_message.emit(f"Angle {angle_idx + 1}/{num_angles} ({angle_deg:.1f} deg)")
# Extract scan area boundaries
x_start, y_start = scan_box['start']
x_end, y_end = scan_box['end']
# Calculate scan lines
y_range = y_end - y_start
num_lines = max(1, int(y_range / row_spacing) + 1)
# Move to start position only for first angle
if angle_idx == 0:
self.status_message.emit("Moving to scan start position...")
controller.move_axis_absolute(AXIS_X, x_start, timeout=30.0)
controller.move_axis_absolute(AXIS_Y, y_start, timeout=30.0)
# Scan each line (snake/boustrophedon pattern)
for line_idx in range(num_lines):
if self.should_stop:
self.scan_failed.emit("Scan aborted by user")
return
y_current = y_start + line_idx * row_spacing
if y_current > y_end:
y_current = y_end
# Alternate scan direction for snake pattern
if line_idx % 2 == 0:
scan_start_x, scan_end_x = x_start, x_end
else:
scan_start_x, scan_end_x = x_end, x_start
self.line_started.emit(line_idx, num_lines, y_current)
# Position to line start (diagonal move - X and Y simultaneously)
# Y move issued first (non-blocking from stage perspective),
# then X move; both complete before scan line begins
controller.move_axis_absolute(AXIS_Y, y_current, timeout=20.0)
controller.move_axis_absolute(AXIS_X, scan_start_x, timeout=20.0)
# Perform scan line (trigger is HIGH during this move)
scan_distance = abs(scan_end_x - scan_start_x)
scan_timeout = max(10.0, scan_distance / scan_velocity * 3)
controller.move_axis_absolute(AXIS_X, scan_end_x, timeout=scan_timeout)
# Update progress
line_progress = int(100 * (line_idx + 1) / num_lines)
self.current_progress.emit(line_progress)
overall = int(100 * (angle_idx + (line_idx + 1) / num_lines) / num_angles)
self.overall_progress.emit(overall)
# Cleanup: disable triggers and restore original velocity
controller.set_trigger(AXIS_X, 0)
controller.set_velocity_params(AXIS_X, max_velocity=orig_x_velocity, acceleration=orig_x_accel)
controller.set_velocity_params(AXIS_Y, max_velocity=orig_y_velocity, acceleration=orig_y_accel)
self.status_message.emit("Scan complete")
self.scan_completed.emit()
except Exception as e:
print(f"ERROR in scan worker: {e}")
import traceback
traceback.print_exc()
# Best-effort cleanup on error
try:
if self.motion_worker and self.motion_worker.controller:
self.motion_worker.controller.set_trigger(AXIS_X, 0)
except Exception:
pass
self.scan_failed.emit(str(e))
finally:
# Re-enable motion worker polling
@@ -2395,8 +2443,8 @@ class ScanVisualizationDialog(QtWidgets.QDialog):
self.current_scan_label.setText("Checking home status...")
# Get home status for both axes
x_homed = controller.is_homed_x
y_homed = controller.is_homed_y
x_homed = controller.am_homed[0]
y_homed = controller.am_homed[1]
print(f"Home status - X: {x_homed}, Y: {y_homed}")
@@ -2430,26 +2478,25 @@ class ScanVisualizationDialog(QtWidgets.QDialog):
QtWidgets.QApplication.processEvents()
try:
success = controller.home_axis(controller.DEST_X_AXIS, timeout=60.0)
if success:
print("X-axis homed successfully")
self.current_scan_progress.setValue(50)
controller.home_axis(AXIS_X, timeout=60.0)
print("X-axis homed successfully")
self.current_scan_progress.setValue(50)
# Small delay to let system stabilize
import time
time.sleep(0.5)
else:
print("ERROR: X-axis homing timeout")
QtWidgets.QMessageBox.critical(
self,
"Homing Error",
"X-axis homing timed out after 60 seconds.\n\n"
"Please check:\n"
"• Stage can move freely\n"
"• No obstructions\n"
"• Stage is connected properly"
)
return False
# Small delay to let system stabilize
import time
time.sleep(0.5)
except TimeoutError:
print("ERROR: X-axis homing timeout")
QtWidgets.QMessageBox.critical(
self,
"Homing Error",
"X-axis homing timed out after 60 seconds.\n\n"
"Please check:\n"
"• Stage can move freely\n"
"• No obstructions\n"
"• Stage is connected properly"
)
return False
except Exception as e:
print(f"ERROR: Failed to home X-axis: {e}")
QtWidgets.QMessageBox.critical(
@@ -2467,26 +2514,25 @@ class ScanVisualizationDialog(QtWidgets.QDialog):
QtWidgets.QApplication.processEvents()
try:
success = controller.home_axis(controller.DEST_Y_AXIS, timeout=60.0)
if success:
print("Y-axis homed successfully")
self.current_scan_progress.setValue(90)
controller.home_axis(AXIS_Y, timeout=60.0)
print("Y-axis homed successfully")
self.current_scan_progress.setValue(90)
# Small delay to let system stabilize
import time
time.sleep(0.5)
else:
print("ERROR: Y-axis homing timeout")
QtWidgets.QMessageBox.critical(
self,
"Homing Error",
"Y-axis homing timed out after 60 seconds.\n\n"
"Please check:\n"
"• Stage can move freely\n"
"• No obstructions\n"
"• Stage is connected properly"
)
return False
# Small delay to let system stabilize
import time
time.sleep(0.5)
except TimeoutError:
print("ERROR: Y-axis homing timeout")
QtWidgets.QMessageBox.critical(
self,
"Homing Error",
"Y-axis homing timed out after 60 seconds.\n\n"
"Please check:\n"
"• Stage can move freely\n"
"• No obstructions\n"
"• Stage is connected properly"
)
return False
except Exception as e:
print(f"ERROR: Failed to home Y-axis: {e}")
QtWidgets.QMessageBox.critical(
@@ -2689,16 +2735,16 @@ class OptionsDialog(QtWidgets.QDialog):
def populate_trigger_modes(self):
"""Populate the trigger mode combo boxes with available options"""
from hardware.bbd202 import TriggerMode
from hardware.pybbd202 import TriggerBitsServo
trigger_options = [
("Disabled", TriggerMode.DISABLED),
("In/Out Relative Move", TriggerMode.IN_OUT_RELATIVE_MOVE),
("In/Out Absolute Move", TriggerMode.IN_OUT_ABSOLUTE_MOVE),
("In/Out Home", TriggerMode.IN_OUT_HOME),
("In/Out Stop", TriggerMode.IN_OUT_STOP),
("Out Only (HIGH during motion)", TriggerMode.OUT_ONLY),
("Out Position", TriggerMode.OUT_POSITION),
("Disabled", 0),
("Trigger In: Relative Move", TriggerBitsServo.TRIGIN_RELMOVE),
("Trigger In: Absolute Move", TriggerBitsServo.TRIGIN_ABSMOVE),
("Trigger In: Home", TriggerBitsServo.TRIGIN_HOMEMOVE),
("Trigger Out: In Motion", TriggerBitsServo.TRIGOUT_INMOTION),
("Trigger Out: Motion Complete", TriggerBitsServo.TRIGOUT_MOTIONCOMPLETE),
("Trigger Out: Max Velocity", TriggerBitsServo.TRIGOUT_MAXVELOCITY),
]
for label, mode in trigger_options: