Compare commits

..

2 Commits

Author SHA1 Message Date
Ninluc bb5327fb93 Technician Debug Web Server
Build, push image, and notify Watchtower / build-image (push) Successful in 42s
Build, push image, and notify Watchtower / notify (push) Successful in 13s
2026-08-19 17:04:33 +02:00
Ninluc 022c45edee Fix API key 2026-08-19 17:04:10 +02:00
5 changed files with 183 additions and 5 deletions
+16 -4
View File
@@ -14,6 +14,7 @@ from sensors import ultrasonicRanger, temp_hum, button, camera, buzzer, rgb_lcd,
from lib.systemd_logs import get_systemd_logs from lib.systemd_logs import get_systemd_logs
from external_display_manager import DisplayManager from external_display_manager import DisplayManager
from rgb_lcd_manager import RGBLCDManager from rgb_lcd_manager import RGBLCDManager
from web_server import start_technician_web_server
rgb_lcd.setRGB(255, 255, 255, brightness=0.8) rgb_lcd.setRGB(255, 255, 255, brightness=0.8)
rgb_lcd.setText("SmartWave\nOrchestrateur") rgb_lcd.setText("SmartWave\nOrchestrateur")
@@ -21,6 +22,8 @@ rgb_lcd.setText("SmartWave\nOrchestrateur")
# --- CONFIGURATION CONSTANTS --- # --- CONFIGURATION CONSTANTS ---
DISPLAY_DEBOUNCE_SECONDS = 3 # Seconds to wait after first change before broadcasting DISPLAY_DEBOUNCE_SECONDS = 3 # Seconds to wait after first change before broadcasting
TECHNICIAN_MODE = False TECHNICIAN_MODE = False
def is_technician_mode_active() -> bool:
return TECHNICIAN_MODE
# --- DB SETUP --- # --- DB SETUP ---
DB_PATH = "orchestrateur/db.sqlite" DB_PATH = "orchestrateur/db.sqlite"
@@ -73,7 +76,7 @@ def get_device_id():
return "RPI_Orchestrateur_Default" return "RPI_Orchestrateur_Default"
def get_api_key(): def get_api_key():
for path in ["api_key.txt", "/home/pi/SmartWave/orchestrateur/api_key.txt"]: for path in ["device_api_key.txt", "/home/pi/SmartWave/orchestrateur/device_api_key.txt"]:
try: try:
with open(path, "r") as f: with open(path, "r") as f:
return f.read().strip() return f.read().strip()
@@ -606,7 +609,9 @@ async def check_rfid_card_api(card_id: str) -> bool:
return len(items) > 0 if isinstance(items, list) else bool(data) return len(items) > 0 if isinstance(items, list) else bool(data)
elif isinstance(data, list): elif isinstance(data, list):
return len(data) > 0 return len(data) > 0
else:
print(f"[RFID Task] OData API returned status {response.status_code}: {response.json()}")
set_cloud_alert(True) # Set cloud alert on API connection failure
return False return False
except Exception as e: except Exception as e:
@@ -673,7 +678,7 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
return return
plan = res_json.get("cook_plan", res_json) plan = res_json.get("plan", {}).get("cook_plan", res_json)
c_time = plan.get("cook_time_seconds") c_time = plan.get("cook_time_seconds")
c_power = plan.get("effective_power_watts") c_power = plan.get("effective_power_watts")
@@ -873,7 +878,14 @@ async def main():
process_messages_task(), process_messages_task(),
monitor_dish_height_task(), monitor_dish_height_task(),
display_broadcast_worker_task(), display_broadcast_worker_task(),
rfid_listener_task() # <-- ADDED HERE rfid_listener_task(),
start_technician_web_server(
get_logs_cb=get_systemd_logs,
telemetry_cb=handle_telemetry_request,
is_tech_mode_cb=is_technician_mode_active,
host=config.TECHNICIAN_WEB_SERVER_HOST,
port=config.TECHNICIAN_WEB_SERVER_PORT
)
) )
if __name__ == "__main__": if __name__ == "__main__":
+4 -1
View File
@@ -1,9 +1,12 @@
paho-mqtt>=1.6,<3 paho-mqtt>=1.6,<3
pyserial>=3.5,<4 pyserial>=3.5,<4
zeroconf>=0.131.0
fastapi>=0.100.0,<1.0.0
uvicorn>=0.20.0,<1.0.0
python-multipart>=0.0.6,<1.0.0
# picamera2>=0.3.36,<4 # → Installed with apt install python3-picamera2 # picamera2>=0.3.36,<4 # → Installed with apt install python3-picamera2
# OpenCV # OpenCV
# sudo apt install -y python3-opencv # sudo apt install -y python3-opencv
# sudo apt install -y opencv-data # sudo apt install -y opencv-data
zeroconf>=0.131.0
# sudo apt install pigpio python3-pigpio # sudo apt install pigpio python3-pigpio
# sudo systemctl enable pigpiod --now # sudo systemctl enable pigpiod --now
+159
View File
@@ -0,0 +1,159 @@
import asyncio
from fastapi import FastAPI, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse
import uvicorn
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SmartWave Technician Portal</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace; background: #121212; color: #e0e0e0; height: 100vh; overflow: hidden; }
.container { display: flex; height: 100vh; }
.logs-section { flex: 1; display: flex; flex-direction: column; border-right: 1px solid #333; padding: 15px; }
.logs-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.logs-header h2 { font-size: 1.1rem; color: #4fc3f7; }
.logs-header button { background: #333; color: #fff; border: 1px solid #555; padding: 4px 10px; border-radius: 4px; cursor: pointer; }
pre#log-output { flex: 1; background: #000; color: #00ff66; padding: 12px; border-radius: 6px; overflow-y: auto; font-size: 0.85rem; line-height: 1.4; white-space: pre-wrap; word-break: break-all; }
.control-section { width: 500px; padding: 20px; background: #1a1a1a; display: flex; flex-direction: column; gap: 15px; }
.control-section h2 { font-size: 1.1rem; color: #ffb74d; margin-bottom: 5px; }
.form-group { display: flex; flex-direction: column; gap: 6px; }
label { font-size: 0.85rem; color: #aaa; }
input[type="text"] { background: #2a2a2a; border: 1px solid #444; color: #fff; padding: 10px; border-radius: 4px; font-size: 0.9rem; }
input[type="text"]:focus { outline: none; border-color: #ffb74d; }
button.btn-submit { background: #ffb74d; color: #121212; border: none; padding: 12px; font-weight: bold; border-radius: 4px; cursor: pointer; transition: background 0.2s; }
button.btn-submit:hover { background: #ffa726; }
#status-msg { font-size: 0.85rem; padding: 8px; border-radius: 4px; display: none; }
.success { background: #1b5e20; color: #a5d6a7; }
.error { background: #b71c1c; color: #ef9a9a; }
</style>
</head>
<body>
<div class="container">
<div class="logs-section">
<div class="logs-header">
<h2>SmartWave Orchestrator System Logs</h2>
<button onclick="fetchLogs()">Refresh</button>
</div>
<pre id="log-output">Loading logs...</pre>
</div>
<div class="control-section">
<h2>Telemetry Control</h2>
<div class="form-group">
<label for="endpoint">Telemetry Endpoint URL:</label>
<input type="text" id="endpoint" value="https://smartwave.matthiasg.dev/telemetry">
</div>
<button class="btn-submit" onclick="triggerTelemetry()">Send Telemetry</button>
<div id="status-msg"></div>
</div>
</div>
<script>
async function fetchLogs() {
try {
const res = await fetch('/api/logs');
const logs = await res.json();
const logContainer = document.getElementById('log-output');
logContainer.textContent = logs.map(entry => {
const ts = entry.timestamp ? entry.timestamp.replace('T', ' ').split('.')[0] : 'RAW';
const pid = entry.pid ? `[PID:${entry.pid}]` : '';
return `${ts} ${pid} ${entry.message}`;
}).join('\\n');
logContainer.scrollTop = logContainer.scrollHeight;
} catch (err) {
console.error("Failed to fetch logs", err);
}
}
async function triggerTelemetry() {
const endpoint = document.getElementById('endpoint').value;
const statusDiv = document.getElementById('status-msg');
statusDiv.style.display = 'block';
statusDiv.className = '';
statusDiv.textContent = 'Sending telemetry...';
try {
const formData = new FormData();
formData.append('endpoint', endpoint);
const res = await fetch('/api/telemetry', {
method: 'POST',
body: formData
});
const data = await res.json();
if (res.ok && data.status === 'ok') {
statusDiv.className = 'success';
statusDiv.textContent = 'Telemetry sent successfully!';
} else {
throw new Error(data.detail || 'Failed');
}
} catch (err) {
statusDiv.className = 'error';
statusDiv.textContent = 'Error sending telemetry: ' + err.message;
}
}
fetchLogs();
setInterval(fetchLogs, 3000);
</script>
</body>
</html>
"""
def create_app(get_logs_cb, telemetry_cb, is_tech_mode_cb) -> FastAPI:
"""Factory creating FastAPI app with injected handler callbacks."""
app = FastAPI(title="Technician Portal")
# Attach callbacks to app.state
app.state.get_systemd_logs = get_logs_cb
app.state.handle_telemetry_request = telemetry_cb
app.state.is_tech_mode = is_tech_mode_cb
# Middleware to block traffic when TECHNICIAN_MODE is False
@app.middleware("http")
async def enforce_technician_mode(request: Request, call_next):
if not request.app.state.is_tech_mode():
return JSONResponse(
status_code=503,
content={"detail": "Technician mode is disabled."}
)
return await call_next(request)
@app.get("/", response_class=HTMLResponse)
async def serve_dashboard():
return HTML_TEMPLATE
@app.get("/api/logs")
async def api_get_logs(request: Request):
try:
logs = await request.app.state.get_systemd_logs(lines=200, service_name="smartwave")
return JSONResponse(content=logs)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/telemetry")
async def api_trigger_telemetry(request: Request, endpoint: str = Form(...)):
try:
await request.app.state.handle_telemetry_request(endpoint=endpoint, do_timeout=True)
return {"status": "ok", "message": f"Telemetry request sent to {endpoint}"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Telemetry trigger failed: {str(e)}")
return app
async def start_technician_web_server(get_logs_cb, telemetry_cb, is_tech_mode_cb, host="192.168.50.1", port=8080):
"""Asynchronous server launcher meant to run within main.py's event loop."""
app = create_app(get_logs_cb, telemetry_cb, is_tech_mode_cb)
config = uvicorn.Config(app, host=host, port=port, log_level="info")
server = uvicorn.Server(config)
await server.serve()
+4
View File
@@ -3,6 +3,10 @@ DEBUG=True
DEBUG_DANGEROUS_AREA=False DEBUG_DANGEROUS_AREA=False
DEBUG_TEMPERATURE_ALERT=False DEBUG_TEMPERATURE_ALERT=False
# Technitian Web Server
TECHNICIAN_WEB_SERVER_HOST = "192.168.50.1"
TECHNICIAN_WEB_SERVER_PORT = 8080
# LoRa # LoRa
LORA_HEARTBEAT_INTERVAL = 30 LORA_HEARTBEAT_INTERVAL = 30