-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathacc_bridge.py
More file actions
206 lines (171 loc) · 7.04 KB
/
Copy pathacc_bridge.py
File metadata and controls
206 lines (171 loc) · 7.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
"""
acc_bridge.py — read ACC shared memory and stream telemetry to the Arduino
dashboard via serial USB.
Usage:
python acc_bridge.py # auto-detect Arduino COM port
python acc_bridge.py --port COM5
python acc_bridge.py --port COM5 --rate 30 # 30 Hz update (default)
python acc_bridge.py --dry-run # print frames to stdout instead
Frame format (sent at --rate Hz, terminated by '\\n'):
T75;B30;C0;R7892;G3;S148;LT83547;LL84102;BL82301;DT-432;P3;
FL42.5;FU3;TT88,91,86,85;TP27.4,27.6,27.2,27.3;
BT420,415,380,378;ABS3;TC2;MP4;BB54.1;FG0;AT22;RT34;GR98;ST1234;MR8500
The script is resilient:
- retries every 1 s while ACC is not running
- reopens the serial port if it disappears (e.g., USB unplug)
- flushes/closes cleanly on Ctrl+C
"""
from __future__ import annotations
import argparse
import signal
import sys
import time
from typing import Optional
import serial
from serial.tools import list_ports
from acc_shared_memory import ACCSharedMemory, Physics, Graphics, StaticInfo
BAUD = 115200
# ── serial port helpers ──────────────────────────────────────────────────
def find_arduino_port() -> Optional[str]:
"""Best-effort: first port whose description mentions Arduino/CH340/USB-SERIAL."""
candidates = list(list_ports.comports())
keywords = ("arduino", "ch340", "ch341", "usb-serial", "usb serial", "wch")
for p in candidates:
desc = (p.description or "").lower()
if any(k in desc for k in keywords):
return p.device
# fallback: if exactly one port exists, use it
if len(candidates) == 1:
return candidates[0].device
return None
def open_serial(port: str) -> serial.Serial:
return serial.Serial(port, BAUD, timeout=0, write_timeout=0.5)
# ── frame builder ────────────────────────────────────────────────────────
def build_frame(p: Physics, g: Graphics, s: StaticInfo) -> str:
"""Build a full telemetry frame with all 26 fields."""
# ACC gear: 0=R, 1=N, 2..N → normalize to -1=R, 0=N, 1..N
gear_norm = p.gear - 1
parts = [
f"T{int(p.gas * 100)}",
f"B{int(p.brake * 100)}",
f"C{int(p.clutch * 100)}",
f"R{p.rpms}",
f"G{gear_norm}",
f"S{int(p.speed_kmh)}",
f"LT{g.current_time_ms}",
f"LL{g.last_time_ms}",
f"BL{g.best_time_ms}",
f"DT{g.delta_ms}",
f"P{g.position}",
f"FL{p.fuel:.1f}",
f"FU{int(g.fuel_estimated_laps)}",
"TT" + ",".join(f"{int(t)}" for t in p.tyre_core_temp),
"TP" + ",".join(f"{x:.1f}" for x in p.tyre_pressure),
"BT" + ",".join(f"{int(t)}" for t in p.brake_temp),
f"ABS{g.abs}",
f"TC{g.tc}",
f"MP{g.engine_map}",
# brake_bias comes as 0..1 float; multiply by 100 for percentage
f"BB{p.brake_bias * 100:.1f}",
f"FG{g.flag}",
f"AT{int(p.air_temp)}",
f"RT{int(p.road_temp)}",
# surface_grip is 0..1; multiply by 100 for percentage display
f"GR{int(g.surface_grip * 100)}",
f"ST{g.stint_left_ms // 1000}",
f"MR{s.max_rpm}",
]
return ";".join(parts) + "\n"
# ── main loop ────────────────────────────────────────────────────────────
def run(port: Optional[str], rate_hz: int, dry_run: bool) -> int:
period = 1.0 / max(1, rate_hz)
sm = ACCSharedMemory()
ser: Optional[serial.Serial] = None
stop = {"flag": False}
def _sig(_a, _b):
stop["flag"] = True
signal.signal(signal.SIGINT, _sig)
if hasattr(signal, "SIGTERM"):
signal.signal(signal.SIGTERM, _sig)
print(f"[bridge] starting (rate={rate_hz}Hz, dry_run={dry_run})")
last_log = 0.0
frames = 0
while not stop["flag"]:
# 1) ensure ACC SM is open
if sm.physics is None:
if not sm.open():
print("[waiting for ACC]", end="\r", flush=True)
time.sleep(1.0)
continue
print("\n[bridge] ACC connected")
# 2) ensure serial is open (unless dry-run)
if not dry_run and ser is None:
chosen = port or find_arduino_port()
if not chosen:
print("[waiting for Arduino COM port]", end="\r", flush=True)
time.sleep(1.0)
continue
try:
ser = open_serial(chosen)
print(f"\n[bridge] serial open on {chosen} @ {BAUD}")
except (serial.SerialException, OSError) as e:
print(f"[bridge] serial open failed ({e}); retry in 1s")
time.sleep(1.0)
continue
# 3) read SM and send frame
try:
phys = sm.read_physics()
graf = sm.read_graphics()
stat = sm.read_static()
except Exception as e:
print(f"\n[bridge] SM read error ({e}); reopening")
sm.close()
time.sleep(0.5)
continue
frame = build_frame(phys, graf, stat)
if dry_run:
sys.stdout.write(frame)
sys.stdout.flush()
else:
try:
ser.write(frame.encode("ascii"))
except (serial.SerialException, OSError) as e:
print(f"\n[bridge] serial write failed ({e}); reopening")
try: ser.close()
except Exception: pass
ser = None
time.sleep(0.5)
continue
frames += 1
now = time.time()
if now - last_log >= 2.0:
print(f"[bridge] {frames} frames sent "
f"RPM={phys.rpms:5d} G={phys.gear-1:>2d} "
f"V={phys.speed_kmh:5.1f} "
f"LAP={graf.current_time_ms/1000:6.3f}s",
end="\r", flush=True)
last_log = now
time.sleep(period)
# cleanup
print("\n[bridge] shutting down")
if ser is not None:
try: ser.flush(); ser.close()
except Exception: pass
sm.close()
return 0
def main() -> int:
ap = argparse.ArgumentParser(description="ACC → Arduino dashboard bridge")
ap.add_argument("--port", help="serial port (e.g. COM5). Auto-detect if omitted.")
ap.add_argument("--rate", type=int, default=30, help="frames per second (default 30)")
ap.add_argument("--dry-run", action="store_true",
help="print frames to stdout instead of opening the serial port")
ap.add_argument("--list-ports", action="store_true",
help="list available COM ports and exit")
args = ap.parse_args()
if args.list_ports:
for p in list_ports.comports():
print(f"{p.device:10s} {p.description}")
return 0
return run(args.port, args.rate, args.dry_run)
if __name__ == "__main__":
sys.exit(main())