Normalize primary communication around UART

This commit is contained in:
root
2026-06-03 02:08:19 -06:00
parent 08a8275a29
commit b494a98de4
15 changed files with 64 additions and 54 deletions
+8
View File
@@ -0,0 +1,8 @@
run:
python3 simulator/app.py
venv:
python3 -m venv .venv
install:
. .venv/bin/activate && pip install -r simulator/requirements.txt
+1 -1
View File
@@ -17,4 +17,4 @@ A custom monitoring and control system for a Nissan Xterra using:
3. ESP32 communications 3. ESP32 communications
4. Temperature sensors 4. Temperature sensors
5. JBD/Xiaoxiang BMS integration 5. JBD/Xiaoxiang BMS integration
6. Logging and dashboards 6. Future integrations / optional logging
+3 -3
View File
@@ -4,7 +4,7 @@
The ESP32 acts as the system controller and provides status and control endpoints. The ESP32 acts as the system controller and provides status and control endpoints.
Primary communication is RS-485. Primary communication is UART over CAT5. WiFi HTTP API is the backup communication path.
WiFi HTTP API is provided for: WiFi HTTP API is provided for:
@@ -48,7 +48,7 @@ Example:
"network": { "network": {
"wifi_enabled": false, "wifi_enabled": false,
"rs485_connected": true "uart_connected": true
} }
} }
``` ```
@@ -167,7 +167,7 @@ Example:
```json ```json
{ {
"wifi_enabled": false, "wifi_enabled": false,
"rs485_connected": true, "uart_connected": true,
"starlink_enabled": false "starlink_enabled": false
} }
``` ```
+3 -3
View File
@@ -19,7 +19,7 @@ The system provides:
- Raspberry Pi Pico 2 W - Raspberry Pi Pico 2 W
- 3.5" SPI Capacitive Touchscreen - 3.5" SPI Capacitive Touchscreen
- RS-485 communications - UART-over-CAT5 communications
- WiFi backup communications - WiFi backup communications
### Responsibilities ### Responsibilities
@@ -36,7 +36,7 @@ The system provides:
### Hardware ### Hardware
- ESP32 Relay Controller - ESP32 Relay Controller
- RS-485 Interface - UART-over-CAT5 interface
- House Battery Interface - House Battery Interface
- DS18B20 Temperature Sensor Bus - DS18B20 Temperature Sensor Bus
- Bosch Relay Drivers - Bosch Relay Drivers
@@ -52,7 +52,7 @@ The system provides:
## Communications ## Communications
Primary: Primary:
- RS-485 - UART over CAT5
Backup: Backup:
- WiFi HTTP API - WiFi HTTP API
+2 -2
View File
@@ -52,14 +52,14 @@ Future:
## System Screen ## System Screen
Displays: Displays:
- RS-485 Status - UART Status
- WiFi Status - WiFi Status
- Message Counts - Message Counts
- Latency - Latency
- Packet Loss - Packet Loss
Testing: Testing:
- RS-485 Disconnect - UART Disconnect
- Sensor Fault Simulation - Sensor Fault Simulation
- Ignition Simulation - Ignition Simulation
+2 -2
View File
@@ -8,7 +8,7 @@ Features:
- Dashboard UI - Dashboard UI
- Alarm System - Alarm System
- Sensor Simulation - Sensor Simulation
- RS-485 Simulation - UART Communication Simulation
- Protocol Layer - Protocol Layer
## Phase 2 — Pico Dashboard ## Phase 2 — Pico Dashboard
@@ -26,7 +26,7 @@ Status: Pending
Features: Features:
- Relay Control - Relay Control
- RS-485 Communications - UART Communications
- Configuration Storage - Configuration Storage
## Phase 4 — DS18B20 Sensors ## Phase 4 — DS18B20 Sensors
+5 -4
View File
@@ -12,7 +12,8 @@
// Ignition Sense // Ignition Sense
#define IGNITION_PIN 34 #define IGNITION_PIN 34
// Future RS485 // UART over CAT5 to Pico dashboard
#define RS485_TX_PIN 21 #define UART_TX_PIN 21
#define RS485_RX_PIN 22 #define UART_RX_PIN 22
#define RS485_DE_PIN 23
// RS-485/MAX3485 fallback only; not currently planned
@@ -50,7 +50,7 @@ void handleStatus() {
JsonObject network = doc.createNestedObject("network"); JsonObject network = doc.createNestedObject("network");
network["wifi_enabled"] = true; network["wifi_enabled"] = true;
network["rs485_connected"] = true; network["uart_connected"] = true;
String output; String output;
serializeJsonPretty(doc, output); serializeJsonPretty(doc, output);
+2 -2
View File
@@ -8,7 +8,6 @@
## Required ## Required
- 2x MAX3485 RS-485 Modules
- 4x DS18B20 Waterproof Sensors - 4x DS18B20 Waterproof Sensors
- 12V→5V Buck Converter - 12V→5V Buck Converter
- CAT5 Cable - CAT5 Cable
@@ -17,8 +16,9 @@
- Automotive Fuse Block - Automotive Fuse Block
- Assorted ATC Fuses - Assorted ATC Fuses
## Future ## Future / Optional Fallback
- 2x MAX3485 RS-485 Modules, only if UART over CAT5 proves unreliable
- Pi Zero 2 W - Pi Zero 2 W
- GPS Module - GPS Module
- ELM327 OBD-II Interface - ELM327 OBD-II Interface
+4 -3
View File
@@ -5,9 +5,10 @@ GPIO 17 Relay 2 (Fridge)
GPIO 4 DS18B20 Bus GPIO 4 DS18B20 Bus
GPIO 21 RS-485 TX GPIO 21 UART TX to Pico over CAT5
GPIO 22 RS-485 RX GPIO 22 UART RX from Pico over CAT5
GPIO 23 RS-485 DE/RE
RS-485/MAX3485 is fallback only and not currently planned.
GPIO 34 Ignition Sense GPIO 34 Ignition Sense
+8 -8
View File
@@ -96,16 +96,16 @@ def comms():
return jsonify(pico.get_comms()) return jsonify(pico.get_comms())
@app.route("/comms/rs485/disconnect", methods=["POST"]) @app.route("/comms/uart/disconnect", methods=["POST"])
def disconnect_rs485(): def disconnect_uart():
pico.disconnect_rs485() pico.disconnect_uart()
return jsonify({"success": True, "rs485_connected": False}) return jsonify({"success": True, "uart_connected": False})
@app.route("/comms/rs485/restore", methods=["POST"]) @app.route("/comms/uart/restore", methods=["POST"])
def restore_rs485(): def restore_uart():
pico.restore_rs485() pico.restore_uart()
return jsonify({"success": True, "rs485_connected": True}) return jsonify({"success": True, "uart_connected": True})
@app.route("/comms/latency", methods=["POST"]) @app.route("/comms/latency", methods=["POST"])
+1 -1
View File
@@ -198,7 +198,7 @@ class ESP32Simulator:
"network": { "network": {
"wifi_enabled": starlink_on or wifi_override_active, "wifi_enabled": starlink_on or wifi_override_active,
"wifi_override_active": wifi_override_active, "wifi_override_active": wifi_override_active,
"rs485_connected": True, "uart_connected": True,
"starlink_enabled": starlink_on "starlink_enabled": starlink_on
}, },
+9 -9
View File
@@ -7,14 +7,14 @@ from protocol import (
toggle_ignition_request, toggle_ignition_request,
toggle_sensor_fault_request, toggle_sensor_fault_request,
) )
from transport import RS485Transport from transport import UARTTransport
class PicoSimulator: class PicoSimulator:
def __init__(self, controller): def __init__(self, controller):
self.transport = RS485Transport(controller) self.transport = UARTTransport(controller)
self.last_status = None self.last_status = None
self.primary_link = "rs485" self.primary_link = "uart"
self.backup_link_available = True self.backup_link_available = True
self.messages_sent = 0 self.messages_sent = 0
self.messages_received = 0 self.messages_received = 0
@@ -40,7 +40,7 @@ class PicoSimulator:
if response.get("type") == "error": if response.get("type") == "error":
if self.last_status: if self.last_status:
self.last_status["network"]["rs485_connected"] = False self.last_status["network"]["uart_connected"] = False
self.last_status["network"]["communication_lost"] = True self.last_status["network"]["communication_lost"] = True
self.add_comms_to_status(self.last_status) self.add_comms_to_status(self.last_status)
return self.last_status return self.last_status
@@ -53,7 +53,7 @@ class PicoSimulator:
"sensor_health": {}, "sensor_health": {},
"relays": {}, "relays": {},
"network": { "network": {
"rs485_connected": False, "uart_connected": False,
"communication_lost": True, "communication_lost": True,
"wifi_enabled": False, "wifi_enabled": False,
"wifi_override_active": False, "wifi_override_active": False,
@@ -63,7 +63,7 @@ class PicoSimulator:
} }
self.last_status = response["data"] self.last_status = response["data"]
self.last_status["network"]["rs485_connected"] = self.transport.connected self.last_status["network"]["uart_connected"] = self.transport.connected
self.last_status["network"]["communication_lost"] = False self.last_status["network"]["communication_lost"] = False
self.add_comms_to_status(self.last_status) self.add_comms_to_status(self.last_status)
return self.last_status return self.last_status
@@ -87,10 +87,10 @@ class PicoSimulator:
def toggle_sensor_fault(self, sensor): def toggle_sensor_fault(self, sensor):
return self.send_message(toggle_sensor_fault_request(sensor)) return self.send_message(toggle_sensor_fault_request(sensor))
def disconnect_rs485(self): def disconnect_uart(self):
self.transport.disconnect() self.transport.disconnect()
def restore_rs485(self): def restore_uart(self):
self.transport.restore() self.transport.restore()
def set_latency(self, latency_ms): def set_latency(self, latency_ms):
@@ -103,7 +103,7 @@ class PicoSimulator:
return { return {
"primary": self.primary_link, "primary": self.primary_link,
"backup_available": self.backup_link_available, "backup_available": self.backup_link_available,
"rs485_connected": self.transport.connected, "uart_connected": self.transport.connected,
"messages_sent": self.messages_sent, "messages_sent": self.messages_sent,
"messages_received": self.messages_received, "messages_received": self.messages_received,
"last_message_time": self.last_message_time, "last_message_time": self.last_message_time,
+12 -12
View File
@@ -24,7 +24,7 @@
<main> <main>
<header> <header>
<h1>Xterra Dashboard</h1> <h1>Xterra Dashboard</h1>
<div id="statusLine">RS-485: -- | WiFi: --</div> <div id="statusLine">UART: -- | WiFi: --</div>
</header> </header>
<section id="dashboard" class="screen active"> <section id="dashboard" class="screen active">
@@ -82,7 +82,7 @@
<section id="system" class="screen"> <section id="system" class="screen">
<h2>System</h2> <h2>System</h2>
<div class="detail-list"> <div class="detail-list">
<div>RS-485 <strong id="sysRs485">--</strong></div> <div>UART <strong id="sysUart">--</strong></div>
<div>WiFi <strong id="sysWifi">--</strong></div> <div>WiFi <strong id="sysWifi">--</strong></div>
<div>WiFi Override <strong id="sysWifiOverride">--</strong></div> <div>WiFi Override <strong id="sysWifiOverride">--</strong></div>
<div>Ignition <strong id="sysIgnition">--</strong></div> <div>Ignition <strong id="sysIgnition">--</strong></div>
@@ -101,8 +101,8 @@
<div>Packet Loss <strong id="commsPacketLoss">--</strong></div> <div>Packet Loss <strong id="commsPacketLoss">--</strong></div>
</div> </div>
<button onclick="disconnectRs485()">Disconnect RS-485</button> <button onclick="disconnectUart()">Disconnect UART</button>
<button onclick="restoreRs485()">Restore RS-485</button> <button onclick="restoreUart()">Restore UART</button>
<div class="settings-list"> <div class="settings-list">
<label> <label>
@@ -235,7 +235,7 @@ function checkAlarms(data) {
const warnings = []; const warnings = [];
if (data.network.communication_lost) { if (data.network.communication_lost) {
warnings.push('Communication lost: RS-485 disconnected'); warnings.push('Communication lost: UART disconnected');
} }
for (const [name, healthy] of Object.entries(data.sensor_health || {})) { for (const [name, healthy] of Object.entries(data.sensor_health || {})) {
@@ -347,7 +347,7 @@ async function loadStatus() {
relayState = data.relays; relayState = data.relays;
document.getElementById('statusLine').textContent = document.getElementById('statusLine').textContent =
`RS-485: ${data.network.rs485_connected ? 'Connected' : 'Lost'} | WiFi: ${data.network.wifi_enabled ? 'Enabled' : 'Disabled'}`; `UART: ${data.network.uart_connected ? 'Connected' : 'Lost'} | WiFi: ${data.network.wifi_enabled ? 'Enabled' : 'Disabled'}`;
if (data.network.communication_lost) { if (data.network.communication_lost) {
document.body.classList.add('comms-lost'); document.body.classList.add('comms-lost');
@@ -393,12 +393,12 @@ async function loadStatus() {
document.getElementById('starlinkBtn').textContent = `Starlink: ${onOff(data.relays.starlink)}`; document.getElementById('starlinkBtn').textContent = `Starlink: ${onOff(data.relays.starlink)}`;
document.getElementById('fridgeBtn').textContent = `Fridge: ${onOff(data.relays.fridge)}`; document.getElementById('fridgeBtn').textContent = `Fridge: ${onOff(data.relays.fridge)}`;
document.getElementById('sysRs485').textContent = data.network.rs485_connected ? 'Connected' : 'Disconnected'; document.getElementById('sysUart').textContent = data.network.uart_connected ? 'Connected' : 'Disconnected';
document.getElementById('sysWifi').textContent = data.network.wifi_enabled ? 'Enabled' : 'Disabled'; document.getElementById('sysWifi').textContent = data.network.wifi_enabled ? 'Enabled' : 'Disabled';
document.getElementById('sysWifiOverride').textContent = data.network.wifi_override_active ? 'Active' : 'Inactive'; document.getElementById('sysWifiOverride').textContent = data.network.wifi_override_active ? 'Active' : 'Inactive';
document.getElementById('sysIgnition').textContent = data.vehicle?.ignition_on ? 'ON' : 'OFF'; document.getElementById('sysIgnition').textContent = data.vehicle?.ignition_on ? 'ON' : 'OFF';
document.getElementById('commsPrimary').textContent = data.network.rs485_connected ? 'RS-485' : 'RS-485 LOST'; document.getElementById('commsPrimary').textContent = data.network.uart_connected ? 'UART' : 'UART LOST';
document.getElementById('commsBackup').textContent = 'WiFi'; document.getElementById('commsBackup').textContent = 'WiFi';
document.getElementById('commsSent').textContent = data.network.messages_sent ?? '--'; document.getElementById('commsSent').textContent = data.network.messages_sent ?? '--';
document.getElementById('commsReceived').textContent = data.network.messages_received ?? '--'; document.getElementById('commsReceived').textContent = data.network.messages_received ?? '--';
@@ -466,8 +466,8 @@ async function toggleIgnition() {
loadStatus(); loadStatus();
} }
async function disconnectRs485() { async function disconnectUart() {
await fetch('/comms/rs485/disconnect', { await fetch('/comms/uart/disconnect', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({}) body: JSON.stringify({})
@@ -476,8 +476,8 @@ async function disconnectRs485() {
loadStatus(); loadStatus();
} }
async function restoreRs485() { async function restoreUart() {
await fetch('/comms/rs485/restore', { await fetch('/comms/uart/restore', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({}) body: JSON.stringify({})
+3 -3
View File
@@ -2,7 +2,7 @@ import random
import time import time
class RS485Transport: class UARTTransport:
def __init__(self, controller): def __init__(self, controller):
self.controller = controller self.controller = controller
self.connected = True self.connected = True
@@ -14,7 +14,7 @@ class RS485Transport:
return { return {
"type": "error", "type": "error",
"success": False, "success": False,
"error": "RS-485 disconnected" "error": "UART disconnected"
} }
if self.latency_ms > 0: if self.latency_ms > 0:
@@ -25,7 +25,7 @@ class RS485Transport:
return { return {
"type": "error", "type": "error",
"success": False, "success": False,
"error": "RS-485 packet lost" "error": "UART packet lost"
} }
return self.controller.handle_message(message) return self.controller.handle_message(message)