Skip to content

Commit d533ba1

Browse files
authored
Merge pull request #15 from Western-Formula-Racing/dev-frontend-can-processing
Implement Telemetry DataStore with rolling window and React hooks
2 parents ac9d25d + 1da584b commit d533ba1

15 files changed

Lines changed: 990 additions & 475 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ downloads/
1515
eggs/
1616
.eggs/
1717
lib/
18+
!pecan/Frontend/pecan-live-dashboard/src/lib/
1819
lib64/
1920
parts/
2021
sdist/
@@ -221,3 +222,4 @@ ENV/
221222

222223
.idea/workspace.xml
223224
/.idea
225+
node_modules/

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,56 @@ daq-radio/
4949
- Transmits data to base station
5050
- Supports both real CAN hardware and CSV simulation
5151

52+
### WebSocket Service Architecture
53+
54+
The frontend uses a **centralized WebSocket service** that runs independently of any component, ensuring continuous data flow across all pages.
55+
56+
#### Location
57+
```
58+
pecan/Frontend/pecan-live-dashboard/src/
59+
├── services/
60+
│ └── WebSocketService.ts # WebSocket connection manager
61+
├── App.tsx # Service initialization
62+
└── pages/
63+
└── Dashboard.tsx # Displays data from DataStore
64+
```
65+
66+
#### How It Works
67+
68+
**1. Service Initialization (App.tsx)**
69+
- WebSocketService starts when the app mounts
70+
- Connects to ESP32 WebSocket server
71+
- Initializes CAN processor with DBC file
72+
- Stays active during entire app session
73+
74+
**2. Message Processing (WebSocketService.ts)**
75+
```
76+
WebSocket Message → CAN Processor → DataStore → React Components
77+
```
78+
79+
**3. Automatic Reconnection**
80+
- Up to 5 reconnection attempts on disconnect
81+
- Linear backoff delay (2s, 4s, 6s, 8s, 10s) TODO: maybe change to exponential
82+
- Seamless recovery from network issues
83+
84+
**4. Component Updates**
85+
- Components use DataStore hooks to access data
86+
- No WebSocket logic in display components
87+
- Clean separation of concerns
88+
89+
#### Benefits
90+
- **Persistent Connection**: WebSocket stays alive across page navigation
91+
- **Centralized Management**: Single point of control for all telemetry data
92+
- **Automatic Recovery**: Handles disconnections gracefully
93+
- **Simpler Components**: Pages focus on display, not data fetching
94+
- **Scalability**: Multiple pages can show live data without multiple connections
95+
96+
#### Development URL
97+
```
98+
Development: ws://localhost:8080/ws
99+
Production: ws://192.168.4.1:8080/ws (ESP32 Access Point)
100+
```
101+
52102
### SavvyCAN Integration
53103
- Real-time CAN monitoring and analysis
54104
- UDP forwarding on port 12347

car-simulate/post.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# DEPRECATED (moving to websocket)
12
import requests
23
import time
34
import json

car-simulate/websocket_sender.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import asyncio
2+
import websockets
3+
import json
4+
import csv
5+
import os
6+
import time
7+
8+
# WebSocket server URL
9+
WS_URL = 'ws://localhost:8080/ws'
10+
11+
# Path to the CAN data file (CSV format: timestamp,CAN,canId,data1,data2,data3,data4,data5,data6,data7,data8)
12+
DATA_FILE = '2025-10-04-09-50-03.csv' # Replace with the exact filename if different
13+
14+
def load_can_data(file_path):
15+
"""Load CAN data from CSV file and format as JSON objects."""
16+
data = []
17+
if not os.path.exists(file_path):
18+
print(f"Error: File {file_path} not found.")
19+
return data
20+
21+
with open(file_path, 'r') as f:
22+
reader = csv.reader(f)
23+
for row in reader:
24+
if len(row) < 11 or row[1] != 'CAN':
25+
continue # Skip invalid rows
26+
try:
27+
message = {
28+
'time': int(row[0]),
29+
'canId': int(row[2]),
30+
'data': [int(x) for x in row[3:11]] # 8 data bytes
31+
}
32+
data.append(message)
33+
except ValueError as e:
34+
print(f"Skipping invalid row: {row} - {e}")
35+
return data
36+
37+
async def send_batch_websocket(websocket, batch):
38+
"""Send a batch of CAN messages via WebSocket."""
39+
try:
40+
message = json.dumps(batch)
41+
await websocket.send(message)
42+
print(f"Sent {len(batch)} messages successfully via WebSocket.")
43+
except Exception as e:
44+
print(f"Failed to send batch: {e}")
45+
raise
46+
47+
async def main():
48+
can_data = load_can_data(DATA_FILE)
49+
if not can_data:
50+
print("No CAN data loaded. Exiting.")
51+
return
52+
53+
batch_size = 100
54+
interval = 1 / 5 # 5 Hz = 0.2 seconds
55+
56+
try:
57+
# Connect to WebSocket server
58+
print(f"Connecting to WebSocket server at {WS_URL}...")
59+
async with websockets.connect(WS_URL) as websocket:
60+
print("Connected successfully!")
61+
62+
index = 0
63+
while True:
64+
# Get the next batch of 100 messages, wrapping around if necessary
65+
batch = can_data[index:index + batch_size]
66+
if len(batch) < batch_size:
67+
# If not enough, take from the start
68+
batch += can_data[:batch_size - len(batch)]
69+
70+
await send_batch_websocket(websocket, batch)
71+
index = (index + batch_size) % len(can_data)
72+
73+
# Wait for the interval
74+
await asyncio.sleep(interval)
75+
76+
except websockets.exceptions.ConnectionClosed:
77+
print("WebSocket connection closed.")
78+
except Exception as e:
79+
print(f"Error: {e}")
80+
81+
if __name__ == '__main__':
82+
# Install required package: pip install websockets
83+
try:
84+
import websockets
85+
except ImportError:
86+
print("Please install websockets package: pip install websockets")
87+
exit(1)
88+
89+
asyncio.run(main())

0 commit comments

Comments
 (0)