""" BBD202 Snake Scan Test - 20x20mm Area Scans a 20mm x 20mm area centered at X=55, Y=37.5 in a snake pattern. Row spacing: 0.5mm Movement order: X+, Y+, X-, Y+ (snake pattern) """ import sys import time import argparse from hardware.bbd202 import MotionController, MotorStatusBits # Scan parameters CENTER_X = 55.0 # mm CENTER_Y = 37.5 # mm SCAN_WIDTH = 20.0 # mm (X direction) SCAN_HEIGHT = 20.0 # mm (Y direction) ROW_SPACING = 0.5 # mm (Y step between rows) # Calculated bounds START_X = CENTER_X - SCAN_WIDTH / 2 # 45.0 mm END_X = CENTER_X + SCAN_WIDTH / 2 # 65.0 mm START_Y = CENTER_Y - SCAN_HEIGHT / 2 # 27.5 mm END_Y = CENTER_Y + SCAN_HEIGHT / 2 # 47.5 mm def snake_scan(mc: MotionController) -> bool: """ Execute a snake scan pattern over the 20x20mm area. Pattern (X+, Y+, X-, Y+): Row 0: X=45 -> X=65 (X+) Step: Y += 0.5 (Y+) Row 1: X=65 -> X=45 (X-) Step: Y += 0.5 (Y+) ... repeat until Y reaches 47.5mm """ print("\n" + "=" * 70) print("SNAKE SCAN TEST: 20mm x 20mm") print("=" * 70) print(f"Center: X={CENTER_X:.1f}mm, Y={CENTER_Y:.1f}mm") print(f"Bounds: X=[{START_X:.1f}, {END_X:.1f}]mm, Y=[{START_Y:.1f}, {END_Y:.1f}]mm") print(f"Row spacing: {ROW_SPACING}mm") print(f"Pattern: X+, Y+, X-, Y+ (snake)") # Calculate number of rows num_rows = int(SCAN_HEIGHT / ROW_SPACING) + 1 print(f"Total rows: {num_rows}") # Stop automatic ACK to prevent interference with move commands # We'll send manual ACKs after each move completes mc.stop_status_ack() print("Using manual ACK mode for scan") # First, move to the starting position print(f"\n--- Moving to start position ({START_X:.1f}, {START_Y:.1f}) ---") mc.move_to_fast(x=START_X, y=START_Y) move_start = time.time() while not mc.poll_until_idle(tolerance=0.005, timeout=0.05): if time.time() - move_start > 30.0: print("** TIMEOUT moving to start position!") return False # Verify starting position mc.poll_positions() time.sleep(0.1) curr_x = mc.position_x curr_y = mc.position_y print(f"At start: X={curr_x:.4f}mm, Y={curr_y:.4f}mm") # 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 X+ (left to right) waypoints.append((END_X, y_pos, f"Row {row + 1}/{num_rows} X+ (Y={y_pos:.1f})")) else: # Odd row: move X- (right to left) waypoints.append((START_X, y_pos, f"Row {row + 1}/{num_rows} X- (Y={y_pos:.1f})")) print(f"\nTotal waypoints: {len(waypoints)}") print("-" * 70) all_passed = True total_distance = 0.0 total_time = 0.0 scan_start_time = time.time() # Execute the snake pattern for i, (target_x, target_y, description) in enumerate(waypoints): # Record start position for distance calculation mc.poll_positions() time.sleep(0.02) prev_x = mc.position_x prev_y = mc.position_y if prev_x is not None and prev_y is not None: distance = ((target_x - prev_x) ** 2 + (target_y - prev_y) ** 2) ** 0.5 total_distance += distance # Clear any previous error mc.clear_last_error() # Issue move command move_start = time.time() mc.move_to_fast(x=target_x, y=target_y) # Wait for move to complete while not mc.poll_until_idle(tolerance=0.005, timeout=0.05): elapsed = time.time() - move_start # Timeout check if elapsed > 30.0: print(f"\n** TIMEOUT on {description} after 30s!") all_passed = False mc.clear_pending_moves() break # Move complete - send ACK to keep controller responsive mc.ack_status_update() 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 progress (compact format) status = "OK" if total_err < 0.05 else "ERR" print(f" {description:35} -> X={final_x:7.3f} Y={final_y:7.3f} err={total_err * 1000:5.1f}um [{status}]") if total_err > 0.05: # 50um tolerance all_passed = False # Check for errors after move err = mc.check_for_errors() if err: print(f" ** Error: {err}") all_passed = False last_err = mc.last_error if last_err: print(f" ** Last error: {last_err}") all_passed = False scan_elapsed = time.time() - scan_start_time # Return to start position print(f"\n--- Returning to start position ({START_X:.1f}, {START_Y:.1f}) ---") 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 # Final summary print(f"\n{'=' * 70}") print("SNAKE SCAN 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"Scan area: {SCAN_WIDTH:.1f}mm x {SCAN_HEIGHT:.1f}mm") print(f"Center: X={CENTER_X:.1f}mm, Y={CENTER_Y:.1f}mm") print(f"Row spacing: {ROW_SPACING}mm") print(f"Rows completed: {num_rows}") print(f"Total distance: {total_distance:.2f}mm") print(f"Scan time: {scan_elapsed:.2f}s") print(f"Average speed: {total_distance / scan_elapsed:.2f}mm/s") print(f"Return error: {return_err * 1000:.1f}um (X={return_x_err * 1000:.1f}um, Y={return_y_err * 1000:.1f}um)") if return_err > 0.05: print(f"\n** FAIL: Return position error exceeds 50um") all_passed = False # Restart automatic ACK mc.start_status_ack() if all_passed: print(f"\n** PASS: Snake scan completed successfully **") else: print(f"\n** FAIL: Issues detected during snake scan **") return all_passed def main(): parser = argparse.ArgumentParser(description="BBD202 Snake Scan Test - 20x20mm Area") parser.add_argument("--auto-home", action="store_true", help="Automatically home axes if not homed (no prompt)") args = parser.parse_args() print("=" * 70) print("BBD202 Snake Scan Test - 20x20mm Area") 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! **") # Check if we should auto-home or prompt should_home = args.auto_home if not should_home: try: response = input("Home axes now? (y/n): ").strip().lower() should_home = response == 'y' except EOFError: print("Non-interactive mode detected. Use --auto-home flag.") return 1 if should_home: 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 else: print("\nCannot run snake scan without homing. Exiting.") return 1 # Run the snake scan success = snake_scan(mc) 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())