Files
scanengine-3/tests/test_bbd202_snake.py
T
2026-02-09 14:40:34 -06:00

267 lines
8.4 KiB
Python

"""
BBD202 Snake Test
Quick test to verify the BBD202 stage driver with a snake scan pattern.
Scans a 20mm x 20mm area with 2mm row spacing in a back-and-forth pattern.
"""
import sys
import time
from hardware.bbd202 import MotionController, MotorStatusBits
def snake_test(mc: MotionController, width: float = 20.0, height: float = 20.0, row_spacing: float = 2.0) -> bool:
"""
Execute a snake scan pattern.
Args:
mc: MotionController instance
width: Scan width in mm (X direction)
height: Scan height in mm (Y direction)
row_spacing: Distance between rows in mm
Pattern:
Start (0,0) -> Move +X to (width, 0) -> Move +Y by row_spacing ->
Move -X to (0, row_spacing) -> Move +Y by row_spacing ->
... repeat until height is covered
"""
print("\n" + "="*70)
print(f"SNAKE TEST: {width}mm x {height}mm area, {row_spacing}mm row spacing")
print("="*70)
# Get starting position
mc.poll_positions()
time.sleep(0.1)
start_x = mc.position_x
start_y = mc.position_y
if start_x is None or start_y is None:
print("ERROR: Could not get initial position")
return False
print(f"\nStarting position: X={start_x:.4f}mm, Y={start_y:.4f}mm")
# Calculate number of rows
num_rows = int(height / row_spacing) + 1
print(f"Number of rows: {num_rows}")
# Generate snake path waypoints
waypoints = []
for row in range(num_rows):
y_pos = start_y + row * row_spacing
if row % 2 == 0:
# Even row: move left to right
waypoints.append((start_x + width, y_pos, f"Row {row+1}/{num_rows} (+X)"))
else:
# Odd row: move right to left
waypoints.append((start_x, y_pos, f"Row {row+1}/{num_rows} (-X)"))
print(f"Total waypoints: {len(waypoints)}")
all_passed = True
total_distance = 0.0
total_time = 0.0
# Execute the snake pattern
for i, (target_x, target_y, description) in enumerate(waypoints):
print(f"\n--- Move {i+1}/{len(waypoints)}: {description} ---")
print(f"Target: X={target_x:.4f}mm, Y={target_y:.4f}mm")
# Record start time and position
move_start = time.time()
mc.poll_positions()
time.sleep(0.02)
curr_x = mc.position_x
curr_y = mc.position_y
if curr_x is not None and curr_y is not None:
distance = ((target_x - curr_x)**2 + (target_y - curr_y)**2)**0.5
total_distance += distance
# Clear any previous error
mc.clear_last_error()
# Issue move command
mc.move_to_fast(x=target_x, y=target_y)
# Wait for move to complete with periodic progress updates
sample_count = 0
max_err = 0.0
while not mc.poll_until_idle(tolerance=0.005, timeout=0.05):
elapsed = time.time() - move_start
curr_x = mc.position_x
curr_y = mc.position_y
if curr_x is not None and curr_y is not None:
x_err = curr_x - target_x
y_err = curr_y - target_y
total_err = (x_err**2 + y_err**2)**0.5
max_err = max(max_err, total_err)
# Print every 10th sample
if sample_count % 10 == 0:
print(f" {elapsed:>5.2f}s X={curr_x:>8.4f} Y={curr_y:>8.4f} err={total_err*1000:>6.1f}um", end='\r')
sample_count += 1
# Timeout check
if elapsed > 30.0:
print(f"\n** TIMEOUT after 30s - stage may be hung!")
all_passed = False
mc.clear_pending_moves()
break
# Move complete
elapsed = time.time() - move_start
total_time += elapsed
mc.poll_positions()
time.sleep(0.02)
final_x = mc.position_x
final_y = mc.position_y
if final_x is not None and final_y is not None:
x_err = final_x - target_x
y_err = final_y - target_y
total_err = (x_err**2 + y_err**2)**0.5
print(f" {elapsed:>5.2f}s X={final_x:>8.4f} Y={final_y:>8.4f} err={total_err*1000:>6.1f}um ** DONE **")
if total_err > 0.05: # 50um tolerance
print(f" ** WARNING: Position error exceeds 50um: {total_err*1000:.1f}um")
all_passed = False
# Check for errors after move
err = mc.check_for_errors()
if err:
print(f" ** Error after move: {err}")
all_passed = False
last_err = mc.last_error
if last_err:
print(f" ** Last error: {last_err}")
all_passed = False
# Return to start position
print(f"\n--- Returning to start position ---")
move_start = time.time()
mc.move_to_fast(x=start_x, y=start_y)
while not mc.poll_until_idle(tolerance=0.005, timeout=0.05):
if time.time() - move_start > 30.0:
print("** TIMEOUT returning to start")
mc.clear_pending_moves()
break
elapsed = time.time() - move_start
total_time += elapsed
# Final summary
print(f"\n{'='*70}")
print("SNAKE TEST SUMMARY")
print(f"{'='*70}")
mc.poll_positions()
time.sleep(0.1)
final_x = mc.position_x
final_y = mc.position_y
if final_x is not None and final_y is not None:
return_x_err = abs(final_x - start_x)
return_y_err = abs(final_y - start_y)
return_err = (return_x_err**2 + return_y_err**2)**0.5
print(f"Start position: X={start_x:.4f}mm, Y={start_y:.4f}mm")
print(f"Final position: X={final_x:.4f}mm, Y={final_y:.4f}mm")
print(f"Return error: {return_err*1000:.1f}um (X={return_x_err*1000:.1f}um, Y={return_y_err*1000:.1f}um)")
print(f"Total distance: {total_distance:.2f}mm")
print(f"Total time: {total_time:.2f}s")
print(f"Average speed: {total_distance/total_time:.2f}mm/s")
print(f"Waypoints: {len(waypoints)}")
if return_err > 0.05: # 50um tolerance
print(f"\n** FAIL: Did not return to start position (error: {return_err*1000:.1f}um > 50um)")
all_passed = False
if all_passed:
print(f"\n** PASS: Snake test completed successfully **")
else:
print(f"\n** FAIL: Issues detected during snake test **")
return all_passed
def main():
print("="*70)
print("BBD202 Snake Test")
print("="*70)
mc = None
try:
# Connect to controller
print("\nConnecting to BBD202 controller...")
mc = MotionController()
mc.connect(enable_updates=True)
print("Connected!")
# Brief hardware info
try:
hw_info = mc.get_hw_info()
print(f"\nHardware: {hw_info['model']} (S/N: {hw_info['serial_number']})")
print(f"Firmware: {hw_info['firmware_version']}")
except Exception as e:
print(f"Could not get hardware info: {e}")
# Check homing status
print("\n--- Checking Homing Status ---")
mc.request_status_update(mc.DEST_X_AXIS)
mc.request_status_update(mc.DEST_Y_AXIS)
time.sleep(0.2)
mc.poll_status()
time.sleep(0.1)
x_homed = mc.is_homed_x
y_homed = mc.is_homed_y
print(f"X-axis homed: {x_homed}")
print(f"Y-axis homed: {y_homed}")
if not (x_homed and y_homed):
print("\n** Axes not homed - will home now **")
print("\nHoming X-axis...")
x_result = mc.home_x_axis(timeout=30.0)
print(f"X-axis: {'SUCCESS' if x_result else 'FAILED'}")
print("\nHoming Y-axis...")
y_result = mc.home_y_axis(timeout=30.0)
print(f"Y-axis: {'SUCCESS' if y_result else 'FAILED'}")
if not (x_result and y_result):
print("\n** Homing failed. Cannot continue. **")
return 1
# Run the snake test
success = snake_test(mc, width=20.0, height=20.0, row_spacing=2.0)
return 0 if success else 1
except KeyboardInterrupt:
print("\n\nTest interrupted by user")
return 1
except Exception as e:
print(f"\n** ERROR: {e}")
import traceback
traceback.print_exc()
return 1
finally:
if mc is not None:
print("\nDisconnecting from controller...")
mc.disconnect()
print("Disconnected")
if __name__ == "__main__":
sys.exit(main())