more dbc updates

This commit is contained in:
royjr
2026-07-19 19:24:38 -04:00
parent e5948ff80c
commit 28aa8cefc8
7 changed files with 1106 additions and 262 deletions
+31 -4
View File
@@ -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
@@ -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]
+62
View File
@@ -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 | `0x5000x53F` | 64 slots, 8 B each, about 20 Hz | `0x4E0`, `0x4E1`, `0x4E3` status/path messages | Legacy `0x2380x24F` may coexist on applicable cars | **Confirmed** 64-slot legacy/Delphi-ESR dialect. |
| Kia Niro EV first generation; legacy Hyundai Palisade; Sonata; Sonata Hybrid | `0x5000x51F` | 32 slots, 8 B each, about 20 Hz | `0x4E00x4E5` status/build/health/alignment family | Legacy `0x2380x255` or related camera candidates may coexist by platform | **Confirmed** 32-slot legacy dialect. The upper `0x5200x53F` bank is absent. |
| Hyundai Kona EV 2022; Kia Ceed PHEV | `0x6020x617` | Two packed tracks in implemented messages | None established | None established | **Observed/candidate** legacy front family; the relationship between `0x6020x611` and `0x6120x617` remains incomplete. |
| Hyundai Elantra HEV 2024 | `0x2100x21F` | 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 | `0x2100x21F` | Detailed dialect, 32 tracks | Right-front candidate: `0x2400x24F` objects plus `0x2700x277` compact records. Left-front candidate: `0x2780x287` objects plus `0x2880x28F` 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 | `0x2100x21F` | 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 | `0x3A50x3C4` | Common 32-track layout, 24 B each, 20 Hz | No independent corner range established | `0x2350x248` camera objects and `0x3600x366` 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 | `0x3A50x3C4` | 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 | `0x3A50x3C4` | 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 | `0x3A50x3C4` | Rich 32-track layout with optional width, length, absolute speed, and orientation | `0x3D00x3D4` synchronized 32-position auxiliary scan array | `0x2350x248` camera objects; `0x3600x366` 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 `0x3A50x3C4` 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 |
| --- | --- | --- |
| `0x2350x248`, 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. |
| `0x3600x366`, 32 B | Newer CAN-FD platforms carrying camera lane/path output | **Not radar:** forward-camera lane/path family. |
| `0x1800x184`, `0x1B60x1B9`, `0x2BB0x2BE` | Sampled Kia Sorento/Sorento HEV and Genesis GV70 candidates | **Not radar:** forward-camera auxiliary lists according to downloaded Hyundai DBCs. |
| `0x2380x24F`, 8 B | Ioniq PHEV/HEV 2022, legacy Palisade, Niro PHEV candidates | **Not established as radar:** strong legacy forward-camera-perception candidate. |
| `0x2380x255`, 8 B, often with `0x25A0x25E` | 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 | `0x5000x51F` | `0x4E00x4E5` companion/status |
| Legacy front, 64 slots | `0x5000x53F` | `0x4E0`, `0x4E1`, `0x4E3` companion/status |
| Legacy alternate front | `0x6020x617` | None established |
| CAN-FD detailed front only | `0x2100x21F` | None established |
| CAN-FD detailed front plus paired front corners | `0x2100x21F` | A/right candidate `0x240/0x270` families plus B/left candidate `0x278/0x288` families |
| CAN-FD compact front only | `0x2100x21F` | None established |
| CAN-FD common front | `0x3A50x3C4` | Camera/fused families may coexist but are not independent radar tracks |
| CAN-FD rich front plus auxiliary scan | `0x3A50x3C4` | Optional `0x3D00x3D4` auxiliary radar array |
No checked route proves a combination containing both the paired
`0x240/0x2700x28F` corner family and `0x3D00x3D4`. 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.
+207 -210
View File
@@ -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. `0x2380x24F`
2. `0x2380x255`
3. the combined `0x1800x184`, `0x1B60x1B9`, and `0x2BB0x2BF` 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 `0x2350x248` 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: `0x3000x307`,
`0x4000x407`, `0x5000x507`, and `0x6000x607`. 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 `0x2350x248` 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 `3A53C4` 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 |
| --- | --- | --- | --- |
| `0x3A50x3C4` | 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. |
| `0x2100x21F` | 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. |
| `0x5000x51F` | 32 × 8-byte messages at 20 Hz | Likely front | Implemented. Confirm special behavior around `0x501/0x502` and validate lateral conversion from azimuth. |
| `0x6020x617` | Generator has double-track `0x6020x611` and alternate single-track `0x6120x617` formats | Likely front | Partially implemented: runtime currently consumes only `0x6020x611`. Confirm whether `0x6120x617` is the same objects, an alternate view, or a separate output before expanding it. |
| `0x2350x248` | 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. |
| `0x2380x24F` | Not implemented | Front candidate | Reverse layout and confirm physical role on the four listed platforms. It overlaps `0x2350x248`, so detection must use the complete range, size, and layout rather than address membership alone. |
| `0x2380x255` | Not implemented | Front candidate | Reverse layout and distinguish it from both `0x2380x24F` and `0x2350x248`. |
| `0x1800x184`, `0x1B60x1B9`, `0x2BB0x2BF` | 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 `0x5000x51F` 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. |
| `0x5000x51F` or `0x5000x53F` | 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². |
| `0x4E00x4E5` | 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. |
| `0x6020x617` | 8 B, two packed tracks in the implemented messages | Front radar | Implemented legacy front layout. The relationship between `602611` and the alternate `612617` definitions remains incomplete. |
| `0x2100x21F` | 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. |
| `0x3A50x3C4` | 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. |
| `0x2350x248` | 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. |
| `0x2400x24F` + `0x2700x277` | 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. |
| `0x2780x28F` | 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. |
| `0x3D00x3D4` | 5 × 32 B, seven packed records per message | Front-radar auxiliary scan list | Optional companion to `3A53C4` 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. |
| `0x3600x366` | 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 `0x2380x24F` or `0x2380x255` | 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 `25A25E`; exact fields remain unparsed. Do not confuse these 8-byte messages with the CAN-FD `235248` camera layout or `240/270` corner layout. |
| CAN-FD `0x1800x184`, `0x1B60x1B9`, `0x2BB0x2BE` | 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 `3A53C4` layout was checked across 21 compatible forum routes
and 817,852 checksum-valid frames. All 726 advertised segments from the 20
unique `21021F` 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 `0x2350x248` 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
`23824F` and `238255` 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 `50053F`
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 `0x3A50x3C4`
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.99760.99996, with 0.020.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 03 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 `63063D` as head-unit/amplifier traffic, and
`5ED5EF` 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 `23824F`, plus its synchronized `201/20A/266/26D` companion groups |
| Hyundai Custin / Elantra 2021 / Elantra HEV 2021 / Sonata / Sonata Hybrid | extended legacy camera candidate `238255`, plus `25A25E` 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 | `3A53C4` confirmed | HDA2 candidate | Unknown | Unknown |
| Hyundai Kona 2nd gen | `3A53C4` confirmed | Unknown | Check CCNC fused output | Unknown |
| Hyundai Kona EV 2nd gen | `3A53C4` confirmed | HDA2 candidate | `CCNC_0x162` observed on sampled CCNC routes | Unknown |
| Hyundai Kona HEV 2nd gen | `3A53C4` confirmed | Unknown | Check CCNC fused output | Unknown |
| Hyundai Santa Fe HEV 5th gen | `3A53C4` confirmed | Unknown | Check CCNC fused output | Unknown |
| Hyundai Sonata 2024 | `3A53C4` confirmed | Unknown | Check CCNC fused output | Unknown |
| Kia Carnival HEV 4th gen (2026) | `3A53C4` confirmed | Unknown | Unknown | Unknown |
| Kia EV6 2025 | `3A53C4` confirmed | HDA2 candidate | Check CCNC fused output | Unknown |
| Kia EV9 | `3A53C4` confirmed; rich extension | HDA2/corner candidate | Check `CCNC_0x162` and upstream inputs | Unknown |
| Kia K4 2025 | `3A53C4` confirmed | Check HDA2 routes | Check CCNC fused output | Unknown |
| Kia K5 2025 | `3A53C4` confirmed | Unknown | Check CCNC fused output | Unknown |
| Kia Niro EV 2nd gen | `3A53C4` confirmed | Check HDA2 routes | Unknown | Unknown |
| Kia Niro HEV 2nd gen | `3A53C4` confirmed | Unknown | Unknown | Unknown |
| Kia Sportage HEV 2026 | **Incompatible overlapping layout** | Unknown | Unknown | Unknown |
| Hyundai Ioniq 9 | `3A53C4` confirmed; rich extension | HDA2 candidate | Check `CCNC_0x162` and upstream inputs | Unknown |
| Legacy front only A | `50051F` or `50053F` | none observed | `4E04E5` radar companion/status | 32- and 64-slot platform variants |
| Legacy front only B | `602617` | none observed | none established | Kona EV 2022, Ceed PHEV candidates |
| CAN-FD front only A | `21021F` | none observed | none established | Elantra HEV 2024 and compact-dialect Tucson/Santa Cruz/Sportage routes |
| CAN-FD front + paired front corners | `21021F` | inferred right A `24024F` + `270277`; inferred left B `27828F` | none required | Older HDA2 Ioniq 5, Palisade 2023, and EV6 observations |
| CAN-FD front only B | `3A53C4` | none observed | camera objects/lanes may be absent or not logged | Compatible HDA1 routes |
| CAN-FD front + camera perception | `3A53C4` | none established | camera objects `235248` and lanes `360366` | Common newer HDA2 pattern; camera traffic is not another radar |
| Rich front + camera perception + auxiliary radar scan | rich `3A53C4` | none established | `235248`, `360366`, plus radar auxiliary records `3D03D4` | 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 `0x2100x21F`
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 | `21021F` detailed confirmed | Unknown | Unknown | Unknown |
| Hyundai Ioniq 5 | `21021F` detailed confirmed on two active routes; one route empty | HDA2 candidate | Unknown | Unknown |
| Hyundai Palisade 2023 | `21021F` detailed confirmed on one active route; one route empty | HDA2 candidate | Unknown | Unknown |
| Kia EV6 | `21021F` 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 | `21021F` compact confirmed | Unknown | Unknown | Unknown |
| Hyundai Tucson 4th gen | `21021F` compact confirmed on four routes | Unknown | Unknown | Unknown |
| Hyundai Tucson HEV 2025 | `21021F` compact confirmed | Unknown | Unknown | Unknown |
| Kia Sportage 5th gen | `21021F` 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 `21021F` | paired `240/27028F` front corners; A=right and B=left strongly inferred |
| Elantra HEV 2024 | detailed `21021F` | no corner family established |
| Santa Cruz 2025 / Tucson family / Sportage 5th gen | compact `21021F` | 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 `3A53C4` where compatible | platform-dependent camera `235` and `360` traffic; no decoded corner family established |
| EV9 / Ioniq 9 | rich `3A53C4` | 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 | `50051F` implemented; verify | Unknown | Unknown | Unknown |
| Kia K7 2017 | `50051F` implemented; verify | Unknown | Unknown | Unknown |
| Kia Niro EV | `50051F` implemented; verify | Unknown | Unknown | Unknown |
| Hyundai Kona EV 2022 | `602617` partially implemented; verify | Unknown | Unknown | Unknown |
| Kia Ceed PHEV | `602617` 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 | `23824F` | Unknown | Unknown | Unknown |
| Kia Niro PHEV 2022 | `23824F` | Unknown | Unknown | Unknown |
| Hyundai Palisade | `23824F` | Unknown | Unknown | Unknown |
| Hyundai Ioniq HEV 2022 | `23824F` | Unknown | Unknown | Unknown |
| Hyundai Custin 1st gen | `238255` | Unknown | Unknown | Unknown |
| Hyundai Elantra 2021 | `238255` | Unknown | Unknown | Unknown |
| Hyundai Sonata | `238255` | Unknown | Unknown | Unknown |
| Hyundai Sonata Hybrid | `238255` | Unknown | Unknown | Unknown |
| Hyundai Elantra HEV 2021 | `238255` | 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 `270277` / `28828F` 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 `235248` `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 `23824F` and `238255`, including
their `201/20A/25A/266/26D` companion groups. Determine whether `250255`
and `25A25E` 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.
- `235248` 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.
- `4E04E5` 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 `235248` as `CAM` and uses triangle markers.
- The desktop radar debugger plots confirmed production tracks, the
debugger-only `612617` alternate objects, and the decoded `240/278` corner
objects. Its `SIGNALS` table exposes the compact `270/288` records, `3D0`
records, `4E04E5` status, `360366` 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.
+266 -2
View File
@@ -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 `0x2350x248` 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
`0x2350x248` exactly. Together with its 33 Hz cadence and co-location with
`0x3600x366`, this corrects the former corner-radar classification:
`0x2350x248` is a forward-camera object list.
- The repository's Delphi `ESR.dbc` exactly matches the `0x5000x53F` 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.99760.99996 with
0.020.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
`0x5000x51F` are required; some radar variants add an identically packed
upper bank at `0x5200x53F`. 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` | 1115 | 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` | 47 | 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 `0x5000x51F` 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 03. 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 | `24124F`, 15 × 24 B | `270277`, 8 × 32 B, seven records each |
| B | left front | `278`, 8 B | `279287`, 15 × 24 B | `28828F`, 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 03 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 351357 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
`21021F` 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 `0x2410x24F` slots equaled
`min(CHANNEL_A_OBJECT_COUNT, 15)` exactly. It ranged from 022; 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, 1155 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 16 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 `3A53C4` 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 `3A53C4`.
| 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 02 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 16 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
+498 -45
View File
@@ -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,