Compare commits
2 Commits
087abca062
...
fb82972e98
| Author | SHA1 | Date | |
|---|---|---|---|
| fb82972e98 | |||
| 21405819f0 |
13
CLAUDE.md
13
CLAUDE.md
@ -145,6 +145,19 @@ sudo usermod -a -G gpio $USER
|
|||||||
# Then log out and back in
|
# 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):**
|
**Form validation errors (422):**
|
||||||
- Check FastAPI endpoint parameter types
|
- Check FastAPI endpoint parameter types
|
||||||
- Handle empty strings vs None for optional fields
|
- Handle empty strings vs None for optional fields
|
||||||
|
|||||||
@ -36,6 +36,13 @@ def main():
|
|||||||
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
|
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
|
||||||
help='Logging level')
|
help='Logging level')
|
||||||
gpio_parser.add_argument('--log-file', help='Log file path')
|
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
|
# Database management commands
|
||||||
db_parser = subparsers.add_parser('db', help='Database management')
|
db_parser = subparsers.add_parser('db', help='Database management')
|
||||||
@ -68,6 +75,8 @@ def main():
|
|||||||
return run_web_server(args)
|
return run_web_server(args)
|
||||||
elif args.command == 'gpio':
|
elif args.command == 'gpio':
|
||||||
return asyncio.run(run_gpio_service(args))
|
return asyncio.run(run_gpio_service(args))
|
||||||
|
elif args.command == 'gpio-info':
|
||||||
|
return show_gpio_info(args)
|
||||||
elif args.command == 'db':
|
elif args.command == 'db':
|
||||||
return run_db_command(args)
|
return run_db_command(args)
|
||||||
else:
|
else:
|
||||||
@ -103,10 +112,17 @@ def run_web_server(args):
|
|||||||
|
|
||||||
async def run_gpio_service(args):
|
async def run_gpio_service(args):
|
||||||
"""Run the GPIO control service"""
|
"""Run the GPIO control service"""
|
||||||
from .gpio import ECMController
|
from .gpio import ECMController, ButtonManager
|
||||||
from .utils.logging_config import configure_logging_from_settings
|
from .utils.logging_config import configure_logging_from_settings
|
||||||
|
|
||||||
logger = get_logger('gpio')
|
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")
|
logger.info("Starting GPIO control service")
|
||||||
|
|
||||||
# Configure logging from database settings
|
# Configure logging from database settings
|
||||||
@ -126,6 +142,40 @@ async def run_gpio_service(args):
|
|||||||
|
|
||||||
return 0
|
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):
|
def run_db_command(args):
|
||||||
"""Run database management commands"""
|
"""Run database management commands"""
|
||||||
logger = get_logger('db')
|
logger = get_logger('db')
|
||||||
|
|||||||
@ -96,6 +96,29 @@ class CoffeeMachineController:
|
|||||||
class ButtonManager:
|
class ButtonManager:
|
||||||
"""Manages physical buttons and their GPIO pins"""
|
"""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):
|
def __init__(self, use_mock: bool = False):
|
||||||
self.use_mock = use_mock
|
self.use_mock = use_mock
|
||||||
self.buttons: Dict[int, GPIOButton | MockGPIO] = {}
|
self.buttons: Dict[int, GPIOButton | MockGPIO] = {}
|
||||||
@ -103,26 +126,76 @@ class ButtonManager:
|
|||||||
|
|
||||||
def add_button(self, button_id: int, gpio_pin: int, handler: Callable):
|
def add_button(self, button_id: int, gpio_pin: int, handler: Callable):
|
||||||
"""Add a button with its handler"""
|
"""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:
|
try:
|
||||||
if self.use_mock:
|
if self.use_mock:
|
||||||
button = MockGPIO(gpio_pin)
|
button = MockGPIO(gpio_pin)
|
||||||
|
logger.info(f"Button {button_id} added on GPIO {gpio_pin} (MOCK)")
|
||||||
else:
|
else:
|
||||||
|
# Try with pull_up first
|
||||||
button = GPIOButton(gpio_pin, pull_up=True, bounce_time=0.2)
|
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)
|
button.when_pressed = lambda: handler(button_id)
|
||||||
self.buttons[button_id] = button
|
self.buttons[button_id] = button
|
||||||
self.button_handlers[button_id] = handler
|
self.button_handlers[button_id] = handler
|
||||||
|
|
||||||
logger.info(f"Button {button_id} added on GPIO {gpio_pin}")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to add button {button_id} on GPIO {gpio_pin}: {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 = MockGPIO(gpio_pin)
|
||||||
button.when_pressed = lambda: handler(button_id)
|
button.when_pressed = lambda: handler(button_id)
|
||||||
self.buttons[button_id] = button
|
self.buttons[button_id] = button
|
||||||
self.button_handlers[button_id] = handler
|
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):
|
def remove_button(self, button_id: int):
|
||||||
"""Remove a button"""
|
"""Remove a button"""
|
||||||
if button_id in self.buttons:
|
if button_id in self.buttons:
|
||||||
@ -151,6 +224,12 @@ class ECMController:
|
|||||||
"""Initialize the controller"""
|
"""Initialize the controller"""
|
||||||
logger.info("Initializing ECM 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
|
# Initialize scale
|
||||||
scale_address = self._get_setting('scale_address', '')
|
scale_address = self._get_setting('scale_address', '')
|
||||||
if scale_address:
|
if scale_address:
|
||||||
|
|||||||
@ -260,26 +260,76 @@ async def create_button(
|
|||||||
logger.error(f"Full traceback: {traceback.format_exc()}")
|
logger.error(f"Full traceback: {traceback.format_exc()}")
|
||||||
raise HTTPException(status_code=400, detail="Failed to create button")
|
raise HTTPException(status_code=400, detail="Failed to create button")
|
||||||
|
|
||||||
@app.put("/buttons/{button_id}")
|
@app.put("/buttons/{button_id}", response_class=HTMLResponse)
|
||||||
async def update_button(
|
async def update_button(
|
||||||
|
request: Request,
|
||||||
button_id: int,
|
button_id: int,
|
||||||
recipe_id: str = Form(""),
|
recipe_id: str = Form(""),
|
||||||
|
gpio_pin: Optional[int] = Form(None),
|
||||||
db: DatabaseManager = Depends(get_db)
|
db: DatabaseManager = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Update button recipe mapping"""
|
"""Update button recipe mapping and/or GPIO pin"""
|
||||||
try:
|
try:
|
||||||
# Convert empty string to None, otherwise convert to int
|
# Convert empty string to None, otherwise convert to int
|
||||||
recipe_id_value = None if recipe_id == "" else int(recipe_id)
|
recipe_id_value = None if recipe_id == "" else int(recipe_id)
|
||||||
|
|
||||||
|
if gpio_pin is not None:
|
||||||
|
# Update both recipe and GPIO pin
|
||||||
|
db.execute_update(
|
||||||
|
"UPDATE buttons SET recipe_id = ?, gpio_pin = ? WHERE id = ?",
|
||||||
|
(recipe_id_value, gpio_pin, button_id)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Update only recipe
|
||||||
db.execute_update(
|
db.execute_update(
|
||||||
"UPDATE buttons SET recipe_id = ? WHERE id = ?",
|
"UPDATE buttons SET recipe_id = ? WHERE id = ?",
|
||||||
(recipe_id_value, button_id)
|
(recipe_id_value, button_id)
|
||||||
)
|
)
|
||||||
return {"status": "success"}
|
|
||||||
|
# Return updated button list
|
||||||
|
buttons = db.execute_query("""
|
||||||
|
SELECT b.*, r.name as recipe_name
|
||||||
|
FROM buttons b
|
||||||
|
LEFT JOIN recipes r ON b.recipe_id = r.id
|
||||||
|
ORDER BY b.name
|
||||||
|
""")
|
||||||
|
recipes = db.execute_query("SELECT * FROM recipes ORDER BY name")
|
||||||
|
return templates.TemplateResponse("partials/button_list.html", {
|
||||||
|
"request": request,
|
||||||
|
"buttons": buttons,
|
||||||
|
"recipes": recipes
|
||||||
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error updating button: {e}")
|
logger.error(f"Error updating button: {e}")
|
||||||
raise HTTPException(status_code=400, detail="Failed to update button")
|
raise HTTPException(status_code=400, detail="Failed to update button")
|
||||||
|
|
||||||
|
@app.delete("/buttons/{button_id}", response_class=HTMLResponse)
|
||||||
|
async def delete_button(
|
||||||
|
request: Request,
|
||||||
|
button_id: int,
|
||||||
|
db: DatabaseManager = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Delete a button"""
|
||||||
|
try:
|
||||||
|
db.execute_update("DELETE FROM buttons WHERE id = ?", (button_id,))
|
||||||
|
|
||||||
|
# Return updated button list
|
||||||
|
buttons = db.execute_query("""
|
||||||
|
SELECT b.*, r.name as recipe_name
|
||||||
|
FROM buttons b
|
||||||
|
LEFT JOIN recipes r ON b.recipe_id = r.id
|
||||||
|
ORDER BY b.name
|
||||||
|
""")
|
||||||
|
recipes = db.execute_query("SELECT * FROM recipes ORDER BY name")
|
||||||
|
return templates.TemplateResponse("partials/button_list.html", {
|
||||||
|
"request": request,
|
||||||
|
"buttons": buttons,
|
||||||
|
"recipes": recipes
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting button: {e}")
|
||||||
|
raise HTTPException(status_code=400, detail="Failed to delete button")
|
||||||
|
|
||||||
# Shot history routes
|
# Shot history routes
|
||||||
@app.get("/shots", response_class=HTMLResponse)
|
@app.get("/shots", response_class=HTMLResponse)
|
||||||
async def list_shots(request: Request, db: DatabaseManager = Depends(get_db)):
|
async def list_shots(request: Request, db: DatabaseManager = Depends(get_db)):
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>GPIO Pin</th>
|
<th>GPIO Pin</th>
|
||||||
<th>Recipe</th>
|
<th>Recipe</th>
|
||||||
|
<th>Status</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -12,7 +13,17 @@
|
|||||||
{% for button in buttons %}
|
{% for button in buttons %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ button.name }}</td>
|
<td>{{ button.name }}</td>
|
||||||
<td>GPIO {{ button.gpio_pin }}</td>
|
<td>
|
||||||
|
<div class="input-group input-group-sm" style="width: 120px;">
|
||||||
|
<span class="input-group-text">GPIO</span>
|
||||||
|
<input type="number" class="form-control"
|
||||||
|
value="{{ button.gpio_pin }}"
|
||||||
|
min="0" max="40"
|
||||||
|
hx-put="/buttons/{{ button.id }}"
|
||||||
|
hx-trigger="change"
|
||||||
|
name="gpio_pin">
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select class="form-select form-select-sm"
|
<select class="form-select form-select-sm"
|
||||||
hx-put="/buttons/{{ button.id }}"
|
hx-put="/buttons/{{ button.id }}"
|
||||||
@ -32,6 +43,16 @@
|
|||||||
{{ 'Configured' if button.recipe_name else 'Not Configured' }}
|
{{ 'Configured' if button.recipe_name else 'Not Configured' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-danger btn-sm"
|
||||||
|
hx-delete="/buttons/{{ button.id }}"
|
||||||
|
hx-target="#button-list"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-confirm="Are you sure you want to delete this button?"
|
||||||
|
title="Delete Button">
|
||||||
|
<i class="bi bi-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user