pi server realised
This commit is contained in:
@@ -5,5 +5,6 @@
|
||||
]
|
||||
},
|
||||
"outputStyle": "iseri",
|
||||
"prefersReducedMotion": false
|
||||
"spinnerTipsEnabled": false,
|
||||
"prefersReducedMotion": true
|
||||
}
|
||||
|
||||
@@ -89,6 +89,33 @@ void UserApp_TaskInit(void)
|
||||
|
||||
/* ---------- WebSocket callbacks ---------- */
|
||||
|
||||
static void rtc_sync_if_needed(const pi_stats_t *stats)
|
||||
{
|
||||
if (!stats->time_valid) return;
|
||||
|
||||
rtcTimeStruct_t rtc = {};
|
||||
Rtc_GetTime(&rtc);
|
||||
|
||||
/* Convert both to seconds-since-midnight for comparison */
|
||||
int pi_secs = stats->time_hour * 3600 + stats->time_minute * 60 + stats->time_second;
|
||||
int rtc_secs = rtc.hour * 3600 + rtc.minute * 60 + rtc.second;
|
||||
int delta = pi_secs - rtc_secs;
|
||||
if (delta < 0) delta = -delta;
|
||||
|
||||
/* Also check date mismatch as an immediate trigger */
|
||||
bool date_mismatch = (rtc.year != stats->time_year ||
|
||||
rtc.month != stats->time_month ||
|
||||
rtc.day != stats->time_day);
|
||||
|
||||
if (date_mismatch || delta > 60) {
|
||||
Rtc_SetTime(stats->time_year, stats->time_month, stats->time_day,
|
||||
stats->time_hour, stats->time_minute, stats->time_second);
|
||||
ESP_LOGI(TAG, "RTC synced from Pi: %04d-%02d-%02d %02d:%02d:%02d (drift: %ds)",
|
||||
stats->time_year, stats->time_month, stats->time_day,
|
||||
stats->time_hour, stats->time_minute, stats->time_second, delta);
|
||||
}
|
||||
}
|
||||
|
||||
static void ws_data_cb(const pi_stats_t *stats)
|
||||
{
|
||||
/* Check alert conditions */
|
||||
@@ -103,6 +130,9 @@ static void ws_data_cb(const pi_stats_t *stats)
|
||||
}
|
||||
}
|
||||
|
||||
/* Sync RTC if Pi time drifts from board clock */
|
||||
rtc_sync_if_needed(stats);
|
||||
|
||||
/* Update UI under LVGL lock */
|
||||
if (Lvgl_lock(100)) {
|
||||
dashboard_ui_update_stats(stats);
|
||||
|
||||
@@ -97,6 +97,30 @@ static void parse_stats_json(const char *data, int len)
|
||||
}
|
||||
}
|
||||
|
||||
/* Parse local_time object for RTC sync */
|
||||
cJSON *lt = cJSON_GetObjectItem(root, "local_time");
|
||||
if (cJSON_IsObject(lt)) {
|
||||
cJSON *y = cJSON_GetObjectItem(lt, "y");
|
||||
cJSON *mo = cJSON_GetObjectItem(lt, "mo");
|
||||
cJSON *d = cJSON_GetObjectItem(lt, "d");
|
||||
cJSON *h = cJSON_GetObjectItem(lt, "h");
|
||||
cJSON *m = cJSON_GetObjectItem(lt, "m");
|
||||
cJSON *s = cJSON_GetObjectItem(lt, "s");
|
||||
if (y && mo && d && h && m && s) {
|
||||
s_stats.time_year = (uint16_t)y->valuedouble;
|
||||
s_stats.time_month = (uint8_t)mo->valuedouble;
|
||||
s_stats.time_day = (uint8_t)d->valuedouble;
|
||||
s_stats.time_hour = (uint8_t)h->valuedouble;
|
||||
s_stats.time_minute = (uint8_t)m->valuedouble;
|
||||
s_stats.time_second = (uint8_t)s->valuedouble;
|
||||
s_stats.time_valid = true;
|
||||
} else {
|
||||
s_stats.time_valid = false;
|
||||
}
|
||||
} else {
|
||||
s_stats.time_valid = false;
|
||||
}
|
||||
|
||||
s_stats.valid = true;
|
||||
xSemaphoreGive(s_stats_mutex);
|
||||
|
||||
|
||||
@@ -36,6 +36,15 @@ typedef struct {
|
||||
uint8_t service_count;
|
||||
uint32_t last_update; // timestamp from server
|
||||
bool valid; // set true after first successful parse
|
||||
|
||||
/* Broken-down local time from Pi for RTC sync */
|
||||
uint16_t time_year;
|
||||
uint8_t time_month;
|
||||
uint8_t time_day;
|
||||
uint8_t time_hour;
|
||||
uint8_t time_minute;
|
||||
uint8_t time_second;
|
||||
bool time_valid; // true when local_time object was parsed
|
||||
} pi_stats_t;
|
||||
|
||||
typedef void (*ws_data_callback_t)(const pi_stats_t *stats);
|
||||
|
||||
5
pi/.gitignore
vendored
Normal file
5
pi/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# python artifacts
|
||||
*/__pycache__
|
||||
__pycache__/
|
||||
*.pyo
|
||||
*.pyc
|
||||
@@ -1 +1,2 @@
|
||||
websockets>=12.0
|
||||
psutil>=5.9.0
|
||||
|
||||
126
pi/stats_server.py
Normal file
126
pi/stats_server.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""WebSocket server that sends real Pi system stats every 2 seconds.
|
||||
|
||||
Drop-in replacement for mock_server.py. Same port (8765), same JSON schema,
|
||||
same 2s push interval. Services remain mocked until systemd integration is added.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import psutil
|
||||
import websockets
|
||||
|
||||
# Prime the CPU percent counter (first call always returns 0.0)
|
||||
psutil.cpu_percent(interval=None)
|
||||
|
||||
# Network baseline for delta calculation
|
||||
_prev_net = psutil.net_io_counters()
|
||||
_prev_net_time = time.monotonic()
|
||||
|
||||
|
||||
def _get_cpu_temp() -> float:
|
||||
"""Read CPU temperature with fallback for different Pi OS versions."""
|
||||
try:
|
||||
temps = psutil.sensors_temperatures()
|
||||
if "cpu_thermal" in temps and temps["cpu_thermal"]:
|
||||
return round(temps["cpu_thermal"][0].current, 1)
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
# Fallback: read sysfs directly (value is in millidegrees)
|
||||
thermal_path = Path("/sys/class/thermal/thermal_zone0/temp")
|
||||
try:
|
||||
millidegrees = int(thermal_path.read_text().strip())
|
||||
return round(millidegrees / 1000.0, 1)
|
||||
except (FileNotFoundError, ValueError, PermissionError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _get_net_throughput() -> tuple[float, float]:
|
||||
"""Calculate network rx/tx in kbps since last call."""
|
||||
global _prev_net, _prev_net_time
|
||||
|
||||
now = time.monotonic()
|
||||
current = psutil.net_io_counters()
|
||||
elapsed = now - _prev_net_time
|
||||
|
||||
if elapsed <= 0:
|
||||
return 0.0, 0.0
|
||||
|
||||
rx_kbps = round((current.bytes_recv - _prev_net.bytes_recv) * 8 / (elapsed * 1000), 1)
|
||||
tx_kbps = round((current.bytes_sent - _prev_net.bytes_sent) * 8 / (elapsed * 1000), 1)
|
||||
|
||||
_prev_net = current
|
||||
_prev_net_time = now
|
||||
|
||||
return rx_kbps, tx_kbps
|
||||
|
||||
|
||||
def _mock_services() -> list[dict]:
|
||||
"""Mocked service status — same logic as mock_server.py."""
|
||||
return [
|
||||
{"name": "docker", "status": random.choice(["running", "running", "running", "stopped"])},
|
||||
{"name": "pihole", "status": random.choice(["running", "running", "running", "stopped"])},
|
||||
{"name": "nginx", "status": random.choice(["running", "running", "stopped"])},
|
||||
{"name": "sshd", "status": "running"},
|
||||
]
|
||||
|
||||
|
||||
def _local_time_fields() -> dict:
|
||||
"""Current local time as broken-down fields for RTC sync."""
|
||||
now = datetime.now()
|
||||
return {
|
||||
"y": now.year,
|
||||
"mo": now.month,
|
||||
"d": now.day,
|
||||
"h": now.hour,
|
||||
"m": now.minute,
|
||||
"s": now.second,
|
||||
}
|
||||
|
||||
|
||||
def generate_stats() -> dict:
|
||||
mem = psutil.virtual_memory()
|
||||
disk = psutil.disk_usage("/")
|
||||
rx_kbps, tx_kbps = _get_net_throughput()
|
||||
|
||||
return {
|
||||
"cpu_pct": psutil.cpu_percent(interval=None),
|
||||
"mem_pct": round(mem.percent, 1),
|
||||
"mem_used_mb": int(mem.used // (1024 * 1024)),
|
||||
"disk_pct": round(disk.percent, 1),
|
||||
"cpu_temp": _get_cpu_temp(),
|
||||
"uptime_hrs": round((time.time() - psutil.boot_time()) / 3600, 1),
|
||||
"net_rx_kbps": rx_kbps,
|
||||
"net_tx_kbps": tx_kbps,
|
||||
"services": _mock_services(),
|
||||
"timestamp": int(time.time()),
|
||||
"local_time": _local_time_fields(),
|
||||
}
|
||||
|
||||
|
||||
async def handler(websocket):
|
||||
addr = websocket.remote_address
|
||||
print(f"Client connected: {addr}")
|
||||
try:
|
||||
while True:
|
||||
stats = generate_stats()
|
||||
await websocket.send(json.dumps(stats))
|
||||
await asyncio.sleep(2)
|
||||
except websockets.ConnectionClosed:
|
||||
print(f"Client disconnected: {addr}")
|
||||
|
||||
|
||||
async def main():
|
||||
print("Pi stats server starting on ws://0.0.0.0:8765")
|
||||
async with websockets.serve(handler, "0.0.0.0", 8765):
|
||||
await asyncio.Future() # run forever
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user