Compare commits

...

850 Commits

Author SHA1 Message Date
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
firestar5683 2ee94f3643 Use saved vehicle settings in learner recovery tests 2026-11-01 21:57:07 -06:00
firestar5683 9e4cba8695 Test angle demand conditioning in StarPilot Controller 2026-11-01 21:28:45 -06:00
firestar5683 7cb8ea919f Isolate preference and replay clock test fixtures 2026-11-01 21:10:04 -06:00
firestar5683 f2f8cfffa8 Stop Auto Learn when learning is switched off 2026-11-01 21:01:08 -06:00
firestar5683 45c081b181 Balance independent Ford lifecycle cases across CI shards 2026-11-01 19:46:33 -06:00
firestar5683 736580721a Match Heatdeath safety builds and stream source verification 2026-11-01 18:59:11 -06:00
firestar5683 374a4134f6 Complete Honda active observation Card fixture 2026-11-01 18:36:08 -06:00
firestar5683 b16fc29ed5 Match radar isolation fixture to process health API 2026-11-01 18:17:07 -06:00
Dom c1e2b5f181 Complete Card fixtures and make SLC test timing deterministic 2026-11-01 17:44:39 -06:00
firestar5683 2c7f252feb Align developer command tests with JotPluggler alias 2026-11-01 17:25:38 -06:00
firestar5683 e522e5c6a3 Share immutable FLM evidence to bound learning memory 2026-11-01 17:14:56 -06:00
firestar5683 55f23c7fd5 Restore separate StarPilot Ford controller choices 2026-11-01 17:12:54 -06:00
firestar5683 61ad53c61f Keep automatic Tesla AP1 discovery on normal wiring 2026-11-01 16:55:04 -06:00
firestar5683 36e0e37086 Keep driving independent of UI restarts 2026-11-01 16:54:44 -06:00
firestar5683 407288852f Preserve Ford AOL through startup and brake transitions 2026-11-01 16:52:24 -06:00
firestar5683 0384be1253 Keep AOL through Nostalgia paddle handoff 2026-11-01 16:50:51 -06:00
firestar5683 a13b6395d4 Build stable traffic mode icon 2026-11-01 16:46:05 -06:00
firestar5683 e7cca0a136 Keep traffic mode icon stable on C4 2026-11-01 16:44:22 -06:00
firestar5683 c0bbed7762 Launch JotPluggler with juggle shortcut 2026-11-01 16:33:44 -06:00
firestar5683 6df04fa8e0 Detect Prius TSS2 steering rack retrofits automatically 2026-11-01 16:23:55 -06:00
firestar5683 40cd8ca264 Keep selected Tesla discovery on its declared CAN bus 2026-11-01 16:15:36 -06:00
firestar5683 d9b60fddc1 Balance measured Ford lifecycle work 2026-11-01 13:45:31 -06:00
firestar5683 153b95a88f Keep Auto Learn evidence current 2026-11-01 13:13:40 -06:00
firestar5683 5b6bdbe8ef Use typed gradient test fixtures 2026-11-01 12:39:39 -06:00
firestar5683 b70b7e16c4 Batch path gradient interpolation 2026-11-01 12:17:17 -06:00
firestar5683 195b7000aa Keep CI shard capacity and reviewed replay metadata consistent 2026-11-01 00:23:15 -05:00
firestar5683 1c7e21c43e Repair controller observation and current regression contracts 2026-11-01 00:08:07 -05:00
Dom e61c7993af Complete Card test interface fixtures for CAN observation 2026-10-31 23:59:05 -05:00
firestar5683 b158bc378a Preserve Ford expiry guards alongside native-first AOL braking 2026-10-31 23:15:31 -05:00
firestar5683 72f16053b5 Keep Ford curvature handoffs within native steering limits 2026-10-31 22:57:13 -05:00
Dom 1fe202b904 Narrow optional Bolt test intent before latch assignment 2026-10-31 22:40:12 -05:00
firestar5683 33bc40d061 Avoid invisible onroad texture draws 2026-10-31 22:35:49 -05:00
firestar5683 0102ad2a35 Review model producer after lane correction removal 2026-10-31 22:08:42 -05:00
firestar5683 5b0f81caa1 Reconcile queued Mach-E inputs before declaring a source fault 2026-10-31 21:56:17 -05:00
firestar5683 77ff308c76 Preserve GM lateral intent across brief Card source gaps 2026-10-31 21:48:35 -05:00
Dom cd585f55c2 Repair alert fixture and document weather epoch clock 2026-10-31 21:06:07 -05:00
firestar5683 feb1f442c2 Repair Ford BluePilot startup fixtures and native test loader identity 2026-10-31 20:44:19 -05:00
firestar5683 a7f4ba8254 Simplify Ford steering settings and preserve independent lateral intent 2026-10-31 20:36:04 -05:00
firestar5683 0f71f4669f Reduce FLM evidence allocations in Controls 2026-10-31 19:27:54 -05:00
firestar5683 2bc4f59d41 Report current FLM admission blockers 2026-10-31 19:25:18 -05:00
firestar5683 e8256ee5b2 Avoid repeated schema misses in the onroad display 2026-10-31 19:11:51 -05:00
firestar5683 1738b3798d Build Galaxy dashboard weather 2026-10-31 18:21:15 -05:00
firestar5683 d69ba777b5 Show outside temperature on Galaxy dashboard 2026-10-31 18:19:40 -05:00
firestar5683 0206e5dfc1 Default lane-centering model breakaway to fifty percent 2026-10-31 18:18:13 -05:00
Dom ff15fc4fae ci: restore selfdrived executable file mode
Restore the existing shebang script's executable mode without changing its source blob.
2026-10-31 17:57:44 -05:00
firestar5683 83e6132b6d Prefer another FLM coordinate after rejected oscillation 2026-10-31 17:56:27 -05:00
firestar5683 31b34665ce Preserve Ford AOL across gas override and lag recovery 2026-10-31 17:55:34 -05:00
firestar5683 8df7979373 Build matching GV60 HDA1 AOL artifacts 2026-10-31 17:43:02 -05:00
firestar5683 6f88e79025 Support captured GV60 HDA1 Alpha Long AOL 2026-10-31 17:32:21 -05:00
firestar5683 23123aefb4 Keep radar failures from disabling independent lateral steering 2026-10-31 17:25:44 -05:00
firestar5683 ddd67e58bc Reuse exact GM configuration checks in driving hot paths 2026-10-31 17:00:37 -05:00
firestar5683 b6e25bfa6d Match CAN-FD longitudinal parsers to fuel and camera layout 2026-10-31 16:55:25 -05:00
firestar5683 f10a7e229b Preserve stock CAN-FD AOL intent through held-gas recovery 2026-10-31 16:45:21 -05:00
firestar5683 ac318aa31e Fix Prius stop-and-go settings and stopped resume 2026-10-31 16:40:36 -05:00
firestar5683 15134f1ec2 Build Panda host binaries for comma hardware 2026-10-31 16:12:56 -05:00
Dom cb0c1507de test: complete default metadata lookup in remaining preference fixtures
Delegate the isolated Ioniq5PE, Toyota shell and GM pitch mock default lookups to real Params metadata. Preserve existing assertions, no-setting-write checks and runtime behavior.
2026-10-31 15:48:59 -05:00
firestar5683 3926f11699 Avoid unnecessary AOL recovery profile validation 2026-10-31 15:46:58 -05:00
firestar5683 8a760a3381 Scope ordinary Ford native lookup to configured Ford cars 2026-10-31 14:54:27 -05:00
firestar5683 58c38a4834 Honor default AOL buttons on classic HKG 2026-10-31 14:52:46 -05:00
Dom eb56047d57 test: supply real defaults and fresh onroad state in isolated fixtures
Delegate mock default lookup to Params metadata and supply the existing healthy UI fixtures with their post-start receive frame and onroad state. Preserve all runtime code, assertions and negative authority coverage.
2026-10-31 14:45:06 -05:00
alan-polk 67ad8ff05b Merge BluePilot Ford lateral library v0.2.1 2026-10-31 13:51:21 -05:00
firestar5683 fdbce123c8 Build matching BluePilot Ford firmware and host binaries 2026-10-31 13:40:02 -05:00
firestar5683 5235f58e07 Keep FLM trial input tapes current between motion deliveries 2026-10-31 13:39:45 -05:00
firestar5683 f26ab5e7f1 Integrate pinned BluePilot defaults and qualify Ford AOL lifecycle 2026-10-31 13:29:29 -05:00
alan-polk 14acf1ed31 Merge BluePilot Ford lateral library v0.2.1 2026-10-31 13:29:27 -05:00
firestar5683 6d2e382e92 Renew FLM from already received motion during FIFO ingest 2026-10-31 13:06:52 -05:00
firestar5683 fcb59fa1da Validate each FLM binding once per witness call 2026-10-31 12:54:03 -05:00
firestar5683 8c404b3b8a Align AOL reset and favorites fixtures with independent controls 2026-10-31 12:23:09 -05:00
firestar5683 3498e84770 Reuse validated immutable FLM source identity content 2026-10-31 12:15:20 -05:00
firestar5683 72f04816c3 Add metric-aware extra stopped-lead spacing 2026-10-31 12:04:00 -05:00
firestar5683 9068ec74f7 Keep quiet-camera HKG discovery on normal wiring 2026-10-31 11:58:04 -05:00
firestar5683 2df9c98963 Keep bookmark shortcuts working while alerts are displayed 2026-10-31 11:49:51 -05:00
firestar5683 70f523edc5 Renew owned FLM trials when admitted queued input becomes ready 2026-10-31 10:46:02 -05:00
firestar5683 c1f37c8314 Renew owned FLM trials when admitted queued input becomes ready 2026-10-31 10:26:39 -05:00
firestar5683 15ed9799cb Renew owned FLM trials when admitted queued input becomes ready 2026-10-31 09:43:22 -05:00
Dom f9dccd0c67 test(ford): return native transmit outcomes to joined angle fixture
Feed exact emitted steering payloads and their accepted/rejected host-bus receipts on the next parser cycle. Preserve all assertions and runtime behavior.
2026-10-31 09:36:31 -05:00
Dom 6a972ae87e test(gm): feed actual steering transmit receipts in healthy fixtures
Keep existing assertions and runtime behavior unchanged. Model healthy transport by returning only actually emitted steering commands; retain missing, stale, wrong-bus and rejected receipt coverage.
2026-10-31 09:25:04 -05:00
firestar5683 5197fe4568 Fix live network label typing and generated catalog lint policy 2026-10-31 08:51:50 -05:00
firestar5683 642d9d657d Record additive engagement receipt enum in schema compatibility review 2026-10-31 08:42:32 -05:00
firestar5683 cd0f46b9d6 Prevent stale lead handoff from reacquiring a clear stopped hold 2026-10-31 08:38:43 -05:00
firestar5683 a6030fa25d Feed native shadow receipts through Ford angle recovery tests 2026-10-31 08:27:09 -05:00
firestar5683 b8200e7bab Revisit pending FLM motion before another evidence drain 2026-10-31 08:19:02 -05:00
firestar5683 f9b678a7c3 Restore HKG AOL with auxiliary Pandas and default buttons 2026-10-31 08:14:06 -05:00
firestar5683 68fca49aab Avoid repeated static Bolt and empty-fault classification in Card 2026-10-31 08:11:52 -05:00
firestar5683 731a9e6c79 Keep Ford angle handovers quiet and preserve gas-override AOL 2026-10-31 08:09:22 -05:00
firestar5683 f506c8f299 Wait for Bolt steering transmit receipts before advancing counters 2026-10-31 08:04:17 -05:00
firestarsdog 6c38692ff7 Preserve SLC decisions to prevent source churn 2026-10-31 06:13:47 -04:00
firestarsdog 5668bafc4c Fix Vision units to prevent inflated metric speed limits 2026-10-31 04:13:19 -04:00
firestarsdog 859145dd8a Use SLC on RES to match SET 2026-10-31 03:41:55 -04:00
Dom 37341d2bca Fix CI type narrowing, fixtures and executable metadata
Preserve SLC validation behavior with explicit None narrowing, use existing Mock test doubles, and restore controlsd executable metadata without content changes. Keep exact torque gain expectations while matching the existing list representation.

Validated 1095 related tests, targeted ty and Ruff; prior baseline comparison and 4236 guard-equivalence cases preserve existing rejection semantics. Refresh the inventory receipt only for these validated changes.
2026-10-31 00:00:28 -05:00
firestarsdog 23a7b93df0 Rebuild Params and cereal for SLC changes 2026-10-31 00:53:56 -04:00
firestarsdog 7bce4121e5 Drop legacy SLC offsets for simpler setup
Use one saved schedule with editable zero defaults. Preserve current schedules and round displayed offsets and edits to whole units without conversion drift.
2026-10-31 00:24:31 -04:00
alan-polk 5c32d95d7b BluePilot lateral library: v0.2.1
Re-vendored at the v0.2.1 release (2b9dc6c), which adds StarPilot as a host.
Since the last copy: the matrix tests only re-key SAFETY_PARAM when the host's
stock base defines it (IQ.Pilot's iqdbc doesn't; StarPilot's does, so no change
here), and a FORKS.md note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:50:58 -05:00
alan-polk a0d80165df Ford AOL: a rejected host actuator frame doesn't hand the bus to the camera
The previous commit let the camera's actuator through again as soon as one host frame was rejected. Mid-steer
that put the camera's inactive frame (its own counter) on the bus in place of the blocked one, and
the F-150's PSCM ramped out and reported lateral control faulty for several seconds, from a single
blocked frame. Without AOL, static blocking makes a rejected frame a missed one, which the PSCM rides
through.

Block the camera's copy while the host is sending, accepted or not; only a host that has stopped
sending for the AOL host timeout gives the actuator back to the camera.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:50:58 -05:00
alan-polk 67f4250d7e Ford AOL: one steering actuator sender while the host's frames are accepted
With AOL, the camera's actuator (0x3D3/0x3D6/0x3CA) is forwarded unless AOL owns steering, but the
host sends its own every frame, active or not. When AOL isn't steering, the PSCM hears both, with
unrelated counters. On a 2021 F-150 this flipped LatCtlSte_D_Stat between Unavailable and Available
every 50-100 ms for seconds at a time, worst when the two counters ran 1-2 apart: steerFaultTemporary
alerts, the panda's LKA-ready check denying lateral, and AOL dropping its latch on re-engagement.
BluePilot's angle-mode release during a manual turn made it easy to hit.

Block the camera's actuator while the host's last frame was accepted and is within the AOL host
timeout, as static blocking does without AOL. A rejected or stale host frame lets the camera's
copy through again. Permission and display ownership are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:49:32 -05:00
alan-polk ed3b1c4c73 Settings: BluePilot lateral for Fords
On the Steering page, for every Ford that can run BluePilot lateral (not the Transit), in Galaxy and the
native settings alike:
- BluePilot Lateral (off by default), Primary Control (curvature/angle) and Use Pinion Yaw Sensor (platforms
  with a pinion geometry row) change while parked; the switch and the pinion sensor apply at the next vehicle
  startup, and Safe Mode keeps BluePilot lateral off.
- the tuning rows of the current mode change any time, since the library reads them every frame; rows that
  depend on Lane Positioning or the custom profile wait for it.
Ranges come from the library's manifest and defaults from params_keys.h, so the rows can't drift from what the
controller reads. A change is checked against the row it was made on, so a request from before its switch
changed is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:49:32 -05:00
alan-polk 49b651af70 openpilot: BluePilot lateral startup bits, per-frame inputs and settings registry
- params_keys.h: every setting in the library's manifest (python -m opendbc.bluepilot_lateral.params.manifest),
  without BACKUP, which StarPilot's params don't have. disable_BP_lat_UI defaults to "1": BluePilot lateral
  is off until the user turns it on.
- car init (VehicleStartupPreferences.prepare): init_ford_safety_param_starpilot ORs the library's startup
  bits into the Ford param. They stay off without the openpilot toggle, in Safe Mode, or when a startup
  setting can't be read.
- card (configure_controller): the Ford CarController gets a BluePilotInputs, which fills the library's
  LateralInputs from modelV2, vehicleParameters and lateralDelay each frame and re-reads the per-frame
  settings every 3 s.
- test_bluepilot_lateral.py: the init path's bits, the real CarController and libsafety with the same
  param, for a CAN and a CAN FD Ford, off/on/on+pinion/pinion-only, curvature and angle mode: every
  steering and LKA frame passes, and only angle mode with BluePilot on steers through the path angle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:49:32 -05:00
alan-polk 40147f9f74 BluePilot lateral library: Mondeo pinion geometry, platforms without a pinion row
Re-vendored from the library (stamped copy, unedited).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:49:32 -05:00
alan-polk bcb0abe576 Ford: hook the BluePilot lateral library into opendbc
- safety/modes/ford.h: includes the library's safety with StarPilot's lateral permission
  (lateral_controls_allowed(), which includes AOL) as its lateral binding. ford_init hands the library
  bits 9-14 of the Ford param (never on the Transit); with the bit set, LateralMotionControl/2 go through
  the library's 4-signal checks instead of the stock ones, Lane_Assist_Data1 carries the angle-mode state,
  and the pinion source replaces yaw and joins the selected RX list (stock, stock-switch or AOL).
- car/ford/carcontroller.py: one BluePilotLateral per Ford; while it's active it sends the steering and
  LKA messages, otherwise the stock path runs unchanged. StarPilot's openpilot side attaches the per-frame
  inputs and settings as bluepilot_inputs.
- safety tests: test.sh runs the library's safety tests by directory and gates the library headers'
  coverage in a run of their own; test_ford_bluepilot_hooks.py covers where the bits ride, the Transit,
  the pinion RX check on every list and the full-list guard. The qualification and mutation runners
  include the library's safety tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:49:32 -05:00
alan-polk 7b36024f6d Ford safety tests: reset the alternative experience after AOL fixtures
The CAN FD display fixtures and the stock-switch class left alternative_experience at 32. Now that the
stock words are AOL words, a later Ford test could run with AOL configured; they reset it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:49:32 -05:00
alan-polk c5ce014573 BluePilot lateral library (stamped copy)
opendbc_repo/opendbc/bluepilot_lateral is BluePilotDev/bluepilot-lateral as released, made by the
library's tools/vendor.sh and checked by its tools/check_vendored.sh (VERSION names the release and
commit). Nothing inside the folder is edited here: changes go to the library, get released, and are
vendored again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:49:32 -05:00
alan-polk 8da0c9d916 Ford: remove the interim BluePilot port; AOL on the stock profiles
StarPilot's angle-only bluepilot_lateral adaptation and the BluePilot-derived curvature strategies (Mach-E,
classic, Explorer, generic CAN FD, new-port curvature, manual-turn latch, curvature preview, pinion
measurement) are removed, with their extended safety profiles 16/32/64 and the interim angle word 128. Ford
lateral is upstream's stock curvature path until the BluePilot lateral library is mounted.

StarPilot's own Ford features stay, re-keyed from the extended words to the stock ones:
- AOL: classic 32 -> 0, Mach-E 18 and the CAN FD trucks 66 -> 2 (+1 with openpilot long); the CAN FD
  retained-display rules apply to word 2. Edge/Mondeo/Transit and the manual bit (256) are unchanged.
- release CAN FD long: word 3 (was 19/67); 19 and 67 are now retired and denied.
- stock cruise switch, CAN FD camera-lead radar handling, hands-free cluster: keyed on the Ford family.
- classic stock ACC (0) keeps the no-ACCDATA TX list the extended classic profile had.

Bits 9-14 of the Ford param are reserved for the BluePilot lateral library's startup bits
(safety/modes/ford_param.h); AOL and pandad mask them out of the profile word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:49:32 -05:00
firestarsdog 2fd0741f56 fix(slc): prevent confirmation prompts when longitudinal control is disengaged 2026-10-30 23:31:59 -04:00
firestar5683 c3d5939ffe Keep Polestar discovery on normal harness wiring 2026-10-30 22:09:43 -05:00
firestar5683 7f8aa3dceb Restore stable effective gain across StarPilot torque controllers 2026-10-30 22:07:21 -05:00
firestarsdog a1fccf70b4 Draw unset SET dash to avoid font fallback 2026-10-30 23:01:32 -04:00
firestarsdog aac227a103 Bind SET engagement to the selected speed limit 2026-10-30 22:53:13 -04:00
firestar5683 0d8c81e294 Renew an owned FLM trial before draining queued evidence 2026-10-30 21:49:47 -05:00
firestarsdog b9744f1cab Cycle shared personality widget on tap 2026-10-30 22:30:42 -04:00
firestarsdog 70b705782c Unify Onroad Long Indicator 2026-10-30 22:28:58 -04:00
firestar5683 efc8527820 Restore measured FLM receiver after queue scheduling regressions 2026-10-30 21:27:10 -05:00
firestar5683 d421abeee1 Yield FLM queues only when actual motion needs service 2026-10-30 21:05:45 -05:00
firestar5683 7da9cea0f6 Yield FLM evidence queues before fresh pose and trial delivery 2026-10-30 20:41:23 -05:00
firestar5683 24b3d17c58 Load live FLM trial validation before the driving loop 2026-10-30 19:51:36 -05:00
firestar5683 64eba05e62 Capture genuine FLM checkpoints before the reconstruction age ceiling 2026-10-30 19:27:50 -05:00
Dom 87736cf206 Use the established sound-pack mock in audio callback test 2026-10-30 19:10:31 -05:00
firestar5683 fb20651274 Retain native-visible FLM experiments through missing-evidence pauses 2026-10-30 19:03:46 -05:00
Dom 84bc481dd0 Align Galaxy test fixtures with current developer controls 2026-10-30 18:58:02 -05:00
firestar5683 e438a1a374 Play one temporary steering warning per AOL fault episode 2026-10-30 18:01:31 -05:00
firestar5683 f7c38a24a8 Show vehicle battery voltage on the Galaxy dashboard 2026-10-30 17:56:44 -05:00
Dom d56266b63b Restore selfdrived executable permission 2026-10-30 17:53:32 -05:00
firestar5683 6984f1da29 Add calibration reset beside driver monitoring in Galaxy 2026-10-30 17:43:17 -05:00
firestar5683 ce6545467b Keep AOL steering active with doors open 2026-10-30 17:36:45 -05:00
firestar5683 1a89d5577a Cover HKG torque steering independence from Alpha Long 2026-10-30 17:26:45 -05:00
firestar5683 39298d97be Remove optional Volt longitudinal tune 2026-10-30 17:21:28 -05:00
firestar5683 d84abc611d Keep door-open AOL pauses free of takeover alerts 2026-10-30 17:19:22 -05:00
firestar5683 60b0f1e826 Reuse exact native primitive validation during FLM input packing 2026-10-30 17:04:36 -05:00
firestar5683 2487154a1e Restore flashed Volt LKAS button mapping 2026-10-30 16:53:57 -05:00
firestar5683 5bedae0794 Refresh FLM producer clock pin for current modeld 2026-10-30 16:44:27 -05:00
Dom 1ab130e4a5 Align updater test fixtures with metadata checks 2026-10-30 16:41:09 -05:00
firestar5683 320e399f98 Keep alternate HKG CAN-FD discovery on normal wiring 2026-10-30 16:29:48 -05:00
firestar5683 1d7e01f9f1 Preserve Mach-E AOL across angle and pinion handovers 2026-10-30 16:26:07 -05:00
firestar5683 37bbd2711d Add toggle defaults reset to Software 2026-10-30 16:12:04 -05:00
Dom d513137550 Repair CI test collection and fixtures 2026-10-30 16:07:17 -05:00
firestar5683 0d32c47b56 Preserve FLM discovery against preselection backlog 2026-10-30 16:05:31 -05:00
firestar5683 6bc374c7d4 Refresh the release inventory after updater source changes 2026-10-30 16:04:39 -05:00
mtawata 867ed373d6 fix fast updater hang; improve version selection list; schedule update install 2026-10-30 13:58:52 -07:00
firestar5683 0fd2353c55 Publish the intended common archive as an ordinary Git blob 2026-10-30 15:37:32 -05:00
firestar5683 21f675eae0 Validate each FLM control graph once during probe preparation 2026-10-30 15:32:49 -05:00
firestar5683 f4dde57839 Keep FLM input reception responsive and numerical work off Controls 2026-10-30 15:09:04 -05:00
firestar5683 b2604f4b8a Ship the executable Params library 2026-10-30 13:23:44 -05:00
firestar5683 ebe7f28aae Update from the live installed Git revision 2026-10-30 13:18:48 -05:00
firestar5683 bf0243f41b Build matching Outback MAIN lateral firmware 2026-10-30 13:15:36 -05:00
firestar5683 da0d0d204b Restore Toyota stop-and-go assistance setting 2026-10-30 13:09:55 -05:00
firestar5683 e9e7481416 Offer both controllers on every torque-tuned vehicle 2026-10-30 13:08:26 -05:00
firestar5683 0fcf127704 Arm Outback lateral on physical cruise MAIN 2026-10-30 13:07:30 -05:00
Dom f7b85976a5 Align lane-centering test fixtures 2026-10-30 13:00:19 -05:00
James Vecellio-Grant 2b3e186822 controls: Blendful lateral policy (alpha) (#2073) 2026-10-30 12:40:57 -05:00
firestar5683 8daabf71f9 Keep live plots refresh cadence and layout steady 2026-10-30 12:02:00 -05:00
firestar5683 1fcfefc977 Show classic HKG AOL icons before cruise engagement 2026-10-30 11:59:48 -05:00
firestar5683 01529d935d Keep FLM numerical preparation within its input budget 2026-10-30 11:27:24 -05:00
firestar5683 ba0e051293 Fix language font loading on device 2026-10-30 11:10:30 -05:00
firestar5683 bfca5476e6 Preserve manager executable permission 2026-10-30 11:07:21 -05:00
firestar5683 bfa9427dbd Fix typed lane centering default enrollment 2026-10-30 11:06:09 -05:00
Dom 02588a19a2 Use typed localization test resources and fixtures 2026-10-30 10:52:25 -05:00
firestar5683 f4ec1c187e Share corroborated CEM lead departure across vehicle configurations 2026-10-30 10:48:59 -05:00
Dom bc494bd7d5 Restore manager executable mode 2026-10-30 10:43:58 -05:00
firestar5683 0b94e10fd9 Preserve Toyota stop engagement while the brake is held 2026-10-30 10:39:14 -05:00
firestar5683 f09bf1fc28 Reuse validated immutable FLM input metadata 2026-10-30 10:34:03 -05:00
firestar5683 de00bb2f11 Preserve missing lane centering preferences on upgrades 2026-10-30 10:28:21 -05:00
firestar5683 7acb954a6d Translate Galaxy and native UI through the shared language setting 2026-10-30 10:27:37 -05:00
firestar5683 5e057b9848 Share display translations across Galaxy and native UI 2026-10-30 10:27:37 -05:00
firestar5683 4fcf11f07a Enable lane centering by default on new installs 2026-10-30 10:22:47 -05:00
Dom 6120b36217 Register existing blindspot setting descriptions 2026-10-30 09:59:21 -05:00
Dom 5c662d9e43 Keep sound selection tests stable with blindspot settings 2026-10-30 09:53:20 -05:00
Dom b64d7cf9e9 Share banked Hyundai CAN-FD RX workspace to recover Panda RAM 2026-10-30 09:07:01 -05:00
firestar5683 18432adb3d Build blindspot alert 2026-10-30 08:41:03 -05:00
firestar5683 fae3f5ccfc Blindspot alert 2026-10-30 08:37:21 -05:00
firestar5683 8fdee1d44e Remove redundant wheel control default buttons 2026-10-30 08:19:47 -05:00
firestar5683 b3b96adecc Preserve Palisade AOL through camera echo races and braking 2026-10-30 07:48:14 -05:00
firestar5683 df9898673e Stabilize GM startup test collection across CI shards 2026-10-30 03:34:54 -05:00
firestar5683 66beca6de7 Avoid overlapping subscriptions for the flashed Volt parser 2026-10-30 02:40:34 -05:00
Dom 717a82245f Account for Galaxy-only accelerator preference in page parity test 2026-10-30 02:23:15 -05:00
firestar5683 d955ebd7a2 Keep schema and caller checks aligned with reviewed source contracts 2026-10-30 02:22:25 -05:00
firestar5683 2cca20624c Finalize the admitted Sportage long AOL startup profile 2026-10-30 02:12:43 -05:00
firestar5683 7db42dd5d6 Distinguish native-rejected Ford UI from transmitted display frames 2026-10-30 01:49:30 -05:00
firestarsdog eff809d839 Add DisengageOnAccelerator to Galaxy 2026-10-30 02:41:22 -04:00
Dom b3a4b964c2 Use typed Quick Select test fixtures
Retain real composition and input-routing methods with typed drawing boundaries.
Preserve existing assertions and validate optional settings requests explicitly.
2026-10-30 01:40:41 -05:00
firestarsdog 0114eb04bb Add compact ui DisengageOnAccelerator 2026-10-30 02:17:36 -04:00
firestarsdog 890c30c3cb Rebuild Params and cereal for SLC lookahead 2026-10-30 02:13:07 -04:00
firestar5683 7e00ee80c4 Align regression and settings coverage with current AOL contracts 2026-10-30 01:05:49 -05:00
firestarsdog e00fb66bc8 Firefox Arabesque Updater 2026-10-30 01:49:51 -04:00
firestarsdog 0d4422552b Large UI: replace radial Quick Select with a stable overlay 2026-10-30 01:43:43 -04:00
firestar5683 c7f3de2416 Clarify speed-limit type validation and settings test contracts 2026-10-30 00:28:07 -05:00
firestar5683 4ea9adbdb9 Order Ford standby display after inactive steering proof 2026-10-30 00:21:06 -05:00
firestar5683 9e7e4dde52 Keep factory wheel roles explicit and restore CI source hygiene 2026-10-30 00:16:33 -05:00
firestarsdog a8bc9d1f24 Merge remote-tracking branch 'origin/Dom' into Dom 2026-10-30 01:10:22 -04:00
firestarsdog 63395a56f1 SLC: add timed adoption of upcoming limits 2026-10-30 01:03:30 -04:00
firestar5683 0a786d37e3 Show factory AOL roles and fix wheel controls safety qualification 2026-10-30 00:01:17 -05:00
firestarsdog 6fe76b728b Mapd: add lookahead for early speed-limit adoption 2026-10-30 00:41:31 -04:00
Dom c65296f6df Initialize native settings localization test fixture
Share isolated real Params with the localization owner and initialize the RHD row.
Preserve existing saved-state and parked-state assertions.
2026-10-29 23:05:03 -05:00
firestar5683 008687eb87 Build Sportage AOL and shared wheel controls fixes 2026-10-29 23:03:49 -05:00
firestar5683 dc7ff90070 Restore Sportage AOL and show effective wheel controls 2026-10-29 22:46:29 -05:00
firestar5683 c6ef6b138a Avoid duplicate native packet validation in FLM 2026-10-29 22:39:28 -05:00
Dom 56c997a284 Restore controlsd executable mode
Preserve the source blob while restoring the script's executable bit.
Refresh the source inventory receipt for the mode-only correction.
2026-10-29 22:22:17 -05:00
firestar5683 f1f8f5696e Use Metric 2026-10-29 22:14:25 -05:00
firestar5683 16f31abd75 RHD Fix 2026-10-29 22:13:23 -05:00
firestar5683 77babc4c85 Release an existing Force Stop hold with RES 2026-10-29 22:10:01 -05:00
firestar5683 d1c2bc14a4 Localize steering command publication stalls 2026-10-29 21:57:08 -05:00
firestar5683 86315e76cb Restore flashed Volt OBD alpha longitudinal profile 2026-10-29 21:55:21 -05:00
firestar5683 52347a3593 Preserve AOL intent through temporary steering overrides 2026-10-29 21:40:46 -05:00
firestar5683 0933e58afc Keep AOL border lit while its latch is armed 2026-10-29 21:30:07 -05:00
firestar5683 cc125cf1eb Preserve Toyota cruise during accelerator override 2026-10-29 21:15:56 -05:00
firestar5683 c073497d44 Recognize short deliberate bookmark swipes 2026-10-29 21:14:56 -05:00
Dom b817c3c26e Fix Subaru init parameter MISRA compliance
Normalize a same-type local copy instead of modifying the input parameter.
Existing safety logic and all 34 paired firmware artifacts remain byte-identical.
Validated 67 debug and 67 release native cases; refresh source inventory receipt.
2026-10-29 20:40:23 -05:00
Dom 075d5c0ffe Cover Impreza MAIN words in the exhaustive AOL protocol contract 2026-10-29 19:41:11 -05:00
Dom 1f1b1d5db9 Match persisted language and bounded proof wait test fixtures 2026-10-29 19:24:19 -05:00
firestar5683 4dd24beabc Separate Volt CC steering minimum from cruise engagement 2026-10-29 19:19:20 -05:00
firestar5683 ad7bfa4899 Build matching 2020 Impreza MAIN lateral artifacts 2026-10-29 19:11:18 -05:00
firestar5683 5910ddb754 Arm 2020 Impreza lateral on physical cruise MAIN 2026-10-29 19:09:50 -05:00
firestar5683 21a3921b16 Keep GM steering active through axis proof delivery races 2026-10-29 19:07:31 -05:00
firestar5683 33e87cea5a Recognize standard HDA2 wiring before diagnostic queries 2026-10-29 18:59:39 -05:00
firestar5683 e3fbbdddb9 Add language selection to Galaxy localization 2026-10-29 18:42:16 -05:00
Dom 76d33aaa81 Keep native test fixtures aligned with current lifecycle contracts 2026-10-29 18:26:08 -05:00
Dom ff62737ac4 Align GM interface fixtures with registered identities and Gen2 Drive 2026-10-29 17:43:51 -05:00
Dom 87b8b2ada5 Cover Polestar receiver counter wraps and fail-closed faults 2026-10-29 17:19:31 -05:00
Dom b43db836a5 Label the non-pedal Bolt camera profile as ACC 2026-10-29 17:17:33 -05:00
firestar5683 1b9bb0cd51 Keep FLM snapshot preparation below live capture priority 2026-10-29 17:03:56 -05:00
Dom c3de3defc3 Keep parked power tests in their own collected module 2026-10-29 17:03:45 -05:00
Dom 268370640d Type Jetlink test parameters for model selections 2026-10-29 17:00:01 -05:00
firestar5683 0d795930ff Keep the FLM live receiver at normal CPU priority 2026-10-29 16:58:34 -05:00
firestar5683 ec5739441e Expose parked shutdown settings in Galaxy 2026-10-29 16:58:07 -05:00
Dom e94198f98e Clarify Subaru MAIN observer for MISRA without changing output 2026-10-29 16:41:26 -05:00
Zeph Leggett 5f8638e894 Jetlink: Active Big chooses Jetlink's model by its ONNX 2026-10-29 16:38:54 -05:00
firestar5683 98d2865a29 Publish the Controls scheduling repair for FLM 2026-10-29 16:28:51 -05:00
firestar5683 0e55c02b28 Prioritize Controls CPU time during FLM learning 2026-10-29 16:28:50 -05:00
Dom 3d7df63099 Normalize continuation indentation within source size limits 2026-10-29 16:26:33 -05:00
firestar5683 47e4855580 Build Polestar counter correction firmware 2026-10-29 16:18:37 -05:00
firestar5683 b33e4a071b Correct Polestar LCA counters at modulo-15 wraps 2026-10-29 16:17:07 -05:00
firestar5683 bc3dce2c0c Keep manager shutdown nonblocking and preserve paused AOL intent 2026-10-29 16:12:56 -05:00
Dom ed5e9e56f2 Assert registered Trax layout without changing stock control contracts 2026-10-29 16:07:54 -05:00
dirwin31 30625319b3 Improve Android Auto reconnection and report screen resolution to Bugsink 2026-10-29 16:06:11 -05:00
Zeph Leggett 51e866a038 Jetlink: Active Big chooses Jetlink's model by its ONNX
With Jetlink running the big model (on, supported, no Chestnut), Active Big
in Galaxy and on the device is Jetlink's choice: the model manager marks a
big model selectable when its catalog entry names the ONNX it was built from
(source_sha256), downloaded or not, since Jetlink's host fetches the ONNX
itself. The choice is the new JetlinkModel param, {sha256, displayName}, the
adapter's big-model key; Chestnut's own Active Big is left as it was. None
leaves the model to the Jetlink app, and the manager words it.

A newer choice made in the Jetlink app is written back into JetlinkModel by
jetlink, so Active Big shows the model that drives; one StarPilot does not
list is named as the app's.

JetlinkModel is a new key in params_keys.h: the native params libraries need
their usual rebuild.
2026-10-29 16:03:03 -05:00
Zeph Leggett 21c34c8f4d Jetlink v0.8.12 2026-10-29 16:03:03 -05:00
firestar5683 2800e5b452 Keep unarmed startup steering restrictions from displaying takeover warnings 2026-10-29 15:49:53 -05:00
firestar5683 85997f719f Quiet incomplete drive history and recover full logs 2026-10-29 15:42:32 -05:00
firestar5683 138da851ee Guard two-line centering against conflicting corrections 2026-10-29 15:37:16 -05:00
dirwin31 556f90458e Merge branch 'Dom' into AA-AutoConnectFix-Dom 2026-10-29 13:11:06 -07:00
firestar5683 e2fdc2a4dc Remember unchanged model verification across restarts 2026-10-29 14:59:31 -05:00
firestar5683 312eb9e94d Recover model verification after a worker failure 2026-10-29 14:51:14 -05:00
firestar5683 37fc53c48a Explain community Polaris models in Galaxy 2026-10-29 14:48:32 -05:00
firestar5683 efbe7a776f Materialize matching Panda host object files 2026-10-29 14:48:03 -05:00
firestar5683 7c5e70fd13 Build matching Ford standby-handover firmware 2026-10-29 14:37:09 -05:00
firestar5683 06a74465c0 Keep Ford standby display through the final steering packet 2026-10-29 14:34:35 -05:00
firestar5683 42057a3f8f Limit low-speed steering advisories to engaged or armed steering 2026-10-29 14:33:29 -05:00
firestar5683 d85372232f Animate Polaris highlights and preserve emoji colors 2026-10-29 14:30:06 -05:00
firestar5683 a952a8f8a8 Keep Mach-E native LONG withdrawal separate from manual steering 2026-10-29 14:27:37 -05:00
firestar5683 0826f3b22d Highlight custom Polaris models 2026-10-29 14:25:17 -05:00
firestar5683 58fb8e820b Identify the 2025 Trax from its CAN layout and VIN 2026-10-29 14:21:21 -05:00
firestar5683 38e14008b9 Build Impreza physical MAIN lateral firmware 2026-10-29 13:41:17 -05:00
firestar5683 4c015f6f49 Arm Impreza lateral on physical MAIN and show stock ACC engagement 2026-10-29 13:39:31 -05:00
firestar5683 256628e50b Keep Ford AOL through Cancel during accelerator override 2026-10-29 13:07:02 -05:00
firestar5683 fa25ea0567 Publish native permission for the bypassed-camera Volt configuration 2026-10-29 12:51:00 -05:00
Dom 99f52a7048 Align Bolt fixtures with Drive feeds and explicit ACC faults 2026-10-29 12:44:53 -05:00
firestar5683 eb2c90b80e Refresh queued GM permission during combined control 2026-10-29 12:37:07 -05:00
firestar5683 53b82a1726 Put Cruise Intervals In Wheel Controls 2026-10-29 12:31:54 -05:00
firestar5683 3b04ff3cc4 Keep downloaded models selectable offline 2026-10-29 12:27:56 -05:00
firestar5683 6e11c50dfc Build jacobw Subaru angle adapter firmware 2026-10-29 12:08:10 -05:00
firestar5683 34ae63cbf6 Adapt jacobw Subaru angle control to StarPilot 2026-10-29 12:06:38 -05:00
firestar5683 1f79ca865f Recognize Outback Wilderness camera firmware and credit jacobw Subaru support 2026-10-29 11:54:05 -05:00
Dom 87cb8f9a63 Assert exact four-byte Toyota button capability in variant parity 2026-10-29 11:52:04 -05:00
firestar5683 21b9fd8136 Build Gen2 Bolt pedal firmware 2026-10-29 11:49:58 -05:00
firestar5683 d317185c00 Allow Drive for Gen2 Bolt pedal 2026-10-29 11:48:24 -05:00
firestar5683 f47452c4b1 Restore radar-filter Toyota distance buttons and wheel mapping 2026-10-29 11:23:58 -05:00
firestar5683 6e124d1e45 Keep inactive ACC traffic flowing for every qualified GM owner 2026-10-29 11:17:06 -05:00
firestar5683 50b4da94ba Maintain inactive ACC streams during ordinary SDGM startup 2026-10-29 11:00:17 -05:00
firestar5683 66bf42ac1e Keep Bolt pedal control available after secondary brake loss 2026-10-29 10:55:11 -05:00
firestar5683 8aa35ed00f Report verified models in usage statistics 2026-10-29 10:51:59 -05:00
firestar5683 6639a95904 Keep lane centering responsive with fresh inputs and continuous tracking 2026-10-29 10:22:45 -05:00
dirwin31 2b069cffc2 Build 2026-10-29 06:43:30 -07:00
dirwin31 ac97f5f447 Add supported Message 2026-10-29 06:31:46 -07:00
dirwin31 e7b53f3099 Bugsink Screen Resolutions 2026-10-29 06:21:31 -07:00
dirwin31 6418bfd5b7 Let the car connect first and clear stuck Android Auto Bluetooth links
At ignition the head unit pages the comma for hands-free at the same moment
the supervisor paged the car. The crossed connections can leave the
controller holding a link the 4.9 kernel no longer knows ("ACL packet for
unknown connection handle 7"), after which every SDP/RFCOMM connect to the
car fails with EALREADY and the session retries the same sequence until the
car gives up.

- Ignition-triggered sessions wait up to 8 s for the car's own hands-free
  link before paging it.
- EALREADY on SDP or the cached RFCOMM channel fails the attempt at once,
  keeps the cached channel, drops the stray handle with a raw HCI Disconnect
  (only when BlueZ reports the car disconnected and the kernel does not list
  that handle), and lets the car connect first on the retry.
2026-10-29 06:21:10 -07:00
Dom 6d87ba090c Assert retired SLC fallback stays inactive after source loss 2026-10-29 03:12:15 -05:00
Dom 3fb884b5ba Follow renamed conditional planner integration fixture 2026-10-29 02:27:25 -05:00
firestarsdog aa99e3b6ca Build for SLC Refinement 2026-10-29 03:04:02 -04:00
firestarsdog ad397f89cc Retire SLC fallback to unify behavior 2026-10-29 02:57:30 -04:00
Dom 4e3775fa06 Refresh source inventory after speed-limit UI cleanup 2026-10-29 01:36:13 -05:00
firestarsdog 01416612a1 Remove "Use X Speed" 2026-10-29 02:16:51 -04:00
firestar5683 8fcd77c953 Recover FLM learning windows and deliver current trial evidence 2026-10-29 00:44:04 -05:00
firestar5683 9e002886f9 Build matching Ford cluster-handover firmware 2026-10-29 00:15:33 -05:00
firestar5683 1f9825e594 Keep Ford cluster standby through driver handover 2026-10-29 00:11:34 -05:00
firestar5683 4a82e92c73 Avoid repeated single-line lane-centering acquisition stalls 2026-10-29 00:09:26 -05:00
firestarsdog 507e464c59 Arabesque Firefox Character Fix 2026-10-29 00:32:05 -04:00
firestar5683 b6b548fc9a Admit fresh Toyota lateral input with cruise already off 2026-10-28 23:17:50 -05:00
firestar5683 434c89ce71 Normalize positive model-origin roundoff for stopped release 2026-10-28 22:57:27 -05:00
firestar5683 50781a9736 Fit StarPort torque widgets to square head-unit layouts 2026-10-28 22:37:17 -05:00
firestar5683 31b4abe4c6 Keep camera ACC messages flowing throughout GM startup 2026-10-28 22:28:52 -05:00
firestar5683 cdc97b58df Recognize HKG HDA1 wiring before diagnostic queries 2026-10-28 22:10:42 -05:00
Dom 4a3a989b3c Publish exact Panda bootstub bytes as ordinary Git blobs 2026-10-28 21:48:37 -05:00
firestar5683 b75d9f8baf Build repaired Panda CAN power-save firmware 2026-10-28 21:37:45 -05:00
firestar5683 b12ed577c0 Preserve stock Hyundai AOL across fresh input transitions 2026-10-28 21:34:28 -05:00
firestar5683 2567a32f70 Preserve Panda power-save state across CAN reinitialization 2026-10-28 21:29:24 -05:00
firestar5683 639866e6f7 Keep Hyundai brake withdrawal guard within source budget 2026-10-28 21:08:08 -05:00
firestar5683 ccdddea522 Preserve classic Hyundai AOL through brake withdrawal 2026-10-28 21:02:30 -05:00
Dom b5f1b8a8e3 Complete ready-state fixture for CAN clock contract 2026-10-28 20:45:58 -05:00
firestar5683 14835e1b42 Ignore logging-only Honda EPS diagnostic responses 2026-10-28 20:21:19 -05:00
firestar5683 f1bc3a8c5b Restore legacy USB Panda bootstub identification 2026-10-28 20:20:40 -05:00
Dom cbeaffba9a Refresh audited GPU capability fixture fingerprints 2026-10-28 20:11:42 -05:00
firestar5683 6537a58bd6 Avoid repeated Toyota parent admission projections 2026-10-28 19:52:55 -05:00
firestar5683 5a12f032ed Reuse exact GM steering profile qualification 2026-10-28 19:48:36 -05:00
firestar5683 094c9ae84e Reuse MEB static eligibility within each longitudinal update 2026-10-28 19:47:36 -05:00
firestar5683 83d4bf30a8 Reuse constant Chrysler AOL qualification masks 2026-10-28 19:46:32 -05:00
firestar5683 fdaaae86b0 Share exact static configuration cache across vehicle makes 2026-10-28 19:44:05 -05:00
firestar5683 8995ddb0df Build Toyota configuration overhead fix 2026-10-28 19:27:04 -05:00
firestar5683 390438432e Reduce Toyota pedal runtime configuration overhead 2026-10-28 19:25:58 -05:00
Dom 26ab9745af Align startup fixtures with GM neutral continuity contract 2026-10-28 19:03:46 -05:00
firestar5683 d8ccb7fd68 Honor StarPort head unit display insets 2026-10-28 18:59:25 -05:00
Dom 03feefed84 Preserve Toyota behavior with single-exit safety helpers 2026-10-28 18:20:31 -05:00
firestar5683 1fa29f6c33 Reverse Proxy 2026-10-28 18:07:26 -05:00
firestar5683 34dfc2e1b5 Build Toyota and Lexus pedal camera-copy parity 2026-10-28 18:04:12 -05:00
firestar5683 5309a0d6bc Preserve Toyota and Lexus primary pedal ownership with camera copies 2026-10-28 18:03:06 -05:00
firestar5683 3c770dc283 Build legacy Prius DSU and pedal receiver port 2026-10-28 17:52:38 -05:00
firestar5683 265ca6c17d Merge current Dom checks for legacy Prius receiver 2026-10-28 17:51:29 -05:00
firestar5683 04b5dd0ad5 Port legacy Prius retrofit DSU and pedal receiver from StarPilot 2026-10-28 17:51:23 -05:00
firestar5683 cdef286dbf Cover HKG stop permissions across native policy families 2026-10-28 17:37:03 -05:00
firestar5683 41692084c9 Build Prius retrofit native DSU mirror fix 2026-10-28 17:31:22 -05:00
firestar5683 dbba95d7fb Keep Prius retrofit stock control when native DSU traffic is mirrored 2026-10-28 17:30:07 -05:00
Dom fee87a4576 Keep transport pause comments within source size budget 2026-10-28 17:22:40 -05:00
firestar5683 df8e234ff6 Preserve CCNC AOL intent through full-engagement stop pauses 2026-10-28 17:05:42 -05:00
firestar5683 ba2275363f Maintain neutral steering throughout GM startup 2026-10-28 16:42:50 -05:00
Dom a4e552c7a3 Classify the compressed Ford regression fixture as binary 2026-10-28 15:36:55 -05:00
firestar5683 65ac3bccf6 Restore Dom predictive stopping across HKG 2026-10-28 15:35:36 -05:00
firestar5683 3e7a3dded8 Release corroborated Mach-E lead departure from CEM standstill hold 2026-10-28 15:25:31 -05:00
Dom 62dd77d6bb Keep transport intent documentation within the text size limit 2026-10-28 15:02:57 -05:00
firestar5683 7a7b6d2208 Identify blocking work inside Controls overruns 2026-10-28 14:58:41 -05:00
firestar5683 b8aa3b9eca Arm Ford Alpha AOL on the first physical MAIN edge 2026-10-28 14:41:17 -05:00
firestar5683 b0e93e304f Retain Ford AOL across first gas-held native grant 2026-10-28 14:29:26 -05:00
firestar5683 3baf4f7c82 Skip unrelated GM steering checks on other vehicles 2026-10-28 14:23:29 -05:00
firestar5683 621c8d31e3 Restore Stinger forecast stop intent with the smooth planner 2026-10-28 14:20:00 -05:00
firestar5683 e095ab6b71 Retain real FLM source packets for bounded frequency accounting 2026-10-28 14:01:27 -05:00
firestar5683 e0fc923875 Keep audio callbacks off protected driving cores 2026-10-28 13:56:22 -05:00
firestar5683 4848960966 Finish GM discovery after an early Ford signature rejection 2026-10-28 13:54:55 -05:00
firestar5683 2e70c76990 Patch native audio fixture attributes without type conflicts 2026-10-28 13:49:31 -05:00
firestar5683 b6bfd182dd Run shared GM discovery regressions in CI 2026-10-28 13:45:13 -05:00
firestar5683 c0ed5f309a Prioritize native audio playback below driving control loops 2026-10-28 13:42:32 -05:00
firestar5683 f2294f575e Keep shared GM discovery on normal CAN wiring 2026-10-28 13:19:53 -05:00
firestar5683 0fcc393380 Pace optional FLM receiver on motion without relaxing evidence guards 2026-10-28 12:33:01 -05:00
Dom 2af7ef756b Refresh audited graphics fixture identities after StarPort rename 2026-10-28 12:01:29 -05:00
firestar5683 db651809df Cover device Params startup state shared with native readers 2026-10-28 11:38:17 -05:00
Dom 39392c37a6 Preserve typed radar projection test overrides 2026-10-28 11:21:11 -05:00
firestar5683 d594c6772a Keep device settings in persistent Params storage 2026-10-28 11:18:25 -05:00
firestar5683 1fcd8b3353 Reduce Controls and Telemetry Parsing Work 2026-10-28 11:14:43 -05:00
Dom 7d9cb9b37b Align discovery and independent button fixtures with current contracts 2026-10-28 11:04:52 -05:00
Dom c55ef78274 Keep settings test documentation within the text size limit 2026-10-28 10:32:21 -05:00
firestar5683 e9b29f139c Reduce Onroad Torque Drawing Overhead 2026-10-28 10:28:03 -05:00
firestar5683 7d3ba7adf9 Avoid Unused Radar Signal Decoding 2026-10-28 10:19:32 -05:00
firestar5683 d2199ddc20 Speed Up Native CAN Signal Extraction 2026-10-28 10:10:39 -05:00
firestar5683 afd33370f6 Measure smaller FLM source experiments within the existing slew limit 2026-10-28 09:59:49 -05:00
firestar5683 b0417bbf61 Keep classic HKG LKAS and MAIN mappings independent 2026-10-28 09:47:49 -05:00
Dom 6617a1675d Refresh settings fixtures and audited StarPort capability identity 2026-10-28 09:38:58 -05:00
firestar5683 9f4fb7dbb4 Preserve FLM evidence during overload and remove unused wakeups 2026-10-28 09:11:23 -05:00
ervteng 316ed6b8b2 kia/stinger: decode 2022+ radar tracks 2026-10-28 09:06:57 -05:00
firestar5683 dbd72abae6 Add Experimental Speed Nudge and Group Driving Personalities 2026-10-28 08:57:26 -05:00
firestar5683 9ade200473 Build Camry filter radar admission fix 2026-10-28 08:56:35 -05:00
firestar5683 c1a00e3ef5 Merge current Dom checks for Camry filter fix 2026-10-28 08:55:20 -05:00
firestar5683 c03f84cf61 Recognize Camry filter with independent cluster radar traffic 2026-10-28 08:54:56 -05:00
Dom 5ce3f02b99 Align Bolt lifecycle fixtures with raw camera payload contract 2026-10-28 08:54:18 -05:00
firestar5683 63243d7e2d Preserve Encouragement Through Acceleration Noise 2026-10-28 08:39:55 -05:00
Dom 7b6d9cb64b Fix StarPort and Firefox static checks 2026-10-28 08:27:36 -05:00
firestar5683 d61cfcb04f Enable stopped-lead CEM by default 2026-10-28 08:23:23 -05:00
firestar5683 c3c7ee985a Identify Bolt variants from VIN and live cruise capability 2026-10-28 08:21:29 -05:00
Dom 51af702b2c Run CI on the Dom development branch 2026-10-28 08:19:35 -05:00
firestar5683 3621ccf9c2 Add Localization Settings and Repair Global Unit Switching 2026-10-28 08:10:00 -05:00
firestarsdog af48674fae Arabesque Firefox v1 2026-10-28 05:08:17 -04:00
Prabhaav Pillai 08fa2bfde2 Build 2026-10-28 02:56:01 -04:00
Prabhaav Pillai 5b2e2dae0f Refactor Android Auto references to StarPort across the codebase 2026-10-28 01:46:55 -04:00
firestarsdog cc184af2b5 Explain braking preset differences 2026-10-28 01:40:54 -04:00
firestarsdog 13a76dfb8e Keep SLC state and confirmations current 2026-10-28 01:09:47 -04:00
ervteng 9736d887b3 kia/stinger: decode 2022+ radar tracks
The 2022+ Kia Stinger has no radar DBC: the radar's object list is sent on
bus 1 as 10 slots x 3 frames (0x238..0x255, 8 bytes, 50 Hz). Decode it by
hand and publish the objects as radar tracks, gated to KIA_STINGER_2022.

- Stinger2022ObjectDecoder pairs the three frames of a slot (same cycle
  counter and object id in all three, completed within 20 ms, the last
  frame consumed so it cannot be reused) and decode_stinger_2022_triplet
  extracts distance, lateral offset, absolute speed and type.
- Ego speed is the WHL_SPD11 4-wheel average on bus 0 times
  wheelSpeedFactor; relative speed is the radar's absolute speed minus it.
- yRel = -lateral_raw (left positive in the car frame). Tracks not seen in
  a cycle are dropped. canError is reported while the parser is invalid or
  no wheel speed has been seen, and objects decoded in such a cycle are
  discarded. The cycle is emitted on the last address, 0x255.
- interface.py: radarUnavailable is False only when all 30 addresses are
  present in the bus 1 fingerprint with length 8.

Two measured corrections to the raw speed:
- A lead braking to a stop is reported at about -0.3 to -0.6 m/s for 1-3 s
  before settling at 0, while the stock SCC11 lead stays near -0.1 (median
  -0.13 over 13 stock-ACC stops, versus -0.45 from the object frames).
  radard's KF turns the recovery into a positive aLeadK for a parked car,
  which the MPC extrapolates as the lead pulling away. Negative speeds
  above -0.7 m/s are reported as 0 for objects within 2 m of centre;
  faster reversals, adjacent-lane and oncoming traffic stay as measured.
- Absolute speed minus the wheel-speed average reads ~0.14 m/s more closing
  than the stock SCC11 lead (ACC_ObjRelSpd) whenever the car is moving, and
  matches it at standstill: the radar's own ego speed runs below the wheel
  speeds. The 0.14 m/s is subtracted from the ego speed, ramped in over the
  first 1 m/s so standstill is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 21:41:04 -07:00
Dom efe875ffd6 Retire completed GM cold-MAIN recovery before a new lateral request
Clear prior recovery wait state only after the existing healthy unlatched baseline is accepted. Preserve native acknowledgment, fault, identity and expiry gates; cover actual Card and native permission transitions.
2026-10-27 22:49:52 -05:00
Dom e53ec2605f Refresh measured custom pytest workload estimates
Preserve every node, grouping rule, shard count and timeout. Calibrate material module estimates from complete hosted outcomes with conservative scheduling margin.
2026-10-27 22:48:33 -05:00
Dom 65660a4d5c Restore selfdrived executable mode
Keep source bytes unchanged and refresh the canonical prebuilt inventory receipt.
2026-10-27 21:56:44 -05:00
Dom c64e7a8852 Bound Mach-E path assistance by the native speed sample window
Keep exact Mach-E19 independent-lateral profile within the existing six-sample native path limit. Preserve ordinary curvature limits, other modes, fault checks and firmware.
2026-10-27 21:37:48 -05:00
Dom f11f2357d8 Preserve bounded Mach-E lateral proof across held-brake source gaps
Reuse the existing original proof deadline for qualified Mach-E safety19 only. Require fresh independent authority for successors and retain fault, cancel, identity and expiry checks.
2026-10-27 21:28:40 -05:00
Dom b57687cda8 Keep ordinary Mach-E pedal withdrawal with its existing mismatch owner
Withdraw native-denied LONG immediately while retaining only an already proven LAT grant. Preserve fault checks, original proof deadlines and ordinary mismatch counter.
2026-10-27 21:24:22 -05:00
Dom 65d02d27a1 Restore qualified stock-SCC Elantra IRQ diagnostic parity
Classify only the recorded firmware-identified Elantra2021 CAN1 IRQ
bit as diagnostic. Preserve raw reports and native permission, request,
receive health, relay, heartbeat and freshness checks. Alpha-long and
all other configurations remain excluded.

Retain expanded caller/mixed-fault/configuration regression coverage.
Validated 257 focused Elantra and GM/Bolt tests; Ruff clean.
2026-10-27 20:56:10 -05:00
Dom 2dfa4b6fe1 Restore qualified Bolt IRQ diagnostic compatibility
Revert the Bolt-specific admission change from 4b754298 while retaining
native authority, receive-state, relay, heartbeat and actuator checks.
Keep raw diagnostic reports and all other profile policies unchanged.

Add actual Card coverage and restore exact diagnostic policy regressions.
2026-10-27 20:36:13 -05:00
firestarsdog d436c06f99 slc 2026-10-27 21:19:38 -04:00
Dom 05d14fc86e Complete Ford test identities and pinion setting description 2026-10-27 18:50:26 -05:00
Dom e4ad4faf9f Rebuild Panda firmware for qualified Ford CI source 5f0bda11
Refresh source-stamped debug artifacts and canonical prebuilt inventory.
2026-10-27 18:47:34 -05:00
Dom 5f0bda110a Fix Ford qualification coverage, MISRA declarations and test fixtures
Preserve geometry, steering limits, defaults and fault checks.
2026-10-27 18:30:15 -05:00
Dom a1d8204b25 CI: align FLM nested wire typing and paired drain fixtures 2026-10-27 18:06:31 -05:00
Dom 3e0a3e3d7a Ford: ship source-stamped pinion measurement firmware
Rebuild all distributed Panda variants and bootstubs from 4cbffe53 with verified Arm GNU12.3.Rel1; preserve development signing convention. Native debug and release builds passed. Refresh canonical protected source/binary inventory receipt. Default-off Ford option is init-only; no vehicle or road execution claimed.
2026-10-27 17:29:42 -05:00
Dom 4cbffe53b3 Ford: add opt-in pinion curvature measurement
Adapt BluePilot PR #145 by jrneulight (fe5b019568c8c13fb894dc6d8d779d60985051a8) to the existing exact native profile protocol. Default off; qualified parked setting applies at next startup. Preserve existing larger Mach-E envelope, all yaw/heartbeat/relay health, command limits and shadow freshness checks.

Cloud qualification: 171 host tests and 170 subtests; 80 new native/geometry tests; 153 existing native tests and 9131 subtests; release native tests, pandad protocol inventory and full ARM debug/release builds passed. Rebuilt source-stamped firmware follows before branch publication.
2026-10-27 17:14:17 -05:00
Dom 4b94f415ad FLM: construct evidence fields without recursive dict conversion
Build nested typed evidence rows directly while retaining every original field and validation. Complete serialized bytes match the prior writer on recorded d8 evidence and regression fixtures.

Fresh-head combined qualification: 459 tests passed. Cloud performance measurements do not claim device loop-rate recovery or a successful learning trial.
2026-10-27 17:01:45 -05:00
Dom 487dfc4bf4 FLM: preserve fresh evidence across paired queue drains
Merge bounded control/output FIFO heads using original publisher headers. Preserve original age, native, source, overflow and malformed-input checks; retain producer order after joins.

Validated with actual pinned cloud dependencies and fresh-head regression tests.
2026-10-27 17:01:24 -05:00
Dom baddf7a59d Align GM fast-path test oracle with restored Bolt fault veto 2026-10-27 16:42:56 -05:00
Dom a2160580a4 Signal test worker readiness after installing termination handler 2026-10-27 16:08:27 -05:00
Dom 4b754298e7 Reject Bolt CAN interrupt faults in AOL admission 2026-10-27 16:05:51 -05:00
Dom cc5b8b672c Hyundai: keep qualified Elantra AOL steering icons visible
Restore the existing Dom lateral-active dashboard icon behavior only for
qualified Elantra 2021 classic-long AOL profiles. Preserve stock-profile
handling, disengagement blinking, warning fields and all actuator/SCC/FCA
packets. Add native-permission and packet-equivalence regression coverage.

Refresh the canonical protected-source receipt.
2026-10-27 15:36:54 -05:00
Dom 9e5bc45ff7 Fix Ford angle shadow publication and bounded restart 2026-10-27 15:35:46 -05:00
Dom 44dfd7493f Handle absent AOL latch in Subaru test diagnostics 2026-10-27 15:27:42 -05:00
Dom 0422970f2d Compress Ford radar fixture without changing decoded samples 2026-10-27 15:05:25 -05:00
prabhaavp 3b67457f9a Revise privacy policy for Galaxy: StarPilot apps
Updated privacy policy to clarify data handling for Galaxy: StarPilot Companion and Galaxy: StarPilot Auto.
2026-10-27 15:52:21 -04:00
Dom 16d118c4a3 Keep automatic mirrored GM pedal discovery on normal CAN wiring
Scope live VIN and firmware queries to bus0 only when the passive pedal
layout and configured/effective GM query topology agree. Preserve normal
CAN fingerprinting, control configuration, and all Panda fault vetoes.

Regression covers recorded Bolt startup layout, real ISO-TP discovery,
negative topology cases, and unchanged pedal safety configuration.
2026-10-27 14:41:11 -05:00
firestar5683 b5807a459d Restore Mach-E stopped-lead braking on rolling approaches 2026-10-27 13:45:34 -05:00
firestar5683 43c5c1538a Rebind Unchanged Map Graphics Capability Guards 2026-10-27 13:41:13 -05:00
firestar5683 6294915b59 Compress Ford Stop Fixture Without Changing Samples 2026-10-27 13:33:58 -05:00
firestar5683 1454919bfe Preserve Subaru AOL intent across native-first hard stops 2026-10-27 13:23:12 -05:00
firestar5683 c1df455977 Retain established Ford model stops through stationary noise 2026-10-27 13:10:51 -05:00
firestar5683 ac9cb41002 Use Strictly Expired Ford Proof in Deadline Refusal Test 2026-10-27 12:57:32 -05:00
firestar5683 c9734821f9 Resynchronize Ford angle only after steering rejections 2026-10-27 12:51:32 -05:00
firestar5683 e1e02b3949 Preserve Ford lateral intent when cancelling cruise 2026-10-27 12:41:47 -05:00
firestar5683 6a029b3faa Avoid Device Artifacts in Documentation CI Checkout 2026-10-27 12:32:56 -05:00
firestar5683 a30733cf64 Retain Ford AOL through native-first brake withdrawal 2026-10-27 12:29:51 -05:00
firestar5683 ff161633bf Complete Galaxy Description and Recovery Coverage 2026-10-27 12:13:27 -05:00
Zeph Leggett cf4301b16e Jetlink: take over on native02e with the small model's warp 2026-10-27 12:10:26 -05:00
firestar5683 4cc02a4a8f Validate Extracted GPU Loader Fixture Callable 2026-10-27 11:12:20 -05:00
firestar5683 8f86a97616 Expose driver monitoring reset in Galaxy 2026-10-27 11:00:04 -05:00
firestar5683 6bad7eb908 Qualify Held Override Source Handoff Through Original Expiry 2026-10-27 10:56:43 -05:00
firestar5683 6effaaa356 Accelerate Chestnut model loading 2026-10-27 10:55:08 -05:00
firestar5683 81f9630470 Align FLM Test Indentation 2026-10-27 10:41:15 -05:00
firestar5683 ec56bb1f3e Align Ford Curvature Oracle with Qualified Mode Handoff 2026-10-27 10:32:42 -05:00
firestar5683 0ae41eacb6 Refresh GM lateral proof at authority selection 2026-10-27 10:13:15 -05:00
firestar5683 dd37cc81e9 Preserve HKG held-brake lateral proof until its original expiry 2026-10-27 10:08:25 -05:00
firestar5683 eec5cde2c5 Restore Dom low-speed vision stop buffer for HKG longitudinal 2026-10-27 10:07:20 -05:00
firestar5683 a6654bf867 Harden Galaxy drive analysis recovery 2026-10-27 10:05:44 -05:00
firestar5683 42c0d6f24a Qualify Toyota Replay Profile and Braking Expectations 2026-10-27 10:02:02 -05:00
firestar5683 860d2559c5 Require native effect in the selected FLM trial direction
Symmetric native probes could qualify a coordinate even when only the opposite direction changed its encoded command. Carry each direction’s measured visibility and physical effect into selection and interrupted-study recovery so a quantized direction cannot borrow the other direction’s evidence.

Keep existing trial bounds, slew, deadlines and independent acceptance criteria. Validate the previous selector failure, full FLM suite and recorded d5 producer/learner replay. Real-drive improvement requires post-update recorded evidence.
2026-10-27 09:53:24 -05:00
firestar5683 1184e06221 Align Ford startup test with stock idle handoff 2026-10-27 09:31:54 -05:00
firestar5683 5fc5709e4d Block Elantra Steering on Panda CAN Interrupt Faults 2026-10-27 09:28:22 -05:00
firestar5683 f63c935029 Build Toyota source deadline continuity fix 2026-10-27 09:24:33 -05:00
firestar5683 c8ae020038 Merge current changes for Toyota source cutoff fix 2026-10-27 09:23:03 -05:00
firestar5683 887401fc58 Preserve unexpired Toyota command proofs across source cutoffs 2026-10-27 09:22:55 -05:00
firestar5683 d60f2d7ada Synchronize Ford curvature mode handoff with active commands 2026-10-27 09:17:24 -05:00
firestar5683 7a2a88c40c Match Ford BluePilot acknowledgements to emitted packets 2026-10-27 08:55:40 -05:00
Dom 3f251eb5ee Account for measured Ford AOL module workload 2026-10-27 04:42:22 -05:00
firestarsdog 5f84b6c763 Unify speed typography for clearer hierarchy 2026-10-27 04:54:30 -04:00
Dom 5b486edff4 Declare exact pandad build outputs for CI provenance 2026-10-27 03:22:05 -05:00
Dom 7dc37f190f Repair Galaxy lint and qualification fixtures after app split 2026-10-27 02:01:37 -05:00
prabhaavp 2571b8d5c4 Merge pull request #257 from dirwin31:AppSplit
Galaxy: Beta group install, app key import and Sentry app alerts
2026-10-27 02:48:02 -04:00
Prabhaav Pillai d0badbe715 Update app IDs and notification handling for Galaxy app 2026-10-27 02:46:51 -04:00
Danny 0c5f83e690 Galaxy: Remove the APK QR code and hide phone internet sharing
The QR code only linked to the removed APK download, so /api/companion/qr.svg is gone and /api/galaxy/qr.svg answers only when Galaxy is paired. Hide the Enable phone internet button, its restart checkbox and the sharing blurb on the Galaxy page.
2026-10-27 02:46:51 -04:00
Danny 7d81b58d52 Companion: Stop shipping the Direct APK
Android users join the Play beta group from Galaxy, so remove the bundled installer, its version file and the workflow that verified them. The comma now reports no shipped app version, so it no longer offers an offline update.
2026-10-27 02:46:51 -04:00
Danny 8f6d643936 Galaxy: Send Android users to the beta group instead of the APK download 2026-10-27 02:46:51 -04:00
Danny d08cd1b08b Galaxy: Add remote keys and app Sentry notifications
Serve the Galaxy app key and its QR code from /api/galaxy/session to direct local connections so the Android app can import the tunnel key on its own. Let the app and the iOS app register for Sentry push alerts.
2026-10-27 02:46:50 -04:00
Prabhaav Pillai 50e60ef156 Add car control sections and update projections in settings for AA support 2026-10-27 02:36:31 -04:00
Dom cb16d27d3d Qualify SLC receipt retention and aligned SET behavior 2026-10-27 01:20:39 -05:00
Dom 6f3e156abd Refresh protected inventory receipt after SLC source update 2026-10-27 00:47:02 -05:00
firestarsdog 6221a89f4b SLC Cleanup 2026-10-27 01:39:37 -04:00
Dom c79315f8d4 Refresh measured Ford custom test workload estimates 2026-10-27 00:14:15 -05:00
Dom 12dab8c31d Calibrate lifecycle workloads and align Honda cruise fixture 2026-10-26 23:24:41 -05:00
firestar5683 03f9a9576e Restore generic Ford curvature cluster ownership 2026-10-26 23:17:19 -05:00
firestar5683 1320712ab4 Pace Ford BluePilot commands from accepted CAN returns 2026-10-26 22:34:06 -05:00
Dom 3758148f83 Mark published Panda ELF objects as binary resources 2026-10-26 22:32:20 -05:00
Dom 6ebdb1b748 Align qualification fixtures and typing with established contracts 2026-10-26 22:13:51 -05:00
firestar5683 7d218d0a4b Restore recorded Elantra diagnostic admission and Panda fault logging 2026-10-26 22:05:26 -05:00
firestar5683 bbbed56e54 Build final Toyota transmit receipt identity fix 2026-10-26 21:59:05 -05:00
firestar5683 34dc3be528 Merge latest steering learning changes for Toyota publication 2026-10-26 21:57:22 -05:00
firestar5683 080d1d3a04 Build Toyota transmit receipt identity fix 2026-10-26 21:57:04 -05:00
firestar5683 6103e0395b Preserve FLM learning windows across unchanged steering repeats
The d5 Ioniq 6 logs repeatedly reset source tracking when Card repeats a command that already arrived on time. Discard only a recent unchanged repeat after the existing native CAN verifier succeeds; add no sample, application time or permission.

Use independently admitted pending CarState brackets to complete original poses without waiting for that command to reach Card. Classify stale control source clocks as transient stale evidence, preserving the original bounded study deadline. Replace Python character loops with equivalent strict text predicates.

Validation: focused regression suite and paced d5 replay with original Card command references; actual controller/CAN trial application and worsening-trial rollback. No timing/acceptance threshold changes or claimed road improvement.
2026-10-26 21:55:26 -05:00
firestar5683 be522b4ccf Merge branch 'SecretGoodStarPilot' of https://github.com/StarPilotHQ/StarPilot into HEAD 2026-10-26 21:54:12 -05:00
firestar5683 9c0cbcd5f4 Ignore Toyota transmit receipts when checking adapter identity 2026-10-26 21:54:11 -05:00
firestarsdog ed2abcc4b9 Torque Bar Optimization + Gradient 2026-10-26 21:55:36 -04:00
firestar5683 eebd74e745 Use an independent target for single-line tracking 2026-10-26 20:13:26 -05:00
firestarsdog 8e4710a4e6 Merge remote-tracking branch 'origin/SecretGoodStarPilot' into SecretGoodStarPilot 2026-10-26 21:00:28 -04:00
firestar5683 0d01b3d818 Restore Subaru AOL steering angle and speed envelope 2026-10-26 19:46:38 -05:00
firestarsdog 1e90f2da72 Speed Limit Confirmations UI Work 2026-10-26 20:38:11 -04:00
firestarsdog 6a07f58ea6 Remove unnecessary 3-second wait from automatic SLC adoption 2026-10-26 20:38:11 -04:00
firestarsdog 632fa39562 Handle Toyota SLC targets within native cruise limits 2026-10-26 20:38:11 -04:00
firestarsdog 2e395aa714 Align shared SLC actions and simplify common flow 2026-10-26 20:38:11 -04:00
Zeph Leggett 8b4902209c Jetlink: move the warp output when the small model loads
The output used to move after the small model's warmup had linked the
warp to its address, which needed the warp to link again by dropping a
private tinygrad cached property. Had that ever stopped working, the
small model's own warp would have kept writing the freed memory.

modeld now asks the adapter's coherent_warp() (Jetlink's make_coherent
with Jetlink on, off legacy9d) and load_verified_model moves the small
model's warp output before warmup(), as Jetlink's own warp moves its
output before its first call. CatalogWarp only reads the answer
(warp_copied): sent from where it is, or copied out, chosen once.
2026-10-26 19:32:00 -05:00
Zeph Leggett 0701c25b14 Jetlink bf4dc94
make_coherent's contract is one rule: move a buffer before the first call of the JIT that writes it. CREDITS.md and JETLINK.md name the new revision.
2026-10-26 19:31:43 -05:00
firestar5683 f6b2c132fb Fix Ford CAN-FD cluster ownership and parked startup warnings 2026-10-26 19:20:54 -05:00
Zeph Leggett f36b39a68b Jetlink: send the warp from coherent memory on a comma
tinygrad maps the warp's QCOM output write-combined, which the CPU reads
uncached: CatalogWarp spent ~2.9 ms a frame copying the frame out, out
of the 46 ms a host has to answer in. It now moves the output to
IO-coherent write-back memory with Jetlink's warp.make_coherent, as
Jetlink's own warp does, and the link sends straight from it (0.22 ms
measured for Jetlink's warp). The warp has run by then, and tinygrad
bakes a held buffer's address in at link time, so its program links
again on the next call. If the kernel refuses the memory, the frame is
copied out as before, and the adapter's log says which.

TinyJit's per-call input checks are left alone: 0.045 ms a call on a
Mac under this tinygrad, not the ~1 ms they cost under Jetlink's.
2026-10-26 19:04:34 -05:00
Zeph Leggett bf09b8eea4 Jetlink e0257f4
warp.make_coherent is public, for an adapter's own warp: it moves a QCOM
output to memory the CPU reads cached, as Jetlink's warp does.
CREDITS.md and JETLINK.md name the new revision.
2026-10-26 19:04:05 -05:00
Zeph Leggett 6e430242e3 Jetlink: the link's warp goes through the runner's warp_frames
CatalogModelState.warp_frames is the warp branch of run(), which run()
and CatalogWarp now both call, so the link enqueues exactly what the JIT
was captured with and reads npy and input_queues live rather than as
they were when it was built. CatalogWarp binds the output once (the
captured JIT returns the same buffer every call), reuses one pair of
warm-up frames for the probe and frees them, and drops their blob
cache entries. model_warp lets a failure raise without logging it
too: Jetlink logs it as it keeps the small model.
2026-10-26 18:50:12 -05:00
dirwin31 6c9ae11f63 Jetlink: send the small model's warp off legacy9d
Jetlink's stock driving warp is a legacy9d pickle, which a native02e
modeld cannot load, so the link never took over there. The adapter's
model_warp (Jetlink's optional hook since 0605e34) now hands Jetlink
the small model's own camera warp (CatalogWarp) on any other runtime:
the same JIT, inputs and blob cache, read into host memory after each
frame, with a probe at load that refuses an output not read live.
Under legacy9d it returns None and Jetlink loads the stock pickle.

From #253, without its edit to jetlink_repo or the hold tolerance
override.
2026-10-26 18:43:20 -05:00
Zeph Leggett db94a2be1a Jetlink 0605e34
The adapter may now supply the warp the link sends (ModelSide.model_warp,
optional; what it returns is checked against interface.FrameWarp before
the frame loop). Without one, or when it returns None, Jetlink loads the
stock driving warp pickle as before. API stays 3.

CREDITS.md and JETLINK.md name the new revision.
2026-10-26 18:41:10 -05:00
firestar5683 c417e793c5 Add Heatdeath Controls Benchmark 2026-10-26 18:28:20 -05:00
firestar5683 718a2c3ddd Keep SpacePilot open across cold setup and interrupted clock reads
Discard a preempted clock pair and try up to three fresh pairs while
retaining the 1 ms accepted span and all existing freshness/resume gates.
Yield after receive-only decoder construction so normal UI collection
refreshes before full physical admission, Offroad request or input.

Recorded d5 clock failure; reproduced timing regressions; 250 focused
native cases, lint/type checks and independent source/evidence review.
No vehicle commands, device deployment or physical cure claim.
2026-10-26 18:17:20 -05:00
firestar5683 76b5f7e512 Avoid rebuilding the favorites catalog on each tap 2026-10-26 17:36:13 -05:00
firestar5683 5e5fa8d9c1 Preserve EV6 AOL on unassigned cruise CANCEL 2026-10-26 17:21:22 -05:00
firestar5683 36111dfdb6 Preserve Subaru AOL intent through stops 2026-10-26 16:52:11 -05:00
prabhaavp 8ead1ab0f9 Merge pull request #252 from dirwin31:AA-Wireless-Setup
Android Auto: improve wireless adapter setup and pairing
2026-10-26 17:44:33 -04:00
Prabhaav Pillai 6ba879ca3b Preserve the full discovery deadline for slow adapters, clear stale receiver selection when switching setup types, reuse the existing UI spinner, and tighten comments and regression tests. 2026-10-26 17:44:18 -04:00
firestar5683 a9af491104 Preserve HKG AOL authorization across button counter rollover 2026-10-26 16:36:50 -05:00
firestar5683 42a918acc2 Keep Encouragement and Gas Override From Stacking 2026-10-26 16:29:29 -05:00
firestar5683 c604521745 Build final Toyota and Lexus configuration port 2026-10-26 16:26:58 -05:00
firestar5683 09d7e2873e Merge current changes for Toyota publication 2026-10-26 16:25:08 -05:00
firestar5683 5189f468d2 Build Toyota and Lexus configuration parity 2026-10-26 16:25:06 -05:00
Dom 40a23df6be Align damping qualification fixtures and preserve MISRA parameter immutability 2026-10-26 16:23:24 -05:00
prabhaavp 7a38780739 Merge pull request #250 from dirwin31:SGSP-Better-Phone-GPS
Preserve usable phone GPS fixes and enforce measurement freshness
2026-10-26 17:22:14 -04:00
firestar5683 5c78ec40d7 Merge current changes for Toyota configuration parity 2026-10-26 16:19:46 -05:00
firestar5683 c6f00f7a4d Complete Toyota and Lexus configuration parity with Dom 2026-10-26 16:19:46 -05:00
firestar5683 a119dfbae0 Encourage Persistent Clear-Road Hesitation 2026-10-26 16:16:35 -05:00
prabhaavp fc6501364b Improve README.md badges and links
Updated badge links in README.md for better visibility.
2026-10-26 16:57:06 -04:00
firestar5683 1fa5818bb3 Refresh StarPilotHQ Project Homepage 2026-10-26 15:47:38 -05:00
firestar5683 ebfea6b3b7 Build SecOC Yaris stock and alpha support 2026-10-26 15:35:00 -05:00
firestar5683 e672bd30c1 Support SecOC Yaris AOL with stock and alpha longitudinal 2026-10-26 15:33:22 -05:00
Danny 4c37c009b7 Better Bugsink Reporting 2026-10-26 13:25:49 -07:00
firestar5683 ba87dd4c70 Build Subaru angle routing fix 2026-10-26 15:23:09 -05:00
Danny a581b01d84 Revert "Android Auto: restore optional car pairing row for wireless adapters"
This reverts commit 44d9c868b2.
2026-10-26 13:20:57 -07:00
firestar5683 63888e0fd8 Restore Subaru angle steering routing and collision warning checks 2026-10-26 15:20:15 -05:00
dirwin31 44d9c868b2 Android Auto: restore optional car pairing row for wireless adapters 2026-10-26 13:06:37 -07:00
firestar5683 e2a5d6ca66 Build RAV4 Prime AOL and sequential gear fix 2026-10-26 15:06:20 -05:00
firestar5683 8f3be8f6fb Enable key-ready RAV4 Prime AOL in sequential gear 2026-10-26 15:05:09 -05:00
dirwin31 b7fcbfee64 Unify Android Auto setup layouts and add themed pairing progress indicators.
Clarify connection stages across wireless, adapter, USB, and tablet options with regression coverage for progress states and responsive layouts.

(cherry picked from commit 489dad0e7733127c7a9654c9a7491d1ab8a554a4)
2026-10-26 13:00:54 -07:00
dirwin31 a420ff55ed AA: Layout Again 2026-10-26 13:00:52 -07:00
dirwin31 54b2f060e7 AA: Put Messages in Pairing Box, Hide Banner 2026-10-26 13:00:51 -07:00
dirwin31 8822e26d5b Android Auto: Wait Longer for Adapters 2026-10-26 13:00:49 -07:00
dirwin31 1d38987d61 AA: Layout Unification 2026-10-26 13:00:48 -07:00
dirwin31 f7fed1776d AA BT: Fix Pairing Window Request from Galaxy 2026-10-26 13:00:47 -07:00
dirwin31 8d2872bed4 Android Auto: Unify Pages 2026-10-26 13:00:45 -07:00
dirwin31 2141786f05 Android Auto: Unblock Pairing Circumstances 2026-10-26 13:00:43 -07:00
dirwin31 2f7d990597 BT: Show Device List Again 2026-10-26 13:00:42 -07:00
dirwin31 49660054f9 Android Auto: Add support for forgetting selected car during auto-connect retries 2026-10-26 13:00:40 -07:00
dirwin31 4c07adab7d UI: Selector Fixes, Less Duplicate for AA Wireless. 2026-10-26 13:00:39 -07:00
dirwin31 6b09da8676 Android Auto: apply workflow fixes
Split Android Auto changes from 08fb4c3001fe95dbbe784af54e09ecc33043a8bd.
2026-10-26 13:00:38 -07:00
dirwin31 8d9115f9e7 Android Auto: Allow choosing wired AA with Jetlink on 2026-10-26 13:00:36 -07:00
dirwin31 2c80f161a2 Android Auto: Retry Bluetooth connects that report unimplemented 2026-10-26 13:00:34 -07:00
dirwin31 2635773393 Android Auto: Send failed session traces to Bugsink 2026-10-26 13:00:33 -07:00
dirwin31 7fea4f939d Android Auto: AAWireless v1 adapter option
Co-authored-by: Peter Nguyen <trung791997@gmail.com>
2026-10-26 13:00:31 -07:00
dirwin31 55a41e9026 AA UI: Fix Blocked Wired 2026-10-26 13:00:30 -07:00
dirwin31 06fefc6768 Android Auto UI: Integrate wired and app settings directly into step 2 2026-10-26 13:00:28 -07:00
dirwin31 dbc8e0ea39 Add car type and profile configuration options for Android Auto setup 2026-10-26 13:00:26 -07:00
firestar5683 0037bdd836 Allow Fast Return to StarPilot 6.x 2026-10-26 14:40:01 -05:00
firestar5683 02ae25b975 Make SpacePilot entry reliable at natural tapping speeds
Count distinct touch releases by their original timestamps, retain taps
during valid Park dwell, and let preparation earn its own admission.
Cancel only the pending C4 startup camera redirect for a freshly parked
open game. All session, physical, alert, control, noOutput, ignition and
input authority checks remain unchanged.

231 focused native cases, original before regressions, current successor
entry/decoder qualification, canonical lint/type checks and independent
review. Host functional evidence; no device changes or headroom claim.
2026-10-26 14:35:25 -05:00
firestar5683 b976cd2e7c Stop Paid Permanent Address Lookups 2026-10-26 14:23:42 -05:00
firestar5683 6c58d58a7a Bind CAN-FD mirror damping to the selected calibration 2026-10-26 14:15:34 -05:00
firestar5683 8098eb454c Limit Steering Counter Repair to Bolt Pedal Profiles 2026-10-26 14:00:50 -05:00
firestar5683 6e88336f3a Trim Repeated AOL Guard Commentary 2026-10-26 13:39:40 -05:00
firestar5683 76c2aba14e Keep Lane Width Calculation Out of NumPy Cold Imports 2026-10-26 13:36:59 -05:00
firestar5683 261e2ca45d Preserve completed FLM windows during inline probe input gaps 2026-10-26 13:31:42 -05:00
firestar5683 d96f6af078 Track GM Steering Acknowledgement Counters 2026-10-26 13:21:55 -05:00
firestar5683 e1818affa2 Preserve Ford AOL through gas-held engagement and steering release 2026-10-26 12:46:43 -05:00
firestar5683 a71a67fd1f Allow Onroad Wi-Fi Settings 2026-10-26 12:37:26 -05:00
firestar5683 4d0246997b Restore lane centering with fresh polled state 2026-10-26 12:36:20 -05:00
firestar5683 d41159e65b Preserve Dom CAN-FD torque damping across longitudinal modes 2026-10-26 12:32:32 -05:00
dirwin31 306afa06a8 AOL: Skip the temporary steering warning while the driver steers 2026-10-26 17:32:23 +00:00
firestar5683 ec25e2330a Consume pending native evidence before FLM collection 2026-10-26 11:41:40 -05:00
Dom 2551c9ce5f lint: make Rebecca shield zip default explicit 2026-10-26 11:26:52 -05:00
firestarsdog cabc7da5b9 Add Rebecca power-ups, Bizarro mode, and scaling difficulty (contributed by Mary) 2026-10-26 12:05:20 -04:00
Dom df021f4e14 test: initialize lane inactivity reason in shared longitudinal fixture 2026-10-26 10:52:03 -05:00
dirwin31 be8cf95de8 AOL: Skip the temporary steering warning while the driver steers
A hard override on the Civic puts the EPS in NO_TORQUE_ALERT_1, which
reads as a temporary steering fault. The armed-AOL warning added
steerTempUnavailableSilent for it every time, even though the driver was
holding the wheel. CarSpecificEvents already stays quiet in that case.
Route 00000060: all 23 status-2 windows cleared before the driver let go.
The warning still shows if the fault is present while the wheel is free.

(cherry picked from commit f9167e2a24007c0822b4eae8870a24100a709325)
2026-10-26 08:25:28 -07:00
firestar5683 7f5d30c30d Reset lane centering for lane changes and identify input refusals 2026-10-26 10:13:21 -05:00
dirwin31 41543cd593 Preserve usable phone GPS fixes and enforce measurement freshness 2026-10-26 08:02:48 -07:00
firestar5683 0f00621c98 Coalesce refreshed FLM receipts with scheduled tracking 2026-10-26 09:34:09 -05:00
firestar5683 099cbd2809 Keep SpacePilot awake and release game mode on verified key-off
Renew the existing activity timer only during active offroad game ticks.
Observe only the exact game-owned Offroad request and restore Auto after
continuous fresh all-Panda ignition-off, closed game, stopped driving
services and healthy noOutput through the existing owner transaction.
Ordinary exit, stale data, reconnect and external mode changes never
restore Auto. UI restart does not adopt a previous game request.

142 source-bound focused cases, original timeout regression, lint/type
checks and independent source/evidence review. Synthetic host lifecycle
qualification only; no device changes or new physical/headroom claim.
2026-10-26 09:17:19 -05:00
firestar5683 0e5e5574c4 Publish refreshed FLM collection receipts on tracking 2026-10-26 09:09:55 -05:00
firestar5683 347397ad38 Add exclusive parked SpacePilot mode and native Big UI
Use the supported Offroad lifecycle with fresh Park, receive-only CAN,
stopped-process and noOutput checks. Add offroad touch/controller play
and the native Big UI five-version-tap entry. Exiting never restores
driving mode automatically within the existing manager session.

Qualified by210 unique focused passing cases, lint/type checks, native
host rendering/input/resource checks and independent lifecycle review.
Actual vehicle lifecycle, hardware inputs and device headroom remain
unqualified; no deployment is part of this change.
2026-10-26 00:01:36 -05:00
firestar5683 35f62c3c5a Bound sampled lane clearance with corroborated body geometry 2026-10-25 23:39:42 -05:00
firestar5683 3da487cd19 Mark NumPy sinc as an intentional API name 2026-10-25 23:11:00 -05:00
firestar5683 d914cf86ba Register automatic Ford discovery regressions in vehicle CI 2026-10-25 23:02:34 -05:00
firestar5683 6413fef0f9 Refine C3 Disengage Sound 2026-10-25 23:00:35 -05:00
firestar5683 87b42f9127 Use combined HKG lateral permission for steering TX and forwarding 2026-10-25 22:59:49 -05:00
firestar5683 822dbe692f Keep automatic Ford discovery on the normal CAN harness 2026-10-25 22:55:54 -05:00
firestar5683 eb2b6c2d23 Preserve Local Changes During Fast Updates 2026-10-25 22:18:41 -05:00
firestar5683 0009df9084 Require fresh Ioniq 6 parser receipts after ECU handoff 2026-10-25 22:06:07 -05:00
firestar5683 f3bbd38e81 Wake Auto Learn on fresh motion and native evidence 2026-10-25 21:05:53 -05:00
firestar5683 a3811ef5dd Keep Auto Learn collection alive during slow metadata delivery 2026-10-25 20:50:06 -05:00
firestar5683 8807d791f1 Require qualified body bounds before added lane correction 2026-10-25 20:04:27 -05:00
Dom a168542200 Fix shared lane test feed cadence without hidden historical batches 2026-10-25 19:49:28 -05:00
firestar5683 aa17fb6f0e Add native parked SpacePilot Easter egg on comma four 2026-10-25 19:43:58 -05:00
firestar5683 d59a37d37f Track single lane lines with smooth transitions and predicted clearance 2026-10-25 19:17:20 -05:00
Dom fff069c0e3 tests: qualify GM factory clocks and FLM physical-effect fixtures 2026-10-25 19:13:41 -05:00
firestar5683 f2b235bc7b Defer EV6 ECU takeover until the inactive control producer is ready 2026-10-25 19:07:34 -05:00
firestar5683 318c440b2b Allow measurable Auto Learn trials without increasing slew 2026-10-25 18:32:04 -05:00
firestar5683 567c190d5d Restore Galaxy settings when returning from tools 2026-10-25 18:23:30 -05:00
Dom ef6354430a ci: refresh protected inventory receipt for privacy policy update 2026-10-25 18:16:42 -05:00
prabhaavp da972263c3 Revise privacy policy for Galaxy: StarPilot Companion 2026-10-25 19:13:17 -04:00
Dom 1cd3c7f179 test: qualify Bolt ACC factory brake clock in native and AOL fixtures 2026-10-25 18:04:20 -05:00
Prabhaav Pillai 19d4b21f6b README and privacy policy for Android App 2026-10-25 19:02:39 -04:00
firestar5683 0ddf2cb8de Reduce comma 3 speaker resonance 2026-10-25 17:53:57 -05:00
firestar5683 eb7afba097 Show Jetlink model status in GPU feedback 2026-10-25 17:41:51 -05:00
firestar5683 27b552cbd8 Reset Jetlink fallback history without GPU compilation 2026-10-25 17:39:43 -05:00
firestar5683 13e1875d20 Synchronize Bolt ACC pedal brake frames with factory counters 2026-10-25 17:33:45 -05:00
Dom 5ccbcc5752 ci: recognize the NumPy sinc identifier in speaker filtering 2026-10-25 17:11:03 -05:00
firestar5683 b5c90af1cf Fix Galaxy Jetlink setup guide link 2026-10-25 16:58:36 -05:00
firestar5683 33fc8be564 Adapt StarPilot sounds for the comma 3 speaker 2026-10-25 16:57:26 -05:00
Dom 2084622bec ci: calibrate material workload estimates from both hosted runs 2026-10-25 16:54:37 -05:00
Dom d3193635a2 ci: rebalance custom pytest using current hosted workload measurements 2026-10-25 16:21:40 -05:00
Dom 3b3960a5b8 Align EV9 pedal contract test with independent lateral authority 2026-10-25 15:37:59 -05:00
firestar5683 de97a6eff3 Resume Auto Learn discovery when the road regime changes 2026-10-25 15:09:51 -05:00
firestar5683 9fc77a256b Try feasible Auto Learn directions at coordinate bounds 2026-10-25 14:59:31 -05:00
firestar5683 447373d78b Verify PE AOL steering continuity through pedal overrides 2026-10-25 14:40:56 -05:00
firestar5683 8a5fbf0be8 Preserve bounded clearance with one visible lane line 2026-10-25 14:31:40 -05:00
firestar5683 1a5ee718c9 Keep HKG AOL armed through full engagement and pedal overrides 2026-10-25 14:28:55 -05:00
Dom 5e83cb218f Condense transport intent comments within source size limit 2026-10-25 14:16:20 -05:00
firestar5683 fc5875831c Arm Ford AOL when cruise engages before the automatic MAIN pulse 2026-10-25 14:10:02 -05:00
firestar5683 24174ec6a6 Build Toyota pedal source continuity fix 2026-10-25 14:02:52 -05:00
firestar5683 ef03c7ba36 Merge current changes for Toyota pedal continuity 2026-10-25 13:58:28 -05:00
firestar5683 59c6935f3c Preserve Toyota AOL intent through held accelerator gaps 2026-10-25 13:57:49 -05:00
firestar5683 3c740d2050 Allow conflated car state snapshots in Auto Learn 2026-10-25 13:55:50 -05:00
firestar5683 3b67ac199e Refresh GM lateral evidence after control preparation 2026-10-25 13:54:50 -05:00
firestar5683 90916a5ff2 Cover recorded RAV4 configuration in brake regression 2026-10-25 13:51:35 -05:00
Dom 71632a6b48 Refresh prebuilt receipt for validated Arabesque assets 2026-10-25 13:50:12 -05:00
firestar5683 baae36fc3a Preserve Toyota AOL intent through held-brake source gaps 2026-10-25 13:47:45 -05:00
firestarsdog 0c842da32e Arabesque: Rebecca, Adrift 2026-10-25 14:33:26 -04:00
Dom d6fe10f60b Fix file manager type contracts and refresh documentation receipt 2026-10-25 13:01:13 -05:00
firestarsdog faa61d48fc Arabesque Files 2026-10-25 13:50:43 -04:00
firestar5683 d33f5b550c Preserve AOL intent during temporary EPS event joins 2026-10-25 12:25:17 -05:00
Dom e404020aca Assert UI test bounds before indexing optional values 2026-10-25 12:22:06 -05:00
firestar5683 0421b31bb1 Remove duplicate Ioniq 6 blind-spot sound alias 2026-10-25 12:14:32 -05:00
firestarsdog 32360730ba Build Panda firmware with tici stall recovery 2026-10-25 05:08:08 -04:00
firestarsdog 4a84cf290d Unify speed-limit signs across UIs for consistent sizing and clarity 2026-10-25 05:05:36 -04:00
firestarsdog 2248773fcc Restore tici fan retries after stalls 2026-10-25 04:58:10 -04:00
Dom 286641c1fe Declare logger metadata test build output 2026-10-25 02:06:43 -05:00
Dom 4b30638588 Isolate FLM native binding fixtures from preloaded extensions 2026-10-25 01:24:48 -05:00
firestar5683 4cd78e7366 Log bounded GM lateral withdrawals after command publication 2026-10-25 01:06:32 -05:00
firestar5683 d17623d930 Preserve Ford AOL intent across EPS gas-release receipt joins 2026-10-25 00:43:59 -05:00
firestar5683 9ca75ddefd Report interrupted pending trials to the learner 2026-10-25 00:42:22 -05:00
Dom cafb6df8c2 Balance custom pytest pedal caller workload 2026-10-25 00:29:21 -05:00
firestar5683 b6e7e63d87 Detect unresponsive steering on stock ASCM Volts 2026-10-24 23:48:28 -05:00
firestar5683 82abd03443 Retain pending learning study across consumed-source timing gaps 2026-10-24 23:36:17 -05:00
firestar5683 5674f11442 Avoid loading model runtime in Controls 2026-10-24 23:26:26 -05:00
firestar5683 49ab8cf392 Record model identity in qlog opening metadata 2026-10-24 23:20:11 -05:00
firestar5683 202a52c353 Match BluePilot angle delay and missing-model input behavior 2026-10-24 22:09:51 -05:00
firestar5683 766f4ceabb Use an explicit failure for unapproved capture in source tests 2026-10-24 22:07:06 -05:00
firestarsdog baab9caf43 Fix false GPU fallback icons 2026-10-24 23:02:19 -04:00
firestar5683 4c250a6a06 Restore Auto Learn source admission after GPU startup fix 2026-10-24 21:55:54 -05:00
firestar5683 809681dc24 Reduce repeated EV9 configuration copies 2026-10-24 21:54:37 -05:00
firestar5683 c12afe13e7 Capture native audio callback timing 2026-10-24 21:46:43 -05:00
firestar5683 d6b8ade37f Defer torque EV ECU disable until inactive controls are ready 2026-10-24 21:34:54 -05:00
Dom 0427ba9fbf Isolate Bolt saved-owner test Params lock 2026-10-24 21:30:33 -05:00
firestar5683 3d8f901f1c Exit failed GPU startup before isolated Small recovery 2026-10-24 21:25:48 -05:00
firestar5683 58bb172bda Reduce repeated startup configuration serialization 2026-10-24 21:02:51 -05:00
firestar5683 e3872c4760 Retain Auto Learn source rejection evidence in route logs 2026-10-24 20:27:33 -05:00
firestar5683 b83bddccf3 Keep sign parity tests independent of a graphics context 2026-10-24 20:25:47 -05:00
firestar5683 d5dcbd1891 Restore original small UI sign proportions 2026-10-24 20:08:38 -05:00
firestar5683 192b64e407 Match compact sign tests to the shorter outline 2026-10-24 18:47:48 -05:00
firestar5683 b1b48eaaf3 Keep UI modules within the source size limit 2026-10-24 18:21:24 -05:00
prabhaavp 3530f447c1 Merge pull request #246 from natehuby76/fix/c4-menu-start
reset c4 menu on reopen
2026-10-24 19:08:19 -04:00
whoisdomi e97e07e1b3 Model runtime fix
Select the model runtime before validating device artifacts
2026-10-24 18:02:09 -05:00
firestar5683 2251e13068 Compact small UI speed limit signs 2026-10-24 17:51:54 -05:00
Nathan 69b0d7c600 reset c4 menu on reopen 2026-10-24 17:42:05 -05:00
firestar5683 1c332c67f8 Reduce digest size-selection overhead 2026-10-24 17:11:24 -05:00
firestar5683 eb97258ed1 Optimize scalar PID clipping 2026-10-24 16:23:06 -05:00
firestar5683 4fe5cca267 Preserve model source renderer API and static contracts 2026-10-24 15:54:45 -05:00
firestarsdog d656e9f422 Large UI Model Loading 2026-10-24 16:49:56 -04:00
Dom 44ad6f5342 Balance lifecycle shards using measured pedal caller cost 2026-10-24 15:43:48 -05:00
firestar5683 a6779a8aa0 Reduce feedback observation copying 2026-10-24 15:41:39 -05:00
2357 changed files with 342498 additions and 30895 deletions
+13 -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,4 +47,17 @@ 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.
openpilot/selfdrive/pandad/main.o -text
openpilot/selfdrive/pandad/panda.o -text
openpilot/selfdrive/pandad/panda_safety.o -text
openpilot/selfdrive/pandad/panda_usb.o -text
openpilot/selfdrive/pandad/pandad.o -text
openpilot/selfdrive/pandad/spi.o -text
# Compressed recorded regression fixture; retain exact binary bytes.
openpilot/starpilot/tests/testdata/ford_mache_lead_departure.json.gz -text
@@ -1,68 +0,0 @@
name: Android companion release
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: android-companion-release
cancel-in-progress: false
jobs:
build:
name: Test, build and sign the companion APK
runs-on: ubuntu-latest
timeout-minutes: 30
environment: android-release
defaults:
run:
working-directory: openpilot/starpilot/companion/android
steps:
- uses: actions/checkout@v7
with:
submodules: false
lfs: false
persist-credentials: false
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Install Android SDK platform and build tools
run: yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "platforms;android-37" "build-tools;37.0.0"
- name: Unit tests
run: ./gradlew testDebugUnitTest
- name: Restore signing key
env:
KEYSTORE_BASE64: ${{ secrets.COMPANION_KEYSTORE_BASE64 }}
run: |
umask 077
printf '%s' "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/companion.jks"
- name: Build and sign the APK
env:
COMPANION_KEYSTORE: ${{ runner.temp }}/companion.jks
COMPANION_KEY_PASSWORD: ${{ secrets.COMPANION_KEY_PASSWORD }}
run: ./build_apk.sh
- name: Remove signing key
if: always()
run: rm -f "$RUNNER_TEMP/companion.jks"
- name: Check the APK
working-directory: openpilot/starpilot/galaxy/bin
run: |
if unzip -l starpilot-companion.apk | grep -q 'assets/galaxy/'; then
echo 'APK still bundles Galaxy' >&2
exit 1
fi
BT="$ANDROID_HOME/build-tools/37.0.0"
"$BT/aapt2" dump badging starpilot-companion.apk | head -1
"$BT/apksigner" verify --print-certs starpilot-companion.apk | grep -E 'certificate (DN|SHA-256)'
cat starpilot-companion.apk.version
- uses: actions/upload-artifact@v4
with:
name: starpilot-companion-apk
path: |
openpilot/starpilot/galaxy/bin/starpilot-companion.apk
openpilot/starpilot/galaxy/bin/starpilot-companion.apk.version
if-no-files-found: error
retention-days: 30
+6 -1
View File
@@ -5,7 +5,7 @@ on:
branches:
- master
- Domathon
- SecretGoodStarPilot
- Dom
pull_request:
workflow_call:
inputs:
@@ -29,6 +29,11 @@ jobs:
steps:
- uses: commaai/timeout@v1
- uses: actions/checkout@v7
with:
sparse-checkout: |
/docs/
/openpilot/selfdrive/assets/icons_mici/settings/comma_icon.png
sparse-checkout-cone-mode: false
# Build
- name: Build docs
+1 -1
View File
@@ -2,7 +2,7 @@ name: iOS companion
on:
push:
branches: [SecretGoodStarPilot, secretgood-ios-integration]
branches: [Dom, secretgood-ios-integration]
paths:
- 'openpilot/starpilot/companion/ios/**'
- 'openpilot/starpilot/galaxy_bluetooth/**'
+1 -1
View File
@@ -2,7 +2,7 @@ name: safety qualification
on:
push:
branches: [master, Domathon, SecretGoodStarPilot]
branches: [master, Domathon, Dom]
pull_request:
workflow_dispatch:
+21 -8
View File
@@ -5,7 +5,7 @@ on:
branches:
- master
- Domathon
- SecretGoodStarPilot
- Dom
pull_request:
workflow_dispatch:
workflow_call:
@@ -35,17 +35,19 @@ jobs:
steps:
- uses: actions/checkout@v7
- name: Validate published device build receipt
if: github.event_name == 'push' && github.ref == 'refs/heads/SecretGoodStarPilot'
if: github.event_name == 'push' && github.ref == 'refs/heads/Dom'
run: python3 tools/ci/check_prebuilt_receipt.py --revision HEAD
- name: Check shipped device runtime dependencies
run: python3 -m unittest tools.laptop_device_build.tests.test_runtime_dependencies -v
run: |
python3 -m unittest tools.laptop_device_build.tests.test_runtime_dependencies -v
python3 -c 'from pathlib import Path; assert b"starting pandad (comma hardware)" in Path("openpilot/selfdrive/pandad/pandad").read_bytes(), "Shipped pandad must use the comma hardware build profile"'
- name: Validate tracked dependencies
run: python3 tools/vendor/check.py --revision HEAD
- run: ./tools/op.sh setup
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
@@ -207,6 +209,15 @@ jobs:
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
timeout-minutes: 5
run: tools/op.sh test opendbc_repo/opendbc/car/gm/tests --json-output gm-test-results.json
- name: Test shared GM discovery, routing, and manual overrides
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
timeout-minutes: 2
run: >-
.venv/bin/python -m pytest -q --import-mode=importlib
opendbc_repo/opendbc/car/gm/tests/test_shared_discovery.py
opendbc_repo/opendbc/car/gm/tests/test_automatic_discovery.py
opendbc_repo/opendbc/car/gm/tests/test_bolt_vin_discovery.py
--junitxml=gm-discovery-results.xml
- name: Test native independent-axis protocol
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: |
@@ -214,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
@@ -227,13 +239,14 @@ jobs:
STARPILOT_PYTHON: ${{ github.workspace }}/.venv/bin/python
run: go test -mod=readonly -race -run '^Test(MsgqNativeLayout|Shadow(GoPythonIPC|ExecutableHostGpsIPC))$' -count=1 -v
- name: Save complete unit test results
if: ${{ always() && (hashFiles('unit-test-results.json') != '' || hashFiles('gm-test-results.json') != '' || hashFiles('ci-results/host-test-diagnostics/**') != '') }}
if: ${{ always() && (hashFiles('unit-test-results.json') != '' || hashFiles('gm-test-results.json') != '' || hashFiles('gm-discovery-results.xml') != '' || hashFiles('ci-results/host-test-diagnostics/**') != '') }}
uses: actions/upload-artifact@v4
with:
name: unit-test-results
path: |
unit-test-results.json
gm-test-results.json
gm-discovery-results.xml
ci-results/host-test-diagnostics/
if-no-files-found: error
@@ -246,7 +259,7 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
steps:
- uses: actions/checkout@v7
- name: Provide JavaScript runtime for Galaxy contract tests
@@ -279,7 +292,7 @@ jobs:
timeout-minutes: 15
env:
RAYLIB_BACKEND: headless
run: .venv/bin/python -m tools.ci.custom_pytest_shards run --index "${{ matrix.shard }}" --count 24 --output "ci-results/custom-pytest-${{ matrix.shard }}"
run: .venv/bin/python -m tools.ci.custom_pytest_shards run --index "${{ matrix.shard }}" --count 32 --output "ci-results/custom-pytest-${{ matrix.shard }}"
- name: Save collection, every outcome, JUnit and durable worker diagnostics
if: always()
uses: actions/upload-artifact@v4
@@ -302,7 +315,7 @@ jobs:
pattern: custom-pytest-*
path: ci-results/custom-pytest-shards
- name: Require every case passed or audited capability-unavailable
run: python3 -m tools.ci.custom_pytest_shards aggregate --directory ci-results/custom-pytest-shards --count 24 --output ci-results/custom-pytest-complete.json
run: python3 -m tools.ci.custom_pytest_shards aggregate --directory ci-results/custom-pytest-shards --count 32 --output ci-results/custom-pytest-complete.json
- name: Save complete custom catalog verdict
if: always()
uses: actions/upload-artifact@v4
+2 -1
View File
@@ -49,6 +49,8 @@ bin/
!openpilot/starpilot/soloist/pulseaudio/modules/*.so
*.a
st[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]
!/openpilot/starpilot/system/starport/
!/openpilot/tools/starport/
*.unchunked
*.clb
*.class
@@ -72,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
+18 -8
View File
@@ -2,7 +2,7 @@
## Jetlink external inference
The comma-side Jetlink client and USB transport in `jetlink_repo` are by Zeph Leggett and the [Jetlink contributors](https://github.com/zoompilot/jetlink). They are vendored from revision `d24328d849b0e5154b55f149dc18c5dfe8bd79cc` under the MIT license, retained in [jetlink_repo/LICENSE](jetlink_repo/LICENSE). StarPilot's adapter (`openpilot/starpilot/models/jetlink_adapter.py`) builds on Jetlink's openpilot layer and owns its param names, Always On Lateral rule, model runner reset, settings and alerts. Companion applications and inference servers remain distributed by Jetlink.
The comma-side Jetlink client and USB transport in `jetlink_repo` are by Zeph Leggett and the [Jetlink contributors](https://github.com/zoompilot/jetlink). They are vendored from revision `a863c3801da6fd925d14eb1f5b1e250f8efcc649` under the MIT license, retained in [jetlink_repo/LICENSE](jetlink_repo/LICENSE). StarPilot's adapter (`openpilot/starpilot/models/jetlink_adapter.py`) builds on Jetlink's openpilot layer and owns its param names, Always On Lateral rule, model runner reset, settings and alerts. Companion applications and inference servers remain distributed by Jetlink.
## MRR35 Hyundai angle and radar
@@ -12,15 +12,11 @@ The copied MRR35 generator source and DBC definition are at `opendbc_repo/opendb
## Ford support adapted from BluePilot
The Ford lateral strategies, extended CAN constructors, camera-radar filtering and native checks are adapted from StarPilot `2efe6017bfe14cee4807833e05f9a7f6ba255cdb`. Their lineage includes BluePilot's `bp-7.0` Ford work, principally by Alan Polk, with contributions from John Christman, Jacob Neulight, tonesto7, Haibin Wen and other sunnypilot and BluePilot contributors. The reference snapshot is [`e1d051d7ba270261b4455068bd68f1a58db15a4a`](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a); the exact checkout used for the earlier StarPilot import was not recorded.
Ford lateral control vendors [BluePilot lateral](https://github.com/BluePilotDev/bluepilot-lateral) v0.2.1 (`2b9dc6cc784faac47a1885d0bda0561f7b76f74d`), developed by Alan Polk (`alan-polk`) and the BluePilot contributors. Alan provided the [StarPilot integration in PR #269](https://github.com/StarPilotHQ/StarPilot/pull/269); his commits and authorship are retained. BluePilot maintains the library and its integration; StarPilot keeps its settings, AOL policy, packaging and integration tests separate. Complete contributor and license notices remain in [the library credits](opendbc_repo/opendbc/bluepilot_lateral/CREDITS.md) and [license](opendbc_repo/opendbc/bluepilot_lateral/LICENSE).
Relevant lineage includes the controller and integration work in [`db2bdff05`](https://github.com/BluePilotDev/bluepilot/commit/db2bdff05df103d71df62f45c2a3cb5211aba6e6) and [`d0aac605f`](https://github.com/BluePilotDev/bluepilot/commit/d0aac605f99d37e9da205e419f7989c1e9eaa386), extended CAN and safety work in [`8f8d6d15f`](https://github.com/BluePilotDev/bluepilot/commit/8f8d6d15f0a590f42b78de964ffb0d0af7f5d63d), and manual-turn detection in [`97867c1eb`](https://github.com/BluePilotDev/bluepilot/commit/97867c1eb57b7472f6fc3de62f0fef576e5a5497).
The Ford CAN FD camera-lead radar filtering is adapted from StarPilot `2efe6017bfe14cee4807833e05f9a7f6ba255cdb`. Its lineage includes BluePilot's `bp-7.0` Ford work, principally by Alan Polk, with contributions from John Christman, Jacob Neulight, tonesto7, Haibin Wen and other sunnypilot and BluePilot contributors. The reference snapshot is [`e1d051d7ba270261b4455068bd68f1a58db15a4a`](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a); the exact checkout used for the earlier StarPilot import was not recorded.
The vehicle strategy and CAN/native owners preserve that attribution while adapting to the current controller interfaces and shared safety limits. Upstream contributors do not maintain or endorse this adaptation. Applicable notices are retained in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
## Optional Ford angle controller
The vendored `opendbc_repo/opendbc/bluepilot_lateral` angle core, codecs and Ford-only safety hooks are adapted directly from [BluePilot bp-7.0 e1d051d7ba270261b4455068bd68f1a58db15a4a](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a), principally by Alan Polk, with John Christman, Jacob Neulight, Nathan Ingraham, Praeuner and other BluePilot, sunnypilot and comma contributors. Exact source paths and intentionally stricter safety checks are recorded in that folder. Both upstream `LICENSE` and `LICENSE.md` notices are retained. Its layout follows Alan Polk's September 2026 library proposal; this is a StarPilot adapter, not a published BluePilot library release.
The radar interface preserves that attribution while adapting to the current interfaces. Upstream contributors do not maintain or endorse this adaptation. Applicable notices are retained in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
## RAV4 Continental ARS510 receiver
@@ -33,3 +29,17 @@ Spotify's Bluetooth output binds the comma's BlueALSA A2DP source to the selecte
## Tesla Pre-AP pedal control
The Pre-AP configuration, pedal codec/feedback, VirtualDAS feedforward table, grade compensation, torque-zero estimator and vehicle-specific gains preserve firestar5683's intentional Dom work, including `7837a3c1aab`, `aed0b7fdb9e9` and pinned `b220a37f4bd119e402225a7aa16a7cd84878d0af`. Original author attribution: firestar5683 <168790843+firestar5683@users.noreply.github.com>. Startup/calibration ownership, current upstream button/PID interfaces and native command bounds are adapted to the current StarPilot architecture; the old permissive native gateway is not transplanted. Existing source/license notices remain intact.
## Subaru angle steering from Jacob Waller's fork
Subaru angle support includes implementation maintained and integrated by **Jacob Waller (jacobw)** in [jacobwaller/openpilot, jul-angle-based](https://github.com/jacobwaller/openpilot/tree/542816196a18dba5f6f0af093c1ece4f415b5a22), with its [opendbc implementation at 8acbeccdab45a47aa7af170d2d17b61f0eb5e4f0](https://github.com/jacobwaller/opendbc/tree/8acbeccdab45a47aa7af170d2d17b61f0eb5e4f0). Jacob authored the VehicleModel angle-limit type correction and integrated the Crosstrek support. The branch also retains the original angle implementation and follow-up work by **lukasloetkolben and elkoled**, alongside comma contributors; their authorship remains theirs.
The inherited source covers Subaru angle platform/firmware definitions, `ES_Status` cruise state, angle controller/CAN integration, vehicle-model limits and native angle enforcement. StarPilot has additional platform-specific limits, driver handoff, discovery and AOL safety guards; this attribution does not claim identical tuning or complete parity with the reference fork. Original source commits, author names, emails and titles are recorded in [the Subaru origin history](opendbc_repo/opendbc/car/subaru/ORIGIN_HISTORY.tsv).
The explicit StarPilot [jacobw Subaru adapter](opendbc_repo/opendbc/car/subaru/JACOBW.md) additionally integrates that branch's low-speed filter, target-first limiting and Crosstrek EPS envelope. Its preserved stricter guards and platform scope are documented alongside the code.
## StarPilot Ford demand tuning
The previous optional StarPilot Controller, formerly in `openpilot/starpilot/car/ford/starpilot_tuning.py`, restored demand tuning from StarPilot `8da0c9d916^`, descended from BluePilot bp-7.0 `e1d051d7ba270261b4455068bd68f1a58db15a4a` through StarPilot `2efe6017bfe14cee4807833e05f9a7f6ba255cdb`. Contributors to that lineage include Alan Polk, John Christman, Jacob Neulight, tonesto7 and Haibin Wen. The current protocol adapter is maintained by StarPilot separately from the pinned BluePilot library; this attribution does not imply BluePilot maintains or endorses the adapted tune.
The current StarPilot Controller replaces that historical curvature-primary tune with a separately maintained adaptation of BluePilot v0.2.1's angle strategy (`2b9dc6cc784faac47a1885d0bda0561f7b76f74d`, `core/lateral_angle_ext.py`). Alan Polk is the principal controller author; John Christman contributed angle-mode anti-stall work, Jacob Neulight the pinion/shadow measurement, Nathan Ingraham and Praeuner high-speed damping, and other contributors are retained in the pinned library's `CREDITS.md`. StarPilot adds bounded small-signal conditioning ahead of the unchanged angle limit chain. This adaptation is not maintained or endorsed by BluePilot; the pinned library and its imported hooks are unchanged.
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+87 -11
View File
@@ -1,16 +1,90 @@
# StarPilot
StarPilot is an independent fork of [openpilot](https://github.com/commaai/openpilot), with custom driving controls, vehicle integrations, raylib interfaces for comma 3 and comma four, and the Galaxy companion interface.
**Domathon is the development branch for StarPilot 7.0.** It rebuilds StarPilot on a current openpilot foundation. Feature migration and vehicle qualification are still in progress; this branch is not a completed replacement for the existing Dom release.
[![Ask DeepWiki](https://img.shields.io/badge/Ask-DeepWiki-blue?logo=readthedocs&logoColor=white)](https://deepwiki.com/StarPilotHQ/StarPilot)
[![Discord](https://img.shields.io/discord/1387432184121393333?label=Discord)](https://firestar.link/discord)
[![Last Updated](https://img.shields.io/github/last-commit/StarPilotHQ/StarPilot/StarPilot)](https://github.com/StarPilotHQ/StarPilot)
[![Wiki](https://img.shields.io/badge/Wiki-StarPilot-blue?logo=wiki)](https://wiki.firestar.link)
[![iOS App Store](https://img.shields.io/badge/iOS-App_Store-black?logo=apple&logoColor=white)](https://apps.apple.com/ca/app/starpilot-bt/id6821039119)
[![Android Beta](https://img.shields.io/badge/Android-Join_Beta-green?logo=android&logoColor=white)](https://groups.google.com/g/galaxy-starpilot-android-beta)
## Explore the project
**StarPilot** is a custom fork of [comma.ai's openpilot](https://comma.ai/openpilot),
an open source driver assistance system.
- [Developer commands](tools/STARPILOT_DEVELOPMENT.md): host tools, replay, and device builds.
- [Driving interfaces](openpilot/starpilot/ui/README.md) and [Galaxy](openpilot/starpilot/galaxy/README.md): native and companion controls.
- [Driving models](openpilot/starpilot/models/README.md): model management, artifact compatibility, and Model Laboratory status.
- [Builds and developer tools](docs/how-to/laptop-device-build.md): local development and device builds.
- [Dependency maintenance](tools/vendor/README.md): tracked source folders and upstream sync records.
**This is the StarPilot 7.0 development branch (`SecretGoodStarPilot`). Feature migration,
vehicle qualification, and Auto Learn validation are still in progress.**
Openpilot provides
* Automated Lane Centering
* Adaptive Cruise Control
* Lane Change Assist
* Driver Monitoring *without wheel nags*
Ford-specific lateral-control and vehicle-support work includes substantial adaptations from
[BluePilot](https://github.com/BluePilotDev/bluepilot/tree/bp-7.0), principally developed by
[Alan Polk](https://github.com/alan-polk) with additional BluePilot contributors. StarPilot's
implementation has since diverged, but that does not erase its lineage. See [CREDITS.md](CREDITS.md)
for the code-level provenance and [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for applicable
upstream notices and terms. BluePilot and its contributors do not maintain or endorse StarPilot;
please direct support requests for this adaptation to the StarPilot project.
Hyundai, Kia, and Genesis angle steering and related vehicle support include substantial adaptations
from [sunnypilot](https://github.com/sunnypilot/sunnypilot/tree/hkg-angle-steering-2025) and its
[opendbc angle-steering branch](https://github.com/sunnypilot/opendbc/tree/hkg-angle-steering-2025).
StarPilot's implementation has diverged significantly; the upstream contributors do not maintain
this adaptation. Detailed code lineage is recorded in [CREDITS.md](CREDITS.md).
StarPilot has a vibrant, welcoming community [discord](https://firestar.link/discord).
Stop by to chat or ask questions!
If your car isn't driving as well as you'd like, [open a ticket so we can help](https://firestar.link/logs)!
## Documentation
Please see [https://wiki.firestar.link](https://wiki.firestar.link) for hardware lists,
installation guides, and software configuration.
## Features
* **Full support for Comma C3, C3X, and C4**
* Full support for chestnut/eGPU on C3, C3X, and C4. C3/C3X may require special gps handling.
* Model switcher with all of comma's tinygrad driving models
* StarPilot custom torque controller
* StarPilot custom longitudinal planner for both radar and VoACC (visual only, radar-less) vehicles
* **Custom tuning for an expanding list of vehicles**
* Firestar Lateral Method -
Auto tuning system to adapt openpilot to *your* car's unique lateral characteristics.
* Galaxy: StarPilot's portal to configure your comma device using your phone from anywhere.
Download models, change settings, update software, diagnostics, manage dashcam video, and more.
* Navigation - Display routes and give the driving model hints on taking exits and turns.
* Always On Lateral (full time steering assist)
* Lane Centering - Enable to center between detected lane lines,
with graceful fallback to model E2E lateral outputs and safety breakout
* Force Stop 2.0 - Better stops at stop signs and traffic lights.
* Speed Limit Controller - Use dashboard, map data, and/or vision detection for speed limit signs
* Curve Speed Controller - Learns from how you take curves
* Conditional Experimental Mode (CEM)
* Conditional Chill Mode (CCM)
* Driving Profiles and Traffic Mode
* Custom themes
* Alert volume customization
* Comma Pedal Interceptor support
* Bluetooth support - Connect keyboards, controllers, steering wheel remotes, and more.
* Sentry mode - Record when motion is detected while parked.
* V-ASM - Detect vehicles in the adjacent/blind spots using the driver-facing camera.
* Additional car support - Toyota SDSU, ZSS, CAN filter; pre-AP tesla; ford; Polestar 2; and more.
* And more!
## GM-only Features
* Increased LKAS fault resiliency
* ASCM_INT and SASCM support
* Custom lateral torque controller, with special tuning for Bolts
* 50% extra torque on 2017 Chevy Bolt
* Improved lateral and longitudinal tuning
* Dashboard cruise control display speed spoofing for vehicles with pedal interceptor
* Extra steering wheel button functionality for vehicles with pedal interceptor
* Optional toggle to boot comma when remote starting your vehicle
## Development
@@ -36,8 +110,10 @@ StarPilot retains upstream vehicle and Panda safety interfaces and adds regressi
Support depends on the vehicle configuration and enabled feature. An existing vehicle identifier or settings page does not by itself establish driving support. Model Laboratory currently exposes management controls, but paired driving inference remains unavailable pending integration and Chestnut validation.
## Upstream and attribution
## Third-Party Notices
The current openpilot baseline is [`78dccf0b482ea1298fd1f6fa59b0554187b02ce6`](https://github.com/commaai/openpilot/commit/78dccf0b482ea1298fd1f6fa59b0554187b02ce6). Its history remains intact, followed by the StarPilot foundation and feature commits. Upstream development continues independently; the baseline changes only after an update has been integrated and checked.
* Portions of this software include modified versions of the Material Design Icons provided by Google under the Apache License 2.0. A copy of the license is included in the `LICENSE-MDI` file.
* Ford support includes software adapted from BluePilot's `bp-7.0` branch. The source repository contains both a standard MIT notice and a separate custom SUNNYPILOT LLC notice; StarPilot preserves both in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
* Hyundai, Kia, and Genesis support includes software adapted directly from sunnypilot's HKG angle-steering branch and sunnypilot/opendbc. StarPilot preserves the applicable notices in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
See [LICENSE](LICENSE), [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md), and [CREDITS.md](CREDITS.md) for licensing and source attribution. Component licenses remain with their source folders. StarPilot is maintained independently of comma.
> This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use.
+17 -16
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)
@@ -318,7 +318,7 @@ SConscript([
if arch == "comma_arm64":
SConscript(['openpilot/system/camerad/SConscript'])
SConscript(['openpilot/starpilot/system/android_auto/SConscript'])
SConscript(['openpilot/starpilot/system/starport/SConscript'])
# Build selfdrive
SConscript([
@@ -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.
+23 -12
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`.
@@ -73,17 +84,7 @@ the exclusion of the other.
## BluePilot Ford support
The Ford lateral strategies, CAN constructors, camera-radar filtering and native checks include work adapted from BluePilot's `bp-7.0` source, through StarPilot `2efe6017bfe14cee4807833e05f9a7f6ba255cdb`. See [CREDITS.md](CREDITS.md#ford-support-adapted-from-bluepilot) for contributors and source revisions. The upstream extension-file and `LICENSE.md` notices reproduced above also apply to the Ford extension sources; they are retained without resolving the difference between their published license descriptions.
## Optional BluePilot angle library adapter
The angle-only adapter in `opendbc_repo/opendbc/bluepilot_lateral` is sourced directly from BluePilot bp-7.0 commit e1d051d7ba270261b4455068bd68f1a58db15a4a. The complete upstream MIT notice is retained in that folder's `LICENSE`, and the simultaneously published Custom MIT notice is retained verbatim in `LICENSE.md`. The applicable SUNNYPILOT acknowledgments reproduced above are also retained in its `CREDITS.md`. This preserves the published terms without resolving their scope or representing the adapted extension as unambiguously plain MIT.
The optional, default-disabled angle lane-centering trim additionally derives from
BluePilot bp-dev e22afa6be9b881fa784c92ebb316db47728a3d81. Its exact source and
contributors are recorded in the library CREDITS.md; the same root/opendbc MIT
and Custom MIT notices coexist unchanged at that revision. This adds attribution,
not a license-scope decision or permission grant.
The Ford CAN FD camera-radar filtering includes work adapted from BluePilot's `bp-7.0` source, through StarPilot `2efe6017bfe14cee4807833e05f9a7f6ba255cdb`. See [CREDITS.md](CREDITS.md#ford-support-adapted-from-bluepilot) for contributors and source revisions. The upstream extension-file and `LICENSE.md` notices reproduced above also apply to that source; they are retained without resolving the difference between their published license descriptions.
## Continental ARS510 radar
@@ -129,7 +130,7 @@ UI framework is bundled.
## Galaxy Bluetooth companion source snapshot
The Galaxy Bluetooth phone bridge and iOS app from https://github.com/firestar5683/StarPilot
The Galaxy Bluetooth phone bridge and iOS app from https://github.com/StarPilotHQ/StarPilot
(natehuby76, PRs #224/#225) are included under `openpilot/starpilot/galaxy_bluetooth/`
and `openpilot/starpilot/companion/ios/`, with their license files (`UPSTREAM-LICENSE`, `openpilot/starpilot/companion/ios/UPSTREAM-GALAXY-LICENSE`).
The bridge runs on openpilot's Python with PyCryptodome 3.23.0 (BSD/public-domain
@@ -140,3 +141,13 @@ dbus-next 0.2.3 (MIT), ifaddr 0.2.0 (MIT) and Zeroconf 0.151.3
`vendor/VENDOR.json` records each PyPI source file and its SHA-256. Zeroconf is
kept as separate, unmodified source files that can be replaced with another
compatible version, as the LGPL requires.
## Subaru angle support from jacobw
The Subaru angle implementation includes work from Jacob Waller's `jul-angle-based` openpilot/opendbc branches and their upstream contributors. See [CREDITS.md](CREDITS.md#subaru-angle-steering-from-jacob-wallers-fork) and `opendbc_repo/opendbc/car/subaru/ORIGIN_HISTORY.tsv` for pinned sources, original authors and titles. The source repositories publish the comma MIT license; the complete applicable notice remains in the project `LICENSE` and `opendbc_repo/LICENSE`. StarPilot's adaptations preserve additional native and caller safety guards.
## StarPilot Ford demand tuning
The historical tuning formerly in `openpilot/starpilot/car/ford/starpilot_tuning.py` included the BluePilot bp-7.0-derived work identified in [CREDITS.md](CREDITS.md#starpilot-ford-demand-tuning). The published upstream license notices retained above and in the pinned BluePilot library remain applicable to the adapted source. StarPilot's separate protocol adapter does not modify those notices or resolve their scope.
The angle-primary adaptation in `openpilot/starpilot/car/ford/starpilot_controller.py` is derived from BluePilot v0.2.1. It retains the complete MIT copyright and permission notice in `opendbc_repo/opendbc/bluepilot_lateral/LICENSE`, including Comma.ai, BluePilot and StarPilot contributors. See the contributor attribution in `CREDITS.md`.
+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.
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PYTHON_BIN="${HEATDEATH_PYTHON:-python3}"
if [[ -z "${HEATDEATH_PYTHON:-}" && -x /usr/local/venv/bin/python3 ]]; then
PYTHON_BIN=/usr/local/venv/bin/python3
fi
exec "$PYTHON_BIN" -B "$ROOT_DIR/tools/heatdeath/heatdeath.py" "$@"
+1 -1
View File
@@ -10,4 +10,4 @@ jetlink: run openpilot's large driving models on an attached Jetson.
# so does the release check (macos/scripts/check-version.sh). The comma runs
# jetlink from a checkout, not an install, so this is also all it has. The Swift
# reads it from Pinned.swift: run JetlinkKit/Scripts/make_pins.py after a bump.
__version__ = '0.8.7'
__version__ = '0.8.12'
+20 -6
View File
@@ -224,12 +224,16 @@ class JetlinkClient:
# -- handshake ------------------------------------------------------------
def hello(self, timeout: float = 5.0) -> dict:
def hello(self, timeout: float = 5.0, pick: dict | None = None) -> dict:
"""Introduce this client. The server starts its session over on a hello,
so this is also how a new owner of the gadget takes over one the server
never saw end; see Session.greet in JetlinkServer."""
never saw end; see Session.greet in JetlinkServer. `pick` is the comma's
own big-model pick and its age, {ref, sha256, nbytes, name, age_s}; the
answer's `pick` is the newer of it and the server's (PickStore)."""
seq = self._next_seq()
client = {'nonce': self.nonce, 'name': self.name}
if pick is not None:
client['pick'] = pick
try:
link = self.t.link_info()
except Exception:
@@ -301,7 +305,8 @@ class JetlinkClient:
frame_skip: int = DEFAULT_FRAME_SKIP,
progress: ProgressFn | None = None,
build_timeout: float = 900.0,
should_stop: StopFn | None = None) -> ModelSpec:
should_stop: StopFn | None = None,
fetch: bool = False, ref: str | None = None) -> ModelSpec:
"""Make the server ready to run this model, uploading and building if needed.
Blocks until ready or the build fails, and returns the spec the server
@@ -309,8 +314,12 @@ class JetlinkClient:
parser rejects an export (tinygrad and the org.tinygrad domain) still runs.
`onnx_path` None means the caller cannot upload (modeld) and a missing
engine is EngineMissing. `progress(stage, frac, msg)` has stage in
upload/patch/parse/build/load. `should_stop` is polled during long waits.
engine is EngineMissing. `fetch` asks a server that lacks the model to
download it itself (state downloading, stage download) rather than answer
need_upload; one that cannot, or is too old to, answers need_upload as
before. `ref` is the catalog ref the model is known by.
`progress(stage, frac, msg)` has stage in
download/upload/patch/parse/build/load. `should_stop` is polled during long waits.
"""
self.progress_cb = progress
self._should_stop = should_stop or (lambda: False)
@@ -318,7 +327,12 @@ class JetlinkClient:
self._engine_state = None
seq = self._next_seq()
self.t.send_json(P.Msg.ENGINE_REQ, seq, {'sha256': sha256, 'nbytes': nbytes, 'frame_skip': frame_skip})
request = {'sha256': sha256, 'nbytes': nbytes, 'frame_skip': frame_skip}
if fetch:
request['fetch'] = True
if ref:
request['ref'] = ref
self.t.send_json(P.Msg.ENGINE_REQ, seq, request)
resp = json.loads(bytes(self._expect(P.Msg.ENGINE_RESP, seq, 60.0).payload))
self._engine_state = resp
log.info("server engine state: %s (%s)", resp['state'], resp.get('detail', ''))
+1 -1
View File
@@ -55,7 +55,7 @@ POLL = 0.5
# what a borrower passes on of the server's hello (Loan.note_server): the owner
# never speaks the protocol, and sleep_after is how it knows whether letting
# go of the gadget lets the Jetson sleep. The rest is for its status record
SERVER_FIELDS = ('protocol', 'device', 'backend', 'runtime_version', 'trt_version', 'sleep_after')
SERVER_FIELDS = ('protocol', 'device', 'backend', 'runtime_version', 'trt_version', 'sleep_after', 'pick', 'fetches')
def _send(conn: socket.socket, msg: dict) -> None:
+36 -3
View File
@@ -45,15 +45,23 @@ class Keys:
offroad: str # BOOL: is the car parked (manager's IsOffroad)
progress: str # JSON {stage, frac, msg, drops}: provisioning and join progress
spec: str # JSON: the built model's spec and whether its engine is built
pointers: str # JSON: catalog ref -> {oid, size}
pointers: str # JSON: catalog ref, or a hash-named pick's sha256 -> {oid, size}
# sunnypilot's model manager: the big-model pick, JSON {ref, displayName},
# and the big-model catalog, JSON {bundles}. Without one both are None, and
# jetlink runs its own default big model
# jetlink runs its own default big model, or the one picked on the server.
# A fork with a picker of its own and no catalog (StarPilot) declares only
# big_model, and may name its pick by the ONNX's hash, {sha256,
# displayName}; a newer pick made on the server is written into it
big_model: str | None = None
catalog: str | None = None
# BOOL: an iPhone on a direct cable is asked to charge from the comma
# (comma.port). Off when unset, and without the key
charge_phone: str | None = None
# STRING: where the model manager takes a pick by its ref and stores it in
# big_model (sunnypilot's ModelManager_DownloadRef). A newer pick made on
# the server is written there, so the comma's own list shows it; without
# the key it drives all the same
select: str | None = None
@dataclass(frozen=True)
@@ -152,6 +160,16 @@ class ModelSide(WorkerSide, Protocol):
frame thread at a hand-back, so it zeroes the history in place and
allocates and compiles nothing; warp.reset_queues makes one."""
def model_warp(self, small: Any, cam_w: int, cam_h: int, model_w: int, model_h: int) -> FrameWarp | None:
"""Optional (OPTIONAL): the warp the link sends frames through, warmed,
or None for openpilot's driving warp pickle at warp_path (warp.Warp). For
a fork whose runtime cannot load that pickle, typically with its small
model's own warp. Asked once on modeld's main thread before the frame
loop, after the small model's warmup; raising keeps the drive on the
small model, as a pickle that does not load does. The offroad checks
still look for the pickle (Warps.built): only modeld has a small model
to ask, so warp_path must name a file either way."""
class Openpilot(ModelSide, Protocol):
"""The whole adapter: one object implements every side."""
@@ -198,13 +216,28 @@ class JoinedModel(Protocol):
"""Let go of the link at modeld's exit."""
class FrameWarp(Protocol):
"""What an adapter's model_warp returns, warp.Warp's shape, run on modeld's
frame thread. Only one of the small and the large model runs a frame, so a
warp that shares the small model's JIT and buffers is never in use by both.
On a comma, a warp that reads its GPU output back moves it first with
warp.make_coherent, before the warp's JIT first runs."""
output: Any # the warped frame, (2, 6, model_h/2, model_w/2) uint8 as bytes: read after wait(), until the next start()
def start(self, frame: int, big_frame: int, tfm: Any, big_tfm: Any) -> None:
"""Warp the camera buffers at these addresses under their 3x3 transforms."""
def wait(self) -> None:
"""Return once `output` holds what start() warped."""
# The members of each protocol an implementation may lack: jetlink reads them
# with getattr and a fallback. conformance() passes an object without one and
# holds one with it to its signature, and adding one keeps API. Kept out of
# the protocols, where a name would be a member itself; a type checker still
# asks for these, as typing has no optional member
OPTIONAL: dict[type, tuple[str, ...]] = {
ModelSide: ('small_reset',),
ModelSide: ('small_reset', 'model_warp'),
SmallModel: ('desire_key', 'numpy_inputs', 'lat_delay', 'PLANPLUS_CONTROL'),
JoinedModel: ('get_action_from_model', 'constants', 'LAT_SMOOTH_SECONDS', 'LONG_SMOOTH_SECONDS', 'lat_delay'),
}
+8 -2
View File
@@ -334,7 +334,7 @@ class JoiningModelState:
self._demote(BEHIND)
elif self._active is not self._small and self._frame_drop_ratio > DROP_LIMIT:
# modeld is behind the large model; this frame is the small model's
self._log.warning("jetlink: modeld dropped %.2f %% of camera frames behind the large model, "
self._log.warning("jetlink: modeld dropped %.2f %% of camera frames behind the large model, " +
"the small model drives from this one", self._frame_drop_ratio * 100)
self._demote(BEHIND)
self._maybe_swap()
@@ -745,6 +745,7 @@ def join(parts, cam_w: int, cam_h: int, small) -> JoiningModelState:
link = links.Link(parts.log)
def prepare():
from jetlink.openpilot.interface import FrameWarp, conformance
from jetlink.openpilot.warp import Warp, prepare_reset
# the gadget first, so the Jetson enumerates while the warp loads. Left to
# the join thread the bind lands ~3 s later, behind the small model's
@@ -759,7 +760,12 @@ def join(parts, cam_w: int, cam_h: int, small) -> JoiningModelState:
try:
# the fork's own reset of its small model, else jetlink's from its shape
ready['reset_small'] = getattr(op, 'small_reset', lambda small: None)(small) or prepare_reset(small)
warp = Warp(parts.warps.load(cam_w, cam_h, *geometry), face.frame_size(cam_w, cam_h))
# the fork's own warp, else openpilot's driving warp pickle
warp = getattr(op, 'model_warp', lambda *args: None)(small, cam_w, cam_h, *geometry)
if warp is None:
warp = Warp(parts.warps.load(cam_w, cam_h, *geometry), face.frame_size(cam_w, cam_h))
elif problems := conformance(warp, FrameWarp):
raise RuntimeError(f"the adapter's model_warp is not a warp: {', '.join(problems)}")
except Exception:
link.close()
raise
+25 -11
View File
@@ -43,6 +43,8 @@ PRESENT_TIMEOUT = 5.0
# hardware; the ceiling is only there so a server that has stopped answering
# does not hold the caller for the rest of the day.
BUILD_TIMEOUT = 1800.0
# and on top of that, a server downloading the model itself: 1.75 GB at 1 MB/s
FETCH_TIMEOUT = 1800.0
class ModelMissing(RuntimeError):
@@ -226,12 +228,15 @@ def identity(parts, entry: dict) -> tuple[str, int]:
"""The picked model's sha256 and byte count, from the catalog model's LFS
pointer, so the comma can name the model without holding or hashing the
ONNX. Looked up once per model ever and kept in a param: the one part of
provisioning that needs the internet."""
provisioning that needs the internet. A model picked by its hash alone is
only sized."""
sha256, nbytes = entry.get('oid'), entry.get('size')
if sha256 and nbytes:
return sha256, int(nbytes)
parts.progress.report('connect', 0.0, 'finding model', code='finding_model')
return parts.models.resolve_pointer(entry['ref'])
if entry.get('ref'):
return parts.models.resolve_pointer(entry['ref'])
return parts.models.resolve_size(sha256)
# What has already been hashed in this process, keyed on the file as it was
@@ -278,21 +283,27 @@ def verified_upload(log, model_path: Path | None, sha256: str, nbytes: int) -> P
def ensure(parts, client, sha256: str, nbytes: int, model_path: Path | None, *,
progress=None, should_stop=None, build_timeout: float = BUILD_TIMEOUT):
progress=None, should_stop=None, build_timeout: float = BUILD_TIMEOUT, ref: str | None = None,
fetch: bool = True):
"""Make the server ready for this model and remember what it answered.
Asks without the file first: the server answers from the sha alone when it
has the model, which is every parked poll and every join. EngineMissing means
the Jetson has neither the plan nor the bytes, and neither has the caller."""
has the model, which is every parked poll and every join. Without the file
here either, the server is asked to download the model itself, which only
the Jetson needs. EngineMissing means the Jetson has neither the plan nor
the bytes and could not get them, and neither has the caller. `fetch` False
never asks for the download: a stand-in is a model the server has. `ref`,
the catalog's name for the model, goes with a download."""
from jetlink.client import EngineMissing
ask = functools.partial(client.ensure_engine, sha256, nbytes, progress=progress,
build_timeout=build_timeout, should_stop=should_stop)
fetch = fetch and model_path is None
ask = functools.partial(client.ensure_engine, sha256, nbytes, progress=progress, should_stop=should_stop)
try:
spec = ask(onnx_path=None)
spec = ask(onnx_path=None, fetch=fetch, ref=ref if fetch else None,
build_timeout=build_timeout + (FETCH_TIMEOUT if fetch else 0.0))
except EngineMissing:
upload = verified_upload(parts.log, model_path, sha256, nbytes)
if upload is None:
raise
spec = ask(onnx_path=upload)
spec = ask(onnx_path=upload, build_timeout=build_timeout)
parts.spec.store(spec)
return spec
@@ -346,11 +357,14 @@ def open_link(parts, link: Link, should_stop=None):
# booting; both resolve on their own
client = connect_patiently(link)
try:
hello = client.hello(timeout=10.0)
hello = parts.models.heard(client.hello(timeout=10.0, pick=parts.models.hello_pick()))
parts.log.warning("jetlink: %s trt %s, engine %s, loaded %s",
hello.get('device'), hello.get('trt_version'),
hello.get('engine_state'), str(hello.get('loaded'))[:16])
link.note_server(hello)
# a newer pick on the server drives, and is ours from now on
parts.models.adopt()
selected = parts.models.selected_model() or selected
sha256, nbytes = identity(parts, selected)
path = parts.models.shipped_model_path()
built = parts.spec.ready_spec()
@@ -370,7 +384,7 @@ def open_link(parts, link: Link, should_stop=None):
# server that restarted reloads from the plan cache, 13 to 25 s; one
# that has never seen this model builds it, 102 to 294 s
spec = ensure(parts, client, sha256, nbytes, path, progress=parts.progress.report_with_eta,
should_stop=should_stop)
should_stop=should_stop, ref=selected.get('ref'), fetch=standin is None)
client.deadline = INFERENCE_TIMEOUT
return client, spec
except EngineMissing as e:
@@ -157,9 +157,9 @@ class JetlinkModelState:
# model manager key off this flag
self.chestnut = True
# a warp.Warp, loaded and warmed ahead: its first call links the JIT, and
# this runs on modeld's frame thread. Frames go out of its output as they
# are, over either transport
# a warp.Warp or the adapter's model_warp, loaded and warmed ahead: its
# first call links the JIT, and this runs on modeld's frame thread. Frames
# go out of its output as they are, over either transport
self.warp = warp
self.input_shapes = spec.input_shapes
@@ -305,9 +305,9 @@ class JetlinkModelState:
# over the cable: how frames go, how many never came back, and what the
# kernel dropped on the way in (softnet), since the link came up
drops = getattr(self.client.t, 'net_drops', lambda: None)()
cable = (f"; frames {'as datagrams' if self.client.t.datagrams else 'on the stream'}, "
cable = (f"; frames {'as datagrams' if self.client.t.datagrams else 'on the stream'}, " +
f"{self.client.frames_lost} lost, net drops {drops}" if drops is not None else '')
self._log.warning("jetlink: frame %d warp %.1f data %.1f send %.1f wait %.1f ms%s; "
self._log.warning("jetlink: frame %d warp %.1f data %.1f send %.1f wait %.1f ms%s; " +
"server gpu %.1f queue %.1f total %.1f ms; ffs maxima prepare %.1f read_wait %.1f handoff %.1f ms%s",
self._frame_id, (frame.t1 - frame.t0) * 1e3, (frame.t2 - frame.t1) * 1e3,
(frame.sent - frame.t2) * 1e3, (t4 - waiting_from) * 1e3,
+196 -28
View File
@@ -16,16 +16,25 @@ export, and jetlink's registry follows that to comma's model repo; see
jetlink.registry.lfs.
Without a model manager (Keys.big_model and Keys.catalog None) there is no
pick and no catalog, and the large model is jetlink's own default, named by its
ref: its pointer resolves from the ref as any pick's does.
pick and no catalog, and the large model is the one picked on the server, else
jetlink's own default, named by its ref: its pointer resolves from the ref as
any pick's does.
A fork with a picker of its own but no catalog jetlink reads (Keys.big_model
without Keys.catalog, StarPilot's) may name its pick by the ONNX's sha256
instead of a ref, which is all its catalog knows of a model. Its size is looked
up from comma's LFS servers once (resolve_size). With no catalog no model
manager owns the slot either, so jetlink writes a newer pick from the server
into it itself, and the fork's picker shows what drives.
"""
from __future__ import annotations
import os
import re
import time
from pathlib import Path
from jetlink.registry.catalog import DEFAULT_BIG_MODEL_NAME, DEFAULT_BIG_MODEL_REF
from jetlink.registry.catalog import DEFAULT_BIG_MODEL_NAME, DEFAULT_BIG_MODEL_REF, is_ref, is_sha256
POINTER_TIMEOUT = 10.0
_REF = re.compile(r'[0-9a-f]{40}')
@@ -39,13 +48,32 @@ INDEX_TTL = 2.0
# on from the .part the last left, so a stall costs the wait, not the bytes
# (lfs.lfs_download)
DOWNLOAD_RETRY_DELAYS = (5.0, 15.0, 30.0, 60.0, 120.0)
# server_pick() before this process has heard a hello
_UNHEARD = object()
def _dated(pick) -> dict | None:
"""A server's pick as its hello carries it, dated by this clock:
{ref, sha256, nbytes, name, at}. None for none, or one naming no model."""
if not isinstance(pick, dict) or not isinstance(pick.get('age_s'), (int, float)):
return None
ref = pick['ref'] if is_ref(pick.get('ref')) else None
sha256 = pick['sha256'] if is_sha256(pick.get('sha256')) else None
if ref is None and sha256 is None:
return None
return {'ref': ref, 'sha256': sha256, 'nbytes': pick.get('nbytes'), 'name': pick.get('name'),
'at': time.time() - float(pick['age_s'])}
class Models:
def __init__(self, op):
self.op = op
self._index_cache: tuple[float, list[dict], dict[str, dict]] | None = None
self._slot_cache: tuple[float, dict | None] | None = None
self._slot_cache: tuple[float, dict | None, float | None] | None = None
# the pick of the last hello this process heard, and the owner's record
# of the last hello any process heard
self._heard: dict | None | object = _UNHEARD
self._record_cache: tuple[float, dict | None] | None = None
def catalog(self) -> list[dict]:
"""sunnypilot's big-model bundles as {name, ref}, newest first, as the model
@@ -77,13 +105,28 @@ class Models:
def resolve_pointer(self, ref: str) -> tuple[str, int]:
"""The oid and size behind a catalog model, fetched the first time and kept for good."""
return self._resolve(ref, self.fetch_pointer)
def fetch_size(self, sha256: str) -> tuple[str, int]:
"""The ONNX with this hash and its byte count, from the LFS servers a
download asks."""
from jetlink.registry.lfs import fetch_size as registry_fetch_size
pointer = registry_fetch_size(sha256, self.lfs_endpoints(), timeout=POINTER_TIMEOUT)
return pointer.oid, pointer.size
def resolve_size(self, sha256: str) -> tuple[str, int]:
"""A model picked by its hash, sized the first time and kept for good with
the catalog's pointers, under the hash."""
return self._resolve(sha256, self.fetch_size)
def _resolve(self, key: str, fetch) -> tuple[str, int]:
known = self.pointers()
if ref in known:
return known[ref]['oid'], int(known[ref]['size'])
oid, size = self.fetch_pointer(ref)
known[ref] = {'oid': oid, 'size': size}
if key in known:
return known[key]['oid'], int(known[key]['size'])
oid, size = fetch(key)
known[key] = {'oid': oid, 'size': size}
self._keep_pointers(known)
self.op.log.warning("jetlink: %s is %s, %d MB", ref[:10], oid[:16], size >> 20)
self.op.log.warning("jetlink: %s is %s, %d MB", key[:10], oid[:16], size >> 20)
return oid, size
def _keep_pointers(self, known: dict[str, dict]) -> None:
@@ -92,9 +135,11 @@ class Models:
self._index_cache = None
@staticmethod
def _row(name: str, ref: str, known: dict[str, dict]) -> dict:
p = known.get(ref) or {}
return {'name': name, 'ref': ref, 'oid': p.get('oid'), 'size': int(p['size']) if p.get('size') else None}
def _row(name: str, ref: str | None, known: dict[str, dict], sha256: str | None = None) -> dict:
"""A model as a catalog row, by its ref's pointer, else by its hash, sized
once resolve_size has looked it up."""
p = known.get(ref) or known.get(sha256) or {}
return {'name': name, 'ref': ref, 'oid': p.get('oid') or sha256, 'size': int(p['size']) if p.get('size') else None}
def _index(self) -> tuple[list[dict], dict[str, dict]]:
"""The catalog's rows and the pointers behind them, read together once per INDEX_TTL."""
@@ -110,18 +155,27 @@ class Models:
return self._index()[0]
def selected_slot(self) -> dict | None:
"""The big-model slot's pick as {name, ref}, from the raw param rather than a
parsed bundle, and memoised: the UI asks every frame."""
"""The big-model slot's pick as {name, ref, sha256}, from the raw param
rather than a parsed bundle, and memoised with when it was made
(picked_at): the UI asks every frame. It names its model by the catalog's
ref, by the ONNX's sha256 (a fork whose catalog knows only that), or by
both; the one it lacks is None."""
now = time.monotonic()
if self._slot_cache is not None and now - self._slot_cache[0] < INDEX_TTL:
return self._slot_cache[1]
key = self.op.keys.big_model
slot = self.op.get(key) if key else None
ref = slot.get('ref') if isinstance(slot, dict) else None
pick = None
if isinstance(ref, str) and ref:
pick = {'name': str(slot.get('displayName') or ref[:10]), 'ref': ref}
self._slot_cache = (now, pick)
slot = slot if isinstance(slot, dict) else {}
ref = slot['ref'] if isinstance(slot.get('ref'), str) and slot['ref'] else None
sha256 = slot['sha256'] if is_sha256(slot.get('sha256')) else None
pick, at = None, None
if ref or sha256:
pick = {'name': str(slot.get('displayName') or (ref or sha256)[:10]), 'ref': ref, 'sha256': sha256}
try:
at = os.stat(Path(self.op.params_dir()) / key).st_mtime
except OSError:
pass
self._slot_cache = (now, pick, at)
return pick
def default_model(self) -> dict | None:
@@ -142,12 +196,129 @@ class Models:
return _TRAILING_DATE.sub('', model['name']) if model else None
def selected_model(self) -> dict | None:
"""The slot's pick, listed in the catalog or not: a ref is enough to find its
pointer. With no pick, default_model()."""
if (pick := self.selected_slot()) is not None:
return self._row(pick['name'], pick['ref'], self._index()[1])
"""The model picked last, here or on the server: the slot's pick, listed
in the catalog or not (a ref is enough to find its pointer), unless the
server's last hello named a newer one. With neither, default_model()."""
if (theirs := self._newer_server_pick()) is not None:
return self._server_row(theirs)
if (own := self.selected_slot()) is not None:
return self._own_row(own)
return self.default_model()
def _own_row(self, own: dict) -> dict:
return self._row(own['name'], own['ref'], self._index()[1], own['sha256'])
# -- picks on both ends -----------------------------------------------------
# A pick is made on either end: in the comma's list, or on the server (its
# web page, an app's Use Model). Each hello carries the comma's own pick and
# how long ago it was made, and the server answers with the newer of that
# and its own (PickStore in JetlinkServer). The answer is kept with the rest
# of the hello (gadget.SERVER, through the owner), dated by this clock, and
# selected_model() takes whichever pick is newer; so a model picked on the
# server drives without being picked here too, and one picked here goes to
# the server at the next hello.
def picked_at(self) -> float | None:
"""When the slot was last written, by this clock: when the pick was made.
None with no pick of our own, or no date for it."""
return self._slot_cache[2] if self.selected_slot() is not None and self._slot_cache else None
def hello_pick(self) -> dict | None:
"""Our own pick, for the hello: {ref, sha256, nbytes, name, age_s}, the
pointer where it is resolved already (no network here). None with nothing
picked here, or no date for it: the server's pick, if any, stands."""
own, at = self.selected_slot(), self.picked_at()
if own is None or at is None:
return None
row = self._own_row(own)
return {'ref': row['ref'], 'sha256': row['oid'], 'nbytes': row['size'], 'name': row['name'],
'age_s': round(max(0.0, time.time() - at), 1)}
def heard(self, hello: dict) -> dict:
"""The server's hello with its `pick` dated by this clock (_dated), which
is how the owner keeps it (lending.SERVER_FIELDS) and server_pick() reads
it. This process goes by it at once, whatever the owner does with it."""
self._heard = _dated(hello.get('pick'))
return {**hello, 'pick': self._heard} if self._heard is not None else hello
def server_pick(self) -> dict | None:
"""The pick the server last answered a hello with, {ref, sha256, nbytes,
name, at}, or None: a server too old to say, or one with no pick. What
this process heard last, else the owner's record of the last hello: the
UI says no hello, and reads it every frame."""
if self._heard is not _UNHEARD:
return self._heard
now = time.monotonic()
if self._record_cache is None or now - self._record_cache[0] >= INDEX_TTL:
from jetlink.comma import gadget
pick = (gadget.read_record(gadget.SERVER) or {}).get('pick')
self._record_cache = (now, pick if isinstance(pick, dict) and isinstance(pick.get('at'), (int, float)) else None)
return self._record_cache[1]
def _newer_server_pick(self) -> dict | None:
"""The server's last pick, when it is another model than ours and was
picked after it, or we have none."""
theirs, own = self.server_pick(), self.selected_slot()
if theirs is None or (own is not None and (self._same(theirs, self._own_row(own)) or
theirs['at'] <= (self.picked_at() or 0.0))):
return None
return theirs
@staticmethod
def _same(pick: dict, row: dict) -> bool:
"""One model: by the catalog ref where both have one, since two refs can
share one export, else by the hash. The server's PickStore.sameModel."""
if pick['ref'] and row['ref']:
return pick['ref'] == row['ref']
return bool(pick['sha256']) and pick['sha256'] == row['oid']
def _server_row(self, pick: dict) -> dict:
"""The server's pick as a catalog row: ours where we list it, with the
server's oid and size, which name the bytes it has."""
listed = next((m for m in self.model_index() if pick['ref'] and m['ref'] == pick['ref']), None)
row = self._row(listed['name'] if listed else pick.get('name') or (pick['ref'] or pick['sha256'])[:16],
pick['ref'], self._index()[1], pick['sha256'])
if pick['sha256'] and pick.get('nbytes'):
row['oid'], row['size'] = pick['sha256'], int(pick['nbytes'])
return row
def adopt(self) -> None:
"""Keep the server's pointer for its pick, by ref and by hash, so naming
the model here needs no lookup; and make a newer pick from the server
ours. With a catalog, ask the model manager for it (Keys.select), so the
comma's own list shows what drives: only a model the catalog lists, which
is all the model manager takes; any other drives all the same
(selected_model). Without one, no model manager owns the slot, so the
pick is written into it, whatever the model. Never raises."""
try:
if (theirs := self.server_pick()) is None:
return
known = self.pointers()
if theirs['sha256'] and theirs.get('nbytes'):
pointer = {'oid': theirs['sha256'], 'size': int(theirs['nbytes'])}
missing = {k: pointer for k in (theirs['ref'], theirs['sha256']) if k and k not in known}
if missing:
self._keep_pointers({**known, **missing})
if self._newer_server_pick() is None:
return
model, keys = self._server_row(theirs), self.op.keys
if keys.catalog is None:
if keys.big_model:
self.op.log.warning("jetlink: %s was picked on the server, picking it here too", model['name'])
slot = {k: v for k, v in (('displayName', model['name']), ('ref', model['ref']), ('sha256', model['oid'])) if v}
self.op.put(keys.big_model, slot, block=True)
self._slot_cache = None
return
# a download the user asked for that the model manager has not taken
# yet is left alone: the next hello asks again
key = keys.select
if key and model['ref'] and self.op.get(key) is None and any(m['ref'] == model['ref'] for m in self.model_index()):
self.op.log.warning("jetlink: %s was picked on the server, picking it here too", model['name'])
self.op.put(key, model['ref'])
self._slot_cache = None
except Exception:
self.op.log.exception("jetlink: could not take the server's pick")
def selected_model_name(self) -> str | None:
selected = self.selected_model()
return selected['name'] if selected else None
@@ -227,11 +398,8 @@ class Models:
def _fetch_once(self, pointer, dest: Path, progress, should_stop) -> Path:
from jetlink.registry.catalog import NetworkError
from jetlink.registry.lfs import lfs_download, lfs_resolve
for endpoint in self.lfs_endpoints():
href = lfs_resolve(endpoint, pointer)
if href is None:
continue
from jetlink.registry.lfs import lfs_download, lfs_hrefs
for endpoint, href in lfs_hrefs(self.lfs_endpoints(), pointer):
self.op.log.warning("jetlink: fetching the large model (%d MB) from %s", pointer.size >> 20, endpoint)
return lfs_download(href, pointer, dest, progress=progress, should_stop=should_stop)
raise NetworkError(f"no LFS server has {pointer.oid[:16]}")
+53 -32
View File
@@ -126,10 +126,8 @@ class ProvisioningRun:
return path
def needs_download(self) -> bool:
"""Does the comma have to fetch the picked model before a Jetson can build
it? Only while its engine is not known to be built and the file is not here.
Asked without the Jetson: a download needs only the internet, and a car
whose Jetson goes off with the ignition has no Jetson while parked."""
"""Could the Jetson need the picked model from the comma? Only while its
engine is not known to be built and the file is not here."""
parts = self.parts
entry = parts.models.selected_model()
if entry is None:
@@ -140,12 +138,17 @@ class ProvisioningRun:
return parts.models.shipped_model_path() is None
def provision(self) -> bool:
"""Make the Jetson ready for the selected model; a host is attached. run()
has fetched the file already unless the spec record said the model was
built; a server that lost it since asks for the bytes, and they are
fetched then."""
"""Make the Jetson ready for the picked model; a host is attached. The
hello settles which pick that is, the comma's or a newer one made on the
server. A server without the model downloads it itself; only one that
cannot (no internet, or too old) gets the bytes from the comma, fetched
here then."""
from jetlink.client import EngineMissing
parts = self.parts
parts.progress.report('connect', 0.0, 'connecting', code='connecting')
hello = self.hello()
self.log.warning("jetlink: server %s trt %s", hello.get('device'), hello.get('trt_version'))
parts.models.adopt()
entry = parts.models.selected_model()
if entry is None:
# no catalog yet; not an error
@@ -154,27 +157,24 @@ class ProvisioningRun:
return False
sha256, nbytes = link.identity(parts, entry)
# only needed if the server turns out not to have this model; None is a
# legitimate state here, see EngineMissing below
# only needed if the server turns out not to have this model and cannot
# get it; None is a legitimate state here, see EngineMissing below
model_path = parts.models.shipped_model_path()
self.log.warning("jetlink: provisioning %s (%d MB, sha %s)",
entry.get('name', sha256[:16]), nbytes >> 20, sha256[:16])
parts.progress.report('connect', 0.0, 'connecting', code='connecting')
def ensure(path):
return link.ensure(parts, self.client, sha256, nbytes, path, progress=parts.progress.report_with_eta,
should_stop=lambda: self.stop)
should_stop=lambda: self.stop, ref=entry.get('ref'))
hello = self.hello()
self.log.warning("jetlink: server %s trt %s", hello.get('device'), hello.get('trt_version'))
try:
spec = ensure(model_path)
except EngineMissing:
# the server has nothing to build from, and neither have we: fetch the
# model and hand it over in this run. The owner lends the link for the
# whole run; leaving after the download would put the build a
# WORKER_BACKOFF later
# the server has nothing to build from and could not download it, and
# neither have we: fetch the model and hand it over in this run. The
# owner lends the link for the whole run; leaving after the download
# would put the build a WORKER_BACKOFF later
if model_path is not None or (model_path := self.fetch_model()) is None:
raise
# a download of minutes can outlast the session; a hello starts a new one
@@ -186,16 +186,35 @@ class ProvisioningRun:
return True
def hello(self) -> dict:
"""Say hello, and pass the answer on to the owner, which needs its
sleep_after to decide whether letting go of the gadget is worth what it
costs and never speaks the protocol to learn it."""
hello = self.client.hello(timeout=10.0)
"""Say hello with our pick, and pass the answer on to the owner, which
needs its sleep_after to decide whether letting go of the gadget is worth
what it costs, never speaks the protocol to learn it, and keeps the
server's pick and whether it downloads for the comma (Models.heard)."""
models = self.parts.models
hello = models.heard(self.client.hello(timeout=10.0, pick=models.hello_pick()))
if self.loan is not None:
self.loan.note_server(hello)
return hello
# -- the parked car -------------------------------------------------------
def without_jetson(self) -> None:
"""No Jetson came to the bind: a car whose Jetson is switched with the
ignition has none while parked. Only the Jetson needs the model, and one
that can download it gets it itself when it is on; the comma fetches it
now only for a Jetson that cannot (no internet when it was last here, or
too old to say), so the build can start the moment it is."""
if not self.has_work():
return
if self.needs_download():
if server_fetches():
self.parts.progress.report('waiting', 0.0, 'the jetson downloads it when it is on', code='waiting_for_host')
return
if self.fetch_model() is None:
return
self.parts.progress.report('waiting', 0.0, 'downloaded, builds when the jetson is on',
code='downloaded_waiting_host')
def has_work(self) -> bool:
"""Is there a reason to wake the Jetson? Only things the link can fix count."""
spec = self.parts.spec.load()
@@ -259,25 +278,21 @@ class ProvisioningRun:
self.log.warning("jetlink: no warp built for this camera, nothing to provision for")
return True
if not self.has_work():
# Nothing to build, unless a model was picked on the server since: a
# Jetson awake on the bus is asked, which costs a hello and no wake. Not
# a phone's cable, which is there only once the app dials in
if not self.has_work() and not (gadget.host_attached() and gadget.link_kind(self.parts.settings.mode()) == 'usb'):
self.log.warning("jetlink: nothing to provision")
return True
finished = False
try:
# the bytes first while no host is on the cable: a parked car whose
# Jetson is switched with the ignition never has one to wait for. A host
# that is there is asked first, since one with the model built needs
# nothing from the comma; provision() fetches it if the host has not
if self.needs_download() and not gadget.host_attached() and self.fetch_model() is None:
return False
if not self.open_link():
return False
if not self.wait_for_jetson():
self.log.warning("jetlink: no jetson within %.0f s, leaving it for the next run", WAKE_TIMEOUT)
if self.parts.models.shipped_model_path() is not None:
self.parts.progress.report('waiting', 0.0, 'downloaded, builds when the jetson is on',
code='downloaded_waiting_host')
self.close_link()
self.without_jetson()
return False
finished = self.provision()
except Exception:
@@ -288,6 +303,12 @@ class ProvisioningRun:
return finished
def server_fetches() -> bool:
"""Did the server the comma last spoke to say it downloads a model itself
(its hello's `fetches`, as the owner keeps it)? No for one too old to say."""
return (gadget.read_record(gadget.SERVER) or {}).get('fetches') is True
def main(argv: list[str] | None = None) -> None:
"""One provisioning round over the fork's adapter, as the owner starts it.
Exits 0 when nothing is left to do, and 1 when the owner should try again."""
+11 -5
View File
@@ -31,14 +31,17 @@ PROGRESS_MIN_INTERVAL = 0.25
# what a progress record's `code` may be, for a fork that words the line
# itself; `msg` stays the English one. `args`, where a code has them:
# downloading and upload {got_mb, total_mb}, download_stalled {retry_s},
# reconnecting {why: lost, behind, load_failed or bad_output}, build {eta_s}.
# From upload on they are stage names, and the server's stages are its own:
# downloading, download and upload {got_mb, total_mb}, download_stalled
# {retry_s}, reconnecting {why: lost, behind, load_failed or bad_output},
# build {eta_s}. downloading is the comma fetching the model, download the
# server fetching it itself. From download on they are stage names, and the
# server's stages are its own:
# a server can add one, so a fork falls back to `msg` for a code it does not
# know. A server message with no stage has no code
PROGRESS_CODES = ('waiting_for_host', 'finding_model', 'model_missing', 'ready_waiting_window', 'reconnecting',
'connecting', 'downloading', 'download_stalled', 'download_failed', 'downloaded_waiting_host',
'engine_ready', 'failed', 'upload', 'patch', 'parse', 'convert', 'compile', 'build', 'load', 'warm')
'engine_ready', 'failed', 'download', 'upload', 'patch', 'parse', 'convert', 'compile', 'build',
'load', 'warm')
def estimated_build_seconds(size: int) -> int:
@@ -104,12 +107,15 @@ class Progress:
size. Only the build: the upload reports MB of MB and a connect has nothing
to predict."""
args = None
size = self._size() if self._size is not None and stage in ('build', 'upload') else None
size = self._size() if self._size is not None and stage in ('build', 'upload', 'download') else None
if size and stage == 'build':
left = estimated_build_seconds(size) * max(0.0, 1.0 - frac)
msg, args = _eta(left), {'eta_s': round(left)}
elif size:
args = {'got_mb': int(frac * size) >> 20, 'total_mb': size >> 20}
if stage == 'download' and args:
# the server downloading the model itself: not the comma's own 'downloading'
msg = f"jetson downloading {args['got_mb']} of {args['total_mb']} MB"
self.report(stage, frac, msg, code=stage or None, args=args)
+10 -5
View File
@@ -161,7 +161,7 @@ class Warp:
out = jit.captured.ret.uop.base.buffer
self._device = out.device
if self._device.startswith('QCOM'):
_make_coherent(out)
make_coherent(out)
# the two buffers every frame passes, written in place by start()
self._tensors = {name: Tensor(np.zeros(shape, dtype=dtype), device=device).realize()
for name, (shape, dtype, device) in specs.items()}
@@ -191,10 +191,15 @@ class Warp:
self._replay(self._inputs, {})
def _make_coherent(out) -> None:
"""Move a QCOM buffer to KGSL memory flagged IOCOHERENT and write-back.
Raises if the kernel keeps either flag back (the allocation's record holds
its answer): write-back memory that is not coherent reads stale frames."""
def make_coherent(out) -> None:
"""Move a QCOM buffer to KGSL memory flagged IOCOHERENT and write-back, so
the CPU reads it through its cache once the GPU is done (see Warp). Raises
if the kernel keeps either flag back (the allocation's record holds its
answer): write-back memory that is not coherent reads stale frames.
For an adapter's model_warp too. Move a buffer before the first call of
the JIT that writes it: that call links its address into the JIT, which
would keep writing the memory this frees."""
from tinygrad.device import Device
dev = Device[out.device]
mem = dev._gpu_alloc(out.nbytes, flags=COHERENT_WRITEBACK)
+30
View File
@@ -294,6 +294,36 @@ def lfs_resolve(endpoint: str, pointer: Pointer, timeout: float = CONNECT_TIMEOU
return None
def lfs_hrefs(endpoints, pointer: Pointer, timeout: float = CONNECT_TIMEOUT, opener=None):
"""Each of `endpoints` that has the object, in order, with its download href."""
for endpoint in endpoints:
href = lfs_resolve(endpoint, pointer, timeout=timeout, opener=opener)
if href is not None:
yield endpoint, href
def fetch_size(oid: str, endpoints=LFS_ENDPOINTS, timeout: float = POINTER_TIMEOUT, opener=None) -> Pointer:
"""The pointer for an ONNX known by its oid alone, as a fork whose catalog
names a model by its hash knows it (StarPilot's). comma's LFS servers hand
out a download address for any oid they hold, whatever size the batch asks
for, and the first byte of that download says how many there are. No
server with it is NotFound; one that would not say, a NetworkError."""
opener = opener or urllib.request.urlopen
for endpoint, href in lfs_hrefs(endpoints, Pointer(oid, 1), timeout=timeout, opener=opener):
request = urllib.request.Request(href, headers={'Range': 'bytes=0-0'})
try:
with opener(request, timeout=timeout) as response:
# a server that ignores the range answers 200 with the whole length
total = (response.headers.get('Content-Range') or '').rpartition('/')[2] \
if response.status == 206 else response.headers.get('Content-Length')
except (OSError, ValueError) as e:
raise NetworkError(f"could not size {oid[:16]} at {endpoint}: {e}") from e
if not str(total).isdigit() or int(total) <= 0:
raise NetworkError(f"{endpoint} did not say how big {oid[:16]} is")
return Pointer(oid, int(total))
raise NotFound(f"no LFS server has {oid[:16]}")
def lfs_download(href: str, pointer: Pointer, dest: Path, progress: ProgressFn | None = None,
should_stop: StopFn | None = None, opener=None) -> Path:
"""Stream to a .part file, hashing as we go, and only then take the name.
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "${ROOT_DIR}/scripts/host_tool_runner.sh" jotpluggler "$@"
+15 -1
View File
@@ -1,5 +1,10 @@
#!/usr/bin/env bash
# Device Params must not depend on the platform used to compile native libraries.
if [ -f /AGNOS ] && [ -z "${PARAMS_ROOT:-}" ]; then
export PARAMS_ROOT="/data/params"
fi
export OMP_NUM_THREADS=1
export MKL_NUM_THREADS=1
export NUMEXPR_NUM_THREADS=1
@@ -21,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"
+5
View File
@@ -291,6 +291,11 @@ struct MapdOut @0xa4f1eb3323f5f582 {
computedMonoTime @31 :UInt64; # Computation boot-time nanoseconds, not send time.
sourceGeneration @32 :UInt64;
producerSession @33 :UInt64; # Random nonzero process session.
# Data-only preview from the same qualified GPS fix; no controller smoothing.
previewSpeedLimit @34 :Float32;
previewDistance @35 :Float32;
previewBoundaryId @36 :Text;
previewWayId @37 :Int64;
enum SampleStatus {
unknown @0;
+357 -309
View File
@@ -2887,12 +2887,12 @@ type MapdOut capnp.Struct
const MapdOut_TypeID = 0xa4f1eb3323f5f582
func NewMapdOut(s *capnp.Segment) (MapdOut, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 112, PointerCount: 6})
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 128, PointerCount: 7})
return MapdOut(st), err
}
func NewRootMapdOut(s *capnp.Segment) (MapdOut, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 112, PointerCount: 6})
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 128, PointerCount: 7})
return MapdOut(st), err
}
@@ -3260,12 +3260,54 @@ func (s MapdOut) SetProducerSession(v uint64) {
capnp.Struct(s).SetUint64(104, v)
}
func (s MapdOut) PreviewSpeedLimit() float32 {
return math.Float32frombits(capnp.Struct(s).Uint32(76))
}
func (s MapdOut) SetPreviewSpeedLimit(v float32) {
capnp.Struct(s).SetUint32(76, math.Float32bits(v))
}
func (s MapdOut) PreviewDistance() float32 {
return math.Float32frombits(capnp.Struct(s).Uint32(112))
}
func (s MapdOut) SetPreviewDistance(v float32) {
capnp.Struct(s).SetUint32(112, math.Float32bits(v))
}
func (s MapdOut) PreviewBoundaryId() (string, error) {
p, err := capnp.Struct(s).Ptr(6)
return p.Text(), err
}
func (s MapdOut) HasPreviewBoundaryId() bool {
return capnp.Struct(s).HasPtr(6)
}
func (s MapdOut) PreviewBoundaryIdBytes() ([]byte, error) {
p, err := capnp.Struct(s).Ptr(6)
return p.TextBytes(), err
}
func (s MapdOut) SetPreviewBoundaryId(v string) error {
return capnp.Struct(s).SetText(6, v)
}
func (s MapdOut) PreviewWayId() int64 {
return int64(capnp.Struct(s).Uint64(120))
}
func (s MapdOut) SetPreviewWayId(v int64) {
capnp.Struct(s).SetUint64(120, uint64(v))
}
// MapdOut_List is a list of MapdOut.
type MapdOut_List = capnp.StructList[MapdOut]
// NewMapdOut creates a new list of MapdOut.
func NewMapdOut_List(s *capnp.Segment, sz int32) (MapdOut_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 112, PointerCount: 6}, sz)
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 128, PointerCount: 7}, sz)
return capnp.StructList[MapdOut](l), err
}
@@ -3395,312 +3437,318 @@ func NewMapdOut_GpsSource_List(s *capnp.Segment, sz int32) (MapdOut_GpsSource_Li
return capnp.NewEnumList[MapdOut_GpsSource](s, sz)
}
const schema_b526ba661d550a59 = "x\xda\x94Z}lT\xd7\x95?\xe7\xbd\xb1\xc7\x9f\x8c" +
"\x877\x04\x0c&\x13(\xd0\x98\xc6i\x8c\xf16\xa6I" +
"]c\x03\xc1\x1a\x83\xc7\xc3G@A\xea\xf5\xbc\xeb\x99" +
"\x07\xcf\xefM\xde{c{\x10\x11\x84\x82\x04YP\x93" +
"n\xd2\x92*lC\x02R\xda\x0d-\xed\x86\x15\x1bQ" +
"-AD\"Y\x90\x92\xaa\xa9\x9a4\xbbI\xa3JI" +
"\xba\x89\xd2v\x13m\xb6\xdb\xe8\xad\xce\xbd\xf3\xf1\xfc\xd1" +
"\x10\xff\xc3\xf8\xfd\xcey\xe7\x9e{\xee\xb9\xe7\x9e\xdf}" +
"\xdc\xf1\xf5\xfao\x86\xda\x1b3\x0d\xa0$\x1f\xae\xaa\xf6" +
"\xa3{v\xbccy?9\x00\xd1E\xe8\xef\xa8\xdb\xba" +
"x\xe4\xf9\x15\xe7!\x14\x06\xe8h\xae\xde\x89Z[u" +
"\x18T\xff\xa7\x1f'\xbav\xfe\xf1\xf2\x83\x93\xb5\xaa\x84" +
"Zm\xf5^\xd4\x16W\x87\x01\xb4\xe6\xeaw\x01\xfd{" +
"{/\x7fgx\xc5\x0f\x0fCt\x91RQ\x06\xec\xc0" +
"p?j\xf3\xc2\xf4R4l\x87\x00\xfd\xb6\x97\xf2\xf7" +
"\xfe\xb8\xf9\xf8#\x90l\xc3Z\xff\xb2\xf5o\xf5\xdb\xae" +
"\xdc\xf7\xdfP\x85\xa4s\xbc\xfe!\xd4\xce\xd4\xd3\x9f\xa7" +
"\xea\xdfR\x00\xfd\x7f\xfc\xcaGyc\xef\xce\xefA\xf4" +
"K\x8a\x7f\xf0\x93O\xbe\xd4\xf1_\x7f:M\x96_\x9f" +
"s\x10\xb5\x0f\xe7\x90\x0f\xef\xcf\xf9>\xa0?\xf79\xcc" +
"d\xae^\x7fb\x86iuE\x86Q\x1b\x88\xd0\xb4\xda" +
"~\x1bW\xfb\xc2\xd9S3h\xb5Fv\xa2v\xb7\xd0" +
"2\xd5\x8eo~:\\\xfd4$\x17\xe1\xb4\xd97\x93" +
"^[D\xbe\xb2\x1d\x01+~%\x17\xe1\x9c\x80:E" +
"\xa8\xe3d\xd3B\xd4\xce6}\x19\xa0\xe3\xa5\xa6\xf10" +
"\xa0\xdf\xd7{\xe9\xe5\xb3o=}fZ\xb4\x92\xcd\x07" +
"Q\xe3\xcd4'\xd6\xfc5@\x7f\xeb\xe9\xe4\xeb\xb7\x8d" +
"]?3\x83\xb7\xbcy'j\x85f\xf2\xf6'\xbfM" +
"?\xf6\xf7\xcf|\xf4O\xd3\xecmm^S\xb1\xb7\x19" +
"\xd0\xdfu\xfe\xdd\xf6\xef|\xf6\xd6Of\xb0\x97'{" +
"G\x85\xbd\xee\xb7\xfe\xf0\xc0\xc0-\xdb\xcf\xcd8\xeap" +
"i\xd4\xab\x98Xb\xa4\xad\x0b3hm%[\x86\xd0" +
"\xca^\xfc\x87GS_\xef\xfb\xc5\x0cZ\xeb\xc8\xd6\x0e" +
"\xa1uH]\xfd6\xbfk\xd9\xe5\x19\xb4:Ik\xa3" +
"\xd0\xfa\xe8\x9f\xbf\xf1\xea\x9dw\xef\xbe:uU\x14\xd2" +
"[\xde<\x17\xb5N\x9akG{s\x9cVe\xcd\x8e" +
"\xed9\xf3\xb5\x1f\xfe\xfbLC/\xa4\xa1\x17\x92\xd1W" +
"\x0e>\x99\xf9\xbf7\xbew}\xa6\xa1Ik\xa3\xd0Z" +
"|%\xfd\xe6\x9b\xde\xc1W\xa7\x85x\xf9\xc2\xb5\xa8u" +
".\x14\xc3.\x14\xc9\xd0\xd5u\xf1\xd4\xb5\x87\xfe\xe7\xd7" +
"\xe4\xa5:\xc5\xe2\x92E\xfd\xa8u.\xa2\x05i_D" +
"\x1b\xa73\xd1\xf7\xed\x13\xdb\xbf\xf7\xc6\x0c\xa3\xcfk\xd9" +
"\x89Zk\x0b\x8d\xde\xf5\xed\x97\x7f\xf0\xfd\xaa\xef\xff'" +
"\x99T\xa6\xa8U\xb5\xacEm^\x0b\x99\x8c\xb6\x8c\x03" +
"\xfa\xb7\xbe\xfa\xeb\x8fn[\xf7\xdc\x9f\xa7\xed\x98|\xcb" +
"n\xd4\x8e\x0a\xcd\xc3-\x19\xc0\xca\xe6\x9blVn\xc5" +
"\x9f\x91\xf6\x8b-\xf3\x01:\xae\xb7\x84)m\x9bO\x9e" +
"\xdcX\xfb\xc1M\x1f\xcf\xe0\xebk\xcbv\xa2\xf6\xfe2" +
"\xf2\xf5\xe9P\xee\xb3\xaf\x1f:\xfe\xbf3h\xbdHZ" +
"\xaf\x0b\xad\x95\xef-\xdf\xf4\xee}\xdf\xf8\xcb\xb4x\x9e" +
"_6\x8c\xdaK\xcb\xc8\xc9\x17\x97\xed\x07\xf4\x9fr\x07" +
"\xaf\xbe\xbc\xeb\xe9\xbfLuR,z\xd5\xf2\x83\xa85" +
"/\x17\xe1Z.\xa2\x1f\xfd\x97\xf1#\x1f\xf6\x0c\xffu" +
"\x86\xe1\x1f[1\x8c\xda\x8fV\xd0\xf0\xfbO\x9c{7" +
"u\xe2\x88?5\x93\xc4\xcc\x0f\xafx\x1e\xb5\x93+\xc8" +
"\x85\xc7W\xfc\x14\xda\xfc4w83\xbf\x9a\x0e\xe5]" +
"\xcf\x1e\xfdjZ\xfc\xdc\x9ef9+\xb7\xa6W<\x0c" +
"q\x97;c\\\xd5W\x0f\"~\xae~\xcac\xce\xa0" +
"a\xda^/sz\xbbm\xcbsls\x101\xd9\xa4" +
"\x86\x00B\x08\x10e;\x01\x92\xdfR1i*\x18E" +
"\x8c!\x81\xc6w\x01\x92\xa6\x8a\xc9\x09\x05\xa3\x8a\x12C" +
"\x05 \x9a'MO\xc5\xe4\x01\x05\xa3\xaa\x1aC\x15 " +
"\xfa@?@r\x9f\x8a\xc9#\x0a\xfa\xd9\xbc\xdeKc" +
"\x80j\x9b8\x17\x14\x9c\x0b\xe8\xbb\x1e\xe7\x8eae0" +
"a\x8c\x1a\xdeFk\x04\xed\xb2\xccbcF\x86y\x06" +
"\xa8\xb6U\x06sY\xdb\xe2\x1br.\x00`S\xa5\x80" +
"\x03b\x13|\xfel\x07XN\xdfh\xe5\xf2\xde\x96B" +
"\x8e\x03\xd0D/\x92\xebZ\x9b\xd2\x0f\x80s\xb4V\xe5" +
"\xe7\x00\x18\xd1Z\x95\xa7\xc8\xb8\xd6\xaa\xfc\x18\x00\xa3Z" +
"\xabr\x19\x00\xe7j\xad\xca\x1b\x00X\xaf\xb5)\xc3\x00" +
"\xa8i\xad\xca\xcb\x00\x18\xd3\xda\xc4/j\xed\xca^\x00" +
"T\xb46e7\x00\xce\xd3Z\xc5\xf3M\xdar\xe5\x0f" +
"\x008_kU~\x09\x80\x0b\xb46\xe5\x1d\x00l\xd6" +
"\xda\xc5\xf3B\xadS\xf9\x01\x00.\xd2:\xc5\xb8-Z" +
"\xa7\xb0\xb7X\xeb\x12\xcf7k]\xc2/\xb5\xf8\x1c\xd2" +
"\xba\x84?q\xadK\xd8\xbdE\xbb[\xd8[\xd2\xd1\xa3" +
"\xacA\x00\xac\xd26*\xcf\x03`\xb5\xb6Q8\xd0\xa8" +
"\xadSv\x02`\x83\xd6#\x1c[\xaa\xdd-^\xfc\x92" +
"\xd6#\x0c/+\xfe.\xef\xe8Q\xe6\x92\x81:m\xa3" +
"\xf2\x10\x00\xae\xd06*\x7f\x06\xc0/w\x0c(KI" +
"p\xab\xb6U\x84\xa0\xb5c\x87\xa2\x10\xb0\xb2c\x97\xfc" +
"\xe3+\x1aS\xbe\x0b\x80\xb7iL\xbc\xda\xa61\x11\xbc" +
"p\xf1\xb9Fc\xcaA\x00\xac\xd5v\x89\xdf\xdb\xb5\x1d" +
"\xe4\x83\xaf\xdb\xe3\x96i3\x1d\x00|\x97{[\x98\x93" +
"\xe1\xe8%\x98\xc7\x1df\xc6{\xd2in\x12\x9e\xcaq" +
"\xae\xcb\x14\xd9<2\x12v\xb97\x05\xed\xb5\xad\x08e" +
"/\xc1\x03,\xd7\x9b\xafr\xc6\xb8\x90\xf7\xca\xb4\x06\x97" +
"{\xdb\x0c\xd7\xb0\xad\xde\xfc$\x91*_J\xd8\x99\x04" +
"\x87\xf0\x98\x1cOh*RU\xf8D.\xf5H\x1f\x83" +
"\xb2\x01\xc3\x92\xe2m\x00\xbe\xc3i&)\x0e\xdd\x9eg" +
"X\x19\xd7w\xd9\x18Oq\xcf\x83\x88|\xe4\xde:\x8b" +
"\x0d\x9b\xd0-\x87\x9fjl\xab\xcb\x85\x9c\xa7\"%\xb1" +
"\x98\x8a2I\x96\xe3\x1c\xf5\xf2\xec\x151\xfb\x804\\" +
"|\xf3\x1e\xdb\xd4\x13\x0as\xbd\x14\xe7\x96P%M\xf4" +
"\x02Q\x16h?W\x9d=S\xc1\x9et\xb8\x18x\x81" +
"*\x12\xddb\x8c\xf2\xcd##.\xf7d \xfa\xf8\x08" +
"\xcb\xa3\xe9%\x98\xc5\xb7\x1ba\xdd\xcb\x96]\xc6R\xdc" +
"\xe2\"p>\x05\x86\xd41oz\x14\x11#L\x01!" +
"t\x88\xa7\xed\xaa\xd1Qn\xe9\\\x17\x12+\xe3\xd2Z" +
"\xa5L{\xbc\xcf\x1e\xb7\xd6\xdb\xce&>!\x1dHD" +
"h\xb2\x95\xb9o\xcdM\x92\x1a\xe1\xa2\x94\xe6\x9eRK" +
"s\xf6\xb6g\x0d\x93\xf7f\x99\x951\xacL\x8awK" +
"u1\xfa w\\4\\\x8f[\x1e\x09\xe4\xb2\xa5\x99" +
"\x95\xe6f\x9f\x0d\xdd27\x8b\xe9\xd1\xefR\x1d*>" +
"\xa4l\x88\xe4\x9d4\x17\x8b:\xe1qG\xb1\x98Y\x0e" +
"\xf3\xa4t\x14b,\x89\xe3\x89Is\x90\xd9;\xe8\x18" +
"q\xdb1\xbcB\x19W\xa5\x19r\x9a\x0f\xf1\xfb\xf3\x86" +
"\xc3]\xda\x0d9\xf4|F\xbf^*\x87\xa5\xd1\xe4r" +
"\x0c:\xdcu\x95\x0d\xcc\xddb\xf7\x145&\x8d\xd7\xa3" +
"\xef\xce\xbb*\x85_\xaefP\xab\x12;\x01*^e" +
"*\xb4\xea\xb6\x9a\xf7\xcaC\xd4\x8a!6\x8fq\xc71" +
"t^Q\xa4U\xeb\xb5-\xdd\xf0\x0c{j0J;" +
"0\x95e\xba=\xde\xcb\x9c\x94\xc7<\x14\xd1\x13\x10\x8e" +
"\x0f\xd8:7\xb7\xad\x92S)b\x1brn\xc2N\xb3" +
"\x08\x19$\xb8\xdf\xb5\xadAd^v\xbdi\xb3\xe2\xac" +
"\x05V\xcd\xbc\xec\x16>!\\\x10\x00\xf3\xb2k\xed\xe0" +
"\x90ESd\x89\xd6#\xe2X\xcc,\x1f\x16U78" +
"\x1a\xc9Y~\xbb8s\xe8\xd0X]:\x1d\xb5]\xb8" +
"\x17 u\x1f\xaa\x98\xcab\xe5\x80\xd48\xf6\x03\xa4t" +
"\xc2sX9#\xb5Q\x1c\x02H\x99\x84O`\xe5\x98" +
"\xd4\xf2B\xdf#\xfc\x00\xe1\xa1\x9a\x18\x86\x00\xb4\x07p" +
"\x15@j\x82\xf0C\x84W\xd5\xc6\xb0\x0a@{\x10w" +
"\x02\xa4\x0e\x10~\x8c\xf0\xea\xba\x18V\x03hG\xf1\x07" +
"\x00\xa9c\x84\x9f <\\\x1f\xa3&B{\x0c\x1f\x02" +
"H\x9d \xfc4\xe15\x0d1\xac\x01\xd0N\x09\xfd\xd3" +
"\x84\x9f#\xbc\xb61\x86\xb5\x00\xdaYt\x00R\xcf\x12" +
"~\x81\xf0\xbap\x0c\xeb\x00\xb4\xf3\xf8\x14@\xea\x02\xe1" +
"WP\xc1%\xf5J\x0c\xeb\x01\xb4\x17p\x0d@\xea\"" +
"\xe1WQAl\x88a\x03\xf5L\xb8\x12 u\x89\xe0" +
"7QA\xdf\xb5i\xcb\x0c\xd8\xd0m\xd9\x94VX\x0b" +
"\x0a\xd6\x02\xfa&\xf3\x0c/\xafs:\xd3\xebA\xc1z" +
"\xc2l+C /c\xcc\x9c\xa6\x17w\xc56\xaa" +
"\x03\x05\xeb\x00\xfda\xce\xa8\x9b\xe8\x03\x95g\xca`\xd6" +
"v\x8c\xbd\xb6\xe5!3{\xd2\xe9\xbc\xc3\xd4t\xa1," +
"\x1c\xe3\x8eg\xa4\x99\x89R\x94.\xd0\x018\xd9Z\x0f" +
"\x16E}A\xab\xae\xac\x94i\x88\x0bY\x19\xcf[\xc6" +
"\x84\x98\x9d\xeb\xb1\xd1\xdc\x80a\x86M\xc3\xc5*P\xb0" +
"\x0a\xb0;\xcb\xdc\xf5\xc6\x04\"(\x88\x80\x91\xb1M\xeb" +
"\xfap\x0e\xe0\xa0\x8a\xc2\xc0\x9c@\x0fS\xf57z\x98" +
"\xcdy\xef\xf6\x14\x1b\xcd\x99\x9c\x123\xef\x8a\xa4\\ " +
"\x9a\xb0\x9e\xb5\xd4\x88D\xbbVQ\x1f\x12m\xa7S_" +
"\x8d\xb6\x11\x18\x8a.\xa7^\xa0*\xbad7\xc0\xfe\xbc" +
"\xb5\xc7\xb2\xc7\xad\xb8eo\xc8\xb9\xbee\xf7\xdac\xdc" +
"a\xa0f\xf8~\xcb\x1e`^:\xeb\x8f\xd2\xbf\\\xdf" +
"\x04\xdd\xb6,!E \x01\xb2\x0a\x7f\xe1>4\xac\xb7" +
"\xaf\xbaQ#:\xa5q\xed\xba\x91>\x85\x81\xf61\x9d" +
"\x1a\x9b\xd5\xbc'\"PnZ\x1f\x7f\x08 \xf9\x84\x8a" +
"\xc9g\x02M\xeb\x19jEO\xab\x98<\x17hZ\xcf" +
"\xae\x04H>\xa3b\xf2J\xa0i}\x814/\xa9\x98" +
"\xbc\xa6 \x86\xc4N\x8c\xbet\x10 yU\xc5\xe4\xaf" +
"h\x1b\xa2\xd8\x86\xd1W\x87\x01\x92\xaf\xa8\x98|S\xc1" +
"r'\x83\x83\x8e\x9d\xa1:)\xfa\xd32a(\xf6\xa7" +
"n\xe9\x88\xa3~\x0c\x14l\x00\x8c\xe4\x98\x97-\xe5@" +
"S\x85\xb1\x01\x8al\xc8\xd9\xae\xa8\xa8\xb2\xdf-\x93\xaf" +
"\xa2=\xd3\xb6sC\xcc\xe3\xd8C\x0b\x98\xe1\x95\xdc-" +
"I <`Xe\xb4\x14T\xf5o\xe5\x96\x9a\xf7\x92" +
"5\x18\xbc\xea\xa8\xdd\x1d`q\xb5C~)\xf5 B" +
"\xc9\xe7o\xc8\xb9)\xda\xd7\x80<y\xa4\\\x19\x7f\xa4" +
"\xae\xa5\xca\xa2ReQ\x03\x95\xf1\xacJ\x95\xe2\x19\xc2" +
"\x9fS\x03\x95\xf1g*U\xc0s\x84_T\x15\xc4b" +
"a\xfcW\x95\x0a\xdd\x05\x82\xaf\x90z\x08ea|A" +
"\xa5\xc2{\x89\xf0k\x84W)\xb20\xbe\xa4>\x0f\x90" +
"\xbaF\xf8o\x08\xafVea|M\x0c\xfb\x0a\xe1o" +
"\x12\x1e\x0e\xc9\xc2\xf8\xba\xb0\xff\x1b\xc2\x7fOx\x8d*" +
"\x0b\xe3\xefT*\x8c\xbf'\xfc\x8f\x84\xd7\x86da\xfc" +
"P\xa5\xc2\xf8\x01\xe1\x9f\x12^W%\x0b\xe3'\xeaw" +
"\x01R\x9f\x12\x1e\x0a)\x18\xad\xaf\x96\x85\x11C\xbf\x04" +
"H\xd5\x84TL\xc5\x08ox[V\xc6h\x88\xfci" +
" |\x01\xe1\x8d\x8bc\xd8\x08\xa0\xcd\x0bQ\xc1o\"" +
"\xbc\x85\xf09\xbf\x8b\xe1\x1c\x00\xad9D~. |" +
"\x19\xe1\x91\x9a\x18F\x00\xb4%\xa1\x97\x01R\xb7\x12\xbe" +
"\x9a\xf0\xa6\xda\x186\x11\x9f\x0fQ|\xee \xfc.\xc2" +
"\xa3u1\x8c\x02h]!\x9a\xd7]\x84\xdfC\xf8\xdc" +
"H\x0c\xe7\x02h\xebB\xc3\x00\xa9>\xc2\x07\x09\xd7\xea" +
"c\xa8\x01h\x03\xa1\x9f\x03\xa4\x06\x09\xbf\x8f\xf0XC" +
"\x0cc\x00\xda\x8e\x10\x1d(\xf7\x11\x9e%|^c\x0c" +
"\xe7\xd1\xc1\x17\xa2\xf8\xe8\x84\xe7\x08\xbf\xa9%\x867\xd1" +
"\xc1'\xf4s\x84\xef#|\xfe;1\x9c\x0f\xa0\x15\x84" +
"?\xfb\x08?B\xf8\x82\xc51\\\x00\xa0\x1d\x0e\xed\x06" +
"H\x1d\"\xfca\xc2\x9bkb\xd8\x0c\xa0\x1d\x17\xf19" +
"B\xf8\xa3\x84/\xac\x8a\xe1B\x00\xed\x11\xe1\xe7\xa3\x84" +
"?I\xf8\xa2\x9bc\xb8\x08@;)\xfcy\x82\xf0g" +
"\x08o\x89\xc7\xb0\x05@;#\xe2y\x9a\xf0s\x84/" +
"^\x16\xc3\xc5\x94\x9e!:\xa0\x9f%\xfc\x02\xe17\xd7" +
"\xc5\xf0f:\xf8D|\x9e#\xfc\x12\xe1\xf1\xfa\x18\xc6" +
"\x01\xb4_\x88y]\"\xfc\x1a\xe1\xb74\xc4\xf0\x16\xca" +
"C\x81_#\xfc7\x84/i\x8c\xe1\x12\xca\xc3\xd0A" +
"\x80\xd4\xaf\x08\x7f;\xa4\xe0\xfeqV\xd8\xc4Fy\xa9" +
"\x14t\x8f\xb3\xc2\x10\x1f)=\xfa\x8e\xcdt\x92\x07\xaa" +
"\x85<v\x12\xc6(\xa8\x86W\xde\xd6V\xb1\x09\x86n" +
"\xd9rM\x13\xa0\xc4\xfb\x8cn\xd7\xa3\xf6\xb6\xa4\xd0\x9d" +
"e{\x99\xa3\x97\xad\x93\xfe=l/\x03u\x06\x10\x1d" +
"\xbd\xcf\xa0\xf7\xd5\x8a\x01\x9f\xe9c\x86k;\x05\x88\xa7" +
"&\x9d\xc4\xf4R\x8f>f \x09\xa5\x0b\xd3dJI" +
"V\xb4\x9b\xc6\x8ac\xb6\xc5\xb7\xb3B\xe9\xa0\x8c\x9b\xcc" +
"\xe2.V\x83\x82\xd5\x80\xbeg\x98<A\xb4Q\xe5z" +
"I\xa5\x1c\x19\xc5\xf0R\xf9L\x86\xbb\x1e\xd7\x85\xf1\xc0" +
"\x89\xee\x16\x05\xd0\xadOv\x97\xbb\x9e1J\x85T\x1f" +
"\xb2\x99\xbe\xdd\xd0U/[\x16\xd2:P\x17\x0ba>" +
"\xe1a\xa4r\xc7\x09\x88\x11@_7\x8aQ]\xef\xd8" +
"\xa3\xdbY\xa17\xce-j\xfa\xcb=F\x91xb\x89" +
"y\x06<\x1a%\xbe\xe4\x8c\xf1\xa9\xf1\x1bg\x85\x147" +
"y\x1a\xa9\xf8\xcb\x1b\x0b\x8cT\xae\xaa\x8a#\x97\xe6\x8c" +
"\x86\xec\xdc\xbd`@\xb2F&;\xce\x0a\xbd\x101\x99" +
"\xebb\xa4rq(\xdf\x8e\x8f\xb3\xc2F\xbd\xd4\x9a\xf8" +
"\xe9b\xf3>\x85\xabT2O\x14\xfem\x1c\xe2\x0eM" +
"\x07\xc3\xa0`\xb8\x18\x1d\xeaD@\xcd\xd3 \xe5\x93\xa3" +
"\xe8b\xa6r@`\xa4r\x94T\xa4\x03\xb6eo\x81" +
"p\xb0!L\xdb\xa3\xb9\xbc\xc7u\x142\x83v@Y" +
"&\xbb\xc8\x0d\x1c-\xee0y2\x96e9\xc7\xd6\xf3" +
"i\xee`\x8a\xbb\xe4cE\xf2\xb9\xbd~\x99\xaaHz" +
"+\xa2M-E\x8dh\x12\xa2kDSU\xbb\x16\x80" +
"v\x8fg\xa4\xf7\xe7\xb8\x93\xe6\x967\x9b\x9e\xa63\xd8" +
"\xd3\xcct\xfcR\xde\x89K8>!\xfa\x99\x061\xf8" +
"b\xd9\xd1\xcd[):\xba\xc6\xb5\x00\xfbG\x1c\xce\xc7" +
"Y!\x926\xbcB\xa9\x93\x9b\x8d'\xed\xb3\xea\xc6\xc2" +
"z\xfb\xeaY\xb6o_\x9b\xed\x00\x9d\xb3}\xa1\xfdF" +
"\xc1\x14w}hQ\x1cc\xe5\xbe\xf0\x01\xea\xf6&T" +
"L\x1e\x0a\xf4\x85\x0f\xae\xaa\\Qb\xb1-<\xbc\x14" +
" y@\xc5\xe41j\x0b\x9bd[x\x94\xde>\xa4" +
"b\xf2\xe1J#\x12=N\xbd\xe21\x15\x93'\x14\x8c" +
"x\x85\x1c\xa5x\xf9\xfbSq\x97\x8d\x10[-\xed\xeb" +
"\xb0\xeb9\xe5\xceo\xd8\xb6\xcd\xf2n\xdd]$\xae\xc1" +
"b?\x9b\x98\xdc1\xdb v\xdc\xe8\x85{\x8a\xf5\x83" +
"\xaa\x87\xd8\x0fw\x8a\xe8\x9c\x92)\xf9x\xbfH\xc9\xc7" +
"v\x0b\x92\xf1\xc8*A2\x8e\x0e\x09\x92q\x98T\xaa" +
"\xa3\x0f\x0c\x03`8Z \xb0&\x9awh\xa7F\xef" +
"\xa7\xf7\xea\xa2\xa3\xf4^}\xd4\xa0\x9f\x86('\xcd\xc6" +
"(\x0b\x90\x13\x7f\xd4\xf6lg\x9c\x15\x00\xa0\xf2w$" +
"aX{\xe2\x9e\x93\xb7\xf6\xf8\xe2\xdf\x84a\x01\xee\xd9" +
"\x9fs\x8cQ\xe6\x14\xfc\xe2o\x02\xc2\x86\xb5\xc7w9" +
"\x155\xe6\x00\x16*\x7f\xc7\x0bd\xc3\xf7\x88\xf81G" +
"\x98/\xff-\xcc\xfby+Ms6 2bp\xdd" +
"w\xb8k\xe8\x9c\xca\xbf\xc1L\xdf4\xc6\x0c+C\xfd" +
"\xaf\xc3\xb9wC\x9aBK:h\x1b\x96'/\x0f\x02" +
"W\xeb\xfd3]\xad\x0f\x01$\xb3*&=j\x8eC" +
"2\x1d\xef'0\xa7br\x1f\xa5c\x95L\xc7\xc2\xde" +
"J2\x7fa\x02\x9d\xce;c\xcc\xcb;P9h}" +
"O^`r\xe86m*)\xd3\xb8\xc2\x17\xd8\xf1\x1d" +
"_\x84\xb0\x0d\x96\x98\x8c(\xad\xe58\xb4R\x1cnU" +
"1\xb9:\x10\x87v\x9a\xf2\x1d*&\xef\x9a\xcd\xecn" +
"\xe0\x00\x11gAW\xc4\xd5\x9dX\x0d\x11\xdf\xd6\x95\"" +
"\xa1\x97\xc8\x84^\xdc/\x12\xbay)@\xc4\xb2-\xee" +
"\x1bt\x96[\xcc\xa4D\xe1\x13\xe5\xbf\xc3i\xe6|\xe1" +
"\xaf+\xa9\x88\xc7<\x9e\x0ca\xe0\x9bs\x14\x97\x867" +
"\xe4\xdc\xe4\x812k\xba_\xa5\xb67G4b_\x90" +
"5\x15\x04\xad\xd9G\xf8\x91 k:\xacR[z\x88" +
"\xf0\x87\x09WQ\xd2\xa6\xe3\xeaS\x00\xa9\x87\x09\x7fB" +
"\xd0&U\xd2\xa6\xc7\x85\xfd\x13\x84\x9f\x16\xb4)$i" +
"\xd3)\x95\xda\xd2'\x09\x7fV\xd0\xa6*I\x9b~$" +
"\xec<K\xf8\x05A\x9b\xaa%m:\xaf^\x9eD\xcb" +
"j\xc2\x926\xbd X\xdfE\xc2\xaf\x0a\xdaT#i" +
"\xd3\x8b\x82f]!\xfc\x15A\x9bj%m\xba.X" +
"_\x85\xae\xd5\xd7I\xda\xf4\x9a\xf0\xf3W\x84\xbfMx" +
"C\xbd\xa4M\xff!h\xd6\xdb\x84\x7f@xc\x83\xa4" +
"M\xef\xab\xd4\xb6\xbfG\xf8\xc7\x84\xcfi\x94\xb4\xe9O" +
"\x82\xc6}\xac\xaa8$X\xd3\x1c\xc9\x9a>\x13\xc3\xfe" +
"\x95\xd4k\x04k\x8aH\xd6T%\xba\xfc\x10u\xe7M" +
"\x8255I\xd6\xd4(XD\x85\xad\xcd\x8dJ\xd64" +
"/\xf4\xfc$V\xa6\xa9\x925-\x09\x91\x9b\xcb\x08\xbf" +
"C\xb0\xa6\xb9\x925\xb5\x09;\xb7\x11~\xa7`M\x9a" +
"dM\x9dB\xffN\xc2\xfb\x04k\x8aI\xd6\xd4#X" +
"M\x85\x95\xcd\x9f'Y\xd3\x80\xb0\x93 \xfc^\xc1\x9a" +
"n\x92\xaci\xab`\x17[\x08\xff\x96`M\xf3%k" +
"\xda%\xd8`\x85\xad-\xfc\xa6dM\\\x8c\x9b%\xdc" +
"\x13\xac\xa9G\xb2\xa6\xfb\x85\x9d\x0a[kY+YS" +
"A\xe0\x13\x84\x1f\x12\xac\xa9W\xb2\xa6\x07\x05~\x80\xf0" +
"c\x825\xf5I\xd6tT\xf8Yfk\x18\x97\xa4\xe9" +
"\x91\xd0R\x80\xd41\x82O\x84\x14\x14\xf7\xd9\xe6\xa0\xc3" +
"!\xe2\xba\x81\xee\x95\x99\xe3\xac\xe0n\xb6\xb0\xf85h" +
"]\xb7\xf8\xc4QQ\x18v\xd8\x1e\x9e02\x10\xcez" +
"n\x19\xd5\x99\x9b\x1d\xb6\x99\x83z\xe0f\xbb\\\xd3t" +
">\xe3X\xe5\x06^\xdcps\x1d\xa6K\x12\xb6\x95!" +
"ix\xa6\xf7\x94m\xdc)\x944\x82\xef\xef\xe7i{" +
"\x03gN\xf9\x8d\x11\xdbI\xf3^\x9b\x81\xeaze\xd0" +
"p\x07\x99\xb3\x87hAe\xe0\x1c\xcb\xbb<\xc1<\x88" +
"\xd0\xe4\xa7\xc0\xb6\x85\xa2\xfa\x19\x16C3@\x80l\xc7" +
"\xdb\xc0\x19`e<\xcfa##Fz\x00m]~" +
"#\x0a\x12\x84\x0csS\x1eK\xef\x09\x8e;j\xeb\x9c" +
"f\x01\x93&*\x0b\xdb\xa0\x03q\xfe\xb9\x8b\xd4\xd3m" +
"\x9a\xf6x0B\x95\xe5\xf0\xec\\\xca\xc8\xa0Ufq" +
"\xf2C\xcbt\xa3\x12O\xd8X\x8a'N\x95m\xe3X" +
"\x8a\xf7\x94ws\\g\xe6\x00\x9b\x80\xf8\x10\xcfpk" +
"2.\xe2\xd6\x93\xf6\x8c1^\x99\xb0\xc7]\x93\xf5\xf6" +
"\xae\x83n+\xc32\x01SEA\x1f\x1a.\x17\"\x9c" +
"&\xdb\x84\xb6\xd7\xe3\x8c\x06\x13Fd\xf3=4\xe1^" +
"'o\xb8\x81\x81D\xee\xcd(\xc9\xe5M\x97\xf7X:" +
"\xc47\x98\x86\xceKx8\x93sg\xf9\x8d{\xca\xb9" +
"|\xe7,;\xf7\xbf\xbb\x91\xfe\xf6\"+\x15\xa44\\" +
"\xc8\x89STv)\x9d\xb2-l\x1b\x12\xa7h\xab<" +
"E\x97\xf7\x8b\xb6p\xc9J\x80\xfd\xe9\xbc\xe3\x10i\xca" +
"9\\7\xd2\x1e\x07\xd4\xfd\x9c\xed\xba\xc6\xb0\xc9Kg" +
"\xab\xa5\x8b]\x10\x19a\x86y\xc3\x8b\xf0\xbe\xe2\x95k" +
"\xf9\xc6u\xca5\xf0\x1a\x80\xe4\xa3*&\x9f\x0c4\x16" +
"'\x87\x02w\xc3\x0aJ\xd7\xcf\xec\x0c\xdc\x0d\xab\x8al" +
"\xb0\xce\x1e\x04H>\xabb\xf2B\xe5\x1a\xf8<\xbd\xfd" +
"\x9c\x8a\xc9W\x02\xd7\xc0\xd7I\xf1\x9a\x8a\xc9\xf7\x14\xec" +
"f\"\xbb\xa6\xa4\xabIs\xad\xe4\x8e\xed1s\xbda" +
"\x82\xca]\xac\x01\x05k p}\xcc\xf5\xf5\x86\xc9]" +
"(K\xcc\xe2w+@\xb7tK\xdcP\xfcRP\x12" +
"a\x1f\xf7\x98a\xbaP\xb9F.\xff\xa7\x92\xe25\xf2" +
"l\x88\xc1\xa4,\xb8Q\xf0K\x9f\xd5\xfa\xba\xa5\x0f\x82" +
"\xba\x96\xd7`\x1d5w}*&\x07\x15,-\xc1\x00" +
"E;\xa1b\xf2\xde\xc0\x12l\xa5 nQ1\xf9-" +
"%0\xe5\x00\x15\x9a]\xd4\xfe?\x00\x00\xff\xff\xd6\xbd" +
"\x94\xef"
const schema_b526ba661d550a59 = "x\xda\x94zkp\x1c\xd5\x95\xff9\xdd3\x1a==" +
"\x1a\xdf1\xd8\x92\xc5\xd8\xc6\xc2\xc8A\xc4F\xf8\x1f\xe4" +
"\xe0\x08Y2\xb6U#\xacV\xdb\x16v\xe1\xaa\\M" +
"_\xcd\xb4\xdd\xea\x1ew\xf7H\x1a\x17.\x1b\xc7T\x01" +
"\x7f\xa8@6\x0f'\x156<LU\x92\x85\x84\xcd\xc2" +
"\x96\x97\"\xb5\x84\"U\x86\xc5U\x90\x0a\xa9<\xc8\xe2" +
"\xe4\x0bI\x96\x14\x9b,\xa9\xcdfC\xf5\xd6\xb9=\x8f" +
"\xd6#\x18}\xf1\xa8\x7f\xe7\xf4\xb9\xe7\x9e{\xee\xb9\xe7" +
"w\xdb[N\xb6\xdc\x16\xdb\xda\x96o\x05E{8\xde" +
"\x10\xa4\x8e\x1d\xfa\xb5\xed\x7f\xe74\xa4:18\xd4|" +
"\xa0k\xea\x85\xeb\x9e\x87X\x02\xa0oM\xc3ad\xbd" +
"\x0d\x09P\x83\xef~\x90\xed?\xfc\x9f/\xdf3_+" +
".\xd5\x9a\x1aN \xebjH\x00\xb05\x0d\xef\x02\x06" +
"w\x0e\xbd\xfc\xf9\xc9\xeb\xbeq/\xa4:\x95\xba2`" +
"\x1f&F\x90\xadJ\xd0K\xa9\x84\x13\x03\x0cz_-" +
"\xdd\xf9\xed5\x0f=\x02Z/6\x05/\xdb\xff\xdar" +
"\xf0\x95\xbb\xfe\x0b\xe2H:\x0f\xb5<\x80\xec\xa9\x16\xfa" +
"\xf3\xf1\x96w\x14\xc0\xe0\xef?\xf1~\xc9<q\xf8\xcb" +
"\x90\xbaV\x09\xce\xfc\xe9O\xd7\xf6\xfd\xc7\x1f\xce\x93\xe5" +
"\x9f\xad8\x83\xec\xf7+\xc8\x87\xdf\xae\xf8\x0a`\xb0\xf2" +
"9\xcc\xe7/^\xfa\xfa\x12\xd3\xeaON\"\x1bM\xd2" +
"\xb4z\x7f\x91Q\x87\x13\x85\xc7\x97\xd0\xeaI\x1eF\xb6" +
"CjYj\xdfm\x7f\x9elx\x12\xb4N\\4\xfb" +
"5\xa4\xd7\x9b\x0c_\x99@\xc0\xba_Z'\xb6G\xd4" +
"\xe5\xbc\x1fm\xef@\xf6L\xfb&\x80\xbeW\xdb'\x1a" +
"\x01\x83\xe1\xa1\x97^{\xe6\x9d'\x9fZ\x14\xadC\x1d" +
"g\x90Mw\xd0\x9c\xcc\x8eO\x01\x06\x07\xcek?\xbb" +
"a\xe6\xd2SKx;\xddq\x18\xd9=\x1d\xe4\xedw" +
"~\x91\xfb\xd2\xff\xff\xe6\xfb\xff\xb0\xc8\xde\x91\x8e\xedu" +
"{\xfb\x00\x83#\xcf\xbf\xbb\xf5\xf3\x1f\xbe\xf3\x9d%\xec" +
"\x9d${\x8fH{\x03\xef\xfc\xee\xe4\xe8\xba\x89g\x97" +
"\x1cu\xb2:\xeaE\xcc\xae7s\xf6\x85%\xb4\x8e\x90" +
"\xad\xe3R\xab\xf0\xe2\xdf}Q\xff\xf4\xf0\xf7\x97\xd0\x1a" +
"%[\\j\x9dUo\xbe,n\xdd\xf8\xf2\x12Z;" +
"HK\x93Z\xef\xff\xd3g\xde\xbce\xc7\xd1\x8b\x0bW" +
"E!\xbd\xde\x8e\x95\xc8vt\xc8\xe5\xee\xc8\xd0\xaal" +
"?4Q\xb4\xde\xfa\xc6\xbf-5t'\x0d\xddIF" +
"\xdf8\xf3X\xfe\x7f\x7f\xfe\xe5KK\x0dMZ\x9a\xd4" +
"\xeaz%\xf7\xf6\xdb\xfe\x997\x17\x85\xb8\xb7s'\xb2" +
"\x1d\x9dr\xd8N\x99\x0c\xfd\xfd/>\xfe\xfa\x03\xff\xfd" +
"\x13\xf2R]\x98bkG\x90\xedXK\x0b\xd2\xbf\x96" +
"6\xce\xb6\xec\xf0\xe7\xceM|\xf9\xe7K\x8c\xde\xd5u" +
"\x18\xd9\xd6.\x1a\xbd\xffs\xaf}\xed+\xf1\xaf\xfc;" +
"\x99T\x16\xa8\xb5u\xedD\xd6\xd5%\xf7b\xd7,`" +
"p\xfd\x9b?y\xff\x86]\xcf\xfdq\xd1\x8e9\xd9u" +
"\x14\xd9#R\xf3\xa1\xae<`}\xf3\xcd7\x1bn\xc5" +
"\x7f!\xedK]W\x03\xf4\xbd\xd5\x95H\x00\x06k\x1e" +
"}to\xd3{W}\xb0\x84\xaf\xbf\xec>\x8c\xec\x0f" +
"\xdd\xe4\xeb\x93\xb1\xe2\x87\x9f>\xfb\xd0\xff,\xa1u\x89" +
"\xb4~%\xb56\xff\xa6\xfb\x8ew\xef\xfa\xcc_\x16\xc5" +
"\xf3\xfb\xdd\x93\xc8\xde\xec&'/u\x9f\x02\x0c\x9e\xf0" +
"\xc6.\xbev\xe4\xc9\xbf,tR.z\xdbug\x90" +
"\xad\xbfN\x86\xeb:\x19\xfd\xd4?\xcf\xde\xf7\xfb\xc1\xc9" +
"\xbf.1\xfc\xa3\x9b&\x91\xfd\xe3&\x1a\xfe\xd4\xb9g" +
"\xdf\xd5\xcf\xdd\x17,\xcc\xa4\xb0\x08mz\x01\xd9S\x9b" +
"\xc8\x85\xc77}\x17z\x83\x9cp\x05\xb7>\x99\x8b\x95" +
"<\xdf\x99\xfedN\xfe\xdc\x98\xe3E\xbb\xb8}H>" +
"\x8c\x0bO\xb83B5n\x1eC\xfcH}\xdd\xe7\xee" +
"\x98i9\xfe\x10w\x87\x06\x1c\xdbw\x1dk\x0cQk" +
"Wc\x001\x04H\xf1\xc3\x00\xdagU\xd4,\x05S" +
"\x88i$\xd0\xfc\x02\x80f\xa9\xa8\xcd)\x98R\x944" +
"*\x00\xa9\x12i\xfa*j\xa7\x15L\xa9j\x1aU\x80" +
"\xd4\xc9\x11\x00\xedn\x15\xb5\xfb\x14\x0c\x0a%c\x88\xc6" +
"\x00\xd5\xb1p%(\xb8\x120\xf0|!\\\xd3\xcec" +
"\xd6\x9c6\xfd\xbd\xf6\x14:5\x99\xcdg\xcc<\xf7M" +
"P\x1d\xbb\x06\x16\x0b\x8e-v\x17=\x00\xc0\xf6z\x01" +
"\x07\xc4v\xf8\xe8\xd9\x8e\xf2\xa2\xb1\xd7.\x96\xfc\xfd\xe5" +
"\xa2\x00\xa0\x89\xbeH\xae\xb3^e\x04\x00W\xb0\x1e\xe5" +
"{\x00\x98d=\xca\x13d\x9c\xf5(\xdf\x06\xc0\x14\xeb" +
"Q^\x06\xc0\x95\xacG\xf99\x00\xb6\xb0^e\x12\x00" +
"\x19\xebQ^\x03\xc04\xeb\x95\xbf\xc8\xb6*'\x00P" +
"a\xbd\xcaQ\x00\\\xc5z\xe4\xf3U\xac[\xf9\x1d\x00" +
"^\xcdz\x94\x1f\x01\xe0j\xd6\xab\xfc\x1a\x00\xd7\xb0\xad" +
"\xf2\xb9\x83mS\xbe\x06\x80\x9dl\x9b\x1cw-\xdb&" +
"\xedu\xb1~\xf9|\x0d\xeb\x97~\xa9\x95\xe7\x18\xeb\x97" +
"\xfedX\xbf\xb4\xbb\x8e\xed\x90\xf6\xd6\xf7\x0d*\xdb\x11" +
"\x00\xe3l\xaf\xf2\x02\x006\xb0\xbd\xd2\x816\xb6K9" +
"\x0c\x80\xadlP:\xb6\x81\xed\x90/^\xcb\x06\xa5\xe1" +
"\x8d\x95\xdf\xee\xbeAe%\x19hf{\x95\x07\x00\xf0" +
":\xb6W\xf9#\x00n\xea\x1bU6\x90\xe0zv@" +
"\x86\xa0\xa7\xef\x90\xa2\x10\xb0\xb9\xefH\xf8\xc7'\x18W" +
"\xbe\x00\x8070._\xede\\\x06/Qynd" +
"\\9\x03\x80M\xec\x88\xfc\xbd\x91\x1d\"\x1f\x02\xc3\x99" +
"\xb5-\x87\x1b\x00\x10x\xc2\xdf\xcf\xdd\xbc@?\xcb}" +
"\xe1r+3\x98\xcb\x09\x8bp\xbd(\x84\x11\xa6\xc8\xbe" +
"\xa9\xa9\x84'\xfc\x05\xe8\x90c'){\x09\x1e\xe5\xc5" +
"\xa1R\xdc\x9d\x11R>\x14\xa65x\xc2?hz\xa6" +
"c\x0f\x95\xe6\x89\xd4\xf0\xa5\xac\x93\xcf\x0aH\xcc\x84\xe3" +
"IM%T\x95>\x91K\x83\xa1\x8fQ\xd9\xa8i\x87" +
"\xe2\x83\x00\x81+h&\xba\x80\x01\xdf7\xed\xbc\x17x" +
"|F\xe8\xc2\xf7!\x19>\x0a\x7f\x97\xcd'-\x18\x08" +
"\x87_h\xec\x80'\xa4\\\xe8\xc9\xaaXNE\x99'" +
"+\x0a\x81Fm\xf6\x8a\x9c}D\x9a\xa8\xbc\xb9\xc7\xb1" +
"\x8c\xac\xc2=_\x17\xc2\x96\xaa\xa4\x89~$\xca\x12\x1d" +
"\x11\xaa{l!8\x98KT\x02/Q%D\xf7\x9b" +
"\xd3b\xdf\xd4\x94'\xfc0\x10\xc3b\x8a\x97\xd0\xf2\xb3" +
"\xdc\x16\x13f\xc2\xf0\x0b5\x97\xb1\x1a\xb7\x8c\x0c\\@" +
"\x81!u,Y>E\xc4LP@\x08\x1d\x179'" +
">=-lC\x18Rb\xe7=Z+\xddrf\x87" +
"\x9dY\xfbv\xc7\xbdC\xcc\x85\x0ed\x934\xd9\xfa\xdc" +
"\x0f\x14\xe7I\xcdDEJs\xd7\xd5\xea\x9c\xfd\x89\x82" +
"i\x89\xa1\x02\xb7\xf3\xa6\x9d\xd7\xc5@\xa8.G\x1f\x13" +
"\xae\x87\xa6\xe7\x0b\xdb'A\xb8l9n\xe7\x845\xec" +
"\xc0@\x98\x9b\x95\xf4\x18\xf1\xa8\x0eU\x1et\x07\x92%" +
"7'\xe4\xa2\xce\xf9\xc2Uln\xd5\xc2</\x1d\xa5" +
"\x18\xab\xe2Lv\xde\x1c\xc2\xec\x1ds\xcd\x8c\xe3\x9a~" +
"\xb9\x86\xab\xa1\x19rZ\x8c\x8b\xe3%\xd3\x15\x1e\xed\x86" +
"\"\xfa\x01\xa7__/bu\xb4p9\xc6\\\xe1y" +
"\xcan\xee\xedw\x06+\x1a\xf3\xc6\x1b4\x8e\x96<\x95" +
"\xc2\x1f\xaefT\xab\x1e;\x09*~}*\xb4\xea\x8e" +
"Z\xf2kC4\xc9!\xf6\xcd\x08\xd75\x0dQW\xa4" +
"U\x1brl\xc3\xf4Mga0\xaa;P/p\xc3" +
"\x99\x1d\xe2\xae\xees\x1fe\xf4$\x84\xb3\xa3\x8e!\xac" +
"\x837\x85S\xa9`\xbb\x8b^\xd6\xc9\xf1$\x19$x" +
"\xc4s\xec1\xe4~\xe1v\xcb\xe1\x95YK\xac\x81\xfb" +
"\x85\xfdbN\xba \x01\xee\x17v:\xd1!+\xa6\xc8" +
"\x12\xadG\xd2\xb5\xb9U;,\xe2W8\x1a\xc9Yq" +
"\xa3<s\xe8\xd0\xb8\xb9z:\xb2#x\x02@\xbf\x0b" +
"U\xd4\x0bX? \x99\xc0\x11\x00\xdd \xbc\x88\xf53" +
"\x92M\xe38\x80n\x11>\x87\xf5c\x92\x95\xa4\xbeO" +
"\xf8i\xc2c\x8di\x8c\x01\xb0\x93x\x13\x80>G\xf8" +
"Y\xc2\xe3Mi\x8c\x03\xb0{\xf00\x80~\x9a\xf0\x07" +
"\x09ohNc\x03\x00\xbb\x1f\xbf\x06\xa0?H\xf89" +
"\xc2\x13-ij\"\xd8\x97\xf0\x01\x00\xfd\x1c\xe1\xe7\x09" +
"olMc#u\x14R\xff<\xe1\xcf\x12\xde\xd4\x96" +
"\xc6&\x00\xf6\x0c\xba\x00\xfa\xd3\x84_ \xbc9\x91\xc6" +
"f\x00\xf6<>\x01\xa0_ \xfc\x15Tp}\x8b\x92" +
"\xc6\x16\x00\xf6\x03\xdc\x0e\xa0\xbfH\xf8ET\x10[\xd3" +
"\xd8\x0a\xc0~\x88\x9b\x01\xf4\x97\x08~\x1b\x15\x0c<\x87" +
"\xb6\xcc\xa8\x03\x03\xb6Ci\x85M\xa0`\x13``q" +
"\xdf\xf4K\x86\xa03\xbd\x05\x14l!\xcc\xb1\xf3\x04\x02" +
"\x8a\x1a\xc6\xadEz\x19On\xa3fP\xb0\x190\x98" +
"\x14\x9c\xba\x89aPE\xbe\x06\x16\x1c\xd7<\xe1\xd8>" +
"rk0\x97+\xb9\\\xcd\x95k\xc2\x19\xe1\xfaf\x8e" +
"[\x18\x8are:\x00\xe7[\x1b\xc4\x8ah8j\xd5" +
"\x0b+e\x0e2RV\xc3K\xb69'g\xe7\xf9|" +
"\xba8jZ\x09\xcb\xf40\x0e\x0a\xc6\x01\x07\x0a\xdc\xbb" +
"\xdd\x9cC\x04\x05\x1109s\xc7\xaea\\\x018\xa6" +
"\xa24\xb0\"\xd2\xc3\xc4\xffF\x0f\xb3\xaf\xe4\xdf\xa8\xf3" +
"\xe9\xa2%(1K\x9eL\xca\xd5\xb2\x09\x1b\xdcI\x8d" +
"H\xaa\xff&\xeaCR[\xe9\xd4WS\xbd\x04\xc6R" +
"\xdd\xd4\x0b\xc4S\xeb\x8f\x02\x9c*\xd9\xc7lg\xd6\xce" +
"\xd8\xce\xee\xa2\x17\xd8\xce\x903#\\\x0ej^\x9c\xb2" +
"\x9dQ\xee\xe7\x0a\xc14\xfd+\x8c;`\xc0\x09KH" +
"\x05\xc8BX\x85?v\x1f\x9a0\xb6\xdet\xa5Ft" +
"A\xe3\xda\x7f%}\x0a\x03\xedc:5\xf6\xa9%_" +
"F\xa0\xd6\xb4~\xf5\x01\x00\xed\xeb*j\xdf\x8c4\xad" +
"OQ+z^E\xed\xd9H\xd3\xfa\xccf\x00\xed\x9b" +
"*j\xafD\x9a\xd6\x1f\x90\xe6K*j\xaf+\x881" +
"\xb9\x13S\xaf\x9e\x01\xd0.\xaa\xa8\xfd\x98\xb6!\xcam" +
"\x98zs\x12@{CE\xedm\x05k\x9d\x0c\x8e\xb9" +
"N\x9e\xea\xa4\xecOk\x84\xa1\xd2\x9fz\xd5#\x8e\xfa" +
"1P\xb0\x150Y\xe4~\xa1\x9a\x03\xedu\xc6\x06(" +
"\xb3\xa1\xe8x\xb2\xa2\x86\xfdn\x8d|U\xecY\x8eS" +
"\x1c\xe7\xbe\xc0AZ\xc0\xbc\xa8\xe7nU\x02\x89Q\xd3" +
"\xae\xa1\xd5\xa0\xaa\x7f+\xb7\xd4\x92\xaf5b\xf4\xaa\xa3" +
"\xe9h\x84\xc55\x8d\x07\xd5\xd4\x83$%_\xb0\xbb\xe8" +
"\xe9\xb4\xaf\x01\x85v\xaeV\x19\xd7\xc4v\x02\xe8\xe9\x98" +
"\x8a\xfa\xbaX\xa42v\xc5\xa8R\xac&|c,R" +
"\x19\xd7\xc7\xa8\x02\xae#\xfc\x86\x98\x82X)\x8c=1" +
"*t\xd7\x13|3\xa9\xc70,\x8c[cTx\xb7" +
"\x10~+\xe1q%,\x8c\xfd\xb1\x17\x00\xf4[\x09\xdf" +
"Cx\x83\x1a\x16\xc6]r\xd8\xdb\x08\xcf\x12\x9e\x88\x85" +
"\x85q\xaf\xb4\xbf\x87\xf0\xfd\x847\xaaaa\xd4bT" +
"\x18\xf7\x13\xfeY\xc2\x9bbaa<\x12\xa3\xc2x\x17" +
"\xe1\x05\xc2\x9b\xe3aa\x14\xb1/\x00\xe8\x05\xc2}\xc2" +
"[\x1a\xc2\xc2x<\xf6#*\xe0\x84\x9f%\xbc\xf5r" +
"X\x19\xef\x91\xfe\xdcM\xf8}\x84\xb7u\xa5\xb1\x0d\x80" +
"\xdd\x1b\xa3\x82\x7f\x9a\xf0\x07\x09_\xf1\xab4\xae\xa0\xc2" +
".\xfd\xbc\x8f\xf0/\x12\x9elLc\x12\x80=\x12{" +
"\x8d\x0a;\xe1\xe7\x09ooJc;\x15v\x19\x9f\xc7" +
"\x08\x7f\x9a\xf0Ts\x1aS\x00\xec[r^O\x13~" +
"\x81\xf0\x95\xc94\xae\xa4\xc2\x1e\x9b\x04\xd0\x9f#\xfc%" +
"\xc2YK\x1a\x19\x00\xfb~\xec{T\xc1\x09\x7f\x9d\xf0" +
"tk\x1a\xd3\x00\xec\xd5\x18\x1d(\xaf\x13\xfeS\xc2W" +
"\xb5\xa5q\x15\x00{K\xc6\xe7\xc7\x84_&\xfc\xaa\xb5" +
"i\xbc\x0a\x80\xfdR\xea_&\xfc=\xc2\xaf\xfeu\x1a" +
"\xaf\x06`\xbf\x95\xfe\xbcG\xf8\x9f\x09_\xdd\x95\xc6\xd5" +
"\x00\xecO\xb1\xa3\x00\xfa\x071\x15\xc7\xe3\x0a\xa6\xd64" +
"\xa6q\x0d\x00\xfbP\x86\xe7\xcf\xa4\x1e#\xbc#\x9e\xc6" +
"\x0e\x00\x86qr3\x16WQo'\xbc\xf3\x9a4v" +
"\x02\xb0\xb68\xb9\xd3J\xf8j\xc2\xd7f\xd2\xb8\x16\x80" +
"\xad\x8aS8\xd3\x84\xaf#\xbckc\x1a\xbb(;\xe3" +
"t>\xaf%\xfcz\xc2\xafiN\xe35\x00\xac;N" +
"\xe1\xd9H\xf8\x16\xc23-i\xcc\x10q\x8c\xd3\xb4\xb6" +
"\x10~+\xe1\xebZ\xd3\xb8\x8e\xd2P\xe2\xb7\x12\xbe\x87" +
"\xf0\xf5mi\\Oi\x18?\x03\xa0\x0f\x13>F\xf8" +
"\x06\x96\xc6\x0d\x00l4N\xe93F\xf8]\x84_\xbb" +
"6\x8d\xd7\x02\xb0CR\xffN\xc2\x0d\xc276\xa4q" +
"#\x00\xe3R\xdf \xbcHxw2\x8d\xdd\xd4_\xc4" +
")l\x16\xe1sq\x05O\xcd\xf2\xf2\x1d|ZT+" +
"\xcd\xc0,/\x8f\x8b\xa9\xeac\xe0:\xdc y\xa4\x18" +
"\x85\xa7Z\xd6\x9c\x06\xd5\xf4kU\xc3\xae\xf4\xd80\x10" +
"vt\x8b\x04\x18\xe2\xc3\xe6\x80\xe7S\xf7\\U\x18(" +
"\xf0\x13\xdc5j\xd6I\x7f\x0f?\xc1A]\x02D\xd7" +
"\x186\xe9}\xb5n \xe0\xc6\x8c\xe99n\x192\xfa" +
"\xbc\x83\x9e^\x1a4fL$a\xe8\xc2\"\x99R\x95" +
"U\xec\xe6\xb0\xee\x98c\x8b\x09^\xae\x9e\xc3\x19\x8b\xdb" +
"\xc2\xc3\x06P\xb0\x010\xf0MKd\x89\x95\xaa\xc2\xa8" +
"\xaa\xd4\"\xa3\x98\xbe^\xca\xe7\x85\xe7\x0bC\x1a\x8f4" +
"\x0c^E\x00\x03\xc6|w\x85\xe7\x9b\xd3T\xa7\x8dq" +
"\x87\x1b\x13\xa6\xa1\xfa\x85\x9a\x90\xd6\x81\x9adH\x889" +
"\x1f\x93\xf5+T@L\x02\x06\x86Y\x89\xea\xed\xae3" +
"=\xc1\xcbC\x19a\x13\xa7\xa8\xb50\x15^\x8bUb" +
"\x1b\xf1h\x9a\xe8\x98;#\x16\xc6o\x96\x97ua\x89" +
"\x1c\xd2\xd9\x12^\x88`\xb2~\x13V\x19\xb9:g4" +
"Cb\xe0G\x03R0\xf3\x85Y^\x1e\x82\xa4\xc5=" +
"\x0f\x93\xf5{\xc9\xf0\xed\xcc,/\xef5\xaa\x9dO\x90" +
"\xabp\x83\x05T\xa8\x9ey\xf2\\9( \xe3\xd2t" +
"0\x01\x0a&*\xd1\xa1F\x07\xd4\x12\x0dR;\x98*" +
".\xe6\xeb\xe7\x0f&\xeb'U]:\xea\xd8\xce~H" +
"D\xfb\xcd\x9c3],\xf9\xc2@)3i\x07\xd4d" +
"a\x93\xba[\xa0-\\\x1e\x1e\xbc5Y\xd1u\x8cR" +
"N\xb8\xa8\x0b\x8f|\x8cJ\xc4\x8c)fu\xac3\xed" +
"Z\xa8+\xb2a\xac,#,\x94\xecD\xa7d\x1b\xdc" +
"-\xef\xc5\xfa\xae\xa8\xc8& 9?\x8a\x1f\xc5Yj" +
"\x94+\xa4\xe9rY\xa95j\x94\xcdNj\xbbl\x0e" +
"\x9bv\x02\xd06\xf5\xcd\xdc\xa9\xa2ps\xc2\xf6\x97\xd3" +
"\x9bm\x8b\xf6fK\xb5\x11\x94\xe0\xf22Q\xcc\xc9\xbe" +
"\xacU\x0e\xde\x15v\xa6\xab6\xcb\xce\xb4m'\xc0\xa9" +
")W\x88Y^N\xe6L\xbf\\\xedH\x97\xe3\xc9\xd6" +
"eu\x95\x09c\xeb\xcd\xcblC?\xb5\xdc\x01\xb6-" +
"\xf7\x85\xadW\x0a\xa6\xbc\xb3D\x9b\xe2\x98\xae\xf5\xb7'" +
"\xa9k\x9dSQ;\x1b\xe9o\xef\xb9\xa9~\xd5\x8a\x95" +
"\xf6\xf6\xde\x0d\x00\xdai\x15\xb5\x07\xa9\xbdm\x0f\xdb\xdb" +
"\xfb\xe9\xed\xb3*j\x0f\xd7\x1b\xaa\xd4C\xd4\xf3>\xa8" +
"\xa2vN\xc1\xa4_.\xd2^\xaa}G\xabl\xe7)" +
"b\xdd\xd5\xdcMx\xbe[\xeb`'\x1d\xc7\xaa\x95\x85" +
"\xa3\x15\x02\x1e=U\x96\x13\x93-\xcb\x0db\xdf\x95^" +
"\xd8S)TT\xa6\xe4~\xb8EF\xe7\xf10%\xbf" +
":\"S\xf2KG%Yz\xe4&I\x96\xee\x1f\x97" +
"d\xe9^RiH\x9d\x9c\x04\xc0D\xaaL`c\xaa" +
"\xe4RIH\x1d\xa7\xf7\x9aS\xd3\xf4^K\xca\xa4\x9f" +
"\xd6\x94 \xcd\xb6\x14\x8f\x90\xac`\xda\xf1\x1dw\x96\x97" +
"\x01\xa0\xfew2k\xda\xc72\xbe[\xb2\x8f\x05\xf2\xdf" +
"\xaci\x03\x1e;Ut\xcdi\xee\x96\x83\xcao\x16\x12" +
"\xa6},\xf0\x04UO\xee\x02\x96\xeb\x7fg\xcad#" +
"\xf0\x89\xc0rW\x9a\xaf\xfd-\xcd\x07%;Gs6" +
"!9e\x0a#p\x85g\x1a\x82\xce\x19\x93[\x81e" +
"\xce\x98v\x9e\xfaxW\x08\xff\x8at\x8b\x96t\xcc1" +
"m?\xbc\x04\x89|\"\x18Y\xea\x13\xc18\x80VP" +
"Q\xf3\xa9\xc9\x8f\x85\xe9x\x9c\xc0\xa2\x8a\xda\xdd\x94\x8e" +
"\xf10\x1d\xcb'\xea\xc9\xfc\xb1/\x02r%w\x86\xfb" +
"%\x17\xea'z\xe0\x87\x17\xb1\x02\x06,\x87J\xca\"" +
"\xce\xf31v|\xdf\xc7!\x9ecUF&Kk-" +
"\x0e=\x14\x87\xebU\xd4n\x8e\xc4a+My\x8b\x8a" +
"\xda\xad\xcb\x99\xdd\x15\x1c\xd8W\xf2o\x94\xb4K^A" +
"\xca\xd5\x90\xf1\xed\xd9,\x13z}\x98\xd0]#2\xa1" +
"\xd7l\x00H\xda\x8e-\x02\x93\x9a\x06\x9b[\x94(b" +
"\xae\xf6w\"\xc7\xdd\x8f\xfd\x95HO\xfa\xdc\x17Z\x0c" +
"#\xdf\xceS\xb8!\xb1\xbb\xe8i\xa7k\xec\xef\xb8J" +
"}hQUQ\xbf[\x8d\xb0\xbf\xb2J\xf4\xecn\xc2" +
"\xefS#\xec\xef^\x95\xfa\xeb\xb3\x84?L\xb8\x8a!" +
"\xfd{H}\x02@\x7f\x98\xf0\xaf\x13\x1eSC\xfa\xf7" +
"Ui\xff\x1c\xe1\xe7\x09\x8f\xc7B\xfa\xf7\xb8J\xfd\xf2" +
"c\x84?MxC<\xa4\x7f\xdf\x92v\x9e&\xfc\x02" +
"\xe1\x89\x86\x90\xfe=\xaf\xbe\x0c\xa0_ \xfc\x15\xc2\x1b" +
"\x13!\xfd\xfb\x81J\xec\xf5E\xc2/\x12\xde\xd4\x18\xd2" +
"\xbf\x1f\xaa\xc4\x1b^!\xfc\x0d\xc2\x9b\x9bB\xfawI" +
"%\xf6\xfa:\xe1?%\xbc\xa59\xa4\x7foI?\x7f" +
"L\xf8e\xc2[[B\xfa\xf7K\x95\xfa\xf7\xcb\x84\xbf" +
"Gx[kH\xff~\xab\x12\xff\xf8\x0d\xe1\x1f\x10\xbe" +
"\xa2-\xa4\x7f\x7fP\x89&}\xa0\xaa8.\xd9\xdf\x8a" +
"\x90\xfd}(\x87\xfd+\xa97J\xf6\x97\x0c\xd9_\\" +
"\xb2\xb9\x18\xd1\xa4v\xc9\xfe\xdaC\xf6\xd7&\xd9Y+" +
"\xe1\xab%\xfbK\x85\xeco\x95d\xcduR\xce\xd4\x90" +
"\xfd\xad\x97\xacv#\xe1[$\xfb[\x19\xb2\xbf^i" +
"\xe7\x06\xc2o\x91\xec\x8f\x85\xeco\x9b\xd4\xbf\x85\xf0a" +
"\xc9\xfe\xd2!\xfb\x1b\x94,r\x98\xf01\xc9\xfeV\x85" +
"\xecoT\xda\xc9\x12~\xa7d\x7fW\x85\xec\xef@\xec" +
"\xcc<\xf6\xbd\xe6\xea\x90\xfe\x1d\x91\xac\xb6\xce\xbe;n" +
"\x0b\xe9\xdfB\xf6\xdd9\x18\xd2\xbf\xe3\xd2N\x91\xf0\xbb" +
"\x09_\xbb3\xa4\x7fe\x89\xd7Yy\xd7PH\xff\xee" +
"\x91x\x9d}_3\x1c\xd2\xbf\xfb\xa5\x9f5\xf6\x8d\x99" +
"\x90\xfd=\x12\xdb\x00\xa0?H\xf0\xb9\x98\x82\xf2^\xde" +
"\x1as\x05$=/\xd2&sk\x96\x97\xbd}6V" +
"\xbej\xed\x1a\x90\x9fj\xea\x0a\x93.?&\xb2f\x1e" +
"\x12\x05\xdf\xab\xa1\x06\xf7\x0a\x93\x0ew\xd1\x88\xdc\xd0\xd7" +
"j\x9a!\x96\x1c\xab\xc6\x14\xe4M\xbd0`\xb1$\xeb" +
"\xd8y\x92&\x96zO9(\xdcrU#\xfa\xfe)" +
"\x91sv\x0b\xee\xd6\xde\x98r\xdc\x9c\x18r8\xa8\x9e" +
"_\x03Mo\x8c\xbb\xc7\x88\x7f\xd4\x07.\xf2\x92'\xb2" +
"\xdc\x87$M~\x01\xec\xd8(\xab\x9fis\xb4\"L" +
"\xcbq\xfd\xdd\x82\x03\xd6\xc7\xf3]>5e\xe6F\xd1" +
"1\xc2o]Q&\x92\xe7\x9e\xee\xf3\xdc\xb1\xe8\xb8\xd3" +
"\x8e!h\x160o\xa2aa\x1bs!#>r\x91" +
"\x06\x07,\xcb\x99\x8dF\xa8\xbe\x1c\xbeS\xd4\xcd<\xda" +
"5\xba\x18~0Zl4\xc4\xb3\x0eV\xe3\x89\x0be" +
"\x07\x05V\xe3\xbd\xe0\xdd\xa20\xb85\xca\xe7 3." +
"\xf2\xc2\x9e\x8f\xcb\xb8\x0d\xe6|sF\xd4'\xec\x0b\xcf" +
"\xe2CC\xbb`\xc0\xce\xf3|\xc4TE@\xfcCH" +
"\x11.\x92\xdd\x81\x8e?\xe8NG\x13Ff\xf3\x1e\x9a" +
"\xf0\x90[2\xbd\xc8@2\xf7\x96\x94\x14K\x96'\x06" +
"m\x032\xbb-\xd3\x10U<\x91/z\xcb\xfcV\xbf" +
"\xe0\\\xbee\x99\x9d\xfb\xff\xbb\x92\xfeD\x85\xfeJ\xf6" +
"\x9b(\x17\xe5)\x1av)\xdb\xc2\xb6\xb0w\\\x9e\xa2" +
"=\xe1)\xda=\"\xdb\xc2\xf5\x9b\x01N\xe5J\xaeK" +
"\xa4\xa9\xe8\x0a\xc3\xcc\xf9\x02\xd0\x08\x8a\x8e\xe7\x99\x93\x96" +
"\xa8\x9e\xad\xb6!wAr\x8a\x9b\xd6\x15/\xf4\x87+" +
"W\xc7\xb5\x9b\xe3\x05\xd7\xd9\xdb\x01\xb4/\xaa\xa8=\x16" +
"i,\x1e\x1d\x8f\xdcq+\x18\xba\xfe\xd4\xe1\xc8\x1d\xb7" +
"\xaa\x84\x0d\xd63g\x00\xb4\xa7U\xd4.\xd4\xaf\xb3\x9f" +
"\xa7\xb7\x9fSQ{#r\x9d}\x89\x14_WQ\xfb" +
"\x8d\x82\x03\\f\xd7\x82t\xb5h\xae\xf5\xdcq|n" +
"\xddnZ\xa0\x0a\x0f\x1bA\xc1F\x88\\\x83\x0b\xe3v" +
"\xd3\x12\x1e\xd4$V\xe5\xfb\x1b\xa0W\xbd\xedn\xad|" +
"\xf1\xa8\x8apX\xf8\xdc\xb4<\xa8_\x87\xd7\xfesL" +
"\xe5:|9\xc4`^\x16\\)\xf8\xd5\xcf\x83\xc3\x03" +
"\xa1\x0f\x92\xba\xd6\xd6`\x175w\xc3*jc\x0aV" +
"\x97`\x94\xa2\x9dUQ\xbb3\xb2\x04\x07(\x88\xfbU" +
"\xd4>\xabD\xa6\x1c\xa1B\xcb\x8b\xda\xff\x05\x00\x00\xff" +
"\xff1<\xb37"
func RegisterSchema(reg *schemas.Registry) {
reg.Register(&schemas.Schema{
+1
View File
@@ -0,0 +1 @@
{"description":"Synthetic Go-produced v2 current-road and data-only preview fixture; no real road data.","event_base64":"AAAAACQAAAAAAAAAAgABAADyBSoBAAAAjwAAAAAAAAAAAAAAEAAHAAAAoEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIABQABAAAAAAAAAAIAAAAAAHBBgAELJwEAAAAA8gUqAQAAAAEAAAAAAAAAKQAAAAAAAAAAAHBCAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAoCAABhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhAAAAAAAAAAA="}
+52 -2
View File
@@ -1,7 +1,6 @@
package cereal
import (
"bytes"
"capnproto.org/go/capnp/v3"
"encoding/base64"
"encoding/json"
@@ -61,7 +60,58 @@ func TestMapdV1HostWireFixture(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(raw, expected) {
actualMessage, err := capnp.Unmarshal(raw)
if err != nil {
t.Fatal(err)
}
oldMessage, err := capnp.Unmarshal(expected)
if err != nil {
t.Fatal(err)
}
actualRoot, err := actualMessage.Root()
if err != nil {
t.Fatal(err)
}
oldRoot, err := oldMessage.Root()
if err != nil {
t.Fatal(err)
}
// Appended default fields can enlarge storage without changing old values.
equal, err := capnp.Equal(actualRoot, oldRoot)
if err != nil || !equal {
t.Fatal("provider wire differs from shared host fixture; review both schemas")
}
}
func TestPreviewHostWireFixture(t *testing.T) {
data, err := os.ReadFile("testdata/mapd_preview_wire.json")
if err != nil {
t.Fatal(err)
}
var fixture struct {
EventBase64 string `json:"event_base64"`
}
if err := json.Unmarshal(data, &fixture); err != nil {
t.Fatal(err)
}
raw, err := base64.StdEncoding.DecodeString(fixture.EventBase64)
if err != nil {
t.Fatal(err)
}
msg, err := capnp.Unmarshal(raw)
if err != nil {
t.Fatal(err)
}
event, err := log.ReadRootEvent(msg)
if err != nil {
t.Fatal(err)
}
out, err := event.MapdOut()
if err != nil {
t.Fatal(err)
}
boundary, err := out.PreviewBoundaryId()
if err != nil || !event.Valid() || out.SampleVersion() != 2 || out.SpeedLimit() != 20 || out.PreviewSpeedLimit() != 15 || out.PreviewDistance() != 60 || len(boundary) != 64 || out.PreviewWayId() != 2 {
t.Fatal("provider preview differs from shared host fixture")
}
}
+82
View File
@@ -0,0 +1,82 @@
package main
import (
"crypto/sha256"
"fmt"
"math"
"pfeifer.dev/mapd/cereal/custom"
"pfeifer.dev/mapd/maps"
)
const previewHorizonM = 500
const previewMaxWays = 64
type LimitPreview struct {
Speed float32
Distance float32
BoundaryID string
WayID int64
}
// A preview follows only a unique legal continuation, never a guessed turn.
func (s *State) previewLimit() LimitPreview {
if !s.ShadowOnly || s.SnapshotID == "" || s.CurrentWay.SelectionType != custom.WaySelectionType_current || s.MatchedLimitMps <= 0 {
return LimitPreview{}
}
way, forward, position := s.CurrentWay.Way, s.CurrentWay.OnWay.IsForward, s.Position
distance := float32(0)
seen := map[int64]bool{way.Id(): true}
for range previewMaxWays {
_, end := way.GetStartEnd(forward)
if distance+position.DistanceTo(end) > previewHorizonM {
return LimitPreview{}
}
segment, found, err := way.DistanceToNode(position, forward, end)
if err != nil || !found || segment < 0 || math.IsNaN(float64(segment)) || math.IsInf(float64(segment), 0) {
return LimitPreview{}
}
distance += segment
box := s.Data.OverlapBox()
if distance <= 0 || distance > previewHorizonM || !box.PosInside(end) {
return LimitPreview{}
}
matches, err := way.MatchingWays(&s.Data, end)
if err != nil {
return LimitPreview{}
}
legal := map[int64]maps.Way{}
for _, next := range matches {
first, last := next.GetStartEnd(true)
if first.Equals(last) && !next.OneWay() {
return LimitPreview{} // A two-way loop has two possible entry directions.
}
if next.Id() > 0 && (next.IsForwardFrom(end) || !next.OneWay()) {
legal[next.Id()] = next
}
}
if len(legal) != 1 {
return LimitPreview{}
}
var next maps.Way
for _, candidate := range legal {
next = candidate
}
if seen[next.Id()] {
return LimitPreview{}
}
nextForward := next.IsForwardFrom(end)
road := CurrentWay{Way: next, OnWay: maps.OnWayResult{IsForward: nextForward}}
speed := road.EffectiveMaxSpeed()
if speed <= 0 || math.IsNaN(speed) || math.IsInf(speed, 0) || speed > math.MaxFloat32 {
return LimitPreview{}
}
if math.Abs(speed-float64(s.MatchedLimitMps)) > .02 {
key := fmt.Sprintf("%s:%d:%d:%t:%.7f:%.7f", s.SnapshotID, way.Id(), next.Id(), nextForward, end.Lat(), end.Lon())
return LimitPreview{float32(speed), distance, fmt.Sprintf("%x", sha256.Sum256([]byte(key))), next.Id()}
}
seen[next.Id()] = true
way, forward, position = next, nextForward, end
}
return LimitPreview{}
}
+200
View File
@@ -0,0 +1,200 @@
package main
import (
"crypto/sha256"
"fmt"
"math"
"os"
"path/filepath"
"strings"
"testing"
"time"
"capnproto.org/go/capnp/v3"
"pfeifer.dev/mapd/cereal"
"pfeifer.dev/mapd/cereal/custom"
"pfeifer.dev/mapd/cereal/offline"
"pfeifer.dev/mapd/maps"
m "pfeifer.dev/mapd/math"
)
func previewState(t *testing.T, specs []syntheticWay, index int, forward bool, latitude float64) State {
t.Helper()
tile := packedRoadTile(t, specs)
return State{ShadowOnly: true, SnapshotID: "verified-snapshot", Data: tile,
Position: m.NewPosition(latitude, -97.9), MatchedLimitMps: float32(specs[index].speed),
CurrentWay: CurrentWay{Way: tile.Ways.At(index), OnWay: maps.OnWayResult{IsForward: forward}, SelectionType: custom.WaySelectionType_current}}
}
func previewRoads() []syntheticWay {
return []syntheticWay{
{1, 20, 35.1, -97.9, 35.101, -97.9, false, 2},
{2, 20, 35.101, -97.9, 35.102, -97.9, false, 2},
{3, 10, 35.102, -97.9, 35.103, -97.9, false, 2},
}
}
func TestPreviewRawDistanceAndStableBoundaryAcrossApproachWays(t *testing.T) {
s := previewState(t, previewRoads(), 0, true, 35.1005)
a := s.previewLimit()
s = previewState(t, previewRoads(), 1, true, 35.1015)
b := s.previewLimit()
if a.Speed != 10 || a.WayID != 3 || a.BoundaryID == "" || a.BoundaryID != b.BoundaryID || math.Abs(float64(a.Distance-b.Distance)-111.2) > 1 {
t.Fatalf("unexpected road preview: %+v %+v", a, b)
}
s.Position = m.NewPosition(35.1014, -97.9)
if s.previewLimit().Distance <= b.Distance {
t.Fatal("raw distance was held at a previous minimum")
}
s.SnapshotID = "changed-snapshot"
if s.previewLimit().BoundaryID == b.BoundaryID {
t.Fatal("boundary identity survived a different snapshot")
}
}
func TestPreviewDirectionUnknownLimitsBranchesAndHorizon(t *testing.T) {
s := previewState(t, previewRoads(), 2, false, 35.1025)
if p := s.previewLimit(); p.Speed != 20 || p.WayID != 2 || p.Distance <= 0 {
t.Fatalf("reverse traversal failed: %+v", p)
}
for _, mode := range []string{"branch", "unknown", "oneway", "horizon", "unqualified", "unverified"} {
t.Run(mode, func(t *testing.T) {
roads := previewRoads()
index, forward, latitude := 0, true, 35.1005
switch mode {
case "branch":
roads = append(roads, syntheticWay{4, 30, 35.101, -97.9, 35.102, -97.899, false, 2})
case "unknown":
roads[1].speed = 0
case "oneway":
roads[1].oneWay = true
index, forward, latitude = 2, false, 35.1025
case "horizon":
roads[0].lat1 = 35.09
latitude = 35.091
}
s := previewState(t, roads, index, forward, latitude)
if mode == "unqualified" {
s.CurrentWay.SelectionType = custom.WaySelectionType_possible
}
if mode == "unverified" {
s.SnapshotID = ""
}
if p := s.previewLimit(); p != (LimitPreview{}) {
t.Fatalf("uncertain preview escaped: %+v", p)
}
})
}
}
func TestPreviewClosedWayRequiresOneLegalDirection(t *testing.T) {
for _, tc := range []struct {
name string
oneWay, branch, wantPreview bool
}{
{"one-way loop", true, false, true},
{"two-way loop", false, false, false},
{"two-way loop plus continuation", false, true, false},
} {
t.Run(tc.name, func(t *testing.T) {
roads := []syntheticWay{
{1, 20, 35.1, -97.9, 35.101, -97.9, true, 2},
{2, 10, 35.101, -97.9, 35.102, -97.9, tc.oneWay, 2},
}
if tc.branch {
roads = append(roads, syntheticWay{3, 15, 35.101, -97.9, 35.102, -97.899, false, 2})
}
decoded, err := capnp.UnmarshalPacked(packedRoadTileBytes(t, roads))
if err != nil {
t.Fatal(err)
}
root, err := decoded.Root()
if err != nil {
t.Fatal(err)
}
msg, _, err := capnp.NewMessage(capnp.SingleSegment(nil))
if err != nil {
t.Fatal(err)
}
if err := msg.SetRoot(root); err != nil {
t.Fatal(err)
}
raw, err := offline.ReadRootOffline(msg)
if err != nil {
t.Fatal(err)
}
ways, err := raw.Ways()
if err != nil {
t.Fatal(err)
}
loop := ways.At(1)
loop.SetMaxLon(-97.898)
nodes, err := loop.NewNodes(4)
if err != nil {
t.Fatal(err)
}
for i, p := range [][2]float64{{35.101, -97.9}, {35.102, -97.9}, {35.102, -97.899}, {35.101, -97.9}} {
nodes.At(i).SetLatitude(p[0])
nodes.At(i).SetLongitude(p[1])
}
data, err := msg.MarshalPacked()
if err != nil {
t.Fatal(err)
}
tile := maps.ReadOffline(data)
if !tile.Loaded {
t.Fatal("closed road tile rejected")
}
s := State{ShadowOnly: true, SnapshotID: "verified-snapshot", Data: tile,
Position: m.NewPosition(35.1005, -97.9), MatchedLimitMps: 20,
CurrentWay: CurrentWay{Way: tile.Ways.At(0), OnWay: maps.OnWayResult{IsForward: true}, SelectionType: custom.WaySelectionType_current}}
p := s.previewLimit()
if tc.wantPreview {
if p.Speed != 10 || p.WayID != 2 || p.Distance <= 0 || p.BoundaryID == "" {
t.Fatalf("legal loop preview missing: %+v", p)
}
} else if p != (LimitPreview{}) {
t.Fatalf("ambiguous loop produced a preview: %+v", p)
}
})
}
}
func TestShadowPreviewSharesFixFreshnessWithoutControlOutputs(t *testing.T) {
roads := []syntheticWay{{1, 20, 35.149, -97.9, 35.151, -97.9, false, 2}, {2, 10, 35.151, -97.9, 35.153, -97.9, false, 2}}
data := packedRoadTileBytes(t, roads)
id := strings.Repeat("a", 64)
dir := filepath.Join(t.TempDir(), id)
area := maps.Area{Box: m.Box{MinPos: m.NewPosition(35, -98), MaxPos: m.NewPosition(35.25, -97.75)}}
path := maps.GenerateBoundsFileName(area, maps.OfflineSettings{OutputDirectory: dir})
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
rel, err := filepath.Rel(dir, path)
if err != nil {
t.Fatal(err)
}
load := maps.SnapshotLoader(dir, maps.SnapshotReceipt{Tiles: []maps.SnapshotTile{{Path: rel, Size: int64(len(data)), SHA256: fmt.Sprintf("%x", sha256.Sum256(data))}}})
s := State{ShadowOnly: true, SnapshotID: id}
s.Init()
boot := uint64(10_100_000_000)
s.bootNow = func() uint64 { return boot }
sample := syntheticSample(t, cereal.GpsSourceExternal, 10_000_000_000, 1, true, true)
s.ProcessGps(sample, true, load, time.Unix(0, 0))
sample.SourceChanged, sample.FixMonoTime = false, 10_050_000_000
s.ProcessGps(sample, true, load, time.Unix(0, 1))
event, output := decodedOutput(t, &s)
boundary, _ := output.PreviewBoundaryId()
if !event.Valid() || output.SpeedLimit() != 20 || output.PreviewSpeedLimit() != 10 || boundary == "" || output.PreviewDistance() <= 0 || output.NextSpeedLimit() != 0 || output.SuggestedSpeed() != 0 {
t.Fatalf("shadow preview: valid=%v status=%v way=%v limit=%v preview=%+v", event.Valid(), output.RoadStatus(), output.WaySelectionType(), output.SpeedLimit(), s.LimitPreview)
}
boot = 10_600_000_000
event, output = decodedOutput(t, &s)
boundary, _ = output.PreviewBoundaryId()
if event.Valid() || output.PreviewSpeedLimit() != 0 || boundary != "" {
t.Fatal("expired GPS retained an upcoming limit")
}
}
+12 -1
View File
@@ -11,6 +11,7 @@ import (
"os"
"reflect"
"runtime"
"slices"
"strconv"
"strings"
"syscall"
@@ -162,7 +163,6 @@ func GenerateOffline(s OfflineSettings) error {
MaxSpeedBackward: ParseMaxSpeed(tags["maxspeed:backward"]),
MaxSpeedAdvisory: ParseMaxSpeed(tags["maxspeed:advisory"]),
Lanes: uint8(lanes),
OneWay: tags["oneway"] == "yes",
Id: int64(way.ID),
HighwayClass: HighwayClassFromTag(tags["highway"]),
@@ -191,6 +191,17 @@ func GenerateOffline(s OfflineSettings) error {
tmpWay.Nodes[i].Latitude = n.Lat
tmpWay.Nodes[i].Longitude = n.Lon
}
switch tags["oneway"] {
case "yes", "1":
tmpWay.OneWay = true
case "-1":
tmpWay.OneWay = true
slices.Reverse(tmpWay.Nodes)
tmpWay.MaxSpeedForward, tmpWay.MaxSpeedBackward = tmpWay.MaxSpeedBackward, tmpWay.MaxSpeedForward
tmpWay.MaxSpeedForwardConditional, tmpWay.MaxSpeedBackwardConditional = tmpWay.MaxSpeedBackwardConditional, tmpWay.MaxSpeedForwardConditional
case "":
tmpWay.OneWay = tags["junction"] == "roundabout" || tags["highway"] == "motorway"
}
tmpWay.Box.MinPos = m.NewPosition(minLat, minLon)
tmpWay.Box.MaxPos = m.NewPosition(maxLat, maxLon)
scannedWays = append(scannedWays, tmpWay)
+78
View File
@@ -0,0 +1,78 @@
package maps
import (
"math"
"os"
"path/filepath"
"testing"
m "pfeifer.dev/mapd/math"
)
func TestGeneratorPreservesFixedOneWayDirectionAndSpeedTags(t *testing.T) {
for _, tc := range []struct {
name, oneWay, highway, junction string
wantOneWay, reverse bool
}{
{"ordinary", "", "residential", "", false, false},
{"yes", "yes", "residential", "", true, false},
{"numeric", "1", "residential", "", true, false},
{"reverse", "-1", "residential", "", true, true},
{"roundabout", "", "residential", "roundabout", true, false},
{"motorway", "", "motorway", "", true, false},
{"roundabout explicit no", "no", "residential", "roundabout", false, false},
{"roundabout explicit zero", "0", "residential", "roundabout", false, false},
{"motorway explicit no", "no", "motorway", "", false, false},
{"motorway explicit zero", "0", "motorway", "", false, false},
{"motorway link", "", "motorway_link", "", false, false},
{"reversible", "reversible", "residential", "roundabout", false, false},
{"alternating", "alternating", "motorway", "", false, false},
} {
t.Run(tc.name, func(t *testing.T) {
forwardRule, backwardRule := "15 @ (Mo-Fr 08:00-09:00)", "25 @ (Sa 10:00-11:00)"
tags := []string{"highway", tc.highway, "maxspeed", "30 mph",
"maxspeed:forward", "10", "maxspeed:backward", "20",
"maxspeed:forward:conditional", forwardRule, "maxspeed:backward:conditional", backwardRule}
if tc.oneWay != "" {
tags = append(tags, "oneway", tc.oneWay)
}
if tc.junction != "" {
tags = append(tags, "junction", tc.junction)
}
root := t.TempDir()
pbf := filepath.Join(root, "road.osm.pbf")
if err := os.WriteFile(pbf, syntheticPBF(tags...), 0600); err != nil {
t.Fatal(err)
}
output := filepath.Join(root, "tiles")
if err := GenerateOffline(OfflineSettings{InputFile: pbf, OutputDirectory: output, Overlap: .001,
Box: m.Box{MinPos: m.NewPosition(35, -98), MaxPos: m.NewPosition(35.25, -97.75)}}); err != nil {
t.Fatal(err)
}
tile, err := FindWaysAroundPositionIn(m.NewPosition(35.15, -97.9), output)
if err != nil || !tile.Loaded || tile.Ways.Len() != 1 {
t.Fatalf("generated road not loaded: %v", err)
}
way := tile.Ways.At(0)
if way.Nodes.Len() != 2 {
t.Fatalf("generated road has %d nodes", way.Nodes.Len())
}
first, last := 35.1, 35.2
forwardSpeed, backwardSpeed := 10./3.6, 20./3.6
if tc.reverse {
first, last = last, first
forwardSpeed, backwardSpeed = backwardSpeed, forwardSpeed
forwardRule, backwardRule = backwardRule, forwardRule
}
firstNode, lastNode := way.Nodes.At(0), way.Nodes.At(1)
if way.Id() != 42 || way.OneWay() != tc.wantOneWay ||
math.Abs(firstNode.Lat()-first) > 1e-9 || math.Abs(lastNode.Lat()-last) > 1e-9 ||
math.Abs(way.MaxSpeed()-13.4112) > 1e-6 ||
math.Abs(way.MaxSpeedForward()-forwardSpeed) > 1e-5 || math.Abs(way.MaxSpeedBackward()-backwardSpeed) > 1e-5 ||
way.ConditionalMaxSpeedRaw(true) != forwardRule || way.ConditionalMaxSpeedRaw(false) != backwardRule {
t.Fatalf("generated road lost direction or speed metadata: oneWay=%v forward=%v backward=%v",
way.OneWay(), way.MaxSpeedForward(), way.MaxSpeedBackward())
}
})
}
}
+12 -4
View File
@@ -309,17 +309,25 @@ func pbfBlock(kind string, raw []byte) []byte {
data = append(data, header...)
return append(data, blob...)
}
func syntheticPBF() []byte {
func syntheticPBF(tags ...string) []byte {
header := pbfBytesField(nil, 4, []byte("OsmSchema-V0.6"))
header = pbfBytesField(header, 5, []byte("LocationsOnWays"))
if len(tags) == 0 {
tags = []string{"highway", "residential", "maxspeed", "30 mph"}
}
var table []byte
for _, value := range []string{"", "highway", "residential", "maxspeed", "30 mph"} {
for _, value := range append([]string{""}, tags...) {
table = pbfBytesField(table, 1, []byte(value))
}
var keys, values []uint64
for i := 0; i < len(tags); i += 2 {
keys = append(keys, uint64(i+1))
values = append(values, uint64(i+2))
}
var way []byte
way = pbfVarintField(way, 1, 42)
way = pbfBytesField(way, 2, pbfPacked(1, 3))
way = pbfBytesField(way, 3, pbfPacked(2, 4))
way = pbfBytesField(way, 2, pbfPacked(keys...))
way = pbfBytesField(way, 3, pbfPacked(values...))
way = pbfBytesField(way, 8, pbfPacked(protowire.EncodeZigZag(1), protowire.EncodeZigZag(1)))
way = pbfBytesField(way, 9, pbfPacked(protowire.EncodeZigZag(351000000), protowire.EncodeZigZag(1000000)))
way = pbfBytesField(way, 10, pbfPacked(protowire.EncodeZigZag(-979000000), protowire.EncodeZigZag(0)))
+9 -6
View File
@@ -149,8 +149,12 @@ func (w *Way) IsForwardFrom(matchNode m.Position) bool {
return true
}
lastNode := w.Nodes.At(w.Nodes.Len()-1)
return !lastNode.Equals(matchNode)
lastNode := w.Nodes.At(w.Nodes.Len() - 1)
if lastNode.Equals(matchNode) {
// A closed one-way can be entered at its repeated endpoint in stored order.
return w.OneWay() && matchNode.Equals(w.Nodes.At(0))
}
return true
}
func IsForward(lineStart m.Position, lineEnd m.Position, bearing float64) bool {
@@ -431,7 +435,7 @@ func (w *Way) DistanceFrom(pos m.Position) (DistanceResult, error) {
minIdx := 0
for i := 0; i < w.Nodes.Len()-1; i++ {
nodeStart := w.Nodes.At(i)
nodeEnd := w.Nodes.At(i+1)
nodeEnd := w.Nodes.At(i + 1)
line := m.Line{Start: nodeStart, End: nodeEnd}
linePosition := line.NearestPosition(pos)
distance := pos.DistanceTo(linePosition.Pos)
@@ -447,7 +451,7 @@ func (w *Way) DistanceFrom(pos m.Position) (DistanceResult, error) {
onWayDistance := minNodeStart.DistanceTo(minLinePosition.Pos)
for i := range minIdx {
nodeStart := w.Nodes.At(i)
nodeEnd := w.Nodes.At(i+1)
nodeEnd := w.Nodes.At(i + 1)
onWayDistance += nodeStart.DistanceTo(nodeEnd)
}
@@ -491,8 +495,7 @@ func (w *Way) MatchingWays(offlineMaps *Offline, matchNode m.Position) ([]Way, e
continue
}
box := way.Box()
if box.Equals(w.Box()) {
if way.Id() == w.Id() {
continue
}
+45
View File
@@ -55,3 +55,48 @@ func TestBearingAlignmentIsAnUndirectedNonnegativePenalty(t *testing.T) {
}
}
}
func TestWayEntryDirectionForOpenAndClosedWays(t *testing.T) {
start, end := m.NewPosition(35.15, -97.9), m.NewPosition(35.151, -97.9)
for _, tc := range []struct {
name string
closed, oneWay, fromEnd, wantForward bool
}{
{"open start", false, true, false, true},
{"open end", false, true, true, false},
{"closed one-way", true, true, false, true},
{"closed two-way", true, false, false, false},
} {
t.Run(tc.name, func(t *testing.T) {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
if err != nil {
t.Fatal(err)
}
raw, err := offline.NewRootWay(seg)
if err != nil {
t.Fatal(err)
}
raw.SetOneWay(tc.oneWay)
positions := []m.Position{start, end}
if tc.closed {
positions = append(positions, start)
}
nodes, err := raw.NewNodes(int32(len(positions)))
if err != nil {
t.Fatal(err)
}
for i, p := range positions {
nodes.At(i).SetLatitude(p.Lat())
nodes.At(i).SetLongitude(p.Lon())
}
from := start
if tc.fromEnd {
from = end
}
way := NewWay(raw)
if got := way.IsForwardFrom(from); got != tc.wantForward {
t.Fatalf("entry direction: got %v want %v", got, tc.wantForward)
}
})
}
}
+7 -1
View File
@@ -24,7 +24,7 @@ type syntheticWay struct {
// The actual offline Capnp representation is packed, decoded, and passed to
// State; tests do not mock GetCurrentWay or BuildMessage.
func packedRoadTile(tb testing.TB, specs []syntheticWay) maps.Offline {
func packedRoadTileBytes(tb testing.TB, specs []syntheticWay) []byte {
tb.Helper()
msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
if err != nil {
@@ -65,6 +65,12 @@ func packedRoadTile(tb testing.TB, specs []syntheticWay) maps.Offline {
if err != nil {
tb.Fatal(err)
}
return data
}
func packedRoadTile(tb testing.TB, specs []syntheticWay) maps.Offline {
tb.Helper()
data := packedRoadTileBytes(tb, specs)
parsed := maps.ReadOffline(data)
if !parsed.Loaded {
tb.Fatal("packed tile rejected")
+9
View File
@@ -46,6 +46,7 @@ type State struct {
SourceGeneration uint64
ProducerSession uint64
MatchedLimitMps float32
LimitPreview LimitPreview
lastMapLoadAttempt time.Time
bootNow func() uint64
// The fix the current road result was computed from. The loop runs at
@@ -90,6 +91,7 @@ func (s *State) clearRoadDerived() {
s.TargetVelocities = nil
s.MapCurveSpeed = 0
s.MatchedLimitMps = 0
s.LimitPreview = LimitPreview{}
s.NextHazard.Reset()
s.NextAdvisorySpeed.Reset()
s.clearSpeedLimitEvidence()
@@ -255,6 +257,7 @@ func (s *State) ProcessGps(sample cereal.GpsSample, success bool, load MapLoader
s.MatchedLimitMps = float32(limit)
s.RoadStatus = custom.MapdOut_SampleStatus_matchedLimit
}
s.LimitPreview = s.previewLimit()
}
func (s *State) SuggestedSpeed() float32 {
@@ -361,6 +364,12 @@ func (s *State) buildMessageAt(now uint64) (*capnp.Message, error) {
output.SetRoadName(s.CurrentWay.Way.Name())
output.SetSpeedLimit(s.MatchedLimitMps)
if s.LimitPreview.BoundaryID != "" {
output.SetPreviewSpeedLimit(s.LimitPreview.Speed)
output.SetPreviewDistance(s.LimitPreview.Distance)
output.SetPreviewBoundaryId(s.LimitPreview.BoundaryID)
output.SetPreviewWayId(s.LimitPreview.WayID)
}
output.SetConditionalSpeedLimit(s.CurrentWay.ConditionalMaxSpeedRaw())
+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:
Binary file not shown.
-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);
}
-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.
@@ -0,0 +1 @@
.github export-ignore
@@ -0,0 +1 @@
__pycache__/
@@ -1,35 +1,53 @@
# BluePilot angle source
# Credits
Adapted from BluePilotDev/bluepilot bp-7.0 at
[e1d051d7ba270261b4455068bd68f1a58db15a4a](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a),
principally Alan Polk, with John Christman, Jacob Neulight, Nathan Ingraham,
Praeuner, tonesto7, Haibin Wen and other BluePilot, sunnypilot and comma contributors.
Source paths: opendbc/sunnypilot/car/ford/lateral_angle_ext.py,
fordcan_ext.py, human_turn.py and opendbc/safety/modes/ford.h.
The simultaneous upstream LICENSE and LICENSE.md notices are retained.
This software is licensed under a custom license requiring permission for use.
This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use.
The BluePilot Ford lateral library: Ford's four-signal lateral control in curvature and angle
modes, the CAN messages that carry it, and the panda safety that checks it. Everything here is
under the MIT terms in [LICENSE](LICENSE).
This is a StarPilot adapter to the September 2026 proposed library shape.
It is not a released BluePilot library, tag, or claim of BluePilot validation.
The existing curvature controller remains StarPilot-owned. Pinion curvature,
curvature-primary settings and the unsafe native reset bypass are not imported.
## Contributors
The exact upstream root notices are additionally retained as upstream-root-LICENSE
and upstream-root-LICENSE.md; LICENSE/ LICENSE.md retain the opendbc notices.
ORIGIN_HISTORY.tsv records the inspected path history with commit IDs/authors/dates.
The private compact upstream root clone preserves its reachable root history.
Pre-conversion opendbc submodule objects are not included in that root history.
- **Alan Polk (`alan-polk`)**: principal author of the curvature and angle controllers, the
human-turn behavior, the angle-mode side channel and its panda safety, and the lane centering
trim.
- **tonesto7**: the CAN message builders and BluePilot's early releases.
- **John Christman**: lateral integration, platform data, and the angle-mode anti-stall work.
- **Jacob Neulight**: the steering-pinion curvature measurement and its panda safety, and the
shadow-curvature measurement.
- **Blue Genie (`blue-genie`)**: the split of BluePilot's lateral control into opendbc that this
library grew from.
- **ghbarker**: the lane centering trim's confidence blend, fallback, filter and rate limit.
- **Nathan Ingraham**: the angle-mode high-speed damping adjustment.
- **Praeuner**: the angle-mode damping range.
- **OneFast440**: the early Ford safety declarations.
- **nitrobass24**: the library's structure, its inputs and settings plumbing, and the safety tests
and CI.
Angle lane-centering trim is separately adapted from BluePilot bp-dev
[e22afa6be9b881fa784c92ebb316db47728a3d81](https://github.com/BluePilotDev/bluepilot/commit/e22afa6be9b881fa784c92ebb316db47728a3d81),
`opendbc/sunnypilot/car/ford/lane_center_trim.py` (blob
`5eedebd79e99493624f9d6af0750d1dd5713306c`). Alan Polk authored the initial
trim, model-position fallback, correction slew and default-strength changes;
ghbarker contributed reference/NaN guards and tests. The source explicitly credits
StarPilot/u/jc01rho for curvature-domain placement/geometry. The core adaptation
adds immutable host projection and conservative stale/driver-pressure suppression.
The pinned bp-7.0 controller baseline and default-disabled policy are unchanged.
Both simultaneous root/opendbc MIT and Custom MIT notices also exist unchanged at
this trim revision; all retained notices and permission acknowledgments apply
without assigning an exclusive license or claiming a new permission grant.
## Third-party code
### comma.ai's opendbc
MIT, Copyright (c) 2020, Comma.ai, Inc.
- `can/fordcan_ext.py`: `create_lat_ctl_msg` and `create_lat_ctl2_msg`, comma's Ford lateral
message builders from `opendbc/car/ford/fordcan.py`, with ramp-type and precision parameters
added.
- `safety/lateral.h`: the measured-table rate check, curvature-error band and lateral-accel cap,
from comma's earlier checks in `opendbc/safety/lateral.h`, and `ford_bp_curvature_cmd_checks`,
adapted from comma's `steer_curvature_cmd_checks`.
- `safety/ford.h`: the rate-of-change shape of the path angle and path offset checks, the
curvature-error band in the shadow-curvature check, and the message-rate formula.
- `core/lateral_curv_ext.py`: `apply_ford_curvature_limits_ext`, adapted from comma's
`apply_ford_curvature_limits`, with its road-roll and lateral-accel constants.
- Small pieces: the message packing and the yaw-rate curvature measurement in
`core/carcontroller_ext.py`, and the model time-index formula in `core/values_ext.py`.
- The BluePilot safety tests start from comma's Ford safety test skeletons.
### StarPilot
MIT, Copyright (c) 2026, firestar5683 and StarPilot contributors
- `core/lane_center_trim.py`: the lane-centering core (the correction formula, lookahead clip,
lane-line probability, std and width gates, the 0.004 correction limit and the lane-line
handling), ported from StarPilot's `selfdrive/controls/lib/lane_centering.py`
(`LaneCenteringController`, originally by jc01rho). The confidence blend, fallback, filter and
rate limit are BluePilot's.
@@ -0,0 +1,83 @@
# Bringing BluePilot lateral to a fork
How an openpilot fork takes BluePilot's Ford lateral control. There are two ways to do it:
1. **Use a release on your own** (preferred). Pin a release of this library and write your own hooks and settings against [HOSTS.md](HOSTS.md). BluePilot isn't in the loop and nothing waits on BluePilot: you consume lateral code that has been driven on Fords.
2. **Work with BluePilot.** BluePilot helps develop your hooks, keeps them in this repo, and tests the library on your fork in its CI, every PR and every night.
Either way the library is the same, and the same rules apply to it (below).
**This is a modified Ford safety model.** The panda safety allows signals and ranges that upstream's Ford safety blocks. Your fork decides whether to ship it, and whether BluePilot lateral is on or off by default for your users.
## The rules for either way
- **Pin a release, unmodified.** The library sits at `<pkg>/bluepilot_lateral/` in your opendbc (`<pkg>` is its package name) as one of:
- a **git submodule** pointing at a release tag, if your fork uses submodules; updating is moving the pointer;
- a **stamped copy** made with `tools/vendor.sh`, if it doesn't (see "The stamp tools").
- **Don't edit inside the library folder**, carry it as a git subtree with its history, or copy parts of it elsewhere. A fix to the library is a PR to this repo; once it's released, every fork gets it.
- **Everything else is yours:** the hooks in your opendbc, the code in your openpilot that fills the library's inputs every frame, and your settings UI.
## Option 1: use a release on your own
1. Pick a release from this repo's releases. Each one says what changed in the lateral control, the CAN messages and the panda safety, and what was validated on a car.
2. Mount it (submodule or stamped copy, above).
3. Write the hooks in your opendbc. [HOSTS.md](HOSTS.md) has the contract: the hook sites, the per-frame inputs and settings, the startup safety bits, and the host bindings to define if your safety function names differ from upstream opendbc's. Each release attaches the hook patch for sunnypilot/opendbc as a worked example.
4. Write your adapter: the small module that hands the library your fork's types (`hosts/sunnypilot/` is the example to copy). It lives in your fork, outside the library folder.
5. Run the library's tests in your tree ([HOSTS.md](HOSTS.md), "Running the tests") and drive it before you ship it.
6. To update, move to a newer release and rerun your tests.
BluePilot doesn't review or approve any of it. Questions and bugs in the library are welcome as issues in this repo.
## Option 2: work with BluePilot
For a fork that wants BluePilot to help with the hooks and keep them working. Open an issue in this repo, "Host: <fork>": where the fork's source and integration branch are, how it carries its opendbc, and how it takes outside changes. From there, this repo keeps for that fork:
- **its adapter**, `hosts/<fork>/`, plus host bindings if its safety names differ from upstream's;
- **its hooks**, `hosts/<fork>/hooks.patch`, until the fork carries them;
- **`hosts/<fork>/host.sh`**: `tree <dir>` builds the fork's opendbc at a pinned commit with the hooks and the library, placed by `vendor.sh`; `test <dir>` runs the library tests, the fork's safety suite with the BluePilot safety tests, and coverage; `make <dir>` writes `hooks.patch` back after a hook is edited in `<dir>/host`;
- **a CI job** that runs `host.sh tree` and `test` on every PR to this repo, so a library change that would break the fork fails here first;
- **a nightly check** that tries the fork's latest integration branch: a pass moves the pinned commit, a failure opens one issue saying which step broke. Once the fork carries the hooks it tests the fork's tree as it is, and runs `check_vendored.sh` on its copy.
The steps:
1. Develop and test the hooks and the fork's side in the fork, together.
2. Add the adapter, `hooks.patch`, `host.sh`, the CI job and the nightly check here, in one PR.
3. Release the library with them.
4. BluePilot opens the PR to the fork: the hooks, its settings UI and the library at that release. Later releases arrive the same way, as PRs the fork takes when it chooses.
An adapter lives in this repo only if this repo's CI tests the library on that fork.
## The stamp tools
For a fork that takes no submodules. Both run from a clone of this repo that has the release tag (`git fetch --tags`).
**`tools/vendor.sh <tag> <folder>`** makes `<folder>` exactly the library at `<tag>`:
- it empties the folder first, so files a release removed don't linger;
- it copies the release's files, without the library's own CI (`.github/`);
- it writes `<folder>/VERSION`: the tag and the commit, e.g. `v1.2.3 0a1b2c3d...`.
```bash
git clone https://github.com/BluePilotDev/bluepilot-lateral.git
cd bluepilot-lateral
tools/vendor.sh <tag> <fork checkout>/<path to its opendbc package>/bluepilot_lateral
```
Commit the folder in the fork. To update, run it again with the new tag: the diff shows exactly what changed between the two releases.
**`tools/check_vendored.sh <folder>`** reads `<folder>/VERSION` and compares the folder with that release, file by file (Python caches ignored). It lists every added, removed or changed file, and exits non-zero if there is one:
```bash
tools/check_vendored.sh <fork checkout>/<path to its opendbc package>/bluepilot_lateral
```
Run it in your CI to catch an edit to the copy. For a fork working with BluePilot, this repo's nightly check runs it too.
## Installer note: nested submodule
A fork that embeds the library as a *nested* submodule (Option 2 — e.g. `fork → opendbc → opendbc/bluepilot_lateral`) can end up with the library missing after an end-user install. Some installers run a non-recursive `git submodule update --init`, which checks out the first-level submodule but not the library nested inside it, and the build then fails to import it. Installs that use `git submodule update --init --recursive` are unaffected.
If your fork is installed that way, self-heal on launch: before the build imports the library, when its directory is empty, run a recursive submodule update scoped to the embedding submodule — guard on the library's `__init__.py`, retry, and fail soft so an offline boot is no worse than before.
## How BluePilot releases
- Lateral changes and changes to the library's safety headers are driven on a car before they're released.
- Every release says what changed in the lateral control, the CAN messages and the panda safety, and what was validated.
@@ -0,0 +1,129 @@
# Hosting the BluePilot lateral
A host is an opendbc fork with this library at `<pkg>/bluepilot_lateral/` and the hook patch applied, plus the openpilot on top of it that fills the library's inputs each frame. `<pkg>` is the fork's opendbc package: `opendbc` on sunnypilot/opendbc (ford-op/opendbc, under bp-sunnypilot), `iqdbc` on IQ.Pilot, `opendbc` on StarPilot. This page is what a host provides and what it gets back.
## Mounting the library
A host mounts this repo at `<pkg>/bluepilot_lateral/`, directly inside its opendbc package, as a git submodule (ford-op/opendbc) or, in a host that takes no submodules (IQPilot, at `artifacts/package_sources/iqdbc/iqdbc/bluepilot_lateral/`), as a stamped copy of a release. Updating is a bump of the submodule's commit, or vendoring the new release (below). The hook patch below carries every file ford-op/opendbc adds or changes outside the library, so since it mounts the library it also carries the mount's `.gitmodules` entry.
The library never names its host's package. Python imports are relative: the library's own modules by `.`/`..`, the host's opendbc by one more level (`from ...car.ford.values import CarControllerParams` in `core/`), and `tests/test_core_imports.py` fails on any absolute `opendbc`/`iqdbc`/`openpilot` import. Safety headers include each other as `../safety/<file>.h` and the host's declarations as `../../safety/declarations.h`, relative to the including file. The host includes the library's safety as `"<pkg>/bluepilot_lateral/safety/ford.h"`.
### Without submodules: a stamped copy
From a clone of this repo, `tools/vendor.sh <release tag> <host>/<pkg>/bluepilot_lateral` makes the folder exactly the library at that tag (files it no longer has are removed), without `.github/` (`.gitattributes`), plus a `VERSION` file: the tag and the commit. Nothing inside the folder is ever edited: a change goes into this repo, gets released, and the host vendors the new tag. `tools/check_vendored.sh <folder>` lists anything in the folder that differs from the release in its `VERSION` and fails if there is something; a host can run it in its CI, and this repo's IQPilot checks run it on IQPilot's copy.
### Host bindings
The safety headers are written against upstream opendbc's names. A host whose names differ defines these before it includes `safety/ford.h` (`safety/lateral_declarations.h` has the defaults):
| macro | default (upstream, sunnypilot) | IQ.Pilot |
|---|---|---|
| `BP_HOST_CURVATURE_LIMITS` | `CurvatureSteeringLimits` | `NativeCurvatureSteeringLimits` |
| `BP_HOST_CURVATURE_STATE` | `curvature_state` | `native_curvature_state` |
| `BP_HOST_RT_CURVATURE_RATE_CHECK` | `rt_curvature_rate_limit_check` | `rt_native_curvature_rate_limit_check` |
| `BP_HOST_LAT_ALLOWED()` | `(controls_allowed \|\| controls_allowed_lateral)` | `is_lat_active()` |
| `BP_HOST_SPEED_MISMATCH_CHECK()` | `speed_mismatch_check(vehicle_speed_2 ...)` | `((void)0)`: IQ's `ford_rx_hook` runs it |
The limits struct needs only `max_curvature`, `curvature_to_can`, `frequency`, `max_curvature_error` and `curvature_error_min_speed`.
## The hook patch
`tools/make_hook_patch.sh` prints it: every file outside the library that ford-op/opendbc changes from `master`, minus its own `.github/` (its CI). It fails unless the edited files are exactly the non-`.github` entries of `.github/bluepilot-hooks.txt`.
| file | what the hook does |
|---|---|
| `opendbc/car/ford/carcontroller.py` | Builds one `BluePilotLateral(CP, CP_SP, packer, CAN)`, the sunnypilot adapter (`hosts/sunnypilot/lateral.py`, below). Each frame `bp.update(CC_SP, frame)` says whether BluePilot lateral is active; if it is, `bp.step_lateral` sends the steering message at `STEER_STEP` (and returns the curvature to pass back next time) and `bp.step_lka` sends Lane_Assist_Data1 at `LKA_STEP`. Otherwise the stock path runs. |
| `opendbc/car/structs.py` | Imports `LateralInputs` from `host_types.py` and adds `CarControlSP.lateralInputs` and `CarParamsSP.lateralInputsRequired`. |
| `opendbc/sunnypilot/car/interfaces.py` | Calls `init_ford_safety_param_sp(CP, CP_SP, params_dict)` (`hosts/sunnypilot/safety_param.py`) at car init: the startup safety bits below, and `lateralInputsRequired` for every Ford. |
| `opendbc/safety/modes/ford.h` | Includes `safety/ford.h` and makes one call per hook site: counter and quality checks for the pinion message, the yaw measurement skipped while the pinion source is on, `ford_bp_rx_hook` (pinion measurement), `ford_bp_lka_tx` (angle-mode state from Lane_Assist_Data1), `ford_bp_lmc_tx` in place of the stock steering check when `BP_LATERAL` is set (the stock check runs unchanged when it isn't), and the pinion RX check list from `ford_bp_init`. |
| `opendbc/safety/tests/test_bluepilot_lateral.py` | New: hands the library's `safety/tests/` to upstream's test discovery, so upstream's `test.sh` and `mutation.py` run the BluePilot safety tests. |
## Every frame: inputs
A host fills these and the settings below for a car whose `CarParamsSP.lateralInputsRequired` is set: every Ford, BluePilot lateral on or off (`init_ford_safety_param_sp`).
`CarControlSP.lateralInputs` (`host_types.LateralInputs`):
| fields | from |
|---|---|
| `pathYawRate`, `pathX`, `pathY` | `modelV2.orientationRate.z`, `modelV2.position.x`, `.y` (33 points) |
| `laneLineLeftX/Y`, `laneLineRightX/Y` | `modelV2.laneLines[1]`, `[2]` |
| `laneLineProbs`, `laneLineStds` | `modelV2.laneLineProbs`, `.laneLineStds` |
| `laneChangeState`, `laneChangeDirection` | `modelV2.meta` |
| `steerRatio`, `stiffnessFactor`, `roll`, `angleOffsetDeg` | the learned vehicle parameters (`vehicleParameters`, paramsd) |
| `lateralDelay` | `lateralDelay.lateralDelay` |
| `valid` | true only when both the model and the vehicle parameters have been seen and every field above is filled |
Each path array has `host_types.MODEL_PATH_POINTS` (33) entries at the times in `host_types.MODEL_T_IDXS` (openpilot's `ModelConstants.T_IDXS`): the core interpolates over that grid. On a frame that isn't valid, or whose arrays aren't the full shape or contain a non-finite value, the library keeps the previous inputs.
## Every frame: settings
Every key in `opendbc.bluepilot_lateral.core.values_ext.BP_LATERAL_PARAMS` (the per-frame settings in `params/manifest.py`; hosts import the list from there), every frame, as (key, value) with the value as bytes (sunnypilot: `CarControlSP.params` entries): bools `"1"`/`"0"`, numbers as text. A missing, empty or unparsable value gives the manifest default. A host publishes its own registry default for a key nobody has set, so a host can carry its own defaults (BP7 uses 3.0 for `LC_PID_gain_UI_curv`). `python -m <pkg>.bluepilot_lateral.params.manifest` prints the keys as `params_keys.h` lines (IQ.Pilot drops `BACKUP`, which its params don't have).
## At startup: the safety bits
`values_ext.ford_safety_param(CP, params_dict)` reads the startup settings (`disable_BP_lat_UI`, `FordPrefSteerAngleCurvature`) and returns the bits. The host sends them to the panda in its second safety param (sunnypilot: the adapter ORs them into `CP_SP.safetyParam`). StarPilot has no second param: its adapter shifts them into bits 9-14 of its Ford `safetyParam` (`hosts/starpilot/safety_param.py`, clear of the bits StarPilot's Ford safety reads), and its `ford_init` hands `(param >> 9) & 0x3F` to `ford_bp_init`:
| bit | |
|---|---|
| 5 `BP_LATERAL` | the panda runs the BluePilot steering checks; the car controller sends the 4-signal messages only when it's set |
| 0 `STEER_ANGLE_CURVATURE`, 1-4 geometry index | the pinion angle replaces the yaw rate as the curvature measurement, on platforms with a row in the pinion table |
The panda keeps what it was given at init: changing `disable_BP_lat_UI` while driving takes full effect at the next init.
## Reading status
`BluePilotLateral.status()` returns `host_types.LateralStatus`, this frame's values for a BluePilot UI, under the names BP7's `controllerStateBP` publisher reads off its car controller:
| field | |
|---|---|
| `lateralUncertainty` | from whichever strategy ran |
| `angleRateLimited` | angle mode: the path angle soft rate limit bit this frame |
| `curvatureRateLimited` | angle mode: the equivalent curvature would have been rate-limited |
| `curvatureDeviationLimited` | either mode: the current-curvature error clip constrained the command |
| `humanTurnLateralPaused` | angle mode: lateral sent inactive during a manual turn |
| `stallBlipActive` | angle mode: the brief mode-0 pulse after a post-override stall |
| `disable_BP_lat_UI` | BluePilot lateral off: the setting, or the panda bit not set at init |
| `primary_lateral_control` | 0 = curvature, 1 = angle |
The four angle-mode values read false outside angle mode. bp-sunnypilot doesn't read status; it has no BluePilot UI.
## What the library uses from the host's opendbc
Every host is an opendbc fork, so the library uses these directly:
- `<pkg>.car`: `structs`, `DT_CTRL`, `ACCELERATION_DUE_TO_GRAVITY`, `scale_rot_inertia_per_mass` and `scale_tire_stiffness_per_mass` (or, on an opendbc from before the per-mass vehicle model, `scale_rot_inertia` and `scale_tire_stiffness`, at unit mass), `common.pid.PIDController`, `vehicle_model.VehicleModel` (either signature), `lateral` (`AngleSteeringLimits`, `apply_std_steer_angle_limits`, `ISO_LATERAL_ACCEL`), `ford.fordcan.CanBus`, `ford.values` (`CarControllerParams`, `FordFlags`).
- `can/fordcan_ext.py` sets LateralMotionControl2's checksum itself: upstream's packer fills Ford checksums (and overwrites it with the same value), IQ.Pilot's doesn't.
- The safety headers use `modes/ford.h`'s Ford constants and upstream's safety helpers, through the bindings above. `safety/lateral.h` and `can/fordcan_ext.py` copy four upstream functions, pinned per host package by `tests/test_bluepilot_upstream_copies.py`, so a host where one changed fails that test (a host with no pins skips it).
- The safety tests use the host's test harness: `<pkg>.safety.tests.test_ford`, `common` and `libsafety.libsafety_py`. `tests/host.py` names the host's own structs, adapter and libsafety setters for the tests that touch them.
## The core's API, and the host adapters
The core takes only the library's types:
- `core.carcontroller_ext.BluePilotLateral(CP, safety_param, packer, CAN)`: `safety_param` is the startup bits (`ford_safety_param`).
- `update(inputs, params, frame)`: `inputs` is a `host_types.LateralInputs`, `params` a `params.param_store.ParamStore` built from the frame's (key, value) settings. Then `step_lateral`, `step_lka` and `status()` as above.
Nothing under `core/`, `can/`, `params/` or `safety/` imports a host (`tests/test_core_imports.py`). `hosts/sunnypilot/`:
- `lateral.py`: a `BluePilotLateral` subclass with sunnypilot's types, `(CP, CP_SP, packer, CAN)` and `update(CC_SP, frame)`. It passes `CP_SP.safetyParam`, `CC_SP.lateralInputs` and the `CC_SP.params` entries as (key, value) pairs.
- `safety_param.py`: `init_ford_safety_param_sp(CP, CP_SP, params_dict)` ORs `ford_safety_param` into `CP_SP.safetyParam`. Its own module, so car init imports only `values_ext`.
`hosts/iqpilot/`, the same for IQ.Pilot's types:
- `lateral.py`: `BluePilotLateral(CP, CP_IQ, packer, CAN)` and `update(CC_IQ, frame)`, passing `CP_IQ.iqSafetyFlags`, `CC_IQ.lateralInputs` and the `CC_IQ.params` entries.
- `safety_param.py`: `init_ford_safety_param_iq(CP, CP_IQ, params_dict)` ORs `ford_safety_param` into `CP_IQ.iqSafetyFlags` (the panda's `current_safety_param_iq`) and sets `CP_IQ.lateralInputsRequired`.
`hosts/starpilot/`, for StarPilot, which has neither a second safety param nor CarControl / CarParams extension structs:
- `lateral.py`: `BluePilotLateral(CP, packer, CAN)`, reading the startup bits back out of the Ford `safetyParam`, and `update(frame_inputs, frame)`. `FrameInputs` carries `lateralInputs` and the `params` entries; StarPilot's openpilot side fills one each frame and hands it to the Ford CarController, in place of a CarControl extension.
- `safety_param.py`: `init_ford_safety_param_starpilot(CP, params_dict)` ORs `ford_safety_param << 9` into `CP.safetyConfigs[-1].safetyParam`; `bp_safety_param(CP)` reads them back; `lateral_inputs_required(CP)` says whether to fill `FrameInputs`. StarPilot's Transit (`FordFlags.LKA_STEERING`, steering through Lane_Assist_Data1 alone) gets neither.
Another host writes its own adapter against the same API.
## Running the tests
- Python tests: `python -m unittest discover -s <pkg>/bluepilot_lateral/tests -t .`, from the directory that holds `<pkg>`.
- Safety tests: the host's `<pkg>/safety/tests/test.sh` runs them, through a loader (ford-op/opendbc's `test_bluepilot_lateral.py`) or by naming `../../bluepilot_lateral/safety/tests` (IQ.Pilot: its cross-mode TX sweep reads `TX_MSGS` off every `Test*` class in its directory, so a loader there breaks it). Coverage of the BluePilot headers and `modes/ford.h`: `safety/tests/bluepilot_coverage.sh`, which runs the library's tests itself where the host has no loader; mutation: `safety/tests/bluepilot_mutation.py` (needs the host's `mutation.py`, which IQ.Pilot doesn't have).
- On a clean sunnypilot/opendbc checkout: `tools/test_on_host.sh <checkout>`, from a ford-op/opendbc checkout. It copies the library as committed and applies the hook patch to a temporary clone, then runs both.
- On IQPilot: `hosts/iqpilot/host.sh tree <dir>` builds iqdbc from IQ.Lvbs's repo at the pinned commit with `hosts/iqpilot/hooks.patch` (IQPilot's hooks, kept here until IQPilot carries them) and the library, vendored; `host.sh test <dir>` runs the library tests, IQPilot's safety suite with the BluePilot ones (minus `known_failures.txt`, IQPilot's own) and coverage; `host.sh make <dir>` writes the hook patch back after editing the hooks in `<dir>/host`. CI's `iqpilot` job runs tree and test. Every night `iqpilot pin check` tries IQ.Lvbs's latest `beta`: green moves the pin in a PR it merges itself (dropping `hooks.patch` once IQPilot carries the hooks), red keeps one issue open saying which step failed.
@@ -1,4 +1,6 @@
Copyright (c) 2020, Comma.ai, Inc.
Copyright (c) 2026, firestar5683 and StarPilot contributors
Copyright (c) 2024-2026, BluePilot contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
@@ -1,21 +0,0 @@
# Custom MIT License
Copyright (c) 2024, Haibin Wen, SUNNYPILOT LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to view and modify the Software, subject to the following conditions:
1. **Permission Required**: Permission Required for Commercial, For-Profit, or Closed Source Use: Use of the Software, in whole or in part, for any commercial purposes, for-profit projects, or in closed source projects requires explicit written permission from the original author(s).
2. **Redistribution**: Any redistribution of the Software, modified or unmodified, must retain this license notice and the following acknowledgment:
"This software is licensed under a custom license requiring permission for use."
3. **Visibility**: Any project that uses the Software must visibly mention the following acknowledgment:
"This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use."
4. **No Warranty**: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Contact sunnypilot Support <support@sunnypilot.ai> for permission requests.
---
Haibin Wen, SUNNYPILOT LLC
@@ -1,38 +0,0 @@
9d190417bf74563abfa6f36996f1e673e9ee0a1b Praeuner 2026-08-13T22:26:15-05:00 Update dampening factor to .25
a881646ea5edc21c0db4c4e74076e0dc14671a4e alan-polk 2026-08-10T09:04:52-05:00 Merge pull request #176 from BluePilotDev/bp-no-stall
9012f76666a5c90764fcaec40832b9a607488c27 John Christman 2026-08-05T17:49:59-04:00 anti-stall
3610e3f18a0ad37d793d7ada61dbc739ab1077a3 Nathan Ingraham 2026-07-23T22:26:29-05:00 self.path_angle_gain_lowC_highV remains the platform-specific base gain. The toggle now controls separate self.user_dampening_factor. l ow_gain_calc uses: self.path_angle_gain_lowC_highV * self.user_dampening_factor UI placement, description, default, and 0.75–1.25 range remain unchanged.
1db52bc79e607ddb00214254dd4d732615e27fe4 Nathan Ingraham 2026-07-23T00:00:51-05:00 -Added “High Speed Dampening” directly below “High Speed Adjustment Factor”, range 0.75–1.25, step 0.01, and default 1.0.
5d994c24edf63f3b9004b2963f18ef4199e5d368 Jacob Neulight 2026-07-18T20:17:04-06:00 Ford safety: optional pinion-sourced angle_meas behind STEER_ANGLE_CURVATURE SP flag
699c17d9fde62c690eed9d68eed7d731744d5137 Jacob Neulight 2026-07-18T19:53:46-06:00 Ford: publish angle-mode shadow curvature from measured curvature during overrides
b7428fee4b116d4832f91c850d54cac2730de2ef alan-polk 2026-07-18T08:52:24-05:00 restore curvature safety checks to bp-6.0 logic.
97867c1eb57b7472f6fc3de62f0fef576e5a5497 alan-polk 2026-07-16T19:18:05-05:00 Human Turn Reset for Angle (#118)
7efee67e3e643d52b973895d6c6765c5cc12e314 alan-polk 2026-07-16T08:19:43-05:00 angle mode: rescale exit-detection threshold for 20Hz cadence
1958a3501ead10ae1f496f0d11e1e8cda6e2dbcc alan-polk 2026-07-14T15:22:38-05:00 Bp 7.0 variable cleanup (#111)
8f8d6d15f0a590f42b78de964ffb0d0af7f5d63d alan-polk 2026-07-14T08:53:28-05:00 Bp 7.0 panda2 (#109)
11ac3b760671b489ca449d522f1edf240a232884 alan-polk 2026-07-09T07:53:21-05:00 angle mode: fix ROC values tuned for 100Hz cadence running at actual 20Hz
dbdcea747e65f574d326a55c9bf1a9d6c9485e22 alan-polk 2026-07-08T09:21:32-05:00 Revert "ford: add current-curvature deviation check to angle mode (panda-side)"
69a9714563622729ba304ac75354ba3fd37b0b39 alan-polk 2026-07-08T09:17:09-05:00 ford: add current-curvature deviation check to angle mode (panda-side)
be7f86a5d0dc1ea3b3f0052d4b54ff0b8a70f843 John Christman 2026-07-06T21:53:55-04:00 cast on debug
5c1fbb07dd4d7d950a01cb2cd1faddd90d41e632 alan-polk 2026-07-06T14:20:49-05:00 controllerStateBP: add angleRateLimited + curvatureRateLimited diagnostics
06d654e5f48ea8fcffce6e98e48e3a30450b5097 alan-polk 2026-06-25T23:28:51-05:00 Revert "temp blend ratio change" — restore predicted-curvature blend to 0.50
540e69fcf4861a2ce5d7ca41467effc231638e27 alan-polk 2026-06-22T08:35:44-05:00 temp blend ratio change
e3819c7e630d85cfb44858840124fe7f72f1762d alan-polk 2026-06-13T20:47:09-05:00 ford.h: don't block angle mode on the inactive curvature signal
5d343738e30267688d0ffa6eebe2ec3ca57b10ee alan-polk 2026-06-13T18:28:30-05:00 ford.h: widen path_angle ROC to 2% looser than lateral_angle_ext _soft_roc
d0aac605f99d37e9da205e419f7989c1e9eaa386 alan-polk 2026-06-13T11:15:50-05:00 BluePilot: add angle-primary lateral control mode
51d5de8991508c862e7c51333e5055425b2b8d23 alan-polk 2026-04-11T12:00:10-05:00 fixing missing punctation in ford.h
8194ceb7e0bbc112c73d393a8f0add3261f6592d alan-polk 2026-04-11T11:51:49-05:00 make safety limits symetrical but control limits asymetrical. Add 1.5s duration to detect human turn
db2bdff05df103d71df62f45c2a3cb5211aba6e6 alan-polk 2026-04-09T23:47:30-05:00 move all external logic to lateral_curv_ext.py. Apply some order of operations fixes to try and eleminate panda blocking messages.
1a8ac23b5bd5d7952c15eb913c2b1e39f14ad92e Blue Genie 2026-04-08T15:10:24-05:00 Adjust panda limits
892be076daf898d806983f847c72357accff7312 Blue Genie 2026-04-08T11:45:28-05:00 Add logging functionality
ce7d27230657159332f64a253a0faea6cf8dcc95 alan-polk 2026-04-08T11:40:02-05:00 fix rate of change limits
021b5146e80ac58fe2e81dbd848bc7cd409ef08e alan-polk 2026-04-07T16:24:38-05:00 align ford.h limits with dbc and carcontroller
7f9212f7f8dc22c4a0a22443158554f4d3652a3c tonesto7 2026-04-05T21:28:37-04:00 Merge bp-6.1 upstream changes into bp-6.0-dev (#124)
dd1a9d3fd8f60462d3d8ac6de3e7860e70e1f667 alan-polk 2025-12-12T12:22:31-06:00 convert opendbc from a submodule to a subfolder
10101012d8a4cc37951a557023824cdd995443a6 alan-polk 2026-08-10T17:23:04Z BluePilot: angle-mode lane centering trim (advanced lane positioning)
560e80e9d593badca08aa60e8ddb692e58033425 alan-polk 2026-08-11T12:50:00Z BluePilot: angle-mode lane centering falls back to model position, not zero
94bf8144d4a683e9f3408f3babf892878f3b81f4 ghbarker 2026-08-12T01:31:33Z ford ALP: restore StarPilot guards (narrow lane, laneline std, NaN params) and cap applied correction at 0.0015
80825b81568ad684fe4ec37a797d3cbaa004fdcf ghbarker 2026-08-13T01:04:02Z split applied-correction ceiling into its own PR per review
e5b92cc48b34ea43ac756a96a96687c9851a188d ghbarker 2026-08-13T01:05:07Z add tests for the restored guards
af4bc410c9ef417ba723212ceaa2a77758dd8ecb alan-polk 2026-08-14T03:24:28Z BluePilot: rate-limit the lane centering correction, not just filter it
8eea9eb68e0c4126ee1c52f251e274d5865b6743 alan-polk 2026-08-19T13:33:00Z BluePilot: lower angle-mode lane centering default strength to 0.25
Can't render this file because it contains an unexpected character in line 14 and column 85.
@@ -1,81 +1,14 @@
# BluePilot Ford angle control
# BluePilot lateral
StarPilot adaptation of BluePilotDev/bluepilot `bp-7.0`, pinned to
[e1d051d7ba270261b4455068bd68f1a58db15a4a](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a).
The layout follows Alan Polk's September 2026 library proposal. This is an
adaptation, not an upstream library release.
BluePilot's Ford lateral control as a library an opendbc fork mounts at `opendbc/bluepilot_lateral/`: both modes (curvature and angle), the CAN messages that carry them, the panda safety that checks them, and a settings manifest.
Existing curvature control remains the default. Explicit `FordLateralMode=1`
selects angle control next drive; the saved choice persists when unavailable.
Only this selector is exposed. Fixed defaults and bounds live in
`params/manifest.json`; pinion-yaw correction and curvature-primary tuning are
outside this library. Angle lane-centering trim is available as a default-disabled
core capability, separately sourced from BluePilot bp-dev
`e22afa6be9b881fa784c92ebb316db47728a3d81`; it adds no public settings.
**This is a modified Ford safety model.** The panda safety allows signals and ranges that upstream's Ford safety blocks. A fork chooses whether to ship it.
`core/` accepts scalar/model inputs without openpilot, Params or messaging
imports. `can/` owns encoding, `safety/ford_bp.h` owns native enforcement, and
`hosts/starpilot.py` connects the host. `params/protocol.json` generates the wire
constants. The folder is vendored without submodules, LFS or an external runtime.
**Status:** `v0.2.0`. It runs on bp-sunnypilot, a sunnypilot fork, through ford-op/opendbc (sunnypilot's opendbc plus the hooks).
## Admission
- [FORKS.md](FORKS.md): how a fork takes BluePilot lateral and works with BluePilot: carrying the library (a submodule or a stamped copy), the stamp tools, and adding a fork.
- [HOSTS.md](HOSTS.md): what a host provides, the hook patch, and how to run the tests.
- [can/LANE_ASSIST_DATA1.md](can/LANE_ASSIST_DATA1.md): the Lane_Assist_Data1 bits angle mode uses.
- [CREDITS.md](CREDITS.md) and [LICENSE](LICENSE) (MIT).
Classic: Bronco Sport Mk1, Escape Mk4, Focus Mk4, Maverick Mk1, Explorer Mk6.
CAN FD: Escape Mk4.5, Expedition Mk4, F150 Mk14, F150 Lightning Mk1, Ranger Mk2,
Mustang Mach-E Mk1.
Exact classic safety parameters 32/33 map to angle 128/129; CAN FD 18/19 and
66/67 map to 130/131. Parameter 131 requires DEBUG; RELEASE retains stock 130.
Longitudinal, radar, fingerprint and parser policy remain host-owned. Admission
uses the final CarParams contract, not brand or steering-control enum alone.
AOL, alternative experiences, Transit, Edge, Mondeo, SecOC, passive, dashcam,
notCar and unsupported static flags are excluded. Auxiliary noOutput requires
parameter zero; production deployments with two Pandas remain unqualified until
actual startup, bus routing and public acknowledgments are verified.
## Safety design
The extension uses small Ford hooks without changing shared safety headers or
existing curvature namespaces. Active steering requires ordinary controls
permission, healthy physical RX and a fresh matching public Panda acknowledgment
of safety parameter, alternative experience, controls, RX validity, heartbeat and
faults. Cruise, gas/brake, checksum, counter, quality and freshness checks apply.
Neutral mode cannot grant permission.
The BluePilot reset-bypass latch is omitted. Rejected frames preserve accepted
actuator history and withdraw the shadow lease; accepted neutral commands or
permission withdrawal reset history. Lost counters recover through neutral
resynchronization. Version or checksum failure cannot fall through to curvature
control.
Active frames carry neutral curvature, offset and curvature rate, with checked
mode, ramp, precision, path angle, counter, checksum and cadence. Native enforcement
checks the shadow against the actual path across gains 0.95–1.3, bounds both by
`min(0.02, 2.4114/v²)`, and bounds the full actual-curvature interval against
measured curvature above 10 m/s. Actual path also obeys the 3.5886 m/s³ equivalent
jerk limit and rolling message-rate limit. Core quantization and timing margins
keep packed commands inside these bounds; an impossible intersection is neutral.
The versioned 0x3CA announcement preserves StarPilot's existing byte 4 bit 1
curvature flag and requires it clear in angle mode. It carries a counter, signed
shadow curvature, protocol version and checksum; its lease expires after 60 ms.
The shadow derives from the final path after rate limiting and saturation.
Control computes at 20 Hz, announces through 33 Hz LKA, then sends the latest
announced result at 20 Hz, adding at most one steering tick. Permission withdrawal
sends neutral immediately. Human-turn recovery uses bounded neutral blips without
a native bypass. The retained Dom factory steering delay is 0.22 s; pinned
BluePilot uses 0.20 s.
These stricter native limits and versioned metadata intentionally differ from
BluePilot 7.0. Identical bytes or safety decisions are not claimed for affected
commands. Runtime and physical ECU qualification remain pending.
## Attribution and notices
See `CREDITS.md`, `ORIGIN_HISTORY.tsv` and the project `THIRD_PARTY_NOTICES.md`.
Both upstream MIT and Custom MIT notices are retained verbatim in `LICENSE`,
`LICENSE.md`, `upstream-root-LICENSE` and `upstream-root-LICENSE.md`. The maintainer
reports author permission to use this adaptation under MIT. Both published notices
remain intact.
History before 2026-09-15 lives in [BluePilotDev/bluepilot](https://github.com/BluePilotDev/bluepilot).
@@ -0,0 +1 @@
v0.2.1 2b9dc6cc784faac47a1885d0bda0561f7b76f74d
@@ -1,34 +0,0 @@
"""Adapter to the proposed BluePilot library; angle only, exact source pinned."""
from opendbc.bluepilot_lateral.can.ford import lateral, lka
from opendbc.bluepilot_lateral.core.angle import Command
from opendbc.bluepilot_lateral.hosts.starpilot import Host, qualified
class BluePilotLateral:
def __init__(self, CP, *, host=None):
if not qualified(CP):
raise ValueError("Angle mode is not configured")
self.host = host or Host(CP)
self.canfd = bool(CP.flags & 1)
self.pending = Command()
self.announced = Command()
self.announced_ns = 0
self.lka_counter = 0
self.apply_curvature_last = 0.0
def step_lateral(self, CC, CS, packer, CAN, frame, now_ns):
self.pending = self.host.update(CC, CS)
command = self.announced
# A pending permission withdrawal is immediate; an increase waits until
# the 33Hz side channel has announced the matching final actuator.
if not self.pending.active or not 0 < self.announced_ns <= now_ns <= self.announced_ns + 60_000_000:
command = Command()
self.announced = command
return [lateral(packer, CAN.main, command, canfd=self.canfd, counter=frame // 5)]
def step_lka(self, packer, CAN, now_ns):
self.announced = self.pending
self.announced_ns = now_ns
message = lka(packer, CAN.main, self.announced.shadow_curvature, self.lka_counter)
self.lka_counter = (self.lka_counter + 1) % 8
return [message]
@@ -0,0 +1,60 @@
# Lane_Assist_Data1: BluePilot's angle-mode side channel
In angle mode the curvature signal in `LateralMotionControl2` is held at 0 and path angle does the
steering, so the panda has no commanded curvature to check against the car's measured curvature.
BluePilot sends that curvature, the *shadow curvature*, plus an "angle mode engaged" flag, in bits of
`Lane_Assist_Data1` (0x3CA) that carry no DBC signal. openpilot sends this message itself at 33 Hz,
and the panda reads it in the TX hook as it goes out. A separate message was tried first: the panda
never receives its own TX, and an RX check for it blocked controls car-wide (2026-07-09).
Packed by `create_lka_msg` in `fordcan_ext.py`; read by `ford_bp_lka_tx` in `../safety/ford.h`. The
byte positions, mask and scale are named once on each side (`BP_LKA_*` in `fordcan_ext.py`,
`FORD_BP_LKA_*` in `../safety/ford_declarations.h`), and `../tests/test_lane_assist_data1.py` checks
that they agree with each other and with the DBC.
## Layout
Bit numbers are within the byte, 7 = most significant. Stock signals are from `ford_lincoln_base_pt.dbc`.
| byte | bits | stock Ford signal | BluePilot |
|---|---|---|---|
| 0 | 7-5 | `LkaActvStats_D2_Req` | |
| 0 | 4-2 | `LdwActvStats_D_Req` | |
| 0 | 1-0 | `LdwActvIntns_D_Req` | |
| 1-2 | 12 bits | `LaCurvature_No_Calc` | |
| 2-3 | 12 bits | `LaRefAng_No_Req` | |
| 4 | 7 | `LaRampType_B_Req` | |
| 4 | 6-5 | `LkaDrvOvrrd_D_Rq` | |
| 4 | 4-1 | none | free |
| 4 | 0 | none | **angle mode engaged** |
| 5-6 | all | none | **shadow curvature** |
| 7 | all | none | free |
## Fields
- **Angle mode engaged** (byte 4 bit 0): 1 while angle mode is the selected lateral mode and
BluePilot lateral is on.
- **Shadow curvature** (bytes 5-6): int16, big-endian, 1e-6 1/m per unit, clamped to
-32768..32767 (about ±0.0328 1/m). It is the curvature angle mode's path angle was derived from,
after the same error clip curvature mode applies; while the driver is pressing, inactive, or
during a human-turn or stall-blip pulse, it is the car's measured curvature instead. It is sent
negated, in Ford's sign convention, like the curvature and path angle in `LateralMotionControl2`.
## What the panda does
- It latches both fields from every `Lane_Assist_Data1` frame that goes through its TX hook,
including one it blocks for another reason.
- **Angle mode engaged widens the path-angle range.** While the flag is set, the lateral command's
path angle may use the DBC's full range (-0.5 to 0.5235 rad); otherwise it is held to ±0.25 rad
(`ford_lmc_checks`). The flag alone does it: curvature 0 without the flag keeps the tight range.
- **The shadow curvature check.** For a lateral command (`LateralMotionControl` on CAN or
`LateralMotionControl2` on CAN FD) with curvature 0, steering enabled and the flag set, the
panda scales the shadow curvature by 0.05 to curvature CAN units (2e-5 1/m, truncated toward
zero) and, above 10 m/s, blocks the frame if it is outside the measured curvature ± the curvature
error band + 1 (`ford_shadow_curvature_error_check`). It has no rate limit: path angle has its own.
- `ford_bp_init` clears the flag.
## Not here yet
A protocol-version byte and a shared bit allocation for other forks (StarPilot) are Phase 3 (the StarPilot host issue, #16):
they change bytes on the wire.
@@ -1,37 +0,0 @@
"""Ford angle codecs, adapted from BluePilot fordcan_ext.py at e1d051d7.
Core commands use planner signs; path and shadow are negated on the wire.
The v1 side channel retains byte4 bit1 for StarPilot's curvature announcement.
"""
from opendbc.bluepilot_lateral.params import protocol as p
def checksum4(dat):
return (15 - (sum(dat[:7]) & 15)) & 15
def lka(packer, bus, shadow, counter):
addr, original, _ = packer.make_can_msg("Lane_Assist_Data1", bus, {"LaRefAng_No_Req": 0., "LaCurvature_No_Calc": 0.})
dat = bytearray(original)
raw = max(-20000, min(20000, round(-shadow / p.SHADOW_SCALE))) & 0xFFFF
dat[4] |= p.ANGLE_MASK | ((counter % p.COUNTER_MODULUS) << p.COUNTER_SHIFT)
dat[5], dat[6] = raw >> 8, raw & 255
dat[7] = p.VERSION | (checksum4(dat) << p.CHECKSUM_SHIFT)
return addr, bytes(dat), bus
def lateral(packer, bus, command, *, canfd, counter):
values = {"HandsOffCnfm_B_Rq": 0, "LatCtlRampType_D_Rq": 2 if command.active else 0,
"LatCtlPrecision_D_Rq": command.precision, "LatCtlPathOffst_L_Actl": 0.0,
"LatCtlPath_An_Actl": -command.path_angle, "LatCtlCurv_No_Actl": 0.0}
if canfd:
values.update({"LatCtl_D2_Rq": int(command.active), "LatCtlCrv_NoRate2_Actl": 0.0,
"LatCtlPath_No_Cnt": counter % 16, "LatCtlPath_No_Cs": 0})
dat = packer.make_can_msg("LateralMotionControl2", bus, values)[1]
fields = ((dat[2] << 3) | (dat[3] >> 5), (dat[6] << 3) | (dat[7] >> 5),
((dat[3] & 31) << 6) | (dat[4] >> 2), ((dat[4] & 3) << 8) | dat[5])
total = int(command.active) + counter % 16 + sum(v + (v >> 8) for v in fields)
values["LatCtlPath_No_Cs"] = 255 - (total & 255)
return packer.make_can_msg("LateralMotionControl2", bus, values)
values.update({"LatCtl_D_Rq": int(command.active), "LatCtlRng_L_Max": 0, "LatCtlCurv_NoRate_Actl": 0.0})
return packer.make_can_msg("LateralMotionControl", bus, values)
@@ -0,0 +1,128 @@
"""
BluePilot Ford CAN message builder extensions (lateral only).
Extended versions of the stock fordcan.py lateral builders:
- Dynamic ramp_type and precision_type for the 4-signal lateral commands
- Lane_Assist_Data1 carries the angle-mode side channel read by bluepilot_lateral/safety/ford.h
Trimmed for the lateral proof-of-concept: the longitudinal, HUD and ICBM builders from
BluePilot's fordcan_ext.py are out of scope here.
"""
from ...car.ford.fordcan import CanBus
# BluePilot: the Lane_Assist_Data1 side channel, see LANE_ASSIST_DATA1.md. ford_declarations.h has the
# same values (tests/test_lane_assist_data1.py).
BP_LKA_ANGLE_MODE_BYTE = 4
BP_LKA_ANGLE_MODE_MASK = 0x1
BP_LKA_SHADOW_MSB_BYTE = 5 # shadow curvature: int16, big-endian, bytes 5-6
BP_LKA_SHADOW_LSB_BYTE = 6
BP_LKA_SHADOW_CURVATURE_SCALE = 1e-6 # 1/meter per raw unit
def create_lka_msg(packer, CAN: CanBus, lat_active: bool, hud_control,
angle_mode_engaged: bool = False, shadow_curvature: float = 0.0):
"""
Creates a CAN message for the Ford LKA Command.
BluePilot extension: accepts lat_active and hud_control parameters for future
lane departure warning integration (unused so far).
Also carries angle_mode_engaged + shadow_curvature (Ford sign) in bits with no DBC signal: the
layout and what the panda does with them are in LANE_ASSIST_DATA1.md.
Frequency is 33Hz.
"""
addr, dat, bus = packer.make_can_msg("Lane_Assist_Data1", CAN.main, {})
dat = bytearray(dat)
shadow_curvature_raw = int(round(shadow_curvature / BP_LKA_SHADOW_CURVATURE_SCALE))
shadow_curvature_raw = max(-32768, min(32767, shadow_curvature_raw)) & 0xFFFF
dat[BP_LKA_ANGLE_MODE_BYTE] |= BP_LKA_ANGLE_MODE_MASK if angle_mode_engaged else 0
dat[BP_LKA_SHADOW_MSB_BYTE] = (shadow_curvature_raw >> 8) & 0xFF
dat[BP_LKA_SHADOW_LSB_BYTE] = shadow_curvature_raw & 0xFF
return addr, bytes(dat), bus
def create_lat_ctl_msg(packer, CAN: CanBus, lat_active: bool, ramp_type: int, precision_type: int,
path_offset: float, path_angle: float, curvature: float, curvature_rate: float):
"""
Creates a CAN message for the Ford TJA/LCA Command (non-CAN FD).
BluePilot extension: dynamic ramp_type and precision_type parameters.
Stock hardcodes ramp_type=0 (Slow) and precision_type=1 (Precise).
Ford lane centering uses a third-order polynomial to describe the road centerline:
c0 (path_offset): lateral offset between vehicle and centerline (positive is right)
c1 (path_angle): heading angle between vehicle and centerline (positive is right)
c2 (curvature): curvature of the centerline (positive is left)
c3 (curvature_rate): rate of change of curvature
Frequency is 20Hz.
"""
values = {
"LatCtlRng_L_Max": 0, # Unknown [0|126] meter
"HandsOffCnfm_B_Rq": 0, # Unknown: 0=Inactive, 1=Active [0|1]
"LatCtl_D_Rq": 1 if lat_active else 0, # Mode: 0=None, 1=ContinuousPathFollowing, 2=InterventionLeft,
# 3=InterventionRight, 4-7=NotUsed [0|7]
"LatCtlRampType_D_Rq": ramp_type, # Ramp speed: 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3]
"LatCtlPrecision_D_Rq": precision_type, # Precision: 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3]
"LatCtlPathOffst_L_Actl": path_offset, # Path offset [-5.12|5.11] meter
"LatCtlPath_An_Actl": path_angle, # Path angle [-0.5|0.5235] radians
"LatCtlCurv_NoRate_Actl": curvature_rate, # Curvature rate [-0.001024|0.00102375] 1/meter^2
"LatCtlCurv_No_Actl": curvature, # Curvature [-0.02|0.02094] 1/meter
}
return packer.make_can_msg("LateralMotionControl", CAN.main, values)
def create_lat_ctl2_msg(packer, CAN: CanBus, mode: int, ramp_type: int, precision_type: int,
path_offset: float, path_angle: float, curvature: float,
curvature_rate: float, counter: int):
"""
Creates a CAN message for the Ford Lane Centering command (CAN FD).
BluePilot extension: dynamic ramp_type and precision_type parameters.
Stock hardcodes ramp_type=0 (Slow) and precision_type=1 (Precise).
This message replaces LateralMotionControl on CAN FD platforms and includes
counter and checksum fields.
Frequency is 20Hz.
"""
values = {
"LatCtl_D2_Rq": mode, # Mode: 0=None, 1=PathFollowingLimitedMode, 2=PathFollowingExtendedMode,
# 3=SafeRampOut, 4-7=NotUsed [0|7]
"LatCtlRampType_D_Rq": ramp_type, # 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3]
"LatCtlPrecision_D_Rq": precision_type, # 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3]
"LatCtlPathOffst_L_Actl": path_offset, # [-5.12|5.11] meter
"LatCtlPath_An_Actl": path_angle, # [-0.5|0.5235] radians
"LatCtlCurv_No_Actl": curvature, # [-0.02|0.02094] 1/meter
"LatCtlCrv_NoRate2_Actl": curvature_rate, # [-0.001024|0.001023] 1/meter^2
"HandsOffCnfm_B_Rq": 0, # 0=Inactive, 1=Active [0|1]
"LatCtlPath_No_Cnt": counter, # [0|15]
"LatCtlPath_No_Cs": 0, # [0|255]
}
# Computed here, not left to the packer: upstream's packer fills Ford checksums itself (and overwrites
# this with the same value), but a host's packer may not (IQ.Pilot's doesn't), and the PSCM drops a
# frame whose checksum is wrong.
dat = packer.make_can_msg("LateralMotionControl2", CAN.main, values)[1]
values["LatCtlPath_No_Cs"] = lat_ctl2_checksum(mode, counter, dat)
return packer.make_can_msg("LateralMotionControl2", CAN.main, values)
def lat_ctl2_checksum(mode: int, counter: int, dat: bytes) -> int:
"""LatCtlPath_No_Cs of a packed LateralMotionControl2: upstream's ford_checksum over the message's
checksum fields (mode, counter, curvature, curvature rate, path angle, path offset)."""
curvature = (dat[2] << 3) | (dat[3] >> 5)
curvature_rate = (dat[6] << 3) | (dat[7] >> 5)
path_angle = ((dat[3] & 0x1F) << 6) | (dat[4] >> 2)
path_offset = ((dat[4] & 0x3) << 8) | dat[5]
checksum = mode + counter
for sig_val in (curvature, curvature_rate, path_angle, path_offset):
checksum += sig_val + (sig_val >> 8)
return 0xFF - (checksum & 0xFF)
@@ -1,208 +0,0 @@
"""Angle-primary Ford steering adapted from Alan Polk's BluePilot bp-7.0.
Source e1d051d7ba270261b4455068bd68f1a58db15a4a lateral_angle_ext.py.
See ../LICENSE, ../LICENSE.md and ../CREDITS.md. This core has no vehicle,
openpilot, Params, messaging or model-constant imports.
"""
from dataclasses import dataclass
import math
from ..params import limits as bounds
from .trim import LaneCenterTrim, LaneGeometry, TrimConfig
def clip(value, low, high):
return max(low, min(high, value))
def interp(value, xs, ys):
if value <= xs[0]:
return ys[0]
for i in range(1, len(xs)):
if value < xs[i]:
return ys[i - 1] + (ys[i] - ys[i - 1]) * (value - xs[i - 1]) / (xs[i] - xs[i - 1])
return ys[-1]
@dataclass(frozen=True)
class Inputs:
active: bool
speed: float
measured_curvature: float
desired_curvature: float
steering_pressed: bool = False
steering_angle: float = 0.0
yaw_predictions: tuple = ()
time_indices: tuple = ()
lateral_delay: float = 0.15
lane_change: bool = False
lane_direction: int = 0
limit_status: int = 0
geometry: LaneGeometry | None = None
speed_min: float | None = None
speed_max: float | None = None
@dataclass(frozen=True)
class Command:
active: bool = False
path_angle: float = 0.0
shadow_curvature: float = 0.0
precision: int = 1
class AngleController:
def __init__(self, *, low_gain=1.0, high_gain=1.15, trim_config=None):
self.trim_config = TrimConfig() if trim_config is None else trim_config
self.trim = LaneCenterTrim()
self.low_gain = low_gain
self.high_gain = high_gain
self.path_last = 0.0
self.speed_last = 0.1
self.desired_last = 0.0
self.human_timer = 0.0
self.press_last = False
self.preturned = False
self.press_timer = 0.0
self.blip_frames = 0
self.blip_cooldown = 0.0
self.blip_hold = 0.0
self.blip_count = 0
def reset(self):
self.trim.reset()
self.path_last = 0.0
self.speed_last = 0.1
self.desired_last = 0.0
self.human_timer = 0.0
self.press_last = False
self.preturned = False
self.press_timer = 0.0
self.blip_frames = 0
self.blip_cooldown = 0.0
self.blip_hold = 0.0
self.blip_count = 0
def step(self, i: Inputs) -> Command:
values = (i.speed, i.measured_curvature, i.desired_curvature, i.steering_angle, i.lateral_delay)
if not i.active or not all(math.isfinite(v) for v in values) or i.speed < 0.0:
self.reset()
return Command()
speed_min = max(i.speed, 0.1) if i.speed_min is None else i.speed_min
speed_max = max(i.speed, 0.1) if i.speed_max is None else i.speed_max
if (not all(math.isfinite(v) for v in (speed_min, speed_max)) or
not 0 < speed_min <= max(i.speed, 0.1) <= speed_max):
self.reset()
return Command()
if i.steering_pressed and not self.press_last:
self.preturned = abs(i.steering_angle) > 45.0
self.press_last = i.steering_pressed
self.human_timer = self.human_timer + 0.05 if i.steering_pressed and abs(i.steering_angle) > 45.0 else 0.0
if self.human_timer + 1e-9 >= (3.0 if self.preturned else 1.5):
self.trim.reset()
self.path_last = 0.0
self.desired_last = i.desired_curvature
self.blip_frames = self.blip_count = 0
self.blip_hold = self.blip_cooldown = self.press_timer = 0.0
return Command()
if i.steering_pressed:
self.press_timer += 0.05
else:
if self.press_timer >= 0.5 and self.blip_cooldown <= 0.0 and self.blip_frames <= 0 and abs(self.path_last) < 0.1:
self.blip_frames = 6
self.press_timer = 0.0
if self.blip_frames > 0:
self.blip_frames -= 1
self.trim.reset()
self.path_last = 0.0
self.desired_last = i.desired_curvature
if self.blip_frames == 0:
self.blip_cooldown = 2.0
return Command()
base = clip(i.lateral_delay, 0.1, 0.15) + 0.05
predictions_valid = (len(i.yaw_predictions) == len(i.time_indices) and len(i.time_indices) >= 17 and
all(math.isfinite(v) for v in (*i.yaw_predictions, *i.time_indices)) and
all(b > a for a, b in zip(i.time_indices, i.time_indices[1:], strict=False)))
curves = tuple(y / max(0.01, i.speed) for y in i.yaw_predictions) if predictions_valid else ()
entering = bool(curves) and abs(interp(base, i.time_indices, curves)) > abs(i.desired_curvature)
factor = 1.0 if entering else interp(abs(i.desired_curvature), (0.005, 0.020), (1.0, 0.0))
lookup = base + bounds.VLT_EXTRA * interp(i.speed, (11.176, 24.5872), (1.0, 0.0)) * factor
predicted = interp(lookup, i.time_indices, curves) if curves else i.desired_curvature
saturated = i.limit_status >= 2 or self.path_last >= 0.5235 * 0.9 or self.path_last <= -0.5 * 0.9
falling = abs(i.desired_curvature) < abs(self.desired_last) - 0.010
blend = bounds.BLEND * 0.25 if not entering and (i.limit_status >= 1 or saturated or falling) else bounds.BLEND
requested = predicted * blend + i.desired_curvature * (1.0 - blend)
self.desired_last = i.desired_curvature
precision = 0 if i.lane_change and ((i.lane_direction == 1 and requested < 0) or
(i.lane_direction == 2 and requested > 0)) else 1
# BluePilot's default lane-change factors are both 1.0.
# Trim requested curvature before every existing actuator/safety limiter.
requested = self.trim.update(requested, i.speed, i.geometry, self.trim_config,
active=i.active, lane_change=i.lane_change, steering_pressed=i.steering_pressed)
kappa = clip(requested, i.measured_curvature - 0.002, i.measured_curvature + 0.002) if i.speed > 9.0 else requested
low = interp(i.speed, (13.5, 26.82), (1.0, self.low_gain))
high = interp(i.speed, (13.5, 26.82), (1.3, self.high_gain))
gain = interp(abs(kappa), (0.0007, 0.001), (low, high))
path = kappa * i.speed * gain
if saturated:
last_mag, magnitude = abs(self.path_last), abs(path)
if magnitude > last_mag:
path = self.path_last
elif last_mag - magnitude > 0.02:
path = math.copysign(last_mag - 0.02, self.path_last)
elif i.limit_status >= 1:
path = clip(path, -abs(self.path_last), abs(self.path_last))
roc = interp(i.speed, (10.0, 15.0, 25.0), (0.055, 0.0425, 0.009))
path = clip(clip(path, -0.5235, 0.5), self.path_last - roc, self.path_last + roc)
# Tightening relative to bp-7.0: the shadow describes the final actuator,
# including ROC/saturation. A truthful derived curvature must fit both the
# measured-curvature interval and the modern ISO envelope. If those bands
# cannot meet the previous actuator ROC, send a neutral mode-0 frame.
effective_speed = max(i.speed, 0.1)
cap = min(bounds.CURVATURE_MAX, bounds.LATERAL_ACCEL / max(speed_max, 1.0) ** 2)
low_kappa, high_kappa = -cap, cap
if i.speed > 9.0:
low_kappa = max(low_kappa, i.measured_curvature - 0.0018)
high_kappa = min(high_kappa, i.measured_curvature + 0.0018)
# Native cannot trust a host-selected feel gain. Intersect every admitted
# gain (0.95..1.3), so the worst equivalent curvature remains bounded.
lower_gain = bounds.GAIN_MAX if low_kappa >= 0.0 else bounds.GAIN_MIN
upper_gain = bounds.GAIN_MIN if high_kappa >= 0.0 else bounds.GAIN_MAX
lower_speed = speed_max if low_kappa >= 0.0 else speed_min
upper_speed = speed_min if high_kappa >= 0.0 else speed_max
lower = max(-bounds.PATH_MAX, low_kappa * lower_speed * lower_gain, self.path_last - roc)
upper = min(-bounds.PATH_MIN, high_kappa * upper_speed * upper_gain, self.path_last + roc)
# Bound the actual-angle equivalent derivative at the worst admitted gain.
# 45ms core margin remains inside the native elapsed-time/50ms cap.
jerk_delta = bounds.LATERAL_JERK / max(i.speed - 1.0, 1.0) ** 2 * bounds.CORE_STEER_PERIOD_S * bounds.GAIN_MIN * effective_speed
jerk_center = self.path_last / self.speed_last * effective_speed
lower = max(lower, jerk_center - jerk_delta)
upper = min(upper, jerk_center + jerk_delta)
lower += bounds.PATH_ROUNDING_MARGIN
upper -= bounds.PATH_ROUNDING_MARGIN
if lower > upper:
self.trim.reset()
self.path_last = 0.0
return Command()
path = clip(path, lower, upper)
self.path_last = path
self.speed_last = effective_speed
shadow = path / (effective_speed * gain)
self.blip_cooldown = max(0.0, self.blip_cooldown - 0.05)
gap = i.desired_curvature - i.measured_curvature
stalled = (not i.steering_pressed and not i.lane_change and i.speed > 9.0 and abs(gap) > 0.004 and
abs(i.desired_curvature) > abs(i.measured_curvature))
if stalled:
if abs(kappa - requested) > 1e-9 and self.blip_cooldown <= 0.0:
self.blip_hold += 0.05
if self.blip_hold >= 0.5 and self.blip_count < 3 and abs(path) < 0.1:
self.blip_frames = 6
self.blip_hold = 0.0
self.blip_count += 1
else:
self.blip_hold = 0.0
if i.steering_pressed or abs(gap) < 0.002:
self.blip_count = 0
return Command(True, path, shadow, precision)
@@ -0,0 +1,234 @@
"""
BluePilot Ford lateral: the one object the Ford CarController calls.
Holds the two strategies, LateralCurvExt and LateralAngleExt, with the inputs and state they
share, and does the strategy dispatch and message assembly. Lives above both strategy modules
(rather than inside either) so it can call into both without the circular import that would
result from one depending on the other.
The CarController owns apply_curvature_last, which the stock and BluePilot paths share: it is
passed into step_lateral and the result returned, so switching BluePilot lateral off mid-drive
continues from the last command, as before.
"""
import math
from types import SimpleNamespace
try:
from ...car import scale_rot_inertia_per_mass, scale_tire_stiffness_per_mass
except ImportError:
# a host on opendbc from before the per-mass vehicle model (IQ.Pilot): its mass-scaled helpers at unit
# mass are the per-mass values
from ...car import scale_rot_inertia, scale_tire_stiffness
def scale_rot_inertia_per_mass(wheelbase):
return scale_rot_inertia(1.0, wheelbase)
def scale_tire_stiffness_per_mass(wheelbase, center_to_front, tire_stiffness_factor):
return scale_tire_stiffness(1.0, wheelbase, center_to_front, tire_stiffness_factor)
from ...car.ford.values import CarControllerParams, FordFlags
from ...car.vehicle_model import VehicleModel
from ..can import fordcan_ext
from .lateral_angle_ext import LateralAngleExt
from .lateral_curv_ext import LateralCurvExt, PrimaryLateralControl
from ..params.manifest import value
from ..params.param_store import ParamStore
from .values_ext import FORD_PINION_WHEELBASE, FordSafetyFlagsSP
from .human_turn import HumanTurnDetector
from ..host_types import LateralInputs, LateralStatus
from .lateral_inputs import ModelView, VehicleParamsView, lateral_inputs_complete
def pinion_vehicle_model(CP):
"""Vehicle model for the pinion-angle curvature measurement: CarSpecs, except where BluePilot's
pinion geometry uses another wheelbase (FORD_PINION_WHEELBASE, the same value as the C table),
keeping the platform's weight split and tire model."""
wheelbase = FORD_PINION_WHEELBASE.get(CP.carFingerprint)
if wheelbase is None:
return VehicleModel(CP)
center_to_front = wheelbase * CP.centerToFront / CP.wheelbase
front, rear = scale_tire_stiffness_per_mass(wheelbase, center_to_front, CP.tireStiffnessFactor)
inertia = scale_rot_inertia_per_mass(wheelbase)
# Both VehicleModel signatures: upstream's per-mass fields, and the mass-scaled ones an older host's
# model reads, at unit mass (mass cancels out of the model, so the two are the same model).
return VehicleModel(SimpleNamespace(
rotationalInertiaPerMass=inertia, wheelbase=wheelbase,
centerToFront=center_to_front, steerRatio=CP.steerRatio,
tireStiffnessFrontPerMass=front, tireStiffnessRearPerMass=rear,
mass=1.0, rotationalInertia=inertia, tireStiffnessFront=front, tireStiffnessRear=rear, steerRatioRear=0.))
class InputTracker:
"""The model / vehicle-parameter inputs both strategies read, and the car's measured curvature.
Owned by BluePilotLateral and updated once per frame from the host's LateralInputs, before either
strategy runs."""
def __init__(self, CP, safety_param: int):
# Model / vehicle-parameter inputs arrive per frame from the host (see update)
self.VM = pinion_vehicle_model(CP) # used only for the pinion-angle curvature measurement
self.model = None
self.lp = None
self.lateral_delay = 0.0
# BluePilot: steering-angle curvature measurement (bad-yaw-sensor workaround).
# Mirrors the STEER_ANGLE_CURVATURE flag the safety firmware reads from
# current_safety_param_sp -- both layers must always agree, so this is init-time
# state from the startup safety bits (values_ext.ford_safety_param), never a live settings read:
# a live flip against stale firmware would fight the panda.
self.bp_pinion_curvature_enabled = bool(safety_param & FordSafetyFlagsSP.STEER_ANGLE_CURVATURE)
def get_current_curvature(self, CS):
"""Measured curvature of the car right now (OP sign convention).
The single measurement source for every BluePilot lateral consumer: the deviation
clip, the stall detector, and the shadow curvature published to ford.h's angle-mode
deviation check. The default source is the RCM yaw rate -- the same family stock
ford.h derives its angle_meas from. The shadow value judged against that check must
always come from the same measurement as the check's own reference, so route all
reads through here.
With the steering-angle curvature measurement enabled (FordPrefSteerAngleCurvature
toggle -> FordSafetyFlagsSP.STEER_ANGLE_CURVATURE), the pinion angle via the vehicle
model is used instead: some vehicles (e.g. a 2021 Explorer with a faulty RCM)
broadcast implausible VehYaw_W_Actl (sign-inverted vs IMU and steering geometry)
while its CAN quality flag still reads OK. The pinion angle (SteeringPinion_Data,
PSCM) is an equivalent measurement, independently validated against the comma IMU
(corr +0.99), and the panda safety angle_meas switches to the same source (see
bluepilot_lateral/safety/ford.h) -- the layers always agree. angleOffsetDeg/roll come from
liveParameters (paramsd, IMU-derived, not the car yaw sensor).
"""
if self.bp_pinion_curvature_enabled:
angle_offset_deg = self.lp.angleOffsetDeg if self.lp is not None else 0.0
roll = self.lp.roll if self.lp is not None else 0.0
return -self.VM.calc_curvature(math.radians(CS.out.steeringAngleDeg - angle_offset_deg),
CS.out.vEgoRaw, roll)
return -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)
def update(self, li: LateralInputs):
"""Take this frame's model / vehicle-parameter inputs from the host and update the vehicle model.
A frame is used only if it has the full modelV2 shape this code indexes into (33 path points,
both inner lane lines, 3+ probs/stds); anything else is ignored and the previous inputs are
kept, exactly as a stale SubMaster message was. That also makes fuzzed or partial inputs
safe: the producer's valid flag is necessary but not trusted on its own."""
if li.valid and lateral_inputs_complete(li):
self.model = ModelView(li)
self.lp = VehicleParamsView(li)
self.lateral_delay = li.lateralDelay
if self.lp is not None:
x = max(self.lp.stiffnessFactor, 0.1)
sr = max(self.lp.steerRatio, 0.1)
self.VM.update_params(x, sr)
class SharedLateralState:
"""The values both strategies read and write, so either mode sees what the other left, as when
the strategies were mixins on one object. bp_curvature_deviation_limited is the BluePilot UI's
deviation flag, from whichever mode ran."""
def __init__(self):
self.path_angle_last = 0.0
self.precision_type = 1 # 1=Precise, 0=Comfortable
self.bp_curvature_deviation_limited = False
self.human_turn_detector = HumanTurnDetector()
class BluePilotLateral:
def __init__(self, CP, safety_param: int, packer, CAN):
"""safety_param: the startup safety bits the panda was given (values_ext.ford_safety_param)."""
self.CP = CP
self.frame = 0
self.inputs = InputTracker(CP, safety_param)
self.shared = SharedLateralState()
self.curv = LateralCurvExt(self.inputs, self.shared)
self.angle = LateralAngleExt(CP, self.inputs, self.shared)
self.packer = packer
self.CAN = CAN
self.disable_BP_lat_UI = False
self.lateral_uncertainty = 0.0 # from whichever strategy ran last, for status()
# the panda only accepts 4-signal messages when this bit was set at init
self.bp_lateral_allowed = bool(safety_param & FordSafetyFlagsSP.BP_LATERAL)
def update(self, inputs: LateralInputs, params: ParamStore, frame):
"""Every frame: take this frame's model / vehicle-parameter inputs and settings, both from the host
(HOSTS.md). Returns whether BluePilot lateral is active."""
self.frame = frame
self.inputs.update(inputs)
self.curv.update_lateral_params(params)
self.angle.update_angle_params(params)
# One flag for every BP consumer (LatCtl dispatch, LKA angle-mode bits, mode reporting): the UI
# toggle, and the panda bit that was set at init. No 4-signal traffic to a stock-configured panda.
self.disable_BP_lat_UI = value(params, "disable_BP_lat_UI") or not self.bp_lateral_allowed
return not self.disable_BP_lat_UI
def step_lateral(self, CC, CS, actuators, apply_curvature_last):
"""At STEER_STEP: the steer message(s). Returns (can_sends, apply_curvature): the CarController
keeps it and passes it back next time as apply_curvature_last."""
# BluePilot: select the BP lateral strategy by primary control variable.
# angle -> LateralAngleExt: kappa -> path_angle (c1), apply_curvature held at 0.
# curvature -> LateralCurvExt: full 4-signal curvature-primary (default).
# Both return a LateralResult packed identically below.
# Do not run the stock curvature limits here or overwrite apply_curvature_last before the
# strategy runs. Panda rate-checks desired_curvature vs the last TX on the bus; that must match
# the prior frame's lat.apply_curvature only (not an intermediate stock-limited value).
angle_mode = self.curv.primary_lateral_control == PrimaryLateralControl.angle
if angle_mode:
lat = self.angle.update_angle_strategy(CC, CS, actuators)
else:
lat = self.curv.update(CC, CS, actuators, apply_curvature_last, self.CP)
apply_curvature = float(lat.apply_curvature)
self.lateral_uncertainty = float(lat.lateralUncertainty)
# BluePilot: angle-mode human-turn override -- send lateral inactive (mode 0) while the
# driver manually turns, so the PSCM releases cleanly instead of stalling 2-3 s on
# re-engage (observed on Mach-E). Panda-clean: every ford.h check has a legitimate
# !steer_control_enabled branch for the zeroed frames; on release, path_angle ramps back
# from 0 through the soft ROC. Curvature mode keeps its own reset_steering path (zeroed
# signals with mode still active) in LateralCurvExt.
# The stall blip (lateral_angle_ext.py) rides the same mode-0 path: a short pulse that
# resets the PSCM's post-override attenuation when the deviation clip deadlocks hands-free.
lat_active = CC.latActive and not (angle_mode and (self.angle.angle_human_turn_active or self.angle.angle_stall_blip_active))
can_sends = []
if self.CP.flags & FordFlags.CANFD:
mode = 1 if lat_active else 0
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
can_sends.append(fordcan_ext.create_lat_ctl2_msg(
self.packer, self.CAN, mode, lat.ramp_type, lat.precision_type,
-lat.path_offset, -lat.path_angle, -lat.apply_curvature, -lat.curvature_rate, counter
))
else:
can_sends.append(fordcan_ext.create_lat_ctl_msg(
self.packer, self.CAN, lat_active, lat.ramp_type, lat.precision_type,
-lat.path_offset, -lat.path_angle, -lat.apply_curvature, -lat.curvature_rate
))
return can_sends, apply_curvature
def status(self) -> LateralStatus:
"""This frame's values for a BluePilot UI (HOSTS.md), as BP7 sets them on its car controller."""
angle_mode = self.curv.primary_lateral_control == PrimaryLateralControl.angle
return LateralStatus(
lateralUncertainty=self.lateral_uncertainty,
angleRateLimited=bool(angle_mode and self.angle.bp_angle_rate_limited),
curvatureRateLimited=bool(angle_mode and self.angle.bp_curvature_rate_limited),
curvatureDeviationLimited=bool(self.shared.bp_curvature_deviation_limited),
humanTurnLateralPaused=bool(angle_mode and self.angle.angle_human_turn_active),
stallBlipActive=bool(angle_mode and self.angle.angle_stall_blip_active),
disable_BP_lat_UI=bool(self.disable_BP_lat_UI),
primary_lateral_control=int(self.curv.primary_lateral_control),
)
def step_lka(self, CC, hud_control):
"""At LKA_STEP: the Lane_Assist_Data1 message."""
# BluePilot: tell bluepilot_lateral/safety/ford.h whether angle mode is engaged, packed into
# Lane_Assist_Data1's unused bits. shadow_curvature is negated to match the
# path_angle/apply_curvature wire convention that ford.h's angle_meas is calibrated against.
# Only called while BluePilot lateral is active (update returned True this frame), so the mode
# alone decides.
angle_mode_engaged = self.curv.primary_lateral_control == PrimaryLateralControl.angle
shadow_curvature = -self.angle.bp_kappa_cmd if angle_mode_engaged else 0.0
return fordcan_ext.create_lka_msg(
self.packer, self.CAN, CC.latActive, hud_control, angle_mode_engaged, shadow_curvature
)
@@ -7,8 +7,8 @@ each mode DOES with the signal differs: curvature-primary (``lateral_curv_ext``)
(reset_steering + post-reset ramp); angle-primary (``lateral_angle_ext``) forces lateral inactive
(mode 0 on the wire) and ramps path_angle back in from zero through its soft ROC on release.
"""
from opendbc.car import DT_CTRL
from opendbc.car.ford.values import CarControllerParams
from ...car import DT_CTRL
from ...car.ford.values import CarControllerParams
# Require sustained hands-on AND a large angle (avoids resetting on small wheel nudges in a curve).
HUMAN_TURN_ANGLE_DEG = 45.0
@@ -14,10 +14,10 @@ for a lane-centering nudge, and it bypasses every limiter angle mode already app
This class instead computes a small correction in the **curvature domain**, meant to be added to
``kappa_cmd`` before any of those limiters run -- so the correction automatically inherits all of
them, with no new safety-relevant code path. This mirrors where a GM-focused fork (StarPilot,
u/jc01rho -- ``selfdrive/controls/lib/drive_helpers.py::LaneCenteringController`` on
``feat/lane-centering-camera-offset``) places their own lane-centering trim: on the universal
``desired_curvature`` signal, upstream of any car-specific actuator conversion. The geometry here
them, with no new safety-relevant code path. The core is ported from StarPilot's
``selfdrive/controls/lib/lane_centering.py`` (``LaneCenteringController``, by jc01rho; see
CREDITS.md), which applies its trim the same way: on the universal ``desired_curvature`` signal,
upstream of any car-specific actuator conversion. The geometry here
(``raw_correction = 2*error / lookahead**2``) is the same ``y ~= 1/2 * kappa * x**2`` relation
BluePilot already uses for PSCM d_ref / path_angle elsewhere in this package, solved for the
curvature that would eliminate a lateral position error at the lookahead distance.
@@ -184,6 +184,12 @@ class LaneCenterTrim:
return 0.0, 0.0
if not (np.all(np.diff(left_x) > 0) and np.all(np.diff(right_x) > 0)):
return 0.0, 0.0
if not all(np.isfinite(v) for v in (probs[1], probs[2], stds[1], stds[2])):
# Unlike the arrays above, a NaN here doesn't raise -- it flows straight into min()/
# np.interp() and poisons confidence (and therefore the correction) with a NaN that
# survives every subsequent frame until a hard reset, instead of erroring or getting
# clipped like everything else in this function.
return 0.0, 0.0
left = float(np.interp(lookahead, left_x, left_y))
right = float(np.interp(lookahead, right_x, right_y))
@@ -194,7 +200,8 @@ class LaneCenterTrim:
# probability via min() -- a single missing/unreliable line (e.g. no line on the curb
# side, only a center stripe) drags confidence toward 0 on its own.
width_tolerance = float(np.interp(width, _WIDTH_TOLERANCE_BP, _WIDTH_TOLERANCE_V))
std_tolerance = float(np.interp(max(float(stds[1]), float(stds[2])), _STD_TOLERANCE_BP, _STD_TOLERANCE_V)) #StarPilot stopped at 0.3, this fades the std through the table
# StarPilot stopped at 0.3, this fades the std through the table
std_tolerance = float(np.interp(max(float(stds[1]), float(stds[2])), _STD_TOLERANCE_BP, _STD_TOLERANCE_V))
confidence = min(float(probs[1]), float(probs[2]), width_tolerance, std_tolerance) #confidence is the weakest signal
scale = float(np.clip(np.interp(confidence, _CONFIDENCE_BP, _CONFIDENCE_V), 0.0, 1.0))
center_y = 0.5 * (left + right)
@@ -2,55 +2,49 @@
BluePilot: Ford CAN-FD path-angle–primary lateral control (developer).
Steering intent is c1 from κ → θ. Converts planner/model curvature into ``path_angle`` using
PSCM short lookahead d_ref and y ≈ ½κ x² ⇒ path_angle = ½ κ d_ref (see
``bluepilot/agent_info/20_FORD_PSCM_KNOWLEDGE_PACK.md``). Predicted curvature (modelV2) is
blended with ``actuators.curvature`` per ``FordPathAngleBlendRatio`` (0 = planner only,
PSCM short lookahead d_ref and y ≈ ½κ x² ⇒ path_angle = ½ κ d_ref. Predicted curvature
(modelV2) is blended with ``actuators.curvature`` by ``path_angle_blend_ratio`` (0 = planner only,
1 = model only).
**c0 (path_offset) is always zero on the wire, unconditionally.** Angle mode has no centering
trim -- an earlier port attempt piped a small additive trim onto path_angle through the
curv-mode ``LC_PID_controller``, but it never actually tracked lane center correctly in this
mode and was removed; only the DBC-required zero c0 remains.
**c0 (path_offset) is always zero on the wire, unconditionally.** An earlier port attempt piped
a small additive trim onto path_angle through the curv-mode ``LC_PID_controller``, but it never
actually tracked lane center correctly in this mode: path_angle here is a derived quantity
(``kappa_cmd * v_ego * curvature_factor``), so an additive trim in that domain has the wrong
(inverted) speed-dependence for a lane-centering nudge, and it bypassed every limiter this file
applies to ``kappa_cmd``. That attempt was removed; only the DBC-required zero c0 remains.
**Lane centering trim (``lane_center_trim.py``)** replaces it: a small correction applied to
``kappa_cmd`` itself (see ``LaneCenterTrim``), before the deviation clip / gain table / PSCM
clamp / soft ROC below -- so it inherits every one of those limiters automatically instead of
bypassing them. Blends toward lane-line center by confidence (same formula as
``lateral_curv_ext``'s ``path_offset``) and falls back to the model's own predicted path -- not
to zero -- when lines are missing/unreliable, so the user's left/right offset still applies on
center-stripe-only roads. Disabled during lane changes, user-tunable (enable, offset, authority)
via ``enable_lane_positioning_ang`` / ``custom_path_offset_ang`` / ``lane_centering_strength_ang``.
**Human-turn override**: while the driver manually turns (same sustained-press + angle criteria
as ``lateral_curv_ext``, via the shared ``HumanTurnDetector``), lateral is forced inactive (mode
0, all-zero signals) instead of winding path_angle into a stale command the PSCM has to reconcile
on release -- on the Mach-E's PSCM that reconciliation cost 2-3 s of dead time before control
resumed. Mode 0 is panda-clean by construction: every ford.h check has a legitimate
!steer_control_enabled branch, so no reset-bypass latch involvement. On release, path_angle ramps
back in from zero through the soft ROC below (no jump seed) -- generous at human-turn speeds, and
admitted by ford.h's path_angle ROC check (2% looser) without any bypass.
!steer_control_enabled branch. On release, path_angle ramps back in from zero through the soft
ROC below (no jump seed) -- generous at human-turn speeds, and admitted by ford.h's path_angle
ROC check (2% looser).
"""
import numpy as np
from numpy import clip, interp
from opendbc.car import DT_CTRL
from opendbc.car.lateral import apply_std_steer_angle_limits
from opendbc.car.ford.values import CAR, CarControllerParams
from opendbc.sunnypilot.car.ford.lateral_curv_ext import LateralResult
from opendbc.sunnypilot.car.ford.human_turn import HumanTurnDetector
from opendbc.sunnypilot.car.ford.values_ext import BP_ANGLE_LIMITS
from selfdrive.modeld.constants import ModelConstants
# Hard-coded per-platform gain defaults.
# CAN vehicles (Escape MK4, Bronco Sport, Explorer, Maverick, Edge)
_GAIN_CAN = (1.00, 1.15)
# CAN-FD body-on-frame trucks (F-150, Lightning, Expedition, Ranger)
_GAIN_CANFD_BOF = (0.95, 0.95)
# CAN-FD unibody SUVs (Mustang Mach-E, Escape MK4.5)
_GAIN_CANFD_SUV = (1.00, 1.05)
_CANFD_BOF_CARS = frozenset({
CAR.FORD_F_150_MK14,
CAR.FORD_F_150_LIGHTNING_MK1,
CAR.FORD_EXPEDITION_MK4,
CAR.FORD_RANGER_MK2,
})
_CANFD_SUV_CARS = frozenset({
CAR.FORD_MUSTANG_MACH_E_MK1,
CAR.FORD_ESCAPE_MK4_5,
})
from ...car import DT_CTRL
from ...car.lateral import apply_std_steer_angle_limits
from ...car.ford.values import CarControllerParams
from .lateral_curv_ext import LateralResult
from .lane_center_trim import LaneCenterTrim
from .values_ext import (
ANGLE_GAIN_CAN, ANGLE_GAIN_CANFD_BOF, ANGLE_GAIN_CANFD_BOF_CARS, ANGLE_GAIN_CANFD_SUV, ANGLE_GAIN_CANFD_SUV_CARS,
BP_ANGLE_LIMITS, LANE_CHANGE_FACTOR_BP, LANE_CHANGE_FACTOR_LOW,
)
from .values_ext import MODEL_T_IDXS
from ..params.manifest import default, value
# DBC ``LatCtlPath_An_Actl`` (rad) — panda safety uses the same in ``ford.h``; PSCM enforces in firmware.
FORD_DBC_PATH_ANGLE_MIN = -0.5
@@ -125,49 +119,62 @@ def pscm_d_ref_m(v_ego_ms: float) -> float:
return d
# the settings angle mode reads (params/manifest.py): (attribute, key)
ANGLE_SETTINGS = (
("low_speed_curv_factor", "FordLowSpeedFactor_ang"),
("high_speed_curv_factor", "FordHighSpeedFactor_ang"),
("user_dampening_factor", "FordHighSpeedDampening_ang"),
("lane_change_factor_high_ang", "lane_change_factor_high_ang"),
("enable_lane_positioning_ang", "enable_lane_positioning_ang"), # the lane centering trim (lane_center_trim.py)
("custom_path_offset_ang", "custom_path_offset_ang"),
("lane_centering_strength_ang", "lane_centering_strength_ang"),
)
class LateralAngleExt:
def __init__(self, CP=None, CP_SP=None):
# Predicted-curvature blend for path_angle: pred * b + desired * (1-b); b from ``FordPathAngleBlendRatio``
def __init__(self, CP, inputs, shared):
self.CP = CP
# the user settings (updated from the host's params each frame), at their defaults until then
for attr, key in ANGLE_SETTINGS:
setattr(self, attr, default(key))
# BluePilotLateral's LateralInputs (read) and SharedLateralState (read and written), common to both strategies
self.inputs = inputs
self.shared = shared
# Predicted-curvature blend for path_angle: pred * b + desired * (1-b); b is this fixed default
self.path_angle_blend_ratio = _FORD_PATH_ANGLE_BLEND_RATIO_DEFAULT
# Max extra VLT above t_base; from ``FordVLTExtraMax`` param
# Max extra VLT above t_base (fixed)
self.vlt_extra_max = _VLT_T_EXTRA_MAX
# Telemetry: final path_angle (rad) after limits (see bp_card_publisher)
# Telemetry: final path_angle (rad) after limits (for the BluePilot UI's bp_card_publisher)
self.bp_path_angle_final = 0.0
# High-speed gain factors: set per-platform via carFingerprint in update_angle_params.
self.path_angle_gain_lowC_highV = 1.0 # dampening at high speed, low curvature
self.path_angle_gain_highC_highV = 1.0 # gain at high speed, high curvature
self.bp_path_angle_gain_lowC_highV = 1.0
self.bp_path_angle_gain_highC_highV = 1.0
# User-tunable "feel" multipliers: read from the angle-tuning Params below.
self.low_speed_curv_factor = 1.0
self.high_speed_curv_factor = 1.0
self.user_dampening_factor = 1.0
# Telemetry copies of the user "feel" multipliers (ANGLE_SETTINGS)
self.bp_low_speed_curv_factor = 1.0
self.bp_high_speed_curv_factor = 1.0
# BluePilot: angle mode's own lane-change scaling factor, independent of curvature mode's
# lane_change_factor_high_curv -- angle needs a boost (>1) where curvature needs a cut (<1).
self.lane_change_factor_high_ang = 1.0
# BluePilot: angle-mode lane centering trim (advanced lane positioning) -- see
# lane_center_trim.py and the module docstring above.
self.lane_center_trim = LaneCenterTrim()
# Telemetry: variable curvature lookup time used this frame (s)
self.bp_curvature_lookup_time = _VLT_T_EXTRA_MAX + 0.3725 # warm start at ~0.5s
# BluePilot: error-clipped kappa path_angle was derived from -- carcontroller.py reads this as
# BluePilot: error-clipped kappa path_angle was derived from -- carcontroller_ext.py reads this as
# shadow_curvature for ford.h's angle-mode deviation check. Actively consumed, not telemetry.
self.bp_kappa_cmd = 0.0
# BluePilot: rate-limit diagnostics (controllerStateBP)
# BluePilot: rate-limit diagnostics (the BluePilot UI's controllerStateBP)
self.bp_angle_rate_limited = False # path_angle soft-ROC clip actually bit this frame
self.bp_curvature_rate_limited = False # equivalent curvature would be rate-limited by curv-mode logic (sim)
self.bp_curvature_deviation_limited = False # current_curvature error-clip constrained kappa_cmd this frame
self.sim_curvature_last = 0.0 # shadow curvature-mode last for the curvatureRateLimited sim
# Exit detection: track previous desired curvature to sense when planner is actively reducing
self._desired_curvature_last = 0.0
# Human-turn override: while the driver manually turns, lateral is forced inactive (mode 0,
# all-zero signals) instead of winding path_angle into a stale command the PSCM can't cleanly
# reconcile on release (2-3 s re-engage dead time observed on Mach-E). See module docstring.
# Note: in CarController this attribute is shared with LateralCurvExt (same mixin instance) --
# only one lateral strategy runs per frame, so a single detector serves both.
self.human_turn_detector = HumanTurnDetector()
self.angle_human_turn_active = False # read by carcontroller to force mode 0
# The detector itself is in BluePilotLateral's shared state: one detector serves both modes.
self.angle_human_turn_active = False # read by BluePilotLateral.step_lateral to force mode 0
# Post-override stall blip state (see module constants). angle_stall_blip_active is read by
# carcontroller to force mode 0, exactly like angle_human_turn_active.
# BluePilotLateral.step_lateral to force mode 0, exactly like angle_human_turn_active.
self.stall_blip_hold_s = 0.0 # accumulated deviation-clip-binding time toward a pulse
self.stall_blip_frames_left = 0 # remaining pulse frames; > 0 -> mode 0 on the wire
self.stall_blip_cooldown_s = 0.0 # re-arm delay after a pulse
@@ -177,47 +184,28 @@ class LateralAngleExt:
def update_angle_params(self, params):
"""Sets per-platform gain defaults and reads user angle-tuning params."""
self._ensure_lateral_curv_initialized(self.CP)
fp = getattr(self.CP, 'carFingerprint', '')
if fp in _CANFD_BOF_CARS:
low, high = _GAIN_CANFD_BOF
elif fp in _CANFD_SUV_CARS:
low, high = _GAIN_CANFD_SUV
if fp in ANGLE_GAIN_CANFD_BOF_CARS:
low, high = ANGLE_GAIN_CANFD_BOF
elif fp in ANGLE_GAIN_CANFD_SUV_CARS:
low, high = ANGLE_GAIN_CANFD_SUV
else:
low, high = _GAIN_CAN
low, high = ANGLE_GAIN_CAN
self.path_angle_gain_lowC_highV = low
self.path_angle_gain_highC_highV = high
if params is not None and hasattr(params, "get"):
for attr, key, min_value, max_value in (
("low_speed_curv_factor", "FordLowSpeedFactor_ang", 0.5, 1.5),
("high_speed_curv_factor", "FordHighSpeedFactor_ang", 0.5, 1.5),
("user_dampening_factor", "FordHighSpeedDampening_ang", 0.25, 1.25),
):
try:
raw = params.get(key, return_default=True)
if raw is not None and raw != b"":
setattr(self, attr, float(clip(
float(raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else raw), min_value, max_value)))
except Exception:
pass
try:
raw = params.get("lane_change_factor_high_ang", return_default=True)
if raw is not None and raw != b"":
self.lane_change_factor_high_ang = float(clip(
float(raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else raw), 0.85, 1.50))
except Exception:
pass
if params is not None:
for attr, key in ANGLE_SETTINGS:
setattr(self, attr, value(params, key))
def update_angle_strategy(self, CC, CS, actuators, CP):
def update_angle_strategy(self, CC, CS, actuators):
"""
Curvature from planner (+ optional predicted blend) → path_angle via ½·κ·d_ref.
c0 (path_offset) is always zero on the wire -- no centering trim in angle mode. c2 and c3 are zero.
Curvature from planner (+ optional predicted blend, + lane centering trim) → path_angle via
½·κ·d_ref. c0 (path_offset) is always zero on the wire; the lane centering trim lives entirely
in the curvature domain (kappa_cmd), not on c0. c2 and c3 are zero.
Blended κ is not passed through Ford c2 rate / DBC limits (those target the curvature actuator).
"""
self._ensure_lateral_curv_initialized(CP)
v_ego = float(CS.out.vEgoRaw)
d_ref = pscm_d_ref_m(v_ego)
d_ref = pscm_d_ref_m(v_ego) # noqa: F841 # BluePilot: unused today; kept verbatim from bp-dev
curvature_rate = 0.0
path_offset = 0.0
@@ -227,12 +215,11 @@ class LateralAngleExt:
precision = 1
if not CC.latActive:
self.path_angle_last = 0.0
self.shared.path_angle_last = 0.0
self.bp_path_angle_final = 0.0
self.apply_curvature_last = 0.0
self.bp_angle_rate_limited = False
self.bp_curvature_rate_limited = False
self.bp_curvature_deviation_limited = False
self.shared.bp_curvature_deviation_limited = False
self.sim_curvature_last = 0.0
# Publish the shadow curvature from the measured curvature while inactive. LKA keeps
# carrying angle_mode_engaged whenever angle mode is configured (independent of
@@ -242,16 +229,17 @@ class LateralAngleExt:
# ford.h's deviation check compares a zero shadow against real measured curvature.
# (ford.h skips the check while steer_control_enabled is 0, so the value is free to
# follow the measurement during the inactive period itself.)
self.bp_kappa_cmd = self.get_current_curvature(CS)
self.human_turn_detector.reset()
self.bp_kappa_cmd = self.inputs.get_current_curvature(CS)
self.shared.human_turn_detector.reset()
self.angle_human_turn_active = False
self.lane_center_trim.reset()
self.stall_blip_hold_s = 0.0
self.stall_blip_frames_left = 0
self.stall_blip_cooldown_s = 0.0
self.stall_blip_count = 0
self.angle_stall_blip_active = False
self.press_timer_s = 0.0
self.precision_type = 1
self.shared.precision_type = 1
return LateralResult(
apply_curvature=0.0,
curvature_rate=0.0,
@@ -263,28 +251,28 @@ class LateralAngleExt:
)
# Human-turn override: sustained driver press + large wheel angle → force lateral inactive
# (carcontroller drops mode to 0; all signals are zero on the wire) so path_angle can't wind
# (BluePilotLateral.step_lateral drops mode to 0; all signals are zero on the wire) so path_angle can't wind
# into a stale command while the driver turns. Always on in angle mode (no param gate) -- the
# curv-suffixed human-turn toggle belongs to curvature mode's reset strategy, and the Mach-E
# PSCM re-engage stall this prevents is not something a user should be able to opt out of.
# On release, no jump seed: path_angle_last is 0, so the normal flow below ramps the command
# back in through the soft ROC -- generous at human-turn speeds, no panda bypass involved.
self.angle_human_turn_active = self.human_turn_detector.update(
# back in through the soft ROC -- generous at human-turn speeds.
self.angle_human_turn_active = self.shared.human_turn_detector.update(
True, CS.out.steeringPressed, CS.out.steeringAngleDeg)
if self.angle_human_turn_active:
self.path_angle_last = 0.0
self.shared.path_angle_last = 0.0
self.bp_path_angle_final = 0.0
self.apply_curvature_last = 0.0
self.bp_angle_rate_limited = False
self.bp_curvature_rate_limited = False
self.bp_curvature_deviation_limited = False
self.shared.bp_curvature_deviation_limited = False
self.sim_curvature_last = 0.0
# Truthful shadow during the override (mirrors the inactive path -- see the comment
# there): the driver is steering, so the honest command is the car's actual curvature,
# and the panda-latched shadow stays current for the re-engage frame.
self.bp_kappa_cmd = self.get_current_curvature(CS)
self.bp_kappa_cmd = self.inputs.get_current_curvature(CS)
# Keep exit detection current so resume doesn't compare against a stale pre-turn value.
self._desired_curvature_last = float(actuators.curvature)
self.lane_center_trim.reset()
# A human turn ends any stall episode -- its own mode 0 does the PSCM reset job. That also
# covers the press so far: only press time accumulated AFTER the latch releases should earn
# a hand-off pulse.
@@ -294,7 +282,7 @@ class LateralAngleExt:
self.stall_blip_count = 0
self.angle_stall_blip_active = False
self.press_timer_s = 0.0
self.precision_type = 1
self.shared.precision_type = 1
return LateralResult(
apply_curvature=0.0,
curvature_rate=0.0,
@@ -314,7 +302,7 @@ class LateralAngleExt:
else:
if (self.press_timer_s >= _PRESS_BLIP_MIN_S and self.stall_blip_cooldown_s <= 0.0
and self.stall_blip_frames_left <= 0
and abs(self.path_angle_last) < _BLIP_MAX_PATH_ANGLE):
and abs(self.shared.path_angle_last) < _BLIP_MAX_PATH_ANGLE):
self.stall_blip_frames_left = _STALL_BLIP_FRAMES
self.press_timer_s = 0.0
@@ -326,17 +314,17 @@ class LateralAngleExt:
if self.stall_blip_frames_left > 0:
self.stall_blip_frames_left -= 1
self.angle_stall_blip_active = True
self.path_angle_last = 0.0
self.shared.path_angle_last = 0.0
self.bp_path_angle_final = 0.0
self.apply_curvature_last = 0.0
self.bp_angle_rate_limited = False
self.bp_curvature_rate_limited = False
self.bp_curvature_deviation_limited = False
self.shared.bp_curvature_deviation_limited = False
self.sim_curvature_last = 0.0
# Truthful shadow during the blip (see the inactive-path comment).
self.bp_kappa_cmd = self.get_current_curvature(CS)
self.bp_kappa_cmd = self.inputs.get_current_curvature(CS)
self._desired_curvature_last = float(actuators.curvature)
self.precision_type = 1
self.lane_center_trim.reset()
self.shared.precision_type = 1
if self.stall_blip_frames_left <= 0:
self.stall_blip_cooldown_s = _STALL_COOLDOWN_S
return LateralResult(
@@ -350,9 +338,9 @@ class LateralAngleExt:
)
self.angle_stall_blip_active = False
self.precision_type = 1
self.shared.precision_type = 1
precision = 1
LP = self.lp
LP = self.inputs.lp # noqa: F841 # BluePilot: unused today; kept verbatim from bp-dev
desired_curvature = float(actuators.curvature)
# Variable lookup time: t_base tracks planner pre-compensation; extra tapers on high speed and large curves.
@@ -361,14 +349,14 @@ class LateralAngleExt:
# curvature, kappa_entering stays True, and the exit-biased blend is permanently disabled — causing the car
# to command max path_angle through the entire apex. 0.15s gives t_base ≤ 0.20s and VLT ≤ 0.33s, restoring
# the 2.8m lookahead that kept kappa_entering False at the apex in successful earlier runs.
_t_base = float(clip(self.sm['liveDelay'].lateralDelay, 0.1, 0.15)) + _DT_MDL
_t_base = float(clip(self.inputs.lateral_delay, 0.1, 0.15)) + _DT_MDL
_speed_factor = float(interp(v_ego, [_VLT_V_LOW_MS, _VLT_V_HIGH_MS], [1.0, 0.0]))
# Direction-aware kappa factor: on curve ENTRY (model shows more curvature at t_base than planner now),
# keep full lookahead so pre-steering begins early. On exit/apex, taper by magnitude to prevent unwind.
_kappa_at_t_base = 0.0
if self.model is not None and len(self.model.orientationRate.z) >= 17:
_curvatures_ref = np.array(self.model.orientationRate.z) / max(0.01, v_ego)
_kappa_at_t_base = abs(float(interp(_t_base, ModelConstants.T_IDXS, _curvatures_ref)))
if self.inputs.model is not None and len(self.inputs.model.orientationRate.z) >= 17:
_curvatures_ref = np.array(self.inputs.model.orientationRate.z) / max(0.01, v_ego)
_kappa_at_t_base = abs(float(interp(_t_base, MODEL_T_IDXS, _curvatures_ref)))
_kappa_entering = _kappa_at_t_base > abs(desired_curvature)
if _kappa_entering:
_kappa_factor = 1.0 # curve deepening ahead: full extra lookahead for gradual entry
@@ -378,10 +366,10 @@ class LateralAngleExt:
self.bp_curvature_lookup_time = curvature_lookup_time
predicted_curvature = 0.0
if self.model is not None and len(self.model.orientationRate.z) >= 17:
curvatures = np.array(self.model.orientationRate.z) / max(0.01, v_ego)
if self.inputs.model is not None and len(self.inputs.model.orientationRate.z) >= 17:
curvatures = np.array(self.inputs.model.orientationRate.z) / max(0.01, v_ego)
predicted_curvature = float(
interp(curvature_lookup_time, ModelConstants.T_IDXS, curvatures)
interp(curvature_lookup_time, MODEL_T_IDXS, curvatures)
)
b = float(self.path_angle_blend_ratio)
@@ -400,8 +388,8 @@ class LateralAngleExt:
# That caused a positive-feedback flat-line: under-steer → CtrSat → path_angle frozen → more under-steer.
# Use DBC-limit proximity instead: only block when path_angle is already near the ±0.5 rad CAN limits,
# which is the only condition where the anti-snap unwind rate cap makes physical sense.
_dbc_sat = (self.path_angle_last >= FORD_DBC_PATH_ANGLE_MAX * 0.90 or
self.path_angle_last <= FORD_DBC_PATH_ANGLE_MIN * 0.90)
_dbc_sat = (self.shared.path_angle_last >= FORD_DBC_PATH_ANGLE_MAX * 0.90 or
self.shared.path_angle_last <= FORD_DBC_PATH_ANGLE_MIN * 0.90)
_in_hard_sat = _pscm_lim >= 2 or _dbc_sat
# BluePilot: per-call delta threshold. The original 0.002 was authored 2026-05-07 on
# bp-sid-simple (9c3d000fd), which ran STEER_STEP=1 (true 100Hz, switched 2026-04-22) -- so it
@@ -415,26 +403,36 @@ class LateralAngleExt:
requested_curvature = predicted_curvature * b_blend + desired_curvature * (1.0 - b_blend)
self._desired_curvature_last = desired_curvature
if self.model is not None:
self.lane_change = self.model.meta.laneChangeState in (1, 2, 3)
if self.inputs.model is not None:
self.lane_change = self.inputs.model.meta.laneChangeState in (1, 2, 3)
else:
self.lane_change = False
lane_change_factor = interp(
v_ego, self.lane_change_factor_bp, [self.lane_change_factor_low, self.lane_change_factor_high_ang]
v_ego, LANE_CHANGE_FACTOR_BP, [LANE_CHANGE_FACTOR_LOW, self.lane_change_factor_high_ang]
)
if self.lane_change and self.model is not None:
if self.model.meta.laneChangeDirection == 1 and requested_curvature < 0:
if self.lane_change and self.inputs.model is not None:
if self.inputs.model.meta.laneChangeDirection == 1 and requested_curvature < 0:
requested_curvature *= lane_change_factor
precision = 0
elif self.model.meta.laneChangeDirection == 2 and requested_curvature > 0:
elif self.inputs.model.meta.laneChangeDirection == 2 and requested_curvature > 0:
requested_curvature *= lane_change_factor
precision = 0
self.precision_type = precision
self.shared.precision_type = precision
# Use planner / predicted κ directly for the κ → path_angle map; we are not sending κ on CAN.
kappa_cmd = float(requested_curvature)
# BluePilot: lane centering trim (advanced lane positioning) -- nudges kappa_cmd toward true
# lane-line center + user offset, gated on lane-line confidence and disabled during lane
# changes (see lane_center_trim.py). Applied here, before the deviation clip below, so the
# trimmed value inherits every limiter this file already applies to kappa_cmd instead of
# bypassing them.
kappa_cmd = self.lane_center_trim.update(
kappa_cmd, self.inputs.model, v_ego, self.enable_lane_positioning_ang,
self.custom_path_offset_ang, self.lane_centering_strength_ang,
CC.latActive, self.lane_change)
# BluePilot: clip kappa_cmd to current_curvature (measured, from yaw rate) +- CURVATURE_ERROR,
# mirroring lateral_curv_ext.py's apply_ford_curvature_limits_ext exactly (same formula, same
# v_ego > 9 gate, same CarControllerParams.CURVATURE_ERROR tolerance). Without this, kappa_cmd
@@ -444,20 +442,18 @@ class LateralAngleExt:
# routinely, not just on genuine pothole/override divergence. Curvature mode has always clipped
# here; this brings angle mode's actual steering intent in line with that proven behavior rather
# than only clipping the value reported to panda (which would make the check a no-op).
current_curvature = self.get_current_curvature(CS)
self.bp_curvature_deviation_limited = False
current_curvature = self.inputs.get_current_curvature(CS)
self.shared.bp_curvature_deviation_limited = False
if v_ego > 9:
_kappa_cmd_pre_error_clip = kappa_cmd
kappa_cmd = float(clip(kappa_cmd, current_curvature - CarControllerParams.CURVATURE_ERROR,
current_curvature + CarControllerParams.CURVATURE_ERROR))
# BluePilot: did this clip actually constrain kappa_cmd this frame (deviation from measured,
# not rate-of-change -- see carcontroller.py)?
self.bp_curvature_deviation_limited = bool(abs(kappa_cmd - _kappa_cmd_pre_error_clip) > 1e-9)
# not rate-of-change)? The BluePilot UI's deviation flag.
self.shared.bp_curvature_deviation_limited = bool(abs(kappa_cmd - _kappa_cmd_pre_error_clip) > 1e-9)
lateral_uncertainty = 0.0 # no curvature-limit ladder until angle-mode torque display is defined
# Speed-interpolated gain: at low speed both curves use 1.0; at high speed the params take effect.
self.low_gain_calc = interp(
v_ego, [13.5, 26.82], [1.0, (self.path_angle_gain_lowC_highV * self.user_dampening_factor)]
@@ -470,7 +466,6 @@ class LateralAngleExt:
path_angle_calc = kappa_cmd * v_ego * self.curvature_factor
path_angle = path_angle_calc
# PSCM authority limit clamp.
# On CANFD Fords in angle mode, LatCtlLim_D_Stat does not fire, so _pscm_lim stays 0.
# _in_hard_sat (computed above) combines _pscm_lim >= 2 with _dbc_sat (path_angle near ±0.5 rad limit).
@@ -479,7 +474,7 @@ class LateralAngleExt:
# Without the decrease cap, model+planner drop path_angle at ~0.36 rad/s at a sharp apex,
# driving desired steering 30°+ ahead of actual while the PSCM is pinned, causing a snap when released.
if _in_hard_sat:
_last = self.path_angle_last
_last = self.shared.path_angle_last
_last_mag = abs(_last)
_curr_mag = abs(path_angle)
if _curr_mag > _last_mag: # magnitude growing — block
@@ -488,7 +483,7 @@ class LateralAngleExt:
_limited_mag = _last_mag - _PSCM_SAT_UNWIND_RATE
path_angle = float(_limited_mag if _last >= 0 else -_limited_mag)
elif _pscm_lim >= 1: # LimitClose (F150/non-angle-mode only): block increases only
path_angle = float(clip(path_angle, -abs(self.path_angle_last), abs(self.path_angle_last)))
path_angle = float(clip(path_angle, -abs(self.shared.path_angle_last), abs(self.shared.path_angle_last)))
path_angle = min(FORD_DBC_PATH_ANGLE_MAX, max(FORD_DBC_PATH_ANGLE_MIN, path_angle))
@@ -504,12 +499,11 @@ class LateralAngleExt:
_soft_roc = float(interp(v_ego, [9., 10., 15., 25.], [0.055, 0.055, 0.0425, 0.009]))
_path_angle_pre_roc = path_angle
path_angle = float(clip(path_angle,
self.path_angle_last - _soft_roc,
self.path_angle_last + _soft_roc))
self.shared.path_angle_last - _soft_roc,
self.shared.path_angle_last + _soft_roc))
# BluePilot: did the soft ROC clip actually limit the path_angle we wanted to send this frame?
self.bp_angle_rate_limited = bool(abs(path_angle - _path_angle_pre_roc) > 1e-9)
# c0 always zero -- no centering trim in angle mode.
path_offset = 0.0
@@ -518,10 +512,9 @@ class LateralAngleExt:
self.bp_path_angle_gain_highC_highV = self.path_angle_gain_highC_highV
self.bp_low_speed_curv_factor = self.low_speed_curv_factor
self.bp_high_speed_curv_factor = self.high_speed_curv_factor
self.path_angle_last = path_angle
self.shared.path_angle_last = path_angle
self.bp_path_angle_final = path_angle
self.apply_curvature_last = 0.0
# BluePilot: the error-clipped kappa path_angle was derived from -- carcontroller.py reads this
# BluePilot: the error-clipped kappa path_angle was derived from -- carcontroller_ext.py reads this
# as shadow_curvature for ford.h's angle-mode deviation check (see fordcan_ext.create_lka_msg).
# Not just telemetry: an actively-consumed value, unlike the removed *_kappa_cmd_raw stubs.
# While the driver is pressing (before the human-turn override latches), the clipped planner
@@ -530,7 +523,7 @@ class LateralAngleExt:
# in-drive lateral safety block observed across ~3h of replayed road-test routes was exactly
# this (driver fighting a sustained curve with the mode still enabled). The honest command
# during a press is the driver's actual curvature.
self.bp_kappa_cmd = self.get_current_curvature(CS) if CS.out.steeringPressed else kappa_cmd
self.bp_kappa_cmd = self.inputs.get_current_curvature(CS) if CS.out.steeringPressed else kappa_cmd
# BluePilot: would the equivalent curvature (kappa_cmd) have been rate-limited by curvature-mode's
# ROC (apply_std_steer_angle_limits)? kappa_cmd is already error-clipped above (same clip
@@ -551,10 +544,10 @@ class LateralAngleExt:
and abs(_stall_gap) > _STALL_GAP_MIN
and abs(desired_curvature) > abs(current_curvature))
if _stalled:
if self.bp_curvature_deviation_limited and self.stall_blip_cooldown_s <= 0.0:
if self.shared.bp_curvature_deviation_limited and self.stall_blip_cooldown_s <= 0.0:
self.stall_blip_hold_s += _STEER_DT
if (self.stall_blip_hold_s >= _STALL_HOLD_S and self.stall_blip_count < _STALL_MAX_BLIPS
and abs(self.path_angle_last) < _BLIP_MAX_PATH_ANGLE):
and abs(self.shared.path_angle_last) < _BLIP_MAX_PATH_ANGLE):
self.stall_blip_frames_left = _STALL_BLIP_FRAMES
self.stall_blip_hold_s = 0.0
self.stall_blip_count += 1
@@ -565,13 +558,12 @@ class LateralAngleExt:
ramp_type = 2
return LateralResult(
apply_curvature=0.0,
curvature_rate=curvature_rate,
path_offset=path_offset,
path_angle=path_angle,
ramp_type=ramp_type,
precision_type=self.precision_type,
precision_type=self.shared.precision_type,
lateralUncertainty=lateral_uncertainty,
)
@@ -0,0 +1,448 @@
"""
BluePilot Ford lateral curvature extension.
Implements Ford's 4-signal lateral control (curvature, curvature_rate, path_offset, path_angle)
using predicted curvature from modelV2, PID-based lane centering, and laneline-aware path offset.
Held by BluePilotLateral (carcontroller_ext.py) as LateralCurvExt.
Ford uses four signals to control steering:
- curvature: primary steering command (also used in upstream, limited to 0.02 m^-1)
- curvature_rate: always 0, like upstream and stock Ford
- path_offset: lateral offset for lane centering (blends model + laneline data)
- path_angle: heading angle correction via PID controller
A detailed explanation of Ford's control protocol:
https://www.f150gen14.com/forum/threads/introducing-bluepilot-a-ford-specific-fork-for-comma3x-openpilot.24241/#post-457706
"""
from collections import namedtuple, deque
from enum import IntEnum
import numpy as np
from numpy import clip, interp
from ...car.common.pid import PIDController
from ...car import ACCELERATION_DUE_TO_GRAVITY
from ...car.lateral import ISO_LATERAL_ACCEL, apply_std_steer_angle_limits
from ...car.ford.values import CarControllerParams, FordFlags
from .values_ext import BP_ANGLE_LIMITS, CURVATURE_MAX, LANE_CHANGE_FACTOR_BP, LANE_CHANGE_FACTOR_LOW
from .values_ext import MODEL_T_IDXS
from ..params.manifest import default, value
# the settings curvature mode reads (params/manifest.py), each kept as an attribute of the same name
CURV_SETTINGS = ("enable_human_turn_detection_curv", "lane_change_factor_high_curv", "pc_blend_ratio_high_C_UI_curv",
"pc_blend_ratio_low_C_UI_curv", "enable_lane_positioning_curv", "custom_path_offset_curv",
"enable_lane_full_mode_curv", "custom_profile_curv", "LC_PID_gain_UI_curv")
class PrimaryLateralControl(IntEnum):
curvature = 0
angle = 1
# CAN FD lateral-accel cap (match opendbc/car/ford/carcontroller.py apply_ford_curvature_limits)
AVERAGE_ROAD_ROLL = 0.06 # ~3.4 degrees, 6% superelevation
MAX_LATERAL_ACCEL = ISO_LATERAL_ACCEL - (ACCELERATION_DUE_TO_GRAVITY * AVERAGE_ROAD_ROLL)
# Result namedtuple returned by LateralCurvExt.update()
LateralResult = namedtuple('LateralResult', [
'apply_curvature',
'curvature_rate',
'path_offset',
'path_angle',
'ramp_type',
'precision_type',
'lateralUncertainty',
])
def apply_ford_curvature_limits_ext(apply_curvature, apply_curvature_last, current_curvature,
v_ego_raw, steering_angle, lat_active, CP):
"""Extended version of apply_ford_curvature_limits that returns
(apply_curvature, max_curvature, curvature_deviation_limited).
The max_curvature value is used by _calculate_lateral_uncertainty() for the steering
torque bar visualization. Stock version returns only apply_curvature.
curvature_deviation_limited (BluePilot): True when the current_curvature +- CURVATURE_ERROR
clip below actually constrained the commanded curvature this frame -- i.e. the desired
maneuver was limited by deviation from measured current curvature, not by rate-of-change
(apply_std_steer_angle_limits below is a separate limiter with its own telemetry).
"""
max_curvature = 1 # large initial value
curvature_deviation_limited = False
# No blending at low speed due to lack of torque wind-up and inaccurate current curvature
if v_ego_raw > 9:
apply_curvature_pre_error_clip = apply_curvature
apply_curvature = np.clip(apply_curvature, current_curvature - CarControllerParams.CURVATURE_ERROR,
current_curvature + CarControllerParams.CURVATURE_ERROR)
curvature_deviation_limited = bool(abs(apply_curvature - apply_curvature_pre_error_clip) > 1e-9)
max_curvature = abs(current_curvature) + CarControllerParams.CURVATURE_ERROR
# Curvature rate limit after driver torque limit (same inputs/order as apply_ford_curvature_limits)
apply_curvature_before_std = apply_curvature
apply_curvature = apply_std_steer_angle_limits(apply_curvature_before_std, apply_curvature_last, v_ego_raw,
steering_angle, lat_active, BP_ANGLE_LIMITS)
# Max one-step envelope for UI: steer_up must use pre-limit curvature (see apply_std_steer_angle_limits)
steer_up = (apply_curvature_last * apply_curvature_before_std >= 0.0
and abs(apply_curvature_before_std) > abs(apply_curvature_last))
rate_limits = BP_ANGLE_LIMITS.ANGLE_RATE_LIMIT_UP if steer_up else BP_ANGLE_LIMITS.ANGLE_RATE_LIMIT_DOWN
std_steer_angle_rate_limit = np.interp(v_ego_raw, rate_limits[0], rate_limits[1])
std_steer_angle_limit = abs(apply_curvature_last) + abs(std_steer_angle_rate_limit)
max_curvature = np.minimum(max_curvature, std_steer_angle_limit)
# Ford CAN FD lateral acceleration limit (more torque available than CAN)
if CP.flags & FordFlags.CANFD:
curvature_accel_limit = MAX_LATERAL_ACCEL / (max(v_ego_raw, 1) ** 2)
apply_curvature = float(np.clip(apply_curvature, -curvature_accel_limit, curvature_accel_limit))
max_curvature = np.minimum(max_curvature, abs(curvature_accel_limit))
return apply_curvature, max_curvature, curvature_deviation_limited
class LateralCurvExt:
"""
BluePilot lateral control extension for Ford vehicles.
Held by BluePilotLateral (carcontroller_ext.py), which calls
update() during the lateral control loop (20Hz) to get the full
4-signal steering command instead of the upstream curvature-only approach.
"""
def __init__(self, inputs, shared):
# BluePilotLateral's LateralInputs (read) and SharedLateralState (read and written), common to both strategies
self.inputs = inputs
self.shared = shared
# Primary lateral control variable: consumed by BluePilotLateral's lateral dispatch.
self.primary_lateral_control = PrimaryLateralControl.curvature
# Settings (updated from the host's params each frame), at their defaults until then
for key in CURV_SETTINGS:
setattr(self, key, default(key))
# Precision/ramp control (precision_type is in the shared state)
self.lateralUncertainty = 0.0
# Predicted curvature blending
self.curvature_lookup_time = 0.2 # seconds into the future for curvature extraction
self.pc_blend_ratio = 0.5
self.pc_blend_ratio_bp = [0.0, 0.001] # curvature breakpoints (1/m)
self.pc_blend_ratio_low = 0.40
self.pc_blend_ratio_high = 0.40
self.pc_blend_ratio_low_C = 0.40 # from UI when custom_profile_curv == 1
self.pc_blend_ratio_high_C = 0.40 # from UI when custom_profile_curv == 1
# Lane change smoothing
self.lane_change = False
self.lane_change_last = False
# Post lane change transition
self.post_lane_change_timer = 0
self.post_lane_change_active = False
self.pre_lane_change_values = {'path_angle': 0.0, 'path_offset': 0.0}
self.max_path_angle_change = 0.00125
self.max_path_offset_change = 0.00125
# Human turn detection (the detector is in the shared state, used by both modes)
self.human_turn = False
self.post_reset_ramp_active = False
self.reset_steering_last = False
# Path offset
self.path_offset_lookup_time = 0.2 # seconds
self.min_laneline_confidence_bp = [0.6, 0.8]
# PID-based path angle for lane centering
self.path_angle_filter_samples = 3
self.path_angle_deque = deque(maxlen=self.path_angle_filter_samples)
self.LC_PID_k_p = 0.25
self.LC_PID_k_i = 0.05
self.LC_PID_controller = PIDController(k_p=self.LC_PID_k_p, k_i=self.LC_PID_k_i, rate=20)
self.LC_PID_speed_bp = [0.0, 9.0, 15.0] # m/s
self.LC_PID_speed_v = [0.0, 0.0, 1.0]
self.LC_path_angle_ROC_bp = [5, 15, 25] # m/s
self.LC_path_angle_ROC_v = [0.003, 0.0015, 0.002] # match panda limits
self.LC_path_angle_reset_counter = 0
self.LC_path_angle_reset_duration = 1.5 # seconds
# DBC signal max limits
self.path_angle_max = 0.5
self.path_offset_max = 2.0
self.curvature_max = CURVATURE_MAX # 0.02
# Previous frame values
self.path_offset_last = 0.0
def update_lateral_params(self, params):
"""Read lateral-related Params from the UI. Called each frame."""
for key in CURV_SETTINGS:
setattr(self, key, value(params, key))
try:
self.primary_lateral_control = PrimaryLateralControl(value(params, "FordPrefLateralControl"))
except ValueError: # a mode other than 0 or 1: curvature mode, the default (#66)
self.primary_lateral_control = PrimaryLateralControl.curvature
def update(self, CC, CS, actuators, apply_curvature_last, CP):
"""
Compute lateral steering signals for the current frame.
Called at 20Hz from BluePilotLateral.step_lateral() when inside the STEER_STEP block.
Args:
CC: CarControl with latActive, hudControl
CS: CarState with vEgoRaw, yawRate, steeringPressed, steeringAngleDeg
actuators: CC.actuators with desired curvature from planner
apply_curvature_last: previous frame's applied curvature
CP: CarParams with flags for CANFD etc.
Returns:
LateralResult namedtuple with all signals needed for CAN message construction.
"""
apply_curvature = 0.0
path_offset = 0.0
path_angle = 0.0
reset_steering = 0
ramp_type = 2
lateralUncertainty = 0.0
curvature_deviation_limited = False
if CC.latActive:
self.shared.precision_type = 1
steeringPressed = CS.out.steeringPressed
steeringAngleDeg_PV = CS.out.steeringAngleDeg
# Select tuning profile
if self.custom_profile_curv == 1:
self.pc_blend_ratio_low_C = self.pc_blend_ratio_low_C_UI_curv
self.pc_blend_ratio_high_C = self.pc_blend_ratio_high_C_UI_curv
self.pc_blend_ratio_v = [self.pc_blend_ratio_low_C, self.pc_blend_ratio_high_C]
# Current and desired curvature
current_curvature = self.inputs.get_current_curvature(CS)
desired_curvature = actuators.curvature
# Extract predicted curvature from modelV2
if self.inputs.model is not None and len(self.inputs.model.orientationRate.z) >= 17:
curvatures = np.array(self.inputs.model.orientationRate.z) / max(0.01, CS.out.vEgoRaw)
predicted_curvature = interp(self.curvature_lookup_time, MODEL_T_IDXS, curvatures)
else:
predicted_curvature = 0.0
# Blend predicted and desired curvature
self.pc_blend_ratio = interp(abs(desired_curvature), self.pc_blend_ratio_bp, self.pc_blend_ratio_v)
requested_curvature = (predicted_curvature * self.pc_blend_ratio) + (desired_curvature * (1 - self.pc_blend_ratio))
# Lane change detection
if self.inputs.model is not None:
self.lane_change = self.inputs.model.meta.laneChangeState in (1, 2, 3)
else:
self.lane_change = False
# Lane change curvature smoothing
lane_change_factor = interp(CS.out.vEgoRaw, LANE_CHANGE_FACTOR_BP,
[LANE_CHANGE_FACTOR_LOW, self.lane_change_factor_high_curv])
if self.lane_change and self.inputs.model is not None:
if self.inputs.model.meta.laneChangeDirection == 1 and requested_curvature < 0:
requested_curvature *= lane_change_factor
self.shared.precision_type = 0
elif self.inputs.model.meta.laneChangeDirection == 2 and requested_curvature > 0:
requested_curvature *= lane_change_factor
self.shared.precision_type = 0
# Human turn: sustained hands-on + large angle (not just a nudge) — see HumanTurnDetector
self.human_turn = self.shared.human_turn_detector.update(
self.enable_human_turn_detection_curv, steeringPressed, steeringAngleDeg_PV)
# Steering reset logic
if self.human_turn or (CS.out.vEgoRaw < 0.1):
reset_steering = 1
if reset_steering == 1:
requested_curvature = 0.0
# Apply curvature limits (extended version returning max_curvature)
apply_curvature, max_curvature, curvature_deviation_limited = apply_ford_curvature_limits_ext(
requested_curvature, apply_curvature_last, current_curvature,
CS.out.vEgoRaw, 0, CC.latActive, CP)
# Lateral uncertainty for torque bar visualization
lateralUncertainty = self._calculate_lateral_uncertainty(requested_curvature, apply_curvature, max_curvature)
# Human turn / standstill reset: after apply_ford_curvature_limits_ext (for torque-bar max_curvature),
# force κ=0 and (below) path_angle=0 on the bus. ford.h accepts that frame (curvature 0 skips the
# curvature checks), but checks the frames after it as usual: a ramp back that exceeds the rate
# limit or sits outside the curvature error band is blocked.
if reset_steering == 1:
apply_curvature = 0.0
self.post_reset_ramp_active = False
else:
# Detect transition from reset to normal
if self.reset_steering_last and not reset_steering:
self.post_reset_ramp_active = True
apply_curvature_last = 0.0 # Reset to ensure clean ramp from 0
# Post-reset ramp: gradually ramp from 0 to avoid safety limit trips
if self.post_reset_ramp_active:
apply_curvature = apply_std_steer_angle_limits(
requested_curvature, apply_curvature_last,
CS.out.vEgoRaw, 0, CC.latActive, BP_ANGLE_LIMITS)
curvature_error = abs(requested_curvature - apply_curvature)
curvature_threshold = max(abs(requested_curvature) * 0.1, 0.001)
if curvature_error < curvature_threshold:
self.post_reset_ramp_active = False
self.reset_steering_last = (reset_steering == 1)
# Path offset: blend model position with laneline data
if self.inputs.model is not None:
path_offset_position = interp(self.path_offset_lookup_time, MODEL_T_IDXS, self.inputs.model.position.y)
path_offset_lanelines = (self.inputs.model.laneLines[1].y[0] + self.inputs.model.laneLines[2].y[0]) / 2
# Laneline width tolerance (prevents jumps when lanes merge/diverge)
laneline_width = self.inputs.model.laneLines[2].y[0] + (-self.inputs.model.laneLines[1].y[0])
laneline_width_tolerance = interp(laneline_width, [3.75, 4.25], [0.81, 0.59])
# Laneline confidence
laneline_confidence = min(self.inputs.model.laneLineProbs[1], self.inputs.model.laneLineProbs[2], laneline_width_tolerance)
if not self.enable_lane_full_mode_curv:
laneline_confidence = 0.0
laneline_path_offset_scale = interp(laneline_confidence, self.min_laneline_confidence_bp, [0.0, 1.0])
path_offset = ((path_offset_position * (1 - laneline_path_offset_scale)) +
(path_offset_lanelines * laneline_path_offset_scale)) + self.custom_path_offset_curv
# No path offset during lane changes
if self.lane_change:
path_offset = 0
# PID-based path angle for lane centering
path_offset_error = path_offset * (self.LC_PID_gain_UI_curv / 100)
LC_PID_speed_factor = interp(CS.out.vEgoRaw, self.LC_PID_speed_bp, self.LC_PID_speed_v)
path_offset_error_adj = path_offset_error * LC_PID_speed_factor
if not self.enable_lane_positioning_curv:
path_offset_error_adj = 0.0
path_angle_low_c = self.LC_PID_controller.update(path_offset_error_adj)
if not self.enable_lane_positioning_curv:
path_angle_low_c = 0.0
if reset_steering == 1:
path_angle_low_c = 0.0
# Rate limit path angle for comfort
path_angle_roc = interp(abs(CS.out.vEgoRaw), self.LC_path_angle_ROC_bp, self.LC_path_angle_ROC_v)
path_angle_low_c = clip(path_angle_low_c, self.shared.path_angle_last - path_angle_roc, self.shared.path_angle_last + path_angle_roc)
# Reset PID if driver applies consistent steering pressure
if steeringPressed:
self.LC_path_angle_reset_counter += 1
else:
self.LC_path_angle_reset_counter = 0
if self.LC_path_angle_reset_counter > self.LC_path_angle_reset_duration * 20:
self.LC_PID_controller.reset()
path_angle = path_angle_low_c # path_angle_high_c not used currently
# Post lane change transition smoothing
path_angle, path_offset = self._handle_post_lane_change_transition(path_angle, path_offset)
# Final reset handling
if reset_steering == 1:
path_angle = 0.0
# Clip all signals to DBC limits
# float() is required: numpy's clip returns np.float64, and capnp rejects that type when
# apply_curvature reaches actuators.curvature via apply_curvature_last in carcontroller.py.
# The cast is exact (both are IEEE 754 doubles), so no output value changes.
apply_curvature = float(clip(apply_curvature, -self.curvature_max, self.curvature_max))
path_offset = float(clip(path_offset, -self.path_offset_max, self.path_offset_max))
path_angle = float(clip(path_angle, -self.path_angle_max, self.path_angle_max))
# Zero path_offset before CAN send (path_offset and path_angle can conflict, causing discomfort)
path_offset = 0.0
# Ramp type selection
if reset_steering == 1:
ramp_type = 3 # Immediate
self.path_angle_deque.clear()
self.LC_PID_controller.reset()
else:
ramp_type = 2 # Fast
else:
# Lateral control off — zero everything
apply_curvature = 0.0
path_offset = 0.0
path_angle = 0.0
self.path_angle_deque.clear()
self.LC_PID_controller.reset()
ramp_type = 0
lateralUncertainty = 0.0
# Update state for next frame
self.lateralUncertainty = lateralUncertainty
self.path_offset_last = path_offset
self.shared.path_angle_last = path_angle
# BluePilot: did the current_curvature +- CURVATURE_ERROR clip constrain the commanded
# curvature this frame (deviation from measured, not rate-of-change)? The BluePilot UI's deviation flag.
self.shared.bp_curvature_deviation_limited = curvature_deviation_limited
return LateralResult(
apply_curvature=apply_curvature,
curvature_rate=0.0, # always 0, like stock Ford
path_offset=path_offset,
path_angle=path_angle,
ramp_type=ramp_type,
precision_type=self.shared.precision_type,
lateralUncertainty=lateralUncertainty,
)
def _handle_post_lane_change_transition(self, path_angle, path_offset):
"""Smooth transition of control variables after lane change completes.
Rate-limits path_angle and path_offset back to target values
over 160 frames (~8 seconds at 20Hz).
"""
# Detect lane change completion (True → False transition)
if self.lane_change_last and not self.lane_change:
self.post_lane_change_active = True
self.post_lane_change_timer = 0
self.pre_lane_change_values = {'path_angle': 0.0, 'path_offset': 0.0}
self.lane_change_last = self.lane_change
if self.post_lane_change_active:
self.post_lane_change_timer += 1
new_path_angle = clip(path_angle,
self.pre_lane_change_values['path_angle'] - self.max_path_angle_change,
self.pre_lane_change_values['path_angle'] + self.max_path_angle_change)
new_path_offset = clip(path_offset,
self.pre_lane_change_values['path_offset'] - self.max_path_offset_change,
self.pre_lane_change_values['path_offset'] + self.max_path_offset_change)
self.pre_lane_change_values = {
'path_angle': new_path_angle,
'path_offset': new_path_offset,
}
if self.post_lane_change_timer >= 160:
self.post_lane_change_active = False
return (new_path_angle, new_path_offset)
return (path_angle, path_offset)
def _calculate_lateral_uncertainty(self, requested_curvature, apply_curvature, max_curvature):
"""Compute ratio of requested to max achievable curvature for the torque bar UI."""
max_curvature = np.clip(max_curvature, apply_curvature, self.curvature_max)
return float(requested_curvature / max_curvature)
@@ -0,0 +1,54 @@
"""
BluePilot: views over the host's LateralInputs (host_types.py) in the modelV2 / vehicleParameters shape
the lateral code reads, and the check that a frame is complete. The host fills LateralInputs once per frame.
"""
import math
from itertools import chain
from types import SimpleNamespace
from ..host_types import MODEL_PATH_POINTS
def lateral_inputs_complete(li) -> bool:
"""True when every array the lateral indexes into has the shape modelV2 always provides, and
every value is finite. A NaN from the model doesn't raise in the lateral math; it reaches the
CAN encoders, which do, so the whole frame is dropped here and the previous inputs are kept."""
return (len(li.pathYawRate) == MODEL_PATH_POINTS and len(li.pathX) == MODEL_PATH_POINTS and len(li.pathY) == MODEL_PATH_POINTS
and len(li.laneLineLeftX) == MODEL_PATH_POINTS and len(li.laneLineLeftY) == MODEL_PATH_POINTS
and len(li.laneLineRightX) == MODEL_PATH_POINTS and len(li.laneLineRightY) == MODEL_PATH_POINTS
and len(li.laneLineProbs) >= 3 and len(li.laneLineStds) >= 3
and all(map(math.isfinite, chain(li.pathYawRate, li.pathX, li.pathY, li.laneLineLeftX, li.laneLineLeftY,
li.laneLineRightX, li.laneLineRightY, li.laneLineProbs, li.laneLineStds,
(li.steerRatio, li.stiffnessFactor, li.roll, li.angleOffsetDeg, li.lateralDelay)))))
class _LaneLine:
def __init__(self, x, y):
self.x = x
self.y = y
class ModelView:
"""The subset of modelV2 the lateral code uses, with the same attribute paths."""
def __init__(self, li):
self.orientationRate = SimpleNamespace(z=li.pathYawRate)
self.position = SimpleNamespace(x=li.pathX, y=li.pathY)
# indices match modelV2.laneLines: 1 = left, 2 = right; 0 and 3 are not used
self.laneLines = [_LaneLine([], []),
_LaneLine(li.laneLineLeftX, li.laneLineLeftY),
_LaneLine(li.laneLineRightX, li.laneLineRightY),
_LaneLine([], [])]
self.laneLineProbs = li.laneLineProbs
self.laneLineStds = li.laneLineStds
self.meta = SimpleNamespace(laneChangeState=li.laneChangeState, laneChangeDirection=li.laneChangeDirection)
class VehicleParamsView:
"""The subset of vehicleParameters the lateral code uses."""
def __init__(self, li):
self.steerRatio = li.steerRatio
self.stiffnessFactor = li.stiffnessFactor
self.roll = li.roll
self.angleOffsetDeg = li.angleOffsetDeg
@@ -1,104 +0,0 @@
"""Pure angle lane-centering trim adapted from BluePilot bp-dev e22afa6b.
Alan Polk and ghbarker; the source also credits StarPilot/u/jc01rho.
See ../CREDITS.md, ../LICENSE and ../LICENSE.md. Geometry/confidence/fallback,
filter and slew follow lane_center_trim.py; this module receives immutable arrays
instead of a host model. Stale geometry and driver pressure additionally suppress
trim, and nonfinite confidence cannot create correction.
"""
from dataclasses import dataclass
import math
from ..params.trim import DEFAULTS, BOUNDS
def clip(value, low, high):
return max(low, min(high, value))
def interp(value, xs, ys):
if value <= xs[0]:
return ys[0]
for index in range(1, len(xs)):
if value < xs[index]:
return ys[index - 1] + (ys[index] - ys[index - 1]) * (value - xs[index - 1]) / (xs[index] - xs[index - 1])
return ys[-1]
@dataclass(frozen=True)
class TrimConfig:
enabled: bool = DEFAULTS["enabled"]
offset_m: float = DEFAULTS["offset_m"]
gain: float = DEFAULTS["gain"]
@dataclass(frozen=True)
class LaneGeometry:
position_x: tuple = ()
position_y: tuple = ()
left_x: tuple = ()
left_y: tuple = ()
right_x: tuple = ()
right_y: tuple = ()
probabilities: tuple = ()
standard_deviations: tuple = ()
fresh: bool = False
def sample(xs, ys, distance):
if (not isinstance(xs, tuple) or not isinstance(ys, tuple) or len(xs) < 2 or len(xs) != len(ys) or
not all(math.isfinite(v) for v in (*xs, *ys)) or not all(b > a for a, b in zip(xs, xs[1:], strict=False))):
return None
return interp(distance, xs, ys)
class LaneCenterTrim:
def __init__(self):
self.correction = 0.0
def reset(self):
self.correction = 0.0
def update(self, requested, speed, geometry, config, *, active, lane_change, steering_pressed=False):
try:
valid = (isinstance(config, TrimConfig) and config.enabled is True and active and not lane_change and
not steering_pressed and isinstance(geometry, LaneGeometry) and geometry.fresh is True and
math.isfinite(requested) and math.isfinite(speed) and speed >= 0.0 and
math.isfinite(config.offset_m) and BOUNDS["offset_m"][0] <= config.offset_m <= BOUNDS["offset_m"][1] and
math.isfinite(config.gain) and BOUNDS["gain"][0] <= config.gain <= BOUNDS["gain"][1])
authority = interp(speed, BOUNDS["speed_mps"], BOUNDS["speed_authority"]) if valid else 0.0
if not valid or authority <= 0.0:
self.reset()
return requested
distance = clip(speed, *BOUNDS["lookahead_m"])
model_y = sample(geometry.position_x, geometry.position_y, distance)
if model_y is None:
self.reset()
return requested
# Bad lane-only geometry retains the source's model-position/offset fallback.
scale, center = self.lane_blend(geometry, distance)
error = scale * (center - model_y) + config.offset_m
raw = 2.0 * error / distance ** 2
target = clip(raw, -BOUNDS["raw_correction"], BOUNDS["raw_correction"]) * config.gain * authority
alpha = 1.0 - math.exp(-BOUNDS["period_s"] / BOUNDS["tau_s"])
filtered = alpha * target + (1.0 - alpha) * self.correction
self.correction = clip(filtered, self.correction - BOUNDS["correction_delta"], self.correction + BOUNDS["correction_delta"])
return requested + self.correction
except (AttributeError, IndexError, TypeError, ValueError, OverflowError):
self.reset()
return requested
def lane_blend(self, geometry, distance):
try:
left = sample(geometry.left_x, geometry.left_y, distance)
right = sample(geometry.right_x, geometry.right_y, distance)
probs, stds = geometry.probabilities, geometry.standard_deviations
if (left is None or right is None or not isinstance(probs, tuple) or not isinstance(stds, tuple) or
len(probs) != 2 or len(stds) != 2 or not all(math.isfinite(p) and 0.0 <= p <= 1.0 for p in probs) or
not all(math.isfinite(s) and s >= 0.0 for s in stds)):
return 0.0, 0.0
confidence = min(*probs, interp(right - left, BOUNDS["width_m"], BOUNDS["width_tolerance"]),
interp(max(stds), BOUNDS["std"], BOUNDS["std_tolerance"]))
scale = clip(interp(confidence, BOUNDS["confidence"], (0.0, 1.0)), 0.0, 1.0)
return scale, 0.5 * (left + right)
except (AttributeError, IndexError, TypeError, ValueError, OverflowError):
return 0.0, 0.0
@@ -0,0 +1,156 @@
from ...car import structs
from ...car.lateral import AngleSteeringLimits
from .. import host_types
from ..params.manifest import PER_FRAME_KEYS, startup_value
class FordSafetyFlagsSP:
"""BluePilot's Ford safety flags: the bits of the second safety param a host sends the panda
(current_safety_param_sp, USB control 0xdf), not the main safetyConfigs[].safetyParam (HOSTS.md).
ford_bp_init reads it with GET_FLAG(current_safety_param_sp, ...), same pattern as Subaru
STOP_AND_GO (subaru_common.h). Plain int constants, not IntFlag: the param must stay a plain int
through the host's serialization.
"""
# Bit layout of the SP safety param (mirrored in ford_bp_init, bluepilot_lateral/safety/ford.h; pinned by
# tests/test_safety_param_bits.py):
# bit 0 STEER_ANGLE_CURVATURE (pinion-sourced curvature measurement)
# bits 1-4 pinion geometry index (FORD_PINION_GEOMETRY_SHIFT / FORD_PINION_GEOMETRY_INDEX)
# bit 5 BP_LATERAL: the panda runs the 4-signal checks (ford_lmc_checks) instead of
# upstream's stock curvature-only checks. Clear = upstream safety, verbatim.
STEER_ANGLE_CURVATURE = 1
BP_LATERAL = 1 << 5
# Geometry-table index for the steering-angle curvature measurement, packed into
# the startup safety bits 1-4 when STEER_ANGLE_CURVATURE is set. Must match the
# ford_pinion_geometry table in bluepilot_lateral/safety/ford.h row for row.
# Index 0 is reserved as invalid: the firmware treats flag-set-but-no-index as feature
# off, so a half-configured param can never select the wrong geometry silently.
# Platforms with no row are in FORD_NO_PINION_PLATFORMS. Indices stay aligned with the C table.
# BluePilot: the per-platform tables key on the platform name (CP.carFingerprint), not the CAR enum,
# so a row for a platform some host lacks never breaks import there; it just never matches.
# FORD_HOST_ONLY_PLATFORMS lists the names with rows here that upstream opendbc's CAR doesn't have; any
# name a host's CAR lacks must be on it (tests/test_platform_tables.py).
FORD_HOST_ONLY_PLATFORMS: frozenset[str] = frozenset({
"FORD_EDGE_MK2", # StarPilot
"FORD_MONDEO_MK5", # BP7, StarPilot
"FORD_TRANSIT_MK5", # StarPilot
})
# No pinion geometry row, so no STEER_ANGLE_CURVATURE (the setting no-ops and the yaw source is kept):
FORD_NO_PINION_PLATFORMS: frozenset[str] = frozenset({
# ALT_STEER_ANGLE: reads a RELATIVE pinion angle (SteeringPinion_Data_Alt + learned offset) and lacks the
# absolute measurement this needs
"FORD_EDGE_MK2",
# LKA steering through Lane_Assist_Data1 alone: no BluePilot lateral at all
"FORD_TRANSIT_MK5",
})
FORD_PINION_GEOMETRY_SHIFT = 1
FORD_PINION_GEOMETRY_INDEX = {
"FORD_BRONCO_SPORT_MK1": 1,
"FORD_ESCAPE_MK4": 2,
"FORD_ESCAPE_MK4_5": 3,
"FORD_EXPEDITION_MK4": 4,
"FORD_EXPLORER_MK6": 5,
"FORD_FOCUS_MK4": 6,
"FORD_F_150_LIGHTNING_MK1": 7,
"FORD_F_150_MK14": 8,
"FORD_MAVERICK_MK1": 9,
"FORD_MONDEO_MK5": 10,
"FORD_MUSTANG_MACH_E_MK1": 11,
"FORD_RANGER_MK2": 12,
}
def ford_safety_param(CP: structs.CarParams, params_dict: dict[str, str]) -> int:
"""The startup safety bits for this car, from the startup settings (HOSTS.md). A host ORs them into the
second safety param it sends the panda (current_safety_param_sp, USB 0xdf) at car init."""
# Steering-angle curvature measurement (bad-yaw-sensor workaround): the STEER_ANGLE_CURVATURE flag +
# the platform geometry-table index; the control side mirrors the same flag
# (carcontroller_ext.InputTracker.get_current_curvature). Platforms without a geometry row silently
# keep stock yaw behavior -- the toggle no-ops rather than half-configuring.
bits = 0
if CP.brand == 'ford':
# BluePilot 4-signal lateral: tell the panda to run the BP checks. Read once here, at init;
# the carcontroller sends 4-signal messages only when this bit is set. Changing the setting
# while driving does not change the panda: stock messages are then judged by the BP
# envelope (which is not identical to the stock one), so the setting takes full effect at
# the next init. Same property as openpilot longitudinal.
bp_lateral = not startup_value(params_dict, "disable_BP_lat_UI")
if bp_lateral:
bits |= FordSafetyFlagsSP.BP_LATERAL
# The pinion-sourced measurement is a BP lateral feature: its wider error band and the
# Python side's yaw-vs-pinion handling only line up on the BP path, so it is off whenever
# BP lateral is off (ford_bp_init enforces the same on the panda).
steer_angle_curvature = bp_lateral and startup_value(params_dict, "FordPrefSteerAngleCurvature")
if steer_angle_curvature:
geometry_index = FORD_PINION_GEOMETRY_INDEX.get(CP.carFingerprint)
if geometry_index is not None:
bits |= FordSafetyFlagsSP.STEER_ANGLE_CURVATURE | (geometry_index << FORD_PINION_GEOMETRY_SHIFT)
return bits
# Pinion geometry wheelbase (m) where BluePilot deliberately differs from CarSpecs. Used by both the
# C table (ford_pinion_geometry in bluepilot_lateral/safety/ford.h) and the Python pinion path
# (carcontroller_ext.pinion_vehicle_model), so the two layers convert pinion angle the same way.
# FORD_F_150_MK14: the trucks with lane centering come in 3.68 and 3.99 m wheelbases and nothing on
# the car tells them apart; the average keeps either within about 4% (#21).
FORD_PINION_WHEELBASE = {
"FORD_F_150_MK14": 3.84,
}
# BluePilot: Max curvature for steering command (m^-1), from DBC file limits
CURVATURE_MAX = 0.02
# BluePilot: Curvature rate limits — 3-point breakpoints for smoother lateral control.
# Upstream opendbc uses 2-point ([5, 25]) with more conservative values.
# These allow higher rates at low speed for responsiveness, lower rates at mid-speed
# for comfort, and very low rates at highway speed for stability.
#
# Control (Python) uses stricter windup than unwind so OP stays inside panda when apply_std
# picks the wrong table vs ford_bp_curvature_cmd_checks. Safety firmware is looser than both: its
# up table is the control's down table, and its down table is 1% looser again — see
# bluepilot_lateral/safety/ford.h FORD_LIMITS.
_BP_ANGLE_RATE_UP = ([5, 16, 25], [0.0025, 0.0012, 0.00008])
_BP_ANGLE_RATE_DOWN = ([5, 16, 25], [0.0025, 0.0014, 0.00018])
BP_ANGLE_LIMITS = AngleSteeringLimits(
0.02, # Max curvature for steering command, m^-1
_BP_ANGLE_RATE_UP,
_BP_ANGLE_RATE_DOWN,
)
# The model's prediction times, part of the host contract (host_types.py); the strategies read it from here.
MODEL_T_IDXS = host_types.MODEL_T_IDXS
# The per-frame settings a host publishes every frame (HOSTS.md; hosts import this name from here). The
# settings, their defaults and how they are read are in params/manifest.py.
BP_LATERAL_PARAMS = PER_FRAME_KEYS
# Lane change smoothing, both modes: speed breakpoints (m/s) and the low-speed factor
LANE_CHANGE_FACTOR_BP = (4.4, 40.23)
LANE_CHANGE_FACTOR_LOW = 0.95
# Angle mode's per-platform high-speed gain defaults (low curvature, high curvature); any other
# platform gets ANGLE_GAIN_CAN. Read by lateral_angle_ext.update_angle_params.
# CAN vehicles (Escape MK4, Bronco Sport, Explorer, Maverick, Edge)
ANGLE_GAIN_CAN = (1.00, 1.15)
# CAN-FD body-on-frame trucks (F-150, Lightning, Expedition, Ranger)
ANGLE_GAIN_CANFD_BOF = (0.95, 0.95)
# CAN-FD unibody SUVs (Mustang Mach-E, Escape MK4.5)
ANGLE_GAIN_CANFD_SUV = (1.00, 1.05)
ANGLE_GAIN_CANFD_BOF_CARS = frozenset({
"FORD_F_150_MK14",
"FORD_F_150_LIGHTNING_MK1",
"FORD_EXPEDITION_MK4",
"FORD_RANGER_MK2",
})
ANGLE_GAIN_CANFD_SUV_CARS = frozenset({
"FORD_MUSTANG_MACH_E_MK1",
"FORD_ESCAPE_MK4_5",
})
@@ -0,0 +1,48 @@
"""BluePilot: the types a host hands the library and reads back from it (HOSTS.md).
Standard library only: opendbc/car/structs.py imports LateralInputs for CarControlSP, so an import of
anything in opendbc here would be an import cycle."""
from dataclasses import dataclass, field
# The model's prediction grid: every path array in LateralInputs has MODEL_PATH_POINTS entries at these
# times, openpilot's ModelConstants.T_IDXS (t = 10 * (i / 32)^2). The core interpolates over it.
MODEL_PATH_POINTS = 33
MODEL_T_IDXS = [10.0 * (i / 32) ** 2 for i in range(MODEL_PATH_POINTS)]
@dataclass
class LateralInputs:
"""Per-frame model / vehicle inputs for the Ford 4-signal lateral control. The host fills them from
modelV2 / vehicleParameters / lateralDelay, so opendbc never subscribes to messaging."""
valid: bool = False # producer saw model + vehicle params and filled every field below
pathYawRate: list[float] = field(default_factory=list) # modelV2.orientationRate.z, 33 points over T_IDXS
pathX: list[float] = field(default_factory=list) # modelV2.position.x
pathY: list[float] = field(default_factory=list) # modelV2.position.y
laneLineLeftX: list[float] = field(default_factory=list) # modelV2.laneLines[1]
laneLineLeftY: list[float] = field(default_factory=list)
laneLineRightX: list[float] = field(default_factory=list) # modelV2.laneLines[2]
laneLineRightY: list[float] = field(default_factory=list)
laneLineProbs: list[float] = field(default_factory=list) # 4 entries
laneLineStds: list[float] = field(default_factory=list)
laneChangeState: int = 0 # modelV2.meta.laneChangeState
laneChangeDirection: int = 0 # modelV2.meta.laneChangeDirection
steerRatio: float = 0.0 # vehicleParameters
stiffnessFactor: float = 0.0
roll: float = 0.0
angleOffsetDeg: float = 0.0
lateralDelay: float = 0.0 # lateralDelay.lateralDelay
@dataclass
class LateralStatus:
"""This frame's values for a BluePilot UI, from BluePilotLateral.status(). The names are the ones
BP7's controllerStateBP publisher reads off its car controller; the four angle-mode values read
False outside angle mode, as BP7 sets them."""
lateralUncertainty: float = 0.0 # from whichever strategy ran last
angleRateLimited: bool = False # angle mode: the path_angle soft-ROC clip bit this frame
curvatureRateLimited: bool = False # angle mode: the equivalent curvature would have been rate-limited
curvatureDeviationLimited: bool = False # either mode: the current-curvature error clip constrained the command
humanTurnLateralPaused: bool = False # angle mode: lateral sent inactive during a manual turn
stallBlipActive: bool = False # angle mode: the brief mode-0 pulse after a post-override stall
disable_BP_lat_UI: bool = False # BluePilot lateral off: the UI toggle, or the panda bit not set at init
primary_lateral_control: int = 0 # 0 = curvature, 1 = angle

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