Add RS485 transport simulation layer
This commit is contained in:
@@ -108,5 +108,19 @@ def restore_rs485():
|
|||||||
return jsonify({"success": True, "rs485_connected": True})
|
return jsonify({"success": True, "rs485_connected": True})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/comms/latency", methods=["POST"])
|
||||||
|
def set_latency():
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
pico.set_latency(data.get("latency_ms", 0))
|
||||||
|
return jsonify({"success": True, "latency_ms": pico.transport.latency_ms})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/comms/packet-loss", methods=["POST"])
|
||||||
|
def set_packet_loss():
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
pico.set_packet_loss(data.get("percent", 0))
|
||||||
|
return jsonify({"success": True, "packet_loss_percent": pico.transport.packet_loss_percent})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||||
|
|||||||
+33
-24
@@ -7,30 +7,30 @@ from protocol import (
|
|||||||
toggle_ignition_request,
|
toggle_ignition_request,
|
||||||
toggle_sensor_fault_request,
|
toggle_sensor_fault_request,
|
||||||
)
|
)
|
||||||
|
from transport import RS485Transport
|
||||||
|
|
||||||
|
|
||||||
class PicoSimulator:
|
class PicoSimulator:
|
||||||
def __init__(self, controller):
|
def __init__(self, controller):
|
||||||
self.controller = controller
|
self.transport = RS485Transport(controller)
|
||||||
self.last_status = None
|
self.last_status = None
|
||||||
self.primary_link = "rs485"
|
self.primary_link = "rs485"
|
||||||
self.rs485_connected = True
|
|
||||||
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
|
||||||
self.last_message_time = None
|
self.last_message_time = None
|
||||||
|
self.last_latency_ms = None
|
||||||
|
|
||||||
def send_message(self, message):
|
def send_message(self, message):
|
||||||
self.messages_sent += 1
|
self.messages_sent += 1
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
if not self.rs485_connected:
|
response = self.transport.send(message)
|
||||||
return {
|
|
||||||
"type": "error",
|
|
||||||
"success": False,
|
|
||||||
"error": "RS-485 disconnected"
|
|
||||||
}
|
|
||||||
|
|
||||||
response = self.controller.handle_message(message)
|
if response.get("type") == "error":
|
||||||
|
return response
|
||||||
|
|
||||||
|
self.last_latency_ms = round((time.time() - start) * 1000, 1)
|
||||||
self.messages_received += 1
|
self.messages_received += 1
|
||||||
self.last_message_time = int(time.time())
|
self.last_message_time = int(time.time())
|
||||||
return response
|
return response
|
||||||
@@ -42,9 +42,7 @@ class PicoSimulator:
|
|||||||
if self.last_status:
|
if self.last_status:
|
||||||
self.last_status["network"]["rs485_connected"] = False
|
self.last_status["network"]["rs485_connected"] = False
|
||||||
self.last_status["network"]["communication_lost"] = True
|
self.last_status["network"]["communication_lost"] = True
|
||||||
self.last_status["network"]["messages_sent"] = self.messages_sent
|
self.add_comms_to_status(self.last_status)
|
||||||
self.last_status["network"]["messages_received"] = self.messages_received
|
|
||||||
self.last_status["network"]["last_message_time"] = self.last_message_time
|
|
||||||
return self.last_status
|
return self.last_status
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -59,22 +57,24 @@ class PicoSimulator:
|
|||||||
"communication_lost": True,
|
"communication_lost": True,
|
||||||
"wifi_enabled": False,
|
"wifi_enabled": False,
|
||||||
"wifi_override_active": False,
|
"wifi_override_active": False,
|
||||||
"starlink_enabled": False,
|
"starlink_enabled": False
|
||||||
"messages_sent": self.messages_sent,
|
|
||||||
"messages_received": self.messages_received,
|
|
||||||
"last_message_time": self.last_message_time
|
|
||||||
},
|
},
|
||||||
"config": {}
|
"config": {}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.last_status = response["data"]
|
self.last_status = response["data"]
|
||||||
self.last_status["network"]["rs485_connected"] = self.rs485_connected
|
self.last_status["network"]["rs485_connected"] = self.transport.connected
|
||||||
self.last_status["network"]["communication_lost"] = False
|
self.last_status["network"]["communication_lost"] = False
|
||||||
self.last_status["network"]["messages_sent"] = self.messages_sent
|
self.add_comms_to_status(self.last_status)
|
||||||
self.last_status["network"]["messages_received"] = self.messages_received
|
|
||||||
self.last_status["network"]["last_message_time"] = self.last_message_time
|
|
||||||
return self.last_status
|
return self.last_status
|
||||||
|
|
||||||
|
def add_comms_to_status(self, status):
|
||||||
|
status["network"]["messages_sent"] = self.messages_sent
|
||||||
|
status["network"]["messages_received"] = self.messages_received
|
||||||
|
status["network"]["last_message_time"] = self.last_message_time
|
||||||
|
status["network"]["latency_ms"] = self.last_latency_ms
|
||||||
|
status["network"]["packet_loss_percent"] = self.transport.packet_loss_percent
|
||||||
|
|
||||||
def set_relay(self, relay, state):
|
def set_relay(self, relay, state):
|
||||||
return self.send_message(set_relay_request(relay, state))
|
return self.send_message(set_relay_request(relay, state))
|
||||||
|
|
||||||
@@ -88,17 +88,26 @@ class PicoSimulator:
|
|||||||
return self.send_message(toggle_sensor_fault_request(sensor))
|
return self.send_message(toggle_sensor_fault_request(sensor))
|
||||||
|
|
||||||
def disconnect_rs485(self):
|
def disconnect_rs485(self):
|
||||||
self.rs485_connected = False
|
self.transport.disconnect()
|
||||||
|
|
||||||
def restore_rs485(self):
|
def restore_rs485(self):
|
||||||
self.rs485_connected = True
|
self.transport.restore()
|
||||||
|
|
||||||
|
def set_latency(self, latency_ms):
|
||||||
|
self.transport.set_latency(latency_ms)
|
||||||
|
|
||||||
|
def set_packet_loss(self, percent):
|
||||||
|
self.transport.set_packet_loss(percent)
|
||||||
|
|
||||||
def get_comms(self):
|
def get_comms(self):
|
||||||
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.rs485_connected,
|
"rs485_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,
|
||||||
|
"latency_ms": self.last_latency_ms,
|
||||||
|
"configured_latency_ms": self.transport.latency_ms,
|
||||||
|
"packet_loss_percent": self.transport.packet_loss_percent
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,11 +97,27 @@
|
|||||||
<div>Messages Sent <strong id="commsSent">--</strong></div>
|
<div>Messages Sent <strong id="commsSent">--</strong></div>
|
||||||
<div>Messages Received <strong id="commsReceived">--</strong></div>
|
<div>Messages Received <strong id="commsReceived">--</strong></div>
|
||||||
<div>Last Message <strong id="commsLast">--</strong></div>
|
<div>Last Message <strong id="commsLast">--</strong></div>
|
||||||
|
<div>Latency <strong id="commsLatency">--</strong></div>
|
||||||
|
<div>Packet Loss <strong id="commsPacketLoss">--</strong></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onclick="disconnectRs485()">Disconnect RS-485</button>
|
<button onclick="disconnectRs485()">Disconnect RS-485</button>
|
||||||
<button onclick="restoreRs485()">Restore RS-485</button>
|
<button onclick="restoreRs485()">Restore RS-485</button>
|
||||||
|
|
||||||
|
<div class="settings-list">
|
||||||
|
<label>
|
||||||
|
Simulated Latency ms
|
||||||
|
<input id="latencyInput" type="number" value="0">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Packet Loss %
|
||||||
|
<input id="packetLossInput" type="number" value="0">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button onclick="saveCommsSimulation()">Save Comms Simulation</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2>Sensor Fault Simulation</h2>
|
<h2>Sensor Fault Simulation</h2>
|
||||||
<div class="settings-list">
|
<div class="settings-list">
|
||||||
<button onclick="toggleSensorFault('fridge_zone_1')">Toggle Fridge Zone 1 Fault</button>
|
<button onclick="toggleSensorFault('fridge_zone_1')">Toggle Fridge Zone 1 Fault</button>
|
||||||
@@ -389,6 +405,14 @@ async function loadStatus() {
|
|||||||
document.getElementById('commsLast').textContent = data.network.last_message_time
|
document.getElementById('commsLast').textContent = data.network.last_message_time
|
||||||
? new Date(data.network.last_message_time * 1000).toLocaleTimeString()
|
? new Date(data.network.last_message_time * 1000).toLocaleTimeString()
|
||||||
: '--';
|
: '--';
|
||||||
|
document.getElementById('commsLatency').textContent =
|
||||||
|
data.network.latency_ms !== null && data.network.latency_ms !== undefined
|
||||||
|
? `${data.network.latency_ms} ms`
|
||||||
|
: '--';
|
||||||
|
document.getElementById('commsPacketLoss').textContent =
|
||||||
|
data.network.packet_loss_percent !== undefined
|
||||||
|
? `${data.network.packet_loss_percent}%`
|
||||||
|
: '--';
|
||||||
|
|
||||||
updateSleepOverlay(data.vehicle?.ignition_on ?? true);
|
updateSleepOverlay(data.vehicle?.ignition_on ?? true);
|
||||||
|
|
||||||
@@ -462,6 +486,25 @@ async function restoreRs485() {
|
|||||||
loadStatus();
|
loadStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveCommsSimulation() {
|
||||||
|
const latency = Number(document.getElementById('latencyInput').value);
|
||||||
|
const packetLoss = Number(document.getElementById('packetLossInput').value);
|
||||||
|
|
||||||
|
await fetch('/comms/latency', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({latency_ms: latency})
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetch('/comms/packet-loss', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({percent: packetLoss})
|
||||||
|
});
|
||||||
|
|
||||||
|
loadStatus();
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleSensorFault(name) {
|
async function toggleSensorFault(name) {
|
||||||
await fetch(`/sensor/${name}/fault`, {
|
await fetch(`/sensor/${name}/fault`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class RS485Transport:
|
||||||
|
def __init__(self, controller):
|
||||||
|
self.controller = controller
|
||||||
|
self.connected = True
|
||||||
|
self.latency_ms = 0
|
||||||
|
self.packet_loss_percent = 0
|
||||||
|
|
||||||
|
def send(self, message):
|
||||||
|
if not self.connected:
|
||||||
|
return {
|
||||||
|
"type": "error",
|
||||||
|
"success": False,
|
||||||
|
"error": "RS-485 disconnected"
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.latency_ms > 0:
|
||||||
|
time.sleep(self.latency_ms / 1000)
|
||||||
|
|
||||||
|
if self.packet_loss_percent > 0:
|
||||||
|
if random.randint(1, 100) <= self.packet_loss_percent:
|
||||||
|
return {
|
||||||
|
"type": "error",
|
||||||
|
"success": False,
|
||||||
|
"error": "RS-485 packet lost"
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.controller.handle_message(message)
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
def restore(self):
|
||||||
|
self.connected = True
|
||||||
|
|
||||||
|
def set_latency(self, latency_ms):
|
||||||
|
self.latency_ms = max(0, int(latency_ms))
|
||||||
|
|
||||||
|
def set_packet_loss(self, percent):
|
||||||
|
self.packet_loss_percent = max(0, min(100, int(percent)))
|
||||||
Reference in New Issue
Block a user