Skip to content

Commit d4f5473

Browse files
committed
add frontend visual serial port selector and remove unnecessary prints
1 parent 83765d0 commit d4f5473

File tree

9 files changed

+190
-15
lines changed

9 files changed

+190
-15
lines changed

Backend/.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,7 @@ recordedData/sessions/*.bin
77
recordedData/processedData/*.csv
88

99
# Python
10-
*.egg-info
10+
*.egg-info
11+
12+
# File sync
13+
Downloaded*/

Backend/core/comms.py

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import aiohttp
55
import asyncio
66
import config
7+
import serial
78

89
import signal
910
import sys
@@ -17,10 +18,11 @@
1718
byte_length = 0
1819
properties = []
1920
frontend_data = {}
20-
solar_car_connection = {'lte': False, 'udp': False}
21+
solar_car_connection = {'lte': False, 'udp': False, 'serial': False}
2122
# Convert dataformat to format string for struct conversion
2223
# Docs: https://docs.python.org/3/library/struct.html
2324
types = {'bool': '?', 'float': 'f', 'char': 'c', 'uint8': 'B', 'uint16': 'H', 'uint64': 'Q'}
25+
serial_port = {"device": "", 'baud': 115200} # shared object with core_api for setting serial device from frontend
2426

2527
def set_format(file_path: str):
2628
global format_string, byte_length, properties
@@ -45,7 +47,7 @@ def unpack_data(data):
4547

4648

4749
class Telemetry:
48-
__tmp_data = {'tcp': b'', 'lte': b'', 'udp': b'', 'file_sync': b''}
50+
__tmp_data = {'tcp': b'', 'lte': b'', 'udp': b'', 'file_sync': b'', 'serial': b''}
4951
latest_tstamp = 0
5052

5153
def listen_udp(self, port: int):
@@ -140,6 +142,56 @@ def listen_tcp(self, server_addr: str, port: int):
140142
solar_car_connection['tcp'] = False
141143
break
142144

145+
def serial_read(self):
146+
global frontend_data, serial_port
147+
latest_tstamp = 0
148+
while True:
149+
curr_device = serial_port['device']
150+
curr_baud = serial_port['baud']
151+
print('serial', curr_device)
152+
if(curr_device):
153+
# Establish a serial connection)
154+
ser = serial.Serial(curr_device, curr_baud)
155+
# if device has been updated then exit loop and connect to new device
156+
while curr_device == serial_port['device']:
157+
print('read serial')
158+
# Read data from serial port
159+
try:
160+
data = b''
161+
if(ser.in_waiting > 0):
162+
data = ser.read(ser.in_waiting)
163+
else:
164+
time.sleep(0.1)
165+
if not data:
166+
# No data received, continue listening
167+
continue
168+
print('read data')
169+
print('data:', data)
170+
packets = self.parse_packets(data, 'serial')
171+
for packet in packets:
172+
if len(packet) == byte_length:
173+
d = unpack_data(packet)
174+
latest_tstamp = time.time()
175+
try:
176+
frontend_data = d.copy()
177+
db.insert_data(d)
178+
except Exception as e:
179+
print(traceback.format_exc())
180+
continue
181+
solar_car_connection['serial'] = True
182+
if time.time() - latest_tstamp / 1000 > 5:
183+
solar_car_connection['lte'] = False
184+
break
185+
except Exception:
186+
print(traceback.format_exc())
187+
solar_car_connection['serial'] = False
188+
serial_port['device'] = ""
189+
break
190+
else:
191+
solar_car_connection['serial'] = False
192+
# wait before retry
193+
time.sleep(1)
194+
143195
async def fetch(self, session, url):
144196
try:
145197
async with session.get(url, timeout=2) as response:
@@ -257,11 +309,12 @@ def sigint_handler(signal, frame):
257309
def start_comms():
258310
# start file sync
259311
p.start()
260-
261-
312+
262313
# Start two live comm channels
263314
vps_thread = threading.Thread(target=lambda : asyncio.run(telemetry.remote_db_fetch(config.VPS_URL)))
264315
vps_thread.start()
265-
socket_thread = threading.Thread(target=lambda: telemetry.listen_udp(config.UDP_PORT))
316+
#socket_thread = threading.Thread(target=lambda: telemetry.listen_udp(config.UDP_PORT))
317+
#socket_thread.start()
318+
socket_thread = threading.Thread(target=lambda: telemetry.serial_read())
266319
socket_thread.start()
267320

Backend/core/core_api.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
from fastapi import APIRouter
2+
import serial.tools.list_ports
23
from . import comms
4+
from pydantic import BaseModel
5+
36
router = APIRouter()
47

58
@router.get("/single-values")
69
async def single_values():
7-
if comms.solar_car_connection['udp'] or comms.solar_car_connection['lte']:
10+
if comms.solar_car_connection['udp'] or comms.solar_car_connection['lte'] or comms.solar_car_connection['serial']:
811
latest_data = comms.frontend_data
912
latest_data['solar_car_connection'] = True
1013
latest_data['udp_status'] = comms.solar_car_connection['udp']
1114
latest_data['lte_status'] = comms.solar_car_connection['lte']
15+
latest_data['serial_status'] = comms.solar_car_connection['serial']
1216
latest_data['timestamps'] = f'{latest_data["tstamp_hr"]:02d}:{latest_data["tstamp_mn"]:02d}:' \
1317
f'{latest_data["tstamp_sc"]:02d}.{latest_data["tstamp_ms"]}'
1418
format_data = {}
@@ -17,3 +21,23 @@ async def single_values():
1721
json_data = {'response': format_data}
1822
return json_data
1923
return {'response': None}
24+
25+
26+
@router.get("/serial-info")
27+
async def list_serial_ports():
28+
"""return currently connected device and all available serial device"""
29+
ports = serial.tools.list_ports.comports()
30+
# Extract the device name from each port object
31+
return {'connected_device': {'device': comms.serial_port['device'], 'baud': comms.serial_port['baud']},
32+
'all_devices': [port.device for port in sorted(ports, key=lambda port: port.device)]
33+
}
34+
35+
class SerialDevice(BaseModel):
36+
device: str
37+
baud: int
38+
39+
@router.post("/connect-device")
40+
async def dev_conn(serial_device: SerialDevice):
41+
"""Connect to serial port, pass in empty device name for disconnect"""
42+
comms.serial_port['device'] = serial_device.device
43+
comms.serial_port['baud'] = serial_device.baud

Backend/file_sync

Backend/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ async def startup():
1111
process.start_processes()
1212

1313
if __name__ == '__main__':
14-
uvicorn.run(app='main:app', host="0.0.0.0", port=config.HOST_PORT)
14+
uvicorn.run(app='main:app', host="0.0.0.0", port=config.HOST_PORT, log_level='critical')
1515

1616

Backend/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@
99
author='Badger Solar Racing Software Team',
1010
author_email='',
1111
description='',
12-
install_requires=['uvicorn','fastapi','redis', 'numpy', 'XlsxWriter', 'pandas', 'aiohttp']
12+
install_requires=['uvicorn','fastapi','redis', 'numpy', 'XlsxWriter', 'pandas', 'aiohttp', 'pyserial']
1313
)

Frontend/src/Components/Communication/Communication.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,8 @@ export default function Communication(props) {
3838
<Flex flex='auto' direction='column'>
3939
<HeadingCell fontSize='2.2vh' label='Communication'/>
4040
<Flex flex='inherit' direction='column' pl='2' pt='2' >
41-
<CommsLabel
42-
boolean={props.data?.solar_car_connection[0]}
43-
label='Solar Car Connection'
44-
/>
4541
<HStack>
46-
<Text fontSize='2vh' style={{ textIndent: 30 }}>&#160;Packet Delay: </Text>
42+
<Text fontSize='2vh'>&#160;Packet Delay: </Text>
4743
<Text fontSize='2vh' backgroundColor={bgColor}>{_getFormattedPacketDelay()}</Text>
4844
</HStack>
4945
<CommsLabel
@@ -56,6 +52,11 @@ export default function Communication(props) {
5652
boolean={props.data?.lte_status[0]}
5753
label='LTE'
5854
/>
55+
<CommsLabel
56+
indent={true}
57+
boolean={props.data?.serial_status[0]}
58+
label='Serial'
59+
/>
5960
<CommsLabel
6061
boolean={props.data?.mainIO_heartbeat[0]}
6162
label='Main IO Heartbeat'

Frontend/src/Components/Dashboard/Dashboard.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import DataRecordingControl from "./DataRecordingControl";
1919
import Temperature from "../Temperature/Temperature";
2020
import dvOptions from "./dataViewOptions";
2121
import getColor from "../Shared/colors";
22+
import SerialSelector from "../SerialSelector/SerialSelector";
2223
import { ROUTES } from "../Shared/misc-constants";
2324
import fauxQueue from "../Graph/faux-queue.json";
2425

@@ -371,6 +372,18 @@ export default function Dashboard(props) {
371372
</Select>
372373
{switchDataView(dataView4)}
373374
</GridItem>
375+
<GridItem
376+
minH="min-content"
377+
rowStart={4}
378+
rowSpan={1}
379+
colStart={1}
380+
colSpan={2}
381+
borderColor={borderCol}
382+
borderWidth={1}
383+
p={1}
384+
>
385+
<SerialSelector/>
386+
</GridItem>
374387
</Grid>
375388
<GraphContainer
376389
flex="2 2 0"
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { useEffect, useState } from "react";
2+
import { Select, Flex, useInterval } from "@chakra-ui/react";
3+
4+
export default function SerialSelector() {
5+
const [allDevices, setAllDevices] = useState([]);
6+
const [selectedDevice, setSelectedDevice] = useState(""); // State to hold the selected device name
7+
const [selectedBaud, setSelectedBaud] = useState(115200);
8+
9+
const refresh = () => {
10+
fetch('/serial-info')
11+
.then((response) => {
12+
if (response.ok) {
13+
return response.json();
14+
} else {
15+
throw new Error(`Error fetching serial port with code ${response.status}`);
16+
}
17+
})
18+
.then((body) => {
19+
setSelectedDevice(body['connected_device']['device']); // Set default device
20+
setSelectedBaud(body['connected_device']['baud']);
21+
setAllDevices(body['all_devices']);
22+
}).catch(error => console.error('Fetch error:', error));
23+
};
24+
25+
useInterval(refresh, 3000);
26+
27+
useEffect(()=> {
28+
fetch("/connect-device", {
29+
method: "POST",
30+
body: JSON.stringify({
31+
device: selectedDevice,
32+
baud: selectedBaud
33+
}),
34+
headers: {
35+
"Content-type": "application/json"
36+
}
37+
});
38+
39+
}, [selectedBaud, selectedDevice])
40+
41+
const getSerialPort = () => {
42+
return (
43+
<Select
44+
placeholder='Select option'
45+
width={'30%'}
46+
padding={2}
47+
value={selectedDevice} // Controlled component with selectedDevice as the current value
48+
onChange={e => setSelectedDevice(e.target.value)} // Handler to update state on user selection
49+
>
50+
{allDevices.map(device => (
51+
<option key={device} value={device}>{device}</option>
52+
))}
53+
</Select>
54+
);
55+
};
56+
57+
const getBaud = () => {
58+
const defaultBaud = [4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000];
59+
return (
60+
<Select
61+
width={'30%'}
62+
padding={2}
63+
value={selectedBaud}
64+
onChange={e => setSelectedBaud(e.target.value)}
65+
>
66+
{defaultBaud.map(baud => (
67+
<option key={baud} value={baud}>{baud}</option>
68+
))}
69+
</Select>
70+
)
71+
}
72+
73+
return (
74+
<Flex flex="auto" direction="row" alignItems="center" justifyContent="center">
75+
<p>Serial Device:</p>
76+
{getSerialPort()}
77+
<p>Baud:</p>
78+
{getBaud()}
79+
</Flex>
80+
);
81+
}

0 commit comments

Comments
 (0)