diff --git a/CLAUDE.md b/CLAUDE.md index ffde3f0..78ac883 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -145,6 +145,19 @@ sudo usermod -a -G gpio $USER # Then log out and back in ``` +**GPIO pin conflicts:** +```bash +# Show safe GPIO pins and current configuration +uv run python run.py gpio-info + +# Show pins during GPIO service startup +uv run python run.py gpio --show-pins +``` + +**Safe GPIO pins for buttons:** 4, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27 + +**Avoid these pins:** 5, 6, 7, 9 (SPI), 14, 15 (UART), 2, 3 (I2C) + **Form validation errors (422):** - Check FastAPI endpoint parameter types - Handle empty strings vs None for optional fields diff --git a/src/ecm_control/__main__.py b/src/ecm_control/__main__.py index c13d1fe..c0b4c34 100644 --- a/src/ecm_control/__main__.py +++ b/src/ecm_control/__main__.py @@ -36,6 +36,13 @@ def main(): choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'], help='Logging level') gpio_parser.add_argument('--log-file', help='Log file path') + gpio_parser.add_argument('--show-pins', action='store_true', help='Show GPIO pin information and exit') + + # GPIO info command + gpio_info_parser = subparsers.add_parser('gpio-info', help='Show GPIO pin information') + gpio_info_parser.add_argument('--log-level', default='INFO', + choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'], + help='Logging level') # Database management commands db_parser = subparsers.add_parser('db', help='Database management') @@ -68,6 +75,8 @@ def main(): return run_web_server(args) elif args.command == 'gpio': return asyncio.run(run_gpio_service(args)) + elif args.command == 'gpio-info': + return show_gpio_info(args) elif args.command == 'db': return run_db_command(args) else: @@ -103,10 +112,17 @@ def run_web_server(args): async def run_gpio_service(args): """Run the GPIO control service""" - from .gpio import ECMController + from .gpio import ECMController, ButtonManager from .utils.logging_config import configure_logging_from_settings logger = get_logger('gpio') + + # Handle --show-pins option + if hasattr(args, 'show_pins') and args.show_pins: + button_manager = ButtonManager() + print(button_manager.get_safe_pins_info()) + return 0 + logger.info("Starting GPIO control service") # Configure logging from database settings @@ -126,6 +142,40 @@ async def run_gpio_service(args): return 0 +def show_gpio_info(args): + """Show GPIO pin information""" + from .gpio import ButtonManager + + button_manager = ButtonManager() + print(button_manager.get_safe_pins_info()) + + # Also show current button configuration from database + try: + db = DatabaseManager() + buttons = db.execute_query("SELECT * FROM buttons ORDER BY name") + + if buttons: + print("\nCurrent Button Configuration:") + for button in buttons: + status = "✅ SAFE" if button['gpio_pin'] in button_manager.SAFE_GPIO_PINS else "⚠️ RISKY" + recipe_info = "" + if button['recipe_id']: + recipe = db.execute_query("SELECT name FROM recipes WHERE id = ?", (button['recipe_id'],)) + if recipe: + recipe_info = f" → {recipe[0]['name']}" + + print(f" Button {button['id']}: {button['name']} on GPIO {button['gpio_pin']} {status}{recipe_info}") + + if button['gpio_pin'] in button_manager.RESERVED_GPIO_PINS: + print(f" ⚠️ Warning: {button_manager.RESERVED_GPIO_PINS[button['gpio_pin']]}") + else: + print("\nNo buttons configured yet.") + + except Exception as e: + print(f"\nError reading button configuration: {e}") + + return 0 + def run_db_command(args): """Run database management commands""" logger = get_logger('db') diff --git a/src/ecm_control/gpio/__init__.py b/src/ecm_control/gpio/__init__.py index 9fcb294..5018935 100644 --- a/src/ecm_control/gpio/__init__.py +++ b/src/ecm_control/gpio/__init__.py @@ -96,6 +96,29 @@ class CoffeeMachineController: class ButtonManager: """Manages physical buttons and their GPIO pins""" + # GPIO pins that are typically safe to use for input buttons on Raspberry Pi + SAFE_GPIO_PINS = [4, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27] + + # GPIO pins that are often reserved or have special functions + RESERVED_GPIO_PINS = { + 0: "I2C ID EEPROM - SDA", + 1: "I2C ID EEPROM - SCL", + 2: "I2C1 SDA", + 3: "I2C1 SCL", + 5: "SPI0 CE1 (may be reserved)", + 6: "SPI0 CE2 (may be reserved)", + 7: "SPI0 CE1 (may be reserved)", + 8: "SPI0 CE0", + 9: "SPI0 MISO", + 10: "SPI0 MOSI", + 11: "SPI0 SCLK", + 12: "PWM0 (PCM_CLK)", + 13: "PWM1 (PCM_FS)", + 14: "UART TXD", + 15: "UART RXD", + 16: "SPI1 CE2" + } + def __init__(self, use_mock: bool = False): self.use_mock = use_mock self.buttons: Dict[int, GPIOButton | MockGPIO] = {} @@ -103,26 +126,76 @@ class ButtonManager: def add_button(self, button_id: int, gpio_pin: int, handler: Callable): """Add a button with its handler""" + + # Validate GPIO pin + if not self.use_mock and not self._validate_gpio_pin(gpio_pin): + logger.warning(f"GPIO pin {gpio_pin} may not be suitable for button input") + try: if self.use_mock: button = MockGPIO(gpio_pin) + logger.info(f"Button {button_id} added on GPIO {gpio_pin} (MOCK)") else: + # Try with pull_up first button = GPIOButton(gpio_pin, pull_up=True, bounce_time=0.2) + logger.info(f"Button {button_id} added on GPIO {gpio_pin} (HARDWARE)") button.when_pressed = lambda: handler(button_id) self.buttons[button_id] = button self.button_handlers[button_id] = handler - logger.info(f"Button {button_id} added on GPIO {gpio_pin}") - except Exception as e: logger.error(f"Failed to add button {button_id} on GPIO {gpio_pin}: {e}") - # Fall back to mock + + # Provide helpful error information + if gpio_pin in self.RESERVED_GPIO_PINS: + logger.error(f"GPIO {gpio_pin} is typically reserved for: {self.RESERVED_GPIO_PINS[gpio_pin]}") + logger.error(f"Consider using one of these safer GPIO pins: {self.SAFE_GPIO_PINS}") + + # Try without pull_up as fallback + if not self.use_mock: + try: + logger.info(f"Retrying GPIO {gpio_pin} without internal pull-up (external pull-up required)") + button = GPIOButton(gpio_pin, pull_up=False, bounce_time=0.2) + button.when_pressed = lambda: handler(button_id) + self.buttons[button_id] = button + self.button_handlers[button_id] = handler + logger.info(f"Button {button_id} added on GPIO {gpio_pin} (no pull-up)") + return + except Exception as e2: + logger.error(f"Retry also failed for GPIO {gpio_pin}: {e2}") + + # Final fallback to mock + logger.warning(f"Falling back to mock button for button {button_id}") button = MockGPIO(gpio_pin) button.when_pressed = lambda: handler(button_id) self.buttons[button_id] = button self.button_handlers[button_id] = handler + def _validate_gpio_pin(self, gpio_pin: int) -> bool: + """Validate if a GPIO pin is suitable for button input""" + if gpio_pin in self.RESERVED_GPIO_PINS: + return False + if gpio_pin not in range(0, 28): # Valid GPIO range for most Pi models + return False + return True + + def get_safe_pins_info(self) -> str: + """Get information about safe GPIO pins to use""" + safe_pins = ", ".join(map(str, self.SAFE_GPIO_PINS)) + reserved_info = "\n".join([f" GPIO {pin}: {desc}" for pin, desc in self.RESERVED_GPIO_PINS.items()]) + + return f""" +GPIO Pin Recommendations: +Safe pins for buttons: {safe_pins} + +Reserved/Special function pins to avoid: +{reserved_info} + +Note: Always check your specific Raspberry Pi model documentation. +External pull-up resistors (10kΩ) are recommended for reliable button operation. +""" + def remove_button(self, button_id: int): """Remove a button""" if button_id in self.buttons: @@ -151,6 +224,12 @@ class ECMController: """Initialize the controller""" logger.info("Initializing ECM Controller...") + # Show GPIO pin information for troubleshooting + if not self.use_mock: + logger.info("GPIO Pin Information:") + logger.info(f"Safe GPIO pins for buttons: {self.button_manager.SAFE_GPIO_PINS}") + logger.info("If you're getting GPIO errors, consider updating your button GPIO pins in the web interface") + # Initialize scale scale_address = self._get_setting('scale_address', '') if scale_address: