Compare commits
2 Commits
d7833100a5
...
bb5327fb93
| Author | SHA1 | Date | |
|---|---|---|---|
| bb5327fb93 | |||
| 022c45edee |
+16
-4
@@ -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__":
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user