This commit is contained in:
royjr
2026-08-07 20:00:45 -04:00
parent ff8f36e918
commit e01b572722
6 changed files with 67 additions and 45 deletions
+1 -1
View File
@@ -150,7 +150,7 @@ forwarded copies.
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
6. Resolve `235248` `UNKNOWN_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.
+10 -11
View File
@@ -275,9 +275,9 @@ kinematics with persistence, hysteresis, and lane plausibility.
## 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.
observed routes. The production parser marks `CLASSIFICATION=4` as oncoming
and leaves other values unknown. The research debugger classifies moving
versus stationary directly from `ABS_SPEED`.
| Signal | Start bit | Size | Scale / offset | Status |
| --- | ---: | ---: | --- | --- |
@@ -285,24 +285,23 @@ raw DBC value.
| `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. |
| `OBJECT_ID` | 40 | 11 | 1 / 0 | Persistent object ID; zero is unused. The former 7-bit decode collided in 19.9% of 82,482 simultaneous active slots; the 11-bit decode had zero collisions. |
| `WIDTH` | 52 | 7 | 0.05 / 0 m | Estimated width. |
| `CLASSIFICATION` | 60 | 3 | 1 / 0 | Unknown, truck, car, motorcycle, bicycle, pedestrian, undecided. |
| `CLASSIFICATION` | 60 | 3 | 1 / 0 | Motion/direction candidate; value 4 is route-confirmed oncoming and all other values are unknown. |
| `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. |
| `ABS_SPEED` | 125 | 12 | 0.05 / -100 m/s | Absolute longitudinal target speed. Against lane-filtered `LONG_DIST` derivatives it had 0.861 correlation, 0.009 m/s median absolute error, and 1.506 m/s 90th-percentile error across 13,545 samples. |
| `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.
On route `56190b7782883f74/00000008--0daecce463`, `CLASSIFICATION=4` tracked
opposing traffic. Lane membership plus consecutive `LONG_DIST` derivatives,
without using `REL_SPEED`, independently validated `ABS_SPEED` and the 11-bit
object ID. Other classification values remain research-only.
## 240/270-28F candidate corner-radar layout
+9
View File
@@ -0,0 +1,9 @@
from tools.radar.ui import camera_motion_display_state
def test_camera_motion_display_state_uses_direct_camera_signals():
assert camera_motion_display_state({"CLASSIFICATION": 4, "ABS_SPEED": 0}) == 4
assert camera_motion_display_state({"CLASSIFICATION": 0, "ABS_SPEED": -1.5}) == 1
assert camera_motion_display_state({"CLASSIFICATION": 0, "ABS_SPEED": 1.5}) == 1
assert camera_motion_display_state({"CLASSIFICATION": 0, "ABS_SPEED": -1.55}) == 2
assert camera_motion_display_state({"CLASSIFICATION": 0, "ABS_SPEED": 1.55}) == 2
+44 -30
View File
@@ -34,7 +34,7 @@ from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.tools.replay.lib.ui_helpers import Calibration, plot_model
from opendbc.can import CANParser
from opendbc.car import structs
from opendbc.car.hyundai.radar_interface import HYUNDAI_RADAR_TRACK_SPECS, RadarInterface
from opendbc.car.hyundai.radar_interface import HYUNDAI_RADAR_TRACK_SPECS, RadarInterface, get_radar_track_motion_state
from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP
os.environ["BASEDIR"] = BASEDIR
@@ -84,7 +84,8 @@ SPEC_BY_RANGE = {
}
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, True, True) # hide moving, oncoming, stationary, unknown
CAMERA_STATIONARY_SPEED_THRESHOLD = 1.5
RESEARCH_BUSES = (0, 1, 2)
RadarSourceKey = tuple[int, int, int]
RADAR_DETAIL_SIGNALS = {
@@ -103,8 +104,8 @@ RADAR_DETAIL_SIGNALS = {
)
} | {"CHECKSUM", "COUNTER"},
"RADAR_235_248": {
"CHECKSUM", "COUNTER", "QUALITY", "AGE", "MOTION_STATE", "OBJECT_ID", "WIDTH", "CLASSIFICATION",
"LONG_DIST", "LAT_DIST", "REL_SPEED", "REL_LAT_SPEED", "REL_ACCEL", "UNKNOWN_1", "UNKNOWN_2",
"CHECKSUM", "COUNTER", "QUALITY", "AGE", "OBJECT_ID", "WIDTH", "CLASSIFICATION",
"LONG_DIST", "LAT_DIST", "REL_SPEED", "REL_LAT_SPEED", "REL_ACCEL", "ABS_SPEED", "UNKNOWN_2",
"UNKNOWN_3", "AZIMUTH",
},
"RADAR_3A5_3C4": {
@@ -1051,16 +1052,16 @@ def radar_source_key(source) -> RadarSourceKey:
return int(source.startAddress), int(source.endAddress), int(source.bus)
def source_filter_state(source_filters: dict[RadarSourceKey, tuple[bool, bool, bool]],
source_key: RadarSourceKey) -> tuple[bool, bool, bool]:
def source_filter_state(source_filters: dict[RadarSourceKey, tuple[bool, bool, bool, bool]],
source_key: RadarSourceKey) -> tuple[bool, bool, bool, bool]:
return source_filters.get(source_key, DEFAULT_SOURCE_FILTERS)
def toggle_source_filter(source_filters: dict[RadarSourceKey, tuple[bool, bool, bool]],
def toggle_source_filter(source_filters: dict[RadarSourceKey, tuple[bool, bool, bool, bool]],
source_key: RadarSourceKey, filter_index: int) -> None:
filters = list(source_filter_state(source_filters, source_key))
filters[filter_index] = not filters[filter_index]
source_filters[source_key] = (filters[0], filters[1], filters[2])
source_filters[source_key] = (filters[0], filters[1], filters[2], filters[3])
def dbc_motion_class(motion_state: int | None) -> str:
@@ -1071,8 +1072,16 @@ def dbc_motion_class(motion_state: int | None) -> str:
)
def camera_motion_display_state(message) -> int:
if int(message["CLASSIFICATION"]) == 4:
return 4
return 1 if abs(float(message["ABS_SPEED"])) <= CAMERA_STATIONARY_SPEED_THRESHOLD else 2
def dbc_motion_category(motion_state: int | None) -> str:
if motion_state in (2, 4):
if motion_state == 4:
return "oncoming"
if motion_state == 2:
return "moving"
if motion_state in (1, 3):
return "stationary"
@@ -1080,7 +1089,7 @@ def dbc_motion_category(motion_state: int | None) -> str:
def dbc_motion_color(motion_state: int | None) -> rl.Color:
return {"moving": PURPLE, "stationary": WHITE}.get(dbc_motion_category(motion_state), MUTED)
return {"moving": CYAN, "oncoming": PURPLE, "stationary": WHITE}.get(dbc_motion_category(motion_state), MUTED)
def dbc_unknown_raw_label(motion_state: int | None) -> str | None:
@@ -1094,7 +1103,7 @@ def display_track_color(track, motion_states: dict[int, int]) -> rl.Color:
def filter_tracks(tracks, motion_states: dict[int, int], track_locations: dict[int, tuple[int, int]], sources,
source_filters: dict[RadarSourceKey, tuple[bool, bool, bool]]):
source_filters: dict[RadarSourceKey, tuple[bool, bool, bool, bool]]):
filtered_tracks = []
for track in tracks:
address, bus = track_locations.get(int(track.trackId), (-1, -1))
@@ -1105,8 +1114,9 @@ def filter_tracks(tracks, motion_states: dict[int, int], track_locations: dict[i
filters = source_filter_state(source_filters, radar_source_key(source)) if source is not None else DEFAULT_SOURCE_FILTERS
category = dbc_motion_category(motion_states.get(int(track.trackId)))
if ((filters[0] and category == "moving")
or (filters[1] and category == "stationary")
or (filters[2] and category == "unknown")):
or (filters[1] and category == "oncoming")
or (filters[2] and category == "stationary")
or (filters[3] and category == "unknown")):
continue
filtered_tracks.append(track)
return filtered_tracks
@@ -1186,11 +1196,10 @@ def get_dbc_track_details(
prefix = f"{stored_address % 2 + 1}_" if len(spec.track_prefixes) > 1 else ""
message = radar_parser.parser.vl[f"RADAR_TRACK_{can_address:x}"]
motion_signal = f"{prefix}MOTION_STATE"
if motion_signal in message:
motion_state = int(message[motion_signal])
if spec.name == "RADAR_235_248":
motion_state = 1 if motion_state in (1, 2, 3, 6) else 2 if motion_state in (5, 8, 9) else 0
states[int(point.trackId)] = motion_state
if spec.name == "RADAR_235_248":
states[int(point.trackId)] = camera_motion_display_state(message)
elif motion_signal in message:
states[int(point.trackId)] = get_radar_track_motion_state(spec, message, prefix)
elif spec.name == "RADAR_500_53F" and int(message.get("ONCOMING_ESR", 0)):
states[int(point.trackId)] = 4
@@ -1659,7 +1668,7 @@ def draw_model_line(points_x, points_y, center_x: float, car_y: float, longitudi
def draw_top_down(font, rect: rl.Rectangle, tracks, show_labels: bool, model,
motion_states: dict[int, int], hide_moving: bool, hide_stationary: bool,
motion_states: dict[int, int], hide_moving: bool, hide_oncoming: bool, hide_stationary: bool,
hide_unknown: bool, track_locations: dict[int, tuple[int, int]], sources,
hovered_id: int | None, selected_id: int | None,
preview_id: int | None = None) -> None:
@@ -1681,7 +1690,8 @@ def draw_top_down(font, rect: rl.Rectangle, tracks, show_labels: bool, model,
draw_text(font, label, scale_x - label_size.x - 10, y - 7, 14, MUTED)
legend = (
(PURPLE, "moving", hide_moving),
(CYAN, "moving", hide_moving),
(PURPLE, "oncoming", hide_oncoming),
(WHITE, "stationary", hide_stationary),
(MUTED, "unknown", hide_unknown),
)
@@ -2314,7 +2324,7 @@ def draw_source_status(font, rect: rl.Rectangle, live_tracks, valid: bool, alive
show_labels: bool, data_source: str, camera_mode: str,
both_cameras_available: bool, remote_address: str | None, bridge_connected: bool,
visible_tracks, motion_states: dict[int, int], track_locations: dict[int, tuple[int, int]],
source_filters: dict[RadarSourceKey, tuple[bool, bool, bool]],
source_filters: dict[RadarSourceKey, tuple[bool, bool, bool, bool]],
table_mode: str, fps: int, show_inactive_sources: bool) -> str | tuple[str, RadarSourceKey] | None:
rl.draw_rectangle_rec(rect, rl.Color(11, 16, 24, 225))
tracks = live_tracks.points if valid else ()
@@ -2356,7 +2366,7 @@ def draw_source_status(font, rect: rl.Rectangle, live_tracks, valid: bool, alive
top_hovered_tooltip = source_hovered_tooltip
source_key = radar_source_key(source)
hide_moving, hide_stationary, hide_unknown = source_filter_state(source_filters, source_key)
hide_moving, hide_oncoming, hide_stationary, hide_unknown = source_filter_state(source_filters, source_key)
source_text = " / ".join(text for text, _ in source_parts)
source_track_ids = [
int(track.trackId)
@@ -2369,10 +2379,11 @@ def draw_source_status(font, rect: rl.Rectangle, live_tracks, valid: bool, alive
)
]
moving_count = sum(dbc_motion_category(motion_states.get(track_id)) == "moving" for track_id in source_track_ids)
oncoming_count = sum(dbc_motion_category(motion_states.get(track_id)) == "oncoming" for track_id in source_track_ids)
stationary_count = sum(
dbc_motion_category(motion_states.get(track_id)) == "stationary" for track_id in source_track_ids
)
unknown_count = max(0, source.trackCount - moving_count - stationary_count)
unknown_count = max(0, source.trackCount - moving_count - oncoming_count - stationary_count)
visible_source_tracks = sum(
source.startAddress <= address <= source.endAddress and bus == source.bus
for track in visible_tracks
@@ -2391,7 +2402,9 @@ def draw_source_status(font, rect: rl.Rectangle, live_tracks, valid: bool, alive
filter_x = count_rect.x + count_rect.width + 9
filter_specs = (
(f"MOVING {moving_count:>{TRACK_COUNT_FIELD_WIDTH}}",
PURPLE, "moving", hide_moving, moving_count),
CYAN, "moving", hide_moving, moving_count),
(f"ONCOMING {oncoming_count:>{TRACK_COUNT_FIELD_WIDTH}}",
PURPLE, "oncoming", hide_oncoming, oncoming_count),
(f"STATIONARY {stationary_count:>{TRACK_COUNT_FIELD_WIDTH}}",
WHITE, "stationary", hide_stationary, stationary_count),
(f"UNKNOWN {unknown_count:>{TRACK_COUNT_FIELD_WIDTH}}",
@@ -2544,7 +2557,7 @@ def ui_thread(addr: str, start_wide: bool = False, start_fused: bool = False, st
camera_projection_height = float(CAMERA_BUFFER_HEIGHT)
show_labels = False
show_inactive_sources = False
source_filters: dict[RadarSourceKey, tuple[bool, bool, bool]] = {}
source_filters: dict[RadarSourceKey, tuple[bool, bool, bool, bool]] = {}
use_can_source = True
table_mode_index = 0
table_scroll = 0
@@ -2899,8 +2912,9 @@ def ui_thread(addr: str, start_wide: bool = False, start_fused: bool = False, st
for source in selected_tracks.trackSources
] or [DEFAULT_SOURCE_FILTERS]
hide_moving = all(filters[0] for filters in source_filter_values)
hide_stationary = all(filters[1] for filters in source_filter_values)
hide_unknown = all(filters[2] for filters in source_filter_values)
hide_oncoming = all(filters[1] for filters in source_filter_values)
hide_stationary = all(filters[2] for filters in source_filter_values)
hide_unknown = all(filters[3] for filters in source_filter_values)
tracks = filter_tracks(
tracks, motion_states, track_locations, selected_tracks.trackSources, source_filters,
)
@@ -2972,7 +2986,7 @@ def ui_thread(addr: str, start_wide: bool = False, start_fused: bool = False, st
)
if isinstance(clicked_action, tuple):
action, source_key = clicked_action
toggle_source_filter(source_filters, source_key, {"moving": 0, "stationary": 1, "unknown": 2}[action])
toggle_source_filter(source_filters, source_key, {"moving": 0, "oncoming": 1, "stationary": 2, "unknown": 3}[action])
table_scroll = 0
elif clicked_action == "source":
use_can_source = not use_can_source
@@ -3082,7 +3096,7 @@ def ui_thread(addr: str, start_wide: bool = False, start_fused: bool = False, st
camera_projection_height, camera_hovered_id, selected_track_id, table_hover_id)
draw_top_down(
font, radar_rect, tracks, show_labels, model, motion_states,
hide_moving, hide_stationary, hide_unknown, track_locations, selected_tracks.trackSources,
hide_moving, hide_oncoming, hide_stationary, hide_unknown, track_locations, selected_tracks.trackSources,
top_down_hovered_id, selected_track_id, table_hover_id,
)
table_mode = TABLE_MODES[table_mode_index]
@@ -3097,7 +3111,7 @@ def ui_thread(addr: str, start_wide: bool = False, start_fused: bool = False, st
)
if isinstance(clicked_action, tuple):
action, source_key = clicked_action
toggle_source_filter(source_filters, source_key, {"moving": 0, "stationary": 1, "unknown": 2}[action])
toggle_source_filter(source_filters, source_key, {"moving": 0, "oncoming": 1, "stationary": 2, "unknown": 3}[action])
table_scroll = 0
elif clicked_action == "source":
use_can_source = not use_can_source
Generated
+2 -2
View File
@@ -1041,7 +1041,7 @@ wheels = [
[[package]]
name = "pytest-xdist"
version = "3.7.1.dev24+g2b4372b"
version = "3.7.1.dev24+g2b4372bd6"
source = { git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da#2b4372bd62699fb412c4fe2f95bf9f01bd2018da" }
dependencies = [
{ name = "execnet" },
@@ -1302,7 +1302,7 @@ provides-extras = ["dev"]
[[package]]
name = "tinygrad"
version = "0.12.0"
version = "0.13.0"
source = { editable = "tinygrad_repo" }
[package.metadata]