Explorar o código

Merge new_reed_homing into kiosk_mode_eglfs

- Add angular homing with reed switch support
- Add sensor offset configuration (in degrees)
- Add background reed switch monitoring
- Add UI for angular homing settings
- Resolved conflicts in connection_manager.py by keeping new_reed_homing implementation
tuanchris hai 3 meses
pai
achega
03a4d74ae1

+ 2 - 0
docker-compose.yml

@@ -9,6 +9,8 @@ services:
       - .:/app
       - .:/app
       # Mount timezone file from host for Still Sands scheduling
       # Mount timezone file from host for Still Sands scheduling
       - /etc/timezone:/etc/host-timezone:ro
       - /etc/timezone:/etc/host-timezone:ro
+      # Mount GPIO memory for hardware access
+      - /dev/gpiomem:/dev/gpiomem
     devices:
     devices:
       - "/dev/ttyACM0:/dev/ttyACM0"
       - "/dev/ttyACM0:/dev/ttyACM0"
       - "/dev/ttyUSB0:/dev/ttyUSB0"
       - "/dev/ttyUSB0:/dev/ttyUSB0"

+ 26 - 0
main.py

@@ -380,6 +380,32 @@ async def set_scheduled_pause(request: ScheduledPauseRequest):
         logger.error(f"Error updating Still Sands settings: {str(e)}")
         logger.error(f"Error updating Still Sands settings: {str(e)}")
         raise HTTPException(status_code=500, detail=f"Failed to update Still Sands settings: {str(e)}")
         raise HTTPException(status_code=500, detail=f"Failed to update Still Sands settings: {str(e)}")
 
 
+@app.get("/api/angular-homing")
+async def get_angular_homing():
+    """Get current angular homing settings."""
+    return {
+        "angular_homing_enabled": state.angular_homing_enabled,
+        "angular_homing_offset_degrees": state.angular_homing_offset_degrees
+    }
+
+class AngularHomingRequest(BaseModel):
+    angular_homing_enabled: bool
+    angular_homing_offset_degrees: float = 0.0
+
+@app.post("/api/angular-homing")
+async def set_angular_homing(request: AngularHomingRequest):
+    """Update angular homing settings."""
+    try:
+        state.angular_homing_enabled = request.angular_homing_enabled
+        state.angular_homing_offset_degrees = request.angular_homing_offset_degrees
+        state.save()
+
+        logger.info(f"Angular homing {'enabled' if request.angular_homing_enabled else 'disabled'}, offset: {request.angular_homing_offset_degrees}°")
+        return {"success": True, "message": "Angular homing settings updated"}
+    except Exception as e:
+        logger.error(f"Error updating angular homing settings: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"Failed to update angular homing settings: {str(e)}")
+
 @app.get("/list_serial_ports")
 @app.get("/list_serial_ports")
 async def list_ports():
 async def list_ports():
     logger.debug("Listing available serial ports")
     logger.debug("Listing available serial ports")

+ 84 - 10
modules/connection/connection_manager.py

@@ -6,8 +6,11 @@ import serial.tools.list_ports
 import websocket
 import websocket
 import asyncio
 import asyncio
 
 
+from modules.core import pattern_manager
 from modules.core.state import state
 from modules.core.state import state
 from modules.led.led_controller import effect_loading, effect_idle, effect_connected, LEDController
 from modules.led.led_controller import effect_loading, effect_idle, effect_connected, LEDController
+from modules.connection.reed_switch import ReedSwitchMonitor
+
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 IGNORE_PORTS = ['/dev/cu.debug-console', '/dev/cu.Bluetooth-Incoming-Port']
 IGNORE_PORTS = ['/dev/cu.debug-console', '/dev/cu.Bluetooth-Incoming-Port']
@@ -378,7 +381,7 @@ def get_machine_steps(timeout=10):
     
     
     # Process results and determine table type
     # Process results and determine table type
     if settings_complete:
     if settings_complete:
-        if y_steps_per_mm == 180 and x_steps_per_mm == 256:
+        if y_steps_per_mm == 180:
             state.table_type = 'dune_weaver_mini'
             state.table_type = 'dune_weaver_mini'
         elif y_steps_per_mm >= 320:
         elif y_steps_per_mm >= 320:
             state.table_type = 'dune_weaver_pro'
             state.table_type = 'dune_weaver_pro'
@@ -397,12 +400,12 @@ def get_machine_steps(timeout=10):
         logger.error(f"Failed to get all machine parameters after {timeout}s. Missing: {', '.join(missing)}")
         logger.error(f"Failed to get all machine parameters after {timeout}s. Missing: {', '.join(missing)}")
         return False
         return False
 
 
-def home(timeout=15):
+def home(timeout=90):
     """
     """
     Perform homing by checking device configuration and sending the appropriate commands.
     Perform homing by checking device configuration and sending the appropriate commands.
-    
+
     Args:
     Args:
-        timeout: Maximum time in seconds to wait for homing to complete (default: 15)
+        timeout: Maximum time in seconds to wait for homing to complete (default: 60)
     """
     """
     import threading
     import threading
     
     
@@ -428,7 +431,7 @@ def home(timeout=15):
                 loop = asyncio.new_event_loop()
                 loop = asyncio.new_event_loop()
                 asyncio.set_event_loop(loop)
                 asyncio.set_event_loop(loop)
                 try:
                 try:
-                    if state.gear_ratio == 6.25:
+                    if state.table_type == 'dune_weaver_mini':
                         result = loop.run_until_complete(send_grbl_coordinates(0, - 30, homing_speed, home=True))
                         result = loop.run_until_complete(send_grbl_coordinates(0, - 30, homing_speed, home=True))
                         if result == False:
                         if result == False:
                             logger.error("Homing failed - send_grbl_coordinates returned False")
                             logger.error("Homing failed - send_grbl_coordinates returned False")
@@ -449,12 +452,83 @@ def home(timeout=15):
             logger.info("Waiting for device to reach idle state after homing...")
             logger.info("Waiting for device to reach idle state after homing...")
             idle_reached = check_idle()
             idle_reached = check_idle()
 
 
-            if idle_reached:
-                state.current_theta = state.current_rho = 0
-                homing_success = True
-                logger.info("Homing completed and device is idle")
-            else:
+            if not idle_reached:
                 logger.error("Device did not reach idle state after homing")
                 logger.error("Device did not reach idle state after homing")
+                homing_complete.set()
+                return
+            else:
+                state.current_theta = state.current_rho = 0
+
+            # Perform angular homing if enabled (Raspberry Pi only)
+            if state.angular_homing_enabled:
+                logger.info("Starting angular homing sequence")
+                try:
+                    # Initialize reed switch monitor
+                    reed_switch = ReedSwitchMonitor(gpio_pin=18)
+
+                    try:
+                        # Reset theta first
+                        logger.info("Resetting theta before angular homing")
+                        asyncio.run(pattern_manager.reset_theta())
+
+                        # Move radial arm to perimeter (theta=0, rho=1.0)
+                        logger.info("Moving radial arm to perimeter (theta=0, rho=1.0)")
+                        asyncio.run(pattern_manager.move_polar(0, 1.0, homing_speed))
+                        
+                        idle_reached = check_idle()
+
+                        if not idle_reached:
+                            logger.error("Device did not reach idle state after moving to perimeter")
+                            homing_complete.set()
+                            return
+
+                        # Wait 1 second for stabilization
+                        logger.info("Waiting for stabilization...")
+                        time.sleep(1)
+
+                        # Perform angular rotation until reed switch is triggered
+                        logger.info("Rotating around perimeter to find home position")
+                        increment = 0.1  # Small angular increment in radians
+                        current_theta = 0
+                        max_theta = 6.28  # One full rotation (2*pi)
+                        reed_switch_triggered = False
+
+                        while current_theta < max_theta:
+                            # Move to next position
+                            current_theta += increment
+                            asyncio.run(pattern_manager.move_polar(current_theta, 1.0, homing_speed))
+
+                            # Small delay to allow reed switch to settle after movement
+                            time.sleep(0.5)
+
+                            # Check reed switch AFTER movement completes
+                            if reed_switch.is_triggered():
+                                logger.info(f"Reed switch triggered at theta={current_theta}")
+                                reed_switch_triggered = True
+                                break
+
+                        if not reed_switch_triggered:
+                            logger.warning("Completed full rotation without reed switch trigger")
+
+                        # Set theta to the offset value (accounting for sensor placement)
+                        # If offset is 0, this is the true home position
+                        # If offset is non-zero, the sensor is physically placed at that angle
+                        # Convert degrees to radians for internal use
+                        import math
+                        offset_radians = math.radians(state.angular_homing_offset_degrees)
+                        state.current_theta = offset_radians
+                        state.current_rho = 1
+                        logger.info(f"Angular homing completed - theta set to {state.angular_homing_offset_degrees}° ({offset_radians:.3f} radians)")
+
+                    finally:
+                        reed_switch.cleanup()
+
+                except Exception as e:
+                    logger.error(f"Error during angular homing: {e}")
+                    # Continue with normal homing completion even if angular homing fails
+
+            homing_success = True
+            logger.info("Homing completed and device is idle")
 
 
             homing_complete.set()
             homing_complete.set()
         except Exception as e:
         except Exception as e:

+ 99 - 0
modules/connection/reed_switch.py

@@ -0,0 +1,99 @@
+"""
+Reed switch monitoring module for Raspberry Pi GPIO.
+Used for angular homing to detect home position.
+"""
+import logging
+import platform
+
+logger = logging.getLogger(__name__)
+
+class ReedSwitchMonitor:
+    """Monitor a reed switch connected to a Raspberry Pi GPIO pin."""
+
+    def __init__(self, gpio_pin=18):
+        """
+        Initialize the reed switch monitor.
+
+        Args:
+            gpio_pin: GPIO pin number (BCM numbering) for the reed switch
+        """
+        self.gpio_pin = gpio_pin
+        self.gpio = None
+        self.is_raspberry_pi = False
+
+        # Try to import and initialize GPIO
+        try:
+            import RPi.GPIO as GPIO
+            self.gpio = GPIO
+
+            # Set up GPIO mode (BCM numbering)
+            self.gpio.setmode(GPIO.BCM)
+
+            # Set up the pin as input with pull-up resistor
+            # Reed switch should connect pin to ground when triggered
+            self.gpio.setup(self.gpio_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
+
+            self.is_raspberry_pi = True
+            logger.info(f"Reed switch initialized on GPIO pin {self.gpio_pin}")
+        except ImportError:
+            logger.warning("RPi.GPIO not available. Reed switch monitoring disabled.")
+        except Exception as e:
+            logger.error(f"Error initializing reed switch: {e}")
+            logger.info("Reed switch monitoring disabled.")
+
+    def is_triggered(self):
+        """
+        Check if the reed switch is currently triggered.
+
+        Returns:
+            bool: True if reed switch is triggered (pin is HIGH), False otherwise
+        """
+        if not self.is_raspberry_pi or not self.gpio:
+            return False
+
+        try:
+            # Pin is HIGH (1) when reed switch is closed (triggered)
+            # This assumes the switch connects the pin to 3.3V when closed
+            return self.gpio.input(self.gpio_pin) == 1
+        except Exception as e:
+            logger.error(f"Error reading reed switch: {e}")
+            return False
+
+    def wait_for_trigger(self, timeout=None):
+        """
+        Wait for the reed switch to be triggered.
+
+        Args:
+            timeout: Maximum time to wait in seconds (None = wait indefinitely)
+
+        Returns:
+            bool: True if triggered, False if timeout occurred
+        """
+        if not self.is_raspberry_pi or not self.gpio:
+            logger.warning("Reed switch not available, cannot wait for trigger")
+            return False
+
+        try:
+            # Wait for rising edge (pin goes from LOW to HIGH)
+            channel = self.gpio.wait_for_edge(
+                self.gpio_pin,
+                self.gpio.RISING,
+                timeout=int(timeout * 1000) if timeout else None
+            )
+            return channel is not None
+        except Exception as e:
+            logger.error(f"Error waiting for reed switch trigger: {e}")
+            return False
+
+    def cleanup(self):
+        """Clean up GPIO resources."""
+        if self.is_raspberry_pi and self.gpio:
+            try:
+                self.gpio.cleanup(self.gpio_pin)
+                logger.info(f"Reed switch GPIO pin {self.gpio_pin} cleaned up")
+            except Exception as e:
+                logger.error(f"Error cleaning up reed switch GPIO: {e}")
+
+    def __del__(self):
+        """Destructor to ensure GPIO cleanup."""
+        self.cleanup()

+ 10 - 1
modules/core/state.py

@@ -34,7 +34,12 @@ class AppState:
         self.gear_ratio = 10
         self.gear_ratio = 10
         # 0 for crash homing, 1 for auto homing
         # 0 for crash homing, 1 for auto homing
         self.homing = 0
         self.homing = 0
-        
+        # Angular homing with reed switch (Raspberry Pi only)
+        self.angular_homing_enabled = False
+        # Angular offset in degrees for reed switch sensor placement
+        # This allows correcting for the physical position of the reed switch
+        self.angular_homing_offset_degrees = 0.0
+
         self.STATE_FILE = "state.json"
         self.STATE_FILE = "state.json"
         self.mqtt_handler = None  # Will be set by the MQTT handler
         self.mqtt_handler = None  # Will be set by the MQTT handler
         self.conn = None
         self.conn = None
@@ -179,6 +184,8 @@ class AppState:
             "y_steps_per_mm": self.y_steps_per_mm,
             "y_steps_per_mm": self.y_steps_per_mm,
             "gear_ratio": self.gear_ratio,
             "gear_ratio": self.gear_ratio,
             "homing": self.homing,
             "homing": self.homing,
+            "angular_homing_enabled": self.angular_homing_enabled,
+            "angular_homing_offset_degrees": self.angular_homing_offset_degrees,
             "current_playlist": self._current_playlist,
             "current_playlist": self._current_playlist,
             "current_playlist_name": self._current_playlist_name,
             "current_playlist_name": self._current_playlist_name,
             "current_playlist_index": self.current_playlist_index,
             "current_playlist_index": self.current_playlist_index,
@@ -218,6 +225,8 @@ class AppState:
         self.y_steps_per_mm = data.get("y_steps_per_mm", 0.0)
         self.y_steps_per_mm = data.get("y_steps_per_mm", 0.0)
         self.gear_ratio = data.get('gear_ratio', 10)
         self.gear_ratio = data.get('gear_ratio', 10)
         self.homing = data.get('homing', 0)
         self.homing = data.get('homing', 0)
+        self.angular_homing_enabled = data.get('angular_homing_enabled', False)
+        self.angular_homing_offset_degrees = data.get('angular_homing_offset_degrees', 0.0)
         self._current_playlist = data.get("current_playlist", None)
         self._current_playlist = data.get("current_playlist", None)
         self._current_playlist_name = data.get("current_playlist_name", None)
         self._current_playlist_name = data.get("current_playlist_name", None)
         self.current_playlist_index = data.get("current_playlist_index", None)
         self.current_playlist_index = data.get("current_playlist_index", None)

+ 2 - 1
requirements.txt

@@ -12,4 +12,5 @@ python-multipart>=0.0.6
 websockets>=11.0.3  # Required for FastAPI WebSocket support
 websockets>=11.0.3  # Required for FastAPI WebSocket support
 requests>=2.31.0
 requests>=2.31.0
 Pillow
 Pillow
-aiohttp
+aiohttp
+RPi.GPIO>=0.7.1; platform_machine == "aarch64" or platform_machine == "armv7l"  # Raspberry Pi GPIO support

+ 93 - 0
static/js/settings.js

@@ -1027,6 +1027,7 @@ async function initializeauto_playMode() {
 document.addEventListener('DOMContentLoaded', function() {
 document.addEventListener('DOMContentLoaded', function() {
     initializeauto_playMode();
     initializeauto_playMode();
     initializeStillSandsMode();
     initializeStillSandsMode();
+    initializeAngularHomingConfig();
 });
 });
 
 
 // Still Sands Mode Functions
 // Still Sands Mode Functions
@@ -1365,3 +1366,95 @@ async function initializeStillSandsMode() {
         });
         });
     }
     }
 }
 }
+
+// Angular Homing Configuration Functions
+async function initializeAngularHomingConfig() {
+    logMessage('Initializing Angular Homing configuration', LOG_TYPE.INFO);
+
+    const angularHomingToggle = document.getElementById('angularHomingToggle');
+    const angularHomingInfo = document.getElementById('angularHomingInfo');
+    const angularOffsetContainer = document.getElementById('angularOffsetContainer');
+    const angularOffsetInput = document.getElementById('angularOffsetInput');
+    const saveHomingConfigButton = document.getElementById('saveHomingConfig');
+
+    // Check if elements exist
+    if (!angularHomingToggle || !angularHomingInfo || !saveHomingConfigButton || !angularOffsetContainer || !angularOffsetInput) {
+        logMessage('Angular Homing elements not found, skipping initialization', LOG_TYPE.WARNING);
+        return;
+    }
+
+    logMessage('All Angular Homing elements found successfully', LOG_TYPE.INFO);
+
+    // Load current angular homing settings
+    try {
+        const response = await fetch('/api/angular-homing');
+        const data = await response.json();
+
+        angularHomingToggle.checked = data.angular_homing_enabled || false;
+        angularOffsetInput.value = data.angular_homing_offset_degrees || 0;
+
+        if (data.angular_homing_enabled) {
+            angularHomingInfo.style.display = 'block';
+            angularOffsetContainer.style.display = 'block';
+        }
+    } catch (error) {
+        logMessage(`Error loading angular homing settings: ${error.message}`, LOG_TYPE.ERROR);
+        // Initialize with defaults if load fails
+        angularHomingToggle.checked = false;
+        angularOffsetInput.value = 0;
+        angularHomingInfo.style.display = 'none';
+        angularOffsetContainer.style.display = 'none';
+    }
+
+    // Function to save settings
+    async function saveAngularHomingSettings() {
+        // Update button UI to show loading state
+        const originalButtonHTML = saveHomingConfigButton.innerHTML;
+        saveHomingConfigButton.disabled = true;
+        saveHomingConfigButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Saving...</span>';
+
+        try {
+            const response = await fetch('/api/angular-homing', {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({
+                    angular_homing_enabled: angularHomingToggle.checked,
+                    angular_homing_offset_degrees: parseFloat(angularOffsetInput.value) || 0
+                })
+            });
+
+            if (!response.ok) {
+                const errorData = await response.json();
+                throw new Error(errorData.detail || 'Failed to save angular homing settings');
+            }
+
+            // Show success state temporarily
+            saveHomingConfigButton.innerHTML = '<span class="material-icons text-lg">check</span><span class="truncate">Saved!</span>';
+            showStatusMessage('Angular homing configuration saved successfully', 'success');
+
+            // Restore button after 2 seconds
+            setTimeout(() => {
+                saveHomingConfigButton.innerHTML = originalButtonHTML;
+                saveHomingConfigButton.disabled = false;
+            }, 2000);
+        } catch (error) {
+            logMessage(`Error saving angular homing settings: ${error.message}`, LOG_TYPE.ERROR);
+            showStatusMessage(`Failed to save settings: ${error.message}`, 'error');
+
+            // Restore button immediately on error
+            saveHomingConfigButton.innerHTML = originalButtonHTML;
+            saveHomingConfigButton.disabled = false;
+        }
+    }
+
+    // Event listeners
+    angularHomingToggle.addEventListener('change', () => {
+        logMessage(`Angular homing toggle changed: ${angularHomingToggle.checked}`, LOG_TYPE.INFO);
+        const isEnabled = angularHomingToggle.checked;
+        angularHomingInfo.style.display = isEnabled ? 'block' : 'none';
+        angularOffsetContainer.style.display = isEnabled ? 'block' : 'none';
+        logMessage(`Info display set to: ${angularHomingInfo.style.display}`, LOG_TYPE.INFO);
+    });
+
+    saveHomingConfigButton.addEventListener('click', saveAngularHomingSettings);
+}

+ 71 - 0
templates/settings.html

@@ -308,6 +308,77 @@ input:checked + .slider:before {
       </div>
       </div>
     </div>
     </div>
   </section>
   </section>
+  <section class="bg-white rounded-xl shadow-sm overflow-hidden">
+    <h2
+      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+    >
+      Homing Configuration
+    </h2>
+    <div class="px-6 py-5 space-y-6">
+      <div class="flex items-center justify-between">
+        <div class="flex-1">
+          <h3 class="text-slate-700 text-base font-medium leading-normal flex items-center gap-2">
+            <span class="material-icons text-slate-600">explore</span>
+            Angular Homing (Raspberry Pi Only)
+          </h3>
+          <p class="text-xs text-slate-500 mt-1">
+            Enable angular homing using a reed switch on GPIO 18 to establish a home position for rotation.
+          </p>
+        </div>
+        <label class="switch">
+          <input type="checkbox" id="angularHomingToggle">
+          <span class="slider round"></span>
+        </label>
+      </div>
+
+      <!-- Angular Offset Input (shown when angular homing is enabled) -->
+      <div id="angularOffsetContainer" class="space-y-2" style="display: none;">
+        <label for="angularOffsetInput" class="text-sm font-medium text-slate-700 flex items-center gap-2">
+          <span class="material-icons text-slate-600 text-base">straighten</span>
+          Sensor Offset (degrees)
+        </label>
+        <input
+          type="number"
+          id="angularOffsetInput"
+          min="0"
+          max="360"
+          step="0.1"
+          value="0"
+          class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-sky-500 focus:border-sky-500 text-sm"
+          placeholder="0.0"
+        />
+        <p class="text-xs text-slate-500">
+          Set the angle (in degrees) where your reed switch is physically mounted. 0° = East, increases clockwise (90° = South, 180° = West, 270° = North).
+        </p>
+      </div>
+
+      <div id="angularHomingInfo" class="text-xs text-slate-600 bg-blue-50 border border-blue-200 rounded-lg p-3" style="display: none;">
+        <div class="flex items-start gap-2">
+          <span class="material-icons text-blue-600 text-base">info</span>
+          <div>
+            <p class="font-medium text-blue-800">Angular Homing Details:</p>
+            <ul class="mt-1 space-y-1 text-blue-700">
+              <li>• After radial homing, the arm moves to the perimeter (y20)</li>
+              <li>• The table rotates around the perimeter until the reed switch on GPIO 18 is triggered</li>
+              <li>• This position is set as the angular home based on your sensor offset</li>
+              <li>• Only works on Raspberry Pi with a reed switch connected to GPIO 18</li>
+              <li>• Reed switch should connect GPIO 18 to ground when triggered</li>
+            </ul>
+          </div>
+        </div>
+      </div>
+
+      <div class="flex justify-end">
+        <button
+          id="saveHomingConfig"
+          class="flex items-center justify-center gap-2 min-w-[140px] cursor-pointer rounded-lg h-10 px-4 bg-sky-600 hover:bg-sky-700 text-white text-sm font-medium leading-normal tracking-[0.015em] transition-colors"
+        >
+          <span class="material-icons text-lg">save</span>
+          <span class="truncate">Save Homing Config</span>
+        </button>
+      </div>
+    </div>
+  </section>
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
     <h2
       class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
       class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"

+ 88 - 0
test_reed_switch.py

@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+"""
+Simple test script to verify reed switch functionality on GPIO 18.
+Run this script on your Raspberry Pi to test the reed switch.
+
+Usage:
+    python test_reed_switch.py
+"""
+
+import time
+import sys
+
+try:
+    from modules.connection.reed_switch import ReedSwitchMonitor
+except ImportError:
+    print("Error: Could not import ReedSwitchMonitor")
+    print("Make sure you're running this from the dune-weaver directory")
+    sys.exit(1)
+
+def main():
+    print("=" * 60)
+    print("Reed Switch Test - GPIO 18")
+    print("=" * 60)
+    print()
+
+    # Initialize the reed switch monitor
+    print("Initializing reed switch monitor on GPIO 18...")
+    reed_switch = ReedSwitchMonitor(gpio_pin=18)
+
+    # Check if we're on a Raspberry Pi
+    if not reed_switch.is_raspberry_pi:
+        print("❌ ERROR: Not running on a Raspberry Pi!")
+        print("This test must be run on a Raspberry Pi with GPIO support.")
+        return
+
+    print("✓ Running on Raspberry Pi")
+    print("✓ GPIO initialized successfully")
+    print()
+    print("=" * 60)
+    print("MONITORING REED SWITCH")
+    print("=" * 60)
+    print()
+    print("Instructions:")
+    print("  • The reed switch should be connected:")
+    print("    - One terminal → GPIO 18")
+    print("    - Other terminal → Ground (any GND pin)")
+    print()
+    print("  • Bring a magnet close to the reed switch to trigger it")
+    print("  • You should see 'TRIGGERED!' when the switch closes")
+    print("  • Press Ctrl+C to exit")
+    print()
+    print("-" * 60)
+
+    try:
+        last_state = None
+        trigger_count = 0
+
+        while True:
+            # Check if reed switch is triggered
+            is_triggered = reed_switch.is_triggered()
+
+            # Only print when state changes (to avoid spam)
+            if is_triggered != last_state:
+                if is_triggered:
+                    trigger_count += 1
+                    print(f"🔴 TRIGGERED! (count: {trigger_count})")
+                else:
+                    print("⚪ Not triggered")
+
+                last_state = is_triggered
+
+            # Small delay to avoid overwhelming the GPIO
+            time.sleep(0.05)
+
+    except KeyboardInterrupt:
+        print()
+        print("-" * 60)
+        print(f"✓ Test completed. Reed switch was triggered {trigger_count} times.")
+        print()
+
+    finally:
+        # Clean up GPIO
+        reed_switch.cleanup()
+        print("✓ GPIO cleaned up")
+        print()
+
+if __name__ == "__main__":
+    main()