tuanchris 3 месяцев назад
Родитель
Сommit
03e7dd7781

+ 13 - 1
main.py

@@ -385,23 +385,35 @@ async def get_angular_homing():
     """Get current angular homing settings."""
     """Get current angular homing settings."""
     return {
     return {
         "angular_homing_enabled": state.angular_homing_enabled,
         "angular_homing_enabled": state.angular_homing_enabled,
+        "angular_homing_gpio_pin": state.angular_homing_gpio_pin,
+        "angular_homing_invert_state": state.angular_homing_invert_state,
         "angular_homing_offset_degrees": state.angular_homing_offset_degrees
         "angular_homing_offset_degrees": state.angular_homing_offset_degrees
     }
     }
 
 
 class AngularHomingRequest(BaseModel):
 class AngularHomingRequest(BaseModel):
     angular_homing_enabled: bool
     angular_homing_enabled: bool
+    angular_homing_gpio_pin: int = 18
+    angular_homing_invert_state: bool = False
     angular_homing_offset_degrees: float = 0.0
     angular_homing_offset_degrees: float = 0.0
 
 
 @app.post("/api/angular-homing")
 @app.post("/api/angular-homing")
 async def set_angular_homing(request: AngularHomingRequest):
 async def set_angular_homing(request: AngularHomingRequest):
     """Update angular homing settings."""
     """Update angular homing settings."""
     try:
     try:
+        # Validate GPIO pin
+        if request.angular_homing_gpio_pin < 2 or request.angular_homing_gpio_pin > 27:
+            raise HTTPException(status_code=400, detail="GPIO pin must be between 2 and 27")
+
         state.angular_homing_enabled = request.angular_homing_enabled
         state.angular_homing_enabled = request.angular_homing_enabled
+        state.angular_homing_gpio_pin = request.angular_homing_gpio_pin
+        state.angular_homing_invert_state = request.angular_homing_invert_state
         state.angular_homing_offset_degrees = request.angular_homing_offset_degrees
         state.angular_homing_offset_degrees = request.angular_homing_offset_degrees
         state.save()
         state.save()
 
 
-        logger.info(f"Angular homing {'enabled' if request.angular_homing_enabled else 'disabled'}, offset: {request.angular_homing_offset_degrees}°")
+        logger.info(f"Angular homing {'enabled' if request.angular_homing_enabled else 'disabled'}, GPIO pin: {request.angular_homing_gpio_pin}, invert: {request.angular_homing_invert_state}, offset: {request.angular_homing_offset_degrees}°")
         return {"success": True, "message": "Angular homing settings updated"}
         return {"success": True, "message": "Angular homing settings updated"}
+    except HTTPException:
+        raise
     except Exception as e:
     except Exception as e:
         logger.error(f"Error updating angular homing settings: {str(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)}")
         raise HTTPException(status_code=500, detail=f"Failed to update angular homing settings: {str(e)}")

+ 5 - 2
modules/connection/connection_manager.py

@@ -463,8 +463,11 @@ def home(timeout=90):
             if state.angular_homing_enabled:
             if state.angular_homing_enabled:
                 logger.info("Starting angular homing sequence")
                 logger.info("Starting angular homing sequence")
                 try:
                 try:
-                    # Initialize reed switch monitor
-                    reed_switch = ReedSwitchMonitor(gpio_pin=18)
+                    # Initialize reed switch monitor with configured GPIO pin and invert state
+                    gpio_pin = state.angular_homing_gpio_pin
+                    invert_state = state.angular_homing_invert_state
+                    logger.info(f"Using GPIO pin {gpio_pin} for reed switch (invert_state={invert_state})")
+                    reed_switch = ReedSwitchMonitor(gpio_pin=gpio_pin, invert_state=invert_state)
 
 
                     try:
                     try:
                         # Reset theta first
                         # Reset theta first

+ 19 - 6
modules/connection/reed_switch.py

@@ -10,14 +10,16 @@ logger = logging.getLogger(__name__)
 class ReedSwitchMonitor:
 class ReedSwitchMonitor:
     """Monitor a reed switch connected to a Raspberry Pi GPIO pin."""
     """Monitor a reed switch connected to a Raspberry Pi GPIO pin."""
 
 
-    def __init__(self, gpio_pin=18):
+    def __init__(self, gpio_pin=18, invert_state=False):
         """
         """
         Initialize the reed switch monitor.
         Initialize the reed switch monitor.
 
 
         Args:
         Args:
             gpio_pin: GPIO pin number (BCM numbering) for the reed switch
             gpio_pin: GPIO pin number (BCM numbering) for the reed switch
+            invert_state: If True, invert the logic (triggered = LOW instead of HIGH)
         """
         """
         self.gpio_pin = gpio_pin
         self.gpio_pin = gpio_pin
+        self.invert_state = invert_state
         self.gpio = None
         self.gpio = None
         self.is_raspberry_pi = False
         self.is_raspberry_pi = False
 
 
@@ -34,7 +36,7 @@ class ReedSwitchMonitor:
             self.gpio.setup(self.gpio_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
             self.gpio.setup(self.gpio_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
 
 
             self.is_raspberry_pi = True
             self.is_raspberry_pi = True
-            logger.info(f"Reed switch initialized on GPIO pin {self.gpio_pin}")
+            logger.info(f"Reed switch initialized on GPIO pin {self.gpio_pin} (invert_state={self.invert_state})")
         except ImportError:
         except ImportError:
             logger.warning("RPi.GPIO not available. Reed switch monitoring disabled.")
             logger.warning("RPi.GPIO not available. Reed switch monitoring disabled.")
         except Exception as e:
         except Exception as e:
@@ -46,15 +48,26 @@ class ReedSwitchMonitor:
         Check if the reed switch is currently triggered.
         Check if the reed switch is currently triggered.
 
 
         Returns:
         Returns:
-            bool: True if reed switch is triggered (pin is HIGH), False otherwise
+            bool: True if reed switch is triggered, False otherwise
+
+        Notes:
+            - If invert_state=False: triggered when pin is HIGH (1)
+            - If invert_state=True: triggered when pin is LOW (0)
         """
         """
         if not self.is_raspberry_pi or not self.gpio:
         if not self.is_raspberry_pi or not self.gpio:
             return False
             return False
 
 
         try:
         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
+            # Read the GPIO pin state
+            pin_state = self.gpio.input(self.gpio_pin)
+
+            # Apply inversion if configured
+            if self.invert_state:
+                # Inverted: triggered when LOW (0)
+                return pin_state == 0
+            else:
+                # Normal: triggered when HIGH (1)
+                return pin_state == 1
         except Exception as e:
         except Exception as e:
             logger.error(f"Error reading reed switch: {e}")
             logger.error(f"Error reading reed switch: {e}")
             return False
             return False

+ 8 - 0
modules/core/state.py

@@ -36,6 +36,10 @@ class AppState:
         self.homing = 0
         self.homing = 0
         # Angular homing with reed switch (Raspberry Pi only)
         # Angular homing with reed switch (Raspberry Pi only)
         self.angular_homing_enabled = False
         self.angular_homing_enabled = False
+        # GPIO pin number (BCM numbering) for reed switch
+        self.angular_homing_gpio_pin = 18
+        # Invert the reed switch state (False = triggered on HIGH, True = triggered on LOW)
+        self.angular_homing_invert_state = False
         # Angular offset in degrees for reed switch sensor placement
         # Angular offset in degrees for reed switch sensor placement
         # This allows correcting for the physical position of the reed switch
         # This allows correcting for the physical position of the reed switch
         self.angular_homing_offset_degrees = 0.0
         self.angular_homing_offset_degrees = 0.0
@@ -185,6 +189,8 @@ class AppState:
             "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_enabled": self.angular_homing_enabled,
+            "angular_homing_gpio_pin": self.angular_homing_gpio_pin,
+            "angular_homing_invert_state": self.angular_homing_invert_state,
             "angular_homing_offset_degrees": self.angular_homing_offset_degrees,
             "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,
@@ -226,6 +232,8 @@ class AppState:
         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_enabled = data.get('angular_homing_enabled', False)
+        self.angular_homing_gpio_pin = data.get('angular_homing_gpio_pin', 18)
+        self.angular_homing_invert_state = data.get('angular_homing_invert_state', False)
         self.angular_homing_offset_degrees = data.get('angular_homing_offset_degrees', 0.0)
         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)

+ 26 - 1
static/js/settings.js

@@ -1373,12 +1373,18 @@ async function initializeAngularHomingConfig() {
 
 
     const angularHomingToggle = document.getElementById('angularHomingToggle');
     const angularHomingToggle = document.getElementById('angularHomingToggle');
     const angularHomingInfo = document.getElementById('angularHomingInfo');
     const angularHomingInfo = document.getElementById('angularHomingInfo');
+    const gpioSelectionContainer = document.getElementById('gpioSelectionContainer');
+    const gpioInput = document.getElementById('gpioInput');
+    const invertStateContainer = document.getElementById('invertStateContainer');
+    const invertStateToggle = document.getElementById('invertStateToggle');
     const angularOffsetContainer = document.getElementById('angularOffsetContainer');
     const angularOffsetContainer = document.getElementById('angularOffsetContainer');
     const angularOffsetInput = document.getElementById('angularOffsetInput');
     const angularOffsetInput = document.getElementById('angularOffsetInput');
     const saveHomingConfigButton = document.getElementById('saveHomingConfig');
     const saveHomingConfigButton = document.getElementById('saveHomingConfig');
 
 
     // Check if elements exist
     // Check if elements exist
-    if (!angularHomingToggle || !angularHomingInfo || !saveHomingConfigButton || !angularOffsetContainer || !angularOffsetInput) {
+    if (!angularHomingToggle || !angularHomingInfo || !saveHomingConfigButton ||
+        !gpioSelectionContainer || !gpioInput || !invertStateContainer ||
+        !invertStateToggle || !angularOffsetContainer || !angularOffsetInput) {
         logMessage('Angular Homing elements not found, skipping initialization', LOG_TYPE.WARNING);
         logMessage('Angular Homing elements not found, skipping initialization', LOG_TYPE.WARNING);
         return;
         return;
     }
     }
@@ -1391,23 +1397,38 @@ async function initializeAngularHomingConfig() {
         const data = await response.json();
         const data = await response.json();
 
 
         angularHomingToggle.checked = data.angular_homing_enabled || false;
         angularHomingToggle.checked = data.angular_homing_enabled || false;
+        gpioInput.value = data.angular_homing_gpio_pin || 18;
+        invertStateToggle.checked = data.angular_homing_invert_state || false;
         angularOffsetInput.value = data.angular_homing_offset_degrees || 0;
         angularOffsetInput.value = data.angular_homing_offset_degrees || 0;
 
 
         if (data.angular_homing_enabled) {
         if (data.angular_homing_enabled) {
             angularHomingInfo.style.display = 'block';
             angularHomingInfo.style.display = 'block';
+            gpioSelectionContainer.style.display = 'block';
+            invertStateContainer.style.display = 'block';
             angularOffsetContainer.style.display = 'block';
             angularOffsetContainer.style.display = 'block';
         }
         }
     } catch (error) {
     } catch (error) {
         logMessage(`Error loading angular homing settings: ${error.message}`, LOG_TYPE.ERROR);
         logMessage(`Error loading angular homing settings: ${error.message}`, LOG_TYPE.ERROR);
         // Initialize with defaults if load fails
         // Initialize with defaults if load fails
         angularHomingToggle.checked = false;
         angularHomingToggle.checked = false;
+        gpioInput.value = 18;
+        invertStateToggle.checked = false;
         angularOffsetInput.value = 0;
         angularOffsetInput.value = 0;
         angularHomingInfo.style.display = 'none';
         angularHomingInfo.style.display = 'none';
+        gpioSelectionContainer.style.display = 'none';
+        invertStateContainer.style.display = 'none';
         angularOffsetContainer.style.display = 'none';
         angularOffsetContainer.style.display = 'none';
     }
     }
 
 
     // Function to save settings
     // Function to save settings
     async function saveAngularHomingSettings() {
     async function saveAngularHomingSettings() {
+        // Validate GPIO pin
+        const gpioPin = parseInt(gpioInput.value);
+        if (isNaN(gpioPin) || gpioPin < 2 || gpioPin > 27) {
+            showStatusMessage('GPIO pin must be between 2 and 27', 'error');
+            return;
+        }
+
         // Update button UI to show loading state
         // Update button UI to show loading state
         const originalButtonHTML = saveHomingConfigButton.innerHTML;
         const originalButtonHTML = saveHomingConfigButton.innerHTML;
         saveHomingConfigButton.disabled = true;
         saveHomingConfigButton.disabled = true;
@@ -1419,6 +1440,8 @@ async function initializeAngularHomingConfig() {
                 headers: { 'Content-Type': 'application/json' },
                 headers: { 'Content-Type': 'application/json' },
                 body: JSON.stringify({
                 body: JSON.stringify({
                     angular_homing_enabled: angularHomingToggle.checked,
                     angular_homing_enabled: angularHomingToggle.checked,
+                    angular_homing_gpio_pin: gpioPin,
+                    angular_homing_invert_state: invertStateToggle.checked,
                     angular_homing_offset_degrees: parseFloat(angularOffsetInput.value) || 0
                     angular_homing_offset_degrees: parseFloat(angularOffsetInput.value) || 0
                 })
                 })
             });
             });
@@ -1452,6 +1475,8 @@ async function initializeAngularHomingConfig() {
         logMessage(`Angular homing toggle changed: ${angularHomingToggle.checked}`, LOG_TYPE.INFO);
         logMessage(`Angular homing toggle changed: ${angularHomingToggle.checked}`, LOG_TYPE.INFO);
         const isEnabled = angularHomingToggle.checked;
         const isEnabled = angularHomingToggle.checked;
         angularHomingInfo.style.display = isEnabled ? 'block' : 'none';
         angularHomingInfo.style.display = isEnabled ? 'block' : 'none';
+        gpioSelectionContainer.style.display = isEnabled ? 'block' : 'none';
+        invertStateContainer.style.display = isEnabled ? 'block' : 'none';
         angularOffsetContainer.style.display = isEnabled ? 'block' : 'none';
         angularOffsetContainer.style.display = isEnabled ? 'block' : 'none';
         logMessage(`Info display set to: ${angularHomingInfo.style.display}`, LOG_TYPE.INFO);
         logMessage(`Info display set to: ${angularHomingInfo.style.display}`, LOG_TYPE.INFO);
     });
     });

+ 46 - 4
templates/settings.html

@@ -322,7 +322,7 @@ input:checked + .slider:before {
             Angular Homing (Raspberry Pi Only)
             Angular Homing (Raspberry Pi Only)
           </h3>
           </h3>
           <p class="text-xs text-slate-500 mt-1">
           <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.
+            Enable angular homing using a reed switch connected to a GPIO pin to establish a home position for rotation.
           </p>
           </p>
         </div>
         </div>
         <label class="switch">
         <label class="switch">
@@ -331,6 +331,46 @@ input:checked + .slider:before {
         </label>
         </label>
       </div>
       </div>
 
 
+      <!-- GPIO Pin Selection (shown when angular homing is enabled) -->
+      <div id="gpioSelectionContainer" class="space-y-2" style="display: none;">
+        <label for="gpioInput" class="text-sm font-medium text-slate-700 flex items-center gap-2">
+          <span class="material-icons text-slate-600 text-base">settings_input_component</span>
+          GPIO Pin Number
+        </label>
+        <input
+          type="number"
+          id="gpioInput"
+          min="2"
+          max="27"
+          step="1"
+          value="18"
+          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="18"
+        />
+        <p class="text-xs text-slate-500">
+          GPIO pin number (BCM numbering) where the reed switch is connected. Common pins: 17, 18, 22, 23, 24, 25, 27.
+        </p>
+      </div>
+
+      <!-- Invert State Toggle (shown when angular homing is enabled) -->
+      <div id="invertStateContainer" class="space-y-2" style="display: none;">
+        <div class="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
+          <div class="flex-1">
+            <label for="invertStateToggle" class="text-sm font-medium text-slate-700 flex items-center gap-2 cursor-pointer">
+              <span class="material-icons text-slate-600 text-base">swap_vert</span>
+              Invert Reed Switch State
+            </label>
+            <p class="text-xs text-slate-500 mt-1">
+              Enable if your reed switch is triggered when LOW instead of HIGH (normally closed configuration).
+            </p>
+          </div>
+          <label class="switch">
+            <input type="checkbox" id="invertStateToggle">
+            <span class="slider round"></span>
+          </label>
+        </div>
+      </div>
+
       <!-- Angular Offset Input (shown when angular homing is enabled) -->
       <!-- Angular Offset Input (shown when angular homing is enabled) -->
       <div id="angularOffsetContainer" class="space-y-2" style="display: none;">
       <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">
         <label for="angularOffsetInput" class="text-sm font-medium text-slate-700 flex items-center gap-2">
@@ -359,10 +399,12 @@ input:checked + .slider:before {
             <p class="font-medium text-blue-800">Angular Homing Details:</p>
             <p class="font-medium text-blue-800">Angular Homing Details:</p>
             <ul class="mt-1 space-y-1 text-blue-700">
             <ul class="mt-1 space-y-1 text-blue-700">
               <li>• After radial homing, the arm moves to the perimeter (y20)</li>
               <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>• The table rotates around the perimeter until the reed switch is triggered</li>
               <li>• This position is set as the angular home based on your sensor offset</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>
+              <li>• Only works on Raspberry Pi with a reed switch connected to your selected GPIO pin</li>
+              <li>• Standard wiring: Reed switch connects GPIO pin to 3.3V (triggered = HIGH)</li>
+              <li>• Inverted wiring: Reed switch connects GPIO pin to ground (triggered = LOW)</li>
+              <li>• Use BCM GPIO numbering (not physical pin numbers)</li>
             </ul>
             </ul>
           </div>
           </div>
         </div>
         </div>

+ 76 - 9
test_reed_switch.py

@@ -1,14 +1,24 @@
 #!/usr/bin/env python3
 #!/usr/bin/env python3
 """
 """
-Simple test script to verify reed switch functionality on GPIO 18.
+Simple test script to verify reed switch functionality.
 Run this script on your Raspberry Pi to test the reed switch.
 Run this script on your Raspberry Pi to test the reed switch.
 
 
 Usage:
 Usage:
-    python test_reed_switch.py
+    python test_reed_switch.py [--gpio PIN] [--invert]
+
+Arguments:
+    --gpio PIN    GPIO pin number (BCM numbering) to test (default: 18)
+    --invert      Invert the switch logic (triggered = LOW instead of HIGH)
+
+Examples:
+    python test_reed_switch.py                    # Test GPIO 18 (default, normal logic)
+    python test_reed_switch.py --gpio 17          # Test GPIO 17 (normal logic)
+    python test_reed_switch.py --gpio 22 --invert # Test GPIO 22 (inverted logic)
 """
 """
 
 
 import time
 import time
 import sys
 import sys
+import argparse
 
 
 try:
 try:
     from modules.connection.reed_switch import ReedSwitchMonitor
     from modules.connection.reed_switch import ReedSwitchMonitor
@@ -17,15 +27,30 @@ except ImportError:
     print("Make sure you're running this from the dune-weaver directory")
     print("Make sure you're running this from the dune-weaver directory")
     sys.exit(1)
     sys.exit(1)
 
 
-def main():
+def main(gpio_pin=18, invert_state=False):
+    """
+    Test the reed switch on the specified GPIO pin.
+
+    Args:
+        gpio_pin: GPIO pin number (BCM numbering) to test
+        invert_state: If True, invert the switch logic (triggered = LOW)
+    """
     print("=" * 60)
     print("=" * 60)
-    print("Reed Switch Test - GPIO 18")
+    print(f"Reed Switch Test - GPIO {gpio_pin}")
+    if invert_state:
+        print("(Inverted Logic: Triggered = LOW)")
+    else:
+        print("(Normal Logic: Triggered = HIGH)")
     print("=" * 60)
     print("=" * 60)
     print()
     print()
 
 
     # Initialize the reed switch monitor
     # Initialize the reed switch monitor
-    print("Initializing reed switch monitor on GPIO 18...")
-    reed_switch = ReedSwitchMonitor(gpio_pin=18)
+    print(f"Initializing reed switch monitor on GPIO {gpio_pin}...")
+    if invert_state:
+        print("Using inverted logic (triggered when pin is LOW)")
+    else:
+        print("Using normal logic (triggered when pin is HIGH)")
+    reed_switch = ReedSwitchMonitor(gpio_pin=gpio_pin, invert_state=invert_state)
 
 
     # Check if we're on a Raspberry Pi
     # Check if we're on a Raspberry Pi
     if not reed_switch.is_raspberry_pi:
     if not reed_switch.is_raspberry_pi:
@@ -42,8 +67,13 @@ def main():
     print()
     print()
     print("Instructions:")
     print("Instructions:")
     print("  • The reed switch should be connected:")
     print("  • The reed switch should be connected:")
-    print("    - One terminal → GPIO 18")
-    print("    - Other terminal → Ground (any GND pin)")
+    print(f"    - One terminal → GPIO {gpio_pin}")
+    if invert_state:
+        print("    - Other terminal → Ground (for inverted logic)")
+        print("    - Pull-up resistor enabled internally")
+    else:
+        print("    - Other terminal → 3.3V (for normal logic)")
+        print("    - Or use internal pull-up and connect to ground")
     print()
     print()
     print("  • Bring a magnet close to the reed switch to trigger it")
     print("  • Bring a magnet close to the reed switch to trigger it")
     print("  • You should see 'TRIGGERED!' when the switch closes")
     print("  • You should see 'TRIGGERED!' when the switch closes")
@@ -85,4 +115,41 @@ def main():
         print()
         print()
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
-    main()
+    # Parse command-line arguments
+    parser = argparse.ArgumentParser(
+        description="Test reed switch functionality on Raspberry Pi GPIO pins",
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+Examples:
+  python test_reed_switch.py                    # Test GPIO 18 (normal logic)
+  python test_reed_switch.py --gpio 17          # Test GPIO 17 (normal logic)
+  python test_reed_switch.py --gpio 22 --invert # Test GPIO 22 (inverted logic)
+
+Note: Uses BCM GPIO numbering (not physical pin numbers)
+      Normal logic: Triggered when HIGH (connected to 3.3V)
+      Inverted logic: Triggered when LOW (connected to ground)
+        """
+    )
+    parser.add_argument(
+        '--gpio',
+        type=int,
+        default=18,
+        metavar='PIN',
+        help='GPIO pin number to test (BCM numbering, default: 18)'
+    )
+    parser.add_argument(
+        '--invert',
+        action='store_true',
+        help='Invert the switch logic (triggered = LOW instead of HIGH)'
+    )
+
+    args = parser.parse_args()
+
+    # Validate GPIO pin range
+    if args.gpio < 2 or args.gpio > 27:
+        print(f"❌ ERROR: GPIO pin must be between 2 and 27 (got {args.gpio})")
+        print("Valid GPIO pins: 2-27 (BCM numbering)")
+        sys.exit(1)
+
+    # Run the test
+    main(gpio_pin=args.gpio, invert_state=args.invert)