From 28aa8cefc8c8cd89eec12705ddf4bc60e77d7583 Mon Sep 17 00:00:00 2001 From: royjr Date: Sun, 19 Jul 2026 19:24:38 -0400 Subject: [PATCH] more dbc updates --- opendbc_repo | 2 +- .../ui/sunnypilot/onroad/radar_tracks.py | 35 +- .../ui/sunnypilot/onroad/test_radar_tracks.py | 41 ++ tools/radar/PLATFORM_MATRIX.md | 62 ++ tools/radar/RADAR_RESEARCH.md | 417 +++++++------- tools/radar/SIGNALS.md | 268 ++++++++- tools/radar/ui.py | 543 ++++++++++++++++-- 7 files changed, 1106 insertions(+), 262 deletions(-) create mode 100644 tools/radar/PLATFORM_MATRIX.md diff --git a/opendbc_repo b/opendbc_repo index a81c9fdb9f..e4360b039f 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit a81c9fdb9f25b80e9ffeeb4d2e53d61639f95f4e +Subproject commit e4360b039f26eeadc3b4253f40626c28fa346cd4 diff --git a/selfdrive/ui/sunnypilot/onroad/radar_tracks.py b/selfdrive/ui/sunnypilot/onroad/radar_tracks.py index b2c334b767..a43438373f 100644 --- a/selfdrive/ui/sunnypilot/onroad/radar_tracks.py +++ b/selfdrive/ui/sunnypilot/onroad/radar_tracks.py @@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details. from dataclasses import dataclass import math import pyray as rl -from opendbc.car.hyundai.radar_interface import RADAR_3A5_3C4 +from opendbc.car.hyundai.radar_interface import RADAR_235_248, RADAR_3A5_3C4 from openpilot.system.ui.lib.application import FontWeight from openpilot.system.ui.widgets.label import UnifiedLabel @@ -31,6 +31,7 @@ class ProjectedRadarTrack: radius: float color: object source_index: int + camera_object: bool track_id: int @@ -62,7 +63,31 @@ def radar_track_source_index(track, sources) -> int: return 0 -def draw_radar_source_marker(center: rl.Vector2, radius: float, color: rl.Color, source_index: int) -> None: +def is_camera_object_source(source) -> bool: + return source.startAddress == RADAR_235_248.start_addr and source.endAddress == RADAR_235_248.end_addr + + +def radar_track_source(track, sources): + address = int(track.sourceAddress) + bus = int(track.sourceBus) + if address == 0: + return None + return next(( + source for source in sources + if source.startAddress <= address <= source.endAddress and source.bus == bus + ), None) + + +def radar_source_label(source) -> str: + prefix = "CAM " if is_camera_object_source(source) else "" + return f"{prefix}{source.startAddress:X}-{source.endAddress:X}" + + +def draw_radar_source_marker(center: rl.Vector2, radius: float, color: rl.Color, source_index: int, + camera_object: bool = False) -> None: + if camera_object: + rl.draw_poly(center, 3, radius, -90.0, color) + return if source_index <= 0: rl.draw_circle(int(center.x), int(center.y), radius, color) return @@ -113,7 +138,7 @@ def format_radar_tracks_onroad_columns(live_tracks, v_ego: float = 0.0) -> tuple if not sources: return "", "none", "", "", "", "" - range_text = "\n".join(f"{source.startAddress:X}-{source.endAddress:X}" for source in sources) + range_text = "\n".join(radar_source_label(source) for source in sources) count_text = "\n".join(str(source.trackCount) for source in sources) motion_states = [int(track.motionState) for track in live_tracks.points] @@ -283,12 +308,14 @@ class RadarTracks: radius = max(1, track_size - 5) if stationary else track_size if not measured: radius = max(1, radius - COASTED_RADIUS_REDUCTION) + source = radar_track_source(track, sources) projected_tracks.append(ProjectedRadarTrack( x=pt[0], y=pt[1], radius=radius, color=color, source_index=radar_track_source_index(track, sources), + camera_object=source is not None and is_camera_object_source(source), track_id=int(track.trackId), )) @@ -307,7 +334,7 @@ class RadarTracks: rl.draw_ring(center, track.radius + 2, track.radius + 5, 0, 360, 24, highlight_color) highlighted_positions[track.track_id] = (x, y) draw_radar_source_marker( - rl.Vector2(x, y), track.radius, track.color, track.source_index, + rl.Vector2(x, y), track.radius, track.color, track.source_index, track.camera_object, ) return highlighted_positions diff --git a/selfdrive/ui/sunnypilot/onroad/test_radar_tracks.py b/selfdrive/ui/sunnypilot/onroad/test_radar_tracks.py index f1e046857d..421958d80b 100644 --- a/selfdrive/ui/sunnypilot/onroad/test_radar_tracks.py +++ b/selfdrive/ui/sunnypilot/onroad/test_radar_tracks.py @@ -78,6 +78,16 @@ def test_format_radar_tracks_columns_range_and_count(): assert format_radar_tracks_onroad_columns(live_tracks) == ("3A5-3C4", "2", "1", "1", "0", "") +def test_format_camera_objects_are_not_labeled_as_radar(): + live_tracks = car.RadarData.new_message() + live_tracks.trackSources = [{"startAddress": 0x235, "endAddress": 0x248, "bus": 1, "trackCount": 3}] + points = live_tracks.init("points", 3) + for point in points: + point.motionState = 2 + + assert format_radar_tracks_onroad_columns(live_tracks) == ("CAM 235-248", "3", "3", "0", "0", "") + + def test_format_radar_tracks_columns_stacks_all_ranges_with_preferred_first(): live_tracks = car.RadarData.new_message() live_tracks.trackSources = [ @@ -114,6 +124,13 @@ def test_format_radar_tracks_columns_shows_non_motion_source(): assert format_radar_tracks_onroad_columns(live_tracks, v_ego=20.0) == ("500-51F", "4", "0", "0", "4", "") +def test_format_radar_tracks_columns_shows_64_track_source(): + live_tracks = car.RadarData.new_message() + live_tracks.trackSources = [{"startAddress": 0x500, "endAddress": 0x53F, "bus": 1, "trackCount": 7}] + + assert format_radar_tracks_onroad_columns(live_tracks) == ("500-53F", "7", "0", "0", "0", "") + + def test_draw_radar_tracks_applies_screen_offset(monkeypatch): live_tracks = car.RadarData.new_message() points = live_tracks.init("points", 1) @@ -203,6 +220,30 @@ def test_draw_radar_tracks_uses_source_shapes_with_preferred_circle(monkeypatch) assert polygons == [(0x500, 4, 6, 45.0)] +def test_draw_camera_objects_uses_triangle(monkeypatch): + live_tracks = car.RadarData.new_message() + live_tracks.trackSources = [{"startAddress": 0x235, "endAddress": 0x248, "bus": 1, "trackCount": 1}] + point = live_tracks.init("points", 1)[0] + point.dRel = 25 + point.yRel = 1 + point.vRel = 2 + point.motionState = radar_tracks.DBC_MOTION_MOVING + point.measured = True + point.sourceAddress = 0x235 + point.sourceBus = 1 + polygons = [] + monkeypatch.setattr( + radar_tracks.rl, "draw_poly", + lambda center, sides, radius, rotation, color: polygons.append((center.x, sides, radius, rotation)), + ) + + radar_tracks.RadarTracks().draw_radar_tracks( + live_tracks, lambda d_rel, y_rel, z: (d_rel, 30), path_offset_z=1.2, track_size=6, + ) + + assert polygons == [(25, 3, 6, -90.0)] + + def test_draw_radar_tracks_shrinks_stationary_dots(monkeypatch): live_tracks = car.RadarData.new_message() point = live_tracks.init("points", 1)[0] diff --git a/tools/radar/PLATFORM_MATRIX.md b/tools/radar/PLATFORM_MATRIX.md new file mode 100644 index 0000000000..963c000a13 --- /dev/null +++ b/tools/radar/PLATFORM_MATRIX.md @@ -0,0 +1,62 @@ +# Hyundai/Kia/Genesis radar range matrix + +This is a forum-ready summary of the routes analyzed for the sunnypilot radar +tracks work. It describes observed protocol families, not guaranteed equipment +for every model year, market, trim, or harness. A range can be present but empty +when the radar is inactive. Raw bus number is also not a layout identifier: +follow the sustained logical A-CAN source and reject brief forwarded copies. + +Status terms: + +- **Confirmed**: complete range, checksum/cadence, and tracked-object layout + were validated across route data. +- **Observed/optional**: verified on at least one qualifying route, but not + every route or trim carrying the main front-radar family. +- **Candidate**: source association or exact semantics remain incomplete. +- **Not radar**: camera or fused-output traffic overlapping radar-like ranges. + +## Platform matrix + +| Platform or platform group | Front tracked-object range | Front layout | Additional radar traffic | Camera/fused traffic that is not another radar | Status and notes | +| --- | --- | --- | --- | --- | --- | +| Kia K7 2017; older Hyundai Ioniq; sampled Ioniq PHEV | `0x500–0x53F` | 64 slots, 8 B each, about 20 Hz | `0x4E0`, `0x4E1`, `0x4E3` status/path messages | Legacy `0x238–0x24F` may coexist on applicable cars | **Confirmed** 64-slot legacy/Delphi-ESR dialect. | +| Kia Niro EV first generation; legacy Hyundai Palisade; Sonata; Sonata Hybrid | `0x500–0x51F` | 32 slots, 8 B each, about 20 Hz | `0x4E0–0x4E5` status/build/health/alignment family | Legacy `0x238–0x255` or related camera candidates may coexist by platform | **Confirmed** 32-slot legacy dialect. The upper `0x520–0x53F` bank is absent. | +| Hyundai Kona EV 2022; Kia Ceed PHEV | `0x602–0x617` | Two packed tracks in implemented messages | None established | None established | **Observed/candidate** legacy front family; the relationship between `0x602–0x611` and `0x612–0x617` remains incomplete. | +| Hyundai Elantra HEV 2024 | `0x210–0x21F` | Detailed dialect, 32 tracks in 16 × 32 B messages, 20 Hz | No paired corner family established | Platform-dependent camera traffic | **Confirmed** detailed `210` front radar. | +| Hyundai Ioniq 5; Hyundai Palisade 2023; Kia EV6, older HDA2 samples | `0x210–0x21F` | Detailed dialect, 32 tracks | Right-front candidate: `0x240–0x24F` objects plus `0x270–0x277` compact records. Left-front candidate: `0x278–0x287` objects plus `0x288–0x28F` compact records. | Isolated `0x235` traffic is not a complete camera-object family | **Confirmed** front radar. Paired front corners are **observed/optional** only on qualifying routes; A=right and B=left are strongly inferred but still need physical occlusion confirmation. | +| Hyundai Santa Cruz 2025; Tucson 4th generation; Tucson HEV 2025; Kia Sportage 5th generation | `0x210–0x21F` | Compact dialect, 32 tracks | No corner family established in checked routes | Platform-dependent camera traffic | **Confirmed** compact `210` front radar. Compact and detailed `210` share core kinematics but use different lifecycle fields. | +| Hyundai Ioniq 6 | `0x3A5–0x3C4` | Common 32-track layout, 24 B each, 20 Hz | No independent corner range established | `0x235–0x248` camera objects and `0x360–0x366` camera lane/path may be present | **Confirmed** common `3A5` front radar. | +| Hyundai Kona/Kona HEV/Kona EV 2nd generation; Santa Fe HEV 5th generation; Sonata 2024 | `0x3A5–0x3C4` | Common 32-track layout | No independent corner range established | Platform-dependent `0x235`, `0x360`, and CCNC `0x162` fused traffic | **Confirmed** common `3A5` front radar. CCNC/HDA generation does not imply rich radar extensions. | +| Kia Carnival HEV 2026; EV6 2025; K4 2025; K5 2025; Niro EV/HEV 2nd generation | `0x3A5–0x3C4` | Common 32-track layout | No independent corner range established | Platform-dependent `0x235`, `0x360`, and CCNC `0x162` fused traffic | **Confirmed** common `3A5` front radar. Bus 0/1 varies by architecture; a bus-2 copy can be forwarding. | +| Kia EV9; Hyundai Ioniq 9 | `0x3A5–0x3C4` | Rich 32-track layout with optional width, length, absolute speed, and orientation | `0x3D0–0x3D4` synchronized 32-position auxiliary scan array | `0x235–0x248` camera objects; `0x360–0x366` camera lane/path; CCNC `0x162` fused traffic | **Confirmed** rich `3A5`; `3D0` is **observed/optional** radar auxiliary traffic. Former `3D0` speed/azimuth interpretations were falsified, so it is counted but not projected as geometry. | +| Kia Sportage HEV 2026 forum sample | Overlaps `0x3A5–0x3C4` addresses | Payload is incompatible with the common `3A5` layout | Unknown | Unknown | **Excluded** from the common decoder; address overlap alone is not layout compatibility. | + +## Other observed ranges + +| Range | Applicable examples | Classification | +| --- | --- | --- | +| `0x235–0x248`, 32 B at about 33 Hz | Newer CAN-FD platforms, including rich EV9/Ioniq 9 captures | **Not radar:** forward-camera object list with class, motion, ID, geometry, velocity, and acceleration. | +| `0x360–0x366`, 32 B | Newer CAN-FD platforms carrying camera lane/path output | **Not radar:** forward-camera lane/path family. | +| `0x180–0x184`, `0x1B6–0x1B9`, `0x2BB–0x2BE` | Sampled Kia Sorento/Sorento HEV and Genesis GV70 candidates | **Not radar:** forward-camera auxiliary lists according to downloaded Hyundai DBCs. | +| `0x238–0x24F`, 8 B | Ioniq PHEV/HEV 2022, legacy Palisade, Niro PHEV candidates | **Not established as radar:** strong legacy forward-camera-perception candidate. | +| `0x238–0x255`, 8 B, often with `0x25A–0x25E` | Custin, Elantra/Elantra HEV 2021, Sonata/Sonata Hybrid candidates | **Not established as radar:** extended legacy forward-camera-perception candidate. | +| `CCNC_0x162` | CCNC platforms | **Not raw radar:** stock fused/cluster lead, side, and rear slots with unknown upstream fusion. | +| Bus-9 `0x300/0x400/0x500/0x600` blocks | Historical EV6 prototype capture | **Historical candidate:** four-corner FL/FR/RL/RR prototype; not reconfirmed in the current production-route corpus. | + +## Supported combinations observed in the corpus + +| Combination | Front | Corners or radar auxiliary | +| --- | --- | --- | +| Legacy front, 32 slots | `0x500–0x51F` | `0x4E0–0x4E5` companion/status | +| Legacy front, 64 slots | `0x500–0x53F` | `0x4E0`, `0x4E1`, `0x4E3` companion/status | +| Legacy alternate front | `0x602–0x617` | None established | +| CAN-FD detailed front only | `0x210–0x21F` | None established | +| CAN-FD detailed front plus paired front corners | `0x210–0x21F` | A/right candidate `0x240/0x270` families plus B/left candidate `0x278/0x288` families | +| CAN-FD compact front only | `0x210–0x21F` | None established | +| CAN-FD common front | `0x3A5–0x3C4` | Camera/fused families may coexist but are not independent radar tracks | +| CAN-FD rich front plus auxiliary scan | `0x3A5–0x3C4` | Optional `0x3D0–0x3D4` auxiliary radar array | + +No checked route proves a combination containing both the paired +`0x240/0x270–0x28F` corner family and `0x3D0–0x3D4`. No distinct production +rear-radar tracked-object range has been confirmed. Rear objects in +`CCNC_0x162` are fused outputs and do not prove a raw rear-radar range. diff --git a/tools/radar/RADAR_RESEARCH.md b/tools/radar/RADAR_RESEARCH.md index e5b711c9ce..5fce060ac1 100644 --- a/tools/radar/RADAR_RESEARCH.md +++ b/tools/radar/RADAR_RESEARCH.md @@ -1,231 +1,228 @@ -# Hyundai/Kia/Genesis radar research roadmap +# Hyundai/Kia/Genesis radar source inventory -This document tracks which object sources exist on each platform, what role -they appear to serve, and what must still be proved. Detailed signal evidence -for the best-understood family is kept in [SIGNALS.md](SIGNALS.md). +This document separates physical radar sources from camera and fused-object +traffic. Signal-level evidence is in [SIGNALS.md](SIGNALS.md). +The copy-ready vehicle/range summary is in +[PLATFORM_MATRIX.md](PLATFORM_MATRIX.md). -The intended end state is an inventory for every platform with four slots: +## Confidence and safety rules -1. **Front** — the forward long-range radar used for longitudinal driving. -2. **Corner** — one CAN message family representing the car's corner-radar - system. A family may contain two or four physical sensors; "one corner - source" does not mean one sensor. -3. **Extras/fused** — ADAS or cluster object outputs that may combine radar, - camera, and other inputs. These must not be called raw radar without proof. -4. **Rear** — a distinct rear-facing track source, if the car exposes one. - Rear tracks may instead be part of the corner family. +- **Confirmed layout** means that bit positions, scales, and behavior agree + across multiple routes. It does not automatically prove the physical ECU. +- **Strong role** means that topology, cadence, geometry, and related messages + support the assignment, but a controlled sensor-occlusion test is still + desirable. +- **Candidate** means useful research data only. Candidate fields must not + affect control or production filtering. +- Identify a source by the complete address set, payload sizes, cadence, + checksum behavior, and logical CAN path. An address or raw bus number alone + is not enough. +- A short copy on another bus is normally a forwarding echo. It is not a + second sensor unless timestamps and payload differences prove independence. -Each slot should eventually name a confirmed source or say **verified absent**. -The inventory must not assume that every platform has every type of hardware. +## Current source families -## Confidence labels - -- **Confirmed** — decoded geometry and behavior agree across multiple routes - or controlled observations. -- **Implemented** — a runtime layout exists and produces plausible tracks, but - its physical role or platform coverage still needs systematic confirmation. -- **Candidate** — a route/post/code clue identifies a likely source, but its - role or layout is not yet established. -- **Prototype** — useful prior reverse-engineering evidence from code that was - never merged or completed. -- **Unknown** — detected traffic with no reliable role yet. -- **Incompatible** — the address range overlaps a known family but its bytes - do not use that layout. -- **Verified absent** — the relevant buses were observed under suitable - conditions and no source exists. - -## Research order - -### 1. Finish the front-radar inventory - -Front radar comes first because it is the source that can affect openpilot -driving. For each existing layout, validate its bus, message size, cadence, -track lifecycle, distance, lateral position, relative velocity, and duplicate -forwarding behavior across multiple platforms. - -Then investigate the unimplemented route families in this order: - -1. `0x238–0x24F` -2. `0x238–0x255` -3. the combined `0x180–0x184`, `0x1B6–0x1B9`, and `0x2BB–0x2BF` sets -4. the incomplete `0x500–?` Ioniq PHEV 2019 observation - -These are front-radar candidates because of how they were grouped in the -supplied route post, not because their physical role has been proved. - -A front source should show forward-centred geometry, useful range ahead of the -car, longitudinal relative speed, stable lead-vehicle continuity, and the -expected response when ego speed changes. Video alignment and route maneuvers -must confirm that it is not a corner or fused-object source. - -### 2. Identify corner-radar systems - -There are two separate clues and they must not be conflated: - -- The supplied route post labels `0x235–0x248` as "corner radar available on - some HDA2 vehicles." The branch already has a one-track-per-message decoder - for this range, but its physical coverage still needs route-level proof. -- The closed [EV6 corner-radar prototype PR](https://github.com/commaai/openpilot/pull/24221) - used bus 9 and four eight-address blocks: `0x300–0x307`, - `0x400–0x407`, `0x500–0x507`, and `0x600–0x607`. Each frame contained - eight subtracks. The code mapped the four blocks to front-left, front-right, - rear-left, and rear-right sensors, but relative speed and status decoding - were unfinished. - -The EV6 prototype is evidence for a four-corner architecture, not evidence -that every HDA2 car or every `0x235–0x248` source uses that architecture. - -Corner confirmation should use scenes with passing traffic, blind-spot -occupancy, cross traffic, and targets moving around the car. Expected evidence -includes short-to-medium range, strong lateral coverage, stable left/right -sectors, and — if rear corners are included — tracks behind the rear axle. -Bus/address blocks should be correlated with individual sectors and physical -sensor occlusion where possible. - -### 3. Trace extras and fused objects - -Stock `CCNC_0x162` lead, left, right, and rear tofu slots are a separate fused -cluster/ADAS output. Existing route evidence shows that these slots can be -active when the optional `3A5–3C4` geometry fields are unavailable, so they -cannot be reconstructed by treating the rich `3A5` bytes as extra tracks. - -The next step is to locate the inputs upstream of `CCNC_0x162`, correlate each -slot with front and corner tracks, and determine whether the output is radar, -camera, or multi-sensor fusion. Unknown extra address groups and unknown DBC -fields come after the front and corner inventories because their behavior is -easier to interpret once the physical radar sources are known. - -Fused outputs may be useful for cluster visualization, but they must not be -fed into driving as independent radar measurements unless duplication, -latency, lifecycle, and provenance are understood. - -### 4. Confirm a rear source only when tracks exist - -Do not create a separate rear-radar category merely because the cluster has -rear tofu slots. Record a rear source only when a distinct message family -outputs repeatable rear-facing tracks. If the rear tracks come from the same -four-corner family, document that under **Corner** and mark **Rear** as -contained in that family. - -## Known and candidate source families - -| Source | Runtime layout | Current role | Status and next proof | +| Source | Layout | Role | Current conclusion | | --- | --- | --- | --- | -| `0x3A5–0x3C4` | 32 × 24-byte messages at 20 Hz | Front | Confirmed across 21 common-layout routes and 817,852 checksum-valid frames. `TRACK_QUALITY`, `RCS`, lifecycle, kinematics, and the optional rich extension are decoded. No dedicated oncoming value was found. Sportage HEV 2026 has an incompatible overlapping layout. | -| `0x210–0x21F` | 16 × 32-byte messages at 20 Hz; two tracks per message | Front | Confirmed across 16 active routes and two dialects. Detailed and compact layouts share the core; compact adds persistent object IDs and uses motion code 3 for stopped and 4 for oncoming, while detailed has no dedicated oncoming value. Three additional streams were empty and one non-HDA2 Telluride route had no 210 range. | -| `0x500–0x51F` | 32 × 8-byte messages at 20 Hz | Likely front | Implemented. Confirm special behavior around `0x501/0x502` and validate lateral conversion from azimuth. | -| `0x602–0x617` | Generator has double-track `0x602–0x611` and alternate single-track `0x612–0x617` formats | Likely front | Partially implemented: runtime currently consumes only `0x602–0x611`. Confirm whether `0x612–0x617` is the same objects, an alternate view, or a separate output before expanding it. | -| `0x235–0x248` | 20 × 32-byte messages at 33 Hz; one track per message | Corner candidate | Implemented geometry, but the post's corner classification still needs HDA2 route/video confirmation. | -| `0x238–0x24F` | Not implemented | Front candidate | Reverse layout and confirm physical role on the four listed platforms. It overlaps `0x235–0x248`, so detection must use the complete range, size, and layout rather than address membership alone. | -| `0x238–0x255` | Not implemented | Front candidate | Reverse layout and distinguish it from both `0x238–0x24F` and `0x235–0x248`. | -| `0x180–0x184`, `0x1B6–0x1B9`, `0x2BB–0x2BF` | Not implemented | Unknown multi-range source | Determine whether the three blocks are one radar protocol, multiple sensors, or fused/metadata outputs before assigning a role. | -| bus 9: `0x300/0x400/0x500/0x600` eight-address blocks | Old EV6 prototype; eight subtracks per message | Four-corner prototype | Search HDA2 logs for the same topology and finish status/relative-speed decoding if found. Do not confuse its `0x500` block with the legacy front `0x500–0x51F` family on another bus/layout. | -| `CCNC_0x162` | Fixed cluster-object slots | Extras/fused | Confirmed as a separate stock output, but its upstream sensor inputs are unknown. | +| `0x500–0x51F` or `0x500–0x53F` | 32 or 64 × 8 B at about 20 Hz | Front radar | Shared legacy core with a Delphi ESR extension on the 64-slot dialect. K7, older Ioniq, and Ioniq PHEV samples carry 64 slots; Niro EV, Palisade, Sonata, and Sonata Hybrid samples carry 32. Implemented as standardized `RADAR_500_53F` with an optional upper half, per-source cycle end, and runtime 32-to-64 upgrade. Corrected 0.1-degree geometry is shared; 64-slot-only ESR fields use explicit aliases for 0.05 m/s² acceleration, lateral rate, width, oncoming, grouping, bridge-object, and range mode, while 32-slot acceleration retains 0.02 m/s². | +| `0x4E0–0x4E5` | 8 B at the `500` radar cadence | Front-radar companion/status | Strong source association across every sampled `500` route. Shared `4E0` is decoded as ESR vehicle speed, yaw, curvature, 16-bit scan index, timestamp, counter, and communication health. The 64-slot radar's `4E1` matches ESR Status2; 32-slot `4E1` retains only the shared counter plus unresolved static metadata. `4E3` path IDs are shared and route-validated. The 32-slot-only `4E2`, `4E4`, and `4E5` carry BCD build metadata, protocol-derived raw ADC channels, and factory/vertical alignment status. | +| `0x602–0x617` | 8 B, two packed tracks in the implemented messages | Front radar | Implemented legacy front layout. The relationship between `602–611` and the alternate `612–617` definitions remains incomplete. | +| `0x210–0x21F` | 16 × 32 B at 20 Hz, two tracks per message | Front radar | Confirmed 32-track front layout. Detailed and compact dialects share the same core. | +| `0x3A5–0x3C4` | 32 × 24 B at 20 Hz, one track per message | Front radar | Confirmed front layout. EV9 and Ioniq 9 populate its optional object-size/orientation extension. | +| `0x235–0x248` | 20 × 32 B at about 33 Hz | **Forward-camera objects** | Corrected classification. Its first 124 bits exactly match the downloaded `FR_CMR_Obj` schema and carry camera quality, class, motion, ID, geometry, velocity, and acceleration. It is not corner radar. | +| `0x240–0x24F` + `0x270–0x277` | status + 15 × 24 B objects + 8 × 32 B scan blocks | Candidate right-front corner channel A | Strong two-sensor topology on older HDA2 Ioniq 5, Palisade 2023, and EV6 routes. A=right is route/video-inferred; tracked-object position and velocity are decoded. Compact scan semantics remain unresolved. | +| `0x278–0x28F` | status + 15 × 24 B objects + 8 × 32 B scan blocks | Candidate left-front corner channel B | Symmetric partner to channel A. B=left is route/video-inferred. The compact low field is only a distance candidate; property/auxiliary bits are unresolved. | +| `0x3D0–0x3D4` | 5 × 32 B, seven packed records per message | Front-radar auxiliary scan list | Optional companion to `3A5–3C4` on sampled EV9/Ioniq 9 routes. It has 32 usable record positions and shares the radar cycle counter, but synchronized testing falsified the former radial-speed and azimuth interpretations. The low 12 bits remain only a distance candidate. | +| `0x360–0x366` | 7 × 32 B | **Forward-camera lane/path** | Camera lane/path geometry, not radar. `362` contains lane confidence; `360/361/363/364` are active geometry, while `365/366` are often default-filled optional slots. | +| legacy `0x238–0x24F` or `0x238–0x255` | 24 or 30 × 8 B at about 32 Hz | **Candidate forward-camera perception** | All 13 checked legacy routes carry one of these exact variants. Several carry a simultaneous `500` front radar at a different cadence, arguing strongly against this being a second radar. The 30-message variant also carries synchronized `25A–25E`; exact fields remain unparsed. Do not confuse these 8-byte messages with the CAN-FD `235–248` camera layout or `240/270` corner layout. | +| CAN-FD `0x180–0x184`, `0x1B6–0x1B9`, `0x2BB–0x2BE` | 32 B at about 30 Hz | **Forward-camera auxiliary lists** | Downloaded Hyundai DBCs identify the transmitters/messages as camera traffic, and route payloads have camera-list cadence and slot behavior. Not a radar family. | +| `CCNC_0x162` | fixed lead/left/right/rear slots | Fused/cluster output | Stock ADAS/cluster output with unknown upstream fusion. Do not publish it as independent raw radar. | +| bus 9 `0x300/0x400/0x500/0x600` eight-address blocks | old prototype, eight subtracks per message | Four-corner prototype | Prior EV6 reverse-engineering mapped four blocks to FL/FR/RL/RR, but status and relative speed were unfinished and the family has not been reconfirmed in the current corpus. | -## Platform inventory +The confirmed `3A5–3C4` layout was checked across 21 compatible forum routes +and 817,852 checksum-valid frames. All 726 advertised segments from the 20 +unique `210–21F` routes were scanned: 16 routes had active targets, three had +a complete but empty stream, and one non-HDA2 Telluride route had no 210 +range. The 16 active routes split evenly between detailed and compact +dialects, and all 13,210,543 checked 210 frames passed the HKG checksum. -This is the investigation queue transcribed from the supplied route post and -augmented only where current branch evidence is explicit. "HDA2 candidate" -means to check both the `0x235–0x248` clue and the older four-block EV6 -topology; it does not claim that either is present. +The forum candidates are now classified at the family level. The legacy +`238–24F` and `238–255` variants are strong forward-camera candidates but +remain unparsed. The `180/1B6/2BB` groups are CAN-FD camera auxiliary lists. +The incomplete Ioniq PHEV `500–?` observation is a complete `500–53F` +64-slot front radar. None must be conflated with the CAN-FD `235` camera or +`240/270` corner layouts merely because their addresses overlap. -### Platforms with `0x3A5–0x3C4` +The `4E0/4E1` 2-bit counters matched in all 2,552 paired frames checked across +seven routes. On the three 64-slot routes, `4E3` matched the same counter in +all 1,133 paired frames. Every one of the 2,369 distinct `4E0` 16-bit +`SCAN_INDEX` steps incremented by one. In a fresh six-route full-segment pass, +decoded `4E0` vehicle speed correlated with `carState.vEgo` at +0.9976–0.99996, with 0.02–0.19 m/s mean absolute error. Both checked 64-slot +routes held `MAXIMUM_TRACKS_ACK=64`; all four 32-slot routes decoded that ESR +field as 1, confirming a different `4E1` dialect. The four sampled 32-slot +variants carried plausible BCD build timestamps in `4E2`; their remaining +`4E1` metadata stayed platform-static. All 7,347 nonzero `4E3` path IDs +referenced active one-based target slots. The 32-slot `4E4/4E5` payloads match +Hyundai-relocated ESR Status5/6 layouts: raw ADC health channels and alignment +status. Across 4,698 `4E5` frames, both factory-alignment fields remained +within the documented 0–3 enum subset; unexercised flags and physical ADC or +misalignment scales remain research-only. -| Platform | Front | Corner | Extras/fused | Rear | +Two other consecutive lookalikes are currently excluded from radar work: +downloaded Hyundai DBCs identify `630–63D` as head-unit/amplifier traffic, and +`5ED–5EF` occurs on routes without `500` tracks and does not follow the +otherwise strong `4E0`/`500` source association. + +### Outstanding forum platform queue + +| Platform | Remaining candidate | +| --- | --- | +| Hyundai Ioniq PHEV / Ioniq HEV 2022 / Palisade / Kia Niro PHEV 2022 | legacy camera candidate `238–24F`, plus its synchronized `201/20A/266/26D` companion groups | +| Hyundai Custin / Elantra 2021 / Elantra HEV 2021 / Sonata / Sonata Hybrid | extended legacy camera candidate `238–255`, plus `25A–25E` and the same companion groups | +| Kia Sorento / Sorento HEV 4th gen / Genesis GV70 | confirm semantics within the classified CAN-FD camera `180/1B6/2BB` auxiliary groups | +| Legacy `500` front-radar platforms | identify static 32-slot `4E1` metadata and calibrate/actively exercise the decoded `4E4/4E5` health and alignment fields | + +The forum also listed Acura MDX and Honda Clarity examples. They remain out of +this Hyundai/Kia/Genesis inventory because the Acura range was unrelated and +the Honda routes did not expose tracks. + +## Confirmed and possible vehicle combinations + +These are distinct source combinations supported by the corpus. “Possible” +describes protocol combinations, not a promise that every trim exposes them. + +| Combination | Front tracked objects | Corner source | Camera/auxiliary source | Known examples or status | | --- | --- | --- | --- | --- | -| Hyundai Ioniq 6 | `3A5–3C4` confirmed | HDA2 candidate | Unknown | Unknown | -| Hyundai Kona 2nd gen | `3A5–3C4` confirmed | Unknown | Check CCNC fused output | Unknown | -| Hyundai Kona EV 2nd gen | `3A5–3C4` confirmed | HDA2 candidate | `CCNC_0x162` observed on sampled CCNC routes | Unknown | -| Hyundai Kona HEV 2nd gen | `3A5–3C4` confirmed | Unknown | Check CCNC fused output | Unknown | -| Hyundai Santa Fe HEV 5th gen | `3A5–3C4` confirmed | Unknown | Check CCNC fused output | Unknown | -| Hyundai Sonata 2024 | `3A5–3C4` confirmed | Unknown | Check CCNC fused output | Unknown | -| Kia Carnival HEV 4th gen (2026) | `3A5–3C4` confirmed | Unknown | Unknown | Unknown | -| Kia EV6 2025 | `3A5–3C4` confirmed | HDA2 candidate | Check CCNC fused output | Unknown | -| Kia EV9 | `3A5–3C4` confirmed; rich extension | HDA2/corner candidate | Check `CCNC_0x162` and upstream inputs | Unknown | -| Kia K4 2025 | `3A5–3C4` confirmed | Check HDA2 routes | Check CCNC fused output | Unknown | -| Kia K5 2025 | `3A5–3C4` confirmed | Unknown | Check CCNC fused output | Unknown | -| Kia Niro EV 2nd gen | `3A5–3C4` confirmed | Check HDA2 routes | Unknown | Unknown | -| Kia Niro HEV 2nd gen | `3A5–3C4` confirmed | Unknown | Unknown | Unknown | -| Kia Sportage HEV 2026 | **Incompatible overlapping layout** | Unknown | Unknown | Unknown | -| Hyundai Ioniq 9 | `3A5–3C4` confirmed; rich extension | HDA2 candidate | Check `CCNC_0x162` and upstream inputs | Unknown | +| Legacy front only A | `500–51F` or `500–53F` | none observed | `4E0–4E5` radar companion/status | 32- and 64-slot platform variants | +| Legacy front only B | `602–617` | none observed | none established | Kona EV 2022, Ceed PHEV candidates | +| CAN-FD front only A | `210–21F` | none observed | none established | Elantra HEV 2024 and compact-dialect Tucson/Santa Cruz/Sportage routes | +| CAN-FD front + paired front corners | `210–21F` | inferred right A `240–24F` + `270–277`; inferred left B `278–28F` | none required | Older HDA2 Ioniq 5, Palisade 2023, and EV6 observations | +| CAN-FD front only B | `3A5–3C4` | none observed | camera objects/lanes may be absent or not logged | Compatible HDA1 routes | +| CAN-FD front + camera perception | `3A5–3C4` | none established | camera objects `235–248` and lanes `360–366` | Common newer HDA2 pattern; camera traffic is not another radar | +| Rich front + camera perception + auxiliary radar scan | rich `3A5–3C4` | none established | `235–248`, `360–366`, plus radar auxiliary records `3D0–3D4` | EV9 and Ioniq 9 sampled routes | +| Front + four-corner prototype | layout depends on the old EV6 capture | bus-9 FL/FR/RL/RR blocks | unknown | Historical prototype only; not current-production support | -### Platforms with `0x210–0x21F` +No current evidence proves a combination containing both the paired +`240/270` corner family and the newer `3D0` auxiliary scan family. No distinct +rear-radar tracked-object range has been confirmed in the current routes. +Rear objects shown by `CCNC_0x162` may be fused from other sensors and do not +prove a rear radar source. -| Platform | Front | Corner | Extras/fused | Rear | -| --- | --- | --- | --- | --- | -| Hyundai Elantra HEV 2024 | `210–21F` detailed confirmed | Unknown | Unknown | Unknown | -| Hyundai Ioniq 5 | `210–21F` detailed confirmed on two active routes; one route empty | HDA2 candidate | Unknown | Unknown | -| Hyundai Palisade 2023 | `210–21F` detailed confirmed on one active route; one route empty | HDA2 candidate | Unknown | Unknown | -| Kia EV6 | `210–21F` detailed confirmed on four active routes; one route empty | Four-block prototype candidate | Unknown | Prototype family may include rear corners | -| Kia K8 HEV 1st gen | Historical supplied route empty; outside the 20-route validation set | Unknown | Unknown | Unknown | -| Hyundai Santa Cruz 2025 | `210–21F` compact confirmed | Unknown | Unknown | Unknown | -| Hyundai Tucson 4th gen | `210–21F` compact confirmed on four routes | Unknown | Unknown | Unknown | -| Hyundai Tucson HEV 2025 | `210–21F` compact confirmed | Unknown | Unknown | Unknown | -| Kia Sportage 5th gen | `210–21F` compact confirmed on two routes | Unknown | Unknown | Unknown | +## Platform observations -### Platforms with legacy implemented layouts +| Platform group | Front | Additional observed traffic | +| --- | --- | --- | +| Ioniq 5 / Palisade 2023 / EV6 older HDA2 samples | detailed `210–21F` | paired `240/270–28F` front corners; A=right and B=left strongly inferred | +| Elantra HEV 2024 | detailed `210–21F` | no corner family established | +| Santa Cruz 2025 / Tucson family / Sportage 5th gen | compact `210–21F` | no corner family established in the checked samples | +| Ioniq 6 / Kona 2 / Kona EV 2 / Santa Fe 5 / Sonata 2024 / Carnival HEV / EV6 2025 / K4 / K5 / Niro 2 | common `3A5–3C4` where compatible | platform-dependent camera `235` and `360` traffic; no decoded corner family established | +| EV9 / Ioniq 9 | rich `3A5–3C4` | camera objects `235`, camera lanes `360`, optional front-radar auxiliary scan `3D0` | +| Sportage HEV 2026 forum sample | overlapping but incompatible `3A5` bytes | excluded from the common-layout decoder | -| Platform | Front | Corner | Extras/fused | Rear | -| --- | --- | --- | --- | --- | -| Hyundai Ioniq | `500–51F` implemented; verify | Unknown | Unknown | Unknown | -| Kia K7 2017 | `500–51F` implemented; verify | Unknown | Unknown | Unknown | -| Kia Niro EV | `500–51F` implemented; verify | Unknown | Unknown | Unknown | -| Hyundai Kona EV 2022 | `602–617` partially implemented; verify | Unknown | Unknown | Unknown | -| Kia Ceed PHEV | `602–617` partially implemented; verify | Unknown | Unknown | Unknown | +HDA2 changes where traffic is exposed, but raw bus number is not a reliable +layout discriminator. Follow sustained logical A-CAN and reject short +forwarded copies. -### Unimplemented front candidates +## What remains to prove -| Platform | Candidate front source | Corner | Extras/fused | Rear | -| --- | --- | --- | --- | --- | -| Hyundai Ioniq PHEV | `238–24F` | Unknown | Unknown | Unknown | -| Kia Niro PHEV 2022 | `238–24F` | Unknown | Unknown | Unknown | -| Hyundai Palisade | `238–24F` | Unknown | Unknown | Unknown | -| Hyundai Ioniq HEV 2022 | `238–24F` | Unknown | Unknown | Unknown | -| Hyundai Custin 1st gen | `238–255` | Unknown | Unknown | Unknown | -| Hyundai Elantra 2021 | `238–255` | Unknown | Unknown | Unknown | -| Hyundai Sonata | `238–255` | Unknown | Unknown | Unknown | -| Hyundai Sonata Hybrid | `238–255` | Unknown | Unknown | Unknown | -| Hyundai Elantra HEV 2021 | `238–255` | Unknown | Unknown | Unknown | -| Kia Sorento 4th gen | `180/1B6/2BB` groups; role unproved | Unknown | Unknown | Unknown | -| Kia Sorento HEV 4th gen | `180/1B6/2BB` groups; role unproved | Unknown | Unknown | Unknown | -| Genesis GV70 1st gen | `180/1B6/2BB` groups; role unproved | Unknown | Unknown | Unknown | -| Hyundai Ioniq PHEV 2019 | Incomplete `500–?` observation | Unknown | Unknown | Unknown | +1. Confirm the route/video-derived A=right and B=left assignment with a + controlled sensor occlusion or authoritative service mapping. +2. Finish both corner status messages and the remaining 24-byte object + fields: the stable 2-bit unknown category, persistent ID, classification, + dimensions, and sensor-health flags. The former acceleration candidate was + falsified and returned to raw bits. Position and longitudinal/lateral + velocity are resolved, and `0x278` byte 3 is the confirmed pre-truncation + channel-A object count: exported A slots equal `min(count, 15)`. + Preserve all remaining unknown bits until they correlate with the compact + scan bins. +3. Decode the 56 fixed-position `270–277` / `288–28F` scan candidates, + including the low 13-bit distance candidate, seven-bit property, and + nine-bit auxiliary fields. Direct same-cycle range association and a linear + record-index-to-angle map both failed time-shift controls, so do not project + these records as geometry until an independent mapping is found. +4. Decode the `3D0` auxiliary scan array without restoring the falsified + radial-speed or azimuth names. Determine whether the low 12 bits are + distance, identify the role and physical angular ordering of the 32 record + positions, and decode the four flag bits plus both unknown bytes. Two rich + routes confirm exact counter synchronization and the sentinel/zero empty + forms, but reject range-only, candidate-geometry, and direct slot-to-slot + associations with `3A5`. Production RadarData must continue to exclude it. +5. Decode the active polynomial/path fields in `360/361/363/364`. Treat + `365/366` constant payloads as unavailable lane slots, not missing radar. +6. Resolve `235–248` `UNKNOWN_1/2/3`, identify every checksum variant, and + validate native camera classification/motion values against video. Confirm + the camera-source assignment and forwarding path on each platform carrying + the range. +7. Search explicitly for the old four-corner topology and for a distinct rear + tracked-object source. Record verified absence when the correct buses and + maneuvers were covered. +8. Resolve the static 32-slot `4E1` metadata, physically calibrate the + protocol-derived `4E4` ADC channels, and capture alignment events to verify + the inactive `4E5` flags and misalignment scales. Exercise the currently + zero FCW path IDs and low `4E3` status flags on a 32-slot platform. Shared + `4E0` and path IDs are resolved; do not transfer unsupported 64-slot + Status2 fields onto the 32-slot payload. +9. Decode the legacy camera candidates `238–24F` and `238–255`, including + their `201/20A/25A/266/26D` companion groups. Determine whether `250–255` + and `25A–25E` are extra objects, path/lane data, or metadata, and correlate + classification and geometry against video. Keep these out of production + RadarData unless source evidence overturns the current camera assignment. +10. Complete the per-platform combination matrix across HDA1/HDA2 and + CCNC/non-CCNC cars. Record the sustained logical CAN source, forwarded + echoes, empty-but-present ranges, and verified absences rather than relying + on raw bus numbers. +11. Add representative replay fixtures and debugger/on-road UI tests for each + confirmed source combination. Promote candidate names or fields only after + multi-route and controlled-scene validation. -The post also included Acura MDX and Honda Clarity examples. They are excluded -from this Hyundai/Kia/Genesis roadmap: the Acura range was unrelated, and the -Honda routes showed no tracks. +## Useful work that does not require video -## Definition of done for one platform +1. Cluster every unknown field by platform, object lifecycle, motion class, + address/slot, and empty/default state. Stable cross-platform enums can be + separated from platform configuration bytes. +2. Use consecutive-cycle derivatives and ego-motion compensation to test + velocity, acceleration, yaw, and sensor-frame hypotheses. Always compare + against opposite-channel and time-shifted controls. +3. Search counter phase and bounded latency, not merely nearest timestamps, + when testing associations between tracked objects, compact scan arrays, + camera objects, and status messages. +4. Build transition matrices for the corner unknown category and remaining + flags. The first pass proved the 2-bit category is + stable within a continuous object but is not lifecycle or validity: all four + values occur on active targets, while empty slots use `LONG_DIST=204.7 m`. + Continue searching the remaining fields for birth, coast, replacement, and + deletion behavior without assuming the visual object class. +5. Compare the same physical field across detailed `210`, compact `210`, + `3A5`, corner objects, and legacy `500` tracks after normalizing coordinates + and lifecycle. Shared scaling should survive route and platform changes. +6. Mine downloaded and public DBC/source comments for transmitter identity and + bit boundaries, but require route evidence before adopting physical names or + scales. +7. Measure source dropouts, checksum/counter discontinuities, and status-bit + transitions together. This can identify communication health, blockage, + alignment, and reset fields without video. +8. Generate debugger capture instructions for later physical confirmation: + cover one corner sensor at a time, use a single stationary target at a + measured distance, and record straight approach/recede passes at known + speed. These tests can settle side assignment and physical scan fields with + much less ambiguity than unconstrained road scenes. -A platform row is complete only after the following are recorded: +## Implementation policy -- exact fingerprint, trim/architecture flags, route segments, and test scenes; -- source bus, address range, payload size, cadence, and counter behavior; -- whether duplicate copies on other buses are forwarding or independent - sensors; -- physical coverage: front, front corners, rear corners, or rear; -- track lifecycle and validity states; -- longitudinal/lateral position, relative velocities, acceleration, and - motion classification when available; -- behavior for moving, stationary, crossing, oncoming, overtaking, cut-in, - and curved-road targets; -- correlation with road/wide camera and, for fused outputs, with every known - raw source; -- explicit control-versus-visualization policy; -- DBC comments, parser tests, representative replay tests, and known - unavailable/default signal values. - -## Safety and implementation rules - -- A consecutive address range is not sufficient identification. Match the - complete range, bus, payload length, cadence, and decoded invariants. -- Do not merge two buses into two radars until byte/timestamp comparison rules - out forwarding. -- Do not use an unknown signal for filtering or control. -- Do not send fused cluster objects to driving as if they were independent raw - tracks. -- Keep all discovered sources available to `tools/radar/ui.py` for research, - even when the production radar interface deliberately filters what is sent - to openpilot. -- Record negative results. A verified absence is more useful than leaving a - platform perpetually marked unknown. +- Production RadarData contains the confirmed tracked-object layouts only. +- `235–248` remains available through RadarData as a camera-object source for + research, with explicit source labeling and its native object-ID validity. +- The corner and `3D0` generators are research DBCs. Their unresolved fields + are named unknown or candidate in comments and are not used for control. +- `4E0–4E5` status fields are present in the `RADAR_500_53F` DBC for research, + and inventoried by the desktop radar debugger, but are not required for + source detection or production track parsing. +- The on-road UI labels `235–248` as `CAM` and uses triangle markers. +- The desktop radar debugger plots confirmed production tracks, the + debugger-only `612–617` alternate objects, and the decoded `240/278` corner + objects. Its `SIGNALS` table exposes the compact `270/288` records, `3D0` + records, `4E0–4E5` status, `360–366` raw camera payloads, and all six + `CCNC_0x162` fused slots. Compact scans and `3D0` remain table-only and are + never projected as geometry. `0x162` is explicitly labeled fused and is not + published as an independent radar source. diff --git a/tools/radar/SIGNALS.md b/tools/radar/SIGNALS.md index 8c676aaf37..4eb7aadc07 100644 --- a/tools/radar/SIGNALS.md +++ b/tools/radar/SIGNALS.md @@ -1,10 +1,14 @@ -# Hyundai 3A5-3C4 and 210-21F radar signal notes +# Hyundai radar, camera-object, and auxiliary signal notes This document tracks the evidence behind the 24-byte `RADAR_3A5_3C4` layout and its shared signal semantics with the 32-byte `RADAR_210_21F` layout. -It deliberately separates a signal being active from its meaning being known. +It also records the now-identified `0x235–0x248` camera objects and the +research-only corner and raw-detection families. It deliberately separates a +signal being active from its meaning being known. The cross-family and per-platform investigation plan lives in [RADAR_RESEARCH.md](RADAR_RESEARCH.md). +The forum-ready platform/range summary lives in +[PLATFORM_MATRIX.md](PLATFORM_MATRIX.md). ## Status terms @@ -56,6 +60,109 @@ An optional field that is dead on one platform is not globally dead. ID and `0/1/2/3 = unknown/stationary/moving/stopped`; the HMVS4 object DBC independently defines value 3 as stopped, a 7-bit quality level, and alive age/lifetime. Neither DBC directly contains the 210 or 3A5 radar layout. +- The same archive's `FR_CMR_Obj` definition matches the first 124 bits of + `0x235–0x248` exactly. Together with its 33 Hz cadence and co-location with + `0x360–0x366`, this corrects the former corner-radar classification: + `0x235–0x248` is a forward-camera object list. +- The repository's Delphi `ESR.dbc` exactly matches the `0x500–0x53F` target + packing and the 64-slot radar's `0x4E0/0x4E1/0x4E3` status packing. A fresh + six-route full-segment check covered two 64-slot and four 32-slot variants. + `0x4E0` radar speed correlated with `carState.vEgo` at 0.9976–0.99996 with + 0.02–0.19 m/s mean absolute error. Both 64-slot variants acknowledged 64 + tracks in `0x4E1`; the four 32-slot payloads decoded as 1, proving that + their `0x4E1` uses a different dialect. + +## 500-53F legacy front-radar layout + +Every eight-byte message is one target slot. The lower 32 slots +`0x500–0x51F` are required; some radar variants add an identically packed +upper bank at `0x520–0x53F`. Inter-arrival timing on all six freshly checked +full segments is about 50 ms, confirming the established 20 Hz cadence. A gap +inside these logs makes frame count divided by total segment span misleading. + +| Signal | Start bit | Size | Scale / offset | Status | +| --- | ---: | ---: | --- | --- | +| `UNKNOWN_1` | 7 | 8 signed | raw | Preserve on 32-slot variants. Its low bits are active but do not behave like ESR target flags. | +| `ONCOMING_ESR` | 0 | 1 | boolean | 64-slot only. Every flagged target in both full-segment checks had negative ground-frame speed. Clear does not distinguish stationary from same-direction moving. | +| `GROUPING_CHANGED_ESR` | 1 | 1 | boolean | 64-slot ESR identity; exact tracker behavior remains to be correlated. | +| `REL_LAT_SPEED_ESR` | 7 | 6 signed | 0.25 / 0 m/s | 64-slot ESR lateral target rate. It is not published for 32-slot variants. | +| `AZIMUTH` | 12 | 10 signed | 0.1 / 0 deg | Corrected from the former 0.2-degree scale. Lateral projection now uses the angle directly rather than a compensating half-scale. | +| `STATE` | 15 | 3 | enum | Track lifecycle; values 3 and 4 are the measured/coasted valid states used by the parser. | +| `LONG_DIST` | 18 | 11 | 0.1 / 0 m | Target range. | +| `REL_ACCEL` | 33 | 10 signed | 0.02 / 0 m/s² | 32-slot dialect scale retained from route behavior. | +| `REL_ACCEL_ESR` | 33 | 10 signed | 0.05 / 0 m/s² | 64-slot ESR scale. Separate overlapping names prevent applying it to the 32-slot dialect. | +| `WIDTH_ESR` | 37 | 4 | 0.5 / 0 m | 64-slot ESR target width. It was populated on both checked 64-slot routes and identically zero on all four 32-slot routes. | +| `COUNTER` | 38 | 1 | 1 / 0 | Per-target rolling count. | +| `BRIDGE_OBJECT_ESR` | 39 | 1 | boolean | 64-slot ESR identity; exact tracker behavior remains to be correlated. | +| `REL_SPEED` | 53 | 14 signed | 0.01 / 0 m/s | Radial range rate. | +| `MED_RANGE_MODE_ESR` | 55 | 2 | enum | 64-slot ESR medium-range operating-mode field. | + +### 4E0-4E5 companion messages + +`0x4E0` has the shared ESR status layout on both 32- and 64-slot variants: + +| Signal | Start bit | Size | Scale / offset | +| --- | ---: | ---: | --- | +| `DSP_TIMESTAMP` | 5 | 7 | 2 / 0 ms | +| `GROUP_COUNTER` | 6 | 2 | 1 / 0 | +| `COMM_ERROR` | 14 | 1 | boolean | +| `RADIUS_CURVATURE` | 13 | 14 signed | 1 / 0 m | +| `SCAN_INDEX` | 31 | 16 | 1 / 0 | +| `YAW_RATE` | 47 | 12 signed | 0.0625 / 0 deg/s | +| `VEHICLE_SPEED` | 50 | 11 | 0.0625 / 0 m/s | + +The 64-slot variant exactly matches ESR Status2 at `0x4E1`. On 32-slot +variants only its low two-bit rolling counter is active; the other 62 bits +are platform-static and do not acknowledge 32 tracks, so the ESR Status2 +names remain 64-slot-only. The `0x4E3` in-path path-ID bytes are shared across +both variants, while its low status flags, counter, range mode, and alignment +angle are confirmed only on the 64-slot variant. + +| `0x4E1` 64-slot signal | Start bit | Size | Scale / offset | +| --- | ---: | ---: | --- | +| `GROUP_COUNTER` | 1 | 2 | 1 / 0 | +| `MAXIMUM_TRACKS_ACK` | 7 | 6 | 1 / 1 | +| `STEERING_ANGLE_ACK` | 10 | 11 | 1 / 0 deg | +| `RAW_DATA_MODE`, `TRANSCEIVER_OPERATIONAL`, `INTERNAL_ERROR`, `RANGE_PERFORMANCE_ERROR`, `OVERHEAT_ERROR` | 11–15 | 1 each | boolean | +| `TEMPERATURE` | 31 | 8 signed | 1 / 0 °C | +| `GROUPING_MODE` | 33 | 2 | 1 / 0 | +| `VEHICLE_SPEED_COMP_FACTOR` | 39 | 6 signed | 0.00195 / 1 | +| `YAW_RATE_BIAS` | 47 | 8 signed | 0.125 / 0 deg/s | +| `DSP_SOFTWARE_VERSION` | 55 | 16 | 1 / 0 | + +| `0x4E3` signal | Start bit | Size | Scale / offset | +| --- | ---: | ---: | --- | +| `GROUP_COUNTER` | 1 | 2 | 1 / 0; 64-slot confirmed | +| `MEDIUM_LONG_RANGE_MODE` | 3 | 2 | 1 / 0; 64-slot confirmed | +| `PARTIAL_BLOCKAGE`, `SIDELOBE_BLOCKAGE`, `LONG_RANGE_GRATING_LOBE_DETECTED`, `TRUCK_TARGET_DETECTED` | 4–7 | 1 each; 64-slot confirmed | +| `ACC_MOVING_PATH_ID` | 15 | 8 | one-based target slot; shared | +| `CMBB_MOVING_PATH_ID`, `CMBB_STATIONARY_PATH_ID` | 23, 31 | 8 each | one-based target slots; shared | +| `FCW_MOVING_PATH_ID`, `FCW_STATIONARY_PATH_ID` | 39, 47 | 8 each | protocol-defined; unexercised on sampled 32-slot routes | +| `AUTO_ALIGN_ANGLE` | 55 | 8 signed | 0.0625 / 0 deg; 64-slot confirmed | +| `ACC_STATIONARY_PATH_ID` | 63 | 8 | one-based target slot; shared | + +All 7,347 nonzero `0x4E3` path-ID observations across the four 32-slot routes +referenced an active `0x500–0x51F` target slot. IDs reached 32, proving +one-based indexing. ACC and CMBB moving selectors commonly referenced the +same slot, as did their stationary selectors. + +The 32-slot variants additionally send `0x4E2`, `0x4E4`, and `0x4E5`: + +- `0x4E2` consistently encodes plausible BCD build timestamps across all four + sampled platforms. +- `0x4E4` matches the byte order and address-shifted envelope of Delphi ESR + Status5: switched-battery, ignition, two temperature, and four supply ADC + channels. The identities are protocol-derived and the raw ADC values are + intentionally uncalibrated; they must not drive a health decision. +- `0x4E5` matches the similarly relocated ESR Status6 alignment payload. In + 4,698 checked frames its two factory-alignment fields used only documented + enum values 0–3. The other flags and alignment-value bytes remained zero, + so their identities are protocol-derived but not independently exercised. + +Although generic ESR defines eight-byte input messages at `0x4F0/0x4F1`, no +such messages were present on these source buses. The observed Hyundai +`0x4F1` was four bytes on other/forwarded buses and is not assigned the ESR +input schema. ## Signal matrix @@ -165,6 +272,163 @@ kinematics with persistence, hysteresis, and lane plausibility. but they cannot be reconstructed merely by reading the 3A5 rich extension. Rear tofus in particular need a source beyond the forward 3A5 radar. +## 235-248 forward-camera object layout + +Each 32-byte message is one camera-object slot. `OBJECT_ID=0` is unused in the +observed routes. The production parser normalizes the native camera motion +enum to the shared `unknown/stationary/moving` display enum, but preserves the +raw DBC value. + +| Signal | Start bit | Size | Scale / offset | Status | +| --- | ---: | ---: | --- | --- | +| `CHECKSUM` | 0 | 16 | 1 / 0 | Checksum field; platform variants exist, so the generator does not force the standard HKG algorithm. | +| `COUNTER` | 16 | 8 | 1 / 0 | Transmit-cycle counter. | +| `QUALITY` | 24 | 7 | 1 / 0 | Camera-object quality/reliability level. | +| `AGE` | 32 | 8 | 1 / 0 | Object alive age. | +| `MOTION_STATE` | 40 | 4 | 1 / 0 | Native camera motion class. | +| `OBJECT_ID` | 44 | 7 | 1 / 0 | Persistent object ID; zero is unused. | +| `WIDTH` | 52 | 7 | 0.05 / 0 m | Estimated width. | +| `CLASSIFICATION` | 60 | 3 | 1 / 0 | Unknown, truck, car, motorcycle, bicycle, pedestrian, undecided. | +| `LONG_DIST` | 64 | 13 | 0.05 / 0 m | Longitudinal relative position. | +| `LAT_DIST` | 78 | 12 | 0.05 / -102.4 m | Lateral relative position. | +| `REL_SPEED` | 91 | 12 | 0.05 / -100 m/s | Longitudinal relative velocity. | +| `REL_LAT_SPEED` | 104 | 10 | 0.05 / -25 m/s | Lateral relative velocity. | +| `REL_ACCEL` | 115 | 9 signed | 0.05 / 0 m/s² | Longitudinal relative acceleration. | +| `UNKNOWN_1` | 125 | 12 | raw | Active extension; semantics unknown. | +| `UNKNOWN_2` | 138 | 12 | raw | Active extension; semantics unknown. | +| `UNKNOWN_3` | 151 | 10 | raw | Active extension; semantics unknown. | +| `AZIMUTH` | 176 | 14 | 360/16384 / -180 deg | Route-validated high-resolution object azimuth. | + +Native motion values are: 0 undefined, 1 standing, 2 parked, 3 stopped, +4 unknown movable, 5 moving, 6 stopped oncoming, 7 unknown oncoming, +8 moving oncoming, and 9 crossing bicycle. This is the only newly decoded +family with explicit oncoming moving and stopped-oncoming values. + +## 240/270-28F candidate corner-radar layout + +The address sizes and symmetry form two complete sensor channels: + +| Channel | Inferred physical side | Status | Object slots | Compact scan records | +| --- | --- | --- | --- | --- | +| A | right front | `240`, 16 B | `241–24F`, 15 × 24 B | `270–277`, 8 × 32 B, seven records each | +| B | left front | `278`, 8 B | `279–287`, 15 × 24 B | `288–28F`, 8 × 32 B, seven records each | + +Both channels run at about 20 Hz and validate with the HKG CAN-FD checksum in +the sampled Ioniq 5, Palisade 2023, and EV6 data. All 288,021 checked frames +from five complete segment samples passed. A synchronized Palisade +parking-garage sweep strongly assigns A to the right-front sensor and B to +the left-front sensor: A alone retained close objects at large negative +lateral position, while B alone retained the corresponding positive-side +objects. This is strong route/video evidence, but a controlled sensor +occlusion remains the final physical confirmation. + +The 24-byte tracked-object record is: + +| Signal | Bit | Size | Scale / offset | Status | +| --- | ---: | ---: | --- | --- | +| `UNKNOWN_CATEGORY` | 24 | 2 | raw | Stable within continuous same-slot tracks and changes mainly when a spatially different target replaces the slot. All values 0–3 occur on active objects, so this is not lifecycle or validity. | +| `RCS` | 56 | 7 signed | 1 / 0 | Strong shared-core identification; raw unit uncalibrated. | +| `LONG_DIST` | 63 | 13 | 0.05 / 0 m | Confirmed vehicle-frame longitudinal position. | +| `LAT_DIST` | 76 | 11 signed | 0.05 / 0 m | Confirmed vehicle-frame lateral position. | +| `UNKNOWN_BIT_87` | 87 | 1 | raw | Active unknown. | +| `REL_SPEED` | 88 | 10 signed | 0.2 / 0 m/s | Confirmed longitudinal relative velocity. Four held-out Palisade samples exercised bit 97 independently of bit 96 at 351–357 m; consecutive distance changes matched approximately -52 m/s rather than the false positive speed produced by the former 9-bit decode. | +| `REL_LAT_SPEED` | 98 | 10 signed | 0.05 / 0 m/s | Confirmed lateral relative velocity. | +| `UNKNOWN_BITS_108_117` | 108 | 10 | raw | Former centered acceleration candidate. Its correlations with consecutive `REL_SPEED` derivatives were -0.445, -0.043, -0.009, -0.111, and -0.124 across five Ioniq 5, Palisade, and EV6 samples, so the physical name and scale were falsified. | + +Empty object slots use the exact `LONG_DIST=204.7 m` (`0xFFE`) sentinel; +`UNKNOWN_CATEGORY=0` must not be used as an empty test. This correction was +validated on five Ioniq 5, Palisade 2023, and EV6 samples. Including +category-zero objects produced longitudinal position-derivative correlations +of 0.949, 0.603, 0.962, 0.948, and 0.967 across those samples. + +The position decode was independently checked against simultaneous +`210–21F` front-radar targets. The 116 unambiguous A matches and 86 B matches +had longitudinal/lateral correlations of 0.999/0.991 and 0.999/0.993, +respectively, with about 0.3 m mean absolute error. Across 14,939 consecutive +same-slot updates, decoded longitudinal and lateral velocity correlated with +position derivatives at 0.964 and 0.944. No lifecycle field has been +identified, and the former acceleration candidate failed derivative checks. +The remaining header, classification, dimension, ID, and health fields stay +bit-preserved rather than borrowing names from the related front-radar core. + +The first payload byte after the counter in `0x278` is +`CHANNEL_A_OBJECT_COUNT`. Across 5,999 synchronized cycles on five Ioniq 5, +Palisade, and EV6 samples, the number of non-sentinel `0x241–0x24F` slots equaled +`min(CHANNEL_A_OBJECT_COUNT, 15)` exactly. It ranged from 0–22; values above +15 prove it is the pre-truncation internal candidate count rather than merely +the number of exported slots. The `0x240` status payload remains unresolved; +notably, it was entirely zero after checksum/counter on one Palisade and one +EV6 sample while being active on the other three routes. + +The compact arrays are not alternate encodings of that object count. Depending +on route and channel, 11–55 of their 56 bins were non-sentinel in a cycle, +versus at most 15 exported tracked objects; count correlations ranged from +weak to moderate and were never exact. + +The compact 32-bit scan record is: + +| Record field | Relative bit | Size | Scale | Status | +| --- | ---: | ---: | --- | --- | +| `DISTANCE_CANDIDATE` | 0 | 13 | 0.05 m | Range-like distribution and 400 m endpoint; no synchronized object association, so physical distance is not confirmed. | +| `RESERVED` | 13 | 3 | raw | Preserve. | +| `PROPERTY` | 16 | 7 | raw | Active categorical unknown; sampled values were quantized. | +| `AUX` | 23 | 9 | raw | Active unknown; velocity and split flag/state hypotheses were falsified. | + +Record 0 starts at message bit 24, message bit 56 is a frame-status byte, and +records 1–6 start at bits 64, 96, 128, 160, 192, and 224. Unused records +commonly contain `0x010D1F40`, with AUX variants such as `0x018D1F40`; its +low-field component is the 400 m endpoint. Contemporaneous range matching was no +better than the opposite channel or a 500 ms-shifted control. Each of the 56 +record positions instead has a characteristic occupancy/range profile, making +this a strong fixed scan/bin-array candidate rather than 56 freely assigned +tracked detections. A direct linear record-index-to-angle search on Ioniq 5 +and Palisade data also performed no better than a 500 ms-shifted control. +Decoding the low field, physical ordering, `PROPERTY`, and `AUX` remains open. + +## 3D0-3D4 front-radar auxiliary scan records + +This optional list accompanies `3A5–3C4` on sampled EV9 and Ioniq 9 routes. +Five 32-byte messages contain seven record positions each; the final three +positions of `3D4` are outside the 32-position list. The repeated +`0xC8782EE0` value and, on EV9, zero are unused records. All 12,090 checked +message frames passed the HKG CAN-FD checksum and carried the same cycle +counter as `3A5–3C4`. + +| Record field | Relative bit | Size | Scale / offset | Status | +| --- | ---: | ---: | --- | --- | +| `DISTANCE_CANDIDATE` | 0 | 12 | 0.05 / 0 m | Range-like distribution and endpoint, but no synchronized tracked-object match; not confirmed. | +| `FLAGS_UNKNOWN` | 12 | 4 | raw | Active values 0–2 in the sampled routes; semantics unknown. | +| `UNKNOWN_BYTE_16` | 16 | 8 | raw | Former radial-speed candidate was falsified. | +| `UNKNOWN_BYTE_24` | 24 | 8 | raw | Former azimuth candidate was falsified. | + +Packing positions match the corner scan records: record 0 at message bit 24, +one alignment byte at bit 56, then records 1–6 at bits 64 through 224. However, +the exact corner `13/3/7/9` field split is not transferred onto this +`12/4/8/8` layout without evidence. + +The stronger synchronized test falsified three tempting interpretations: + +- Range-only, candidate polar-geometry, and direct record-index-to-track-index + associations were no better than opposite or 500 ms-shifted controls. +- `UNKNOWN_BYTE_16` had essentially zero correlation with `3A5` longitudinal + or radial relative speed. +- `UNKNOWN_BYTE_24` had essentially zero correlation with `3A5` target angle. + +The 32 positions do show stable position-dependent occupancy and low-field +profiles, so a fixed scan/bin or intermediate radar-array role remains +plausible. They are not independent tracked objects, remain excluded from +production RadarData, and are shown only as raw records in the debugger's +`SIGNALS` table, never projected as geometry. + +## 360-366 forward-camera lane/path family + +These seven 32-byte messages belong to the forward camera. `362` already +contains left/right lane-line confidence. `360`, `361`, `363`, and `364` +carry changing lane/path polynomial-like geometry; their field boundaries are +not yet decoded. `365` and `366` are commonly constant/default-filled, +consistent with unavailable extra lane slots. Their presence or emptiness +must not be interpreted as a radar hardware combination. + ## Reserved/dead bits Bits `26`, `29`, `39`, `55`, `117`, `136-137`, `143`, `171`, and diff --git a/tools/radar/ui.py b/tools/radar/ui.py index af83753c09..9d37d50a6b 100755 --- a/tools/radar/ui.py +++ b/tools/radar/ui.py @@ -75,23 +75,62 @@ WHITE = rl.Color(255, 255, 255, 255) PURPLE = rl.Color(190, 125, 255, 255) SPEC_BY_RANGE = { - (spec.start_addr, spec.end_addr): spec + (spec.start_addr, end_addr): spec for spec in HYUNDAI_RADAR_TRACK_SPECS + for end_addr in {spec.required_end_addr, spec.end_addr} } SPEC_BY_NAME = {spec.name: spec for spec in HYUNDAI_RADAR_TRACK_SPECS} PREFERRED_RADAR_SOURCE = "RADAR_3A5_3C4" -DEFAULT_SOURCE_FILTERS = (False, True, True) # hide moving, stationary, unknown +DEFAULT_SOURCE_FILTERS = (False, False, False) # hide moving, stationary, unknown RadarSourceKey = tuple[int, int, int] RADAR_DETAIL_SIGNALS = { - "MOTION_STATE", "REL_LAT_SPEED", "ABS_SPEED", "WIDTH", "LENGTH", "ORIENTATION_ANGLE", - "AGE", "COAST_AGE", "STATE_ALT", "TRACK_COUNTER", + "RADAR_210_21F": { + f"{prefix}{signal}" + for prefix in ("1_", "2_") + for signal in ( + "MOTION_STATE", "TRACK_QUALITY", "AGE", "COAST_AGE", "STATE", "STATE_ALT", "RCS", + "REL_LAT_SPEED", "NEW_SIGNAL_4", "NEW_SIGNAL_18", "OBJECT_ID", + ) + }, + "RADAR_3A5_3C4": { + "MOTION_STATE", "TRACK_QUALITY", "AGE", "COAST_AGE", "STATE", "STATE_ALT", "RCS", + "REL_LAT_SPEED", "ABS_SPEED", "WIDTH", "LENGTH", "ORIENTATION_ANGLE", "TRACK_COUNTER", + "NEW_SIGNAL_4", "NEW_SIGNAL_5", "NEW_SIGNAL_12", "NEW_SIGNAL_13", "NEW_SIGNAL_14", + "NEW_SIGNAL_15", "NEW_SIGNAL_16", "NEW_SIGNAL_17", "NEW_SIGNAL_18", + }, } -TABLE_MODES = ("motion", "kinematics", "object") +TABLE_MODES = ("motion", "kinematics", "object", "signals") PLAYBACK_SPEEDS = (0.2, 0.5, 1.0, 2.0, 4.0, 8.0) TRACK_COUNT_FIELD_WIDTH = 3 SOURCE_CIRCLE_RADIUS_SCALE = 0.8 +@dataclass(frozen=True) +class ResearchSourceSpec: + name: str + start_address: int + end_address: int + message_sizes: tuple[int, ...] + details: str + + +RESEARCH_SOURCE_SPECS = ( + ResearchSourceSpec("CCNC_FUSED_OBJECTS", 0x162, 0x162, (32,), "fused cluster objects, not raw radar 32 B"), + ResearchSourceSpec("RADAR_500_STATUS_CORE", 0x4E0, 0x4E1, (8,) * 2, "front-radar status 20 Hz ESR/shared 4E0"), + ResearchSourceSpec("RADAR_500_BUILD", 0x4E2, 0x4E2, (8,), "optional build metadata 20 Hz"), + ResearchSourceSpec("RADAR_500_PATHS", 0x4E3, 0x4E3, (8,), "front-radar path selectors/status 20 Hz"), + ResearchSourceSpec("RADAR_500_HEALTH", 0x4E4, 0x4E5, (8,) * 2, "optional ADC/alignment status 20 Hz"), + ResearchSourceSpec("RADAR_602_ALT", 0x612, 0x617, (8,) * 6, "alternate legacy front objects 8 B"), + ResearchSourceSpec("CORNER_A_OBJECTS", 0x240, 0x24F, (16, *([24] * 15)), "right-front corner objects (inferred) 20 Hz"), + ResearchSourceSpec("CORNER_A_DETECTIONS", 0x270, 0x277, (32,) * 8, "right-front compact scan bins (inferred) 20 Hz"), + ResearchSourceSpec("CORNER_B_OBJECTS", 0x278, 0x287, (8, *([24] * 15)), "left-front corner objects (inferred) 20 Hz"), + ResearchSourceSpec("CORNER_B_DETECTIONS", 0x288, 0x28F, (32,) * 8, "left-front compact scan bins (inferred) 20 Hz"), + ResearchSourceSpec("RADAR_RAW_DETECTIONS", 0x3D0, 0x3D4, (32,) * 5, "front-radar auxiliary scan records 20 Hz 32 B"), + ResearchSourceSpec("CAMERA_LANE_PATH", 0x360, 0x366, (32,) * 7, "forward-camera lane/path geometry 32 B"), +) +RESEARCH_SPEC_BY_RANGE = {(spec.start_address, spec.end_address): spec for spec in RESEARCH_SOURCE_SPECS} + + @dataclass class ManagedReplay: process: subprocess.Popen @@ -401,6 +440,17 @@ class DisplayTrackSignals: state: int stateAlt: int trackCounter: int + signalSummary: str + + +@dataclass(frozen=True) +class DisplayRawSignal: + startAddress: int + endAddress: int + address: int + bus: int + label: str + signalSummary: str @dataclass(frozen=True) @@ -408,10 +458,305 @@ class RadarSnapshot: points: tuple[DisplayTrack, ...] = () trackSources: tuple[DisplaySource, ...] = () trackSignals: tuple[DisplayTrackSignals, ...] = () + rawSignals: tuple[DisplayRawSignal, ...] = () radarTracksAvailable: bool = False +class ResearchCanDecoder: + """Inventory auxiliary families without publishing them as control-facing radar tracks.""" + + def __init__(self): + self.payloads: dict[tuple[str, int], dict[int, bytes]] = {} + + def update(self, can_messages) -> None: + for address, dat, bus in can_messages: + for spec in RESEARCH_SOURCE_SPECS: + if not spec.start_address <= address <= spec.end_address: + continue + expected_size = spec.message_sizes[address - spec.start_address] + if len(dat) == expected_size: + self.payloads.setdefault((spec.name, int(bus)), {})[int(address)] = bytes(dat) + + @staticmethod + def _packed_detection_records(payload: bytes) -> tuple[int, ...]: + return tuple( + int.from_bytes(payload[offset:offset + 4], "little") + for offset in (3, 8, 12, 16, 20, 24, 28) + ) + + @staticmethod + def _signed(raw: int, size: int) -> int: + return raw - (1 << size) if raw & (1 << (size - 1)) else raw + + @classmethod + def _little(cls, raw: int, start: int, size: int, *, signed: bool = False) -> int: + value = (raw >> start) & ((1 << size) - 1) + return cls._signed(value, size) if signed else value + + @classmethod + def _big(cls, raw: int, start: int, size: int, *, signed: bool = False) -> int: + value = 0 + bit = start + for _ in range(size): + value = (value << 1) | ((raw >> bit) & 1) + bit = bit + 15 if bit % 8 == 0 else bit - 1 + return cls._signed(value, size) if signed else value + + @classmethod + def _corner_object(cls, payload: bytes) -> tuple[float, float, float] | None: + raw = int.from_bytes(payload, "little") + raw_dist = (raw >> 63) & 0x1FFF + if raw_dist == 0xFFE: + return None + + d_rel = raw_dist * 0.05 + # Exclude the high-range default/sentinel region and points outside the debugger plot. + if not 0 < d_rel <= MAX_FORWARD_DISTANCE: + return None + + y_rel = cls._signed((raw >> 76) & 0x7FF, 11) * 0.05 + v_rel = cls._signed((raw >> 88) & 0x3FF, 10) * 0.2 + return d_rel, y_rel, v_rel + + @classmethod + def _radar_500_status_summary(cls, address: int, raw: int) -> str: + if address == 0x4E0: + return " ".join(( + f"CTR:{cls._little(raw, 6, 2)}", + f"TS:{cls._big(raw, 5, 7) * 2}ms", + f"SCAN:{cls._big(raw, 31, 16)}", + f"SPD:{cls._big(raw, 50, 11) * 0.0625:.2f}m/s", + f"YAW:{cls._big(raw, 47, 12, signed=True) * 0.0625:.2f}deg/s", + f"CURV:{cls._big(raw, 13, 14, signed=True)}m", + f"COMM:{cls._big(raw, 14, 1)}", + )) + if address == 0x4E1: + return " ".join(( + f"CTR:{cls._big(raw, 1, 2)}", + f"MAX:{cls._big(raw, 7, 6) + 1}", + f"STEER:{cls._big(raw, 10, 11)}", + f"RAW:{cls._big(raw, 11, 1)}", + f"OP:{cls._big(raw, 12, 1)}", + f"ERR:{cls._big(raw, 13, 1)}/{cls._big(raw, 14, 1)}/{cls._big(raw, 15, 1)}", + f"TEMP:{cls._big(raw, 31, 8, signed=True)}C", + f"GROUP:{cls._big(raw, 33, 2)}", + f"VER:{cls._big(raw, 55, 16):04X}", + )) + if address == 0x4E2: + values = tuple(cls._little(raw, byte * 8, 8) for byte in range(8)) + return " ".join(( + f"BUILD:{values[0]:02X}-{values[1]:02X}-{values[2]:02X}", + f"{values[3]:02X}:{values[4]:02X}", + f"U5-7:{values[5]:02X}/{values[6]:02X}/{values[7]:02X}", + )) + if address == 0x4E3: + return " ".join(( + f"CTR:{cls._big(raw, 1, 2)}", + f"MODE:{cls._big(raw, 3, 2)}", + f"BLOCK:{cls._big(raw, 4, 1)}/{cls._big(raw, 5, 1)}/{cls._big(raw, 6, 1)}", + f"TRUCK:{cls._big(raw, 7, 1)}", + f"PATHS:{'/'.join(str(cls._big(raw, start, 8)) for start in (15, 23, 31, 39, 47, 63))}", + f"ALIGN:{cls._big(raw, 55, 8, signed=True) * 0.0625:.2f}deg", + )) + if address == 0x4E4: + names = ("BAT", "IGN", "T1", "T2", "5VA", "5VDX", "3V3", "10V") + return " ".join(f"{name}:{cls._little(raw, byte * 8, 8):02X}" for byte, name in enumerate(names)) + if address == 0x4E5: + return " ".join(( + f"1V8:{cls._little(raw, 0, 8):02X}", + f"N5V:{cls._little(raw, 8, 8):02X}", + f"WAVE:{cls._little(raw, 16, 8):02X}", + f"PWR:{cls._little(raw, 24, 3)}", + f"VUP:{cls._little(raw, 27, 1)}", + f"FACT:{cls._little(raw, 32, 3)}/{cls._little(raw, 35, 3)}", + f"FLAGS:{cls._little(raw, 38, 1)}/{cls._little(raw, 39, 1)}", + f"MIS:{cls._little(raw, 40, 8, signed=True)}/{cls._little(raw, 56, 8, signed=True)}", + f"UPDATES:{cls._little(raw, 48, 8)}", + )) + return f"RAW:{raw:016X}" + + def snapshot( + self, + ) -> tuple[ + tuple[DisplaySource, ...], + tuple[DisplayTrack, ...], + tuple[DisplayTrackSignals, ...], + tuple[DisplayRawSignal, ...], + dict[int, tuple[int, int]], + ]: + sources = [] + tracks = [] + track_signals = [] + raw_signals = [] + track_locations = {} + for spec in RESEARCH_SOURCE_SPECS: + for (name, bus), payloads in self.payloads.items(): + if name != spec.name or any(address not in payloads for address in range(spec.start_address, spec.end_address + 1)): + continue + + track_count = 0 + if spec.name == "RADAR_602_ALT": + for address in range(spec.start_address, spec.end_address + 1): + raw = int.from_bytes(payloads[address], "little") + d_rel = self._little(raw, 0, 10) * 0.25 + if not 0 < d_rel < 255.75: + continue + y_rel = self._little(raw, 10, 11) * 0.03 - 30.705 + v_rel = self._little(raw, 21, 10) * 0.25 - 128 + object_id = self._little(raw, 31, 9) + track_id = 800_000 + bus * 0x10000 + address + tracks.append(DisplayTrack( + trackId=track_id, + dRel=d_rel, + yRel=y_rel, + vRel=v_rel, + aRel=float("nan"), + canAddress=address, + canBus=bus, + )) + track_signals.append(DisplayTrackSignals( + trackId=track_id, + relLatSpeed=float("nan"), + absSpeed=float("nan"), + width=float("nan"), + length=float("nan"), + orientationAngle=float("nan"), + age=-1, + coastAge=-1, + state=-1, + stateAlt=-1, + trackCounter=-1, + signalSummary=" ".join(( + f"ID:{object_id}", + f"S1:{self._little(raw, 42, 8) - 128}", + f"S2:{self._little(raw, 50, 6) - 32}", + f"CTR:{self._little(raw, 56, 4)}", + f"CRC:{self._little(raw, 60, 4):X}", + )), + )) + track_locations[track_id] = (address, bus) + track_count += 1 + elif spec.name in ("CORNER_A_OBJECTS", "CORNER_B_OBJECTS"): + object_start = 0x241 if spec.name == "CORNER_A_OBJECTS" else 0x279 + for address in range(object_start, object_start + 15): + decoded = self._corner_object(payloads[address]) + if decoded is None: + continue + track_id = 900_000 + bus * 0x10000 + address + d_rel, y_rel, v_rel = decoded + tracks.append(DisplayTrack( + trackId=track_id, + dRel=d_rel, + yRel=y_rel, + vRel=v_rel, + aRel=float("nan"), + canAddress=address, + canBus=bus, + )) + raw = int.from_bytes(payloads[address], "little") + category = (raw >> 24) & 0x3 + rcs = self._signed((raw >> 56) & 0x7F, 7) + unknown_87 = (raw >> 87) & 0x1 + unknown_108 = (raw >> 108) & 0x3FF + unknown_118 = (raw >> 118) & 0x3FF + unknown_128 = (raw >> 128) & 0xFFFFFFFFFFFFFFFF + track_signals.append(DisplayTrackSignals( + trackId=track_id, + relLatSpeed=self._signed((raw >> 98) & 0x3FF, 10) * 0.05, + absSpeed=float("nan"), + width=float("nan"), + length=float("nan"), + orientationAngle=float("nan"), + age=-1, + coastAge=-1, + state=-1, + stateAlt=-1, + trackCounter=-1, + signalSummary=" ".join(( + f"CAT:{category}", f"RCS:{rcs}", f"U87:{unknown_87}", + f"U108:{unknown_108:03X}", f"U118:{unknown_118:03X}", f"U128:{unknown_128:016X}", + )), + )) + track_locations[track_id] = (address, bus) + track_count += 1 + elif spec.name == "CCNC_FUSED_OBJECTS": + raw = int.from_bytes(payloads[0x162], "little") + fused_slots = ( + ("FRONT", self._little(raw, 64, 5), self._little(raw, 69, 11) * 0.1, self._little(raw, 80, 7) * 0.1), + ("FRONT_ALT", self._little(raw, 88, 5), self._little(raw, 93, 11) * 0.1, self._little(raw, 104, 7) * 0.1), + ("LEFT", self._little(raw, 112, 5), self._little(raw, 117, 11) * 0.1, self._little(raw, 128, 7) * 0.1), + ("RIGHT", self._little(raw, 136, 5), self._little(raw, 141, 11) * 0.1, self._little(raw, 152, 7) * 0.1), + ("LEFT_REAR", self._big(raw, 167, 5), self._big(raw, 175, 8) * 0.1, self._big(raw, 182, 7) * 0.1), + ("RIGHT_REAR", self._big(raw, 196, 5), self._little(raw, 197, 8) * 0.1, self._little(raw, 205, 7) * 0.1), + ) + for label, status, distance, lateral in fused_slots: + raw_signals.append(DisplayRawSignal( + spec.start_address, spec.end_address, 0x162, bus, label, + f"STATUS:{status} DIST:{distance:.1f}m LAT:{lateral:.1f}m", + )) + track_count += status != 0 + raw_signals.append(DisplayRawSignal( + spec.start_address, spec.end_address, 0x162, bus, "FAULTS", + " ".join(( + f"FSS:{self._little(raw, 213, 3)}", f"FCA:{self._little(raw, 216, 3)}", + f"LSS:{self._little(raw, 219, 3)}", f"SLA:{self._little(raw, 222, 3)}", + f"DAW:{self._little(raw, 225, 3)}", f"HBA:{self._little(raw, 228, 3)}", + f"SCC:{self._little(raw, 231, 3)}", f"LFA:{self._little(raw, 234, 3)}", + )), + )) + elif spec.name in ("CORNER_A_DETECTIONS", "CORNER_B_DETECTIONS", "RADAR_RAW_DETECTIONS"): + for address in range(spec.start_address, spec.end_address + 1): + for record, raw in enumerate(self._packed_detection_records(payloads[address])): + if raw == 0: + continue + if spec.name in ("CORNER_A_DETECTIONS", "CORNER_B_DETECTIONS"): + if (raw & 0x1FFF) == 8000: + continue + raw_signals.append(DisplayRawSignal( + spec.start_address, spec.end_address, address, bus, f"REC{record}", + " ".join(( + f"DIST?:{(raw & 0x1FFF) * 0.05:.2f}", + f"RSV:{(raw >> 13) & 0x7}", + f"PROP:{(raw >> 16) & 0x7F}", + f"AUX:{(raw >> 23) & 0x1FF}", + f"RAW:{raw:08X}", + )), + )) + track_count += 1 + continue + if raw == 0xC8782EE0: + continue + raw_signals.append(DisplayRawSignal( + spec.start_address, spec.end_address, address, bus, f"REC{record}", + " ".join(( + f"DIST?:{(raw & 0xFFF) * 0.05:.2f}", + f"FLAGS:{(raw >> 12) & 0xF}", + f"U16:{(raw >> 16) & 0xFF:02X}", + f"U24:{(raw >> 24) & 0xFF:02X}", + f"RAW:{raw:08X}", + )), + )) + track_count += 1 + elif spec.name.startswith("RADAR_500_"): + for address in range(spec.start_address, spec.end_address + 1): + raw = int.from_bytes(payloads[address], "little") + raw_signals.append(DisplayRawSignal( + spec.start_address, spec.end_address, address, bus, f"{address:X}", + self._radar_500_status_summary(address, raw), + )) + elif spec.name == "CAMERA_LANE_PATH": + for address in range(spec.start_address, spec.end_address + 1): + payload = payloads[address] + raw_signals.append(DisplayRawSignal( + spec.start_address, spec.end_address, address, bus, f"{address:X}", + f"CTR:{payload[2]} DATA:{payload[3:].hex().upper()}", + )) + sources.append(DisplaySource(spec.start_address, spec.end_address, bus, track_count)) + return tuple(sources), tuple(tracks), tuple(track_signals), tuple(raw_signals), track_locations + + def make_radar_snapshot(radar_data, track_signals: tuple[DisplayTrackSignals, ...] = (), + raw_signals: tuple[DisplayRawSignal, ...] = (), track_locations: dict[int, tuple[int, int]] | None = None) -> RadarSnapshot: track_locations = track_locations or {} return RadarSnapshot( @@ -431,20 +776,26 @@ def make_radar_snapshot(radar_data, track_signals: tuple[DisplayTrackSignals, .. trackCount=int(source.trackCount), ) for source in radar_data.trackSources), trackSignals=track_signals, + rawSignals=raw_signals, radarTracksAvailable=bool(radar_data.radarTracksAvailable), ) def source_name(start_address: int, end_address: int) -> str: spec = SPEC_BY_RANGE.get((start_address, end_address)) - return spec.name if spec is not None else f"RADAR_{start_address:X}_{end_address:X}" + if spec is not None: + return "CAMERA_OBJECTS_235_248" if spec.source_kind == "camera" else spec.name + research_spec = RESEARCH_SPEC_BY_RANGE.get((start_address, end_address)) + return research_spec.name if research_spec is not None else f"RADAR_{start_address:X}_{end_address:X}" def source_details(start_address: int, end_address: int) -> str: spec = SPEC_BY_RANGE.get((start_address, end_address)) - if spec is None: - return "unknown format" - return f"{spec.frequency} Hz {spec.message_size} B" + if spec is not None: + prefix = "forward-camera objects " if spec.source_kind == "camera" else "" + return f"{prefix}{spec.frequency} Hz {spec.message_size} B" + research_spec = RESEARCH_SPEC_BY_RANGE.get((start_address, end_address)) + return research_spec.details if research_spec is not None else "unknown format" def radar_source_sort_key(source) -> tuple[bool, int, int, int]: @@ -463,6 +814,8 @@ def radar_source_key(source) -> RadarSourceKey: def source_filter_state(source_filters: dict[RadarSourceKey, tuple[bool, bool, bool]], source_key: RadarSourceKey) -> tuple[bool, bool, bool]: + if source_key[:2] == (0x3D0, 0x3D4): + return source_filters.get(source_key, (False, True, False)) return source_filters.get(source_key, DEFAULT_SOURCE_FILTERS) @@ -476,7 +829,7 @@ def toggle_source_filter(source_filters: dict[RadarSourceKey, tuple[bool, bool, def dbc_motion_class(motion_state: int | None) -> str: if motion_state is None: return "unknown" - return {0: "unknown (raw 0)", 1: "stationary", 2: "moving"}.get( + return {0: "unknown (raw 0)", 1: "stationary", 2: "moving", 3: "stopped", 4: "oncoming"}.get( motion_state, f"unknown (raw {motion_state})", ) @@ -518,7 +871,8 @@ def enable_dbc_detail_signals(radar_interface: RadarInterface, enhanced_parsers: """Add visualization-only signals to the branch parser without maintaining a second CAN decoder.""" for radar_parser in radar_interface.radar_parsers: parser_key = (radar_parser.spec.name, radar_parser.bus) - if parser_key in enhanced_parsers or radar_parser.spec.name != "RADAR_3A5_3C4": + detail_signals = RADAR_DETAIL_SIGNALS.get(radar_parser.spec.name) + if parser_key in enhanced_parsers or detail_signals is None: continue # This UI can intentionally run slower than replay and uses conflated CAN, so don't apply parser liveness checks here. @@ -527,7 +881,7 @@ def enable_dbc_detail_signals(radar_interface: RadarInterface, enhanced_parsers: radar_parser.spec.dbc_name, messages, radar_parser.bus, - signals={*radar_parser.spec.signals, *RADAR_DETAIL_SIGNALS}, + signals={*radar_parser.spec.signals, *detail_signals}, ) for message_state in parser.message_states.values(): message_state.ignore_alive = True @@ -535,6 +889,10 @@ def enable_dbc_detail_signals(radar_interface: RadarInterface, enhanced_parsers: enhanced_parsers.add(parser_key) +def dbc_signal_value(message, prefix: str, name: str, default=0): + return message.get(f"{prefix}{name}", default) + + def get_dbc_track_details( radar_interface: RadarInterface, ) -> tuple[dict[int, int], tuple[DisplayTrackSignals, ...], dict[int, tuple[int, int]]]: @@ -553,7 +911,7 @@ def get_dbc_track_details( active_bus = radar_interface.active_radar_buses.get(track_key[0]) locations[int(point.trackId)] = (can_address, active_bus if active_bus is not None else -1) - if track_key[0] != "RADAR_3A5_3C4": + if track_key[0] not in RADAR_DETAIL_SIGNALS: continue radar_parser = next((parser for parser in radar_interface.radar_parsers @@ -561,21 +919,45 @@ def get_dbc_track_details( if radar_parser is None: continue - message = radar_parser.parser.vl[f"RADAR_TRACK_{track_key[1]:x}"] - if "MOTION_STATE" in message: - states[int(point.trackId)] = int(message["MOTION_STATE"]) + prefix = f"{stored_address % 2 + 1}_" if spec.name == "RADAR_210_21F" else "" + message = radar_parser.parser.vl[f"RADAR_TRACK_{can_address:x}"] + motion_signal = f"{prefix}MOTION_STATE" + if motion_signal in message: + states[int(point.trackId)] = int(message[motion_signal]) + + if spec.name == "RADAR_210_21F": + signal_summary = " ".join(( + f"Q:{int(dbc_signal_value(message, prefix, 'TRACK_QUALITY'))}", + f"RCS:{int(dbc_signal_value(message, prefix, 'RCS'))}", + f"U4:{int(dbc_signal_value(message, prefix, 'NEW_SIGNAL_4'))}", + f"U18:{int(dbc_signal_value(message, prefix, 'NEW_SIGNAL_18'))}", + f"ID:{int(dbc_signal_value(message, prefix, 'OBJECT_ID'))}", + )) + else: + geometry = "/".join( + str(int(dbc_signal_value(message, prefix, f"NEW_SIGNAL_{index}"))) for index in range(12, 18) + ) + signal_summary = " ".join(( + f"Q:{int(dbc_signal_value(message, prefix, 'TRACK_QUALITY'))}", + f"RCS:{int(dbc_signal_value(message, prefix, 'RCS'))}", + f"U4:{int(dbc_signal_value(message, prefix, 'NEW_SIGNAL_4'))}", + f"U5:{int(dbc_signal_value(message, prefix, 'NEW_SIGNAL_5'))}", + f"U18:{int(dbc_signal_value(message, prefix, 'NEW_SIGNAL_18'))}", + f"G12-17:{geometry}", + )) details.append(DisplayTrackSignals( trackId=int(point.trackId), - relLatSpeed=float(message["REL_LAT_SPEED"]), - absSpeed=float(message["ABS_SPEED"]), - width=float(message["WIDTH"]), - length=float(message["LENGTH"]), - orientationAngle=float(message["ORIENTATION_ANGLE"]), - age=int(message["AGE"]), - coastAge=int(message["COAST_AGE"]), - state=int(message["STATE"]), - stateAlt=int(message["STATE_ALT"]), - trackCounter=int(message["TRACK_COUNTER"]), + relLatSpeed=float(dbc_signal_value(message, prefix, "REL_LAT_SPEED", float("nan"))), + absSpeed=float(dbc_signal_value(message, prefix, "ABS_SPEED", float("nan"))), + width=float(dbc_signal_value(message, prefix, "WIDTH", float("nan"))), + length=float(dbc_signal_value(message, prefix, "LENGTH", float("nan"))), + orientationAngle=float(dbc_signal_value(message, prefix, "ORIENTATION_ANGLE", float("nan"))), + age=int(dbc_signal_value(message, prefix, "AGE", -1)), + coastAge=int(dbc_signal_value(message, prefix, "COAST_AGE", -1)), + state=int(dbc_signal_value(message, prefix, "STATE", -1)), + stateAlt=int(dbc_signal_value(message, prefix, "STATE_ALT", -1)), + trackCounter=int(dbc_signal_value(message, prefix, "TRACK_COUNTER", -1)), + signalSummary=signal_summary, )) return states, tuple(details), locations @@ -620,8 +1002,10 @@ def radar_decoder_worker(addr: str, output_queue) -> None: radar_cp.flags = 0 radar_cp_sp = structs.CarParamsSP(flags=HyundaiFlagsSP.RADAR_FULL_RADAR.value) radar_interface = RadarInterface(radar_cp, radar_cp_sp) + research_decoder = ResearchCanDecoder() enhanced_parsers: set[tuple[str, int]] = set() - snapshot = RadarSnapshot() + radar_snapshot = RadarSnapshot() + snapshot = radar_snapshot motion_states: dict[int, int] = {} last_publish_time = 0.0 last_can_message_time = 0.0 @@ -632,11 +1016,20 @@ def radar_decoder_worker(addr: str, output_queue) -> None: if sm.updated["can"]: last_can_message_time = time.monotonic() can_messages = [(message.address, bytes(message.dat), message.src) for message in sm["can"]] + research_decoder.update(can_messages) radar_data = radar_interface.update([(sm.logMonoTime["can"], can_messages)]) enable_dbc_detail_signals(radar_interface, enhanced_parsers) if radar_data is not None: motion_states, track_signals, track_locations = get_dbc_track_details(radar_interface) - snapshot = make_radar_snapshot(radar_data, track_signals, track_locations) + radar_snapshot = make_radar_snapshot(radar_data, track_signals, track_locations=track_locations) + research_sources, research_tracks, research_signals, raw_signals, _ = research_decoder.snapshot() + snapshot = RadarSnapshot( + points=(*radar_snapshot.points, *research_tracks), + trackSources=(*radar_snapshot.trackSources, *research_sources), + trackSignals=(*radar_snapshot.trackSignals, *research_signals), + rawSignals=raw_signals, + radarTracksAvailable=radar_snapshot.radarTracksAvailable, + ) now = time.monotonic() if radar_data is not None or now - last_publish_time >= 0.1: @@ -1092,11 +1485,54 @@ def dbc_track_state(state: int) -> str: def draw_track_table(font, rect: rl.Rectangle, tracks, motion_states: dict[int, int], scroll: int, selected_id: int | None, hovered_id: int | None, track_signals: dict[int, DisplayTrackSignals], track_locations: dict[int, tuple[int, int]], - table_mode: str) -> None: + table_mode: str, raw_signals: tuple[DisplayRawSignal, ...] = ()) -> None: rl.draw_rectangle_rec(rect, BACKGROUND) rl.draw_line_ex(rl.Vector2(rect.x, rect.y), rl.Vector2(rect.x + rect.width, rect.y), 2.0, GRID) sorted_tracks = sorted(tracks, key=lambda track: (track.dRel, track.trackId)) capacity = table_capacity(rect) + + if table_mode == "signals": + rows = [ + ( + "{:X}/B{}".format(*track_locations.get(int(track.trackId), (-1, -1))), + f"TRACK {track.trackId}", + track_signals[int(track.trackId)].signalSummary, + int(track.trackId), + ) + for track in sorted_tracks + if int(track.trackId) in track_signals + ] + rows.extend( + ( + f"{signal.address:X}/B{signal.bus}", + f"{source_name(signal.startAddress, signal.endAddress).removeprefix('RADAR_')} {signal.label}", + signal.signalSummary, + None, + ) + for signal in sorted(raw_signals, key=lambda item: ( + item.startAddress, item.bus, item.address, item.label, + )) + ) + visible_rows = rows[scroll:scroll + capacity] + draw_text(font, "SIGNALS", rect.x + 12, rect.y + 8, 21, TEXT) + if rows: + draw_text(font, f"{scroll + 1}-{scroll + len(visible_rows)} / {len(rows)}", + rect.x + rect.width - 112, rect.y + 11, 16, MUTED) + columns = (("CAN", 0.02), ("SOURCE / ITEM", 0.12), ("DECODED / RAW SIGNALS", 0.36)) + header_y = rect.y + 36 + for title, offset in columns: + draw_text(font, title, rect.x + rect.width * offset, header_y, 15, MUTED) + for row, (can_location, label, summary, track_id) in enumerate(visible_rows): + y = header_y + 23 + row * 24 + if track_id == selected_id: + rl.draw_rectangle_rec(rl.Rectangle(rect.x, y - 3, rect.width, 24), rl.Color(CYAN.r, CYAN.g, CYAN.b, 42)) + elif row % 2: + rl.draw_rectangle_rec(rl.Rectangle(rect.x, y - 3, rect.width, 24), rl.Color(255, 255, 255, 8)) + draw_text(font, can_location, rect.x + rect.width * 0.02, y, 15, TEXT) + draw_text(font, label, rect.x + rect.width * 0.12, y, 15, CYAN if track_id is None else TEXT) + draw_text(font, summary, rect.x + rect.width * 0.36, y, 15, TEXT) + return + visible = sorted_tracks[scroll:scroll + capacity] draw_text(font, f"TRACK {table_mode.upper()}", rect.x + 12, rect.y + 8, 21, TEXT) @@ -1143,21 +1579,29 @@ def draw_track_table(font, rect: rl.Rectangle, tracks, motion_states: dict[int, (f"{track.dRel:.1f}", 0.19, TEXT), (f"{track.yRel:+.1f}", 0.29, TEXT), (f"{track.vRel:+.1f}", 0.39, TEXT), - (f"{detail.relLatSpeed:+.1f}" if detail else "n/a", 0.50, TEXT if detail else MUTED), + (f"{detail.relLatSpeed:+.1f}" if detail and math.isfinite(detail.relLatSpeed) else "n/a", + 0.50, TEXT if detail and math.isfinite(detail.relLatSpeed) else MUTED), (f"{track.aRel:+.1f}" if math.isfinite(track.aRel) else "n/a", 0.61, TEXT), - (f"{detail.absSpeed:.1f}" if detail else "n/a", 0.73, TEXT if detail else MUTED), - (str(detail.age) if detail else "n/a", 0.88, TEXT if detail else MUTED), + (f"{detail.absSpeed:.1f}" if detail and math.isfinite(detail.absSpeed) else "n/a", + 0.73, TEXT if detail and math.isfinite(detail.absSpeed) else MUTED), + (str(detail.age) if detail and detail.age >= 0 else "n/a", 0.88, TEXT if detail and detail.age >= 0 else MUTED), ) elif table_mode == "object": values = ( (str(track.trackId), 0.02, TEXT), (can_location, 0.09, TEXT if can_address >= 0 else MUTED), - (f"{detail.width:.1f}" if detail else "n/a", 0.21, TEXT if detail else MUTED), - (f"{detail.length:.1f}" if detail else "n/a", 0.33, TEXT if detail else MUTED), - (f"{detail.orientationAngle:+.0f}" if detail else "n/a", 0.45, TEXT if detail else MUTED), - (dbc_track_state(detail.state) if detail else "n/a", 0.57, TEXT if detail else MUTED), - (str(detail.coastAge) if detail else "n/a", 0.74, TEXT if detail else MUTED), - (str(detail.trackCounter) if detail else "n/a", 0.87, TEXT if detail else MUTED), + (f"{detail.width:.1f}" if detail and math.isfinite(detail.width) else "n/a", + 0.21, TEXT if detail and math.isfinite(detail.width) else MUTED), + (f"{detail.length:.1f}" if detail and math.isfinite(detail.length) else "n/a", + 0.33, TEXT if detail and math.isfinite(detail.length) else MUTED), + (f"{detail.orientationAngle:+.0f}" if detail and math.isfinite(detail.orientationAngle) else "n/a", + 0.45, TEXT if detail and math.isfinite(detail.orientationAngle) else MUTED), + (dbc_track_state(detail.state) if detail and detail.state >= 0 else "n/a", + 0.57, TEXT if detail and detail.state >= 0 else MUTED), + (str(detail.coastAge) if detail and detail.coastAge >= 0 else "n/a", + 0.74, TEXT if detail and detail.coastAge >= 0 else MUTED), + (str(detail.trackCounter) if detail and detail.trackCounter >= 0 else "n/a", + 0.87, TEXT if detail and detail.trackCounter >= 0 else MUTED), ) else: values = ( @@ -1542,8 +1986,8 @@ def draw_source_status(font, rect: rl.Rectangle, live_tracks, valid: bool, alive spec = SPEC_BY_RANGE.get((source.startAddress, source.endAddress)) compact_name = source_name(source.startAddress, source.endAddress).removeprefix("RADAR_") source_parts = [ - (compact_name, f"Radar CAN address range 0x{source.startAddress:X}-0x{source.endAddress:X}"), - (f"B{source.bus}", f"CAN bus {source.bus} carrying this radar source"), + (compact_name, f"CAN address range 0x{source.startAddress:X}-0x{source.endAddress:X}"), + (f"B{source.bus}", f"CAN bus {source.bus} carrying this source"), ] if spec is not None: source_parts.extend(( @@ -1551,7 +1995,8 @@ def draw_source_status(font, rect: rl.Rectangle, live_tracks, valid: bool, alive (f"{spec.message_size} B", "Radar CAN message payload size"), )) else: - source_parts.append(("unknown format", "No supported radar format matches this address range")) + details = source_details(source.startAddress, source.endAddress) + source_parts.append((details, details)) source_hovered_tooltip = draw_status_parts( font, tuple(source_parts), source_x, row_y, 16, CYAN, mouse_position, " / ", ) @@ -2043,6 +2488,7 @@ def ui_thread(addr: str, start_wide: bool = False, start_fused: bool = False, st if use_can_source: selected_tracks = can_tracks tracks = decoded_tracks + raw_signals = can_tracks.rawSignals data_valid = can_data_seen data_alive = can_data_alive motion_states = decoded_motion_states @@ -2054,6 +2500,7 @@ def ui_thread(addr: str, start_wide: bool = False, start_fused: bool = False, st data_valid = live_data_seen data_alive = sm.alive["liveTracks"] tracks = list(selected_tracks.points) if data_valid else [] + raw_signals = () motion_states = match_decoded_track_values(tracks, decoded_tracks, decoded_motion_states) track_signals = match_decoded_track_values(tracks, decoded_tracks, decoded_track_signals) track_locations = match_decoded_track_values(tracks, decoded_tracks, decoded_track_locations) @@ -2069,12 +2516,18 @@ def ui_thread(addr: str, start_wide: bool = False, start_fused: bool = False, st tracks = filter_tracks( tracks, motion_states, track_locations, selected_tracks.trackSources, source_filters, ) + current_table_mode = TABLE_MODES[table_mode_index] capacity = table_capacity(table_rect) - max_scroll = max(0, len(tracks) - capacity) + table_row_count = ( + sum(int(track.trackId) in track_signals for track in tracks) + len(raw_signals) + if current_table_mode == "signals" + else len(tracks) + ) + max_scroll = max(0, table_row_count - capacity) if rl.check_collision_point_rec(rl.get_mouse_position(), table_rect): table_scroll -= int(rl.get_mouse_wheel_move() * 3) table_scroll = int(np.clip(table_scroll, 0, max_scroll)) - table_hover_id = None if composite_mode else table_hovered_track_id( + table_hover_id = None if composite_mode or current_table_mode == "signals" else table_hovered_track_id( tracks, table_rect, table_scroll, rl.get_mouse_position(), ) if (table_hover_id is not None @@ -2183,7 +2636,7 @@ def ui_thread(addr: str, start_wide: bool = False, start_fused: bool = False, st current_hovered_id = camera_hovered_id if camera_hovered_id is not None else top_down_hovered_id selected_track_id = retain_selected_track_id(selected_track_id, current_hovered_id, tracks) - if selected_track_id is not None: + if selected_track_id is not None and current_table_mode != "signals": sorted_tracks = sorted(tracks, key=lambda track: (track.dRel, track.trackId)) hovered_row = next((index for index, track in enumerate(sorted_tracks) if int(track.trackId) == selected_track_id), None) @@ -2223,7 +2676,7 @@ def ui_thread(addr: str, start_wide: bool = False, start_fused: bool = False, st ) table_mode = TABLE_MODES[table_mode_index] draw_track_table(font, table_rect, tracks, motion_states, table_scroll, selected_track_id, table_hover_id, - track_signals, track_locations, table_mode) + track_signals, track_locations, table_mode, raw_signals) clicked_action = draw_source_status( font, status_rect, selected_tracks, data_valid, data_alive, replay_process is not None or can_data_seen or live_data_seen, show_labels, data_source,