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
+58 -89
View File
@@ -6,7 +6,7 @@ Provides async command queueing and position updates via Qt signals.
"""
from PyQt6 import QtCore
from hardware.bbd202 import MotionController
from hardware.pybbd202 import ThorlabsServoDriver, AXIS_X, AXIS_Y
import queue
import time
from typing import Optional, Dict, Any
@@ -45,7 +45,7 @@ class MotionWorker(QtCore.QObject):
def __init__(self):
super().__init__()
self.controller: Optional[MotionController] = None
self.controller: Optional[ThorlabsServoDriver] = None
self.is_connected = False
self.command_queue = queue.Queue()
self.running = True
@@ -65,9 +65,9 @@ class MotionWorker(QtCore.QObject):
self.last_x_moving = None
self.last_y_moving = None
# Position update throttling (active requests can be slow)
# Position update throttling
self.last_position_update_time = 0
self.position_update_interval = 0.2 # seconds between position requests
self.position_update_interval = 0.2 # seconds between position reads
# Flag to pause polling during scanning (scan worker handles its own position queries)
self.scanning_active = False
@@ -133,27 +133,25 @@ class MotionWorker(QtCore.QObject):
def do_connect(self):
"""Connect to the motion controller"""
try:
self.controller = MotionController()
self.controller.connect() # ACKs are sent reactively when enable_updates=True
self.controller = ThorlabsServoDriver()
self.controller.connect()
# Enable channels
self.controller.set_channel_enable_state(self.controller.DEST_X_AXIS, True)
self.controller.set_channel_enable_state(self.controller.DEST_Y_AXIS, True)
self.controller.enable_axis(AXIS_X)
self.controller.enable_axis(AXIS_Y)
# Request status updates for both axes to populate status bits (including homed state)
# This is necessary because status bits are not sent automatically after connect
self.controller.request_status_update(self.controller.DEST_X_AXIS)
self.controller.request_status_update(self.controller.DEST_Y_AXIS)
# Wait a moment for the asynchronous status update responses
time.sleep(0.2)
# Start polling to populate cached state (positions, homed, moving, errors)
self.controller.start_polling(interval=0.2)
# Wait for first polling cycle to populate status
time.sleep(0.3)
# Set initial velocity parameters
for dest in [self.controller.DEST_X_AXIS, self.controller.DEST_Y_AXIS]:
for dest in [AXIS_X, AXIS_Y]:
self.controller.set_velocity_params(
dest,
min_velocity=0.0,
acceleration=self.acceleration,
max_velocity=self.jog_speed
max_velocity=self.jog_speed,
acceleration=self.acceleration
)
self.is_connected = True
@@ -188,29 +186,25 @@ class MotionWorker(QtCore.QObject):
return
try:
# Determine destination
dest = self.controller.DEST_X_AXIS if axis == 'x' else self.controller.DEST_Y_AXIS
dest = AXIS_X if axis == 'x' else AXIS_Y
# Calculate relative distance
distance = self.step_size * direction
# Set relative move parameters
self.controller.set_move_rel_params(dest, distance)
# Execute the move (non-blocking - we don't wait for completion)
# Use a very short timeout since we're doing continuous jogging
self.controller.move_relative(dest, timeout=0.5)
# Execute the move (blocking, with short timeout for continuous jogging)
self.controller.move_axis_relative(dest, distance, timeout=0.5)
# Update position
self.update_position()
self.move_completed.emit(axis)
except TimeoutError:
# Timeout is expected during continuous jog - don't report as error
pass
except Exception as e:
# Don't emit errors for timeout - that's expected during continuous jog
if "timeout" not in str(e).lower():
print(f"Jog error: {e}")
self.error_occurred.emit(f"Jog failed: {str(e)}")
print(f"Jog error: {e}")
self.error_occurred.emit(f"Jog failed: {str(e)}")
def do_home(self, axis: str):
"""Home an axis"""
@@ -218,7 +212,7 @@ class MotionWorker(QtCore.QObject):
return
try:
dest = self.controller.DEST_X_AXIS if axis == 'x' else self.controller.DEST_Y_AXIS
dest = AXIS_X if axis == 'x' else AXIS_Y
print(f"Homing {axis.upper()} axis...")
self.controller.home_axis(dest, timeout=20.0)
@@ -229,6 +223,9 @@ class MotionWorker(QtCore.QObject):
print(f"{axis.upper()} axis homed successfully")
except TimeoutError:
print(f"Home timeout: {axis.upper()} axis")
self.error_occurred.emit(f"Homing {axis.upper()} timed out")
except Exception as e:
print(f"Home error: {e}")
self.error_occurred.emit(f"Homing {axis.upper()} failed: {str(e)}")
@@ -244,12 +241,11 @@ class MotionWorker(QtCore.QObject):
self.jog_speed = speed
self.acceleration = accel
for dest in [self.controller.DEST_X_AXIS, self.controller.DEST_Y_AXIS]:
for dest in [AXIS_X, AXIS_Y]:
self.controller.set_velocity_params(
dest,
min_velocity=0.0,
acceleration=self.acceleration,
max_velocity=self.jog_speed
max_velocity=self.jog_speed,
acceleration=self.acceleration
)
except Exception as e:
@@ -261,8 +257,11 @@ class MotionWorker(QtCore.QObject):
return
try:
dest = self.controller.DEST_X_AXIS if axis == 'x' else self.controller.DEST_Y_AXIS
self.controller.set_channel_enable_state(dest, enabled)
dest = AXIS_X if axis == 'x' else AXIS_Y
if enabled:
self.controller.enable_axis(dest)
else:
self.controller.disable_axis(dest)
state_str = "enabled" if enabled else "disabled"
print(f"{axis.upper()} axis {state_str}")
@@ -275,35 +274,25 @@ class MotionWorker(QtCore.QObject):
if not self.is_connected or not self.controller:
return
# Throttle position requests to avoid slowing down the main loop
# Throttle position reads to avoid excessive signal emission
current_time = time.time()
if current_time - self.last_position_update_time < self.position_update_interval:
return
self.last_position_update_time = current_time
try:
# Actively request positions from the controller instead of relying on cached values
# This ensures we always have up-to-date position data
# Use longer timeout (1.5s) to accommodate high-speed scanning at 200mm/s
x_pos = self.controller.get_position(self.controller.DEST_X_AXIS, timeout=1.5)
y_pos = self.controller.get_position(self.controller.DEST_Y_AXIS, timeout=1.5)
# Read cached positions (populated by polling worker)
x_pos = self.controller.positions[0]
y_pos = self.controller.positions[1]
if x_pos is not None and y_pos is not None:
# Always emit on first update, or if position changed significantly (> 0.001mm)
if (self.last_x is None or self.last_y is None or
abs(x_pos - self.last_x) > 0.001 or abs(y_pos - self.last_y) > 0.001):
self.last_x = x_pos
self.last_y = y_pos
print(f"Position update: X={x_pos:.3f}mm, Y={y_pos:.3f}mm")
self.position_updated.emit(x_pos, y_pos)
# else: silently skip incomplete position data during busy scanning
# Always emit on first update, or if position changed significantly (> 0.001mm)
if (self.last_x is None or self.last_y is None or
abs(x_pos - self.last_x) > 0.001 or abs(y_pos - self.last_y) > 0.001):
self.last_x = x_pos
self.last_y = y_pos
print(f"Position update: X={x_pos:.3f}mm, Y={y_pos:.3f}mm")
self.position_updated.emit(x_pos, y_pos)
except RuntimeError as e:
# RuntimeError indicates actual hardware error (overtemp, encoder fault, etc.)
print(f"CRITICAL: Motor error detected: {e}")
self.error_occurred.emit(str(e))
# Stop requesting position updates to avoid spam
self.is_connected = False
except Exception as e:
print(f"Error updating position: {e}")
import traceback
@@ -315,8 +304,8 @@ class MotionWorker(QtCore.QObject):
return
try:
x_homed = self.controller.is_homed_x
y_homed = self.controller.is_homed_y
x_homed = self.controller.am_homed[0]
y_homed = self.controller.am_homed[1]
# Only emit if status changed
if x_homed != self.last_x_homed or y_homed != self.last_y_homed:
@@ -332,42 +321,22 @@ class MotionWorker(QtCore.QObject):
def update_motion_status(self):
"""Update motion status and emit signal if changed.
NOTE: On BBD202 firmware v2.1.5, the is_in_motion_x/y properties
do NOT reliably detect motion - they may always return False even
during active movement. This is a known firmware limitation.
For scan execution, use the ScanWorker.move_and_wait() method which
uses position-based motion detection instead of status bits.
This UI status display is best-effort only.
The new driver's polling worker keeps am_moving[], am_error[]
up to date automatically via status update messages.
"""
if not self.is_connected or not self.controller:
return
try:
# Actively poll for status updates
self.controller.poll_status()
# Check for any error conditions
error_msg = self.controller.check_for_errors()
if error_msg:
print(f"Motor error detected: {error_msg}")
self.error_occurred.emit(error_msg)
self.controller.clear_last_error()
if self.controller.am_error[0]:
self.error_occurred.emit("X-axis error detected")
if self.controller.am_error[1]:
self.error_occurred.emit("Y-axis error detected")
# Read the cached status (may not accurately reflect motion on some firmware)
x_moving = self.controller.is_in_motion_x
y_moving = self.controller.is_in_motion_y
# Also check if there are pending moves (more reliable)
if self.controller.is_move_pending():
# If there are pending moves, we're likely still moving
# This provides a backup indication when status bits fail
pending = self.controller.get_pending_targets()
if self.controller.DEST_X_AXIS in pending:
x_moving = True
if self.controller.DEST_Y_AXIS in pending:
y_moving = True
# Read cached motion status (updated by polling worker)
x_moving = self.controller.am_moving[0]
y_moving = self.controller.am_moving[1]
# Only emit if status changed
if x_moving != self.last_x_moving or y_moving != self.last_y_moving: