""" BBD202 Stage Diagnostic Tests Diagnostic tests for the Thorlabs MLS203/BBD202 motion controller to: 1. Check if axes are properly homed 2. Move in a 15mm square pattern while outputting position data 3. Detect stage hangs through position monitoring 4. Output response information for debugging """ import sys import time from typing import Optional from hardware.bbd202 import MotionController, MotorStatusBits def format_status_bits(bits: Optional[int]) -> str: """Format status bits as a readable string with key flags.""" if bits is None: return "None (no status received)" flags = [] status = MotorStatusBits(bits) # Key flags to check if MotorStatusBits.HOMED in status: flags.append("HOMED") if MotorStatusBits.HOMING in status: flags.append("HOMING") if MotorStatusBits.ENABLED in status: flags.append("ENABLED") if MotorStatusBits.SETTLED in status: flags.append("SETTLED") if MotorStatusBits.TRACKING in status: flags.append("TRACKING") if MotorStatusBits.INMOTIONCW in status: flags.append("INMOTIONCW") if MotorStatusBits.INMOTIONCCW in status: flags.append("INMOTIONCCW") if MotorStatusBits.POWEROK in status: flags.append("POWEROK") if MotorStatusBits.ERROR in status: flags.append("ERROR") if MotorStatusBits.POSITIONERROR in status: flags.append("POSITIONERROR") if MotorStatusBits.OVERTEMP in status: flags.append("OVERTEMP") if MotorStatusBits.COMMUTATIONERROR in status: flags.append("COMMUTATIONERROR") flag_str = ", ".join(flags) if flags else "NO_FLAGS" return f"0x{bits:08X} [{flag_str}]" def test_homing_status(mc: MotionController) -> bool: """ Test 1: Check if both axes are properly homed. Returns True if both axes are homed, False otherwise. """ print("\n" + "="*70) print("TEST 1: Checking Homing Status") print("="*70) # Request fresh status from both axes print("\nRequesting status update from both axes...") mc.request_status_update(mc.DEST_X_AXIS) mc.request_status_update(mc.DEST_Y_AXIS) time.sleep(0.2) # Allow time for response # Also poll status to ensure we have latest mc.poll_status() time.sleep(0.1) # Check X-axis x_bits = mc.status_bits_x x_homed = mc.is_homed_x print(f"\nX-Axis Status:") print(f" Raw status bits: {format_status_bits(x_bits)}") print(f" is_homed_x: {x_homed}") # Check Y-axis y_bits = mc.status_bits_y y_homed = mc.is_homed_y print(f"\nY-Axis Status:") print(f" Raw status bits: {format_status_bits(y_bits)}") print(f" is_homed_y: {y_homed}") # Check for errors last_err = mc.last_error if last_err: print(f"\n** Last Error: {last_err}") error_check = mc.check_for_errors() if error_check: print(f"** Error condition detected: {error_check}") # Summary print(f"\n--- Homing Status Summary ---") if x_homed and y_homed: print("PASS: Both axes are homed") return True else: print("FAIL: One or both axes are NOT homed") if not x_homed: print(" -> X-axis needs homing") if not y_homed: print(" -> Y-axis needs homing") return False def test_square_pattern(mc: MotionController, side_length: float = 15.0) -> bool: """ Test 2: Move the stage in a square pattern. Moves in a 15mm square starting from current position, outputting position data at each step to detect hangs. Pattern: Start -> +X -> +Y -> -X -> -Y (back to start) Uses the new wait_until_settled() method for reliable motion detection. """ print("\n" + "="*70) print(f"TEST 2: Square Pattern Movement ({side_length}mm)") print("="*70) print("Using wait_until_settled() for motion detection") # 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") # Define the square corners (relative to start) corners = [ (start_x + side_length, start_y), # Move +X (start_x + side_length, start_y + side_length), # Move +Y (start_x, start_y + side_length), # Move -X (start_x, start_y), # Move -Y (back to start) ] corner_names = ["+X edge", "+Y edge (diagonal)", "-X edge", "Return to start"] all_passed = True for i, (target_x, target_y) in enumerate(corners): print(f"\n--- Move {i+1}/4: {corner_names[i]} ---") print(f"Target: X={target_x:.4f}mm, Y={target_y:.4f}mm") # Clear any previous error mc.clear_last_error() # Record start time move_start = time.time() # Issue move command (non-blocking) mc.move_to_fast(x=target_x, y=target_y) # Use poll_until_idle() in a loop to show progress print(f"\n{'Time':>8} {'X_pos':>12} {'Y_pos':>12} {'X_err':>10} {'Y_err':>10} {'Pending'}") print("-" * 70) sample_count = 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 # Print every 5th sample if sample_count % 5 == 0: pending = mc.get_pending_targets() pending_str = ", ".join([f"{'X' if d==0x21 else 'Y'}" for d in pending.keys()]) print(f"{elapsed:>7.3f}s {curr_x:>12.4f} {curr_y:>12.4f} {x_err:>+10.4f} {y_err:>+10.4f} {pending_str:>8}") 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 - show final status 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 print(f"{elapsed:>7.3f}s {curr_x:>12.4f} {curr_y:>12.4f} {x_err:>+10.4f} {y_err:>+10.4f} ** DONE **") print(f"\nMove completed in {elapsed:.3f}s") print(f"Final position: X={curr_x:.4f}mm, Y={curr_y:.4f}mm") print(f"Position error: X={abs(x_err)*1000:.1f}um, Y={abs(y_err)*1000:.1f}um") # 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 # Short delay between moves time.sleep(0.1) # Final summary print(f"\n--- Square Pattern Summary ---") 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: total_x_err = abs(final_x - start_x) total_y_err = abs(final_y - start_y) print(f"Final position: X={final_x:.4f}mm, Y={final_y:.4f}mm") print(f"Start position: X={start_x:.4f}mm, Y={start_y:.4f}mm") print(f"Return error: X={total_x_err*1000:.1f}um, Y={total_y_err*1000:.1f}um") if total_x_err > 0.05 or total_y_err > 0.05: print("FAIL: Did not return to start position accurately (>50um)") all_passed = False if all_passed: print("PASS: Square pattern completed successfully") else: print("FAIL: Issues detected during square pattern") return all_passed def print_diagnostic_info(mc: MotionController): """Print comprehensive diagnostic information about the controller.""" print("\n" + "="*70) print("CONTROLLER DIAGNOSTIC INFO") print("="*70) try: hw_info = mc.get_hw_info() print(f"\nHardware Info:") print(f" Serial Number: {hw_info['serial_number']}") print(f" Model: {hw_info['model']}") print(f" Firmware: {hw_info['firmware_version']}") print(f" HW Version: {hw_info['hw_version']}") print(f" Channels: {hw_info['num_channels']}") print(f" Notes: {hw_info['notes']}") except Exception as e: print(f" Error getting hardware info: {e}") # Request and display velocity params print(f"\nVelocity Parameters:") mc.send_command(0x0414, param1=0x01, dest=mc.DEST_X_AXIS) # REQ_VELPARAMS mc.send_command(0x0414, param1=0x01, dest=mc.DEST_Y_AXIS) time.sleep(0.1) x_vel = mc.velocity_params_x y_vel = mc.velocity_params_y if x_vel: print(f" X-Axis: max_vel={x_vel['max_velocity']:.2f}mm/s, accel={x_vel['acceleration']:.2f}mm/s²") if y_vel: print(f" Y-Axis: max_vel={y_vel['max_velocity']:.2f}mm/s, accel={y_vel['acceleration']:.2f}mm/s²") def test_bay_and_channel_status(mc: MotionController) -> dict: """ Test bay occupancy and channel enable states. Returns dict with diagnostic info about each axis. """ print("\n" + "="*70) print("BAY AND CHANNEL DIAGNOSTICS") print("="*70) results = {'x': {}, 'y': {}} # Query bay status using MGMSG_RACK_REQ_BAYUSED (0x0060) print("\n--- Querying Bay Status (RACK_REQ_BAYUSED 0x0060) ---") print("Sending to controller (0x11)...") # Send bay request to controller mc.send_command(0x0060, param1=0x00, param2=0x00, dest=0x11, source=0x01) time.sleep(0.2) # Check for response in queue msgs = mc.get_all_messages() bay_response = None for msg in msgs: print(f" Received: msg_id=0x{msg.msg_id:04X}, source=0x{msg.source:02X}, " f"param1=0x{msg.param1:02X}, param2=0x{msg.param2:02X}, data={msg.data.hex() if msg.data else 'none'}") if msg.msg_id == 0x0061: # RACK_GET_BAYUSED bay_response = msg if bay_response: # param1 contains bay_ident (which bay), param2 contains state print(f"\nBay status response: bay={bay_response.param1}, state=0x{bay_response.param2:02X}") else: print(" No RACK_GET_BAYUSED response received") # Check channel enable state for X-axis (0x21) print("\n--- X-Axis (Bay 1 / dest=0x21) Channel Status ---") print(f" Destination address: 0x{mc.DEST_X_AXIS:02X}") try: x_enabled = mc.get_channel_enable_state(mc.DEST_X_AXIS, timeout=2.0) print(f" Channel enabled: {x_enabled}") results['x']['enabled'] = x_enabled except Exception as e: print(f" Error querying channel state: {e}") results['x']['enabled'] = None # Get X position to verify communication x_pos = mc.get_position(mc.DEST_X_AXIS, timeout=2.0) print(f" Current position: {x_pos:.4f}mm" if x_pos is not None else " Position query failed!") results['x']['position'] = x_pos results['x']['communicating'] = x_pos is not None # Check channel enable state for Y-axis (0x22) print("\n--- Y-Axis (Bay 2 / dest=0x22) Channel Status ---") print(f" Destination address: 0x{mc.DEST_Y_AXIS:02X}") try: y_enabled = mc.get_channel_enable_state(mc.DEST_Y_AXIS, timeout=2.0) print(f" Channel enabled: {y_enabled}") results['y']['enabled'] = y_enabled except Exception as e: print(f" Error querying channel state: {e}") results['y']['enabled'] = None # Get Y position to verify communication y_pos = mc.get_position(mc.DEST_Y_AXIS, timeout=2.0) print(f" Current position: {y_pos:.4f}mm" if y_pos is not None else " Position query failed!") results['y']['position'] = y_pos results['y']['communicating'] = y_pos is not None return results def test_y_axis_move_verbose(mc: MotionController) -> bool: """ Test Y-axis movement using the new wait_until_settled() method. Sends a small Y-axis move and monitors progress. """ print("\n" + "="*70) print("Y-AXIS VERBOSE MOVE TEST") print("="*70) print("Using wait_until_settled() for motion detection") # Get current Y position mc.poll_positions() time.sleep(0.1) start_y = mc.position_y if start_y is None: print("ERROR: Cannot get Y position") return False print(f"\nCurrent Y position: {start_y:.4f}mm") # Target: move 5mm in Y target_y = start_y + 5.0 print(f"Target Y position: {target_y:.4f}mm") # Issue move command using move_to_fast (which tracks pending moves) print("\n--- Issuing Y-axis move via move_to_fast() ---") mc.move_to_fast(y=target_y) # Show pending moves pending = mc.get_pending_targets() print(f" Pending moves: {pending}") print(f" is_move_pending(): {mc.is_move_pending()}") # Monitor using poll_until_idle print("\n--- Monitoring Y-axis with poll_until_idle() ---") print(f"{'Time':>6} {'Y_pos':>10} {'Y_err':>10} {'Pending'}") print("-" * 45) start_time = time.time() movement_detected = False while not mc.poll_until_idle(tolerance=0.005, timeout=0.05): elapsed = time.time() - start_time curr_y = mc.position_y if curr_y is not None: y_err = curr_y - target_y pending = "Y" if mc.is_move_pending() else "-" print(f"{elapsed:>5.2f}s {curr_y:>10.4f} {y_err:>+10.4f} {pending:>8}") if abs(curr_y - start_y) > 0.01: movement_detected = True if elapsed > 10.0: print("\n** TIMEOUT after 10s **") mc.clear_pending_moves() break # Final status elapsed = time.time() - start_time final_y = mc.position_y print(f"\n--- Y-Axis Move Summary ---") print(f"Start position: {start_y:.4f}mm") print(f"Target position: {target_y:.4f}mm") print(f"Final position: {final_y:.4f}mm" if final_y else "Final position: unknown") print(f"Move time: {elapsed:.3f}s") if final_y is not None: distance_moved = abs(final_y - start_y) print(f"Distance moved: {distance_moved:.4f}mm") if distance_moved < 0.01: print("\n** FAIL: Y-axis did not move at all! **") print("Possible causes:") print(" 1. Y-axis channel is disabled") print(" 2. Y-axis motor driver issue") print(" 3. Command not reaching Y-axis bay") print(" 4. Mechanical obstruction") return False elif abs(final_y - target_y) < 0.05: print("\n** PASS: Y-axis moved to target **") return True else: print(f"\n** PARTIAL: Y-axis moved but not to target **") return False return movement_detected def test_enable_and_move_y(mc: MotionController) -> bool: """ Explicitly enable Y-axis channel and attempt movement. """ print("\n" + "="*70) print("ENABLE Y-AXIS AND MOVE TEST") print("="*70) # First, explicitly enable Y-axis channel print("\n--- Enabling Y-axis channel (MOD_SET_CHANENABLESTATE) ---") print(f" Sending to dest=0x{mc.DEST_Y_AXIS:02X}, enable=True") try: mc.set_channel_enable_state(mc.DEST_Y_AXIS, enabled=True) time.sleep(0.2) print(" Enable command sent") except Exception as e: print(f" Error sending enable: {e}") # Verify it's enabled try: y_enabled = mc.get_channel_enable_state(mc.DEST_Y_AXIS, timeout=2.0) print(f" Channel enabled state: {y_enabled}") except Exception as e: print(f" Error checking state: {e}") # Request fresh status mc.request_status_update(mc.DEST_Y_AXIS) time.sleep(0.2) y_bits = mc.status_bits_y print(f" Status bits: {format_status_bits(y_bits)}") # Now try the verbose move test return test_y_axis_move_verbose(mc) def main(): print("="*70) print("BBD202 Stage Diagnostic Test") print("="*70) mc = None try: # Connect to controller print("\nConnecting to BBD202 controller...") mc = MotionController() mc.connect(enable_updates=True) print("Connected!") # Print diagnostic info print_diagnostic_info(mc) # Run bay and channel diagnostics channel_results = test_bay_and_channel_status(mc) # Run homing status test homed = test_homing_status(mc) if not homed: print("\n" + "-"*70) response = input("Axes not homed. Home now? (y/n): ").strip().lower() if response == 'y': print("\nHoming X-axis...") x_result = mc.home_x_axis(timeout=30.0) print(f"X-axis homing: {'SUCCESS' if x_result else 'FAILED'}") print("\nHoming Y-axis...") y_result = mc.home_y_axis(timeout=30.0) print(f"Y-axis homing: {'SUCCESS' if y_result else 'FAILED'}") if not (x_result and y_result): print("\nHoming failed. Cannot continue with movement test.") return 1 # Re-check homing status test_homing_status(mc) else: print("\nSkipping movement tests (axes not homed)") return 1 # Run Y-axis verbose test first to diagnose the issue y_axis_ok = test_enable_and_move_y(mc) # If Y-axis failed, skip the square pattern if not y_axis_ok: print("\n** Y-axis movement failed - skipping square pattern test **") square_ok = False else: # Run square pattern test square_ok = test_square_pattern(mc, side_length=15.0) # Final summary print("\n" + "="*70) print("DIAGNOSTIC SUMMARY") print("="*70) print(f"X-Axis Channel: {'OK' if channel_results['x'].get('communicating') else 'FAIL'}") print(f"Y-Axis Channel: {'OK' if channel_results['y'].get('communicating') else 'FAIL'}") print(f"Homing Status: {'PASS' if homed else 'FAIL'}") print(f"Y-Axis Move: {'PASS' if y_axis_ok else 'FAIL'}") print(f"Square Pattern: {'PASS' if square_ok else 'FAIL/SKIPPED'}") if homed and y_axis_ok and square_ok: print("\nAll tests PASSED") return 0 else: print("\nSome tests FAILED - see details above") return 1 except KeyboardInterrupt: print("\n\nTest interrupted by user") return 1 except Exception as e: print(f"\nError during test: {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())