Skip to content

Commit d680bdb

Browse files
Chrixcoclaude
andcommitted
Add CLAUDE.md handoff guide for next session
Captures architecture, the AC quirks we discovered live (Python 3.3 import limits, '%' end-marker, Windows ConnectionResetError, single- subscriber UDP, etc.), the data we receive but don't show yet, open work, and the verification recipe -- so a fresh Claude session can pick up the project without re-discovering everything. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 56d27a0 commit d680bdb

1 file changed

Lines changed: 217 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# CLAUDE.md — handoff for the next session
2+
3+
OnTrack is a PyQt6 telemetry dashboard for Assetto Corsa. Speaks AC's two
4+
first-party transports directly; no in-game plugin. Read this once before
5+
touching code so you don't relearn the gotchas the hard way.
6+
7+
## Run / test / lint
8+
9+
```bash
10+
pip install -e ".[dev]" # one-time
11+
ontrack # launches the dashboard (or: python -m ontrack_dashboard)
12+
pytest # 24 tests, all should be green
13+
python -m ruff check ontrack_dashboard tests
14+
python scripts/monitor.py # one-shot data audit; --watch for continuous
15+
```
16+
17+
The dashboard logs to **stderr**. The launch helper used during this
18+
session redirects to `dashboard.log` (gitignored). View → Console or
19+
**Ctrl+Shift+C** for the in-app live view.
20+
21+
## Architecture in one screen
22+
23+
```
24+
Assetto Corsa (Windows)
25+
26+
├── UDP port 9996 ────► UDPReceiver (QThread)
27+
│ RTCarInfo 328 B │
28+
│ ▼
29+
│ TelemetryPacket.from_bytes
30+
31+
└── Local\acpmf_physics ──► SharedMemoryReader (QThread)
32+
SPageFilePhysics │
33+
34+
PhysicsPacket.from_struct
35+
36+
MainWindow._dispatch_merged()
37+
── dataclasses.replace(udp, fuel=…, tyre_temps_c=…) ──►
38+
widgets paintEvent
39+
```
40+
41+
Two independent transports run in parallel `QThread`s. `MainWindow`
42+
keeps the latest of each and overlays physics-only fields (fuel, tyre
43+
core temps) onto the latest UDP packet before pushing to widgets via
44+
`dataclasses.replace`. Widgets stay oblivious to which transport
45+
supplied which field.
46+
47+
## Critical AC behaviours (each one bit us at least once)
48+
49+
1. **AC's embedded Python 3.3 has no `_socket`.** Means no plugin can
50+
`import socket` without bundling `_socket.pyd` from a long-EOL build.
51+
We dropped the plugin entirely in favour of AC's native transports.
52+
Don't rebuild the plugin path.
53+
54+
2. **UDP handshake is required.** AC won't push anything until the
55+
client sends `struct.pack('<3i', 0, 1, 0)` (operation 0 = HANDSHAKE)
56+
to port 9996. After the 408/808-byte response, send op 1
57+
(SUBSCRIBE_UPDATE). See `network/udp_receiver.py`.
58+
59+
3. **AC handshake strings use `%` as end-of-string marker**, not the C
60+
null. They're also padded with uninitialised stack memory after the
61+
terminator. `_decode_wchar` in `telemetry.py` cuts at whichever
62+
marker comes first (`%` or `\x00`). Regression tests cover both.
63+
64+
4. **Windows raises `ConnectionResetError` on UDP recvfrom** when the
65+
destination has no listener (ICMP "port unreachable"). We disable
66+
it with `SIO_UDP_CONNRESET=False` at socket creation AND defensively
67+
catch `ConnectionResetError` in the recv loop. Without this the
68+
receiver loop dies the moment AC isn't in a session.
69+
70+
5. **AC allows only one UDP subscriber at a time.** Running the
71+
dashboard and `scripts/monitor.py` simultaneously means the monitor
72+
gets zero frames. Stop the dashboard first or use the in-app
73+
console for live monitoring.
74+
75+
6. **Shared memory persists briefly after AC exits.** Don't assume
76+
"SHM available" means AC is in a session — `acs.exe` may have
77+
already quit. Check the process list if confused.
78+
79+
7. **AC sends (0, 0, 0) coordinates during loading screens.** The
80+
circuit map widget filters these out so the trace doesn't anchor on
81+
an origin point that never gets driven.
82+
83+
## Repo layout
84+
85+
```
86+
ontrack_dashboard/
87+
├── main.py entry point + logging config + LogBus install
88+
├── app.py MainWindow, the three-column layout, signal wiring
89+
├── telemetry.py wire-format types: TelemetryPacket, SessionInfo,
90+
│ the handshake protocol, RTCarInfo struct fmt
91+
├── theme.py colors, fonts, GLOBAL_QSS, build_app_palette
92+
├── logging_bridge.py singleton LogBus + handler for the in-app console
93+
├── network/
94+
│ ├── udp_receiver.py UDP client (handshake, subscribe, re-handshake)
95+
│ └── shared_memory.py SPageFilePhysics ctypes struct + reader thread
96+
├── settings/
97+
│ ├── config_manager.py ~/.config/ontrack/settings.json
98+
│ └── settings_dialog.py
99+
└── widgets/
100+
├── card.py NeumorphCard base (dual paint-time shadows)
101+
├── shift_indicator.py RPM LED strip + gear character
102+
├── speed_display.py big cyan number + half-arc
103+
├── acceleration_display.py G ball with magenta trail
104+
├── fuel_display.py vertical bar, amber number
105+
├── pedals_card.py throttle + brake bars
106+
├── input_graph.py rolling 7s throttle/brake trace
107+
├── assists_card.py ABS + TC pills (off / on / ACTIVE)
108+
├── wheel_temps_card.py 2x2 corners, color-coded by temp
109+
├── car_info_panel.py car/driver/track from handshake
110+
├── circuit_map_card.py auto-recorded track outline + position dot
111+
├── race_stats_panel.py lap counter, current/best/last, delta
112+
└── console_window.py in-app log + live values, with filters
113+
114+
tests/ 24 pytest cases (run `pytest -v` for the list)
115+
scripts/monitor.py standalone CLI audit tool
116+
```
117+
118+
## Conventions
119+
120+
- **Frozen dataclasses with slots** for all wire-format types
121+
(`TelemetryPacket`, `PhysicsPacket`, `SessionInfo`). Never mutate;
122+
always construct new ones (use `dataclasses.replace` for overlay).
123+
- **`from __future__ import annotations`** in every module.
124+
- **`logging.getLogger(__name__)`**, never `print`.
125+
- **No type checking framework**, but every public function gets type
126+
hints. Use `X | None` (PEP 604), `tuple[...]` generics, etc.
127+
Project is Python 3.10+.
128+
- **Ruff config** in `pyproject.toml` enforces E/F/W/I/B/UP/SIM. Run it
129+
after any non-trivial change.
130+
- **Widget update signature** is always `def update_data(self, packet:
131+
TelemetryPacket)`. If a widget needs session metadata
132+
(`car_name`/`track_name`/etc.), add a separate `set_session(session)`
133+
method and wire it in `MainWindow.on_session_info`.
134+
- **Painting**: each widget extends `NeumorphCard` and overrides
135+
`paint_content(painter)`. Use `self.content_rect()` for the drawable
136+
area inside padding+shadow margins. Use theme constants
137+
(`ACCENT_CYAN`, `FG_MUTED`, `FONT_LABEL_CAPS`, etc.), never hardcoded
138+
colors or fonts.
139+
140+
## Data the dashboard receives but doesn't show yet
141+
142+
Available from `TelemetryPacket` but not consumed by any widget:
143+
- `g_vert` (vertical G)
144+
- Most of the RTCarInfo wheel arrays (`slipAngle[4]`, `slipRatio[4]`,
145+
`tyreSlip[4]`, `ndSlip[4]`, `load[4]`, `Dy[4]`, `Mz[4]`,
146+
`tyreDirtyLevel[4]`, `camberRAD[4]`, `tyreRadius[4]`,
147+
`tyreLoadedRadius[4]`, `suspensionHeight[4]`) — we don't unpack them
148+
past the float slots we use. To consume any of them, extend
149+
`TelemetryPacket` and update the positional unpack in `from_bytes`.
150+
151+
Available from `PhysicsPacket`:
152+
- `tyre_pressures`, `tyre_wear` — captured, ready to be displayed
153+
- AC's shared memory carries ~80 more physics fields we don't parse
154+
(suspension travel, damage, heading/pitch/roll, DRS, TC level,
155+
carDamage[5], etc.). Add to the `SPageFilePhysics` ctypes struct in
156+
declaration order, then expose via `PhysicsPacket.from_struct`.
157+
158+
## Open work the user mentioned
159+
160+
In rough priority order:
161+
162+
1. **InputGraph button + filters.** The user asked for an action on
163+
the input-trace card that opens a detail window showing full
164+
session / last race / last lap, with filters to "improve driving on
165+
a specific circuit". Probably needs a `SessionRecorder` that
166+
accumulates samples per lap (detect via `packet.lap` increment),
167+
stored in `MainWindow`. Then a detail QDialog driven from a "…"
168+
button on the InputGraph card.
169+
170+
2. **Display tyre pressures / wear.** Both already in
171+
`PhysicsPacket`. Either extend `WheelTempsCard` to show secondary
172+
readings on hover/click, or add a separate tyre-detail card.
173+
174+
## How to verify a change works end-to-end
175+
176+
1. `pytest` — must stay green.
177+
2. `python -m ruff check ontrack_dashboard tests` — must say "All
178+
checks passed!".
179+
3. Launch the app (`ontrack`), open the in-app console
180+
(Ctrl+Shift+C), watch the Live Values tab + Log tab with AC running
181+
in a session. Field updates and packet counters prove the change is
182+
live, not just compiled.
183+
4. If you broke the wire format, `scripts/monitor.py` (with the
184+
dashboard stopped) shows what the protocol layer actually decodes.
185+
186+
## Don't do these
187+
188+
- **Don't rebuild the plugin.** It can't work without vendoring
189+
`_socket.pyd`, and we deliberately went a different route. See
190+
`feedback-ac-python-plugins` memory.
191+
- **Don't add per-widget visibility toggles to settings.** The curated
192+
layout is intentional; the old `show_speed` / `show_rpm` flags were
193+
removed when we restructured.
194+
- **Don't print to stdout/stderr.** Use `logging.getLogger(__name__)`
195+
so the in-app console captures it.
196+
- **Don't hardcode colors or fonts.** Use the theme constants.
197+
- **Don't force-push without `--force-with-lease`.** When pushing to
198+
a divergent main, see the `feedback-ontrack-push` memory for the
199+
established workflow.
200+
201+
## Useful one-liners
202+
203+
```powershell
204+
# What's actually in dashboard.log right now
205+
Get-Content C:\Users\chris\Documents\Github\OnTrack\dashboard.log
206+
207+
# Force-read AC's log even when it shows 0 bytes (Windows open-file lock)
208+
$log = "$env:USERPROFILE\Documents\Assetto Corsa\logs\py_log.txt"
209+
$fs = [System.IO.File]::Open($log,'Open','Read','ReadWrite')
210+
(New-Object System.IO.StreamReader($fs)).ReadToEnd(); $fs.Close()
211+
212+
# Is AC in a session right now?
213+
Get-Process | ? { $_.ProcessName -in @('acs','AssettoCorsa') }
214+
215+
# What's on UDP 20777/9996?
216+
netstat -ano -p UDP | Select-String "9996|20777"
217+
```

0 commit comments

Comments
 (0)