Compare commits

...

226 Commits

Author SHA1 Message Date
firestarsdog 254e9cf1b0 Match lower SLC limits on SET to avoid stale targets 2026-11-04 16:17:22 -05:00
firestarsdog 68c0b55a00 last 2026-11-04 12:16:09 -05:00
firestarsdog 446801ca74 Match newer Dashboard packets to avoid stale SET 2026-11-04 11:52:42 -05:00
firestarsdog 7fe8dd5e83 Fix SLC standstill SET and stale wheel writes 2026-11-04 11:42:45 -05:00
whoisdomi 72d886cef9 Adopt SLC Speed Limit button fixes 2026-11-04 10:14:11 -06:00
firestarsdog b161b33ae8 Refresh ARM64 device artifacts and release receipt 2026-11-04 01:25:08 -05:00
firestarsdog b0c03bdbda Fixes 2026-11-04 00:46:58 -05:00
firestarsdog 08cfff03af Build 2026-11-03 22:47:22 -05:00
firestarsdog af4d7e5a0d flop 2026-11-03 22:15:26 -05:00
firestar5683 14534287c1 Detect C-HR CAN filters alongside native radar traffic 2026-11-03 20:22:32 -06:00
Dom 2929a25afe Add parked manual editing for thirty steering coordinates 2026-11-04 02:14:49 +00:00
firestar5683 465db1bc61 Preserve Ford lateral intent across separate brake status receipts 2026-11-03 19:55:37 -06:00
firestar5683 89af8a6569 Preserve Subaru intent through stop and source handoffs 2026-11-03 18:30:19 -06:00
Dom dd4214f8f5 Restore direct LAN DNS entry to Galaxy safely
Discover exact OS/DHCP/PTR identities independently of request Host. Redirect arbitrary aliases only at the direct-local inert landing document, preserving Host, Origin, session and remote boundaries. Refresh the protected-inventory receipt for the documentation-only protected change; native inputs are unchanged.
2026-11-03 17:03:01 -06:00
firestar5683 180f94dc35 Recover firmware reads interrupted by unrelated diagnostics 2026-11-03 16:02:27 -06:00
prabhaavp e997847db0 Merge pull request #275 from dirwin31/CarViewFontFix
StarPort: Missing Fonts
2026-11-03 16:44:04 -05:00
Danny 228534e0cf StarPort: Load fonts in the car screen and log full process commands 2026-11-03 13:37:46 -08:00
firestarsdog 5e02c2387a Arabesque time harden 2026-11-03 16:20:25 -05:00
Dom e6f1ad93ee Exercise native timer and interrupt health in siren harness 2026-11-03 14:43:02 -06:00
Dom 5f2caf8249 Align loader affinity fixture with captured camera dimensions 2026-11-03 14:24:38 -06:00
firestar5683 7b16c71824 Recheck live tuning against the accepted baseline 2026-11-03 13:21:03 -06:00
firestarsdog e381bf6a44 Check offline Firefox against AGNOS 2026-11-03 14:07:29 -05:00
firestarsdog c7b81a5768 More offline Arabesque stuff 2026-11-03 13:41:15 -05:00
firestar5683 7920339796 Prevent same-bus HKG advisory packet retransmission 2026-11-03 12:33:24 -06:00
Dom fc45eb6862 Style Polaris model cards by series 2026-11-03 12:06:50 -06:00
firestar5683 b302c2ef8f Avoid repeated CCNC longitudinal configuration clones 2026-11-03 11:53:35 -06:00
firestar5683 4d804768ab Keep conditional experimental mode independent of planner policy 2026-11-03 11:49:49 -06:00
firestarsdog 9b7f4c6646 Arabesque, include offline depends for install 2026-11-03 12:33:55 -05:00
firestar5683 14205db69d Reset HKG angle recovery gain and rejected command history 2026-11-03 11:33:09 -06:00
firestar5683 4cf5c9e098 Keep Subaru fixed-angle handoff within native rate limits 2026-11-03 11:25:13 -06:00
firestar5683 bf4e27fca5 Build matching Carnival independent lateral and longitudinal firmware 2026-11-03 11:12:56 -06:00
firestar5683 d42ddc3ac5 Restore Carnival ICE independent lateral and longitudinal startup 2026-11-03 11:01:08 -06:00
firestar5683 55d6fb1778 Continue learning after restoring a saved tune 2026-11-03 10:53:50 -06:00
prabhaavp 91dad8b66d Merge pull request #273 from dirwin31:StarPort-AutoSetup-20.2
StarPort: guided setup, a setup ladder, and wireless adapter fixes
2026-11-03 11:22:05 -05:00
Prabhaav Pillai 047294a1d0 Merge Dom into StarPort 2026-11-03 11:19:55 -05:00
firestar5683 a336e65dd8 Improve Tuning and Analysis controls and plot tabs 2026-11-03 10:15:16 -06:00
firestar5683 dd9608eecd Preserve Subaru lateral on fresh input after output expiry 2026-11-03 10:09:06 -06:00
Prabhaav Pillai 123e3c20a0 StarPort setup, reconnect handling, and UI consistency 2026-11-03 10:58:57 -05:00
whoisdomi 80975369ee Force Stop: add dashboard signal back 2026-11-03 09:18:24 -06:00
whoisdomi 7ec8cc721d Hold CEM for cross traffic leads 2026-11-03 09:18:24 -06:00
whoisdomi 1d2d18fa2a Force Stop Temp Hold 2026-11-03 09:18:23 -06:00
firestar5683 42a1244e1b Show retained Auto Learn study progress 2026-11-03 08:28:22 -06:00
Danny f148770788 Build 2026-11-03 05:40:06 -08:00
Danny 755d82a7e6 UI Unification 2026-11-03 05:40:06 -08:00
Danny 7d7bf43fac Account for Starport on Dev Page 2026-11-03 05:40:06 -08:00
Danny 63b05a9294 Fix test for AA wireless 2026-11-03 05:40:06 -08:00
Danny 80f707aa3d StarPort: Review fixes for the stall notice, car forget and car adoption
Reuse atomic_write and the sr-only class, drop dead code, update stale tests
2026-11-03 05:40:06 -08:00
Danny 6d99039382 Test Fixes 2026-11-03 05:40:06 -08:00
Danny cfdcd14ca8 Show car vs app in pairing 2026-11-03 05:40:06 -08:00
Danny 5be95f48df AAwireless: Stop tearing the Session down every 60s 2026-11-03 05:40:06 -08:00
Danny fb9b3516b1 Allow Disconnect from Galaxy for Starport Projecting Car 2026-11-03 05:40:06 -08:00
Danny 15b2612d7f Hold RFComm for a moment, Better Traces. 2026-11-03 05:40:06 -08:00
dirwin31 e99e552f21 StarPort: wait out the comma's own Wi-Fi switching networks, and say so on the car screen
- When the comma's Wi-Fi joins or roams while the car is on aa0, wait up to 8 s for video acks instead of 2.5 s; reconnect at once if the car takes its screen back.
- After a drop it causes, the car screen shows "Reconnected: comma Wi-Fi Joined <network>" (the network's SSID) for 8 s; Galaxy and bug reports name it too.
- Logs ack_grace, ack_grace_recovered and other_wifi_changed events.
2026-11-03 05:40:06 -08:00
dirwin31 f0c5a666c5 Galaxy Bluetooth setup: one systemd reload, after the radio has started
- Link the bridge and discovery units with --no-reload and reload once (was four reloads, 1-3 s each on a comma four).
- Wait (up to 45 s after boot) while starpilot-bluetooth-radio is still activating, so the reload can't stall btattach or bluetoothd.
- Failure handling unchanged; on a comma four this removed the ~20 s Bluetooth boots (now 16.4-16.6 s with stock AGNOS).
2026-11-03 05:40:06 -08:00
dirwin31 3b8d201839 Hide other devices once selected one 2026-11-03 05:40:06 -08:00
dirwin31 067f30dfa4 Bring the purple back to the guided setup's selections
The selected adapter or app segment is solid purple with a soft glow instead
of grey, the selected connection option gets a purple-tinted row and a filled
icon tile, the current step and the primary button glow, and a faint purple
wash sits behind the step track.
2026-11-03 05:40:05 -08:00
dirwin31 1d0525fe41 Keep the guided card open after choosing how the car connects
Choosing a USB cable or a tablet app switches the connection at once, which
counts as setup done, so the card closed under the person's finger and left
no way to change the choice. A choice made in the card now holds it open
until Done. In the guided setup the home card's button reads Change how your
car connects, so the way back is visible.
2026-11-03 05:40:05 -08:00
dirwin31 363d942ec9 Theme the guided setup with Galaxy's own colors, font and icons
Done steps and checks use the theme's success color, the current step and
selection use the primary purple, text uses Galaxy's Inter and weight tokens,
and the connection options use the outline icons the rest of the page uses
(car-front, usb-symbol, usb-c, tablet) on a single primary-tinted tile
instead of Apple's multicolour gradients.
2026-11-03 05:40:05 -08:00
dirwin31 b21952a392 Reuse the existing StarPort list, row and disclosure styles in the guided card
The guided card's lists, paired-car rows, checklist rows and handshake
disclosure now use the page's own gx-starport-list, gx-starport-row,
gx-starport-row__text and gx-starport-wiki classes, restyled once for the
card, instead of parallel guided-only copies. Radii, spacing and motion use
Galaxy's tokens. Only the step track, icon tiles, radio rows, segmented
control and checklist marks remain new, and the block is about a fifth
shorter.
2026-11-03 05:40:05 -08:00
dirwin31 2678e29cb3 Redesign the guided StarPort setup; keep the dropdowns to the classic screens
The guided card is rebuilt in an Apple-style: a three-step track (support,
car, connect) that fills as setup moves along, a large title, inset grouped
lists with coloured icon tiles for how the car connects, a segmented control
for the adapter model and the tablet app, iOS-style checklist rows for the
connection, and one full-width primary button. It uses Galaxy's theme colors
so light and dark both work, and reads cleanly down to a phone.

The classic step 2 and the home card's dropdowns now render only in the
classic setup. The guided card replaces them and hosts the page's own
pairing, cable and app instructions and the Connect button, so nothing is
lost. A finished setup can be reopened from Setup and Pairing, and Done
closes it. Paired cars the page's own list does not show yet are offered in
the card; the rest stay in the pairing step.
2026-11-03 05:40:05 -08:00
dirwin31 75a29d3e71 Put the full report in the logs bundle; turn the Bluetooth-first refusal into a step
The logs bundle, and so the Discord send, now carries report.json: the same
tags and per-session detail a Bugsink report has (setup, offered modes, reach
class, ladder, SDP, strategy) for the newest five sessions, sanitized and
size-capped. It is built best-effort: if it cannot be, the bundle is
unaffected.

The adapter setup's 'Connect your paired car in Galaxy Bluetooth first'
refusal (27 Bugsink reports) now shows a step with a link to Bluetooth, next
to the usual question while setup is unfinished and alone once it is done.
2026-11-03 05:40:05 -08:00
dirwin31 e5a981b5ad Ladder passes are per ignition; car adoption leaves the owner's audio setup alone
The comma can stay powered between drives, so the three passes belong to an
ignition (an onroad start), not to a boot; a reboot still resets them. When a
learned strategy is dropped after two handshake failures the ladder carries
on from the next rung instead of starting at the one that just failed, which
the dongle flip also avoided.

Choosing the only paired Android Auto car from housekeeping runs with nobody
at the screen, and selecting a receiver unselects that device as the
Bluetooth audio output. It now skips a car that is the selected audio device,
or when that cannot be read, leaves it to the setup screen's Use button. It
reports problems once instead of every 20 s and gives the projection role
back, keeping only the standby gateway auto-connect needs.
2026-11-03 05:40:05 -08:00
dirwin31 54eab0df8d Replay the real Bugsink failure texts through the reachability classifier; tag the reply style
The 686 pre-video failures Bugsink showed (54 distinct texts) are kept as a
fixture. The test checks the intended class for each: Host is down and page
timeouts are a sleeping car, refused and unanswered handshakes are not ready,
and anything after the link is up is not a reachability problem. Reports tag
the reply style and Wi-Fi band the working strategy used.
2026-11-03 05:40:05 -08:00
dirwin31 99a9025afe Report how the channel was found and what the ladder tried; wait longer for a slow car
Reports carry the SDP outcome (where the RFCOMM channel came from and how
many retries it took). The Discord post names the handshake options tried
and how each ended. A car that once needed 70 s to be ready now gets that
many stage timeouts to answer the Wi-Fi handshake, never fewer than the
standard wait and capped at nine.
2026-11-03 05:40:05 -08:00
dirwin31 0783710c9a Add the guided StarPort setup behind the New pairing experience switch
A card above the classic steps asks one question (how does the car connect:
built-in wireless, adapter, USB cable or app), shows only the instructions
for that answer, and lists paired cars that offer Android Auto to choose
between when there is more than one. While connecting it shows a live
checklist of the stages (Bluetooth, the car's Wi-Fi, securing the connection,
the dashboard), a list of the handshake options being tried once one has
failed or one is remembered, and a notice that the first connection can take
a few minutes or more than one drive. Every action is one the page already had.

The card is on by default; Use classic setup turns the switch off, and Try
the guided setup turns it back on. Returning drivers see the card only while
a connection is in progress. The state function is pure and tested without a
browser.
2026-11-03 05:40:05 -08:00
dirwin31 3d26454122 Let Galaxy read and change the new pairing experience switch
The setup response carries newSetup, and a new_setup control action turns the
guided setup off or on. It is handled before the StarPort-enabled check and
never calls the daemon, so it works as the way back to the classic screens
when the service is off or broken. It needs a signed-in session and park.
2026-11-03 05:40:05 -08:00
dirwin31 57900f3d73 Add the setup ladder: try handshakes in order, remember the one that worked
One module now decides how to answer a car's handshake, and it is also the
learned profile: the strategy that last worked is the one tried first.

The first pass is today's behavior. Later rungs are knobs that already
existed: the other version reply (the flip the dongle work added, now owned
here), the phone-style reply with both bands and with 2.4 GHz only, a longer
wait for a car that keeps saying not ready, and, for adapters only, the
handshake variations.

A failure advances the ladder only when the car answered the version
exchange, then went silent or refused the Wi-Fi setup, before any video was
acknowledged in the session. A sleeping car, this comma's own Bluetooth
trouble and anything after video (a car shutting off) never move it. Three
passes per boot at most, resuming at the first untried rung on the next
ignition; a learned strategy is dropped after two handshake failures; a
session that streams for a minute makes its strategy the learned one.

State is a small per-car file beside the config (setup_state.json, newest 16
cars, damaged or unreadable means a fresh start, never blocks a connection).
The status exposes the ladder for the setup screen, and reports carry what
was tried, how each rung ended and how long the car took to be ready.
2026-11-03 05:40:05 -08:00
dirwin31 2af7bdd1dd Add opt-in H.265 for a head unit that offers no H.264 mode
Allow H.265 video (Developer > StarPort, off by default) lets a car that
offers only H.265 get H.265. A car with an H.264 mode is chosen exactly as
before whatever the setting; this is replay-tested over several offers.

The hardware encoder library is ABI 4: an HEVC option selects H.265 Main
profile at high-tier level 5 and allows up to 1920x1080 (H.264 stays at
1280x720). The Python side keeps H.265 apart from the H.264 code: its own
codec-config and wire-frame functions for the two-byte NAL header, its own
access unit check, and an encoder subclass. If the car is told H.265 and the
hardware encoder cannot open, the session fails with the reason; there is no
fallback to another codec.

Measured on a parked comma with a scratch copy of the library: H.265 1080p
costs about 13 ms per frame on the NV12 path (30 ms from RGBA), mid-stream
keyframes carry VPS/SPS/PPS, and the H.264 session is unchanged. The
encoder splits every H.265 picture into two slices, which the check allows.
2026-11-03 05:40:05 -08:00
dirwin31 5dbcbb9618 Choose the car automatically when exactly one paired device offers Android Auto
A car that is paired and offers wireless Android Auto but was never chosen
(the pairing window closed first, or it was paired in Bluetooth settings)
is adopted by housekeeping when it is the only candidate. This reads the
cached BlueZ service list, so it never pages a device, and it does not run
during a pairing window or session, for adapter setups, or once a car is
chosen. With several candidates nothing is guessed; they are listed in the
status as car_candidates for the setup screen to offer.
2026-11-03 05:40:05 -08:00
dirwin31 d5f6d6ee72 Only wait on a Wi-Fi interface NetworkManager reports as unmanaged or unavailable
The first version waited on any state below disconnected, which also caught
the unknown state and broke the lease tests. Wait on states 10 and 20 only.
2026-11-03 05:40:05 -08:00
dirwin31 cad0022d3e Wait for NetworkManager to manage a shared Wi-Fi interface and tell stream losses apart
Nineteen Bugsink joins were refused with 'device is strictly unmanaged'.
On a shared interface the join now waits up to 8 s for NetworkManager to
manage it (dedicated mode already waited) and otherwise reports that the
interface is unmanaged and for how long, which the diagnosis already maps
to wifi_not_ready.

Stream losses keep the projection_interrupted category but now carry a kind
(stalled, silent, disconnected) with matching next steps, reported as
aa.loss_kind, so a car shutting off is no longer read as link loss.
2026-11-03 05:40:05 -08:00
dirwin31 dbb1fe6547 Add a StarPort section to the Developer page with the new pairing switch and an H.265 setting
The Developer page gets a heading row, StarPort, that groups the StarPort
settings: the New pairing experience switch (on unless turned off), Stay on
Wi-Fi (moved under the heading, same key, label and default) and Allow
H.265 video (off by default, daemon setting allow_hevc).

The heading is a row whose key starts with heading:, projected with
heading=true. It has no choices, is never available and cannot be changed,
and Galaxy draws it as a label. The new pairing switch is a plain file next
to the other StarPort data rather than a Params key, so it reads and changes
with the StarPort service down and needs no device rebuild; absent or
unreadable means on.
2026-11-03 05:40:05 -08:00
dirwin31 6854118aa2 Wait out an unreachable car by why it was unreachable
Most failed drives in Bugsink never reached video and stopped at Bluetooth,
but one backoff treated a sleeping radio, a service that was not up yet and
this comma's own busy stack alike.

reachability.classify() separates a car that never answered a page (Host is
down, page timeout), a car that answered but refused or reset, and a local
Bluetooth failure. A sleeping car or busy controller now waits at least as
long as before and often longer; other failures wait exactly as before. A
sleeping car no longer makes the comma forget the cached RFCOMM channel.
The class is logged on attempt_failed and reported as aa.reach_class.

The 141 'unknown' failures at the discovering stage now get a diagnosis:
car_asleep, car_not_ready or bluetooth_stack_busy, with next steps.
2026-11-03 05:40:05 -08:00
dirwin31 8e0073f110 Show the chosen setup in the StarPort log send
The Discord post now names the setup type, profile and whether it was
chosen or detected, from the newest session log that recorded one, next to
the car and head unit. The zip already carries the same data in each
session log's session_start and discovery events.
2026-11-03 05:40:05 -08:00
dirwin31 fd660ef6f7 Report the chosen setup, the car's offered video modes and the working strategy
Reports could not tell a built-in unit from a dongle, and offers and
handshakes were only visible in failure text, so there was nothing to
compare working pairings against.

session_start now records the setup type (builtin/adapter/usb/app), the
resolved profile and whether it was chosen or detected. Session reports
carry every video mode the head unit offered, whether the comma's address
reached the car in the pairing request, and, for streaming sessions, the
strategy that worked (window, reply style, Wi-Fi channel type). The drive
report gains low-cardinality tags (aa.setup_type, aa.adapter_model,
aa.profile_source, aa.phone_address_sent); long values stay in contexts.
Old logs without these fields still report. aa.schema is now 3.
2026-11-03 05:40:05 -08:00
dirwin31 745b1e94de Send the comma's Bluetooth address in the head unit pairing request
LeaseAwarePhone (used for every wireless session) had no adapter() method,
so Supervisor._adapter_address() hit AttributeError, swallowed it, and
returned "". The Bluetooth-service pairing request was therefore never sent
(bluetooth_opened always logged phone_address: "").

A 2020 myChevrolet (head unit 2.46.1.6) through an AAWireless Two+ then
accepted and acknowledged video but never showed it. A real phone sends the
pairing request about 1 s into the session; the car shows Android Auto once
its hands-free link to that address is up.

Pass adapter() through to the wrapped BluezPhone. It is a read-only property
lookup, so it needs no phone-role lease, like devices() and device().
_adapter_address() now logs adapter_address_unavailable instead of failing
silently.

Tested on the car (session-000052): phone_address sent, pairing response
received, picture displayed, touch input worked, 20-29 fps fully acked.

Co-authored-by: droobah <34214975+droobah@users.noreply.github.com>
2026-11-03 05:40:04 -08:00
dirwin31 085aa2a173 Send StarPort logs from Logs & Diagnostics
Adds a Send logs card to the StarPort Logs page so a user can report a
problem with all of their reportable logs: an optional note of up to 60
characters, and the same zip as Download all logs, sent from the device to
the developer's log inbox. Each device keeps its own thread, titled with the
car fingerprint and a hashed device tag. Manual sends only; no automatic
failure reports. If the zip is over the 10 MB size limit, it is resent
without the device snapshot.
2026-11-03 05:37:41 -08:00
Danny b8f3bcebcb StarPort: Remember which version reply each v4 head unit started with 2026-11-03 05:37:41 -08:00
Danny 29f498d77e StarPort: Unofficial dongle option, v4 head unit version reply fallback, patience for not-ready cars 2026-11-03 05:37:41 -08:00
firestar5683 31d5868e90 Narrow validated tracking history before indexing 2026-11-03 05:27:45 -06:00
firestar5683 0f57b620ef Keep learner confirmation pending through acknowledgement 2026-11-03 05:13:06 -06:00
firestar5683 bd917d43d8 Continue unfinished tuning studies across maneuvers 2026-11-03 04:07:11 -06:00
firestar5683 be17536f2b Validate only the causal tuning pose interval 2026-11-03 02:35:13 -06:00
firestar5683 ef974ecc66 Keep consumed tuning evidence through publication 2026-11-03 01:48:50 -06:00
firestar5683 10d170e799 Report live steering trials without legacy counter errors 2026-11-03 01:07:00 -06:00
firestar5683 3924bd2e39 Give Auto Learn a clear Galaxy tuner and saved-state status 2026-11-03 00:21:38 -06:00
firestar5683 208dc09bca Build matching HKG coffee-break advisory firmware 2026-11-02 23:17:28 -06:00
firestar5683 ff9b397393 Update AGNOS release receipt 2026-11-02 23:01:34 -06:00
firestar5683 de7c4c30ad Activate C3 firmware chain and AGNOS 20.2.1 2026-11-02 23:01:34 -06:00
firestar5683 507373cb7d Restore HKG coffee-break advisory replacements 2026-11-02 22:57:08 -06:00
firestar5683 58fc0bb4e7 Preserve HKG angle AOL intent through temporary EPS refusals 2026-11-02 22:19:54 -06:00
Dom 112c775a2c Restore executable mode for controlsd entrypoint 2026-11-02 22:11:25 -06:00
Dom a5622e8933 Rebuild Panda firmware for low-speed Ioniq 6 indication policy 2026-11-02 21:44:12 -06:00
Dom fb7592f834 Keep Ioniq 6 injected blind-spot indications neutral below 5 mph 2026-11-02 21:37:07 -06:00
Dom 5f960fba75 Retain Subaru lateral intent across qualified temporary EPS refusal 2026-11-02 18:28:50 -06:00
Dom 4a46f59825 Block incompatible AGNOS bootloader updates on comma 3 2026-11-02 18:16:58 -06:00
Dom 6df20cbb76 Keep Traffic border stable during snapshot settings reads 2026-11-02 17:54:14 -06:00
Dom f5c84b4871 Use repository parameterized helper in modem tests 2026-11-02 17:26:31 -06:00
firestar5683 35f3058ea1 Finish upstream compatibility 2026-11-02 16:38:36 -06:00
firestar5683 8defcc42b5 Build upstream compatibility artifacts 2026-11-02 16:38:36 -06:00
firestar5683 b044e891e9 Use unsigned flash self-test size arithmetic 2026-11-02 16:38:35 -06:00
firestar5683 6bcdbd0e81 Initialize RAM self-test alias storage 2026-11-02 16:38:34 -06:00
firestar5683 7d062147d3 Complete native safety type checks 2026-11-02 16:38:34 -06:00
firestar5683 1ecde54bb0 Complete synchronization test fixtures 2026-11-02 16:38:34 -06:00
firestar5683 f00fc85847 Pin the retained upstream schema contract 2026-11-02 16:38:34 -06:00
firestar5683 1650f86d22 Qualify upstream compatibility 2026-11-02 16:38:34 -06:00
Adeeb Shihadeh 788e655999 up: reset the UP in pandad loopback and can_replay (#39192) 2026-11-02 16:38:34 -06:00
Adeeb Shihadeh 61bc147001 Move HITL tests to UP (#39138) 2026-11-02 16:38:34 -06:00
Adeeb Shihadeh 8e828c2c70 Improve HITL test robustness (#39153) 2026-11-02 16:38:34 -06:00
firestar5683 2e39fc2162 Finish native transport dependency cleanup 2026-11-02 16:38:33 -06:00
Adeeb Shihadeh a61a0873cc replace bridge with webrtc (#39158) 2026-11-02 16:38:33 -06:00
Adeeb Shihadeh e2b90a34a6 remove zmq (#65) 2026-11-02 16:38:33 -06:00
Adeeb Shihadeh 2e6845146b raise max message size (#25) 2026-11-02 16:38:33 -06:00
firestar5683 242e2ac0a0 Warn for sustained high-angle HKG steering requests 2026-11-02 16:37:56 -06:00
firestarsdog f34746452a Add Arabesque clock for offline time recovery 2026-11-02 17:25:51 -05:00
firestar5683 97b5237e94 Build corrected Panda flash self-tests 2026-11-02 16:14:48 -06:00
firestar5683 20da579f92 Check H7 firmware CRC within its actual application flash region 2026-11-02 15:55:53 -06:00
firestar5683 789b5365c0 Preserve HKG lateral through brake receipt ordering during steering override 2026-11-02 15:54:48 -06:00
firestar5683 a931a94f79 Build matching CCNC takeover and resume artifacts 2026-11-02 15:29:25 -06:00
firestar5683 9576fd0c02 Restore CCNC driver takeover, resume and dashboard behavior 2026-11-02 15:23:58 -06:00
firestar5683 ccd9bfc4b8 Build Panda diagnostics 2026-11-02 15:05:15 -06:00
firestar5683 c802cb51a3 Build Panda diagnostic firmware 2026-11-02 15:05:14 -06:00
firestar5683 7125680cd3 Qualify Panda diagnostic requests 2026-11-02 15:03:26 -06:00
Daniel Koepping dea0ca1a91 cereal: name the new panda faults (#39133) 2026-11-02 14:50:47 -06:00
firestar5683 4975f7c387 Build Tesla cooperative steering host and Panda firmware 2026-11-02 14:48:21 -06:00
AngusBell97 63beafc6af Restore Tesla Model 3 cooperative steering 2026-11-02 14:45:21 -06:00
firestar5683 aa397cad54 Refresh safety RX build receipt 2026-11-02 14:20:21 -06:00
firestar5683 962d13da89 Build safety RX firmware 2026-11-02 14:19:45 -06:00
firestar5683 26b75e83fa Preserve fork safety RX configurations 2026-11-02 14:16:48 -06:00
Adeeb Shihadeh ba4d173709 safety: compare speed mismatch in integer units (#3877) 2026-11-02 14:16:45 -06:00
Adeeb Shihadeh 1e91930094 safety: pack RX descriptors (#3872) 2026-11-02 14:16:44 -06:00
Adeeb Shihadeh c7d78e5462 safety: make the RX check tables const (#3870) 2026-11-02 14:16:44 -06:00
Adeeb Shihadeh 2645933f91 safety: keep RX check state only for the active safety config (#3864) 2026-11-02 14:16:41 -06:00
Adeeb Shihadeh 9640b6e049 safety: const CRC lookup tables (#3867) 2026-11-02 14:16:40 -06:00
Adeeb Shihadeh 63944ec1c3 safety: pack CanMsg and CanMsgCheck (#3868) 2026-11-02 14:16:39 -06:00
Adeeb Shihadeh ccf112fb9c safety: integer math for the RX check lag threshold (#3869) 2026-11-02 14:16:39 -06:00
Adeeb Shihadeh fb72658c89 safety: make the remaining TX message lists const (#3863) 2026-11-02 14:16:39 -06:00
firestar5683 bc57fa8eb8 Preserve GM stock-cruise lateral intent through accelerator-held source gaps 2026-11-02 14:13:25 -06:00
firestar5683 d2680ea3db Keep Tinygrad vendor tree exact 2026-11-02 13:44:48 -06:00
firestar5683 a0283d271b Refresh model runtime source receipt 2026-11-02 13:38:46 -06:00
YassineYousfi 55e4f4289a bump tinygrad usb wrap fix (#39204) 2026-11-02 13:38:46 -06:00
YassineYousfi 70f3c8897d modeld: tinygrad jit copies and update tinygrad (#39187) 2026-11-02 13:38:46 -06:00
firestar5683 cfefb493aa Use planner-primary angle demand with bounded symmetric smoothing 2026-11-02 13:33:20 -06:00
firestar5683 05d60a67ea Refresh dependency documentation receipt 2026-11-02 13:03:41 -06:00
Adeeb Shihadeh 9f15200211 slash is the default (#3879) 2026-11-02 13:03:40 -06:00
Shane Smiskol e07299dd9a Car porting checklist (#3803) 2026-11-02 13:03:40 -06:00
firestar5683 ba40d9fe5a Refresh cleanup source receipt 2026-11-02 12:56:09 -06:00
Adeeb Shihadeh f2bac62f71 remove more dead code (#39196) 2026-11-02 12:56:09 -06:00
firestarsdog f27f1f22c2 Arabesque time sync to prevent Firefox issues 2026-11-02 13:55:41 -05:00
firestar5683 d1f4a06744 Refresh shared UI source receipt 2026-11-02 12:54:13 -06:00
Adeeb Shihadeh a4cf46600d ui: share ModelRenderer between tizi and mici (#39198) 2026-11-02 12:52:14 -06:00
Adeeb Shihadeh bfca6d7f3d ui: share CameraView between tizi and mici (#39197) 2026-11-02 12:52:13 -06:00
firestar5683 e094c3b624 Build fleet driver monitoring lock 2026-11-02 12:49:33 -06:00
Harald Schäfer 4f4525e59c Fleet: force always-on driver monitoring (#39169) 2026-11-02 12:46:12 -06:00
firestar5683 6140bd54a6 Update low-speed model action receipt 2026-11-02 12:37:31 -06:00
Harald Schäfer 07606c8f9e modeld: raise low-speed curvature thresholds (#39182) 2026-11-02 12:36:18 -06:00
firestar5683 b409ef52cb Build native message queue bindings 2026-11-02 12:30:26 -06:00
Adeeb Shihadeh fa3138c929 bump msgq: nanobind (#39200) 2026-11-02 12:28:46 -06:00
firestar5683 fa02d93a01 Recover Galaxy updates after gateway disconnects 2026-11-02 11:53:41 -06:00
firestar5683 aa3266824c Refresh modem update receipt 2026-11-02 11:47:13 -06:00
firestarsdog d7b5b8aef2 AGNOS Modem Fix 2026-11-02 12:31:45 -05:00
firestar5683 ad67ae64b4 Verify native UI build receipt 2026-11-02 11:22:15 -06:00
firestar5683 969ab9083c Verify native UI runtime artifacts 2026-11-02 11:20:21 -06:00
firestar5683 3fdd0d122b Build StarPilot native Raylib support 2026-11-02 11:14:49 -06:00
Adeeb Shihadeh 3c9a722083 Switch to nanobind raylib bindings (#39193) 2026-11-02 11:13:02 -06:00
Dom cbd6e1eb52 Use typed Card owners in radar publication tests 2026-11-02 09:57:40 -06:00
firestar5683 f1e5ee7312 Connect large Home dashboard to cached drive history 2026-11-02 09:40:11 -06:00
firestar5683 91c1fee59a Qualify Upstream Python Cleanup 2026-11-02 09:28:07 -06:00
Adeeb Shihadeh c08eb2fc0b remove dead code (#39195) 2026-11-02 09:26:06 -06:00
firestar5683 295374ecec Qualify Faster Unit Tests 2026-11-02 09:24:41 -06:00
Adeeb Shihadeh c710aea5ce faster unit tests (#39202) 2026-11-02 09:24:41 -06:00
firestar5683 e5931a7321 Qualify Radar Publication 2026-11-02 09:22:32 -06:00
firestar5683 25247f09ba Test Radar Publication Fault Semantics 2026-11-02 09:17:59 -06:00
Shane Smiskol 393a6139cf card: only radar CAN errors invalidate radarTracks (#39185) 2026-11-02 09:17:59 -06:00
firestar5683 9ef9ef54b2 Qualify Unix Datagram Logging 2026-11-02 08:57:34 -06:00
firestar5683 3705334acc Build Unix Datagram Logging 2026-11-02 08:57:34 -06:00
Adeeb Shihadeh 63ef69c04b swaglog: replace zmq with unix datagram socket (#39157) 2026-11-02 08:57:32 -06:00
firestar5683 5bbfafa3e7 Qualify AGNOS 20.2 Release 2026-11-02 08:46:49 -06:00
Adeeb Shihadeh ca5cbb342b AGNOS 20.2 (#39178) 2026-11-02 08:44:29 -06:00
firestar5683 8f59bfc56c Restore pre-rebase Prius torque shaping 2026-11-02 08:16:52 -06:00
firestar5683 7bcbee716a Refresh saved UI display flags once per second 2026-11-02 08:14:45 -06:00
firestar5683 8ef661eb59 Build matching Tesla screen lateral safety firmware 2026-11-02 08:13:18 -06:00
firestar5683 2ccc5fa9db Keep Tesla screen lateral off until fresh engagement 2026-11-02 08:11:23 -06:00
firestar5683 97c6040a9f Avoid takeover for expired idle GM cruise-main intent 2026-11-02 08:01:29 -06:00
firestar5683 1c035f3e6d Verify disabled Auto Learn without a runtime 2026-11-02 07:58:56 -06:00
firestar5683 8ff493640d Preserve HKG AOL through temporary EPS during driver override 2026-11-02 07:57:38 -06:00
firestar5683 ffc1aec50b Use planner-primary angle demand with bounded symmetric smoothing 2026-11-02 07:50:07 -06:00
firestar5683 d55e0b01f6 Qualify Integration Test Cleanup 2026-11-02 01:06:38 -06:00
firestar5683 dc6c5868f4 Clean Up Upstream Integration Tests 2026-11-02 01:05:32 -06:00
firestar5683 4d8df1f754 Qualify Camera Client Reuse 2026-11-02 00:55:15 -06:00
firestar5683 1e5f5c6791 Test Reused Camera Client Lifecycle 2026-11-02 00:50:20 -06:00
Adeeb Shihadeh 5808a49a52 UI: cache camera clients (#39166) 2026-11-02 00:48:19 -06:00
firestar5683 77cf37119c Qualify Upstream CAN Updates 2026-11-02 00:48:03 -06:00
Adeeb Shihadeh f7d3dfd037 Rate limit CAN timeout logs (#3898) 2026-11-02 00:46:06 -06:00
Shane Smiskol dea30ba5e2 Tesla: add Model Y EPS firmware (92),Y4003.10.2 (#3891) 2026-11-02 00:46:06 -06:00
Shane Smiskol 69711eb73f VW MEB: add pre-2024 HMS_Status (#3862) 2026-11-02 00:46:06 -06:00
Keenin Krehbiel 50514ff9ec Model 3 fingerprint for 2026.32.7 software (#3839) 2026-11-02 00:46:06 -06:00
Amy Jeanes 88eb1f00af Tesla: add Model Y EPS fingerprint (#3845) 2026-11-02 00:46:06 -06:00
firestar5683 993726afba Qualify Athena Input Validation 2026-11-02 00:43:01 -06:00
firestar5683 30d4b9e23f Preserve Internal Athena Validation 2026-11-02 00:40:42 -06:00
Adeeb Shihadeh 15e63f04f2 athena: validate all rpc params (#39190) 2026-11-02 00:40:42 -06:00
firestar5683 281098b7ab Qualify Upstream Calibration Updates 2026-11-02 00:38:41 -06:00
firestar5683 2c9dced9a8 Test Calibration Identity Changes 2026-11-02 00:36:43 -06:00
firestar5683 849c3715d9 Preserve Calibration Cache Validation 2026-11-02 00:36:14 -06:00
Daniel Koepping 8ef2ce2a0e docs: update safety wording (#39130) 2026-11-02 00:35:35 -06:00
Shane Smiskol fd1eb1662f calibrationd: don't reset calibration on a mock fingerprint (#39172) 2026-11-02 00:35:35 -06:00
Shane Smiskol 939b4889b2 calibrationd: use first block to only seed warp (#39170) 2026-11-02 00:35:35 -06:00
Shane Smiskol 7d6cf4e387 calibrationd: reset calibration when the car changes (#39171) 2026-11-02 00:35:35 -06:00
Adeeb Shihadeh 25830237de pack.py: include opendbc and check zipapps are self-contained (#39137) 2026-11-02 00:35:35 -06:00
firestar5683 b90a0d485c Keep Ford path angle within sign-correct wire bounds 2026-11-02 00:17:54 -06:00
firestar5683 0b7df146f8 Keep Ford controller comments technical and generic 2026-11-02 00:02:19 -06:00
firestar5683 7f630d2358 Qualify Upstream Touch Fixes 2026-11-01 23:29:45 -06:00
firestar5683 4c49768b24 Preserve Keyboard Gradient Compatibility 2026-11-02 05:27:52 +00:00
Shane Smiskol e20a02d930 ui: ignore additional fingers in last_mouse_event (#39144) 2026-11-02 05:27:52 +00:00
Shane Smiskol 04441d6049 ui: decel check on px moved per touch sample, not velocity (#39143) 2026-11-02 05:27:52 +00:00
Shane Smiskol ce9d9492c2 ui: mici keyboard multitouch (#39140) 2026-11-02 05:27:52 +00:00
Shane Smiskol 1376cf9d00 ui: fit fling velocity over the last 6 touch samples (#39142) 2026-11-02 05:27:52 +00:00
Shane Smiskol 9c4be6a6c1 ui: ignore additional fingers in scroll panel (#39141) 2026-11-02 05:27:52 +00:00
Harald Schäfer 7c3bf67791 SCons: avoid premature dependency scans in progress counter (#39131) 2026-11-02 05:27:51 +00:00
firestar5683 f353afa400 Skip disabled Auto Learn work in Controls 2026-11-01 23:19:52 -06:00
Dom 9e2d7594eb Model native receipt observation in selfdrived fixtures 2026-11-01 23:17:30 -06:00
firestar5683 5f741d9887 Keep Mazda source handoffs within the original control lease 2026-11-01 23:04:00 -06:00
firestar5683 865948a00e Preserve mixed HKG AOL after stock cruise disengages 2026-11-01 23:00:18 -06:00
firestar5683 5e92608ab7 Keep first AOL arming available during native startup 2026-11-01 22:41:34 -06:00
firestar5683 f429155f0f Keep StarPilot steering active after ordinary driver release 2026-11-01 22:34:13 -06:00
firestar5683 4e14f14461 Skip disabled Auto Learn CAN observations 2026-11-01 22:26:13 -06:00
1218 changed files with 68966 additions and 17316 deletions
+2 -1
View File
@@ -39,7 +39,6 @@ tinygrad_model_runtime/** !filter !diff !merge -text
*.a -text
*.so -text
*.so.* -text
openpilot/cereal/messaging/bridge -text
openpilot/selfdrive/pandad/pandad -text
openpilot/tools/replay/replay -text
openpilot/system/camerad/camerad -text
@@ -48,6 +47,8 @@ openpilot/system/loggerd/encoderd -text
openpilot/system/loggerd/loggerd -text
openpilot/starpilot/galaxy/bin/frpc_linux_arm64 -text
openpilot/starpilot/maps/provider/mapd -text
openpilot/tools/arabesque_firefox/offline/*.deb -text
openpilot/tools/arabesque_firefox/offline/*.tar.xz -text
openpilot/selfdrive/test/process_replay/refs/starpilot/*.zst -text
# Published Panda object files are AArch64 ELF, not text.
+2 -1
View File
@@ -47,7 +47,7 @@ jobs:
id: setup
- name: Build native dependencies for packaging tests
id: build
run: scons -j2 openpilot/common/libparams_c.so msgq_repo/msgq/ipc_pyx.so
run: scons -j2 openpilot/common/libparams_c.so msgq_repo/msgq/_ipc.so
- name: Test dependency tools
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: python3 -m unittest discover -s tools/vendor/tests -v
@@ -225,6 +225,7 @@ jobs:
openpilot/selfdrive/pandad/tests/test_pandad_canprotocol
openpilot/selfdrive/pandad/tests/test_aol_protocol
openpilot/selfdrive/pandad/tests/test_aol_wire
openpilot/selfdrive/pandad/tests/test_panda_faults
openpilot/selfdrive/pandad/tests/test_producer_timing
openpilot/selfdrive/pandad/tests/test_producer_sink
- name: Test map provider to host messaging
-1
View File
@@ -74,7 +74,6 @@ openpilot/selfdrive/pandad/tests/test_aol_wire
openpilot/selfdrive/pandad/tests/test_panda_usb
openpilot/cereal/services.h
openpilot/cereal/gen
openpilot/cereal/messaging/bridge
openpilot/selfdrive/ui/translations/tmp
openpilot/selfdrive/car/tests/cars_dump
openpilot/system/camerad/camerad
+16 -15
View File
@@ -57,7 +57,7 @@ assert arch in [
"Darwin", # macOS arm64 (x86 not supported)
]
pkg_names = ['acados', 'capnproto', 'ffmpeg', 'json11', 'ncurses', 'zeromq', 'zstd']
pkg_names = ['acados', 'capnproto', 'ffmpeg', 'json11', 'ncurses', 'zstd']
if arch != 'Darwin':
pkg_names.append('libusb')
pkgs = [importlib.import_module(name) for name in pkg_names]
@@ -172,7 +172,7 @@ env = Environment(
COMPILATIONDB_USE_ABSPATH=True,
REDNOSE_ROOT="#rednose_repo",
tools=["default", "cython", "compilation_db", "rednose_filter"],
toolpath=["#msgq_repo/site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
toolpath=["#rednose_repo/site_scons/site_tools"],
)
# SCons' Darwin linker tool doesn't define the variables used to expand RPATH.
if arch == "Darwin":
@@ -292,7 +292,7 @@ SConscript(['opendbc_repo/opendbc/can/SConscript'])
# Build common module
SConscript(['openpilot/common/SConscript'])
Import('_common')
common = [_common, 'json11', 'zmq']
common = [_common, 'json11']
Export('common')
# Build messaging (cereal + msgq + socketmaster + their dependencies)
@@ -329,6 +329,7 @@ SConscript([
'openpilot/selfdrive/ui/SConscript',
'openpilot/starpilot/audio/native/SConscript',
'openpilot/starpilot/models/runtime_bridge/SConscript',
'openpilot/starpilot/ui/native/SConscript',
'openpilot/starpilot/flm/SConscript',
])
@@ -347,7 +348,7 @@ if GetOption('extras') and arch != "comma_arm64":
env.CompilationDatabase('compile_commands.json')
# progress output
def count_scons_nodes(nodes):
def get_scons_nodes(nodes):
seen = set()
stack = list(nodes)
@@ -356,28 +357,28 @@ def count_scons_nodes(nodes):
if node in seen:
continue
seen.add(node)
executor = node.get_executor()
if executor is not None:
stack += executor.get_all_prerequisites() + executor.get_all_children()
# Only scan directories; generated sources are not ready yet.
stack += (node.prerequisites or []) + node.children(scan=isinstance(node, SCons.Node.FS.Dir))
return len(seen)
return seen
progress_interval = 5
progress_count = 0
progress_total = max(1, count_scons_nodes(env.arg2nodes(BUILD_TARGETS or [Dir('.')], env.fs.Entry)))
progress_nodes = get_scons_nodes(env.arg2nodes(BUILD_TARGETS or [Dir('.')], env.fs.Entry))
progress_total = max(1, len(progress_nodes))
def progress_function(node):
global progress_count
if progress_count >= progress_total:
if node not in progress_nodes:
return
progress_count = min(progress_count + progress_interval, progress_total)
progress = round(100. * progress_count / progress_total, 1)
progress_nodes.remove(node)
if len(progress_nodes) % progress_interval:
return
progress = round(100. * (1 - len(progress_nodes) / progress_total), 1)
sys.stderr.write("\rBuilding: %5.1f%%" % progress if sys.stderr.isatty() else "progress: %.1f\n" % progress)
if progress == 100. and sys.stderr.isatty():
sys.stderr.write("\n")
sys.stderr.flush()
Progress(progress_function, interval=progress_interval)
Progress(progress_function)
AddPostAction(BUILD_TARGETS or [Dir('.')], prune_cache_dir)
# Release CI rebuilds tracked device outputs for the host architecture.
+11
View File
@@ -1,5 +1,16 @@
# Third-party notices
## Arabesque Firefox
The offline payload contains original Mozilla and Ubuntu archives. The pinned
versions, upstream download URLs and hashes are listed in
[`runtime.json`](openpilot/tools/arabesque_firefox/runtime.json). Mozilla publishes
the [Firefox 153.6.0esr source](https://archive.mozilla.org/pub/firefox/releases/153.6.0esr/source/)
and [Firefox notices](https://www.mozilla.org/en-US/about/legal/firefox/).
The Ubuntu archives retain their package copyright files. The
[Ubuntu package archive](https://ubuntu.com/project/docs/how-ubuntu-is-made/concepts/package-archive/)
provides source packages alongside the binaries.
## Jetlink
Jetlink's comma client is copyright (c) 2026 Zeph Leggett and distributed under the MIT License. Its complete license and permission notice are retained in [jetlink_repo/LICENSE](jetlink_repo/LICENSE); source and revision are recorded in [CREDITS.md](CREDITS.md#jetlink-external-inference) and `upstream-sync.json`.
+1 -1
View File
@@ -77,7 +77,7 @@ while [[ $# -gt 0 ]]; do
;;
--cereal|-cereal)
shortcut_targets+=(openpilot/cereal/libcereal.a openpilot/cereal/libsocketmaster.a
openpilot/cereal/messaging/bridge openpilot/cereal/services.h)
openpilot/cereal/services.h)
;;
-j|--jobs)
passthrough_args+=("$1")
+26 -1
View File
@@ -5,6 +5,8 @@ openpilot ALC and openpilot LDW do not automatically drive the vehicle or reduce
While changing lanes, openpilot is not capable of looking next to you or checking your blind spot. Only nudge the wheel to initiate a lane change after you have confirmed it's safe to do so.
Unexpected steering may occur. openpilot ALC may not steer to avoid stopped or slow-moving vehicles, cross traffic, construction barriers or cones, motorcycles, children, pedestrians, animals, or other objects on the road.
Many factors can impact the performance of openpilot ALC and openpilot LDW, causing them to be unable to function as intended. These include, but are not limited to:
* Poor visibility (heavy rain, snow, fog, etc.) or weather conditions that may interfere with sensor operation.
@@ -13,6 +15,14 @@ Many factors can impact the performance of openpilot ALC and openpilot LDW, caus
* The device is mounted incorrectly.
* When in sharp curves, like on-off ramps, intersections etc...; openpilot is designed to be limited in the amount of steering torque it can produce.
* In the presence of restricted lanes or construction zones.
* When the lane markings are unclear, worn, missing, or they cross each other.
* Tar marks, shadows, road cracks, temporary or construction lane markings, or other road imperfections.
* When lanes merge or split.
* When entering a highway on-ramp or exiting an off-ramp.
* On roads with intersections or roundabouts.
* On slippery roads.
* When towing a trailer.
* When entering or exiting a tunnel.
* When driving on highly banked roads or in presence of strong cross-wind.
* Extremely hot or cold temperatures.
* Bright light (due to oncoming headlights, direct sunlight, etc.).
@@ -24,6 +34,8 @@ The list above does not represent an exhaustive list of situations that may inte
openpilot ACC and openpilot FCW are not systems that allow careless or inattentive driving. It is still necessary for the driver to pay close attention to the vehicle’s surroundings and to be ready to re-take control of the gas and the brake at all times.
openpilot ACC may not stop your vehicle in time to avoid a collision with a vehicle that quickly stops in front of you. openpilot ACC may occasionally cause your vehicle to brake when not required or when you are not expecting it.
Many factors can impact the performance of openpilot ACC and openpilot FCW, causing them to be unable to function as intended. These include, but are not limited to:
* Poor visibility (heavy rain, snow, fog, etc.) or weather conditions that may interfere with sensor operation.
@@ -35,9 +47,20 @@ Many factors can impact the performance of openpilot ACC and openpilot FCW, caus
* In presence of traffic signs or stop lights, which are not detected by openpilot at this time.
* When the posted speed limit is below the user selected set speed. openpilot does not detect speed limits at this time.
* In presence of vehicles in the same lane that are not moving.
* When a vehicle you are following moves out of your driving path and a stationary or slow-moving vehicle or object is in front of you.
* Narrow vehicles, such as bicycles or motorcycles.
* Vehicles or objects only partially in the driving lane.
* On curves, openpilot ACC may not detect a vehicle in the same lane, and may accelerate to the set speed.
* Cross traffic.
* Oncoming vehicles.
* Animals or objects in the road.
* Rare objects such as trailers, ramps, cargo, open doors, etc. protruding from vehicles.
* When abrupt braking maneuvers are required. openpilot is designed to be limited in the amount of deceleration and acceleration that it can produce.
* When surrounding vehicles perform close cut-ins from neighbor lanes.
* Driving on hills, narrow, or winding roads.
* On slippery roads.
* When towing a trailer.
* When entering or exiting a tunnel.
* Extremely hot or cold temperatures.
* Bright light (due to oncoming headlights, direct sunlight, etc.).
* Interference from other equipment that generates radar waves.
@@ -46,7 +69,9 @@ The list above does not represent an exhaustive list of situations that may inte
## Limitations of openpilot DM
openpilot DM should not be considered an exact measurement of the alertness of the driver.
openpilot DM displays a series of escalating warnings. Disregard of these warnings may result in openpilot being unavailable for a period of time.
openpilot DM should not be considered an exact measurement of the alertness of the driver. openpilot DM cannot always detect if the driver is tired or inattentive.
Many factors can impact the performance of openpilot DM, causing it to be unable to function as intended. These include, but are not limited to:
+6
View File
@@ -4,6 +4,12 @@ openpilot is an Adaptive Cruise Control (ACC) and Automated Lane Centering (ALC)
Like other ACC and ALC systems, openpilot is a failsafe passive system and it requires the
driver to be alert and to pay attention at all times.
openpilot is not an autonomous driving system. It is not a collision avoidance system or a
substitute for safe and attentive driving. The driver is always responsible for the safe
operation of the vehicle and must be ready to take control at any time. The driver can override
openpilot at any time by braking, accelerating, or steering the vehicle. openpilot may
disengage without warning. Read the [limitations](https://github.com/commaai/openpilot/blob/master/docs/LIMITATIONS.md) carefully before using openpilot.
To assist the driver in maintaining alertness, openpilot includes a driver monitoring feature
that alerts when it detects driver distraction.
+10 -1
View File
@@ -26,9 +26,18 @@ fi
export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="19.8.1"
export AGNOS_VERSION="20.2.1"
fi
export AGNOS_UPDATE_POLICY="auto"
# C3 selects its complete matching firmware chain in the AGNOS manifest loader.
# Unknown hardware retains its installed OS.
if [ -f /AGNOS ] || [ -e /sys/firmware/devicetree/base/model ]; then
case "$(tr -d '\0' 2>/dev/null < /sys/firmware/devicetree/base/model)" in
"comma tici"|"comma tizi"|"comma mici") ;;
*) export AGNOS_UPDATE_POLICY="retain" ;;
esac
fi
export STAGING_ROOT="/data/safe_staging"
+3
View File
@@ -20,3 +20,6 @@ libmessaging.*
libmessaging_shared.*
.sconsign.dblite
.mypy_cache/
/msgq/_ipc.pyi
/msgq/visionipc/_visionipc.pyi
+2 -3
View File
@@ -1,4 +1,3 @@
include README.md pyproject.toml setup.py SConstruct SConscript
recursive-include site_scons *.py
recursive-include msgq *.py *.pyx *.pxd *.h *.cc
global-exclude *.so *.o *.os *.a *.pyc
recursive-include msgq *.py py.typed *.h *.cc
global-exclude *.pyi *.so *.o *.os *.a *.pyc
+33 -4
View File
@@ -1,7 +1,29 @@
Import('env', 'envCython', 'common')
import sys
import sysconfig
from pathlib import Path
import nanobind
Import('env', 'arch', 'common')
envNanobind = env.Clone(LIBS=[])
nb_dir = Path(nanobind.source_dir()).parent
envNanobind.Append(CPPPATH=[sysconfig.get_paths()['include'], nanobind.include_dir(), str(nb_dir / "ext/robin_map/include")],
CCFLAGS=["-fvisibility=hidden", "-fno-strict-aliasing"])
if arch == "Darwin":
envNanobind.Append(LINKFLAGS=["-bundle", "-undefined", "dynamic_lookup"])
else:
# Python symbols resolve at import time.
envNanobind["LINKFLAGS"] = [f for f in env["LINKFLAGS"] if f != "-Wl,--no-undefined"] + ["-pthread", "-shared"]
# Third-party runtime sources do not follow our shadowing warning policy.
envNanobindRuntime = envNanobind.Clone()
envNanobindRuntime['CCFLAGS'] = [flag for flag in envNanobindRuntime['CCFLAGS'] if not flag.startswith('-Wshadow')]
nanobind_lib = envNanobindRuntime.SharedObject('msgq/nb_combined.os', str(nb_dir / "src/nb_combined.cpp"))
visionipc_dir = Dir('msgq/visionipc')
python_suffix = ".so"
# Build msgq
msgq_objects = env.SharedObject([
@@ -10,7 +32,7 @@ msgq_objects = env.SharedObject([
'msgq/msgq.cc',
])
msgq = env.Library('msgq', msgq_objects)
msgq_python = envCython.Program('msgq/ipc_pyx.so', 'msgq/ipc_pyx.pyx', LIBS=envCython["LIBS"]+[msgq]+common)
msgq_python = envNanobind.Program(f'msgq/_ipc{python_suffix}', ['msgq/ipc_py.cc', nanobind_lib], LIBS=envNanobind["LIBS"]+[msgq]+common)
# Build Vision IPC
vipc_files = ['visionipc.cc', 'visionipc_server.cc', 'visionipc_client.cc']
@@ -24,10 +46,17 @@ vipc_objects = env.SharedObject(vipc_sources)
visionipc = env.Library('visionipc', vipc_objects)
vipc_libs = envCython["LIBS"] + [visionipc, msgq] + common
envCython.Program(f'{visionipc_dir.abspath}/visionipc_pyx.so', f'{visionipc_dir.abspath}/visionipc_pyx.pyx',
vipc_libs = envNanobind["LIBS"] + [visionipc, msgq] + common
visionipc_python = envNanobind.Program(f'{visionipc_dir.abspath}/_visionipc{python_suffix}', [f'{visionipc_dir.abspath}/visionipc_py.cc', nanobind_lib],
LIBS=vipc_libs)
# ASan modules require a preloaded runtime before Python can import them.
if "-fsanitize=address" not in env["LINKFLAGS"]:
root = Dir('.').abspath
for module in ('msgq._ipc', 'msgq.visionipc._visionipc'):
env.Command(module.replace('.', '/') + '.pyi', [msgq_python, visionipc_python],
f'"{sys.executable}" -m nanobind.stubgen -i "{root}" -m {module} -o "$TARGET"')
if GetOption('extras'):
env.Program('msgq/test_runner', ['msgq/msgq_tests.cc'], LIBS=[msgq]+common)
+1 -15
View File
@@ -1,7 +1,6 @@
import os
import platform
import subprocess
import sysconfig
arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()
if platform.system() == "Darwin":
@@ -14,7 +13,6 @@ cpppath = [
"#/",
'#msgq/',
'/usr/lib/include',
sysconfig.get_paths()['include'],
]
AddOption('--minimal',
@@ -62,21 +60,9 @@ env = Environment(
CFLAGS="-std=gnu11",
CXXFLAGS="-std=c++1z",
CPPPATH=cpppath,
CYTHONCFILESUFFIX=".cpp",
tools=["default", "cython"]
tools=["default"]
)
Export('env', 'arch', 'common')
envCython = env.Clone(LIBS=[])
envCython["CCFLAGS"] += ["-Wno-#warnings", "-Wno-cpp", "-Wno-shadow", "-Wno-deprecated-declarations"]
envCython["CCFLAGS"].remove('-Werror')
if arch == "Darwin":
envCython["LINKFLAGS"] = ["-bundle", "-undefined", "dynamic_lookup"]
else:
envCython["LINKFLAGS"] = ["-pthread", "-shared"]
Export('envCython')
SConscript(['SConscript'])
+2 -2
View File
@@ -18,12 +18,12 @@ test:
ty:
run: ty check .
codespell:
run: codespell {files} -L ned,stdio,master --builtin clear,rare,informal,usage,code,names,en-GB_to_en-US -S uv.lock,*_pyx.cpp
run: codespell {files} -L ned,stdio,master --builtin clear,rare,informal,usage,code,names,en-GB_to_en-US -S uv.lock
files: git ls-tree -r HEAD --name-only
cppcheck:
run: cppcheck --error-exitcode=1 --inline-suppr --language=c++ --force --quiet -j4 --check-level=exhaustive $(git ls-files '*.cc' | grep -v -E '(msgq_tests|test_runner)\.cc')
cpplint:
run: cpplint --exclude=msgq/ipc_pyx.cpp --exclude=msgq/visionipc/visionipc_pyx.cpp --recursive --quiet --counting=detailed --linelength=240 --filter=-build,-legal,-readability,-runtime,-whitespace,+build/include_subdir,+build/forward_decl,+build/include_what_you_use,+build/deprecated,+whitespace/comma,+whitespace/line_length,+whitespace/empty_if_body,+whitespace/empty_loop_body,+whitespace/empty_conditional_body,+whitespace/forcolon,+whitespace/parens,+whitespace/semicolon,+whitespace/tab,+readability/braces msgq/
run: cpplint --recursive --quiet --counting=detailed --linelength=240 --filter=-build,-legal,-readability,-runtime,-whitespace,+build/include_subdir,+build/forward_decl,+build/include_what_you_use,+build/deprecated,+whitespace/comma,+whitespace/line_length,+whitespace/empty_if_body,+whitespace/empty_loop_body,+whitespace/empty_conditional_body,+whitespace/forcolon,+whitespace/parens,+whitespace/semicolon,+whitespace/tab,+readability/braces msgq/
# *** tests ***
test_runner:
-1
View File
@@ -1 +0,0 @@
ipc_pyx.cpp
+4 -4
View File
@@ -1,8 +1,8 @@
from msgq.ipc_pyx import Poller, SubSocket, PubSocket, SocketEventHandle, toggle_fake_events, \
from msgq._ipc import Poller, SubSocket, PubSocket, SocketEventHandle, toggle_fake_events, \
set_fake_prefix, get_fake_prefix, delete_fake_prefix, wait_for_one_event
from msgq.ipc_pyx import MultiplePublishersError, IpcError
from msgq._ipc import MultiplePublishersError, IpcError
from typing import Optional, List, Union
from typing import Optional, List
__all__ = [
"Poller",
@@ -25,7 +25,7 @@ __all__ = [
NO_TRAVERSAL_LIMIT = 2**64-1
def fake_event_handle(endpoint: str, identifier: Optional[Union[str, bytes]] = None, override: bool = True, enable: bool = False) -> SocketEventHandle:
def fake_event_handle(endpoint: str, identifier: Optional[str] = None, override: bool = True, enable: bool = False) -> SocketEventHandle:
ident = identifier if identifier is not None else get_fake_prefix()
handle = SocketEventHandle(endpoint, ident, override)
if override:
BIN
View File
Binary file not shown.
-61
View File
@@ -1,61 +0,0 @@
# distutils: language = c++
#cython: language_level=3
from libcpp.string cimport string
from libcpp.vector cimport vector
from libcpp cimport bool
cdef extern from "msgq/event.h":
cdef cppclass Event:
@staticmethod
int wait_for_one(vector[Event], int) except +
Event()
Event(int)
void set()
int clear()
void wait(int) except +
bool peek()
int fd()
cdef cppclass SocketEventHandle:
@staticmethod
void toggle_fake_events(bool)
@staticmethod
void set_fake_prefix(string)
@staticmethod
string fake_prefix()
SocketEventHandle(string, string, bool)
bool is_enabled()
void set_enabled(bool)
Event recv_called()
Event recv_ready()
cdef extern from "msgq/ipc.h":
cdef cppclass Message:
void init(size_t)
void init(char *, size_t)
void close()
size_t getSize()
char *getData()
cdef cppclass SubSocket:
SubSocket() nogil
int connect(string, bool, size_t) nogil
Message * receive(bool) nogil
void setTimeout(int) nogil
cdef cppclass PubSocket:
PubSocket()
int connect(string, size_t)
int sendMessage(Message *)
int send(char *, size_t)
bool all_readers_updated()
cdef cppclass Poller:
Poller()
void registerSocket(SubSocket *)
vector[SubSocket*] poll(int) nogil
+113
View File
@@ -0,0 +1,113 @@
#include <cerrno>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
#include <nanobind/nanobind.h>
#include <nanobind/stl/optional.h>
#include <nanobind/stl/string.h>
#include <nanobind/stl/vector.h>
#include "msgq/event.h"
#include "msgq/ipc.h"
namespace nb = nanobind;
namespace {
struct IpcError : std::runtime_error {
using std::runtime_error::runtime_error;
};
struct MultiplePublishersError : IpcError {
using IpcError::IpcError;
};
void check_error(bool failed, int error, const std::string &endpoint = "") {
if (!failed) return;
std::string message = "Messaging failure " + (endpoint.empty() ? "" : "with " + endpoint) + ": " + std::strerror(error);
if (error == EADDRINUSE) throw MultiplePublishersError(message);
throw IpcError(message);
}
} // namespace
NB_MODULE(_ipc, m) {
// Daemon threads can still hold sockets at exit.
nb::set_leak_warnings(false);
nb::exception<IpcError> ipc_error(m, "IpcError");
nb::exception<MultiplePublishersError>(m, "MultiplePublishersError", ipc_error);
m.def("toggle_fake_events", &SocketEventHandle::toggle_fake_events, nb::arg("enabled"));
m.def("set_fake_prefix", &SocketEventHandle::set_fake_prefix, nb::arg("prefix"));
m.def("get_fake_prefix", &SocketEventHandle::fake_prefix);
m.def("delete_fake_prefix", [] { SocketEventHandle::set_fake_prefix(""); });
m.def("wait_for_one_event", &Event::wait_for_one, nb::arg("events"), nb::arg("timeout") = -1, nb::call_guard<nb::gil_scoped_release>());
nb::class_<Event>(m, "Event")
.def(nb::init<>())
.def("set", &Event::set)
.def("clear", &Event::clear)
.def("wait", &Event::wait, nb::arg("timeout") = -1, nb::call_guard<nb::gil_scoped_release>())
.def("peek", &Event::peek)
.def_prop_ro("fd", &Event::fd)
.def_prop_ro("ptr", [](Event &event) { return reinterpret_cast<uintptr_t>(&event); });
nb::class_<SocketEventHandle>(m, "SocketEventHandle")
.def(nb::init<std::string, std::string, bool>(), nb::arg("endpoint"), nb::arg("identifier"), nb::arg("override"))
.def_prop_rw("enabled", &SocketEventHandle::is_enabled, &SocketEventHandle::set_enabled)
.def_prop_ro("recv_called_event", &SocketEventHandle::recv_called, nb::keep_alive<0, 1>())
.def_prop_ro("recv_ready_event", &SocketEventHandle::recv_ready, nb::keep_alive<0, 1>());
nb::class_<SubSocket>(m, "SubSocket")
.def(nb::init<>())
.def("connect", [](SubSocket &socket, const std::string &name, bool conflate, size_t segment_size) {
nb::gil_scoped_release release;
int result = socket.connect(name, conflate, segment_size);
check_error(result != 0, errno, name);
}, nb::arg("endpoint"), nb::arg("conflate") = false, nb::arg("segment_size") = 0)
.def("setTimeout", &SubSocket::setTimeout, nb::arg("timeout"), nb::call_guard<nb::gil_scoped_release>())
.def("receive", [](SubSocket &socket, bool non_blocking) -> std::optional<nb::bytes> {
std::unique_ptr<Message> message;
{
nb::gil_scoped_release release;
message.reset(socket.receive(non_blocking));
}
if (!message) return std::nullopt;
return nb::bytes(message->getData(), message->getSize());
}, nb::arg("non_blocking") = false);
nb::class_<Poller>(m, "Poller")
.def(nb::init<>())
.def("registerSocket", &Poller::registerSocket, nb::arg("socket"), nb::keep_alive<1, 2>())
.def("poll", &Poller::poll, nb::arg("timeout"), nb::rv_policy::reference, nb::call_guard<nb::gil_scoped_release>());
nb::class_<PubSocket>(m, "PubSocket")
.def(nb::init<>())
.def("connect", [](PubSocket &socket, const std::string &name, size_t segment_size) {
int result = socket.connect(name, segment_size);
check_error(result != 0, errno, name);
}, nb::arg("endpoint"), nb::arg("segment_size") = 0)
.def("send", [](PubSocket &socket, nb::bytes data) {
int result = socket.send(const_cast<char *>(data.c_str()), data.size());
check_error(result != static_cast<int>(data.size()), errno);
}, nb::arg("data"))
.def("all_readers_updated", &PubSocket::all_readers_updated)
.def("wait_for_readers", [](PubSocket &socket, double timeout, double interval) {
// Python's sleep releases the GIL and handles signals while waiting.
nb::object sleep = nb::module_::import_("time").attr("sleep");
auto deadline = std::chrono::steady_clock::now() + std::chrono::duration<double>(timeout);
while (std::chrono::steady_clock::now() < deadline) {
if (socket.all_readers_updated()) return;
sleep(interval);
}
PyErr_SetString(PyExc_TimeoutError, "subscriber did not connect");
throw nb::python_error();
}, nb::arg("timeout") = 1.0, nb::arg("interval") = 0.001);
}
-230
View File
@@ -1,230 +0,0 @@
# distutils: language = c++
# cython: c_string_encoding=ascii, language_level=3
import time
from libcpp.string cimport string
from libcpp.vector cimport vector
from libcpp cimport bool
from libc cimport errno
from libc.string cimport strerror
from cython.operator import dereference
from .ipc cimport SubSocket as cppSubSocket
from .ipc cimport PubSocket as cppPubSocket
from .ipc cimport Poller as cppPoller
from .ipc cimport Message as cppMessage
from .ipc cimport Event as cppEvent, SocketEventHandle as cppSocketEventHandle
class IpcError(Exception):
def __init__(self, endpoint=None):
suffix = f"with {endpoint.decode('utf-8')}" if endpoint else ""
message = f"Messaging failure {suffix}: {strerror(errno.errno).decode('utf-8')}"
super().__init__(message)
class MultiplePublishersError(IpcError):
pass
def toggle_fake_events(bool enabled):
cppSocketEventHandle.toggle_fake_events(enabled)
def set_fake_prefix(string prefix):
cppSocketEventHandle.set_fake_prefix(prefix)
def get_fake_prefix():
return cppSocketEventHandle.fake_prefix()
def delete_fake_prefix():
cppSocketEventHandle.set_fake_prefix(b"")
def wait_for_one_event(list events, int timeout=-1):
cdef vector[cppEvent] items
for event in events:
items.push_back(dereference(<cppEvent*><size_t>event.ptr))
return cppEvent.wait_for_one(items, timeout)
cdef class Event:
cdef cppEvent event;
cdef setEvent(self, cppEvent event):
self.event = event
def set(self):
self.event.set()
def clear(self):
return self.event.clear()
def wait(self, int timeout=-1):
self.event.wait(timeout)
def peek(self):
return self.event.peek()
@property
def fd(self):
return self.event.fd()
@property
def ptr(self):
return <size_t><void*>&self.event
cdef class SocketEventHandle:
cdef cppSocketEventHandle * handle;
def __cinit__(self, string endpoint, string identifier, bool override):
self.handle = new cppSocketEventHandle(endpoint, identifier, override)
def __dealloc__(self):
del self.handle
@property
def enabled(self):
return self.handle.is_enabled()
@enabled.setter
def enabled(self, bool value):
self.handle.set_enabled(value)
@property
def recv_called_event(self):
e = Event()
e.setEvent(self.handle.recv_called())
return e
@property
def recv_ready_event(self):
e = Event()
e.setEvent(self.handle.recv_ready())
return e
cdef class Poller:
cdef cppPoller * poller
cdef list sub_sockets
def __cinit__(self):
self.sub_sockets = []
self.poller = new cppPoller()
def __dealloc__(self):
del self.poller
def registerSocket(self, SubSocket socket):
self.sub_sockets.append(socket)
self.poller.registerSocket(socket.socket)
def poll(self, timeout):
sockets = []
cdef int t = timeout
with nogil:
result = self.poller.poll(t)
for s in result:
socket = SubSocket()
socket.setPtr(s)
sockets.append(socket)
return sockets
cdef class SubSocket:
cdef cppSubSocket * socket
cdef bool is_owner
def __cinit__(self):
with nogil:
self.socket = new cppSubSocket()
self.is_owner = True
def __dealloc__(self):
if self.is_owner:
with nogil:
del self.socket
cdef setPtr(self, cppSubSocket * ptr):
if self.is_owner:
with nogil:
del self.socket
self.is_owner = False
self.socket = ptr
def connect(self, string endpoint, bool conflate=False, size_t segment_size=0):
cdef int r
with nogil:
r = self.socket.connect(endpoint, conflate, segment_size)
if r != 0:
if errno.errno == errno.EADDRINUSE:
raise MultiplePublishersError(endpoint)
else:
raise IpcError(endpoint)
def setTimeout(self, int timeout):
with nogil:
self.socket.setTimeout(timeout)
def receive(self, bool non_blocking=False):
cdef cppMessage *msg
with nogil:
msg = self.socket.receive(non_blocking)
if msg == NULL:
return None
else:
sz = msg.getSize()
m = msg.getData()[:sz]
with nogil:
del msg
return m
cdef class PubSocket:
cdef cppPubSocket * socket
def __cinit__(self):
self.socket = new cppPubSocket()
def __dealloc__(self):
del self.socket
def connect(self, string endpoint, size_t segment_size=0):
r = self.socket.connect(endpoint, segment_size)
if r != 0:
if errno.errno == errno.EADDRINUSE:
raise MultiplePublishersError(endpoint)
else:
raise IpcError(endpoint)
def send(self, bytes data):
length = len(data)
r = self.socket.send(<char*>data, length)
if r != length:
if errno.errno == errno.EADDRINUSE:
raise MultiplePublishersError
else:
raise IpcError
def all_readers_updated(self):
return self.socket.all_readers_updated()
def wait_for_readers(self, double timeout=1.0, double interval=0.001):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if self.all_readers_updated():
return
time.sleep(interval)
raise TimeoutError("subscriber did not connect")
Binary file not shown.
+95
View File
@@ -569,3 +569,98 @@ TEST_CASE("32 active subscribers retain every message across publisher restart")
msgq_close_queue(&pub);
std::remove(file.c_str());
}
static int64_t millis_since(std::chrono::steady_clock::time_point start)
{
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count();
}
static std::thread send_later(msgq_queue_t *q, int delay_ms)
{
return std::thread([=]() {
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
uint64_t value = 1;
msgq_msg_t msg;
msgq_msg_init_data(&msg, (char *)&value, sizeof(value));
msgq_msg_send(&msg, q);
msgq_msg_close(&msg);
});
}
TEST_CASE("msgq_poll times out")
{
cleanup_test_queue();
msgq_queue_t writer, reader;
msgq_new_queue(&writer, "test_queue", 1024);
msgq_new_queue(&reader, "test_queue", 1024);
msgq_init_publisher(&writer);
msgq_init_subscriber(&reader);
msgq_pollitem_t items[] = {{&reader, 0}};
auto start = std::chrono::steady_clock::now();
REQUIRE(msgq_poll(items, 1, 100) == 0);
REQUIRE(millis_since(start) >= 100);
REQUIRE(msgq_poll(items, 1, 0) == 0);
send_later(&writer, 0).join();
REQUIRE(msgq_poll(items, 1, 0) == 1);
msgq_close_queue(&writer);
msgq_close_queue(&reader);
}
TEST_CASE("msgq_poll wakes up on send")
{
cleanup_test_queue();
msgq_queue_t writer, reader;
msgq_new_queue(&writer, "test_queue", 1024);
msgq_new_queue(&reader, "test_queue", 1024);
msgq_init_publisher(&writer);
msgq_init_subscriber(&reader);
msgq_pollitem_t items[] = {{&reader, 0}};
std::thread sender = send_later(&writer, 50);
auto start = std::chrono::steady_clock::now();
int n = msgq_poll(items, 1, 5000);
int64_t elapsed = millis_since(start);
sender.join();
REQUIRE(n == 1);
REQUIRE(elapsed < 1000);
msgq_close_queue(&writer);
msgq_close_queue(&reader);
}
TEST_CASE("msgq_poll multiple queues")
{
cleanup_test_queue();
remove("/dev/shm/msgq_test_queue2");
remove("/tmp/msgq_test_queue2");
msgq_queue_t writer1, writer2, reader1, reader2;
msgq_new_queue(&writer1, "test_queue", 1024);
msgq_new_queue(&writer2, "test_queue2", 1024);
msgq_new_queue(&reader1, "test_queue", 1024);
msgq_new_queue(&reader2, "test_queue2", 1024);
msgq_init_publisher(&writer1);
msgq_init_publisher(&writer2);
msgq_init_subscriber(&reader1);
msgq_init_subscriber(&reader2);
msgq_pollitem_t items[] = {{&reader1, 0}, {&reader2, 0}};
std::thread sender = send_later(&writer2, 50);
auto start = std::chrono::steady_clock::now();
int n = msgq_poll(items, 2, 5000);
int64_t elapsed = millis_since(start);
sender.join();
REQUIRE(n == 1);
REQUIRE(elapsed < 1000);
REQUIRE(items[0].revents == 0);
REQUIRE(items[1].revents == 1);
msgq_close_queue(&writer1);
msgq_close_queue(&writer2);
msgq_close_queue(&reader1);
msgq_close_queue(&reader2);
}
View File
-2
View File
@@ -1,2 +0,0 @@
visionipc_pyx.cpp
*.so
+1 -1
View File
@@ -1,4 +1,4 @@
from msgq.visionipc.visionipc_pyx import VisionBuf, VisionIpcClient, VisionIpcServer, get_endpoint_name
from msgq.visionipc._visionipc import VisionBuf, VisionIpcClient, VisionIpcServer, get_endpoint_name
__all__ = [
"VisionBuf",
Binary file not shown.
+106 -55
View File
@@ -1,6 +1,7 @@
import gc
import uuid
import struct
import unittest
import uuid
from typing import Optional
from msgq.visionipc import VisionIpcServer, VisionIpcClient
@@ -10,7 +11,6 @@ class TestVisionIpc(unittest.TestCase):
client: Optional[VisionIpcClient]
def setUp(self):
# Each test needs independent listener and publisher endpoints in parallel runs.
self.name = f"test_{uuid.uuid4().hex}"
self.server = None
self.client = None
@@ -19,88 +19,139 @@ class TestVisionIpc(unittest.TestCase):
self.client = None
self.server = None
def setup_vipc(self, *stream_types, num_buffers=1, width=100, height=100, conflate=False):
self.server = VisionIpcServer(self.name)
for stream_type in stream_types:
self.server.create_buffers(stream_type, num_buffers, width, height)
self.server.start_listener()
def setup_vipc(
self, name: str, stream_type: int, *other_stream_types: int,
num_buffers: int = 1, width: int = 100, height: int = 100, conflate: bool = False,
) -> tuple[VisionIpcServer, VisionIpcClient, int]:
self.server = server = VisionIpcServer(self.name)
for stream in (stream_type, *other_stream_types):
server.create_buffers(stream, num_buffers, width, height)
server.start_listener()
if len(stream_types):
self.client = VisionIpcClient(self.name, stream_types[0], conflate)
assert self.client.connect(True)
else:
self.client = None
return self.server, self.client
self.client = client = VisionIpcClient(self.name, stream_type, conflate)
assert client.connect(True)
buffer_len = client.buffer_len
assert buffer_len is not None
return server, client, buffer_len
def test_connect(self):
self.setup_vipc(0)
assert self.client is not None
assert self.client.is_connected()
_, client, _ = self.setup_vipc("camerad", 0)
assert client.is_connected()
def test_available_streams(self):
stream_types = (0, 2)
self.setup_vipc(*stream_types)
self.setup_vipc("camerad", *stream_types)
available_streams = VisionIpcClient.available_streams(self.name, True)
assert available_streams == set(stream_types)
def test_buffers(self):
width, height, num_buffers = 100, 200, 5
self.setup_vipc(0, num_buffers=num_buffers, width=width, height=height)
assert self.client is not None
assert self.client.width == width
assert self.client.height == height
assert self.client.buffer_len is not None and self.client.buffer_len > 0
assert self.client.num_buffers == num_buffers
_, client, buffer_len = self.setup_vipc("camerad", 0, num_buffers=num_buffers, width=width, height=height)
assert client.width == width
assert client.height == height
assert buffer_len > 0
assert client.num_buffers == num_buffers
def test_send_single_buffer(self):
self.setup_vipc(0)
assert self.server is not None
assert self.client is not None
assert self.client.buffer_len is not None
buf = bytearray(self.client.buffer_len)
server, client, buffer_len = self.setup_vipc("camerad", 0)
buf = bytearray(buffer_len)
struct.pack_into("<Q", buf, 0, 1234)
self.server.send(0, buf, frame_id=1337)
server.send(0, buf, frame_id=1337)
recv_buf = self.client.recv()
recv_buf = client.recv()
assert recv_buf is not None
data = recv_buf.data
assert isinstance(data, memoryview)
assert struct.unpack_from("<Q", data, 0)[0] == 1234
assert len(data) == self.client.buffer_len
assert data[8:].nbytes == self.client.buffer_len - 8
assert self.client.frame_id == 1337
assert len(data) == buffer_len
assert data[8:].nbytes == buffer_len - 8
assert client.frame_id == 1337
assert recv_buf.frame_id == 1337
def test_no_conflate(self):
self.setup_vipc(0)
assert self.server is not None
assert self.client is not None
assert self.client.buffer_len is not None
buf = bytearray(self.client.buffer_len)
self.server.send(0, buf, frame_id=1)
self.server.send(0, buf, frame_id=2)
server, client, buffer_len = self.setup_vipc("camerad", 0)
buf = bytearray(buffer_len)
server.send(0, buf, frame_id=1)
server.send(0, buf, frame_id=2)
recv_buf = self.client.recv()
recv_buf = client.recv()
assert recv_buf is not None
assert self.client.frame_id == 1
assert client.frame_id == 1
recv_buf = self.client.recv()
recv_buf = client.recv()
assert recv_buf is not None
assert self.client.frame_id == 2
assert client.frame_id == 2
def test_conflate(self):
self.setup_vipc(0, conflate=True)
assert self.server is not None
assert self.client is not None
assert self.client.buffer_len is not None
buf = bytearray(self.client.buffer_len)
self.server.send(0, buf, frame_id=1)
self.server.send(0, buf, frame_id=2)
server, client, buffer_len = self.setup_vipc("camerad", 0, conflate=True)
buf = bytearray(buffer_len)
server.send(0, buf, frame_id=1)
server.send(0, buf, frame_id=2)
recv_buf = self.client.recv()
recv_buf = client.recv()
assert recv_buf is not None
assert self.client.frame_id == 2
assert client.frame_id == 2
recv_buf = self.client.recv(timeout_ms=5)
recv_buf = client.recv(timeout_ms=5)
assert recv_buf is None
def test_send_buffer_types(self):
server, client, buffer_len = self.setup_vipc("camerad", 0)
data = bytes(i % 256 for i in range(buffer_len))
strided = bytearray(len(data) * 2)
strided[::2] = data
for payload in (data, bytearray(data), memoryview(data), memoryview(strided)[::2], memoryview(data[::-1])[::-1]):
with self.subTest(type=type(payload)):
server.send(0, payload, frame_id=42, timestamp_sof=123, timestamp_eof=456)
buf = client.recv()
assert buf is not None
self.assertEqual(bytes(buf.data), data)
self.assertEqual(buf.frame_id, 42)
self.assertEqual(client.timestamp_sof, 123)
self.assertEqual(client.timestamp_eof, 456)
self.assertFalse(client.valid)
def test_send_wrong_size(self):
server, _, _ = self.setup_vipc("camerad", 0)
with self.assertRaises(AssertionError):
server.send(0, b"short")
def test_buffer_keeps_client_alive(self):
server, client, buffer_len = self.setup_vipc("camerad", 0)
data = b"a" * buffer_len
server.send(0, data, frame_id=42)
buf = client.recv()
assert buf is not None
self.client = None
del client
gc.collect()
self.assertEqual(buf.frame_id, 42)
view = buf.data
del buf
gc.collect()
self.assertEqual(bytes(view), data)
self.assertFalse(view.readonly)
view[0] = ord("b")
self.assertEqual(view[0], ord("b"))
def test_unconnected_properties(self):
client = VisionIpcClient("unconnected", 0, False)
for value in (client.width, client.height, client.stride, client.uv_offset, client.buffer_len):
self.assertIsNone(value)
self.assertEqual(client.num_buffers, 0)
self.assertFalse(client.is_connected())
self.assertFalse(client.connect(False))
self.assertIsNone(client.recv(timeout_ms=0))
def test_exact_uint64_timestamps_survive_receive_and_timeout(self):
server, client, size = self.setup_vipc("clock", 0, width=32, height=18)
payload = bytes(size)
for frame, sof, eof in ((0, 0, 0), (1, 2**53 + 1, 2**53 + 19),
(2**32 - 1, 2**63 + 17, 2**64 - 1)):
with self.subTest(frame=frame, sof=sof, eof=eof):
server.send(0, payload, frame_id=frame, timestamp_sof=sof, timestamp_eof=eof)
buffer = client.recv(timeout_ms=1000)
assert buffer is not None
assert (client.frame_id, client.timestamp_sof, client.timestamp_eof) == (frame, sof, eof)
assert client.recv(timeout_ms=0) is None
assert (client.frame_id, client.timestamp_sof, client.timestamp_eof) == (frame, sof, eof)
-52
View File
@@ -1,52 +0,0 @@
# distutils: language = c++
#cython: language_level=3
from libcpp.string cimport string
from libcpp.vector cimport vector
from libcpp.set cimport set
from libc.stdint cimport uint32_t, uint64_t
from libcpp cimport bool, int
cdef extern from "msgq/visionipc/visionbuf.h":
ctypedef uint32_t VisionStreamType "VisionStreamType"
cdef cppclass VisionBuf:
void * addr
int fd
size_t len
size_t width
size_t height
size_t stride
size_t uv_offset
size_t idx
void set_frame_id(uint64_t id)
uint64_t get_frame_id()
cdef extern from "msgq/visionipc/visionipc.h":
struct VisionIpcBufExtra:
uint32_t frame_id
uint64_t timestamp_sof
uint64_t timestamp_eof
bool valid
cdef extern from "msgq/visionipc/visionipc_server.h":
string get_endpoint_name(string, VisionStreamType)
cdef cppclass VisionIpcServer:
VisionIpcServer(string)
void create_buffers(VisionStreamType, size_t, size_t, size_t)
void create_buffers_with_sizes(VisionStreamType, size_t, size_t, size_t, size_t, size_t, size_t)
VisionBuf * get_buffer(VisionStreamType)
void send(VisionBuf *, VisionIpcBufExtra *, bool)
void start_listener()
cdef extern from "msgq/visionipc/visionipc_client.h":
cdef cppclass VisionIpcClient:
int num_buffers
VisionBuf buffers[1]
VisionIpcClient(string, VisionStreamType, bool)
VisionBuf * recv(VisionIpcBufExtra *, int) nogil
bool connect(bool)
bool is_connected()
@staticmethod
set[VisionStreamType] getAvailableStreams(string, bool)
+109
View File
@@ -0,0 +1,109 @@
#include <cstdint>
#include <cstring>
#include <optional>
#include <string>
#include <nanobind/nanobind.h>
#include <nanobind/ndarray.h>
#include <nanobind/stl/optional.h>
#include <nanobind/stl/set.h>
#include <nanobind/stl/string.h>
#include "msgq/visionipc/visionbuf.h"
#include "msgq/visionipc/visionipc.h"
#include "msgq/visionipc/visionipc_client.h"
#include "msgq/visionipc/visionipc_server.h"
namespace nb = nanobind;
namespace {
struct PythonVisionIpcClient : VisionIpcClient {
using VisionIpcClient::VisionIpcClient;
VisionIpcBufExtra extra = {};
};
using Buffer = nb::ndarray<nb::memview, uint8_t, nb::ndim<1>, nb::device::cpu>;
using InputBuffer = nb::ndarray<const uint8_t, nb::ndim<1>, nb::device::cpu>;
} // namespace
NB_MODULE(_visionipc, m) {
// Daemon threads can still hold clients and servers at exit.
nb::set_leak_warnings(false);
m.def("get_endpoint_name", &get_endpoint_name, nb::arg("name"), nb::arg("stream"));
nb::class_<VisionBuf>(m, "VisionBuf")
.def_prop_ro("data", [](VisionBuf &buf) { return Buffer(buf.addr, {buf.len}); }, nb::rv_policy::reference_internal,
nb::sig("def data(self) -> memoryview"))
.def_ro("width", &VisionBuf::width)
.def_ro("height", &VisionBuf::height)
.def_ro("stride", &VisionBuf::stride)
.def_ro("uv_offset", &VisionBuf::uv_offset)
.def_ro("idx", &VisionBuf::idx)
.def_ro("fd", &VisionBuf::fd)
.def_prop_ro("frame_id", &VisionBuf::get_frame_id);
nb::class_<VisionIpcServer>(m, "VisionIpcServer")
.def(nb::init<std::string>(), nb::arg("name"))
.def("create_buffers", &VisionIpcServer::create_buffers, nb::arg("tp"), nb::arg("num_buffers"), nb::arg("width"), nb::arg("height"))
.def("create_buffers_with_sizes", &VisionIpcServer::create_buffers_with_sizes,
nb::arg("tp"), nb::arg("num_buffers"), nb::arg("width"), nb::arg("height"), nb::arg("size"), nb::arg("stride"), nb::arg("uv_offset"))
.def("send", [](VisionIpcServer &server, VisionStreamType tp, InputBuffer data,
uint32_t frame_id, uint64_t timestamp_sof, uint64_t timestamp_eof) {
VisionBuf *buf = server.get_buffer(tp);
if (buf->len != data.size()) {
PyErr_SetString(PyExc_AssertionError, "buffer size does not match frame size");
throw nb::python_error();
}
// The old binding accepted strided byte views as well as contiguous data.
auto *source = data.data();
if (data.stride(0) == 1) {
std::memcpy(buf->addr, source, data.size());
} else {
auto *target = static_cast<uint8_t *>(buf->addr);
for (size_t i = 0; i < data.size(); ++i) target[i] = source[static_cast<int64_t>(i) * data.stride(0)];
}
buf->set_frame_id(frame_id);
VisionIpcBufExtra extra = {frame_id, timestamp_sof, timestamp_eof, false};
server.send(buf, &extra, false);
}, nb::arg("tp"), nb::arg("data").noconvert(), nb::arg("frame_id") = 0, nb::arg("timestamp_sof") = 0, nb::arg("timestamp_eof") = 0,
nb::sig("def send(self, tp: int, data: bytes | bytearray | memoryview, frame_id: int = 0, timestamp_sof: int = 0, timestamp_eof: int = 0) -> None"))
.def("start_listener", &VisionIpcServer::start_listener);
nb::class_<PythonVisionIpcClient>(m, "VisionIpcClient")
.def(nb::init<std::string, VisionStreamType, bool>(), nb::arg("name"), nb::arg("stream"), nb::arg("conflate"))
.def_prop_ro("width", [](PythonVisionIpcClient &client) -> std::optional<size_t> {
return client.num_buffers ? std::make_optional(client.buffers[0].width) : std::nullopt;
})
.def_prop_ro("height", [](PythonVisionIpcClient &client) -> std::optional<size_t> {
return client.num_buffers ? std::make_optional(client.buffers[0].height) : std::nullopt;
})
.def_prop_ro("stride", [](PythonVisionIpcClient &client) -> std::optional<size_t> {
return client.num_buffers ? std::make_optional(client.buffers[0].stride) : std::nullopt;
})
.def_prop_ro("uv_offset", [](PythonVisionIpcClient &client) -> std::optional<size_t> {
return client.num_buffers ? std::make_optional(client.buffers[0].uv_offset) : std::nullopt;
})
.def_prop_ro("buffer_len", [](PythonVisionIpcClient &client) -> std::optional<size_t> {
return client.num_buffers ? std::make_optional(client.buffers[0].len) : std::nullopt;
})
.def_prop_ro("num_buffers", [](PythonVisionIpcClient &client) { return client.num_buffers; })
.def_prop_ro("frame_id", [](PythonVisionIpcClient &client) { return client.extra.frame_id; })
.def_prop_ro("timestamp_sof", [](PythonVisionIpcClient &client) { return client.extra.timestamp_sof; })
.def_prop_ro("timestamp_eof", [](PythonVisionIpcClient &client) { return client.extra.timestamp_eof; })
.def_prop_ro("valid", [](PythonVisionIpcClient &client) { return client.extra.valid; })
.def("recv", [](PythonVisionIpcClient &client, int timeout_ms) {
nb::gil_scoped_release release;
return client.recv(&client.extra, timeout_ms);
}, nb::arg("timeout_ms") = 100, nb::rv_policy::reference_internal,
nb::sig("def recv(self, timeout_ms: int = 100) -> VisionBuf | None"))
// Holds the GIL: connect remaps the buffers other threads may be reading.
.def("connect", [](PythonVisionIpcClient &client, bool blocking) {
return client.connect(blocking);
}, nb::arg("blocking"))
.def("is_connected", [](PythonVisionIpcClient &client) { return client.is_connected(); })
.def_static("available_streams", &VisionIpcClient::getAvailableStreams, nb::arg("name"), nb::arg("block"),
nb::call_guard<nb::gil_scoped_release>());
}
@@ -1,10 +0,0 @@
# distutils: language = c++
#cython: language_level=3
from .visionipc cimport VisionBuf as cppVisionBuf
cdef class VisionBuf:
cdef cppVisionBuf * buf
@staticmethod
cdef create(cppVisionBuf*)
-163
View File
@@ -1,163 +0,0 @@
# distutils: language = c++
# cython: c_string_encoding=ascii, language_level=3
from libc.string cimport memcpy
from libc.stdint cimport uint32_t, uint64_t
from libcpp cimport bool
from libcpp.string cimport string
from .visionipc cimport VisionIpcServer as cppVisionIpcServer
from .visionipc cimport VisionIpcClient as cppVisionIpcClient
from .visionipc cimport VisionBuf as cppVisionBuf
from .visionipc cimport VisionIpcBufExtra, VisionStreamType
from .visionipc cimport get_endpoint_name as cpp_get_endpoint_name
def get_endpoint_name(string name, VisionStreamType stream):
return cpp_get_endpoint_name(name, stream).decode('utf-8')
cdef class VisionBuf:
@staticmethod
cdef create(cppVisionBuf * cbuf):
buf = VisionBuf()
buf.buf = cbuf
return buf
@property
def data(self):
cdef unsigned char[:] data = <unsigned char[:self.buf.len]> self.buf.addr
return memoryview(data)
@property
def width(self):
return self.buf.width
@property
def height(self):
return self.buf.height
@property
def stride(self):
return self.buf.stride
@property
def uv_offset(self):
return self.buf.uv_offset
@property
def idx(self):
return self.buf.idx
@property
def fd(self):
return self.buf.fd
@property
def frame_id(self):
return self.buf.get_frame_id()
cdef class VisionIpcServer:
cdef cppVisionIpcServer * server
def __init__(self, string name):
self.server = new cppVisionIpcServer(name)
def create_buffers(self, VisionStreamType tp, size_t num_buffers, size_t width, size_t height):
self.server.create_buffers(tp, num_buffers, width, height)
def create_buffers_with_sizes(self, VisionStreamType tp, size_t num_buffers, size_t width, size_t height, size_t size, size_t stride, size_t uv_offset):
self.server.create_buffers_with_sizes(tp, num_buffers, width, height, size, stride, uv_offset)
def send(self, VisionStreamType tp, const unsigned char[:] data, uint32_t frame_id=0, uint64_t timestamp_sof=0, uint64_t timestamp_eof=0):
cdef cppVisionBuf * buf = self.server.get_buffer(tp)
# Populate buffer
assert buf.len == len(data)
memcpy(buf.addr, &data[0], len(data))
buf.set_frame_id(frame_id)
cdef VisionIpcBufExtra extra
extra.frame_id = frame_id
extra.timestamp_sof = timestamp_sof
extra.timestamp_eof = timestamp_eof
extra.valid = False
self.server.send(buf, &extra, False)
def start_listener(self):
self.server.start_listener()
def __dealloc__(self):
del self.server
cdef class VisionIpcClient:
cdef cppVisionIpcClient * client
cdef VisionIpcBufExtra extra
def __cinit__(self, string name, VisionStreamType stream, bool conflate):
self.client = new cppVisionIpcClient(name, stream, conflate)
def __dealloc__(self):
del self.client
@property
def width(self):
return self.client.buffers[0].width if self.client.num_buffers else None
@property
def height(self):
return self.client.buffers[0].height if self.client.num_buffers else None
@property
def stride(self):
return self.client.buffers[0].stride if self.client.num_buffers else None
@property
def uv_offset(self):
return self.client.buffers[0].uv_offset if self.client.num_buffers else None
@property
def buffer_len(self):
return self.client.buffers[0].len if self.client.num_buffers else None
@property
def num_buffers(self):
return self.client.num_buffers
@property
def frame_id(self):
return self.extra.frame_id
@property
def timestamp_sof(self):
return self.extra.timestamp_sof
@property
def timestamp_eof(self):
return self.extra.timestamp_eof
@property
def valid(self):
return self.extra.valid
def recv(self, int timeout_ms=100):
cdef cppVisionBuf * buf
# release the GIL, this can block for timeout_ms
with nogil:
buf = self.client.recv(&self.extra, timeout_ms)
if not buf:
return None
return VisionBuf.create(buf)
def connect(self, bool blocking):
return self.client.connect(blocking)
def is_connected(self):
return self.client.is_connected()
@staticmethod
def available_streams(string name, bool block):
return cppVisionIpcClient.getAvailableStreams(name, block)
Binary file not shown.
+13 -13
View File
@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools>=77", "Cython>=3.1", "scons"]
requires = ["setuptools>=77", "nanobind>=2.12", "scons"]
build-backend = "setuptools.build_meta"
[project]
@@ -21,10 +21,14 @@ dependencies = []
Repository = "https://github.com/commaai/msgq"
[project.optional-dependencies]
dev = [
"setuptools", # for distutils
"Cython",
# for building from source with SCons
build = [
"nanobind>=2.12",
"scons",
]
dev = [
"msgq-ipc[build]",
"setuptools", # for distutils
"ruff",
"cppcheck",
"cpplint",
@@ -41,13 +45,16 @@ include = ["msgq", "msgq.*"]
namespaces = false
[tool.setuptools.package-data]
"msgq" = ["*.pxd", "*.h"]
"msgq.visionipc" = ["*.pxd", "*.h"]
"msgq" = ["*.pyi", "py.typed", "*.h"]
"msgq.visionipc" = ["*.pyi", "*.h"]
[tool.cibuildwheel]
build = "cp311-* cp312-* cp313-* cp314-*"
test-command = "python -m unittest discover -s msgq"
[tool.cibuildwheel.macos.environment]
MACOSX_DEPLOYMENT_TARGET = "10.13"
# https://beta.ruff.rs/docs/configuration/#using-pyprojecttoml
[tool.ruff]
lint.select = ["E", "F", "W", "PIE", "C4", "ISC", "RUF100", "A"]
@@ -56,10 +63,3 @@ lint.flake8-implicit-str-concat.allow-multiline=false
line-length = 160
target-version="py311"
[tool.ty.src]
exclude = ["site_scons/"]
[tool.ty.rules]
# Cython modules are compiled at build time, not available for static analysis
unresolved-import = "ignore"
+7 -3
View File
@@ -2,6 +2,7 @@ import os
from pathlib import Path
import subprocess
import sys
import sysconfig
from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext
@@ -11,16 +12,19 @@ class SConsBuildExt(build_ext):
def build_extensions(self):
subprocess.check_call([sys.executable, "-m", "SCons", "--minimal", "-j", str(self.parallel or os.cpu_count() or 1)])
for ext in self.extensions:
source = Path(*ext.name.split(".")).with_suffix(".so")
module = Path(*ext.name.split("."))
source = module.with_suffix(sysconfig.get_config_var("EXT_SUFFIX"))
target = Path(self.get_ext_fullpath(ext.name))
target.parent.mkdir(parents=True, exist_ok=True)
self.copy_file(str(source), str(target))
stub = module.with_suffix(".pyi")
self.copy_file(str(stub), str(target.parent / stub.name))
setup(
ext_modules=[
Extension("msgq.ipc_pyx", sources=[]),
Extension("msgq.visionipc.visionipc_pyx", sources=[]),
Extension("msgq._ipc", sources=[]),
Extension("msgq.visionipc._visionipc", sources=[]),
],
cmdclass={"build_ext": SConsBuildExt},
)
-72
View File
@@ -1,72 +0,0 @@
import re
import SCons
from SCons.Action import Action
from SCons.Scanner import Scanner
pyx_from_import_re = re.compile(r'^from\s+(\S+)\s+cimport', re.M)
pyx_import_re = re.compile(r'^cimport\s+(\S+)', re.M)
cdef_import_re = re.compile(r'^cdef extern from\s+.(\S+).:', re.M)
def pyx_scan(node, env, path, arg=None):
contents = node.get_text_contents()
# from <module> cimport ...
matches = pyx_from_import_re.findall(contents)
# cimport <module>
matches += pyx_import_re.findall(contents)
# Modules can be either .pxd or .pyx files
files = [m.replace('.', '/') + '.pxd' for m in matches]
files += [m.replace('.', '/') + '.pyx' for m in matches]
# cdef extern from <file>
files += cdef_import_re.findall(contents)
# Handle relative imports
cur_dir = str(node.get_dir())
files = [cur_dir + f if f.startswith('/') else f for f in files]
# Filter out non-existing files (probably system imports)
files = [f for f in files if env.File(f).exists()]
return env.File(files)
pyxscanner = Scanner(function=pyx_scan, skeys=['.pyx', '.pxd'], recursive=True)
cythonAction = Action("$CYTHONCOM")
def create_builder(env):
try:
cython = env['BUILDERS']['Cython']
except KeyError:
cython = SCons.Builder.Builder(
action=cythonAction,
emitter={},
suffix=cython_suffix_emitter,
single_source=1
)
env.Append(SCANNERS=pyxscanner)
env['BUILDERS']['Cython'] = cython
return cython
def cython_suffix_emitter(env, source):
return "$CYTHONCFILESUFFIX"
def generate(env):
env["CYTHON"] = "cythonize"
env["CYTHONCOM"] = "$CYTHON $CYTHONFLAGS $SOURCE"
env["CYTHONCFILESUFFIX"] = ".cpp"
c_file, _ = SCons.Tool.createCFileBuilders(env)
c_file.suffix['.pyx'] = cython_suffix_emitter
c_file.add_action('.pyx', cythonAction)
c_file.suffix['.py'] = cython_suffix_emitter
c_file.add_action('.py', cythonAction)
create_builder(env)
def exists(env):
return True
+2 -2
View File
@@ -18,8 +18,8 @@ lefthook run test
trap 'rm -rf "$TEST_DIR"' EXIT
uv venv --python "$DIR/.venv/bin/python" "$TEST_DIR/.venv"
uv pip install --python "$TEST_DIR/.venv/bin/python" "$DIR"
cd "$TEST_DIR"
"$TEST_DIR/.venv/bin/python" -m unittest msgq.tests.test_messaging
cd "$("$TEST_DIR/.venv/bin/python" -c "import sysconfig; print(sysconfig.get_path('purelib'))")"
"$TEST_DIR/.venv/bin/python" -m unittest discover -t . -s msgq
)
# *** all done ***
+1 -1
View File
@@ -30,7 +30,7 @@ body:
attributes:
label: Provide a route where the issue occurs
description: Ensure the route is fully uploaded at https://useradmin.comma.ai. We cannot look into issues without routes, or at least a Dongle ID.
placeholder: 77611a1fac303767|2020-05-11--16-37-07
placeholder: 77611a1fac303767/2020-05-11--16-37-07
validations:
required: true
+1 -1
View File
@@ -22,7 +22,7 @@ body:
attributes:
label: Provide a route where the issue occurs if applicable
description: Ensure the route is fully uploaded at https://useradmin.comma.ai
placeholder: 77611a1fac303767|2020-05-11--16-37-07
placeholder: 77611a1fac303767/2020-05-11--16-37-07
- type: input
id: version
+4
View File
@@ -109,6 +109,10 @@ Start off by recording a route with lots of interesting events: enable LKAS and
Use the [longitudinal maneuvers](https://github.com/commaai/openpilot/tree/master/tools/longitudinal_maneuvers) report to evaluate your car's longitudinal control and tune it.
### Validation
Before submitting a port, go through the [car porting checklist](docs/CAR_PORTING_CHECKLIST.md).
## Contributing
All opendbc development is coordinated on GitHub and [Discord](https://discord.comma.ai). Check out the `#dev-opendbc-cars` channel and `Vehicle Specific` section.
+270
View File
@@ -0,0 +1,270 @@
# Car porting checklist
This checklist covers common issues we find in new car ports and many of the things we check before merging them. Use it as a guide to help you write a high-quality port and get it reviewed faster.
Not everything will apply to your car. If something can't be tested, let us know what and why. Where it applies, check each supported variant and both stock ACC and openpilot longitudinal. Some checks need the full openpilot stack, not just opendbc. Field names like `accFaulted` are defined in [`car.capnp`](../opendbc/car/car.capnp).
## Vehicle and setup
- Vehicle details
- [ ] Record make, model, year, trim, market, and relevant driver assistance options in `values.py`.
- If all variants in the US market come equipped with the necessary ADAS package, no package documentation is required.
- If a vehicle varies in features in another market, add a new [`CarDocs`](../opendbc/car/docs_definitions.py) entry.
- [ ] Add every observed ECU firmware version to `fingerprints.py`.
- [ ] Identify differences from related supported cars; verify reused signals and commands on this variant.
- Gas, gear, and brake signals often move between ICE, hybrid, and EV variants (e.g. Hyundai, Subaru, Toyota SecOC).
- Harness and CAN topology
- [ ] Confirm harness pinout, CAN bus assignments, and CAN/CAN FD configuration.
- [ ] Identify which ECUs send, receive, and forward each relevant message.
- This is useful to know which messages we need to forward and block in safety.
- Stock behavior and power lifecycle
- [ ] Record baseline stock LKAS, ACC, stop-and-go, and driver override behavior for reference when writing the port.
- Stock behavior should be matched when possible, e.g. how stock ACC commands standstill and resume.
- [ ] Verify ignition detection, startup, shutdown, and vehicle sleep with the device connected.
## Identification and parameters
- [ ] The car identifies correctly on repeated cold starts without a manually forced fingerprint or fingerprint cache.
- [ ] [Firmware queries](../opendbc/car/fw_versions.py) use the correct ECU addresses and buses without causing any DTCs on the dashboard.
- [ ] Exact and [fuzzy fingerprinting](../opendbc/car/fw_versions.py) distinguish variants that need different control or safety behavior.
- [ ] Unknown or ambiguous firmware does not silently select an incompatible platform.
- A vehicle variant we do not support that has identical firmware (such as lacking ACC or LKAS hardware) should be detected with a CAN signal in `interface.py` or `carstate.py` depending on what we can check, and set `dashcamOnly`.
- [ ] Reliable source for wheelbase, steering ratio, and other vehicle model parameters.
- [ ] `minSteerSpeed`, `minEnableSpeed`, `autoResumeSng`, and `steerAtStandstill` in `carParams` match observed behavior.
## CAN parsing and message construction
- Signal definitions and input validation
- [ ] Message addresses, lengths, buses, frequencies, endianness, signedness, scales, and units are verified.
- [ ] Checksums and counters validate against real captures, including counter wraparound.
- [ ] Missing, stale, malformed, or checksum-invalid messages produce the expected invalid CAN state.
- [ ] Variable-rate or intermittent messages have an [explicit frequency](../opendbc/can/parser.py) in `get_can_parsers` so they don't cause false CAN errors.
- For example, VW `Blinkmodi_02`, `HCA_01`, and `AWV_03` are 1 Hz when inactive and 50 Hz when active.
- [ ] Startup defaults and invalid signal values cannot look like valid engagement conditions.
- Outgoing messages and forwarding
- [ ] Copied stock messages preserve unrelated fields and use fresh source data.
- [ ] Transmitted messages have the correct bus, timing, counters, checksums, and request bits.
- [ ] Forwarding and replacement rules prevent duplicate actuation messages or bus loops.
- Diagnostics
- [ ] Diagnostic sessions, [ECU disabling](../opendbc/car/disable_ecu.py), and keepalives work across startup, restart, and shutdown, if used.
- This is for disabling an ECU, such as to gain longitudinal control. Some ports do not require this.
## Vehicle state: basics
- Seatbelt and doors
- [ ] Driver seatbelt: buckle and unbuckle both update `seatbeltUnlatched` correctly.
- [ ] Doors: open and close each door individually; `doorOpen` remains true until all monitored doors close.
- Gears
- [ ] Gear selection: ideally we parse park, reverse, neutral, drive, and alternate drive modes such as sport, B (brake) gear on hybrids, etc.
- If we cannot parse all gears, ensure we can disengage cruise on non-drive/sport gears, or anything that stock ACC would normally disengage for.
- [ ] Unknown gear values are handled explicitly instead of defaulting to drive.
- Pedals and regen
- [ ] Brake pedal: light press, firm press, hold, and release update `brakePressed` reliably.
- [ ] Accelerator pedal: press and release update `gasPressed`, including while cruise is active.
- [ ] Driver pedal signals are distinguished from braking or acceleration commanded by ACC/openpilot.
- In the past, we found some accelerator signals either activate when stock ACC accelerates, or don't activate when the user's gas was under the stock ACC request. `gasPressed` must be true whenever the user is pressing the gas pedal any amount, in any state.
- [ ] Regen paddles or other driver regen controls update `regenBraking`, if applicable.
- Parking brake and auto hold
- [ ] Parking brake engagement and release update `parkingBrake`.
- [ ] Auto hold engagement and release update `brakeHoldActive`, independently of the brake pedal.
- Speed and standstill
- [ ] Vehicle speed and individual wheel speeds have correct units and plausible values.
- [ ] Speed and `standstill` use the same signals as the safety model.
- For example, Nissan and GM use the rear wheel speeds to match safety.
- [ ] `standstill` distinguishes a complete stop from slow creep and updates promptly on drive-off.
- [ ] Cluster speed and cruise set speed agree with the car in both metric and imperial settings.
- Steering
- [ ] Steering angle has the correct sign, scale, center offset, and behavior through its full range.
- [ ] Steering torque has the correct sign and scale; driver torque and EPS torque are not confused.
- [ ] `steeringPressed` detects driver input in both directions without triggering on normal commanded steering.
- Use [`CS.update_steering_pressed`](../opendbc/car/interfaces.py) if the signal is noisy.
- Blinkers and blind spots
- [ ] Left/right blinkers are reported correctly.
- If we only have lamp state, use [`CS.update_blinker_from_lamp`](../opendbc/car/interfaces.py) so it doesn't oscillate with the blinker.
- If we only have stalk position, use [`CS.update_blinker_from_stalk`](../opendbc/car/interfaces.py) so short taps are held.
- [ ] Left/right blind spot indications are mapped correctly and clear when the stock indication clears.
- Faults and stock system state
- [ ] Temporary and permanent steering faults are logged correctly and recover only when the fault clears.
- EPS initializing states at startup are not permanent faults (e.g. VW, BYD).
- Expected low-speed steering lockouts should set `lowSpeedAlert` instead of a fault, usually with hysteresis.
- [ ] `accFaulted`, `carFaultedNonCritical`, `carNotReady`, and `vehicleSensorsInvalid` are reported correctly.
- [ ] ESP disabled/active states and stock AEB/FCW states are parsed where available.
- [ ] Stock LKAS settings (`invalidLkasSetting`) and conventional/non-adaptive cruise modes (`cruiseState.nonAdaptive`) are detected where relevant.
- For example, Nissan requires stock LKAS off, Mazda requires it on, and Tesla requires Autosteer off.
## Buttons and cruise state
- [ ] Set, resume, cancel, main cruise, speed up/down, gap, and LKAS buttons are mapped where present.
- [ ] Button press and release edges each occur once; held buttons do not cause unintended repeated engagement.
- [ ] Short presses, long presses, and simultaneous button presses are handled correctly.
- [ ] Cruise available (main), enabled, standstill, and set speed are parsed properly and match stock behavior.
- Cruise enabled should stay true during driver accelerator override and at standstill. Some dash signals fall in these states (e.g. Subaru).
- `cruiseState.standstill` should only be true if the car requires a resume press to drive off.
- [ ] Set speed changes, unit changes, and minimum/maximum set speeds behave correctly.
## Engagement and cancellation
- Engagement conditions
- [ ] The intended set/resume action engages once, only when the car is ready (`carNotReady`).
- [ ] Engagement refusal and disengagement for seatbelt, doors, gear, parking brake, and faults match the intended policy.
- Cancellation
- [ ] The physical cancel button disengages openpilot and cancels stock cruise as intended.
- [ ] If stock ACC is controlling longitudinal, verify our cancel message stops commanding acceleration.
- [ ] Software cancellation works in each supported cruise state, including standstill and driver accelerator override.
- [ ] Cancel wins over simultaneous set/resume requests; repeated cancel requests do not resume or re-enable cruise.
- If the cancel button can toggle pause/resume or the main button can engage, use `blockPcmEnable` to only allow PCM enable after a real user button press (e.g. Hyundai).
- [ ] Cruise main off disengages and prevents unintended re-engagement.
- Disengagement and recovery
- [ ] Releasing a pedal, closing a door, buckling a belt, or clearing a fault does not unexpectedly re-engage unless we are still in the soft-disable grace period.
- [ ] Stock ACC disengagement/fault causes the expected openpilot response when using stock longitudinal control.
- [ ] Lateral-only and longitudinal-active states set their respective actuation request bits correctly.
- For example, `CC.enabled` should be used when keeping the car's cruise engaged during a longitudinal override while `CC.longActive` falls.
## Lateral control
- Commands, limits, and driver override
- [ ] Small left/right commands produce the expected steering direction.
- [ ] Command magnitude and rate limits match the platform and safety implementation.
- [ ] Driver override reduces or cancels openpilot steering assistance as intended, in both directions.
- Some platforms disengage both steering and cruise on a steering override (e.g. Tesla); use `steeringDisengage` for this.
- Low-speed behavior
- [ ] Steering behaves correctly around the minimum steering speed, including crossing it in both directions.
- [ ] Check for low-speed oscillation and flag it if present. One solution is to low-pass filter the request below a certain speed.
- Faults and recovery
- [ ] Sustained steering, high steering rates, and request-bit transitions do not cause unexpected EPS faults.
- [ ] Temporary steering unavailability stops active requests and produces the expected user alert.
- Tuning and reported output
- [ ] Tuning is checked across representative speeds and curves for oscillation, lag, and saturation.
- [ ] Reported `actuatorsOutput` matches the command actually sent after clipping and rate limiting.
## Longitudinal control
- Supported control modes
- [ ] Stock ACC works correctly with openpilot lateral control, including cancel, override, and stop-and-go.
- There are two levels of support with openpilot-controlled longitudinal:
- [ ] Alpha longitudinal is chosen if we cannot maintain stock AEB or other safety features, or the car does not have a radar we can parse. openpilot longitudinal is then only available on development branches.
- [ ] openpilot longitudinal is the only mode and available in all branches if both of the above requirements are met, plus we have a good understanding of the car's longitudinal API.
- Acceleration, braking, and driver override
- [ ] Acceleration and braking command signs, scales, limits, and rate limits are verified.
- [ ] Acceleration, coasting, and braking transitions are smooth and free of conflicting requests.
- Some cars accept gas and brake commands separately; never request both at once.
- [ ] Brake press disengages while moving as well as while at a standstill.
- [ ] Accelerator override and release behave correctly, i.e. we stop requesting acceleration (inactive, or 0 m/s^2) while continuing to steer.
- Stopping, holding, and resuming
- [ ] Low-speed braking reaches a complete stop without unexpected creep or brake release.
- [ ] Stop hold works for short and extended stops; timeout and parking brake behavior are understood.
- [ ] Resume/drive-off releases hold in the correct sequence without unintended acceleration.
- [ ] Resume is sent only when requested and supported.
- [ ] Stops and starts on grades behave correctly without unexpected rollback or parking brake/gear transitions.
- [ ] `stopAccel`, `vEgoStopping`, `vEgoStarting`, `startingState`, and `startAccel` are tuned if needed.
- Powertrain states and faults
- [ ] Behavior is checked across relevant powertrain states, such as engine stop/start.
- [ ] ACC faults and transient not-ready states inhibit requests appropriately and recover cleanly.
- Tuning and stock safety features
- [ ] `longitudinalActuatorDelay` and tuning are evaluated across representative speeds, following, stops, and starts.
- [ ] Stock AEB/FCW interaction and any feature loss from ECU disabling are understood and documented.
## Radar, alerts, and stock features
- Radar
- [ ] Radar distance, lateral position, relative velocity, signs, units, and track IDs are verified, if supported.
- [ ] Radar track creation, updates, disappearance, invalid targets, and timeouts behave correctly.
- [ ] `radarDelay` is set if known.
- HUD and alerts
- [ ] HUD engaged/standby state, set speed, lane lines, lead indication, and gap bars match the requested state.
- [ ] Takeover, steering unavailable, and other required alerts appear and clear correctly.
- Stock feature interaction
- [ ] Stock LKAS and ACC do not fight openpilot commands.
- [ ] Stock safety messages are forwarded or arbitrated as intended; validate AEB interactions with replay/bench tests.
- [ ] Normal operation does not introduce persistent dash warnings or diagnostic trouble codes.
- Some cars may display warning messages on the dash or force a disengage if they detect a conflict between what their camera is sending and our request through read-back signals from the PCM or EPS. If we need to spoof a message to the camera/ADAS ECU, we need to verify it does not affect stock safety features such as LKAS or AEB.
## Safety and failure handling
- Message permissions and engagement
- [ ] The selected safety mode permits only intended transmit addresses, buses, and message lengths.
- [ ] Safety engagement/disengagement agrees with the car interface for cruise, buttons, pedals, and motion state.
- [ ] Non-zero actuation or active request bits are rejected when safety's [`controls_allowed`](../opendbc/safety/safety.h) is false.
- [ ] Any messages permitted while disengaged are limited to their intended inactive/cancel behavior.
- Actuation limits
- [ ] Steering magnitude, rate, driver torque, measured torque, and request-bit checks are covered as applicable.
- [ ] Longitudinal limits, inactive values, and cancel/resume restrictions are covered as applicable.
- [ ] All actuation-capable fields in transmitted messages are constrained, including fields unused by the controller.
- Communication failures and recovery
- [ ] Invalid counters/checksums, missing required input messages, and stale state disable control as intended.
- [ ] Relay malfunction detection and forwarding restrictions cover the actual harness topology.
- Test coverage
- [ ] Safety tests cover every new safety flag variant and reject disallowed commands.
## Regression tests
- Routes and automated checks
- [ ] Add representative routes to the [car test routes](../opendbc/car/tests/routes.py), covering relevant variants and modes.
- [ ] Add or update tests for fingerprinting, parsing, command construction, and discovered edge cases.
- [ ] Run applicable [car interface tests](../opendbc/car/tests/) and [safety tests](../opendbc/safety/tests/).
- [ ] [`test_models.py`](../opendbc/car/tests/test_models.py) passes on the new routes; it replays them to check CAN validity and that safety agrees with carstate and accepts our commands.
- [ ] Run the repository checks in [`test.sh`](../test.sh) and fix any failures.
- Regression validation
- [ ] Recheck related existing cars affected by shared DBC, controller, interface, or safety changes.
- [ ] Repeat startup, engagement, cancellation, and shutdown over multiple drives and ignition cycles.
+9 -1
View File
@@ -146,6 +146,7 @@ class CANParser:
self.can_invalid_cnt: int = CAN_INVALID_CNT
self.last_nonempty_nanos: int = 0
self._last_update_nanos: int = 0
self._last_invalid_log_nanos: int | None = None
def _add_message(self, name_or_addr: str | int, freq: float | None = None) -> None:
if isinstance(name_or_addr, numbers.Number):
@@ -201,13 +202,20 @@ class CANParser:
valid = True
counters_valid = True
bus_timeout = self.bus_timeout
invalid_messages = set()
for state in self.message_states.values():
if state.counter_fail >= MAX_BAD_COUNTER:
counters_valid = False
state.rate_limited_log(self._last_update_nanos, f"counter invalid, {state.counter_fail=} {MAX_BAD_COUNTER=}")
if not state.valid(self._last_update_nanos, bus_timeout):
valid = False
state.rate_limited_log(self._last_update_nanos, "not valid (timeout or missing)")
invalid_messages.add(state.address)
if invalid_messages and (self._last_invalid_log_nanos is None or
self._last_update_nanos - self._last_invalid_log_nanos >= 10_000_000_000):
messages = ', '.join(f"{hex(addr)} {self.message_states[addr].name}" for addr in sorted(invalid_messages))
carlog.warning(f"CANParser: bus {self.bus} not valid (timeout or missing): {messages}")
self._last_invalid_log_nanos = self._last_update_nanos
# TODO: probably only want to increment this once per update() call
self.can_invalid_cnt = 0 if valid else min(self.can_invalid_cnt + 1, CAN_INVALID_CNT)
+5 -1
View File
@@ -360,6 +360,7 @@ class CarController(CarControllerBase):
self.volt_cc_cancel_pending = False
self.volt_cc_last_cancel_frame = 0
self.volt_cc_metric = False
self.volt_cc_speed_ceiling_mps = None
self.volt_cc_last_button_frame = 0
self.volt_cc_last_direction_frame = None
self.volt_cc_consumed_source_ns = 0
@@ -1396,7 +1397,10 @@ class CarController(CarControllerBase):
self.volt_cc_consumed_source_ns = source_ns
can_sends.append(gmcan.create_buttons(self.packer_pt, CanBus.POWERTRAIN, (CS.buttons_counter + 1) % 4, button))
elif self.frame % 4 == 0:
button, rate = button_request(CS.out.vEgo, CS.out.cruiseState.speed, actuators.accel, CS.out.vCruise,
cruise_kph = CS.out.vCruise
if self.volt_cc_speed_ceiling_mps is not None:
cruise_kph = min(cruise_kph, self.volt_cc_speed_ceiling_mps * CV.MS_TO_KPH)
button, rate = button_request(CS.out.vEgo, CS.out.cruiseState.speed, actuators.accel, cruise_kph,
bool(CC.hudControl.leadVisible), self.volt_cc_metric)
source_ns = physical.button_credit_ns
if ((self.frame - self.volt_cc_last_button_frame) * DT_CTRL > rate and
@@ -335,8 +335,8 @@ def test_actual_card_active_observation_requires_fresh_selfdrive_state(monkeypat
host.observe_ioniq6_long_authority = lambda *args: None
host.update_vehicle_state_context = lambda *args: None
def update_cruise_speed(_state, _pending, *, approved=None):
assert approved is None
def update_cruise_speed(_state, _pending, *, wheel_context=None):
assert wheel_context is None
host.update_cruise_speed = update_cruise_speed
host.v_cruise_helper = SimpleNamespace(slc_consumed_button=None, toyota=None)
host.startup_panda_configured = lambda: True
@@ -8,6 +8,9 @@ class AngleTxRecovery:
self.clock = time.monotonic_ns if clock is None else clock
self.address = address
self.bus = bus
self.length = 24 if address == 0xCB else 32
self.active_index = 3 if address == 0xCB else 9
self.active_mask = 0xF if address == 0xCB else 3
self.sent = deque(maxlen=4)
self.last_packet_ns = 0
self.pending_frame = -1
@@ -16,7 +19,7 @@ class AngleTxRecovery:
def record(self, frames, frame):
now_ns = self.clock()
for address, data, bus in frames:
if address == self.address and bus == self.bus and len(data) == 32 and (data[9] >> 4 & 3) == 2:
if address == self.address and bus == self.bus and len(data) == self.length and (data[self.active_index] >> 4 & self.active_mask) == 2:
self.sent.append((frame, now_ns, bytes(data)))
def observe(self, packets, frame):
@@ -26,7 +29,7 @@ class AngleTxRecovery:
continue
self.last_packet_ns = stamp
for address, data, bus in frames:
if address != self.address or bus != self.bus + 0xC0 or len(data) != 32:
if address != self.address or bus != self.bus + 0xC0 or len(data) != self.length:
continue
# Consume an exact recent command once. The original local send clock
# also bounds delayed echoes when controller updates have been paused.
@@ -96,9 +96,12 @@ def qualified(cp, *, marked_only=False):
SPORTAGE_LONG_AOL_WORDS = frozenset(0x0804 | fuel | topology for fuel in (0, 2) for topology in (0, 8, 16, 144, 32, 40))
LONG_AOL_WORDS = frozenset((0x080D, 0x0815, 0x0895, 0x2815, 0x2895)) | SPORTAGE_LONG_AOL_WORDS
CARNIVAL_LONG_AOL_WORD = 0x28B4
LONG_AOL_WORDS = frozenset((CARNIVAL_LONG_AOL_WORD, 0x080D, 0x0815, 0x0895, 0x2815, 0x2895)) | SPORTAGE_LONG_AOL_WORDS
LONG_EV_CARS = frozenset((CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_KONA_EV_2ND_GEN))
INDEPENDENT_LONG_BUTTON_CARS = LONG_EV_CARS | {CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, CAR.GENESIS_GV60_EV_1ST_GEN, CAR.KIA_SPORTAGE_5TH_GEN}
INDEPENDENT_LONG_BUTTON_CARS = LONG_EV_CARS | {
CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, CAR.GENESIS_GV60_EV_1ST_GEN, CAR.KIA_SPORTAGE_5TH_GEN, CAR.KIA_CARNIVAL_2025,
}
def qualified_long(cp, *, marked_only=False):
@@ -118,6 +121,9 @@ def qualified_long(cp, *, marked_only=False):
elif cp.carFingerprint == CAR.KIA_SPORTAGE_5TH_GEN:
if word is None or word | 0x804 not in SPORTAGE_LONG_AOL_WORDS:
return False
elif cp.carFingerprint == CAR.KIA_CARNIVAL_2025:
if word != 0x20B0:
return False
elif word not in (0x11, 0x91):
return False
raw = int(cp.safetyConfigs[config_index(cp)].safetyParam)
@@ -24,6 +24,7 @@ from opendbc.car.hyundai import hyundaicanfd, hyundaican
from opendbc.car.hyundai.hyundaicanfd import CanBus
from opendbc.car.hyundai.ray_pedal import create_ray_pedal_command, ray_pedal_gas, ray_pedal_enabled
from opendbc.car.hyundai.ioniq6_longitudinal import Ioniq6LongitudinalPolicy
from opendbc.car.hyundai.ioniq6_bsm import BlindspotStatus
from opendbc.car.hyundai.canfd_dash_icons import CanFDDashIcons
from opendbc.car.hyundai.classic_scc_aol import qualified as qualified_classic_stock_aol
from opendbc.car.hyundai.classic_long_aol import AOL_EXPERIENCE, qualified as qualified_classic_long_aol
@@ -71,7 +72,7 @@ def angle_torque_reduction_gain(steering_torque, v_ego, lat_active, last_gain):
bp4 = np.interp(v_ego, [2.0, 22.0], [400.0, 700.0])
target = np.interp(abs(steering_torque), [bp1, bp2, bp3, bp4], [ceiling, shelf, shelf, floor])
else:
target = 0.0
return 0.0
return round(rate_limit(target, last_gain, -0.014, 0.004) / 0.004) * 0.004
@@ -117,7 +118,8 @@ class CarController(CarControllerBase):
self.angle_vm = VehicleModel(CP) if CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING else None
self.apply_angle_last = 0.0
self.angle_gain_last = 0.0
self.angle_tx_recovery = (AngleTxRecovery(0x110, self.CAN.ACAN)
self.angle_tx_recovery = (AngleTxRecovery(0xCB, self.CAN.ECAN)
if pe_long_qualified(CP) or ev9_long_qualified(CP) else AngleTxRecovery(0x110, self.CAN.ACAN)
if CP.carFingerprint in (CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9) and
(qualified_angle_aol(CP, marked_only=True) or qualified_ccnc_ev_stock(CP)) else None)
@@ -296,6 +298,7 @@ class CarController(CarControllerBase):
self.frame += 1
if _projection is not None:
_projection.finish(new_actuators.torque)
can_sends.extend(CS.daw.command(now_nanos))
return new_actuators, can_sends
def create_can_msgs(self, apply_steer_req, apply_torque, torque_fault, set_speed_in_units, accel, stopping,
@@ -435,6 +438,12 @@ class CarController(CarControllerBase):
if ev9_long:
drive = CS.out.gearShifter == structs.CarState.GearShifter.drive
measured = CS.angle_steering_angle
if self.angle_tx_recovery is not None and self.angle_tx_recovery.take_rejection(self.frame):
self.apply_angle_last = float(np.clip(measured, -self.params.ANGLE_LIMITS.STEER_ANGLE_MAX,
self.params.ANGLE_LIMITS.STEER_ANGLE_MAX))
self.angle_gain_last = 0.0
if self.ev9_angle_filter is not None:
self.ev9_angle_filter.x = self.apply_angle_last
safety_speed = max(CS.out.vEgoRaw - 1.0, 1.0)
bound = min(get_max_angle_vm(safety_speed, self.ev9_legacy_envelope, self.params),
get_max_angle_vm(safety_speed, self.angle_vm, self.params))
@@ -510,8 +519,9 @@ class CarController(CarControllerBase):
)
else:
steering_status_active = CC.enabled
if self.ev6_aol_status or (self.ioniq6_longitudinal is not None and
self.CP.safetyConfigs[-1].safetyParam in (0x8815, 0x8895)):
if ((self.CP.carFingerprint == CAR.KIA_CARNIVAL_2025 and self.CP.safetyConfigs[-1].safetyParam == 0x28B4) or
self.ev6_aol_status or (self.ioniq6_longitudinal is not None and
self.CP.safetyConfigs[-1].safetyParam in (0x8815, 0x8895))):
# AOL may steer while ordinary cruise engagement remains off.
steering_status_active |= CC.latActive
can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN,
@@ -587,6 +597,9 @@ class CarController(CarControllerBase):
can_now_ns = getattr(CS, "ioniq6_bsm_now_ns", 0)
status = (sources.status(can_now_ns) if sources is not None and can_now_ns > self.ioniq6_bsm_last_can_ns else None)
if status is not None:
# Neutralize only the injected dashboard indication below 5 mph.
if CS.out.vEgoRaw < 5 * CV.MPH_TO_MS:
status = BlindspotStatus(0, 0)
can_sends.extend(hyundaicanfd.create_ioniq6_blindspot_status(self.ioniq6_bsm_counter, status))
self.ioniq6_bsm_counter = (self.ioniq6_bsm_counter + 1) & 0xff
self.ioniq6_bsm_last_can_ns = can_now_ns
@@ -62,6 +62,8 @@ def get_non_scc_cruise_signals(flags: int, car=None) -> tuple[str, str, str, str
class CarState(CarStateBase):
def __init__(self, CP):
super().__init__(CP)
from opendbc.car.hyundai.daw import DawSource
self.daw = DawSource(CP)
self.forte_lkas_sources = NonSccLkasSources() if qualified_non_scc(CP) and CP.flags & HyundaiFlags.HAS_LDA_BUTTON else None
if (qualified_classic_scc(CP) or qualified_classic_long(CP)) and CP.flags & HyundaiFlags.HAS_LDA_BUTTON:
self.forte_lkas_sources = ClassicSccLkasSources(CP.carFingerprint)
@@ -130,6 +132,7 @@ class CarState(CarStateBase):
self.ccnc_162_ts_ns = 0
self.ccnc_1b5_ts_ns = 0
self.dashboard_limit = LimitTracker()
self.dashboard_stop_sign = None
self.ioniq6_bsm_sources = (Ioniq6BlindspotSources()
if CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 and CP.openpilotLongitudinalControl else None)
self.ioniq6_bsm_now_ns = 0
@@ -604,6 +607,9 @@ class CarState(CarStateBase):
status, value_mps = hyundai_canfd_sign(speed, system_status, self.is_metric)
self.dashboard_limit.update(timestamp, status, value_mps, valid_until_ns=expiry)
stamp = cp_cam.ts_nanos["ADAS_0x380"]["STOP_SIGN"]
self.dashboard_stop_sign = (bool(cp_cam.vl["ADAS_0x380"]["STOP_SIGN"]), stamp) if stamp > 0 else None
return ret
def get_can_parsers_canfd(self, CP):
@@ -663,6 +669,12 @@ class CarState(CarStateBase):
msgs.append(("CRUISE_BUTTONS_ALT", math.nan))
if not (ev9_long_qualified(CP) or pe_long_qualified(CP)):
(cam_msgs if CP.flags & HyundaiFlags.CANFD_CAMERA_SCC else msgs).append(("SCC_CONTROL", 50))
elif CP.carFingerprint == CAR.KIA_CARNIVAL_2025 and CP.openpilotLongitudinalControl and CP.safetyConfigs[-1].safetyParam in (0x20B4, 0x28B4):
msgs = [(name, frequency) for name, frequency in msgs if name != "CRUISE_BUTTONS_ALT"]
msgs.extend((("CRUISE_BUTTONS_ALT", 50), ("ACCELERATOR_BRAKE_ALT", 100), ("TCS", 50),
("WHEEL_SPEEDS", 100), ("MDPS", 100), (self.gear_msg_canfd, 100),
("STEERING_SENSORS", 100), ("DOORS_SEATBELTS", 10), ("BLINKERS", 10), ("SCC_CONTROL", math.nan)))
cam_msgs.append(("CAM_0x362", 20))
elif torque_ev_scc_eligible(CP):
msgs.extend(((self.accelerator_msg_canfd, 100), ("TCS", 50), ("WHEEL_SPEEDS", 100), ("MDPS", 100)))
msgs.extend((("CRUISE_BUTTONS_ALT", math.nan), ("DOORS_SEATBELTS", math.nan),
@@ -699,6 +711,7 @@ class CarState(CarStateBase):
msgs.append(("SCC_CONTROL", 50))
if ("CAM_0x2a4", 20) not in cam_msgs:
cam_msgs.append(("CAM_0x2a4", 20))
cam_msgs.append(("ADAS_0x380", math.nan))
return {
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], msgs, CanBus(CP).ECAN),
# Native CANParser accepts NaN to subscribe without a CAN-valid frequency gate.
+48
View File
@@ -0,0 +1,48 @@
from opendbc.car import CanData
from opendbc.car.hyundai.hyundaicanfd import CanBus, hkg_can_fd_checksum
from opendbc.car.hyundai.values import CAR, HyundaiFlags, is_blended
CANFD_DAW_WORDS = frozenset((0x11, 0x91, 0x811, 0x891, 0x8015, 0x8095, 0x8815, 0x8895))
class DawSource:
def __init__(self, cp):
self.canfd = bool(cp.flags & HyundaiFlags.CANFD)
self.enabled = (cp.brand == 'hyundai' and not cp.passive and not cp.dashcamOnly and not cp.notCar and
not is_blended(cp) and (not self.canfd or cp.carFingerprint == CAR.HYUNDAI_IONIQ_6))
if self.canfd:
self.enabled = bool(self.enabled and cp.safetyConfigs and
int(cp.safetyConfigs[-1].safetyParam) in CANFD_DAW_WORDS and CanBus(cp).ECAN % 4 == 1)
self.address = 0x11A if self.canfd else 0x53E
self.source_bus = CanBus(cp).ECAN if self.canfd else 2
self.target_bus = CanBus(cp).ECAN if self.canfd else 0
self.length = 16 if self.canfd else 6
self.data = None
self.stamp = 0
def update(self, packets):
if not self.enabled:
return
for stamp, frames in packets:
for address, data, bus in frames:
if address != self.address or bus != self.source_bus:
continue
if stamp <= self.stamp or len(data) != self.length or (self.canfd and
int.from_bytes(data[:2], 'little') != hkg_can_fd_checksum(address, None, data)):
self.data = None
continue
self.data, self.stamp = bytes(data), stamp
def command(self, now_ns):
if (not self.enabled or self.source_bus == self.target_bus or self.data is None or
not 0 < self.stamp <= now_ns <= self.stamp + 100_000_000):
return []
data = bytearray(self.data)
if self.canfd:
if (data[7] >> 3) & 7 == 1:
data[7] &= ~0x38
data[:2] = hkg_can_fd_checksum(self.address, None, data).to_bytes(2, 'little')
elif 1 <= data[5] & 7 <= 3:
data[5] &= ~7
return [CanData(self.address, bytes(data), self.target_bus)]
@@ -314,7 +314,7 @@ def qualified(cp):
cp.alternativeExperience not in (0, 32) or
not cp.alphaLongitudinalAvailable or len(cp.safetyConfigs) != 1 or cp.safetyConfigs[0].safetyParam != LONG_PARAM):
return False
if type(cp) is _DynamicStructBuilder and cp.schema == structs.CarParams.schema:
if type(cp) in (_DynamicStructBuilder, _DynamicStructReader) and cp.schema == structs.CarParams.schema:
return metadata_qualified(cp)
stock = stock_copy(cp)
stock.alternativeExperience = 0
@@ -49,6 +49,11 @@ EXISTING_OWNERS_OR_DISTINCT_RECEIVERS = frozenset((
CAR.GENESIS_GV80,
))
def carnival_long(cp):
return (cp.carFingerprint == CAR.KIA_CARNIVAL_2025 and qualified_long(cp) and
cp.safetyConfigs[0].safetyParam in (0x20B4, 0x28B4))
STOCK_WORDS = (0x10, 0x11, 0x12, 0x90, 0x91, 0x92)
@@ -65,6 +70,8 @@ def qualified(cp):
HyundaiFlags.CANFD_ALT_GEARS | HyundaiFlags.CANFD_ALT_GEARS_2)
if not required(cp) or cp.carFingerprint not in CARS:
return False
if carnival_long(cp):
return cp.alphaLongitudinalAvailable and cp.alternativeExperience == 0
factory = CAR(cp.carFingerprint)
stock = stock_copy(cp)
return (cp.alphaLongitudinalAvailable and cp.alternativeExperience == 0 and not cp.pcmCruise and
@@ -91,13 +98,13 @@ class GenericHDA2Startup(RadarHDA2Startup):
raise RuntimeError('Ordinary HDA-II finalization requires configured startup')
if self.prepared_for(ci.CP):
return
long_aol = self.outcome is Outcome.SENT_UNCONFIRMED and qualified_long(ci.CP, marked_only=True)
long_aol = self.outcome in (Outcome.PENDING, Outcome.SENT_UNCONFIRMED) and qualified_long(ci.CP, marked_only=True)
if not long_aol and (self.outcome not in (Outcome.STOCK_UNTOUCHED, Outcome.STOCK_RESTORED) or
not stock_qualified(ci.CP, marked_only=True)):
raise RuntimeError('Ordinary HDA-II startup does not admit this AOL configuration')
expected: Any = deepcopy(self.prepared_cp)
configs = expected.get('safetyConfigs', [])
allowed_words = tuple(word | 4 for word in STOCK_WORDS) if long_aol else STOCK_WORDS
allowed_words = (*tuple(word | 4 for word in STOCK_WORDS), 0x20B4) if long_aol else (*STOCK_WORDS, 0x20B0)
if (len(configs) != 1 or configs[0]['safetyModel'] != 'hyundaiCanfd' or
configs[0]['safetyParam'] not in allowed_words or expected['alternativeExperience'] != 0):
raise RuntimeError('Ordinary HDA-II prepared stock profile cannot be finalized')
@@ -107,3 +114,50 @@ class GenericHDA2Startup(RadarHDA2Startup):
if ci.CP.to_dict() != expected:
raise RuntimeError('Ordinary HDA-II finalization changed unrelated CarParams')
self.prepared_cp = expected
def prepare_when_controls_ready(self, *, admission):
if not carnival_long(self.cp):
return self.prepare(admission=admission)
if self.outcome is not None or self.closed:
raise RuntimeError('Carnival startup preparation is single-use')
if admission() is not True:
return self.prepare(admission=lambda: False)
self.admission = admission
self.floor_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
self.outcome = Outcome.PENDING
self.prepared_cp = deepcopy(self.cp.to_dict())
return self.cp
def commit_pending(self, ci, *, sources_current, control_current):
if (not carnival_long(ci.CP) or self.outcome is not Outcome.PENDING or self.closed or not self.published or
ci is not self.ci or not self.prepared_for(ci.CP) or not sources_current or not control_current or
self.admission is None or self.admission() is not True):
raise RuntimeError('Carnival takeover requires current diagnostic, source and inactive control admission')
try:
success = self._disable(self.callbacks[0], self._send, bus=self.bus, addr=self.address,
com_cont_req=self.communication_control_request())
if not success or not self.disable_attempted:
raise RuntimeError('Carnival pending ECU disable was not sent successfully')
self.outcome = Outcome.SENT_UNCONFIRMED
self.floor_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
from opendbc.car import make_tester_present_msg
self.keeper = self.keeper_factory(
lambda: self.callbacks[1]([make_tester_present_msg(self.address, self.bus, suppress_response=True)]), clock=self.clock)
self.keeper.start()
except BaseException:
if self.keeper is not None:
self.keeper.stop()
try:
restored = not (self.session_attempted or self.disable_attempted) or self._restore()
except Exception:
restored = False
self.outcome = Outcome.ABORT_UNCERTAIN
if not restored:
raise RuntimeError('Carnival pending takeover failed; stock restoration unverified') from None
raise
def before_control(self, **context):
if self.outcome is Outcome.PENDING:
self.check()
return False
return super().before_control(**context)
@@ -285,6 +285,17 @@ def create_ioniq6_blindspot_status(counter: int, status: BlindspotStatus) -> lis
def create_ccnc(packer, CAN, openpilot_longitudinal, enabled, hud, left_blinker, right_blinker, msg_161, msg_162, msg_1b5,
is_metric, out, main_cruise_enabled, lfa_icon):
if lfa_icon:
# The host owns lateral; keep unrelated faults and obstruction warnings.
for field in ("FAULT_LSS", "FAULT_HDA", "FAULT_DAS", "FAULT_LFA", "FAULT_DAW"):
if msg_162.get(field) == 1:
msg_162[field] = 0
if msg_161.get("ALERTS_2") == 5:
msg_161.update({"ALERTS_2": 0, "SOUNDS_2": 0})
if msg_161.get("ALERTS_3") == 17:
msg_161["ALERTS_3"] = 0
if msg_161.get("ALERTS_5") in (2, 5):
msg_161["ALERTS_5"] = 0
msg_161["DAW_ICON"] = 0
lane_change_speed_min = 8.9408
any_blinker = left_blinker or right_blinker
curvature = {i: (31 if i == -1 else 13 - abs(i + 15)) if i < 0 else 15 + i for i in range(-15, 16)}
@@ -396,6 +396,9 @@ class CarInterface(CarInterfaceBase):
return ret
def update(self, can_packets):
if self.CS.daw.enabled:
can_packets = list(can_packets)
self.CS.daw.update(can_packets)
recovery = getattr(getattr(self, 'CC', None), 'angle_tx_recovery', None)
if recovery is not None:
can_packets = list(can_packets)
@@ -1,8 +1,9 @@
from math import isfinite
from capnp.lib.capnp import _DynamicStructBuilder, _DynamicStructReader
from opendbc.car import structs
from opendbc.car.hyundai.values import CAR
from opendbc.car.hyundai.ccnc_ev_stock import qualified as stock_qualified
from opendbc.car.hyundai.ccnc_ev_stock import qualified as stock_qualified, metadata_qualified
from opendbc.car.hyundai.ev9_longitudinal import copy_cp
STOCK_PARAM = 0x5491
@@ -36,13 +37,17 @@ def qualified(cp):
if (not required(cp) or cp.pcmCruise or not cp.alphaLongitudinalAvailable or
cp.alternativeExperience not in (0, 32) or len(cp.safetyConfigs) != 1 or
cp.safetyConfigs[0].safetyParam != LONG_PARAM or
cp.steerControlType != structs.CarParams.SteerControlType.angle or not cp.deprecated.startingState or
not all(isfinite(value) for value in (cp.deprecated.startAccel, cp.deprecated.vEgoStarting, cp.deprecated.vEgoStopping,
cp.deprecated.stoppingDecelRate, cp.longitudinalActuatorDelay)) or
abs(cp.deprecated.startAccel - 1.) > 1e-6 or abs(cp.deprecated.vEgoStarting - .4) > 1e-6 or
abs(cp.deprecated.vEgoStopping - .3) > 1e-6 or abs(cp.deprecated.stoppingDecelRate - .4) > 1e-6 or
cp.steerControlType != structs.CarParams.SteerControlType.angle):
return False
deprecated = cp.deprecated
if (not deprecated.startingState or not all(isfinite(value) for value in (deprecated.startAccel, deprecated.vEgoStarting, deprecated.vEgoStopping,
deprecated.stoppingDecelRate, cp.longitudinalActuatorDelay)) or
abs(deprecated.startAccel - 1.) > 1e-6 or abs(deprecated.vEgoStarting - .4) > 1e-6 or
abs(deprecated.vEgoStopping - .3) > 1e-6 or abs(deprecated.stoppingDecelRate - .4) > 1e-6 or
abs(cp.longitudinalActuatorDelay - .35) > 1e-6):
return False
if type(cp) in (_DynamicStructBuilder, _DynamicStructReader) and cp.schema == structs.CarParams.schema:
return metadata_qualified(cp)
stock = copy_cp(cp)
stock.alternativeExperience = 0
stock.openpilotLongitudinalControl = False
@@ -0,0 +1,126 @@
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from opendbc.can import CANPacker
from opendbc.car import structs
from opendbc.car.hyundai.angle_tx_recovery import AngleTxRecovery
from opendbc.car.hyundai.carcontroller import angle_torque_reduction_gain
from opendbc.car.hyundai.interface import CarInterface
from opendbc.car.hyundai.tests.test_ev9_long_controller import packets
from opendbc.car.hyundai.tests.test_stock_angle_aol import source_profile
from opendbc.car.hyundai.values import CAR
@pytest.mark.parametrize('gain', (0.18, 0.532, 0.692, 0.848))
def test_inactive_steering_does_not_retain_pre_fault_gain(gain):
released = angle_torque_reduction_gain(0, 5., False, gain)
assert released == 0., 'INACTIVE_STEERING_RETAINED_GAIN'
assert angle_torque_reduction_gain(0, 5., True, released) == 0.004
def profile(identity, long, aol):
cp = source_profile(identity)
if long:
if identity == CAR.HYUNDAI_IONIQ_5_PE:
from opendbc.car.hyundai.ioniq5pe_long_profile import candidate
else:
from opendbc.car.hyundai.ev9_longitudinal import candidate
cp = candidate(cp, enabled=True, is_release=False)
cp.alternativeExperience = 32 if aol else 0
elif aol:
cp.safetyConfigs[0].safetyParam |= 0x800 if not cp.safetyConfigs[0].safetyParam & 0x4000 else 0
cp.alternativeExperience = 32
return cp
def controller(identity, long, aol):
cp = profile(identity, long, aol)
ci = CarInterface(cp)
# Isolate command recovery after the separately tested owner handoff.
ci.CC.pe_startup = SimpleNamespace(handed_off=True, can_emit=lambda controller: True)
packer = CANPacker(ci.can_parsers[next(iter(ci.can_parsers))].dbc.name)
for tick in range(1, 14):
frames = packets(packer, tick, advance_counter=True)
if not long and tick % 2 == 0:
frames.append(packer.make_can_msg('SCC_CONTROL', 1, {'COUNTER': tick // 2, 'ACCMode': 1, 'MainMode_ACC': 1}))
ci.update([(tick * 10_000_000, frames)])
assert ci.CS.out.canValid
return ci, packer
@pytest.mark.parametrize('identity', (CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9))
@pytest.mark.parametrize('long', (False, True))
@pytest.mark.parametrize('aol', (False, True))
def test_actual_controller_fault_and_full_lateral_withdrawal_restart_gain_from_zero(identity, long, aol):
ci, _ = controller(identity, long, aol)
cs = ci.CS
cs.out.vEgoRaw = cs.out.vEgo = 5.
cs.out.steeringAngleDeg = cs.angle_steering_angle = 20.
command = structs.CarControl(enabled=True, latActive=True)
command.actuators.steeringAngleDeg = 50.
ci.CC.apply_angle_last = 20.
ci.CC.angle_gain_last = .692
cs.out.steerFaultTemporary = cs.angle_steering_fault = True
ci.CC.frame = 1
_, inactive = ci.apply(command.as_reader(), 200_000_000)
assert ci.CC.angle_gain_last == 0., 'FAULT_RETAINED_PREVIOUS_GAIN'
cs.out.steerFaultTemporary = cs.angle_steering_fault = False
ci.CC.frame = 3
_, output = ci.apply(command.as_reader(), 210_000_000)
assert ci.CC.angle_gain_last == .004
address = 0xCB if long else 0x110
payload = next(data for addr, data, _ in output if addr == address)
assert payload[6 if long else 12] == 1
command.latActive = command.enabled = False
ci.CC.frame = 7
ci.apply(command.as_reader(), 220_000_000)
assert ci.CC.angle_gain_last == 0.
@pytest.mark.parametrize('case', ('current','accepted','wrong_bus','wrong_address','wrong_payload','short','stale','duplicate','inactive'))
def test_long_echo_exact_source_freshness_and_one_use(case):
now = [1_000_000_000]
recovery = AngleTxRecovery(0xCB, 1, clock=lambda: now[0])
raw = bytearray(24)
raw[3] = 0x10 if case == 'inactive' else 0x20
recovery.record([(0xCB, bytes(raw), 1)], 10)
now[0] += 31_000_000 if case == 'stale' else 10_000_000
if case == 'wrong_payload':
raw[6] ^= 1
data = bytes(raw[:23] if case == 'short' else raw)
bus = 129 if case == 'accepted' else 192 if case == 'wrong_bus' else 193
address = 0x110 if case == 'wrong_address' else 0xCB
recovery.observe([(now[0], [(address, data, bus)])], 11)
assert recovery.take_rejection(11) == (case in ('current','duplicate'))
assert not recovery.take_rejection(11)
recovery.observe([(now[0]+1, [(address, data, bus)])], 12)
assert not recovery.take_rejection(12)
@pytest.mark.parametrize('identity', (CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9))
def test_long_controller_consumes_current_rejection_without_retaining_filtered_history(identity):
now = [1_000_000_000]
with patch('time.monotonic_ns', side_effect=lambda: now[0]):
ci, packer = controller(identity, True, True)
assert ci.CC.angle_tx_recovery is not None, 'LONG_REJECTION_RECOVERY_MISSING'
cs = ci.CS
cs.out.vEgo = cs.out.vEgoRaw = 5.
cs.out.steeringAngleDeg = cs.angle_steering_angle = -20.
command = structs.CarControl(enabled=True, latActive=True)
command.actuators.steeringAngleDeg = -100.
ci.CC.apply_angle_last = -70.
ci.CC.ev9_angle_filter.x = -70.
ci.CC.angle_gain_last = .692
ci.CC.frame = 21
_, frames = ci.apply(command.as_reader(), now[0])
emitted = next(frame for frame in frames if frame[0] == 0xCB)
now[0] += 10_000_000
ci.update([(now[0], [(emitted[0], emitted[1], 193)])])
# Current physical angle is the new native limiter baseline after rejection.
cs.out.canValid, cs.out.canTimeout = True, False
cs.out.steeringAngleDeg = cs.angle_steering_angle = -20.
_, frames = ci.apply(command.as_reader(), now[0])
assert abs(ci.CC.apply_angle_last + 20.) <= 5.
assert ci.CC.angle_gain_last == .004
@@ -0,0 +1,215 @@
"""Keep factory SCC until an inactive producer is current; never bypass native RX."""
import os
from unittest.mock import patch
import pytest
from opendbc.car import Bus, structs
from opendbc.car.hyundai.ecu_startup import Outcome
from opendbc.car.hyundai.interface import CarInterface
from opendbc.car.hyundai.radar_interface import RadarInterface
from opendbc.car.hyundai.tests.test_torque_ev_startup import Keeper
from opendbc.car.hyundai.tests.test_generic_hda2_startup_lifecycle import physical_frames as source_frames
from opendbc.car.hyundai.tests.test_ccnc_six import params
from opendbc.car.hyundai.values import CAR
from opendbc.car.hyundai.canfd_stock_aol import qualified_long
from opendbc.car.hyundai.torque_ev_scc import eligible as ev_scc_eligible
from opendbc.safety.tests.libsafety import libsafety_py
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.car.card import Car
from openpilot.starpilot.vehicle_startup import VehicleStartupOwner
@pytest.mark.parametrize("scenario", (
"healthy_suppressed", "healthy_positive", "stale_control", "active_control", "missing_source",
"native_mode", "cruise_fault", "stock_engaged", "disable_negative", "disable_malformed", "restore_failure", "publisher_failure"))
def test_actual_card_defers_carnival_until_initialized_and_then_native_rx(scenario):
stock = params(CAR.KIA_CARNIVAL_2025, hda2=True, alt_buttons=True, alpha=True, adas=True, alt_lka=True)
assert stock.safetyConfigs[0].safetyParam == 0x20B4 and qualified_long(stock) and not ev_scc_eligible(stock)
native = libsafety_py.libsafety
assert native.set_safety_hooks(int(structs.CarParams.SafetyModel.elm327), 1) == 0
native.init_tests()
now = [100_000_000_000]
requests, emitted = [], []
muted = [False]
def send(frames):
for address, data, bus in frames:
assert native.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
emitted.extend(frames)
if scenario == "publisher_failure" and any(data[:4] == b"\x03\x28\x83\x01" for _, data, _ in frames):
muted[0] = True
raise OSError("diagnostic publisher failed after possible ECU delivery")
def query(sender, receiver, bus, addresses, commands, responses):
command = commands[0]
requests.append((now[0], command))
class Query:
def get_data(self, timeout):
sender([(0x730, bytes([len(command)]) + command + bytes(7 - len(command)), bus)])
if command == b"\x28\x83\x01":
muted[0] = True
if scenario in ("disable_negative", "restore_failure"):
return {(0x730, None): b"\x7f\x28\x22"}
if scenario == "disable_malformed":
return {(0x730, None): b"\x68"}
return {(0x730, None): b"\x68\x03"} if scenario == "healthy_positive" else {}
if command == b"\x28\x00\x01":
if scenario == "restore_failure":
return {}
muted[0] = False
return {(0x730, None): b""}
return Query()
def sleep(seconds):
now[0] += round(seconds * 1e9)
def update_sm(host, command, stale=False):
message = messaging.new_message("carControl", valid=True, logMonoTime=now[0] - (151_000_000 if stale else 0))
message.carControl = command
panda = messaging.new_message("pandaStates", 1, valid=True, logMonoTime=now[0])
state = panda.pandaStates[0]
state.safetyModel, state.safetyParam = native.get_current_safety_mode(), native.get_current_safety_param()
state.alternativeExperience = host.CP.alternativeExperience
state.ignitionLine = True
state.safetyRxChecksInvalid = native.safety_test_toyota_rx_summary_invalid()
host.sm.update_msgs(now[0] / 1e9, [message.as_reader(), panda.as_reader()])
with OpenpilotPrefix(), patch.dict(os.environ, {"SIMULATION": "0", "AOL_REPLAY_RUNTIME": "1"}), \
patch("time.clock_gettime_ns", side_effect=lambda clock: now[0]), \
patch("time.monotonic_ns", side_effect=lambda: now[0]), \
patch("time.monotonic", side_effect=lambda: now[0] / 1e9), \
patch("time.sleep", side_effect=sleep):
saved = Params()
for key in ("OpenpilotEnabledToggle", "AlwaysOnLateral", "AlphaLongitudinalEnabled"):
saved.put_bool(key, True, block=True)
holder = VehicleStartupOwner()
prepared = holder.prepare(stock, CarInterface, (list, send), requested=True,
admission=lambda: True,
defer_until_controls=True)
startup = holder.owner
assert startup is not None
startup.clock, startup.keeper_factory, startup._query = lambda: now[0] / 1e9, Keeper, query
try:
assert startup.outcome is Outcome.PENDING and not requests and not emitted
assert not startup.before_control(configured=True, sources_current=True, control_current=True)
card = Car(CI=CarInterface(prepared), RI=RadarInterface(prepared), startup_owner=startup)
startup.admission = card.startup_diagnostic_admission
card.params.put_bool("IsOffroad", False, block=True)
original_cp = card.CP.to_dict()
assert card.CP.safetyConfigs[0].safetyParam == 0x28B4 and card.CP.alternativeExperience == 32
assert not ev_scc_eligible(card.CP)
# Explicit synthetic delayed producer: stock has not been told to stop.
for tick in range(1, 701):
now[0] += 10_000_000
state = card.CI.update([(now[0], [f for f in source_frames(card.CI, tick) if f[0] not in (0x1A0, 0x1CF, 0x160)])])
assert state.canValid and not state.canTimeout and not state.accFaulted
assert not requests and not muted[0] and startup.keeper is None
command = structs.CarControl(enabled=scenario == "active_control", latActive=scenario == "active_control").as_reader()
if scenario == "missing_source":
parser = card.CI.can_parsers[Bus.pt]
parser.message_states[parser.dbc.name_to_msg["TCS"].address].timestamps.clear()
if scenario == "cruise_fault":
state.accFaulted = True
if scenario == "stock_engaged":
state.cruiseState.enabled = True
config = card.CP.safetyConfigs[-1]
if scenario == "native_mode":
native.set_alternative_experience(card.CP.alternativeExperience)
assert native.set_safety_hooks(int(config.safetyModel.raw), int(config.safetyParam)) == 0
update_sm(card, command, stale=scenario == "stale_control")
card.controls_update(state, command, initialize_only=True)
assert not requests and not muted[0] and not card.ci_initialized
if scenario in ("disable_negative", "disable_malformed", "restore_failure", "publisher_failure"):
with pytest.raises(RuntimeError, match="pending"):
card.controls_update(state, command)
assert startup.outcome is Outcome.ABORT_UNCERTAIN and not card.ci_initialized
assert card.CP.to_dict() == original_cp and not startup.handed_off
assert muted[0] == (scenario == "restore_failure")
assert startup.restored == (scenario != "restore_failure")
return
card.controls_update(state, command)
if not scenario.startswith("healthy"):
assert not requests and not muted[0] and not card.ci_initialized
assert startup.outcome is Outcome.PENDING and card.CP.to_dict() == original_cp
return
assert [command for _, command in requests] == [b"\x10\x03", b"\x28\x83\x01"]
assert muted[0] and card.ci_initialized and startup.outcome is Outcome.SENT_UNCONFIRMED
assert startup.keeper.started and not startup.handed_off
# No SCC while native is diagnostic, mismatched, or awaiting RX health.
emitted.clear()
card.controls_update(state, command)
assert not emitted and not startup.handed_off
native.set_alternative_experience(card.CP.alternativeExperience)
assert native.set_safety_hooks(int(config.safetyModel.raw), int(config.safetyParam)) == 0
native.init_tests()
update_sm(card, command)
card.controls_update(state, command)
assert not emitted and not startup.handed_off
for tick in range(701, 811):
now[0] += 10_000_000
frames = [f for f in source_frames(card.CI, tick) if f[0] not in (0x1A0, 0x1CF, 0x160)]
state = card.CI.update([(now[0], frames)])
native.set_timer(tick * 10_000)
for address, data, bus in frames:
native.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data))
if tick % 100 == 0:
native.safety_tick()
assert native.safety_config_valid()
update_sm(card, command)
captured = []
def publish(frames, valid=True):
if valid:
send(frames)
card.vehicle_startup.sent(frames, valid=valid)
captured.extend(frames)
with patch.object(card, "publish_sendcan", side_effect=publish):
card.controls_update(state, command)
assert any(address == 0x1A0 and bus == 1 for address, _, bus in captured)
decoder = card.CI.can_parsers[Bus.pt]
decoder.update((now[0], captured))
scc = decoder.vl['SCC_CONTROL']
assert scc['ACCMode'] == 0 and scc['aReqRaw'] == 0 and scc['aReqValue'] == 0
assert startup.handed_off and not startup.keeper.started
assert not native.get_controls_allowed() and native.aol_get_permission_mask() == 0
assert card.CP.to_dict() == original_cp
finally:
holder.close()
def test_refused_takeover_preserves_exact_stock_aol_and_sends_no_disable():
from opendbc.car.hyundai.ioniq6_handoff import TimestampedCanPacket
from opendbc.car.hyundai.generic_hda2_startup import GenericHDA2Startup
from opendbc.car.hyundai.canfd_stock_aol import qualified as stock_qualified
cp = params(CAR.KIA_CARNIVAL_2025, hda2=True, alt_buttons=True, alpha=True, adas=True, alt_lka=True)
now, tick, emitted = [100_000_000_000], [0], []
startup = None
def receive():
tick[0] += 1
now[0] += 10_000_000
return [TimestampedCanPacket(source_frames(startup.ci, tick[0]), now[0])]
with patch('time.clock_gettime_ns', side_effect=lambda clock: now[0]), \
patch('time.monotonic', side_effect=lambda: now[0] / 1e9), patch('time.sleep'):
startup = GenericHDA2Startup(cp, (receive, emitted.extend), clock=lambda: now[0] / 1e9, keeper_factory=Keeper)
prepared = startup.prepare_when_controls_ready(admission=lambda: False)
assert not prepared.openpilotLongitudinalControl and prepared.pcmCruise and not emitted
ci = CarInterface(prepared)
try:
startup.configure(ci)
assert startup.ready and ci.CS.out.canValid and startup.sources_current(ci, now[0])
ci.CP.safetyConfigs[0].safetyParam |= 0x800
assert ci.CP.safetyConfigs[0].safetyParam == 0x28B0 and stock_qualified(ci.CP, marked_only=True)
startup.finalize_aol_configuration(ci)
startup.seal_publication()
assert startup.prepared_for(ci.CP) and not emitted
assert startup.outcome is Outcome.STOCK_UNTOUCHED and startup.keeper is None
finally:
startup.close()
@@ -0,0 +1,51 @@
import pytest
from opendbc.car import Bus
from opendbc.car.hyundai.tests.test_ccnc_six import params
from opendbc.car.hyundai.values import CAR, HyundaiFlags
from opendbc.car.hyundai.canfd_stock_aol import qualified_long
from opendbc.car.hyundai.generic_hda2_startup import qualified
from opendbc.car.hyundai.carstate import CarState
def carnival():
return params(CAR.KIA_CARNIVAL_2025, hda2=True, alt_buttons=True, alpha=True, adas=True, alt_lka=True)
@pytest.mark.parametrize('word,experience,expected', [
(0x20B4,0,True), (0x28B4,32,True), (0x28B4,0,False), (0x20B4,32,False),
(0x28B6,32,False), (0x28B0,32,False), (0x8B4,32,False), (0x28B5,32,False),
])
def test_exact_profile(word, experience, expected):
cp = carnival()
cp.safetyConfigs[0].safetyParam, cp.alternativeExperience = word, experience
assert qualified_long(cp) == expected
@pytest.mark.parametrize('mutation', ['hybrid','other_car','passive','dashcam','wrong_model','angle','other_bus'])
def test_reject_other_configuration(mutation):
cp = carnival()
if mutation == 'hybrid':
cp.flags = int(cp.flags | HyundaiFlags.HYBRID)
elif mutation == 'other_car':
cp.carFingerprint = CAR.KIA_CARNIVAL_HEV_4TH_GEN
elif mutation == 'passive':
cp.passive = True
elif mutation == 'dashcam':
cp.dashcamOnly = True
elif mutation == 'angle':
cp.flags = int(cp.flags | HyundaiFlags.CANFD_ANGLE_STEERING)
elif mutation == 'other_bus':
cp.safetyConfigs = [dict(safetyModel='noOutput'),dict(safetyModel='hyundaiCanfd',safetyParam=0x20B4)]
else:
cp.safetyConfigs[0].safetyModel = 'noOutput'
assert not qualified(cp)
def test_takeover_health_binds_physical_sources_not_removed_scc():
cp = carnival()
parser = CarState(cp).get_can_parsers(cp)[Bus.pt]
assert parser.message_states[0x1AA].frequency == 50
assert 0x1CF not in parser.addresses
assert not parser.message_states[0x1A0].frequency > 0
for address in (0x100,0x175,0xA0,0xEA):
assert parser.message_states[address].frequency > 0
@@ -0,0 +1,72 @@
from unittest.mock import patch
import pytest
from opendbc.car.hyundai import ev9_longitudinal, ioniq5pe_long_profile
from opendbc.car.hyundai.tests.test_stock_angle_aol import source_profile
from opendbc.car.hyundai.values import CAR, HyundaiFlags
def profile(identity):
module = ioniq5pe_long_profile if identity == CAR.HYUNDAI_IONIQ_5_PE else ev9_longitudinal
cp = module.candidate(source_profile(identity), enabled=True, is_release=False)
return module, cp
@pytest.mark.parametrize('identity', (CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9))
@pytest.mark.parametrize('reader', (False, True))
def test_canonical_long_validation_does_not_clone_carparams(identity, reader):
module, cp = profile(identity)
cp.alternativeExperience = 32
value = cp.as_reader() if reader else cp
before = cp.to_dict()
with patch.object(module, 'copy_cp', side_effect=AssertionError('CYCLIC_LONG_QUALIFICATION_CLONED_CP')):
assert module.qualified(value)
cp.passive = True
assert not module.qualified(value)
cp.passive = False
assert module.qualified(value)
assert cp.to_dict() == before
@pytest.mark.parametrize('identity', (CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9))
@pytest.mark.parametrize('mutation', ('passive','dashcam','notcar','brand','fuel','long','pcm','experience','available','word','model','slots'))
def test_every_admission_field_is_still_checked_on_every_call(identity, mutation):
module, cp = profile(identity)
assert module.qualified(cp)
if mutation == 'passive':
cp.passive = True
elif mutation == 'dashcam':
cp.dashcamOnly = True
elif mutation == 'notcar':
cp.notCar = True
elif mutation == 'brand':
cp.brand = 'toyota'
elif mutation == 'fuel':
cp.flags = int(cp.flags | HyundaiFlags.HYBRID)
elif mutation == 'long':
cp.openpilotLongitudinalControl = False
elif mutation == 'pcm':
cp.pcmCruise = True
elif mutation == 'experience':
cp.alternativeExperience = 33
elif mutation == 'available':
cp.alphaLongitudinalAvailable = False
elif mutation == 'word':
cp.safetyConfigs[0].safetyParam ^= 4
elif mutation == 'model':
cp.safetyConfigs[0].safetyModel = 'noOutput'
else:
cp.safetyConfigs = [cp.safetyConfigs[0].to_dict(), cp.safetyConfigs[0].to_dict()]
assert not module.qualified(cp)
assert not module.qualified(cp.as_reader())
@pytest.mark.parametrize('field,value', [('startingState',False),('startAccel',.5),('vEgoStarting',.5),('vEgoStopping',.4),
('stoppingDecelRate',.5),('startAccel',float('nan')),('startAccel',float('inf'))])
def test_pe_frozen_longitudinal_values_still_refuse_mutation(field, value):
module, cp = profile(CAR.HYUNDAI_IONIQ_5_PE)
assert module.qualified(cp)
setattr(cp.deprecated, field, value)
assert not module.qualified(cp)
assert not module.qualified(cp.as_reader())
@@ -131,7 +131,7 @@ class TestCcncSix(unittest.TestCase):
safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, cp.safetyConfigs[-1].safetyParam)
safety.init_tests()
bus = can.ECAN
source_values = {"COUNTER": 17, "SET_ME_1": 1, "CRUISE_BUTTONS": 0, "BYTE6": 0x55}
source_values: dict[str, float] = {"COUNTER": 17, "SET_ME_1": 1, "CRUISE_BUTTONS": 0, "BYTE6": 0x55}
addr, raw, _ = packer.make_can_msg("CRUISE_BUTTONS_ALT", bus, source_values)
data = bytearray(raw)
data[0:2] = hkg_can_fd_checksum(addr, None, data).to_bytes(2, "little")
@@ -163,6 +163,12 @@ class TestCcncSix(unittest.TestCase):
active_source[0:2] = hkg_can_fd_checksum(addr, None, active_source).to_bytes(2, "little")
self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, active_source)))
self.assertFalse(safety.safety_tx_hook(tx))
source["COUNTER"] = source_values["COUNTER"] = 19
_, raw, _ = packer.make_can_msg("CRUISE_BUTTONS_ALT", bus, source_values)
data = bytearray(raw)
data[0:2] = hkg_can_fd_checksum(addr, None, data).to_bytes(2, "little")
resume = create_carnival_alt_resume(packer, cp, can, source)
tx = libsafety_py.make_CANPacket(resume[0], resume[2], resume[1])
self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, bytes(data))))
wrong_counter = bytearray(resume[1])
wrong_counter[2] += 1
@@ -177,8 +183,8 @@ class TestCcncSix(unittest.TestCase):
safety.set_timer(0)
self.assertTrue(safety.safety_tx_hook(tx))
source["COUNTER"] = 19
source_values["COUNTER"] = 19
source["COUNTER"] = 20
source_values["COUNTER"] = 20
source_values["ADAPTIVE_CRUISE_MAIN_BTN"] = 1
_, raw, _ = packer.make_can_msg("CRUISE_BUTTONS_ALT", bus, source_values)
data = bytearray(raw)
@@ -226,3 +232,37 @@ class TestCcncSix(unittest.TestCase):
wrap_resume = create_carnival_alt_resume(packer, cp, can, {**source, "COUNTER": 255, "CRUISE_BUTTONS": 0})
self.assertEqual(wrap_resume[1][2], 0)
self.assertTrue(safety.safety_tx_hook(libsafety_py.make_CANPacket(wrap_resume[0], wrap_resume[2], wrap_resume[1])))
def test_ccnc_owned_warnings_preserve_critical_and_obstruction_fields(self):
from opendbc.can import CANParser
from opendbc.car import structs
from opendbc.car.hyundai.hyundaicanfd import create_ccnc
packer = CANPacker('hyundai_canfd_generated')
cp = params(CAR.KIA_CARNIVAL_HEV_4TH_GEN, alt_buttons=True)
critical = ('FAULT_FSS', 'FAULT_FCA', 'FAULT_SCC', 'FAULT_ESS')
owned = ('FAULT_LSS', 'FAULT_HDA', 'FAULT_DAS', 'FAULT_LFA', 'FAULT_DAW')
for active in (False, True):
for fault_value in (1, 2, 3):
for alert in (5, 7, 8, 9, 10, 14, 21):
with self.subTest(active=active, fault_value=fault_value, alert=alert):
first = {name: 0 for name in packer.dbc.name_to_msg['CCNC_0x161'].sigs}
second = {name: 0 for name in packer.dbc.name_to_msg['CCNC_0x162'].sigs}
first.update(COUNTER=17, ALERTS_2=alert, SOUNDS_2=3, DAW_ICON=1, ALERTS_3=17, ALERTS_5=5)
second.update({name: fault_value for name in (*critical, *owned)})
second['COUNTER'] = 23
hud = structs.CarControl.HUDControl()
state = structs.CarState(vEgo=20., steeringAngleDeg=0.)
frames = create_ccnc(packer, CanBus(cp), False, active, hud, False, False,
first, second, None, True, state, True, active)
parser = CANParser('hyundai_canfd_generated', [('CCNC_0x161', 0), ('CCNC_0x162', 0)], CanBus(cp).ECAN)
parser.update((1_000_000_000, frames))
a, b = parser.vl['CCNC_0x161'], parser.vl['CCNC_0x162']
self.assertEqual(a['ALERTS_2'], 0 if active and alert == 5 else alert)
self.assertEqual(a['SOUNDS_2'], 0 if active and alert == 5 else 3)
self.assertEqual(a['DAW_ICON'], 0 if active else 1)
self.assertEqual((a['COUNTER'], b['COUNTER']), (17, 23))
for name in critical:
self.assertEqual(b[name], fault_value)
for name in owned:
self.assertEqual(b[name], 0 if active and fault_value == 1 else fault_value)
@@ -0,0 +1,139 @@
import pytest
from opendbc.car import CanData, gen_empty_fingerprint, structs
from opendbc.car.hyundai.daw import DawSource
from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum
from opendbc.car.hyundai.interface import CarInterface
from opendbc.car.hyundai.tests.test_ioniq6_stock_parser import params as ioniq6_params
from opendbc.car.hyundai.values import CAR
BASE = 1_000_000_000
def params(canfd):
return ioniq6_params(True)[0] if canfd else CarInterface.get_params(
CAR.HYUNDAI_SONATA_HYBRID, gen_empty_fingerprint(), [], False, False, False)
def source(canfd, status):
data = bytearray.fromhex('1234567898f8') if not canfd else bytearray(range(16))
if canfd:
data[7] = (data[7] & ~0x38) | (status << 3)
data[:2] = hkg_can_fd_checksum(0x11A, None, data).to_bytes(2, 'little')
else:
data[5] = (data[5] & ~7) | status
return bytes(data)
@pytest.mark.parametrize('status', range(8))
def test_exact_current_advisory_only_transform(status):
canfd = False
owner = DawSource(params(canfd))
data = source(canfd, status)
assert not owner.command(BASE)
owner.update([(BASE, [CanData(owner.address, data, owner.source_bus)])])
command, = owner.command(BASE)
expected = bytearray(data)
if canfd:
if status == 1:
expected[7] &= ~0x38
expected[:2] = hkg_can_fd_checksum(owner.address, None, expected).to_bytes(2, 'little')
elif 1 <= status <= 3:
expected[5] &= ~7
assert command == CanData(owner.address, bytes(expected), owner.target_bus)
assert owner.data == data
assert owner.command(BASE + 100_000_000)
assert not owner.command(BASE + 100_000_001)
@pytest.mark.parametrize('canfd,fault', [(canfd, fault) for canfd in (False, True)
for fault in ('future', 'old', 'reversed', 'length', 'returned', 'wrong_bus', 'wrong_address', 'crc')
if canfd or fault != 'crc'])
def test_bad_or_missing_sources_cannot_create_replacements(canfd, fault):
owner = DawSource(params(canfd))
data = source(canfd, 1)
stamp, bus, addr = BASE, owner.source_bus, owner.address
if fault == 'length':
data = data[:-1]
elif fault == 'returned':
bus += 128
elif fault == 'wrong_bus':
bus = 0 if canfd else 1
elif fault == 'wrong_address':
addr += 1
elif fault == 'crc':
data = bytes([data[0] ^ 1]) + data[1:]
elif fault == 'reversed':
owner.update([(BASE + 1, [CanData(addr, data, bus)])])
owner.update([(stamp, [CanData(addr, data, bus)])])
now = BASE - 1 if fault == 'future' else BASE + 100_000_001 if fault == 'old' else BASE
assert not owner.command(now)
def test_actual_parser_and_controller_keep_optional_message_out_of_driving_health():
cp = params(False)
before = cp.to_dict()
first, second = CarInterface(cp.as_reader().as_builder()), CarInterface(cp.as_reader().as_builder())
data = bytes.fromhex('000000000883')
plain = first.update([(BASE, [])])
observed = second.update([(BASE, [CanData(0x53E, data, 2)])])
assert observed.to_dict() == plain.to_dict()
control = structs.CarControl()
baseline = first.apply(control.as_reader(), BASE)[1]
actual = second.apply(control.as_reader(), BASE)[1]
assert [frame for frame in actual if frame[0] != 0x53E] == baseline
assert [frame for frame in actual if frame[0] == 0x53E] == [CanData(0x53E, bytes.fromhex('000000000880'), 0)]
assert cp.to_dict() == before
def test_unrelated_canfd_profiles_do_not_emit_new_frames():
cp = CarInterface.get_params(CAR.KIA_EV6, gen_empty_fingerprint(), [], False, False, False)
owner = DawSource(cp)
assert not owner.enabled
owner.update([(BASE, [CanData(0x11A, source(True, 1), 1)])])
assert not owner.command(BASE)
@pytest.mark.parametrize('word', (0, 1, 9, 0x15, 0x5491, 0x5495))
def test_unsupported_ioniq6_native_profiles_cannot_emit_advisories(word):
cp = params(True)
cp.safetyConfigs[-1].safetyParam = word
owner = DawSource(cp)
assert not owner.enabled
owner.update([(BASE, [CanData(0x11A, source(True, 1), owner.source_bus)])])
assert not owner.command(BASE)
@pytest.mark.parametrize('word', (0x11, 0x91, 0x811, 0x891, 0x8015, 0x8095, 0x8815, 0x8895))
@pytest.mark.parametrize('status', range(8))
def test_live_same_bus_canfd_source_is_never_retransmitted(word, status):
cp = params(True)
cp.safetyConfigs[-1].safetyParam = word
owner = DawSource(cp)
assert owner.enabled and owner.source_bus == owner.target_bus
for tick in range(20):
stamp = BASE + tick * 10_000_000
data = bytearray(source(True, status))
data[2] = tick
data[:2] = hkg_can_fd_checksum(owner.address, None, data).to_bytes(2, 'little')
owner.update([(stamp, [CanData(owner.address, bytes(data), owner.source_bus)])])
assert owner.data == bytes(data)
assert owner.command(stamp) == []
assert owner.command(stamp + 5_000_000) == []
@pytest.mark.parametrize('alternate', (False, True))
def test_actual_ioniq6_controller_keeps_live_source_off_sendcan(alternate):
cp, _ = ioniq6_params(alternate)
before = cp.to_dict()
first, second = CarInterface(cp.as_reader().as_builder()), CarInterface(cp.as_reader().as_builder())
data = source(True, 1)
plain = first.update([(BASE, [])])
observed = second.update([(BASE, [CanData(0x11A, data, 1)])])
assert observed.to_dict() == plain.to_dict()
control = structs.CarControl()
baseline = first.apply(control.as_reader(), BASE)[1]
actual = second.apply(control.as_reader(), BASE)[1]
assert actual == baseline
assert all(frame[0] != 0x11A for frame in actual)
assert cp.to_dict() == before
@@ -271,16 +271,16 @@ class TestEV9FreshMetadataQualification(unittest.TestCase):
cp.safetyConfigs[0].safetyParam ^= 1
self.assertTrue(qualified(cp))
def test_valid_builder_avoids_copy_and_reader_keeps_copy_contract(self):
def test_typed_builder_and_reader_avoid_copy_with_original_semantics(self):
from unittest.mock import patch
from opendbc.car.hyundai import ev9_longitudinal
cp = self.base()
with patch.object(ev9_longitudinal, 'stock_copy', side_effect=AssertionError('unexpected whole CP copy')):
self.assertTrue(qualified(cp))
with structs.CarParams.from_bytes(cp.to_bytes()) as reader:
with patch.object(ev9_longitudinal, 'stock_copy', wraps=ev9_longitudinal.stock_copy) as copied:
self.assertTrue(qualified(reader))
self.assertEqual(copied.call_count, 1)
expected = self.original(reader)
with patch.object(ev9_longitudinal, 'stock_copy', side_effect=AssertionError('unexpected whole CP copy')):
self.assertEqual(qualified(reader), expected)
def test_custom_owner_keeps_original_access_and_copy_behavior(self):
cp = self.base()
@@ -149,7 +149,7 @@ class TestHyundaiFingerprint(unittest.TestCase):
self.assertEqual(output_parser.vl["CCNC_0x162"]["COUNTER"], 9)
self.assertEqual(output_parser.vl["CCNC_0x161"]["BACKGROUND"], 3)
self.assertEqual(output_parser.vl["CCNC_0x162"]["SPEEDLIMIT"], 60)
self.assertEqual(output_parser.vl["CCNC_0x162"]["FAULT_DAS"], 1)
self.assertEqual(output_parser.vl["CCNC_0x162"]["FAULT_DAS"], 0)
self.assertEqual(output_parser.vl["CCNC_0x162"]["LEAD_DISTANCE"], 47 if alpha_long else 25)
for signal, expected in (("ALERTS_3", 7), ("ALERTS_5", 4), ("SOUNDS_3", 5)):
self.assertEqual(output_parser.vl["CCNC_0x161"][signal], expected)
@@ -1,9 +1,11 @@
import json
import unittest
from pathlib import Path
from types import SimpleNamespace
from opendbc.can import CANPacker
from opendbc.car import Bus, CanData, gen_empty_fingerprint, structs
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.hyundai.carcontroller import CarController
from opendbc.car.hyundai.hyundaicanfd import create_ioniq6_blindspot_status, hkg_can_fd_checksum
from opendbc.car.hyundai.ioniq6_bsm import BlindspotStatus, Ioniq6BlindspotSources
@@ -118,6 +120,7 @@ class TestIoniq6BlindspotStatus(unittest.TestCase):
t = 10_000_000_000 # CAN BOOTTIME; controller below sees MONOTONIC.
ci.update([(t, [packer.make_can_msg("BLINDSPOTS_FRONT_CORNER_2", 1, {"SIDE_DETECT_STATE": 0x10}),
packer.make_can_msg("BLINKERS", 1, {})])])
ci.CS.out.vEgoRaw = 10.0
control = structs.CarControl()
_, frames = controller.update(control.as_reader(), ci.CS, t - 9_000_000_000)
rear_front = [(addr, payload, bus) for addr, payload, bus in frames if addr in (0x1BA, 0x1E5)]
@@ -126,6 +129,7 @@ class TestIoniq6BlindspotStatus(unittest.TestCase):
controller.update(control.as_reader(), ci.CS, t - 9_000_000_000 + i * 10_000_000)
ci.update([(t + 50_000_000, [packer.make_can_msg("BLINDSPOTS_FRONT_CORNER_2", 1, {"SIDE_DETECT_STATE": 0x10}),
packer.make_can_msg("BLINKERS", 1, {})])])
ci.CS.out.vEgoRaw = 10.0
_, next_frames = controller.update(control.as_reader(), ci.CS, t - 9_000_000_000 + 50_000_000)
self.assertEqual([(addr, payload, bus) for addr, payload, bus in next_frames if addr in (0x1BA, 0x1E5)],
create_ioniq6_blindspot_status(1, BlindspotStatus(1, 0)))
@@ -136,3 +140,45 @@ class TestIoniq6BlindspotStatus(unittest.TestCase):
ci.update([(t + 160_000_000, [packer.make_can_msg("WHEEL_SPEEDS", 1, {})])])
_, stale = controller.update(control.as_reader(), ci.CS, t - 9_000_000_000 + 160_000_000)
self.assertFalse(any(addr in (0x1BA, 0x1E5) for addr, _, _ in stale))
def test_controller_low_speed_indication_boundary_and_source_preservation(self):
cp = long_params()
ci = CarInterface(cp)
controller = CarController(DBC[cp.carFingerprint], cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
control = structs.CarControl()
controller.ioniq6_bsm_counter = 250 # Exercise rollover while crossing the speed threshold.
threshold = 5 * CV.MPH_TO_MS
# Include repeated crossings; each fresh pair keeps the rolling counter.
speeds = (0.0, threshold - 1e-6, threshold, threshold + 1e-6, 10.0, 0.0, threshold)
for left, right in ((False, False), (True, False), (False, True), (True, True)):
for left_lamp, right_lamp in ((False, False), (True, False), (False, True), (True, True)):
for speed in speeds:
with self.subTest(left=left, right=right, lamps=(left_lamp, right_lamp), speed=speed):
t = 10_000_000_000 + controller.frame * 10_000_000
ci.update([(t, [packer.make_can_msg("BLINDSPOTS_FRONT_CORNER_2", 1,
{"SIDE_DETECT_STATE": (0x10 if left else 0) | (0x08 if right else 0)}),
packer.make_can_msg("BLINKERS", 1, {})])])
# Exact float boundary without Cap'n Proto Float32 quantization.
out = ci.CS.out
ci.CS.out = SimpleNamespace(**{name: getattr(out, name) for name in out.schema.fields})
ci.CS.out.vEgoRaw = speed
sources = Ioniq6BlindspotSources()
sources.observe(corner_source_ns=t, corner_state=(0x10 if left else 0) | (0x08 if right else 0),
lamp_source_ns=t, left_lamp=left_lamp, right_lamp=right_lamp)
ci.CS.ioniq6_bsm_sources = sources
source_status = sources.status(t)
counter = controller.ioniq6_bsm_counter
_, frames = controller.update(control.as_reader(), ci.CS, t)
status_frames = [(addr, payload, bus) for addr, payload, bus in frames if addr in (0x1BA, 0x1E5)]
expected = BlindspotStatus(0, 0) if speed < threshold else source_status
self.assertEqual(status_frames, create_ioniq6_blindspot_status(counter, expected))
self.assertEqual(sources.status(t), source_status)
for addr, payload, _ in status_frames:
self.assertEqual(payload[2], counter)
self.assertEqual(int.from_bytes(payload[:2], "little"), hkg_can_fd_checksum(addr, None, bytearray(payload)))
# The speed filter does not change the existing paired-send cadence.
for i in range(1, 5):
_, skipped = controller.update(control.as_reader(), ci.CS, t + i * 10_000_000)
self.assertFalse(any(addr in (0x1BA, 0x1E5) for addr, _, _ in skipped))
ci.CS.out = out
@@ -870,7 +870,7 @@ class TestIoniq6PrepublicationHandoff(unittest.TestCase):
card_owner.is_metric = True
card_owner.experimental_mode = False
card_owner.initial_cruise_speed = 40
card_owner.v_cruise_helper = SimpleNamespace(update_v_cruise=lambda *_, slc_enabled=False, slc_approved=None: None,
card_owner.v_cruise_helper = SimpleNamespace(update_v_cruise=lambda *_, slc_enabled=False, wheel_context=None: None,
initialize_v_cruise=lambda *_, **kwargs: None, slc_consumed_button=None, toyota=None,
slc_cruise_change=None, v_cruise_kph=60.0, v_cruise_cluster_kph=60.0)
card_owner.RI = SimpleNamespace(update=lambda _: None)
@@ -8,7 +8,7 @@ from opendbc.car.hyundai.canfd_stock_aol import qualified_long
def eligible(cp):
from opendbc.car.hyundai.values import CAR
# Genesis keeps its existing longitudinal controller.
return cp.carFingerprint not in (CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, CAR.GENESIS_GV60_EV_1ST_GEN) and qualified_long(cp)
return cp.carFingerprint not in (CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, CAR.GENESIS_GV60_EV_1ST_GEN, CAR.KIA_CARNIVAL_2025) and qualified_long(cp)
def options(available, control_state, lead):
@@ -96,6 +96,8 @@ class IsoTpParallelQuery:
msg.send(self.request[0], setup_only=len(self.functional_addrs) > 0)
results = {}
interrupted_reads = set()
retried_reads = set()
start_time = time.monotonic()
addrs_responded = set() # track addresses that have ever sent a valid iso-tp frame for timeout logging
response_timeouts = {tx_addr: start_time + timeout for tx_addr in self.msg_addrs}
@@ -126,9 +128,15 @@ class IsoTpParallelQuery:
counter = request_counter[tx_addr]
expected_response = self.response[counter]
request = self.request[counter]
if (len(request) == 3 and request[0] == uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER and
not dat.startswith((bytes([request[0] + 0x40]) + request[1:], b'\x7f' + request[:1]))):
interrupted_reads.add(tx_addr)
continue
response_valid = dat.startswith(expected_response)
if response_valid:
interrupted_reads.discard(tx_addr)
if counter + 1 < len(self.request):
response_timeouts[tx_addr] = time.monotonic() + timeout
msg.send(self.request[counter + 1])
@@ -149,6 +157,11 @@ class IsoTpParallelQuery:
cur_time = time.monotonic()
for tx_addr in response_timeouts:
if cur_time - response_timeouts[tx_addr] > 0:
if not request_done[tx_addr] and tx_addr in interrupted_reads and tx_addr not in retried_reads:
msgs[tx_addr].send(self.request[request_counter[tx_addr]])
response_timeouts[tx_addr] = cur_time + timeout
retried_reads.add(tx_addr)
continue
if not request_done[tx_addr]:
if request_counter[tx_addr] > 0:
carlog.error(f"iso-tp query timeout after receiving partial response: {tx_addr}")
@@ -132,7 +132,8 @@ class CarController(CarControllerBase):
self.apply_angle_last = CS.out.steeringAngleDeg
self.ascent_angle_initialized = True
angle_active = False
if angle_active and not self.angle_was_active and self.CP.carFingerprint != CAR.SUBARU_ASCENT_2023:
if (angle_active and not self.angle_was_active and
self.CP.carFingerprint in (CAR.SUBARU_CROSSTREK_2025, CAR.SUBARU_FORESTER_2022)):
self.apply_angle_last = CS.out.steeringAngleDeg
if self.CP.carFingerprint in (CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023):
apply_angle = apply_std_steer_angle_limits(actuators.steeringAngleDeg, self.apply_angle_last,
@@ -6,7 +6,8 @@ from opendbc.car.interfaces import CarControllerBase
from opendbc.car.tesla.teslacan import TeslaCAN
from opendbc.car.tesla.preap.stock import PreAPStockCarController
from opendbc.car.tesla.hw1 import HW1CarController
from opendbc.car.tesla.values import CAR, CarControllerParams
from opendbc.car.tesla.values import CAR, CarControllerParams, TeslaSafetyFlags
from opendbc.car.tesla.coop_steering import CooperativeSteeringController
from opendbc.car.vehicle_model import VehicleModel
@@ -27,6 +28,9 @@ class CarController(CarControllerBase):
self.hw1 = HW1CarController(dbc_names, CP)
return
self.apply_angle_last = 0
self.coop_steer = CooperativeSteeringController()
self.coop_enabled = CP.carFingerprint == CAR.TESLA_MODEL_3 and bool(CP.safetyConfigs[0].safetyParam & TeslaSafetyFlags.COOP_STEERING)
self.steering_limit_info = {}
self.packer = CANPacker(dbc_names[Bus.party])
self.tesla_can = TeslaCAN(CP, self.packer)
@@ -46,14 +50,27 @@ class CarController(CarControllerBase):
# Tesla EPS enforces disabling steering on heavy lateral override force.
# When enabling in a tight curve, we wait until user reduces steering force to start steering.
# Canceling is done on rising edge and is handled generically with CC.cruiseControl.cancel
lat_active = CC.latActive and CS.hands_on_level < 3
lat_active = CC.latActive and (not CS.out.steeringDisengage if self.coop_enabled else CS.hands_on_level < 3)
if not lat_active:
self.steering_limit_info = {}
if self.frame % 2 == 0:
# Angular rate limit based on speed
self.apply_angle_last = apply_steer_angle_limits_vm(actuators.steeringAngleDeg, self.apply_angle_last, CS.out.vEgoRaw, CS.out.steeringAngleDeg,
lat_active, CarControllerParams, self.VM)
can_sends.append(self.tesla_can.create_steering_control(self.apply_angle_last, lat_active))
command_angle, lat_active = self.coop_steer.update(self.apply_angle_last, lat_active, self.coop_enabled, CS, self.VM)
self.command_angle = command_angle
if self.coop_enabled and lat_active:
self.steering_limit_info = {
"valid": True, "monoTime": now_nanos,
"modelLimitErrorDeg": abs(actuators.steeringAngleDeg - self.apply_angle_last),
"resumeLimitErrorDeg": self.coop_steer.resume_limit_error_deg,
"cooperativeLimitErrorDeg": self.coop_steer.cooperative_limit_error_deg,
"cooperativeOffsetDeg": self.coop_steer.cooperative_offset_deg,
"combinedLimitErrorDeg": abs(actuators.steeringAngleDeg + self.coop_steer.cooperative_offset_deg - command_angle),
}
can_sends.append(self.tesla_can.create_steering_control(command_angle, lat_active))
if self.frame % 10 == 0:
can_sends.append(self.tesla_can.create_steering_allowed())
@@ -74,7 +91,7 @@ class CarController(CarControllerBase):
# TODO: HUD control
new_actuators = actuators.as_builder()
new_actuators.steeringAngleDeg = self.apply_angle_last
new_actuators.steeringAngleDeg = getattr(self, "command_angle", self.apply_angle_last)
self.frame += 1
return new_actuators, can_sends
+4 -2
View File
@@ -5,7 +5,7 @@ from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.interfaces import CarStateBase
from opendbc.car.tesla.preap.stock import PreAPStockCarState, get_stock_parsers
from opendbc.car.tesla.hw1 import HW1CarState, get_hw1_can_parsers
from opendbc.car.tesla.values import CAR, DBC, CANBUS, GEAR_MAP, STEER_THRESHOLD, TeslaFlags
from opendbc.car.tesla.values import CAR, DBC, CANBUS, GEAR_MAP, STEER_THRESHOLD, STEER_DISENGAGE_THRESHOLD, TeslaFlags, TeslaSafetyFlags
class TeslaScreenCANParser(CANParser):
def __init__(self):
@@ -31,6 +31,7 @@ class CarState(CarStateBase):
self.hands_on_level = 0
self.das_control = {}
return
self.cooperative_steering = CP.carFingerprint == CAR.TESLA_MODEL_3 and bool(CP.safetyConfigs[0].safetyParam & TeslaSafetyFlags.COOP_STEERING)
self.can_define = CANDefine(DBC[CP.carFingerprint][Bus.party])
self.shifter_values = self.can_define.dv["DI_systemStatus"]["DI_gear"]
@@ -102,7 +103,8 @@ class CarState(CarStateBase):
# FSD disengages using union of handsOnLevel (slow overrides) and high angle rate faults (fast overrides, high speed)
eac_error_code = self.can_define.dv["EPAS3S_sysStatus"]["EPAS3S_eacErrorCode"].get(int(epas_status["EPAS3S_eacErrorCode"]), None)
ret.steeringDisengage = self.hands_on_level >= 3 or (eac_status == "EAC_INHIBITED" and
eac_error_code == "EAC_ERROR_HIGH_ANGLE_RATE_SAFETY")
eac_error_code == "EAC_ERROR_HIGH_ANGLE_RATE_SAFETY") or (
self.cooperative_steering and abs(ret.steeringTorque) > STEER_DISENGAGE_THRESHOLD)
# Cruise state
cruise_state = self.can_define.dv["DI_state"]["DI_cruiseState"].get(int(cp_party.vl["DI_state"]["DI_cruiseState"]), None)
@@ -0,0 +1,143 @@
import math
import numpy as np
from opendbc.car import DT_CTRL, rate_limit
from opendbc.car.lateral import apply_steer_angle_limits_vm
from opendbc.car.tesla.values import CarControllerParams
from opendbc.car.vehicle_model import VehicleModel
DT_LAT_CTRL = DT_CTRL * CarControllerParams.STEER_STEP
STEER_RESUME_RATE_LIMIT_RAMP_RATE = 300.0 # deg/s^2
STEER_OVERRIDE_MIN_TORQUE = 0.5 # Nm
STEER_OVERRIDE_MAX_TORQUE = 2.5 # Nm
STEER_OVERRIDE_TORQUE_RANGE = STEER_OVERRIDE_MAX_TORQUE - STEER_OVERRIDE_MIN_TORQUE
STEER_OVERRIDE_MAX_LAT_ACCEL = 2.0 # m/s^2
STEER_OVERRIDE_DELTA_GAIN_LIMIT = 125.0 # deg/s/Nm
def apply_bounds(signal: float, limit: float) -> float:
return float(np.clip(signal, -limit, limit))
def apply_deadzone(signal: float, deadzone: float) -> float:
return signal - apply_bounds(signal, deadzone)
def get_steer_from_lat_accel(lat_accel: float, v_ego: float, VM: VehicleModel) -> float:
curvature = lat_accel / max(1.0, v_ego) ** 2
return math.degrees(VM.get_steer_from_curvature(curvature, v_ego, 0.0))
def get_override_torque_to_angle(v_ego: float, VM: VehicleModel) -> float:
max_angle = CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX
steer_from_lat_accel = apply_bounds(get_steer_from_lat_accel(STEER_OVERRIDE_MAX_LAT_ACCEL, v_ego, VM), max_angle)
return steer_from_lat_accel / STEER_OVERRIDE_TORQUE_RANGE
def calc_override_angle_delta_limit(torque: float) -> float:
max_gain = CarControllerParams.ANGLE_LIMITS.MAX_ANGLE_RATE / DT_LAT_CTRL / STEER_OVERRIDE_TORQUE_RANGE
return torque * min(STEER_OVERRIDE_DELTA_GAIN_LIMIT, max_gain) * DT_LAT_CTRL
class SteerRateLimiter:
def __init__(self):
self.last = 0.0
def reset(self, angle: float) -> None:
self.last = angle
def update(self, angle: float, angle_delta_limit: float) -> float:
limited = rate_limit(angle, self.last, -angle_delta_limit, angle_delta_limit)
self.last = limited
return limited
class CooperativeSteeringController:
def __init__(self):
self.apply_angle_last = 0.0
self.coop_apply_angle_last = 0.0
self.angle_override = 0.0
self.resume_rate_limiter_delta = SteerRateLimiter()
self.resume_rate_limiter = SteerRateLimiter()
self.resume_limit_error_deg = 0.0
self.cooperative_limit_error_deg = 0.0
self.cooperative_offset_deg = 0.0
def reset_override_state(self, apply_angle: float) -> None:
self.apply_angle_last = apply_angle
self.angle_override = 0.0
self.coop_apply_angle_last = apply_angle
def reset_resume_state(self, apply_angle: float) -> None:
self.resume_rate_limiter_delta.reset(0.0)
self.resume_rate_limiter.reset(apply_angle)
def update_override_angle(self, apply_angle_delta: float, driver_torque: float, v_ego: float, VM: VehicleModel) -> float:
driver_torque = apply_deadzone(driver_torque, STEER_OVERRIDE_MIN_TORQUE)
torque_to_angle = get_override_torque_to_angle(v_ego, VM)
target_angle = driver_torque * torque_to_angle
holding_torque = self.angle_override / torque_to_angle if abs(v_ego) > 0.1 else 0.0
torque_delta = driver_torque - holding_torque
angle_delta_limit = calc_override_angle_delta_limit(abs(torque_delta))
angle_override_delta = float(np.clip(target_angle - self.angle_override, -angle_delta_limit, angle_delta_limit))
# Avoid counting model-requested motion and driver-requested motion twice.
if angle_override_delta * apply_angle_delta > 0.0:
angle_override_delta -= apply_bounds(apply_angle_delta, abs(angle_override_delta))
self.angle_override += angle_override_delta
return self.angle_override
def unwind_override_angle(self, saturation_error: float) -> None:
if self.angle_override * saturation_error > 0.0:
self.angle_override -= apply_bounds(saturation_error, abs(self.angle_override))
def apply_resume_rate_limit(self, lat_active: bool, apply_angle: float) -> float:
if not lat_active:
self.reset_resume_state(apply_angle)
return apply_angle
angle_rate_delta = self.resume_rate_limiter_delta.update(
CarControllerParams.ANGLE_LIMITS.MAX_ANGLE_RATE,
STEER_RESUME_RATE_LIMIT_RAMP_RATE * DT_LAT_CTRL ** 2,
)
return self.resume_rate_limiter.update(apply_angle, angle_rate_delta)
def update(self, apply_angle: float, lat_active: bool, enabled: bool, CS, VM: VehicleModel) -> tuple[float, bool]:
self.resume_limit_error_deg = 0.0
self.cooperative_limit_error_deg = 0.0
self.cooperative_offset_deg = 0.0
if not enabled:
self.reset_resume_state(apply_angle)
self.reset_override_state(apply_angle)
return apply_angle, lat_active
requested_angle = apply_angle
apply_angle = self.apply_resume_rate_limit(lat_active, apply_angle)
self.resume_limit_error_deg = abs(requested_angle - apply_angle)
if not lat_active:
self.reset_override_state(apply_angle)
return apply_angle, False
apply_angle_delta = apply_angle - self.apply_angle_last
self.apply_angle_last = apply_angle
self.cooperative_offset_deg = self.update_override_angle(apply_angle_delta, CS.out.steeringTorque, CS.out.vEgo, VM)
apply_angle += self.cooperative_offset_deg
limited_angle = apply_steer_angle_limits_vm(
apply_angle,
self.coop_apply_angle_last,
CS.out.vEgoRaw,
CS.out.steeringAngleDeg,
True,
CarControllerParams,
VM,
)
self.coop_apply_angle_last = limited_angle
self.cooperative_limit_error_deg = abs(apply_angle - limited_angle)
self.unwind_override_angle(apply_angle - limited_angle)
return limited_angle, True
@@ -15,6 +15,7 @@ FW_VERSIONS = {
b'TeM3_ES014p11_0.0.0 (25),ES014.19.0',
b'TeM3_E014p10_0.0.0 (24),E014.20.2',
b'TeM3_E014p10_0.0.0 (24),EL014.20.2',
b'TeM3_E014p10_0.0.0 (35),E014.21.2',
b'TeMYG4_DCS_Update_0.0.0 (13),E4014.28.1',
b'TeMYG4_DCS_Update_0.0.0 (9),E4014.26.0',
b'TeMYG4_Legacy3Y_0.0.0 (2),E4015.02.0',
@@ -41,6 +42,7 @@ FW_VERSIONS = {
b'TeM3_E014p10_0.0.0 (16),YP002.18.00',
b'TeM3_E014p10_0.0.0 (24),Y002.21.2',
b'TeM3_E014p10_0.0.0 (24),YP002.21.2',
b'TeM3_E014p10_0.0.0 (35),Y002.22.2',
b'TeM3_ES014p11_0.0.0 (16),YS002.17',
b'TeM3_ES014p11_0.0.0 (25),YS002.19.0',
b'TeMYG4_DCS_Update_0.0.0 (13),Y4002.27.1',
@@ -57,6 +59,7 @@ FW_VERSIONS = {
b'TeMYG4_SingleECU_0.0.0 (28),Y4S002.23.0',
b'TeMYG4_SingleECU_0.0.0 (33),Y4S002.26',
b'TeMYG4_Main_0.0.0 (87),Y4003.09.3',
b'TeMYG4_Main_0.0.0 (92),Y4003.10.2',
],
},
CAR.TESLA_MODEL_X: {
@@ -1,8 +1,8 @@
"""Exact modern Tesla screen harness capability and frozen safety selection."""
from opendbc.car.tesla.values import CAR, TeslaFlags
SCREEN_WORDS = frozenset((512, 513, 1536, 1537))
BASE_WORDS = frozenset((0, 1))
SCREEN_WORDS = frozenset((512, 513, 1536, 1537, 768, 769, 1792, 1793))
BASE_WORDS = frozenset((0, 1, 256, 257))
def screen_tap_supported(cp):
@@ -14,7 +14,8 @@ def screen_tap_supported(cp):
cp.flags & ~int(TeslaFlags.MISSING_DAS_SETTINGS | TeslaFlags.HAS_VEHICLE_BUS | TeslaFlags.AOL_SCREEN_BUTTON)):
return False
word = int(cp.safetyConfigs[0].safetyParam)
return (word in BASE_WORDS | SCREEN_WORDS and bool(word & 1) == bool(cp.openpilotLongitudinalControl) and
return (word in BASE_WORDS | SCREEN_WORDS and (not word & 256 or cp.carFingerprint == CAR.TESLA_MODEL_3) and
bool(word & 1) == bool(cp.openpilotLongitudinalControl) and
bool(word & 512) == bool(cp.flags & TeslaFlags.AOL_SCREEN_BUTTON))
except (AttributeError, ValueError, TypeError, IndexError):
return False
@@ -31,7 +32,7 @@ def screen_qualified(cp, *, marked_only=False):
def apply_screen_button(cp, requested, brake_disengage):
if not screen_tap_supported(cp):
return
base = int(cp.safetyConfigs[0].safetyParam) & 1
base = int(cp.safetyConfigs[0].safetyParam) & 257
cp.flags &= ~int(TeslaFlags.AOL_SCREEN_BUTTON)
cp.safetyConfigs[0].safetyParam = base
if requested:
@@ -0,0 +1,184 @@
import math
from types import SimpleNamespace
import pytest
from opendbc.car import gen_empty_fingerprint
from opendbc.car.tesla.carcontroller import CarController, get_safety_CP
from opendbc.car.tesla.coop_steering import CooperativeSteeringController
from opendbc.car.tesla.interface import CarInterface
from opendbc.car.tesla.values import CAR, DBC, CarControllerParams, TeslaSafetyFlags
from opendbc.car.vehicle_model import VehicleModel
def make_car_state(torque=0.0, speed=15.0, angle=0.0):
return SimpleNamespace(out=SimpleNamespace(
steeringTorque=torque,
steeringAngleDeg=angle,
vEgo=speed,
vEgoRaw=speed,
))
@pytest.fixture
def vehicle_model():
return VehicleModel(get_safety_CP())
def test_disabled_preserves_angle_command(vehicle_model):
controller = CooperativeSteeringController()
angle, lat_active = controller.update(12.5, True, False, make_car_state(torque=2.0), vehicle_model)
assert angle == 12.5
assert lat_active
def test_light_driver_torque_adjusts_angle(vehicle_model):
controller = CooperativeSteeringController()
angle = 0.0
for _ in range(10):
angle, lat_active = controller.update(0.0, True, True, make_car_state(torque=1.5), vehicle_model)
assert lat_active
assert angle > 0.0
assert angle <= CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX
def test_inactive_lateral_resets_override(vehicle_model):
controller = CooperativeSteeringController()
for _ in range(10):
controller.update(0.0, True, True, make_car_state(torque=1.5), vehicle_model)
angle, lat_active = controller.update(8.0, False, True, make_car_state(torque=1.5, angle=8.0), vehicle_model)
assert angle == 8.0
assert not lat_active
angle, lat_active = controller.update(8.0, True, True, make_car_state(angle=8.0), vehicle_model)
assert angle == 8.0
assert lat_active
@pytest.mark.parametrize(("candidate", "enabled", "expected"), (
(CAR.TESLA_MODEL_3, False, False),
(CAR.TESLA_MODEL_3, True, True),
(CAR.TESLA_MODEL_Y, True, False),
(CAR.TESLA_MODEL_S_PREAP, True, False),
))
def test_safety_flag_is_model_3_only(candidate, enabled, expected):
params = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], False, False, True)
from openpilot.starpilot.car.tesla.coop_preferences import prepare
prepare(params, enabled)
has_flag = any(config.safetyParam & TeslaSafetyFlags.COOP_STEERING.value for config in params.safetyConfigs)
assert has_flag is expected
if candidate != CAR.TESLA_MODEL_S_PREAP:
assert CarController(DBC[candidate], params).coop_enabled is expected
def assert_finite_nonnegative_limit_errors(controller):
errors = (controller.resume_limit_error_deg, controller.cooperative_limit_error_deg)
assert all(math.isfinite(error) and error >= 0.0 for error in errors)
assert math.isfinite(controller.cooperative_offset_deg)
def test_zero_torque_has_zero_cooperative_diagnostics(vehicle_model):
controller = CooperativeSteeringController()
angle, lat_active = controller.update(0.0, True, True, make_car_state(), vehicle_model)
assert angle == 0.0
assert lat_active
assert controller.resume_limit_error_deg == 0.0
assert controller.cooperative_limit_error_deg == 0.0
assert controller.cooperative_offset_deg == 0.0
assert_finite_nonnegative_limit_errors(controller)
def test_steady_light_torque_reports_offset_without_real_limiting(vehicle_model):
controller = CooperativeSteeringController()
for _ in range(100):
controller.update(0.0, True, True, make_car_state(torque=0.9), vehicle_model)
assert controller.cooperative_offset_deg > 2.5
assert controller.resume_limit_error_deg < 2.5
assert controller.cooperative_limit_error_deg < 2.5
assert_finite_nonnegative_limit_errors(controller)
def test_torque_reversal_updates_signed_offset_without_negative_errors(vehicle_model):
controller = CooperativeSteeringController()
for _ in range(100):
controller.update(0.0, True, True, make_car_state(torque=0.9), vehicle_model)
for _ in range(200):
controller.update(0.0, True, True, make_car_state(torque=-0.9), vehicle_model)
assert controller.cooperative_offset_deg < -2.5
assert_finite_nonnegative_limit_errors(controller)
def test_release_reports_gradual_offset_unwind(vehicle_model):
controller = CooperativeSteeringController()
for _ in range(100):
controller.update(0.0, True, True, make_car_state(torque=1.5), vehicle_model)
offsets = []
for _ in range(100):
controller.update(0.0, True, True, make_car_state(), vehicle_model)
offsets.append(controller.cooperative_offset_deg)
assert offsets[0] > offsets[-1] >= 0.0
assert all(next_offset <= offset for offset, next_offset in zip(offsets, offsets[1:], strict=False))
assert offsets[-1] == pytest.approx(0.0, abs=1e-6)
assert_finite_nonnegative_limit_errors(controller)
def test_resume_ramp_reports_resume_limiting(vehicle_model):
controller = CooperativeSteeringController()
controller.reset_resume_state(0.0)
controller.reset_override_state(0.0)
controller.update(20.0, True, True, make_car_state(), vehicle_model)
assert controller.resume_limit_error_deg > 2.5
assert controller.cooperative_limit_error_deg < 2.5
assert controller.cooperative_offset_deg == 0.0
def test_final_limiter_reports_cooperative_target_clipping(vehicle_model):
controller = CooperativeSteeringController()
controller.reset_resume_state(20.0)
controller.reset_override_state(0.0)
controller.update(20.0, True, True, make_car_state(), vehicle_model)
assert controller.resume_limit_error_deg == 0.0
assert controller.cooperative_limit_error_deg > 2.5
assert controller.cooperative_offset_deg == 0.0
def test_final_limiter_remains_visible_with_light_torque(vehicle_model):
controller = CooperativeSteeringController()
controller.reset_resume_state(20.0)
controller.reset_override_state(0.0)
controller.update(20.0, True, True, make_car_state(torque=-0.9), vehicle_model)
assert abs(controller.cooperative_offset_deg) > 0.0
assert controller.cooperative_limit_error_deg > 2.5
def test_diagnostics_reset_on_disabled_update(vehicle_model):
controller = CooperativeSteeringController()
controller.reset_resume_state(20.0)
controller.update(20.0, True, True, make_car_state(), vehicle_model)
assert controller.cooperative_limit_error_deg > 2.5
controller.update(4.0, True, False, make_car_state(torque=2.0), vehicle_model)
assert controller.resume_limit_error_deg == 0.0
assert controller.cooperative_limit_error_deg == 0.0
assert controller.cooperative_offset_deg == 0.0
@@ -52,7 +52,7 @@ def test_no_harness_old_encoding_malformed_cp_or_reused_flags():
before = cp.to_bytes()
apply_screen_button(cp, True, True)
assert cp.to_bytes() == before
for word in (2, 3, 16, 256, 1024, 514, 1538):
for word in (2, 3, 16, 258, 1024, 514, 1538):
cp = screen_params()
cp.safetyConfigs[0].safetyParam = word
assert not screen_tap_supported(cp)
+3
View File
@@ -132,6 +132,7 @@ class CarControllerParams:
class TeslaSafetyFlags(IntFlag):
LONG_CONTROL = 1
HW1 = 16
COOP_STEERING = 256
AOL_SCREEN_BUTTON = 512
AOL_SCREEN_DISENGAGE_ON_BRAKE = 1024
@@ -153,3 +154,5 @@ class TeslaFlags(IntFlag):
DBC = CAR.create_dbc_map()
STEER_THRESHOLD = 1
STEER_DISENGAGE_THRESHOLD = 5.0
+2 -2
View File
@@ -153,7 +153,7 @@ class CarTestRoute(NamedTuple):
routes = [
CarTestRoute("efdf9af95e71cd84/2022-05-13--19-03-31", COMMA.COMMA_BODY),
CarTestRoute("c5e3aa51055c8f47|2023-12-06--20-01-44", CHRYSLER.JEEP_CHEROKEE_5TH_GEN),
CarTestRoute("c5e3aa51055c8f47/2023-12-06--20-01-44", CHRYSLER.JEEP_CHEROKEE_5TH_GEN),
CarTestRoute("0c94aa1e1296d7c6/2021-05-05--19-48-37", CHRYSLER.JEEP_GRAND_CHEROKEE),
CarTestRoute("91dfedae61d7bd75/2021-05-22--20-07-52", CHRYSLER.JEEP_GRAND_CHEROKEE_2019),
CarTestRoute("420a8e183f1aed48/2020-03-05--07-15-29", CHRYSLER.CHRYSLER_PACIFICA_2018_HYBRID), # 2017
@@ -410,7 +410,7 @@ routes = [
CarTestRoute("ffcd23abbbd02219/2024-02-28--14-59-38", VOLKSWAGEN.VOLKSWAGEN_CADDY_MK3),
CarTestRoute("cae14e88932eb364/2021-03-26--14-43-28", VOLKSWAGEN.VOLKSWAGEN_GOLF_MK7), # Stock ACC
CarTestRoute("3cfdec54aa035f3f/2022-10-13--14-58-58", VOLKSWAGEN.VOLKSWAGEN_GOLF_MK7), # openpilot longitudinal
CarTestRoute("578742b26807f756|00000010--41ee3e5bec", VOLKSWAGEN.VOLKSWAGEN_JETTA_MK6),
CarTestRoute("578742b26807f756/00000010--41ee3e5bec", VOLKSWAGEN.VOLKSWAGEN_JETTA_MK6),
CarTestRoute("58a7d3b707987d65/2021-03-25--17-26-37", VOLKSWAGEN.VOLKSWAGEN_JETTA_MK7),
CarTestRoute("4d134e099430fba2/2021-03-26--00-26-06", VOLKSWAGEN.VOLKSWAGEN_PASSAT_MK8),
CarTestRoute("3cfdec54aa035f3f/2022-07-19--23-45-10", VOLKSWAGEN.VOLKSWAGEN_PASSAT_NMS),
@@ -0,0 +1,124 @@
from unittest.mock import patch
import pytest
from opendbc.car import CanData, structs
from opendbc.car.fw_versions import match_fw_to_car
from opendbc.car.hyundai.fingerprints import FW_VERSIONS
from opendbc.car.hyundai.values import CAR
from opendbc.car.isotp_parallel_query import IsoTpParallelQuery
from opendbc.car.vin import VIN_UNKNOWN
CAMERA = FW_VERSIONS[CAR.KIA_CARNIVAL_2025][(structs.CarParams.Ecu.fwdCamera, 0x7C4, None)][0]
class DiagnosticBus:
def __init__(self, replies):
self.replies = replies
self.now = 0.
self.requests = []
self.pending = []
self.consecutive = []
def schedule(self, payload, delay=.01, bus=1):
if len(payload) <= 7:
self.pending.append((self.now + delay, CanData(0x7CC, bytes([len(payload)]) + payload, bus)))
else:
self.pending.append((self.now + delay, CanData(0x7CC, bytes([0x10, len(payload)]) + payload[:6], bus)))
self.consecutive = [CanData(0x7CC, bytes([0x20 | (i % 16)]) + payload[j:j+7], bus)
for i, j in enumerate(range(6, len(payload), 7), 1)]
def send(self, packets):
for address, data, bus in packets:
assert address == 0x7C4 and bus == 1
if data[0] == 0x30:
self.pending.extend((self.now + .01 * i, frame) for i, frame in enumerate(self.consecutive, 1))
self.consecutive = []
else:
request = data[1:1+data[0]]
self.requests.append(request)
for payload, delay, response_bus in self.replies(len(self.requests)):
self.schedule(payload, delay, response_bus)
def recv(self, wait_for_one=False):
if wait_for_one:
self.now += .01
ready = [frame for deadline, frame in self.pending if deadline <= self.now]
self.pending = [(deadline, frame) for deadline, frame in self.pending if deadline > self.now]
return [ready] if ready else []
def query(self, request=b'\x22\xf1\x00', response=b'\x62', total_timeout=1.):
with patch('opendbc.car.isotp_parallel_query.time.monotonic', side_effect=lambda: self.now):
return IsoTpParallelQuery(self.send, self.recv, 1, [0x7C4], [request], [response]).get_data(.05, total_timeout)
@pytest.mark.parametrize('foreign', (b'\x59\x02\x0c\x62\x39\x86\x08', b'\x7f\x19\x78',
b'\x62\xf1\x90x'))
@pytest.mark.parametrize('same_attempt', (False, True))
def test_foreign_diagnostic_reply_cannot_finish_firmware_query(foreign, same_attempt):
def replies(attempt):
if attempt == 1:
result = [(foreign, .01, 1)]
if same_attempt:
result.append((b'\x62' + CAMERA, .03, 1))
return result
return [(b'\x62' + CAMERA, .01, 1)]
bus = DiagnosticBus(replies)
assert bus.query() == {(0x7C4, None): CAMERA}
assert bus.requests == [b'\x22\xf1\x00'] * (1 if same_attempt else 2)
assert bus.now < .3
def test_complete_transport_recovery_still_requires_both_existing_firmware_identities():
bus = DiagnosticBus(lambda attempt: [(b'\x59\x02\x0c\x62\x39\x86\x08', .01, 1)] if attempt == 1
else [(b'\x62' + CAMERA, .01, 1)])
camera = bus.query()[(0x7C4, None)]
radar = FW_VERSIONS[CAR.KIA_CARNIVAL_2025][(structs.CarParams.Ecu.fwdRadar, 0x7D0, None)][-1]
fw = [structs.CarParams.CarFw(ecu=ecu, address=address, brand='hyundai', fwVersion=version, bus=1)
for ecu, address, version in ((structs.CarParams.Ecu.fwdCamera, 0x7C4, camera),
(structs.CarParams.Ecu.fwdRadar, 0x7D0, radar))]
assert match_fw_to_car(fw, VIN_UNKNOWN, log=False) == (True, {CAR.KIA_CARNIVAL_2025})
assert not match_fw_to_car(fw[1:], VIN_UNKNOWN, log=False)[1]
fw[0].fwVersion = camera + b'unknown'
assert not match_fw_to_car(fw, VIN_UNKNOWN, log=False)[1]
@pytest.mark.parametrize('payload', (b'\x7f\x22\x31', b'\x7f\x22\x21'))
def test_current_request_negative_response_remains_terminal(payload):
bus = DiagnosticBus(lambda attempt: [(payload, .01, 1)])
assert bus.query() == {}
assert len(bus.requests) == 1
def test_continuous_foreign_traffic_gets_only_one_read_retry():
bus = DiagnosticBus(lambda attempt: [(b'\x59\x02\x0c', .01, 1)])
assert bus.query() == {}
assert len(bus.requests) == 2
assert bus.now < .15
@pytest.mark.parametrize('response_bus', (0, 2, 129, 193))
def test_wrong_bus_and_echo_cannot_supply_identity_or_trigger_retry(response_bus):
bus = DiagnosticBus(lambda attempt: [(b'\x62' + CAMERA, .01, response_bus)])
assert bus.query() == {}
assert len(bus.requests) == 1
def test_actuation_service_is_never_retried():
bus = DiagnosticBus(lambda attempt: [(b'\x59\x02\x0c', .01, 1)])
assert bus.query(b'\x28\x83\x03', b'\x68') == {}
assert bus.requests == [b'\x28\x83\x03']
def test_matching_pending_response_keeps_existing_wait_contract():
bus = DiagnosticBus(lambda attempt: [(b'\x7f\x22\x78', .01, 1), (b'\x62' + CAMERA, .12, 1)])
assert bus.query() == {(0x7C4, None): CAMERA}
assert len(bus.requests) == 1
def test_foreign_pending_cannot_extend_total_deadline():
bus = DiagnosticBus(lambda attempt: [(b'\x7f\x19\x78', .01, 1)])
assert bus.query(total_timeout=.075) == {}
assert len(bus.requests) == 2
assert bus.now <= .1
+2 -3
View File
@@ -106,9 +106,8 @@ class CarInterface(CarInterfaceBase):
# Only give steer angle deadzone to for bad angle sensor prius
for fw in car_fw:
if fw.ecu == "eps" and not fw.fwVersion == b'8965B47060\x00\x00\x00\x00\x00\x00':
retrofit = candidate == CAR.TOYOTA_PRIUS_RETROFIT or smart_dsu
ret.steerActuatorDelay = 0.14 if retrofit else 0.25
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning, steering_angle_deadzone_deg=0.3 if retrofit else 0.2)
ret.steerActuatorDelay = 0.14
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning, steering_angle_deadzone_deg=0.3)
# 2021+ TSS2 steering rack swapped into a TSS-P car, not supported
if candidate == CAR.TOYOTA_PRIUS and not smart_dsu and fw.ecu == "eps" and fw.fwVersion == b'8965B47070\x00\x00\x00\x00\x00\x00':
ret.dashcamOnly = True
@@ -13,11 +13,11 @@ PROFILES = {
}
def _camry_radar_bus(candidate, bus, messages):
# The 2018–20 Camry exposes an independent eight-byte 2FF stream alongside
def _cluster_radar_bus(candidate, bus, messages):
# The 2018–20 Camry and 2017–20 C-HR expose an independent eight-byte 2FF stream alongside
# its six cluster-radar frames on bus1 (cluster_radar.CLUSTERS). It is not
# the four-byte filter advertisement or a bus0 smartDSU status source.
return (candidate == CAR.TOYOTA_CAMRY and bus == 1 and messages.get(0x2FF) == 8 and
return (candidate in (CAR.TOYOTA_CAMRY, CAR.TOYOTA_CHR) and bus == 1 and messages.get(0x2FF) == 8 and
all(messages.get(address) == 8 for address in range(0x680, 0x686)))
@@ -29,12 +29,12 @@ def hardware_supported(candidate, fingerprint):
# Panda discovery may also expose its forwarded camera-bus copy.
if (candidate in PROFILES and fingerprint.get(0, {}).get(0x2FF) == 4 and
not any(address in messages for messages in fingerprint.values() for address in (0x201, 0x2AA)) and
all((bus in (0, 2) and messages[0x2FF] == 4) or _camry_radar_bus(candidate, bus, messages)
all((bus in (0, 2) and messages[0x2FF] == 4) or _cluster_radar_bus(candidate, bus, messages)
for bus, messages in fingerprint.items() if 0x2FF in messages)):
return True
return (candidate in PROFILES and fingerprint.get(0, {}).get(0x2AA) == 8 and
all(bus in (0, 2) and frames[0x2AA] == 8 for bus, frames in fingerprint.items() if 0x2AA in frames) and
not any(0x201 in frames or 0x2FF in frames and not _camry_radar_bus(candidate, bus, frames)
not any(0x201 in frames or 0x2FF in frames and not _cluster_radar_bus(candidate, bus, frames)
for bus, frames in fingerprint.items()))
@@ -695,6 +695,9 @@ BO_ 874 BLINDSPOTS_FRONT_CORNER_2: 16 XXX
SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX
SG_ SIDE_DETECT_STATE : 24|8@1+ (1,0) [0|255] "" XXX
BO_ 896 ADAS_0x380: 24 XXX
SG_ STOP_SIGN : 83|1@1+ (1,0) [0|1] "" XXX
BO_ 961 BLINKER_STALKS: 8 XXX
SG_ CHECKSUM_MAYBE : 7|8@0+ (1,0) [0|255] "" XXX
SG_ COUNTER_ALT : 15|4@0+ (1,0) [0|15] "" XXX
@@ -217,6 +217,7 @@ BO_ 258 ESC_50: 48 XXX
BO_ 261 VMM_01: 8 XXX
SG_ CRC : 0|8@1+ (1,0) [0|255] "" XXX
SG_ CNT : 8|4@1+ (1,0) [0|15] "" XXX
SG_ HMS_Status : 36|4@1+ (1,0) [0|15] "" XXX
SG_ Brake : 53|7@1+ (1,0) [0|3] "" XXX
BO_ 278 ESP_10: 8 Gateway_MQB
@@ -2027,6 +2028,7 @@ CM_ SG_ 219 AWV_Init_Status "Power-up state only, reads 2 once running. Does not
CM_ SG_ 219 Unknown_192 "Constant per platform (0 on all gen1 ID.4/Born, 1 on all gen2 ID.4)";
CM_ SG_ 219 Unknown_27 "Low bit tracks AWV_Unavailable, but is also set in 20-33% of normal driving";
CM_ SG_ 219 Unknown_80 "Low bit is normally 1 and clears with AWV_Unavailable, but also clears in ~1% of normal driving";
CM_ SG_ 261 HMS_Status "Hold manager response to ACC_18.ACC_Anforderung_HMS, pre-2024 only. 2024+ reports it in VMM_02";
CM_ SG_ 313 Long_Control_Inhibit "Goes high after user brakes hard, causes temporary TSK_Status fault";
CM_ SG_ 313 HMS_Status "Hold manager response to ACC_18.ACC_Anforderung_HMS and auto hold, 2024+ only";
CM_ SG_ 333 SET_ME_0x2FE "Only set by stock ACC on 2024+; reads 0 on pre-2024 and is not packed by mebcan";
@@ -2129,6 +2131,7 @@ VAL_ 253 EDS_Eingriff 0 "kein_EDS_Eingriff" 1 "EDS_Eingriff";
VAL_ 253 ESP_Eingriff 0 "kein_ESP_Eingriff" 1 "ESP_Eingriff_aktiv";
VAL_ 253 ESP_ASP 0 "inaktiv" 1 "aktiv";
VAL_ 253 ESC_Neutralschaltung 0 "keine_Anforderung" 1 "Neutralschaltung_angefordert";
VAL_ 261 HMS_Status 0 "not_holding" 1 "holding" 5 "starting" 6 "releasing" 9 "stopping";
VAL_ 267 TSK_Status 0 "init" 1 "disabled" 2 "enabled" 3 "regulating" 4 "accel_pedal_override" 5 "brake_only" 6 "temp_fault" 7 "perm_fault";
VAL_ 278 ESP_QBit_Wegimpuls_VL 0 "gueltiger_Wert" 1 "Ersatz_Init_oder_Fehlerwert";
VAL_ 278 ESP_QBit_Wegimpuls_VR 0 "gueltiger_Wert" 1 "Ersatz_Init_oder_Fehlerwert";
+29 -24
View File
@@ -42,13 +42,16 @@
#define GET_BIT(msg, b) ((bool)!!(((msg)->data[((b) / 8U)] >> ((b) % 8U)) & 0x1U))
#define GET_FLAG(value, mask) (((value) & (mask)) == (mask))
#define BUILD_SAFETY_CFG(rx, tx) ((safety_config){(rx), (sizeof((rx)) / sizeof((rx)[0])), \
// length of an RX check array; fails to compile if it has more checks than rx_status can track
#define RX_CHECKS_LEN(rx) ((sizeof((rx)) / sizeof((rx)[0])) + \
(0U * sizeof(char[((sizeof((rx)) / sizeof((rx)[0])) <= MAX_RX_CHECKS) ? 1 : -1])))
#define BUILD_SAFETY_CFG(rx, tx) ((safety_config){(rx), RX_CHECKS_LEN(rx), \
(tx), (sizeof((tx)) / sizeof((tx)[0])), \
false})
#define SET_RX_CHECKS(rx, config) \
do { \
(config).rx_checks = (rx); \
(config).rx_checks_len = sizeof((rx)) / sizeof((rx)[0]); \
(config).rx_checks_len = RX_CHECKS_LEN(rx); \
(config).disable_forwarding = false; \
} while (0);
@@ -67,6 +70,8 @@ uint64_t GET_BYTES_64_LE(const CANPacket_t *msg, int start, int len);
extern const int MAX_WRONG_COUNTERS;
#define MAX_ADDR_CHECK_MSGS 3U
// max number of RX checks in a safety config; their dynamic state is shared by all safety modes
#define MAX_RX_CHECKS 17U
#define MAX_SAMPLE_VALS 6
// used to represent floating point vehicle speed in a sample_t
#define VEHICLE_SPEED_FACTOR 1000.0
@@ -90,8 +95,8 @@ struct lookup_t {
typedef struct {
int addr;
unsigned int bus;
int len;
uint8_t bus;
uint8_t len;
bool check_relay; // if true, trigger relay malfunction if existence on destination bus and block forwarding to destination bus
bool disable_static_blocking; // if true, static blocking is disabled so safety mode can dynamically handle it (e.g. selective AEB pass-through)
bool check_relay_immediately; // if true, skip the usual transition grace for this exact TX address and bus
@@ -178,36 +183,38 @@ typedef struct {
} LongitudinalLimits;
typedef struct {
int addr;
unsigned int bus;
int len;
uint32_t frequency; // expected frequency of the message [Hz]
bool ignore_checksum; // checksum check is not performed when set to true
bool ignore_counter; // counter check is not performed when set to true
uint8_t max_counter; // maximum value of the counter. 0 means that the counter check is skipped
bool ignore_quality_flag; // true if quality flag check is skipped
unsigned int addr : 29;
unsigned int bus : 3;
unsigned int len : 7;
unsigned int frequency : 7; // expected frequency of the message [Hz]
bool ignore_checksum : 1; // checksum check is not performed when set to true
bool ignore_counter : 1; // counter check is not performed when set to true
unsigned int max_counter : 8; // maximum value of the counter. 0 means that the counter check is skipped
bool ignore_quality_flag : 1; // true if quality flag check is skipped
} CanMsgCheck;
// dynamic state of an RX check, reset on safety mode init
typedef struct {
// dynamic flags, reset on safety mode init
bool msg_seen;
int index; // if multiple messages are allowed to be checked, this stores the index of the first one seen. only msg[msg_index] will be used
bool valid_checksum; // true if and only if checksum check is passed
int wrong_counters; // counter of wrong counters, saturated between 0 and MAX_WRONG_COUNTERS
bool valid_quality_flag; // true if the message's quality/health/status signals are valid
uint8_t last_counter; // last counter value
uint32_t last_timestamp; // micro-s
int wrong_counters; // counter of wrong counters, saturated between 0 and MAX_WRONG_COUNTERS
uint8_t index; // if multiple messages are allowed to be checked, this stores the index of the first one seen. only msg[msg_index] will be used
uint8_t last_counter; // last counter value
bool msg_seen;
bool valid_checksum; // true if and only if checksum check is passed
bool valid_quality_flag; // true if the message's quality/health/status signals are valid
bool lagging; // true if and only if the time between updates is excessive
} RxStatus;
// params and flags about checksum, counter and frequency checks for each monitored address
// params and flags about checksum, counter and frequency checks for each monitored address.
// constant: the state of the current safety config's checks is in rx_status, indexed alike
typedef struct {
CanMsgCheck msg[MAX_ADDR_CHECK_MSGS]; // check either messages (e.g. honda steer)
RxStatus status;
} RxCheck;
extern RxStatus rx_status[MAX_RX_CHECKS];
typedef struct {
RxCheck *rx_checks;
const RxCheck *rx_checks;
int rx_checks_len;
const CanMsg *tx_msgs;
int tx_msgs_len;
@@ -242,8 +249,6 @@ int to_signed(int d, int bits);
void update_sample(struct sample_t *sample, int sample_new);
bool get_longitudinal_allowed(void);
int ROUND(float val);
void gen_crc_lookup_table_8(uint8_t poly, uint8_t crc_lut[]);
void gen_crc_lookup_table_16(uint16_t poly, uint16_t crc_lut[]);
bool steer_torque_cmd_checks(int desired_torque, int steer_req, const TorqueSteeringLimits limits);
bool steer_angle_cmd_checks(int desired_angle, bool steer_control_enabled, const AngleSteeringLimits limits);
bool steer_angle_cmd_checks_vm(int desired_angle, bool steer_control_enabled, const AngleSteeringLimits limits,
+1 -1
View File
@@ -43,7 +43,7 @@ static bool body_tx_hook(const CANPacket_t *msg) {
}
static safety_config body_init(uint16_t param) {
static RxCheck body_rx_checks[] = {
static const RxCheck body_rx_checks[] = {
{.msg = {{0x201, 0, 8, 100U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
};
+1 -1
View File
@@ -99,7 +99,7 @@ static safety_config byd_init(uint16_t param) {
{0x3B0, 0, 8, .check_relay = false}, // PCM_BUTTONS (cruise cancel button spoof)
};
static RxCheck byd_rx_checks[] = {
static const RxCheck byd_rx_checks[] = {
{.msg = {{0x11F, 0, 5, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // STEER_MODULE_2 (steering angle)
{.msg = {{0x1F0, 0, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // WHEELSPEED_CLEAN (vehicle speed)
{.msg = {{0x242, 0, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // DRIVE_STATE (gas and brake pressed)
+4 -4
View File
@@ -20,7 +20,7 @@ static bool chrysler_hold_ready(void) {
const uint32_t now = microsecond_timer_get();
bool healthy = !safety_rx_checks_invalid;
for (int i = 0; i < current_safety_config.rx_checks_len; i++) {
const RxStatus *status = &current_safety_config.rx_checks[i].status;
const RxStatus *status = &rx_status[i];
healthy &= status->msg_seen && status->valid_checksum && status->valid_quality_flag &&
(status->wrong_counters < MAX_WRONG_COUNTERS) && !status->lagging &&
(safety_get_ts_elapsed(now, status->last_timestamp) <= 1000000U);
@@ -196,7 +196,7 @@ static safety_config chrysler_init(uint16_t param) {
chrysler_hold_stock_timestamp = 0U;
for (int i = 0; i < 8; i++) { chrysler_hold_stock[i] = 0U; }
static RxCheck chrysler_ram_dt_rx_checks[] = {
static const RxCheck chrysler_ram_dt_rx_checks[] = {
{.msg = {{CHRYSLER_RAM_DT_EPS_2, 0, 8, 100U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{CHRYSLER_RAM_DT_ESP_1, 0, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{CHRYSLER_RAM_DT_ESP_8, 0, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
@@ -204,7 +204,7 @@ static safety_config chrysler_init(uint16_t param) {
{.msg = {{CHRYSLER_RAM_DT_DAS_3, 2, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
};
static RxCheck chrysler_rx_checks[] = {
static const RxCheck chrysler_rx_checks[] = {
{.msg = {{CHRYSLER_EPS_2, 0, 8, 100U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{CHRYSLER_ESP_1, 0, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{514, 0, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
@@ -232,7 +232,7 @@ static safety_config chrysler_init(uint16_t param) {
};
#ifdef ALLOW_DEBUG
static RxCheck chrysler_ram_hd_rx_checks[] = {
static const RxCheck chrysler_ram_hd_rx_checks[] = {
{.msg = {{CHRYSLER_RAM_HD_EPS_2, 0, 8, 100U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{CHRYSLER_RAM_HD_ESP_1, 0, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{CHRYSLER_RAM_HD_ESP_8, 0, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
@@ -9,7 +9,7 @@ static safety_config chrysler_cusw_init(uint16_t param) {
{0x2FAU, 0, 3, .check_relay = false},
};
static RxCheck chrysler_cusw_rx_checks[] = {
static const RxCheck chrysler_cusw_rx_checks[] = {
{.msg = {{0x1E4U, 0, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{0x1E8U, 0, 5, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{0x1ECU, 0, 8, 100U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
+2 -2
View File
@@ -1160,7 +1160,7 @@ static safety_config gm_init(uint16_t safety_param) {
gm_extended_rx_checks[3].msg[0].addr = (gm_camera_pedal && !gm_camera_pedal_f1) ? 0xBEU : 0xF1U;
static RxCheck gm_rx_checks[] = {
static const RxCheck gm_rx_checks[] = {
GM_COMMON_RX_CHECKS
};
#define GM_ASCM_INTERCEPT_RX_CHECKS \
@@ -1237,7 +1237,7 @@ static safety_config gm_init(uint16_t safety_param) {
{.msg = {{0x1F5, 0, 8, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
};
static RxCheck gm_ev_rx_checks[] = {
static const RxCheck gm_ev_rx_checks[] = {
GM_COMMON_RX_CHECKS
{.msg = {{0xBD, 0, 7, 40U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
};
+6 -6
View File
@@ -351,7 +351,7 @@ static safety_config honda_nidec_init(uint16_t param) {
// 0x1FA is dynamically forwarded based on stock AEB
// 0xE4 is steering on all cars except CRV and RDX, 0x194 for CRV and RDX,
// 0x1FA is brake control, 0x30C is acc hud, 0x33D is lkas hud
static CanMsg HONDA_N_TX_MSGS[] = {
static const CanMsg HONDA_N_TX_MSGS[] = {
{0xE4, 0, 5, .check_relay = true},
{0x194, 0, 4, .check_relay = true},
{0x1FA, 0, 8, .check_relay = false},
@@ -426,7 +426,7 @@ static safety_config honda_bosch_init(uint16_t param) {
honda_mvl_buttons_fresh = 0U;
honda_interceptor_reset(false);
// Bosch
static CanMsg HONDA_BOSCH_TX_MSGS[] = {
static const CanMsg HONDA_BOSCH_TX_MSGS[] = {
{0xE4, 0, 5, .check_relay = true},
{0xE5, 0, 8, .check_relay = true},
{0x296, 1, 4, .check_relay = false},
@@ -437,7 +437,7 @@ static safety_config honda_bosch_init(uint16_t param) {
};
// Bosch w/ gas and brakes
static CanMsg HONDA_BOSCH_LONG_TX_MSGS[] = {
static const CanMsg HONDA_BOSCH_LONG_TX_MSGS[] = {
{0xE4, 1, 5, .check_relay = true},
{0x1DF, 1, 8, .check_relay = true},
{0x1EF, 1, 8, .check_relay = false},
@@ -459,21 +459,21 @@ static safety_config honda_bosch_init(uint16_t param) {
};
// Bosch radarless
static CanMsg HONDA_RADARLESS_TX_MSGS[] = {
static const CanMsg HONDA_RADARLESS_TX_MSGS[] = {
{0xE4, 0, 5, .check_relay = true},
{0x296, 2, 4, .check_relay = false},
{0x33D, 0, 8, .check_relay = true},
};
// Bosch radarless w/ gas and brakes
static CanMsg HONDA_RADARLESS_LONG_TX_MSGS[] = {
static const CanMsg HONDA_RADARLESS_LONG_TX_MSGS[] = {
{0xE4, 0, 5, .check_relay = true},
{0x33D, 0, 8, .check_relay = true},
{0x1C8, 0, 8, .check_relay = true},
{0x30C, 0, 8, .check_relay = true},
};
static CanMsg HONDA_CANFD_TX_MSGS[] = {
static const CanMsg HONDA_CANFD_TX_MSGS[] = {
{0xE4, 0, 5, .check_relay = true},
{0x296, 0, 4, .check_relay = false},
{0x33D, 0, 8, .check_relay = true},
@@ -92,9 +92,9 @@ static bool honda_stock_aol_sources_current(void) {
bool current = false;
if ((honda_stock_aol_eps_check() != NULL) && (honda_stock_aol_gear_check() != NULL)) {
const uint32_t now = microsecond_timer_get();
current = honda_stock_aol_eps_check()->status.msg_seen && honda_stock_aol_gear_check()->status.msg_seen &&
(safety_get_ts_elapsed(now, honda_stock_aol_eps_check()->status.last_timestamp) <= 300000U) &&
(safety_get_ts_elapsed(now, honda_stock_aol_gear_check()->status.last_timestamp) <= 300000U) &&
current = rx_status[honda_stock_aol_eps_index].msg_seen && rx_status[honda_stock_aol_eps_index + 1U].msg_seen &&
(safety_get_ts_elapsed(now, rx_status[honda_stock_aol_eps_index].last_timestamp) <= 300000U) &&
(safety_get_ts_elapsed(now, rx_status[honda_stock_aol_eps_index + 1U].last_timestamp) <= 300000U) &&
(safety_get_ts_elapsed(now, honda_stock_aol_main_ts) <= 300000U) &&
(!honda_interceptor || (honda_interceptor_sensor_seen && honda_interceptor_sensor_valid &&
(safety_get_ts_elapsed(now, honda_interceptor_sensor_us) <= 200000U)));
@@ -149,9 +149,16 @@ static uint8_t honda_stock_aol_permission_mask(void) {
static bool honda_stock_aol_selected(const CANPacket_t *msg, const RxCheck *check) {
bool selected = false;
if ((check != NULL) && check->status.msg_seen) {
const CanMsgCheck *source = &check->msg[check->status.index];
selected = msg_matches(msg, source->addr, source->bus, source->len);
if (check != NULL) {
for (int i = 0; i < current_safety_config.rx_checks_len; i++) {
if (check == &honda_rx_workspace[i]) {
if (rx_status[i].msg_seen) {
const CanMsgCheck *source = &check->msg[rx_status[i].index];
selected = msg_matches(msg, source->addr, source->bus, source->len);
}
break;
}
}
}
return selected;
}
+13 -12
View File
@@ -2,6 +2,7 @@
#include "opendbc/safety/declarations.h"
#include "opendbc/safety/modes/hyundai_common.h"
#include "opendbc/safety/modes/hyundai_daw.h"
#include "opendbc/safety/modes/hyundai_classic_non_scc_aol.h"
#include "opendbc/safety/modes/hyundai_blended_stock_aol.h"
@@ -350,7 +351,7 @@ static bool hyundai_tx_hook(const CANPacket_t *msg) {
const TorqueSteeringLimits HYUNDAI_STEERING_LIMITS_ALT = HYUNDAI_LIMITS(270, 2, 3);
const TorqueSteeringLimits HYUNDAI_STEERING_LIMITS_ALT_2 = HYUNDAI_LIMITS(170, 2, 3);
bool tx = true;
bool tx = (msg->addr != 0x53EU) || hyundai_daw_tx(msg);
if (hyundai_ray_pedal && (msg->addr == 0x200U)) {
const uint16_t track1 = ((uint16_t)msg->data[0] << 8U) | msg->data[1];
@@ -360,7 +361,7 @@ static bool hyundai_tx_hook(const CANPacket_t *msg) {
bool rx_valid = hyundai_ray_pedal_healthy && !safety_rx_checks_invalid &&
(safety_get_ts_elapsed(microsecond_timer_get(), hyundai_ray_pedal_ts) <= 1000000U);
for (int i = 0; i < current_safety_config.rx_checks_len; i++) {
const RxStatus *status = &current_safety_config.rx_checks[i].status;
const RxStatus *status = &rx_status[i];
rx_valid &= status->msg_seen && status->valid_checksum && status->valid_quality_flag &&
(status->wrong_counters < MAX_WRONG_COUNTERS) && !status->lagging &&
(safety_get_ts_elapsed(microsecond_timer_get(), status->last_timestamp) <= 1000000U);
@@ -638,6 +639,7 @@ static safety_config hyundai_init(uint16_t param) {
};
hyundai_common_init(param);
hyundai_daw_init(false, false);
if ((param == 0x800EU) && (alternative_experience == 0)) {
hyundai_long_main_policy = HYUNDAI_MAIN_TRACK;
}
@@ -656,9 +658,6 @@ static safety_config hyundai_init(uint16_t param) {
hyundai_blended_hda2 |= (param == 0x2014U);
#endif
hyundai_blended_stock |= hyundai_blended_alpha;
if (hyundai_blended_hda2) {
gen_crc_lookup_table_16(0x1021U, hyundai_canfd_crc_lut);
}
hyundai_ray_pedal = param == 0x9805U;
hyundai_ray_pedal_healthy = false;
hyundai_ray_pedal_ts = 0U;
@@ -760,7 +759,7 @@ static safety_config hyundai_init(uint16_t param) {
} else if (hyundai_longitudinal) {
// Use CLU11 (buttons) to manage controls allowed instead of SCC cruise state
static RxCheck hyundai_fcev_long_rx_checks[] = {
static const RxCheck hyundai_fcev_long_rx_checks[] = {
HYUNDAI_COMMON_RX_CHECKS(false)
HYUNDAI_FCEV_GAS_ADDR_CHECK
};
@@ -784,7 +783,7 @@ static safety_config hyundai_init(uint16_t param) {
}
}
} else if (hyundai_camera_scc) {
static RxCheck hyundai_cam_scc_rx_checks[] = {
static const RxCheck hyundai_cam_scc_rx_checks[] = {
HYUNDAI_COMMON_RX_CHECKS(false)
HYUNDAI_SCC12_ADDR_CHECK(2)
};
@@ -796,12 +795,12 @@ static safety_config hyundai_init(uint16_t param) {
ret = BUILD_SAFETY_CFG(hyundai_cam_scc_rx_checks, HYUNDAI_CAMERA_SCC_TX_MSGS);
}
} else {
static RxCheck hyundai_rx_checks[] = {
static const RxCheck hyundai_rx_checks[] = {
HYUNDAI_COMMON_RX_CHECKS(false)
HYUNDAI_SCC12_ADDR_CHECK(0)
};
static RxCheck hyundai_fcev_rx_checks[] = {
static const RxCheck hyundai_fcev_rx_checks[] = {
HYUNDAI_COMMON_RX_CHECKS(false)
HYUNDAI_SCC12_ADDR_CHECK(0)
HYUNDAI_FCEV_GAS_ADDR_CHECK
@@ -903,12 +902,12 @@ static safety_config hyundai_init(uint16_t param) {
const safety_config denied = {0};
ret = denied;
}
return ret;
return hyundai_daw_configure(ret, false, !hyundai_blended_stock && !hyundai_blended_alpha);
}
static safety_config hyundai_legacy_init(uint16_t param) {
// older hyundai models have less checks due to missing counters and checksums
static RxCheck hyundai_legacy_rx_checks[] = {
static const RxCheck hyundai_legacy_rx_checks[] = {
HYUNDAI_COMMON_RX_CHECKS(true)
HYUNDAI_SCC12_ADDR_CHECK(0)
};
@@ -925,6 +924,7 @@ static safety_config hyundai_legacy_init(uint16_t param) {
}
}
hyundai_common_init(normalized);
hyundai_daw_init(false, false);
classic_non_scc_aol_configure(normalized, !legacy_long);
hyundai_legacy = true;
hyundai_blended_stock = false;
@@ -953,10 +953,11 @@ static safety_config hyundai_legacy_init(uint16_t param) {
#endif
}
hyundai_classic_scc_aol_rx_config(&ret, true);
return ret;
return hyundai_daw_configure(ret, false, true);
}
static void hyundai_optional_rx_hook(const CANPacket_t *msg) {
hyundai_daw_rx(msg);
if ((msg->addr == 0x391U) || (msg->addr == 0x50CU)) {
blended_aol_rx(msg);
}
+114 -31
View File
@@ -2,6 +2,7 @@
#include "opendbc/safety/declarations.h"
#include "opendbc/safety/modes/hyundai_common.h"
#include "opendbc/safety/modes/hyundai_daw.h"
#include "opendbc/safety/modes/hyundai_canfd_aol.h"
#include "opendbc/safety/modes/hyundai_canfd_angle_aol.h"
@@ -180,11 +181,14 @@ static bool hyundai_canfd_alt_buttons = false;
static bool hyundai_canfd_lka_steer_msg_alt = false;
static bool hyundai_canfd_carnival_alt_resume = false;
static bool hyundai_canfd_carnival_source_valid = false;
static bool hyundai_canfd_carnival_previous_valid = false;
static bool hyundai_canfd_carnival_counter_seen = false;
static bool hyundai_canfd_carnival_counter_consumed = false;
static bool hyundai_canfd_angle_steering = false;
static bool hyundai_canfd_angle_observed_adas = false;
static bool hyundai_canfd_ioniq6_long = false;
static bool aol_ioniq6_long = false;
static bool hyundai_stock_torque_aol = false;
static bool aol_optional_release_policy = false;
static bool aol_optional_release_neutral = false;
static uint8_t aol_optional_release_previous = 0U;
@@ -204,6 +208,7 @@ static bool aol_ioniq6_lateral_token_claimed = false;
static uint32_t aol_ioniq6_gesture_ts = 0U;
static bool aol_ioniq6_enable_counter_seen = false;
static bool aol_ioniq6_enable_pending = false;
static int aol_stock_enable_previous = HYUNDAI_BTN_NONE;
static bool aol_ioniq6_enable_neutral = false;
static bool aol_ioniq6_enable_release = false;
static uint32_t aol_ioniq6_enable_release_ts = 0U;
@@ -220,6 +225,10 @@ static bool hyundai_canfd_gv60_heartbeat_seen = false;
static uint8_t hyundai_canfd_gv60_heartbeat_counter = 0U;
static bool hyundai_canfd_ioniq6_heartbeat_seen = false;
static uint8_t hyundai_canfd_ioniq6_heartbeat_counter = 0U;
static bool hyundai_canfd_ioniq6_bsm_low_speed = false;
static bool hyundai_canfd_ioniq6_bsm_wheel_seen = false;
static uint8_t hyundai_canfd_ioniq6_bsm_wheel_counter = 0U;
static uint32_t hyundai_canfd_ioniq6_bsm_wheel_ts = 0U;
static bool hyundai_canfd_ioniq6_corner_valid = false;
static bool hyundai_canfd_ioniq6_corner_counter_seen = false;
static uint8_t hyundai_canfd_ioniq6_corner_counter = 0U;
@@ -239,6 +248,7 @@ static uint32_t hyundai_canfd_ioniq6_bsm_pair_ts = 0U;
static uint8_t hyundai_canfd_angle_model = 0U;
static uint8_t hyundai_canfd_carnival_consumed_counter = 0U;
static uint8_t hyundai_canfd_carnival_source[16] = {0};
static uint8_t hyundai_canfd_carnival_previous[16] = {0};
// Standard CRUISE_BUTTONS sources use either 0..14 or 0..15. Only the
// observed terminal rollover may omit 15; repeats and other gaps stay invalid.
@@ -249,6 +259,7 @@ static bool hyundai_canfd_button_counter_follows(uint8_t previous, uint8_t curre
static void aol_ioniq6_enable_reset(void) {
aol_ioniq6_enable_counter_seen = false;
aol_stock_enable_previous = HYUNDAI_BTN_NONE;
aol_ioniq6_enable_neutral = false;
aol_ioniq6_enable_pending = false;
aol_ioniq6_enable_release = false;
@@ -256,6 +267,7 @@ static void aol_ioniq6_enable_reset(void) {
static void aol_ioniq6_reset(void) {
aol_ioniq6_long = false;
hyundai_stock_torque_aol = false;
aol_ioniq6_enable_reset();
aol_optional_release_capable = false;
aol_optional_release_policy = false;
@@ -329,9 +341,9 @@ static uint8_t aol_ioniq6_request_mask(void) {
aol_host_axis_mask = 0U;
} else if (heartbeat_engaged) {
aol_ioniq6_session_started = true;
if ((hyundai_canfd_ioniq6_long || hyundai_ev9_long_aol ||
if ((hyundai_stock_torque_aol || hyundai_canfd_ioniq6_long || hyundai_ev9_long_aol ||
(aol_optional_release_capable && !aol_optional_release_policy)) && controls_allowed && aol_ioniq6_enable_release &&
((aol_host_axis_mask & 0x3U) == 0x3U) &&
((aol_host_axis_mask & 0x3U) == (hyundai_stock_torque_aol ? 0x1U : 0x3U)) &&
(safety_get_ts_elapsed(now, aol_ioniq6_enable_release_ts) <= AOL_HOST_REQUEST_TIMEOUT_US)) {
aol_ioniq6_lateral_latch = true;
aol_ioniq6_enable_release = false;
@@ -421,16 +433,17 @@ static bool hyundai_canfd_ioniq6_rx_healthy(void) {
if (healthy) {
for (int i = 0; i < current_safety_config.rx_checks_len; i++) {
const RxCheck *check = &current_safety_config.rx_checks[i];
healthy = check->status.msg_seen && !check->status.lagging && check->status.valid_checksum &&
check->status.valid_quality_flag && (check->status.wrong_counters < MAX_WRONG_COUNTERS);
const RxStatus *status = &rx_status[i];
healthy = status->msg_seen && !status->lagging && status->valid_checksum &&
status->valid_quality_flag && (status->wrong_counters < MAX_WRONG_COUNTERS);
if (healthy) {
const uint32_t frequency = check->msg[check->status.index].frequency;
const uint32_t frequency = check->msg[status->index].frequency;
if (frequency < 10U) {
healthy = false;
} else {
const uint32_t period_limit = 10000000U / frequency;
const uint32_t max_age = (period_limit > 100000U) ? period_limit : 100000U;
healthy = safety_get_ts_elapsed(now, check->status.last_timestamp) <= max_age;
healthy = safety_get_ts_elapsed(now, status->last_timestamp) <= max_age;
}
}
if (!healthy) {
@@ -441,6 +454,7 @@ static bool hyundai_canfd_ioniq6_rx_healthy(void) {
return healthy;
}
static uint32_t hyundai_canfd_carnival_source_ts = 0U;
static uint32_t hyundai_canfd_carnival_previous_ts = 0U;
static unsigned int hyundai_canfd_get_lka_addr(void) {
return hyundai_canfd_lka_steer_msg_alt ? 0x110U : 0x50U;
@@ -514,6 +528,7 @@ static void hyundai_canfd_ioniq6_optional_rx_hook(const CANPacket_t *msg) {
}
static void hyundai_canfd_optional_rx_hook(const CANPacket_t *msg) {
hyundai_daw_rx(msg);
if (hyundai_canfd_ioniq6_long && (msg->bus == 1U)) {
hyundai_canfd_ioniq6_optional_rx_hook(msg);
}
@@ -585,11 +600,11 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *msg) {
}
const bool lkas_button = hyundai_canfd_alt_buttons ? GET_BIT(msg, 39U) : GET_BIT(msg, 23U);
const bool previous_controls_allowed = controls_allowed;
const int previous_cruise_button = cruise_button_prev;
const int previous_cruise_button = hyundai_stock_torque_aol ? aol_stock_enable_previous : cruise_button_prev;
const int main_policy = hyundai_canfd_ccnc_ev_long ? (hyundai_canfd_pe_long ? HYUNDAI_MAIN_WITHDRAW : HYUNDAI_MAIN_TRACK) : HYUNDAI_MAIN_NONE;
const bool normal_main_button = hyundai_canfd_ccnc_ev_long && GET_BIT(msg, 21U);
hyundai_common_cruise_buttons_check(cruise_button, main_button, normal_main_button, main_policy);
if ((hyundai_canfd_ioniq6_long || hyundai_ev9_long_aol ||
if ((hyundai_stock_torque_aol || hyundai_canfd_ioniq6_long || hyundai_ev9_long_aol ||
(aol_optional_release_capable && !aol_optional_release_policy)) && aol_ioniq6_long) {
static uint8_t aol_ioniq6_enable_counter = 0U;
static uint32_t aol_ioniq6_enable_ts = 0U;
@@ -606,7 +621,7 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *msg) {
aol_ioniq6_enable_pending = true;
} else if (aol_ioniq6_enable_pending && (cruise_button == HYUNDAI_BTN_NONE) &&
((previous_cruise_button == HYUNDAI_BTN_SET) || (previous_cruise_button == HYUNDAI_BTN_RESUME))) {
if (controls_allowed && aol_rx_healthy()) {
if ((controls_allowed || hyundai_stock_torque_aol) && aol_rx_healthy()) {
heartbeat_engaged_mismatches = 0U;
aol_ioniq6_enable_release = true;
aol_ioniq6_enable_release_ts = now;
@@ -621,6 +636,7 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *msg) {
aol_ioniq6_enable_counter_seen = true;
aol_ioniq6_enable_counter = counter;
aol_ioniq6_enable_ts = now;
aol_stock_enable_previous = cruise_button;
}
if (hyundai_canfd_ccnc_ev_long && !previous_controls_allowed && controls_allowed) {
hyundai_ev9_inactive_accel_count = 0U;
@@ -690,11 +706,26 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *msg) {
}
}
if (hyundai_canfd_carnival_alt_resume && (msg_matches(msg, 0x1aaU, pt_bus))) {
static uint8_t hyundai_canfd_carnival_rx_counter = 0U;
const bool source_fresh = !hyundai_canfd_carnival_counter_seen ||
hyundai_canfd_button_counter_follows(hyundai_canfd_carnival_rx_counter, msg->data[2], 0xFFU);
hyundai_canfd_carnival_counter_seen = true;
hyundai_canfd_carnival_rx_counter = msg->data[2];
const bool preceding_valid = source_fresh && hyundai_canfd_carnival_source_valid &&
hyundai_canfd_button_counter_follows(hyundai_canfd_carnival_source[2], msg->data[2], 0xFFU);
hyundai_canfd_carnival_previous_valid = false;
hyundai_canfd_carnival_source_valid = false;
const bool idle_buttons = ((msg->data[4] & 0xF4U) == 0U) && ((msg->data[5] & 0x02U) == 0U);
if (idle_buttons && ((!hyundai_canfd_carnival_counter_consumed) ||
if (source_fresh && idle_buttons && ((!hyundai_canfd_carnival_counter_consumed) ||
(msg->data[2] != hyundai_canfd_carnival_consumed_counter)) &&
(hyundai_canfd_get_checksum(msg) == (hyundai_common_canfd_compute_checksum(msg) ^ 0x041DU))) {
if (preceding_valid) {
for (int i = 0; i < 16; i++) {
hyundai_canfd_carnival_previous[i] = hyundai_canfd_carnival_source[i];
}
hyundai_canfd_carnival_previous_ts = hyundai_canfd_carnival_source_ts;
hyundai_canfd_carnival_previous_valid = true;
}
for (int i = 0; i < 16; i++) {
hyundai_canfd_carnival_source[i] = msg->data[i];
}
@@ -729,6 +760,18 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *msg) {
// average of all 4 wheel speeds. Conversion: raw * 0.03125 / 3.6 = m/s
UPDATE_VEHICLE_SPEED((fr + rr + rl + fl) / 4.0 * 0.03125 * KPH_TO_MS);
if (hyundai_canfd_ioniq6_long) {
// Mean wheel speed < 5 mph: raw sum < 2.2352 * 4 * 3.6 / 0.03125.
// This hook runs only for valid required RX; TX also checks its freshness.
const bool fresh_counter = !hyundai_canfd_ioniq6_bsm_wheel_seen ||
(msg->data[2] == (uint8_t)(hyundai_canfd_ioniq6_bsm_wheel_counter + 1U));
hyundai_canfd_ioniq6_bsm_low_speed = fresh_counter && ((fr + rr + rl + fl) < 1030U);
if (fresh_counter) {
hyundai_canfd_ioniq6_bsm_wheel_ts = microsecond_timer_get();
}
hyundai_canfd_ioniq6_bsm_wheel_seen = true;
hyundai_canfd_ioniq6_bsm_wheel_counter = msg->data[2];
}
}
// cruise state
@@ -991,9 +1034,15 @@ static bool hyundai_canfd_tx_hook_valid(const CANPacket_t *msg) {
body[6] = right << 5U;
body[7] = 0x80U | (SAFETY_MAX(left, right) << 3U);
body[16] = left | (right << 2U);
bool physical_body = true;
bool neutral_body = hyundai_canfd_ioniq6_bsm_low_speed &&
(safety_get_ts_elapsed(now, hyundai_canfd_ioniq6_bsm_wheel_ts) <= 100000U);
for (int i = 3; i < 24; i++) {
valid &= msg->data[i] == body[i];
physical_body &= msg->data[i] == body[i];
// Only the complete frozen neutral indication may replace a detection.
neutral_body &= msg->data[i] == ((i == 7) ? 0x80U : 0U);
}
valid &= physical_body || neutral_body;
if (valid) {
hyundai_canfd_ioniq6_bsm_counter_seen = true;
hyundai_canfd_ioniq6_bsm_counter = msg->data[2];
@@ -1061,23 +1110,31 @@ static bool hyundai_canfd_tx_hook_valid(const CANPacket_t *msg) {
}
}
if (msg->addr == 0x1aaU) {
bool current_match = hyundai_canfd_carnival_source_valid &&
safety_get_ts_elapsed(microsecond_timer_get(), hyundai_canfd_carnival_source_ts) <= 100000U &&
(msg->data[2] == (uint8_t)(hyundai_canfd_carnival_source[2] + 1U));
bool previous_match = hyundai_canfd_carnival_previous_valid &&
safety_get_ts_elapsed(microsecond_timer_get(), hyundai_canfd_carnival_previous_ts) <= 100000U &&
(msg->data[2] == (uint8_t)(hyundai_canfd_carnival_previous[2] + 1U));
bool allowed = hyundai_canfd_carnival_alt_resume && controls_allowed && !safety_rx_checks_invalid &&
hyundai_canfd_carnival_source_valid &&
safety_get_ts_elapsed(microsecond_timer_get(), hyundai_canfd_carnival_source_ts) <= 100000U &&
(msg->data[2] == (uint8_t)(hyundai_canfd_carnival_source[2] + 1U)) &&
((msg->data[3] & 0x30U) == 0x10U) && ((msg->data[4] & 0x70U) == 0x10U) &&
(hyundai_canfd_get_checksum(msg) == (hyundai_common_canfd_compute_checksum(msg) ^ 0x041DU));
for (int i = 3; i < 16; i++) {
const uint8_t changed_bits = (i == 3) ? 0x30U : ((i == 4) ? 0x70U : 0U);
const uint8_t stable_bits = (uint8_t)~changed_bits;
allowed &= ((msg->data[i] & stable_bits) == (hyundai_canfd_carnival_source[i] & stable_bits));
current_match &= ((msg->data[i] & stable_bits) == (hyundai_canfd_carnival_source[i] & stable_bits));
previous_match &= ((msg->data[i] & stable_bits) == (hyundai_canfd_carnival_previous[i] & stable_bits));
}
allowed &= current_match || previous_match;
if (!allowed) {
tx = false;
} else {
hyundai_canfd_carnival_consumed_counter = hyundai_canfd_carnival_source[2];
hyundai_canfd_carnival_consumed_counter = current_match ? hyundai_canfd_carnival_source[2] : hyundai_canfd_carnival_previous[2];
hyundai_canfd_carnival_counter_consumed = true;
hyundai_canfd_carnival_source_valid = false;
hyundai_canfd_carnival_previous_valid = false;
if (current_match) {
hyundai_canfd_carnival_source_valid = false;
}
}
}
@@ -1217,7 +1274,7 @@ static bool hyundai_canfd_tx_hook(const CANPacket_t *msg) {
}
}
if (ordinary_angle_valid && !((hyundai_canfd_ioniq6_long || hyundai_canfd_ccnc_ev_long) && safety_rx_checks_invalid)) {
tx = hyundai_canfd_tx_hook_valid(msg);
tx = (msg->addr == 0x11AU) ? hyundai_daw_tx(msg) : hyundai_canfd_tx_hook_valid(msg);
}
if ((hyundai_ordinary_angle_stock && !hyundai_angle_aol_enabled && (msg->addr == 0x110U) && tx) ||
(independent_command && independent_active && tx)) {
@@ -1236,6 +1293,7 @@ static bool hyundai_canfd_tx_hook(const CANPacket_t *msg) {
static void hyundai_canfd_reset_ioniq6_state(void) {
hyundai_canfd_ioniq6_long = false;
aol_ioniq6_long = false;
hyundai_stock_torque_aol = false;
aol_ioniq6_enable_reset();
aol_optional_release_capable = false;
aol_optional_release_policy = false;
@@ -1254,6 +1312,10 @@ static void hyundai_canfd_reset_ioniq6_state(void) {
hyundai_canfd_gv60_heartbeat_counter = 0U;
hyundai_canfd_ioniq6_heartbeat_seen = false;
hyundai_canfd_ioniq6_heartbeat_counter = 0U;
hyundai_canfd_ioniq6_bsm_low_speed = false;
hyundai_canfd_ioniq6_bsm_wheel_seen = false;
hyundai_canfd_ioniq6_bsm_wheel_counter = 0U;
hyundai_canfd_ioniq6_bsm_wheel_ts = 0U;
hyundai_canfd_ioniq6_corner_valid = false;
hyundai_canfd_ioniq6_corner_counter_seen = false;
hyundai_canfd_ioniq6_corner_counter = 0U;
@@ -1297,13 +1359,15 @@ static safety_config hyundai_canfd_init_validated(uint16_t supplied_param) {
// under torque LKA + LONG. Other uses of the bit retain their original meaning.
const uint16_t damping_profile_mask = HYUNDAI_PARAM_CANFD_ANGLE | 16U | HYUNDAI_PARAM_LONGITUDINAL | 8192U;
hyundai_canfd_lkas_damping_100 = (param & damping_profile_mask) == (16U | HYUNDAI_PARAM_LONGITUDINAL | 8192U);
const bool carnival_long_requested = (param == 0x20B4U) || (param == 0x28B4U);
if (hyundai_canfd_lkas_damping_100) {
param &= (uint16_t)~8192U;
}
const bool angle_requested = GET_FLAG(param, HYUNDAI_PARAM_CANFD_ANGLE);
const bool ioniq6_stock_aol_requested = hyundai_canfd_stock_torque_aol_param(param);
#ifdef ALLOW_DEBUG
const bool torque_long_aol_requested = hyundai_canfd_torque_long_aol_param(param) && ((unsigned int)alternative_experience == 32U);
const bool torque_long_aol_requested = ((carnival_long_requested && ((param & 0x0800U) != 0U)) || hyundai_canfd_torque_long_aol_param(param)) && ((unsigned int)alternative_experience == 32U);
#else
const bool torque_long_aol_requested = false;
#endif
@@ -1483,11 +1547,12 @@ static safety_config hyundai_canfd_init_validated(uint16_t supplied_param) {
hyundai_common_init(common_param);
gen_crc_lookup_table_16(0x1021, hyundai_canfd_crc_lut);
hyundai_canfd_alt_buttons = GET_FLAG(param, HYUNDAI_PARAM_CANFD_ALT_BUTTONS);
hyundai_canfd_carnival_alt_resume = !angle_requested && GET_FLAG(common_param, HYUNDAI_PARAM_CARNIVAL_ALT_RESUME) &&
hyundai_canfd_alt_buttons && !GET_FLAG(common_param, HYUNDAI_PARAM_LONGITUDINAL) && !hyundai_longitudinal;
hyundai_canfd_carnival_source_valid = false;
hyundai_canfd_carnival_previous_valid = false;
hyundai_canfd_carnival_counter_seen = false;
hyundai_canfd_carnival_counter_consumed = false;
hyundai_canfd_carnival_source_ts = 0U;
hyundai_canfd_lka_steer_msg_alt = GET_FLAG(param, HYUNDAI_PARAM_CANFD_LKA_STEER_MSG_ALT);
@@ -1496,11 +1561,12 @@ static safety_config hyundai_canfd_init_validated(uint16_t supplied_param) {
hyundai_canfd_angle_model = 0U;
#ifdef ALLOW_DEBUG
hyundai_canfd_ioniq6_long = ioniq6_long_requested;
hyundai_canfd_torque_ev_long = (param == 0x0015U) || (param == 0x0095U) || (torque_long_aol_requested && GET_FLAG(param, 16U)) || gv60_long_requested;
hyundai_canfd_torque_ev_long = (param == 0x0015U) || (param == 0x0095U) || (torque_long_aol_requested && GET_FLAG(param, 16U) && !carnival_long_requested) || gv60_long_requested;
#endif
hyundai_torque_aol_cancel_preserves_lateral = ioniq6_stock_aol_requested || torque_long_aol_requested ||
(param == 0x8815U) || (param == 0x8895U);
aol_optional_release_capable = torque_long_aol_requested;
hyundai_stock_torque_aol = ioniq6_stock_aol_requested;
aol_ioniq6_long = ioniq6_stock_aol_requested || torque_long_aol_requested || (param == 0x8815U) || (param == 0x8895U);
if (aol_ioniq6_long) {
static const AolSafetyPolicy aol_ioniq6_policy = {
@@ -1745,25 +1811,38 @@ static safety_config hyundai_canfd_init_validated(uint16_t supplied_param) {
#endif
if (hyundai_longitudinal) {
if (hyundai_canfd_lka_steer_msg) {
static RxCheck hyundai_canfd_lka_steer_msg_long_rx_checks[] = {
static const RxCheck hyundai_canfd_lka_steer_msg_long_rx_checks[] = {
HYUNDAI_CANFD_STD_BUTTONS_RX_CHECKS(1)
};
static const RxCheck hyundai_canfd_lka_alt_buttons_long_rx_checks[] = {
HYUNDAI_CANFD_ALT_BUTTONS_RX_CHECKS(1)
};
static const RxCheck carnival_long_rx_checks[] = {
HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(1, 0x100)
};
ret = BUILD_SAFETY_CFG(hyundai_canfd_lka_steer_msg_long_rx_checks, HYUNDAI_CANFD_LKA_STEER_MSG_LONG_TX_MSGS);
if (carnival_long_requested) {
SET_RX_CHECKS(carnival_long_rx_checks, ret);
} else if (hyundai_canfd_alt_buttons) {
SET_RX_CHECKS(hyundai_canfd_lka_alt_buttons_long_rx_checks, ret);
} else {
// Standard-button profiles retain their existing physical source.
}
if (hyundai_canfd_lka_steer_msg_alt) {
SET_TX_MSGS(HYUNDAI_CANFD_LKA_STEER_MSG_ALT_LONG_TX_MSGS, ret);
}
} else {
// Longitudinal checks for LFA steering
static RxCheck hyundai_canfd_long_rx_checks[] = {
static const RxCheck hyundai_canfd_long_rx_checks[] = {
HYUNDAI_CANFD_STD_BUTTONS_RX_CHECKS(0)
};
static RxCheck hyundai_canfd_alt_buttons_long_rx_checks[] = {
static const RxCheck hyundai_canfd_alt_buttons_long_rx_checks[] = {
HYUNDAI_CANFD_ALT_BUTTONS_RX_CHECKS(0)
};
static CanMsg hyundai_canfd_lfa_steering_camera_scc_tx_msgs[] = {
static const CanMsg hyundai_canfd_lfa_steering_camera_scc_tx_msgs[] = {
HYUNDAI_CANFD_LFA_STEERING_CAMERA_SCC_TX_MSGS(true)
};
static CanMsg hyundai_canfd_ccnc_camera_scc_tx_msgs[] = {
@@ -1791,7 +1870,7 @@ static safety_config hyundai_canfd_init_validated(uint16_t supplied_param) {
// *** LKA steering checks ***
// E-CAN is on bus 1, SCC messages are sent on cars with ADRV ECU.
// Does not use the alt buttons message
static RxCheck hyundai_canfd_lka_steer_msg_rx_checks[] = {
static const RxCheck hyundai_canfd_lka_steer_msg_rx_checks[] = {
HYUNDAI_CANFD_STD_BUTTONS_RX_CHECKS(1)
HYUNDAI_CANFD_SCC_ADDR_CHECK(1)
};
@@ -1820,12 +1899,12 @@ static safety_config hyundai_canfd_init_validated(uint16_t supplied_param) {
}
} else if (!hyundai_camera_scc) {
// Radar sends SCC messages on these cars instead of camera
static RxCheck hyundai_canfd_radar_scc_rx_checks[] = {
static const RxCheck hyundai_canfd_radar_scc_rx_checks[] = {
HYUNDAI_CANFD_STD_BUTTONS_RX_CHECKS(0)
HYUNDAI_CANFD_SCC_ADDR_CHECK(0)
};
static RxCheck hyundai_canfd_alt_buttons_radar_scc_rx_checks[] = {
static const RxCheck hyundai_canfd_alt_buttons_radar_scc_rx_checks[] = {
HYUNDAI_CANFD_ALT_BUTTONS_RX_CHECKS(0)
HYUNDAI_CANFD_SCC_ADDR_CHECK(0)
};
@@ -1841,17 +1920,17 @@ static safety_config hyundai_canfd_init_validated(uint16_t supplied_param) {
// *** LFA steering checks ***
// Camera sends SCC messages on LFA steering cars.
// Both button messages exist on some platforms, so we ensure we track the correct one using flag
static RxCheck hyundai_canfd_rx_checks[] = {
static const RxCheck hyundai_canfd_rx_checks[] = {
HYUNDAI_CANFD_STD_BUTTONS_RX_CHECKS(0)
HYUNDAI_CANFD_SCC_ADDR_CHECK(2)
};
static RxCheck hyundai_canfd_alt_buttons_rx_checks[] = {
static const RxCheck hyundai_canfd_alt_buttons_rx_checks[] = {
HYUNDAI_CANFD_ALT_BUTTONS_RX_CHECKS(0)
HYUNDAI_CANFD_SCC_ADDR_CHECK(2)
};
static CanMsg hyundai_canfd_lfa_steering_camera_scc_tx_msgs[] = {
static const CanMsg hyundai_canfd_lfa_steering_camera_scc_tx_msgs[] = {
HYUNDAI_CANFD_LFA_STEERING_CAMERA_SCC_TX_MSGS(false)
};
static CanMsg hyundai_canfd_ccnc_camera_scc_tx_msgs[] = {
@@ -2159,6 +2238,7 @@ static safety_config hyundai_canfd_init_validated(uint16_t supplied_param) {
}
static safety_config hyundai_canfd_init(uint16_t param) {
hyundai_daw_init(true, false);
hyundai_canfd_reset_ioniq6_state();
hyundai_ordinary_angle_reset();
#ifdef ALLOW_DEBUG
@@ -2177,6 +2257,9 @@ static safety_config hyundai_canfd_init(uint16_t param) {
(param == 0x8815U) || (param == 0x8895U);
if (valid && marker_valid) {
ret = hyundai_canfd_init_validated(param);
const bool daw_profile = (param == 0x0011U) || (param == 0x0091U) || (param == 0x0811U) || (param == 0x0891U) ||
(param == 0x8015U) || (param == 0x8095U) || (param == 0x8815U) || (param == 0x8895U);
ret = hyundai_daw_configure(ret, true, daw_profile);
}
return ret;
}
@@ -25,6 +25,6 @@ static inline bool hyundai_canfd_torque_long_aol_param(uint16_t param) {
((base & 4U) != 0U) && ((fuel == 0U) || (fuel == 2U)) &&
((topology == 0U) || (topology == 8U) || (topology == 16U) ||
(topology == 144U) || (topology == 32U) || (topology == 40U));
return sportage || (param == 0x080DU) || (param == 0x0815U) || (param == 0x0895U) || (param == 0x2815U) || (param == 0x2895U);
return (param == 0x28B4U) || sportage || (param == 0x080DU) || (param == 0x0815U) || (param == 0x0895U) || (param == 0x2815U) || (param == 0x2895U);
}
#endif
@@ -2,8 +2,33 @@
#include "opendbc/safety/declarations.h"
extern uint16_t hyundai_canfd_crc_lut[256];
uint16_t hyundai_canfd_crc_lut[256];
// CRC-16 lookup table for poly 0x1021 (CRC-16/XMODEM)
// generated with: python3 -c 'f=lambda c:[c:=((c<<1)^0x1021)&0xFFFF if c&0x8000 else (c<<1)&0xFFFF for _ in range(8)][-1];print(",\n".join(" "+", ".join(f"0x{f(i<<8):04X}U" for i in range(r,min(r+12,256))) for r in range(0,256,12)))'
extern const uint16_t hyundai_canfd_crc_lut[256];
const uint16_t hyundai_canfd_crc_lut[256] = {
0x0000U, 0x1021U, 0x2042U, 0x3063U, 0x4084U, 0x50A5U, 0x60C6U, 0x70E7U, 0x8108U, 0x9129U, 0xA14AU, 0xB16BU,
0xC18CU, 0xD1ADU, 0xE1CEU, 0xF1EFU, 0x1231U, 0x0210U, 0x3273U, 0x2252U, 0x52B5U, 0x4294U, 0x72F7U, 0x62D6U,
0x9339U, 0x8318U, 0xB37BU, 0xA35AU, 0xD3BDU, 0xC39CU, 0xF3FFU, 0xE3DEU, 0x2462U, 0x3443U, 0x0420U, 0x1401U,
0x64E6U, 0x74C7U, 0x44A4U, 0x5485U, 0xA56AU, 0xB54BU, 0x8528U, 0x9509U, 0xE5EEU, 0xF5CFU, 0xC5ACU, 0xD58DU,
0x3653U, 0x2672U, 0x1611U, 0x0630U, 0x76D7U, 0x66F6U, 0x5695U, 0x46B4U, 0xB75BU, 0xA77AU, 0x9719U, 0x8738U,
0xF7DFU, 0xE7FEU, 0xD79DU, 0xC7BCU, 0x48C4U, 0x58E5U, 0x6886U, 0x78A7U, 0x0840U, 0x1861U, 0x2802U, 0x3823U,
0xC9CCU, 0xD9EDU, 0xE98EU, 0xF9AFU, 0x8948U, 0x9969U, 0xA90AU, 0xB92BU, 0x5AF5U, 0x4AD4U, 0x7AB7U, 0x6A96U,
0x1A71U, 0x0A50U, 0x3A33U, 0x2A12U, 0xDBFDU, 0xCBDCU, 0xFBBFU, 0xEB9EU, 0x9B79U, 0x8B58U, 0xBB3BU, 0xAB1AU,
0x6CA6U, 0x7C87U, 0x4CE4U, 0x5CC5U, 0x2C22U, 0x3C03U, 0x0C60U, 0x1C41U, 0xEDAEU, 0xFD8FU, 0xCDECU, 0xDDCDU,
0xAD2AU, 0xBD0BU, 0x8D68U, 0x9D49U, 0x7E97U, 0x6EB6U, 0x5ED5U, 0x4EF4U, 0x3E13U, 0x2E32U, 0x1E51U, 0x0E70U,
0xFF9FU, 0xEFBEU, 0xDFDDU, 0xCFFCU, 0xBF1BU, 0xAF3AU, 0x9F59U, 0x8F78U, 0x9188U, 0x81A9U, 0xB1CAU, 0xA1EBU,
0xD10CU, 0xC12DU, 0xF14EU, 0xE16FU, 0x1080U, 0x00A1U, 0x30C2U, 0x20E3U, 0x5004U, 0x4025U, 0x7046U, 0x6067U,
0x83B9U, 0x9398U, 0xA3FBU, 0xB3DAU, 0xC33DU, 0xD31CU, 0xE37FU, 0xF35EU, 0x02B1U, 0x1290U, 0x22F3U, 0x32D2U,
0x4235U, 0x5214U, 0x6277U, 0x7256U, 0xB5EAU, 0xA5CBU, 0x95A8U, 0x8589U, 0xF56EU, 0xE54FU, 0xD52CU, 0xC50DU,
0x34E2U, 0x24C3U, 0x14A0U, 0x0481U, 0x7466U, 0x6447U, 0x5424U, 0x4405U, 0xA7DBU, 0xB7FAU, 0x8799U, 0x97B8U,
0xE75FU, 0xF77EU, 0xC71DU, 0xD73CU, 0x26D3U, 0x36F2U, 0x0691U, 0x16B0U, 0x6657U, 0x7676U, 0x4615U, 0x5634U,
0xD94CU, 0xC96DU, 0xF90EU, 0xE92FU, 0x99C8U, 0x89E9U, 0xB98AU, 0xA9ABU, 0x5844U, 0x4865U, 0x7806U, 0x6827U,
0x18C0U, 0x08E1U, 0x3882U, 0x28A3U, 0xCB7DU, 0xDB5CU, 0xEB3FU, 0xFB1EU, 0x8BF9U, 0x9BD8U, 0xABBBU, 0xBB9AU,
0x4A75U, 0x5A54U, 0x6A37U, 0x7A16U, 0x0AF1U, 0x1AD0U, 0x2AB3U, 0x3A92U, 0xFD2EU, 0xED0FU, 0xDD6CU, 0xCD4DU,
0xBDAAU, 0xAD8BU, 0x9DE8U, 0x8DC9U, 0x7C26U, 0x6C07U, 0x5C64U, 0x4C45U, 0x3CA2U, 0x2C83U, 0x1CE0U, 0x0CC1U,
0xEF1FU, 0xFF3EU, 0xCF5DU, 0xDF7CU, 0xAF9BU, 0xBFBAU, 0x8FD9U, 0x9FF8U, 0x6E17U, 0x7E36U, 0x4E55U, 0x5E74U,
0x2E93U, 0x3EB2U, 0x0ED1U, 0x1EF0U
};
static const uint8_t HYUNDAI_PREV_BUTTON_SAMPLES = 8; // roughly 160 ms
@@ -0,0 +1,117 @@
#pragma once
#include "opendbc/safety/modes/hyundai_common.h"
static uint16_t hyundai_daw_addr;
static unsigned int hyundai_daw_bus;
static unsigned int hyundai_daw_len;
static uint8_t hyundai_daw_data[16];
static uint8_t hyundai_daw_previous[16];
static bool hyundai_daw_previous_seen;
static uint32_t hyundai_daw_previous_ts;
static uint32_t hyundai_daw_rx_ts;
static uint32_t hyundai_daw_tx_ts;
static bool hyundai_daw_seen;
static bool hyundai_daw_counter_seen;
static bool hyundai_daw_sent;
static void hyundai_daw_init(bool canfd, bool enabled) {
hyundai_daw_addr = enabled ? (canfd ? 0x11AU : 0x53EU) : 0U;
hyundai_daw_bus = canfd ? 1U : 0U;
hyundai_daw_len = canfd ? 16U : 6U;
hyundai_daw_previous_seen = false;
hyundai_daw_previous_ts = 0U;
hyundai_daw_seen = false;
hyundai_daw_counter_seen = false;
hyundai_daw_sent = false;
hyundai_daw_rx_ts = 0U;
hyundai_daw_tx_ts = 0U;
}
static void hyundai_daw_rx(const CANPacket_t *msg) {
const bool canfd = hyundai_daw_len == 16U;
if ((hyundai_daw_addr != 0U) && (msg->addr == hyundai_daw_addr) && (msg->bus == (canfd ? hyundai_daw_bus : 2U))) {
const uint32_t now = microsecond_timer_get();
const bool fresh = hyundai_daw_counter_seen && (safety_get_ts_elapsed(now, hyundai_daw_rx_ts) <= 100000U);
bool valid = GET_LEN(msg) == hyundai_daw_len;
if (valid && canfd) {
valid = GET_BYTES_LE(msg, 0, 2) == (hyundai_common_canfd_compute_checksum(msg) ^ 0x041DU);
if (fresh) {
valid &= (uint8_t)(msg->data[2] - hyundai_daw_data[2]) == 1U;
}
}
hyundai_daw_previous_seen = canfd && valid && fresh && hyundai_daw_seen;
if (hyundai_daw_previous_seen) {
(void)memcpy(hyundai_daw_previous, hyundai_daw_data, hyundai_daw_len);
hyundai_daw_previous_ts = hyundai_daw_rx_ts;
}
hyundai_daw_seen = valid;
if (valid) {
(void)memcpy(hyundai_daw_data, msg->data, hyundai_daw_len);
hyundai_daw_rx_ts = now;
hyundai_daw_counter_seen = true;
}
}
}
static uint8_t hyundai_daw_expected(const uint8_t *data, unsigned int i) {
uint8_t expected = data[i];
if ((hyundai_daw_len == 16U) && (i == 7U) && (((expected >> 3U) & 7U) == 1U)) {
expected &= (uint8_t)~0x38U;
} else if ((hyundai_daw_len == 6U) && (i == 5U) && ((expected & 7U) >= 1U) && ((expected & 7U) <= 3U)) {
expected &= (uint8_t)~7U;
} else {
// Non-advisory payload bits are copied exactly from the current source.
}
return expected;
}
static bool hyundai_daw_tx(const CANPacket_t *msg) {
const uint32_t now = microsecond_timer_get();
const bool canfd = hyundai_daw_len == 16U;
bool valid = hyundai_daw_seen && (hyundai_daw_addr != 0U) && (msg->addr == hyundai_daw_addr) &&
(msg->bus == hyundai_daw_bus) && (GET_LEN(msg) == hyundai_daw_len) &&
(safety_get_ts_elapsed(now, hyundai_daw_rx_ts) <= 100000U) &&
(!hyundai_daw_sent || (safety_get_ts_elapsed(now, hyundai_daw_tx_ts) >= 5000U));
if (valid) {
const uint8_t *source = hyundai_daw_data;
if (canfd && (msg->data[2] != hyundai_daw_data[2])) {
valid = hyundai_daw_previous_seen && (msg->data[2] == hyundai_daw_previous[2]) &&
(safety_get_ts_elapsed(now, hyundai_daw_previous_ts) <= 100000U);
if (valid) {
// A one-frame receive race cannot replace newer warning or status data.
for (unsigned int i = 3U; i < hyundai_daw_len; i++) {
valid &= hyundai_daw_expected(hyundai_daw_previous, i) == hyundai_daw_expected(hyundai_daw_data, i);
}
source = hyundai_daw_previous;
}
}
for (unsigned int i = canfd ? 2U : 0U; i < hyundai_daw_len; i++) {
valid &= msg->data[i] == hyundai_daw_expected(source, i);
}
if (canfd) {
valid &= GET_BYTES_LE(msg, 0, 2) == (hyundai_common_canfd_compute_checksum(msg) ^ 0x041DU);
}
}
if (valid) {
hyundai_daw_sent = true;
hyundai_daw_tx_ts = now;
}
return valid;
}
static safety_config hyundai_daw_configure(safety_config ret, bool canfd, bool enabled) {
hyundai_daw_init(canfd, false);
if (enabled && (ret.tx_msgs_len > 0) && (ret.tx_msgs_len < 64)) {
static CanMsg daw_tx_msgs[64];
for (int i = 0; i < ret.tx_msgs_len; i++) {
daw_tx_msgs[i] = ret.tx_msgs[i];
}
const CanMsg daw = {canfd ? 0x11AU : 0x53EU, canfd ? 1U : 0U, canfd ? 16U : 6U, .check_relay = false};
daw_tx_msgs[ret.tx_msgs_len] = daw;
ret.tx_msgs_len++;
ret.tx_msgs = daw_tx_msgs;
hyundai_daw_init(canfd, true);
}
return ret;
}
+1 -1
View File
@@ -91,7 +91,7 @@ static safety_config mazda_init(uint16_t param) {
{MAZDA_LKAS_HUD, 0, 8, .check_relay = true},
};
static RxCheck mazda_rx_checks[] = {
static const RxCheck mazda_rx_checks[] = {
{.msg = {{MAZDA_CRZ_CTRL, 0, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{MAZDA_CRZ_BTNS, 0, 8, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{MAZDA_STEER_TORQUE, 0, 8, 83U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
+21 -5
View File
@@ -2,8 +2,6 @@
#include "opendbc/safety/declarations.h"
static uint8_t mg_crc_lut[256];
static uint32_t mg_get_checksum(const CANPacket_t *msg) {
uint32_t checksum = msg->data[7];
if (msg->addr == 0x1ecU) {
@@ -13,6 +11,26 @@ static uint32_t mg_get_checksum(const CANPacket_t *msg) {
}
static uint32_t mg_compute_checksum(const CANPacket_t *msg) {
// CRC-8 lookup table for poly 0x1D (SAE J1850)
// generated with: python3 -c 'f=lambda c:[c:=((c<<1)^0x1D)&0xFF if c&0x80 else (c<<1)&0xFF for _ in range(8)][-1];print(",\n".join(" "+", ".join(f"0x{f(i):02X}U" for i in range(r,r+16)) for r in range(0,256,16)))'
static const uint8_t mg_crc_lut[256] = {
0x00U, 0x1DU, 0x3AU, 0x27U, 0x74U, 0x69U, 0x4EU, 0x53U, 0xE8U, 0xF5U, 0xD2U, 0xCFU, 0x9CU, 0x81U, 0xA6U, 0xBBU,
0xCDU, 0xD0U, 0xF7U, 0xEAU, 0xB9U, 0xA4U, 0x83U, 0x9EU, 0x25U, 0x38U, 0x1FU, 0x02U, 0x51U, 0x4CU, 0x6BU, 0x76U,
0x87U, 0x9AU, 0xBDU, 0xA0U, 0xF3U, 0xEEU, 0xC9U, 0xD4U, 0x6FU, 0x72U, 0x55U, 0x48U, 0x1BU, 0x06U, 0x21U, 0x3CU,
0x4AU, 0x57U, 0x70U, 0x6DU, 0x3EU, 0x23U, 0x04U, 0x19U, 0xA2U, 0xBFU, 0x98U, 0x85U, 0xD6U, 0xCBU, 0xECU, 0xF1U,
0x13U, 0x0EU, 0x29U, 0x34U, 0x67U, 0x7AU, 0x5DU, 0x40U, 0xFBU, 0xE6U, 0xC1U, 0xDCU, 0x8FU, 0x92U, 0xB5U, 0xA8U,
0xDEU, 0xC3U, 0xE4U, 0xF9U, 0xAAU, 0xB7U, 0x90U, 0x8DU, 0x36U, 0x2BU, 0x0CU, 0x11U, 0x42U, 0x5FU, 0x78U, 0x65U,
0x94U, 0x89U, 0xAEU, 0xB3U, 0xE0U, 0xFDU, 0xDAU, 0xC7U, 0x7CU, 0x61U, 0x46U, 0x5BU, 0x08U, 0x15U, 0x32U, 0x2FU,
0x59U, 0x44U, 0x63U, 0x7EU, 0x2DU, 0x30U, 0x17U, 0x0AU, 0xB1U, 0xACU, 0x8BU, 0x96U, 0xC5U, 0xD8U, 0xFFU, 0xE2U,
0x26U, 0x3BU, 0x1CU, 0x01U, 0x52U, 0x4FU, 0x68U, 0x75U, 0xCEU, 0xD3U, 0xF4U, 0xE9U, 0xBAU, 0xA7U, 0x80U, 0x9DU,
0xEBU, 0xF6U, 0xD1U, 0xCCU, 0x9FU, 0x82U, 0xA5U, 0xB8U, 0x03U, 0x1EU, 0x39U, 0x24U, 0x77U, 0x6AU, 0x4DU, 0x50U,
0xA1U, 0xBCU, 0x9BU, 0x86U, 0xD5U, 0xC8U, 0xEFU, 0xF2U, 0x49U, 0x54U, 0x73U, 0x6EU, 0x3DU, 0x20U, 0x07U, 0x1AU,
0x6CU, 0x71U, 0x56U, 0x4BU, 0x18U, 0x05U, 0x22U, 0x3FU, 0x84U, 0x99U, 0xBEU, 0xA3U, 0xF0U, 0xEDU, 0xCAU, 0xD7U,
0x35U, 0x28U, 0x0FU, 0x12U, 0x41U, 0x5CU, 0x7BU, 0x66U, 0xDDU, 0xC0U, 0xE7U, 0xFAU, 0xA9U, 0xB4U, 0x93U, 0x8EU,
0xF8U, 0xE5U, 0xC2U, 0xDFU, 0x8CU, 0x91U, 0xB6U, 0xABU, 0x10U, 0x0DU, 0x2AU, 0x37U, 0x64U, 0x79U, 0x5EU, 0x43U,
0xB2U, 0xAFU, 0x88U, 0x95U, 0xC6U, 0xDBU, 0xFCU, 0xE1U, 0x5AU, 0x47U, 0x60U, 0x7DU, 0x2EU, 0x33U, 0x14U, 0x09U,
0x7FU, 0x62U, 0x45U, 0x58U, 0x0BU, 0x16U, 0x31U, 0x2CU, 0x97U, 0x8AU, 0xADU, 0xB0U, 0xE3U, 0xFEU, 0xD9U, 0xC4U
};
uint32_t checksum;
if (msg->addr == 0x1ecU) {
uint32_t torque = ((msg->data[0] & 0x7U) << 8) | msg->data[1];
@@ -104,11 +122,9 @@ static bool mg_tx_hook(const CANPacket_t *msg) {
static safety_config mg_init(uint16_t param) {
SAFETY_UNUSED(param);
gen_crc_lookup_table_8(0x1DU, mg_crc_lut);
static const CanMsg MG_TX_MSGS[] = {{0x1fd, 0, 8, .check_relay = true}};
static RxCheck mg_rx_checks[] = {
static const RxCheck mg_rx_checks[] = {
{.msg = {{0x23c, 0, 8, .frequency = 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // SCS_HSC2_FrP19 (speed)
{.msg = {{0xaf, 0, 8, .frequency = 100U, .ignore_checksum = true, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // GW_HSC2_HCU_FrP00 (gas pedal)
{.msg = {{0x1ec, 0, 8, .frequency = 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // EPS_HSC2_FrP03 (driver torque)
+1 -1
View File
@@ -109,7 +109,7 @@ static safety_config psa_init(uint16_t param) {
{PSA_LANE_KEEP_ASSIST, PSA_MAIN_BUS, 8, .check_relay = true}, // EPS steering
};
static RxCheck psa_rx_checks[] = {
static const RxCheck psa_rx_checks[] = {
{.msg = {{PSA_HS2_DAT_MDD_CMD_452, PSA_ADAS_BUS, 6, 20U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // cruise state
{.msg = {{PSA_HS2_DYN_ABR_38D, PSA_MAIN_BUS, 8, 25U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // speed
{.msg = {{PSA_STEERING_ALT, PSA_MAIN_BUS, 7, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // steering angle
@@ -118,8 +118,9 @@ static bool subaru_avh_tx(const CANPacket_t *msg) {
// Rejected required wheel-speed input must also invalidate its cached source.
for (int i = 0; i < current_safety_config.rx_checks_len; i++) {
const RxCheck *check = &current_safety_config.rx_checks[i];
if (((unsigned int)check->msg[check->status.index].addr == 0x13AU) && (check->msg[check->status.index].bus == 1U)) {
allowed &= check->status.valid_checksum && (check->status.wrong_counters < MAX_WRONG_COUNTERS);
const RxStatus *status = &rx_status[i];
if (((unsigned int)check->msg[status->index].addr == 0x13AU) && (check->msg[status->index].bus == 1U)) {
allowed &= status->valid_checksum && (status->wrong_counters < MAX_WRONG_COUNTERS);
}
}
@@ -83,7 +83,7 @@ static safety_config subaru_preglobal_init(uint16_t param) {
};
// TODO: do checksum and counter checks after adding the signals to the outback dbc file
static RxCheck subaru_preglobal_rx_checks[] = {
static const RxCheck subaru_preglobal_rx_checks[] = {
{.msg = {{MSG_SUBARU_PG_Throttle, SUBARU_PG_MAIN_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{MSG_SUBARU_PG_Steering_Torque, SUBARU_PG_MAIN_BUS, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{MSG_SUBARU_PG_CruiseControl, SUBARU_PG_MAIN_BUS, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
+6 -2
View File
@@ -4,6 +4,7 @@
#include "opendbc/safety/modes/tesla_screen_aol.h"
static bool tesla_longitudinal = false;
static bool tesla_coop_steering = false;
static bool tesla_stock_aeb = false;
// Only rising edges while controls are not allowed are considered for these systems:
@@ -102,12 +103,14 @@ static void tesla_rx_hook(const CANPacket_t *msg) {
const int angle_meas_new = (((msg->data[4] & 0x3FU) << 8) | msg->data[5]) - 8192U;
update_sample(&angle_meas, angle_meas_new);
const unsigned int raw_torsion_bar_torque = ((msg->data[2] & 0x0FU) << 8) | msg->data[3];
const int torsion_bar_torque = (int)raw_torsion_bar_torque - 2050;
const int hands_on_level = msg->data[4] >> 6; // EPAS3S_handsOnLevel
const int eac_status = msg->data[6] >> 5; // EPAS3S_eacStatus
const int eac_error_code = msg->data[2] >> 4; // EPAS3S_eacErrorCode
// Disengage on normal user override, or if high angle rate fault from user overriding extremely quickly
steering_disengage = (hands_on_level >= 3) || ((eac_status == 0) && (eac_error_code == 9));
steering_disengage = (hands_on_level >= 3) || (tesla_coop_steering && (SAFETY_ABS(torsion_bar_torque) > 500)) || ((eac_status == 0) && (eac_error_code == 9));
}
// Vehicle speed (DI_speed)
@@ -328,6 +331,7 @@ static safety_config tesla_init(uint16_t param) {
{0x27D, 0, 3, .check_relay = true, .disable_static_blocking = true}, // APS_eacMonitor
};
tesla_coop_steering = GET_FLAG(param, 256U);
tesla_longitudinal = false;
#ifdef ALLOW_DEBUG
tesla_longitudinal = GET_FLAG(param, 1U);
@@ -341,7 +345,7 @@ static safety_config tesla_init(uint16_t param) {
tesla_autopark = true;
tesla_autopark_prev = false;
static RxCheck tesla_model3_y_rx_checks[] = {
static const RxCheck tesla_model3_y_rx_checks[] = {
{.msg = {{0x2b9, 2, 8, 25U, .max_counter = 7U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // DAS_control
{.msg = {{0x488, 2, 4, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // DAS_steeringControl
{.msg = {{0x257, 0, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // DI_speed (speed in kph)
@@ -64,11 +64,11 @@ static bool tesla_preap_sources_ready(bool require_belt) {
(safety_get_ts_elapsed(microsecond_timer_get(), tesla_preap_stw_ts) <= 300000U);
const uint32_t now = microsecond_timer_get();
for (int i = 0; i < current_safety_config.rx_checks_len; i++) {
ready = ready && current_safety_config.rx_checks[i].status.msg_seen &&
current_safety_config.rx_checks[i].status.valid_checksum &&
current_safety_config.rx_checks[i].status.valid_quality_flag &&
(current_safety_config.rx_checks[i].status.wrong_counters < MAX_WRONG_COUNTERS) &&
(safety_get_ts_elapsed(now, current_safety_config.rx_checks[i].status.last_timestamp) <= 1000000U);
ready = ready && rx_status[i].msg_seen &&
rx_status[i].valid_checksum &&
rx_status[i].valid_quality_flag &&
(rx_status[i].wrong_counters < MAX_WRONG_COUNTERS) &&
(safety_get_ts_elapsed(now, rx_status[i].last_timestamp) <= 1000000U);
}
return ready;
}
@@ -370,8 +370,8 @@ static safety_config tesla_preap_init(uint16_t param) {
};
safety_config config = BUILD_SAFETY_CFG(rx, tx);
if (pedal) {
config.rx_checks[config.rx_checks_len - 1].msg[0].bus = tesla_preap_pedal_bus;
config.rx_checks[config.rx_checks_len - 1].status = (RxStatus){0};
rx[config.rx_checks_len - 1].msg[0].bus = tesla_preap_pedal_bus;
rx_status[config.rx_checks_len - 1] = (RxStatus){0};
} else {
config.rx_checks_len--;
}

Some files were not shown because too many files have changed in this diff Show More