diff --git a/simulator/app.py b/simulator/app.py index f7a639f..45d3f49 100644 --- a/simulator/app.py +++ b/simulator/app.py @@ -108,5 +108,19 @@ def restore_rs485(): 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__": app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/simulator/pico_sim.py b/simulator/pico_sim.py index 0456889..0b85bd0 100644 --- a/simulator/pico_sim.py +++ b/simulator/pico_sim.py @@ -7,30 +7,30 @@ from protocol import ( toggle_ignition_request, toggle_sensor_fault_request, ) +from transport import RS485Transport class PicoSimulator: def __init__(self, controller): - self.controller = controller + self.transport = RS485Transport(controller) self.last_status = None self.primary_link = "rs485" - self.rs485_connected = True self.backup_link_available = True self.messages_sent = 0 self.messages_received = 0 self.last_message_time = None + self.last_latency_ms = None def send_message(self, message): self.messages_sent += 1 + start = time.time() - if not self.rs485_connected: - return { - "type": "error", - "success": False, - "error": "RS-485 disconnected" - } + response = self.transport.send(message) - 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.last_message_time = int(time.time()) return response @@ -42,9 +42,7 @@ class PicoSimulator: if self.last_status: self.last_status["network"]["rs485_connected"] = False self.last_status["network"]["communication_lost"] = True - self.last_status["network"]["messages_sent"] = self.messages_sent - self.last_status["network"]["messages_received"] = self.messages_received - self.last_status["network"]["last_message_time"] = self.last_message_time + self.add_comms_to_status(self.last_status) return self.last_status return { @@ -59,22 +57,24 @@ class PicoSimulator: "communication_lost": True, "wifi_enabled": False, "wifi_override_active": False, - "starlink_enabled": False, - "messages_sent": self.messages_sent, - "messages_received": self.messages_received, - "last_message_time": self.last_message_time + "starlink_enabled": False }, "config": {} } 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"]["messages_sent"] = self.messages_sent - self.last_status["network"]["messages_received"] = self.messages_received - self.last_status["network"]["last_message_time"] = self.last_message_time + self.add_comms_to_status(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): return self.send_message(set_relay_request(relay, state)) @@ -88,17 +88,26 @@ class PicoSimulator: return self.send_message(toggle_sensor_fault_request(sensor)) def disconnect_rs485(self): - self.rs485_connected = False + self.transport.disconnect() 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): return { "primary": self.primary_link, "backup_available": self.backup_link_available, - "rs485_connected": self.rs485_connected, + "rs485_connected": self.transport.connected, "messages_sent": self.messages_sent, "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 } diff --git a/simulator/templates/index.html b/simulator/templates/index.html index 3b96a87..ef41eb9 100644 --- a/simulator/templates/index.html +++ b/simulator/templates/index.html @@ -97,11 +97,27 @@
Messages Sent --
Messages Received --
Last Message --
+
Latency --
+
Packet Loss --
+
+ + + + + +
+

Sensor Fault Simulation

@@ -389,6 +405,14 @@ async function loadStatus() { document.getElementById('commsLast').textContent = data.network.last_message_time ? 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); @@ -462,6 +486,25 @@ async function restoreRs485() { 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) { await fetch(`/sensor/${name}/fault`, { method: 'POST', diff --git a/simulator/transport.py b/simulator/transport.py new file mode 100644 index 0000000..db66024 --- /dev/null +++ b/simulator/transport.py @@ -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)))