Compare commits

...

283 Commits

Author SHA1 Message Date
royjr d48cd9d75c Merge branch 'hyundai-custom-button' into ccnc-port-hyundai-custom-button 2026-10-28 11:03:01 -04:00
royjr 20fcf35696 Merge branch 'master' into hyundai-custom-button 2026-10-27 16:10:18 -04:00
royjr 5a950c6524 Merge branch 'master' into ccnc-port 2026-10-27 14:13:44 -04:00
royjr ae2df123c5 Update opendbc_repo 2026-10-27 14:13:18 -04:00
amzoo db27a433fc Toyota TSS2: custom longitudinal tuning (#1774)
* TSS2-Smooth: add param, flag injection, UI toggle, and metadata

- Register TSS2-Smooth param in params_keys.h
- Add TSS2-Smooth to toyota keys in initialize_params()
- Update opendbc submodule to use CP_SP flag instead of direct Params import
- Add TSS2 Smooth Longitudinal toggle to Toyota settings UI
- Disable toggle when stock longitudinal is enforced
- Add TSS2-Smooth entry to params_metadata.json

* toyota: rework TSS2 smooth param and toggle

* bump opendbc

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-10-27 00:43:20 -04:00
royjr fddd0a35f4 Merge branch 'master' into ccnc-port 2026-10-25 23:39:51 -04:00
Jason Wen 08bf9b3511 Merge branch 'master' into hyundai-custom-button 2026-10-25 22:55:36 -04:00
Jason Wen c9316cc3d2 Merge branch 'master' into hyundai-custom-button
# Conflicts:
#	opendbc_repo
2026-10-25 22:50:04 -04:00
Jason Wen a06366ce92 controls: decouple RELC from BSM blocking, allow nudge override (#2049)
* controls: decouple RELC from BSM blocking, allow nudge override

* nah
2026-10-25 22:48:26 -04:00
nitrobass24 861fd118a1 ci: scons cache test runs (#2074) 2026-10-25 17:23:33 -07:00
royjr 49bc83f623 Merge branch 'master' into ccnc-port 2026-10-23 01:07:54 -04:00
royjr ec59771282 Update opendbc_repo 2026-10-23 01:07:50 -04:00
Jason Wen 8e740e0ac6 bump opendbc 2026-10-22 22:20:29 -04:00
royjr a06f1bc571 Merge branch 'master' into ccnc-port 2026-10-22 16:28:38 -04:00
James Vecellio-Grant 9d029d683d models: self healing test for tg (#2072)
* models: self healing test for tg

* rm container

* ew

* set env

* set var
2026-10-22 13:43:42 -04:00
Amy Jeanes f78c6c2377 models: publish the download speed and derive the eta from it (#2052)
* models: publish the download speed and derive the eta from it

Fills in DownloadProgress.speed (bytes per second, declared with the parallel
downloader). The reporter measures the rate between its 0.5 s reports, smooths
it, and derives the eta from the bytes remaining at that rate instead of
extrapolating elapsed time. Speed is zeroed when a download ends, fails or is
cancelled and for a cached artifact, and copied across artifacts that share a
file like the other progress fields.

The big UI's Models page shows "12.3 MB/s" next to the progress bar and the
mici models panel shows it beside the percentage. Both take the max across a
bundle's artifacts, not the sum, since artifacts sharing a file mirror the same
transfer and only one file is ever in flight.

A stall decays the smoothed speed toward 0 without ever reaching it, so below
0.05 MB/s, which would only display as "0.0 MB/s", it snaps to 0. Both UIs
then show "--.- MB/s" while downloading with no rate (before the first reading
or during a stall) instead of a stuck "0.0 MB/s" or nothing, and the speed
recovers straight to the real rate once data flows again. The snap also keeps
the eta in range: without it a stall of about 25 s drove remaining / speed past
the capnp UInt32 eta, and the capnp error failed the download outright instead
of leaving the stall to the read timeout and retries. A test holds a piece
stalled until the published speed reaches exactly 0, and errors on that
overflow without the snap.

Assisted-by: Claude Code (Claude Opus 4.8)
Assisted-by: Claude Code (Claude Opus 5.5)

* remove unneeded comments

Assisted-by: Claude Code (Claude Opus 5.5)

* models: take the speed baseline before the workers start

The reporter took its starting byte count after the piece workers were
submitted, and starting each pool thread lets earlier workers run, so
bytes landed in that gap never counted toward a speed sample. Take the
baseline in _download_file before any worker exists and pass it in.

The test runs every piece inline in submit, so the whole file lands
before the reporter starts; with the late baseline the speed stays 0.

Assisted-by: Claude Code (Claude Opus 5.5)
2026-10-21 16:13:52 -07:00
James Vecellio-Grant 1aa3a430fb models: pin tg to @02e6981 (#2071) 2026-10-21 11:11:24 -07:00
Amy Jeanes ddd7b14ebc models: download whole-file artifacts with parallel byte ranges (#2051)
* models: download whole-file artifacts with parallel byte ranges

HuggingFace's Xet CAS throttles each connection to roughly 1-2 MB/s but does
not limit connection count, so the single-stream _download_file left the link
idle most of the time. Replace it with a downloader that probes the size with
a one-byte Range request, creates the file at its final size and streams each
8 MB piece straight to its offset from one of 12 worker threads. The file is
never copied or assembled and only ever occupies its own size on disk. A
server that answers the probe with 200 gets one plain stream instead.
Every request sends Accept-Encoding: identity, because byte ranges count
encoded bytes while requests hands back decoded ones, so a compressed
response would land at the wrong offsets and fail as a short read.

Workers only write bytes; a reporter coroutine on the event loop publishes
progress every 0.5 s (instead of every 128 KB), re-raises the first worker
failure and polls the cancel ref. Transient errors (connection, timeout,
truncated body, HTTP 408/429/500/502/503/504) give a piece up to three
attempts; any other HTTP error fails the download at once. The whole-file
path now also publishes "verifying" while hashing before and after the
transfer, and the download is hashed against the manifest exactly as before.

A cancel has to wait for in-flight pieces to unwind (a blocked socket read
cannot be interrupted), so the row shows "canceling..." through a new
DownloadProgress.canceling flag instead of a frozen bar, and the mici cancel
button hides once the ref is gone, as it already does on the big UI.

DownloadProgress gains speed, canceling and resuming, appended to the capnp so
older readers are unaffected. All three are declared here so the wire format
lands in one place; speed and resuming are filled in by the follow-up speed
and resume changes.

Builds on #2050, which drops chunking and publishes models as whole files;
this makes that whole-file download parallel.

Measured on a comma 3X on WiFi: ~2 MB/s single stream, 17-24 MB/s steady and
30-60 MB/s peak with 12 range connections, the count picked from an on-device
sweep. Disk is not the bottleneck (eMMC writes at 56-67 MB/s with fdatasync).

Tests drive the real downloader against a local threaded range server: piece
tiling, actual parallelism (a stalled piece does not block the others), the
no-range fallback, per-piece retry, 4xx, cancel via ModelManager_DownloadRef
and mid-piece, verify/cached/downloaded state, a probe failure moving
past verifying, and hash mismatch cleanup.

Also drops two chunk-era leftovers in the tests: a stale "no chunks" note on
manifest_bundle and a one-item URL loop in the live manifest check.

Assisted-by: Claude Code (Claude Opus 4.8)
Assisted-by: Claude Code (Claude Opus 5.5)

* remove unneeded comments

* models: trim the manager tests to the regressions they catch

Each remaining test is there because a deliberate break in manager.py,
helpers.py or fetcher.py fails it. Tests that only re-checked what another
already catches are merged into a happy-path download or a table, and
checks of constants rather than behaviour are dropped. The fake server
now starts per test, keeps its state in one dataclass, and can cut a
response short.

Against 66 deliberate breaks, every one the old tests caught still fails,
and a 404 being retried, a short read and its retry, and the display
flags choosing the big model without a GPU are now caught too.

Assisted-by: Claude Code (Claude Opus 5.5)

* Update test_manager_download.py

* Update manager.py

---------

Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
2026-10-21 09:35:43 -07:00
royjr db02972082 Merge branch 'master' into ccnc-port 2026-10-19 20:24:20 -04:00
royjr 6c5711081b Update opendbc_repo 2026-10-19 20:24:12 -04:00
James Vecellio-Grant 922513f312 ci: lfs pointers survive gc (#2066)
* ci: lfs pointers survive gc

* comma got gc'ed

* format
2026-10-18 22:41:23 -07:00
royjr f5ea257318 Merge branch 'master' into ccnc-port 2026-10-16 12:56:47 -04:00
royjr 92a39eb58c Merge branch 'master' into ccnc-port 2026-10-16 12:40:49 -04:00
royjr 107a14650e Merge branch 'master' into ccnc-port 2026-10-07 13:42:56 -04:00
royjr 9dd71b14a7 Update opendbc_repo 2026-10-07 13:42:44 -04:00
royjr 45f7fcdd05 Update opendbc_repo 2026-09-28 00:28:40 -04:00
royjr dfd06dbf69 Update opendbc_repo 2026-09-23 21:47:39 -04:00
royjr 110f453d5f Update opendbc_repo 2026-09-17 00:04:11 -04:00
royjr 320b9e8783 Merge branch 'master' into ccnc-port 2026-09-16 13:01:07 -04:00
royjr 12f2c30c14 Merge branch 'master' into ccnc-port 2026-09-07 00:44:44 -04:00
royjr 0999b0cbe9 Merge branch 'master' into ccnc-port 2026-09-02 15:16:21 -04:00
royjr ed1121e61c Update opendbc_repo 2026-09-02 15:16:02 -04:00
royjr 50d7f75bfc Merge branch 'master' into ccnc-port 2026-08-30 16:21:17 -04:00
royjr 15c7f52e40 Merge branch 'master' into ccnc-port 2026-08-28 20:28:33 -04:00
royjr 8dfe04a318 Merge branch 'master' into ccnc-port 2026-08-24 14:19:36 -04:00
royjr 9648ac2f04 Merge branch 'master' into ccnc-port 2026-08-23 21:48:17 -04:00
royjr a03a11e333 Merge branch 'master' into ccnc-port 2026-08-23 19:36:45 -04:00
royjr 53b1070b09 Merge branch 'master' into ccnc-port 2026-08-23 11:05:40 -04:00
royjr 68bf6d8162 Merge branch 'master' into ccnc-port 2026-08-22 13:19:05 -04:00
royjr 8b472291d0 Merge branch 'master' into ccnc-port 2026-08-21 17:12:13 -04:00
royjr 797c8d4293 Update opendbc_repo 2026-08-21 17:09:31 -04:00
royjr 7568528507 Merge branch 'master' into ccnc-port 2026-08-19 12:53:59 -04:00
royjr 6665c3acf6 Merge branch 'master' into ccnc-port 2026-08-17 10:14:33 -04:00
royjr 3b13eb4d0d Update opendbc_repo 2026-08-17 10:14:19 -04:00
royjr 424311491c ui: preserve custom button presses 2026-08-16 21:14:39 -04:00
royjr a8795674b8 ui: remove custom button quiet mode 2026-08-16 16:33:39 -04:00
royjr 823b447fff ui: adapt custom button cycle by device 2026-08-16 16:29:12 -04:00
royjr c36db99c2d ui: cycle custom button navigation 2026-08-16 16:27:59 -04:00
royjr 5426b6af86 ui: toggle custom button navigation targets 2026-08-16 16:26:44 -04:00
royjr bf0cdd667b ui: add custom button navigation actions 2026-08-16 16:24:42 -04:00
royjr 9af59caf9c ui: compartmentalize custom button handling 2026-08-16 16:21:05 -04:00
royjr 1a75c53ea4 ui: handle custom steering button on comma 4 2026-08-16 16:18:09 -04:00
royjr 83999585ec Merge branch 'master' into hyundai-custom-button
# Conflicts:
#	opendbc_repo
#	selfdrive/ui/feedback/feedbackd.py
#	sunnypilot/sunnylink/params_metadata.json
2026-08-16 16:02:16 -04:00
royjr 138353adb4 Update opendbc_repo 2026-08-16 15:39:52 -04:00
royjr f7eb4c2561 Update opendbc_repo 2026-08-15 01:39:27 -04:00
royjr be9255358c Update opendbc_repo 2026-08-15 01:35:33 -04:00
royjr f48ccb5d7a Merge branch 'master' into ccnc-port 2026-08-14 20:42:45 -04:00
royjr 99559d6749 Update opendbc_repo 2026-08-14 20:42:36 -04:00
royjr 4b6f0ffb46 Merge branch 'master' into ccnc-port 2026-08-11 11:56:46 -04:00
royjr 55415382ce Update opendbc_repo 2026-08-11 11:56:38 -04:00
royjr 9f30901eb6 Merge branch 'master' into ccnc-port 2026-08-10 16:33:50 -04:00
royjr d2045d24fb Update opendbc_repo 2026-08-10 16:33:37 -04:00
royjr 6488a11e4d Merge branch 'master' into ccnc-port 2026-08-02 14:27:34 -04:00
royjr 7b22b65313 Merge branch 'master' into ccnc-port 2026-07-27 15:29:04 -04:00
royjr b59a50101e Update opendbc_repo 2026-07-27 15:28:51 -04:00
royjr 3e30962bd3 Merge branch 'master' into ccnc-port 2026-07-25 03:04:23 -04:00
royjr 3274043063 Update opendbc_repo 2026-07-25 03:04:12 -04:00
royjr c624550d2e Merge branch 'master' into ccnc-port 2026-07-20 15:52:41 -04:00
royjr 6759038671 Merge branch 'master' into ccnc-port 2026-07-20 09:18:18 -04:00
royjr aa636e75c8 Merge branch 'master' into ccnc-port 2026-07-16 00:23:49 -04:00
royjr 5ff3c3bd8d Update opendbc_repo 2026-07-16 00:23:43 -04:00
royjr 1e11b52290 Update opendbc_repo 2026-07-14 00:40:02 -04:00
royjr 76e0204025 Update opendbc_repo 2026-07-14 00:37:13 -04:00
royjr cc88f2cbd6 Update opendbc_repo 2026-07-10 22:00:25 -04:00
royjr ee0ac199a4 Update opendbc_repo 2026-07-03 08:08:29 -04:00
royjr d5ed828eaa Update opendbc_repo 2026-07-01 19:30:06 -04:00
royjr 9fca585f2a Merge branch 'master' into ccnc-port 2026-06-30 22:14:33 -04:00
royjr bb1259303e Update opendbc_repo 2026-06-30 22:14:23 -04:00
royjr e135051ca8 Merge branch 'master' into ccnc-port 2026-06-13 22:38:22 -04:00
royjr 12bef55d8a Update opendbc_repo 2026-06-13 22:12:00 -04:00
royjr 2f7a45e6c8 Merge branch 'master' into ccnc-port 2026-06-08 22:01:57 -04:00
royjr 936ebfc12b Update opendbc_repo 2026-06-08 22:01:44 -04:00
royjr aa0c9dc0eb Merge branch 'master' into ccnc-port 2026-06-04 09:52:35 -04:00
royjr 7476a866e7 Update opendbc_repo 2026-06-04 09:52:06 -04:00
royjr 610d857e33 Merge branch 'master' into ccnc-port 2026-05-28 08:30:40 -04:00
royjr d2f47407d0 Update opendbc_repo 2026-05-12 21:03:18 -04:00
royjr db75ec76ea Merge branch 'master' into ccnc-port 2026-05-12 20:44:57 -04:00
royjr 24066465d7 Update opendbc_repo 2026-05-12 20:44:48 -04:00
royjr a7abbd6e25 Update opendbc_repo 2026-04-20 18:22:12 -04:00
royjr 878982447c Merge branch 'master' into ccnc-port 2026-04-19 12:06:46 -04:00
royjr 576527a36b Merge branch 'master' into ccnc-port 2026-04-17 05:42:35 -04:00
royjr ef8c35da24 Update opendbc_repo 2026-04-17 05:41:21 -04:00
royjr 85688b1040 Merge branch 'master' into ccnc-port 2026-04-16 19:48:39 -04:00
royjr 0fb2199130 Update opendbc_repo 2026-04-16 19:47:57 -04:00
royjr d48d756c1d Update opendbc_repo 2026-04-08 00:56:54 -04:00
royjr 2ed298a0c9 Merge branch 'master' into ccnc-port 2026-04-08 00:51:13 -04:00
royjr d68f038949 Update opendbc_repo 2026-04-08 00:51:11 -04:00
royjr 7231571e57 Merge branch 'master' into ccnc-port 2026-04-03 23:34:00 -04:00
royjr b37f1419d3 Update opendbc_repo 2026-04-03 23:33:19 -04:00
royjr cd85a66790 Merge branch 'master' into ccnc-port 2026-03-26 00:53:07 -04:00
royjr 305ea87daf Update opendbc_repo 2026-03-26 00:52:44 -04:00
royjr 4bbfc793e0 Merge branch 'master' into ccnc-port 2026-03-15 15:44:59 -04:00
royjr d5d983676e Update opendbc_repo 2026-03-13 16:41:05 -04:00
royjr de8a96a398 Merge branch 'master' into ccnc-port 2026-03-13 16:41:00 -04:00
royjr 0cbf45f699 Merge branch 'master' into ccnc-port 2026-03-11 23:28:59 -04:00
royjr 0d68a3a2ab Merge branch 'master' into ccnc-port 2026-03-09 19:58:32 -04:00
royjr 9e85a85059 Update opendbc_repo 2026-03-09 19:58:18 -04:00
royjr 0373c327c0 Update opendbc_repo 2026-03-02 10:10:38 -05:00
royjr efe9e5c200 Update opendbc_repo 2026-03-02 02:15:05 -05:00
royjr 8a249a45dc Update opendbc_repo 2026-03-02 02:06:16 -05:00
royjr bdbefe67f6 Update opendbc_repo 2026-03-02 01:40:31 -05:00
royjr 675bb166ad Merge branch 'master' into ccnc-port 2026-03-01 17:18:11 -05:00
royjr 1b717a7e88 Merge branch 'master' into ccnc-port 2026-03-01 13:23:30 -05:00
royjr 86f55a8ba9 Update opendbc_repo 2026-03-01 13:23:24 -05:00
royjr 629392d2f7 Update opendbc_repo 2026-02-27 16:31:59 -05:00
royjr bc414bdc8b Update opendbc_repo 2026-02-26 23:53:26 -05:00
royjr 7ca5649f2c Merge branch 'master' into ccnc-port 2026-02-26 23:52:46 -05:00
royjr 641ee8fa87 Update opendbc_repo 2026-02-26 23:52:27 -05:00
royjr 56c276158c Merge branch 'master' into ccnc-port 2026-02-24 14:12:12 -05:00
royjr c65308a8bd Update opendbc_repo 2026-02-24 14:12:01 -05:00
royjr 994e526460 Merge branch 'master' into ccnc-port 2026-02-18 12:06:05 -05:00
royjr 1defae36b7 Update opendbc_repo 2026-02-18 12:05:53 -05:00
royjr 40dc4e1591 Merge branch 'master' into hyundai-custom-button 2026-02-16 22:43:14 -05:00
royjr cce4810904 Update opendbc_repo 2026-02-16 22:43:09 -05:00
royjr 5b58f9c1f5 Update opendbc_repo 2026-02-13 23:34:16 -05:00
royjr 815ac7732b Merge branch 'master' into hyundai-custom-button 2026-02-13 23:02:12 -05:00
royjr 2321f9d8f5 Update opendbc_repo 2026-02-13 23:02:06 -05:00
royjr 8f029fd0ef Merge branch 'master' into ccnc-port 2026-02-13 23:01:41 -05:00
royjr ddb46284dc Update opendbc_repo 2026-02-13 23:01:16 -05:00
royjr 9effc754d9 Merge branch 'master' into ccnc-port 2026-02-06 01:16:06 -05:00
royjr e49ffc2a2d Update opendbc_repo 2026-02-06 01:15:59 -05:00
royjr b9020e0003 Update params_metadata.json 2026-02-04 22:07:48 -05:00
royjr ceb466578a Update params_metadata.json 2026-02-04 22:07:19 -05:00
royjr e1cfea4924 Update RELEASES.md 2026-02-04 22:04:29 -05:00
royjr dd8a2bf83a Merge branch 'master' into hyundai-custom-button 2026-02-04 21:45:41 -05:00
royjr ba36a28f2a Update opendbc_repo 2026-02-04 21:43:53 -05:00
royjr cf2010389a Revert "Update opendbc_repo"
This reverts commit 43522c4922.
2026-02-04 19:14:43 -05:00
royjr 43522c4922 Update opendbc_repo 2026-02-04 19:14:28 -05:00
royjr 2cacd0b3e5 Merge branch 'master' into ccnc-port 2026-01-24 12:50:18 -05:00
royjr c4b8859dff Update opendbc_repo 2026-01-24 12:50:11 -05:00
royjr 8fb0953205 Merge branch 'master' into ccnc-port 2026-01-11 22:17:38 -05:00
royjr 63d1c8835f Merge branch 'master' into ccnc-port 2026-01-10 13:03:48 -05:00
royjr 17a185606d Merge branch 'master' into ccnc-port 2026-01-09 16:40:32 -05:00
royjr da10131392 Merge branch 'master' into ccnc-port 2025-12-28 17:18:51 -05:00
royjr 7107c2ba14 Merge branch 'master' into ccnc-port 2025-12-23 12:13:48 -05:00
royjr 95b6e877ac Update opendbc_repo 2025-12-23 12:13:29 -05:00
royjr eb02c6570e Update opendbc_repo 2025-12-21 15:43:04 -05:00
royjr 1be8ae31c4 Merge branch 'master' into ccnc-port 2025-12-19 01:04:42 -05:00
royjr 04dcd38856 Update opendbc_repo 2025-12-19 01:04:30 -05:00
royjr 22ccf0d72f Merge branch 'master' into ccnc-port 2025-12-15 17:02:50 -05:00
royjr 3c969bb627 Merge branch 'master' into ccnc-port 2025-12-13 23:22:22 -05:00
royjr 20f8011feb Update opendbc_repo 2025-12-13 23:22:11 -05:00
royjr 9cf17e74a1 Merge branch 'master' into ccnc-port 2025-12-12 23:19:56 -05:00
royjr 2c4efdf557 Merge branch 'master' into ccnc-port 2025-12-07 13:29:48 -05:00
royjr 4cd3d3c16c Merge branch 'master' into ccnc-port 2025-12-02 12:56:21 -05:00
royjr 637f3ae9c8 Merge branch 'master' into ccnc-port 2025-12-01 14:41:03 -05:00
royjr 464ee80f71 Merge branch 'master' into ccnc-port 2025-11-26 00:27:53 -05:00
royjr 2743a04613 Merge branch 'master' into ccnc-port 2025-11-24 18:44:13 -05:00
royjr 7f9978d001 Merge branch 'master' into ccnc-port 2025-11-22 00:21:15 -05:00
royjr 4b83961c67 Merge branch 'master' into ccnc-port 2025-11-21 16:23:22 -05:00
royjr c00eaf428a Update opendbc_repo 2025-11-21 16:23:01 -05:00
royjr 0a9993e8d4 Merge branch 'master' into ccnc-port 2025-11-19 16:49:59 -05:00
royjr 0af214a985 Update opendbc_repo 2025-11-19 16:49:51 -05:00
royjr af43385e3a Merge branch 'master' into ccnc-port 2025-11-11 10:19:51 -05:00
royjr 0ab2b8c590 Update opendbc_repo 2025-11-07 19:59:50 -05:00
royjr 67ab18a0de Merge branch 'master' into ccnc-port 2025-11-07 19:23:51 -05:00
royjr e87dc15b30 Update opendbc_repo 2025-11-07 19:23:37 -05:00
royjr 192d08516c Merge branch 'master' into ccnc-port 2025-11-02 19:23:00 -05:00
royjr 3cf001c59c Update opendbc_repo 2025-11-02 19:22:49 -05:00
royjr f2ccd021da Merge branch 'master' into ccnc-port 2025-11-02 14:07:54 -05:00
royjr c9fc900f64 Update opendbc_repo 2025-11-02 14:07:44 -05:00
royjr 3c37c5ce5d Update opendbc_repo 2025-10-30 11:28:49 -04:00
royjr 7c45889e4e Merge branch 'master' into ccnc-port 2025-10-30 11:27:50 -04:00
royjr 2aabb7aee8 Merge branch 'master' into ccnc-port 2025-10-24 14:16:09 -04:00
royjr 3859e9962f Update opendbc_repo 2025-10-24 14:15:51 -04:00
royjr 810efbab72 Merge branch 'master' into ccnc-port 2025-10-18 07:33:11 -04:00
royjr ec27bec326 Update opendbc_repo 2025-10-18 07:32:33 -04:00
royjr 250d553157 Merge branch 'master' into ccnc-port 2025-10-14 21:57:32 -04:00
royjr cea54a0ca8 Update opendbc_repo 2025-10-14 21:57:26 -04:00
royjr 8e72d783bd Update opendbc_repo 2025-10-13 22:41:21 -04:00
royjr 1b0dc103dc Merge branch 'master' into ccnc-port 2025-10-11 23:51:46 -04:00
royjr 6c364d292b Update opendbc_repo 2025-10-11 23:51:31 -04:00
royjr bcdec2ce84 Merge branch 'master' into ccnc-port 2025-10-10 17:29:05 -04:00
royjr 3deaeb3759 Merge branch 'master' into ccnc-port 2025-10-10 15:02:32 -04:00
royjr c669f0984a Update opendbc_repo 2025-10-10 15:02:17 -04:00
royjr 1cb5f24439 Merge branch 'master' into hyundai-custom-button 2025-10-08 21:37:43 -04:00
royjr b6a3f21a52 Merge branch 'master' into hyundai-custom-button 2025-10-08 18:27:27 -04:00
royjr 2ecc26b623 Update opendbc_repo 2025-10-08 18:27:19 -04:00
royjr 145f75fe06 Merge branch 'master' into hyundai-custom-button 2025-10-08 17:57:50 -04:00
royjr bd0578cbf9 Update opendbc_repo 2025-10-08 17:17:28 -04:00
royjr 46dd946740 Merge branch 'master' into ccnc-port 2025-10-07 01:36:06 -04:00
royjr 9da4b3653e Update opendbc_repo 2025-10-07 01:35:58 -04:00
royjr 4e21ae7c50 Update opendbc_repo 2025-10-05 06:21:56 -04:00
royjr bb91e92237 Update opendbc_repo 2025-10-05 06:01:15 -04:00
royjr 14b4c4f85b Update opendbc_repo 2025-10-02 09:20:32 -04:00
royjr 0660b542c3 Merge branch 'master' into ccnc-port 2025-10-01 16:01:32 -04:00
royjr 2b893b90c9 Update opendbc_repo 2025-10-01 16:01:26 -04:00
royjr f5139178ed Merge branch 'master' into ccnc-port 2025-09-30 14:39:57 -04:00
royjr fb43b755f2 Update opendbc_repo 2025-09-30 14:39:50 -04:00
royjr 07f5b967d8 Merge branch 'master' into ccnc-port 2025-09-24 21:30:29 -04:00
royjr ea19c7d3bb Update opendbc_repo 2025-09-24 21:30:17 -04:00
royjr e461842cbb Merge branch 'master' into ccnc-port 2025-09-23 05:55:02 -04:00
royjr a73c9659d5 Update opendbc_repo 2025-09-23 05:54:50 -04:00
royjr cb796fbc76 Merge branch 'master' into ccnc-port 2025-09-18 20:10:43 -04:00
royjr 6bf75fc557 Update opendbc_repo 2025-09-18 20:10:37 -04:00
royjr 9a1fc28819 Merge branch 'master' into ccnc-port 2025-09-18 13:54:36 -04:00
royjr 0741d05e92 Update opendbc_repo 2025-09-18 13:54:23 -04:00
royjr 1ad008107d Merge branch 'master' into ccnc-port 2025-09-15 01:40:27 -04:00
royjr feebd9df93 Reapply "UI: Developer UI (#1233)"
This reverts commit 15e5d2efb9.
2025-09-15 01:40:21 -04:00
royjr c2e5ced3e5 Update opendbc_repo 2025-09-15 01:39:51 -04:00
royjr 15e5d2efb9 Revert "UI: Developer UI (#1233)"
This reverts commit 1bb4ca2547.
2025-09-12 02:10:29 -04:00
royjr a3929d0b54 Merge branch 'master' into ccnc-port 2025-09-12 01:40:29 -04:00
royjr 794f8f9991 Update opendbc_repo 2025-09-08 09:27:31 -04:00
royjr 68fa5e3f21 Merge branch 'master' into ccnc-port 2025-09-07 13:13:37 -04:00
royjr 86c6cc1f48 Merge branch 'master' into ccnc-port 2025-09-03 22:22:12 -04:00
royjr eb7ffbf093 Update opendbc_repo 2025-09-03 10:14:58 -04:00
royjr 40f24cc0b6 Update opendbc_repo 2025-09-03 10:08:01 -04:00
royjr 3919095752 Update opendbc_repo 2025-09-03 10:05:35 -04:00
royjr 754efac063 Merge branch 'master' into hyundai-custom-button 2025-09-03 09:57:35 -04:00
royjr 132cc156f4 Update opendbc_repo 2025-09-03 09:57:27 -04:00
royjr 74d63be1c3 Merge branch 'master' into ccnc-port 2025-09-03 09:50:55 -04:00
royjr 8894486a1a Update opendbc_repo 2025-09-03 09:50:49 -04:00
royjr 810599315d Merge branch 'master' into ccnc-port 2025-08-31 16:53:30 -04:00
royjr 6f3ab810c8 Update opendbc_repo 2025-08-31 16:53:24 -04:00
royjr 230f78b8d3 Merge branch 'master' into ccnc-port 2025-08-26 12:14:46 -04:00
royjr f1affec088 Update opendbc_repo 2025-08-26 12:14:22 -04:00
nayan 95ad932efb disable lkas per upstream & fix mixed events 2025-08-25 09:15:20 -04:00
nayan ae30f4119f Merge remote-tracking branch 'origin/master' into hyundai-custom-button
# Conflicts:
#	RELEASES.md
#	opendbc_repo
#	selfdrive/ui/feedback/feedbackd.py
2025-08-25 09:04:57 -04:00
nayan d251ae4976 pr be messing with pr 2025-08-24 16:40:48 -04:00
royjr 97d8ef242c Merge branch 'master' into ccnc-port 2025-08-24 15:12:57 -04:00
royjr a63fff9b45 Update opendbc_repo 2025-08-24 15:12:46 -04:00
nayan 33324e590b use altButton2 event 2025-08-23 22:24:39 -04:00
nayan fea10ffd22 Merge remote-tracking branch 'origin/master' into hyundai-custom-button 2025-08-23 22:00:59 -04:00
nayan f1e11e3f06 bump opendbc 2025-08-23 22:00:50 -04:00
royjr cb3893daaa Merge branch 'master' into ccnc-port 2025-08-23 10:33:24 -04:00
royjr 29f60df74b Merge branch 'master' into ccnc-port 2025-08-22 11:18:27 -04:00
royjr c6c072e1f4 Update opendbc_repo 2025-08-22 11:18:18 -04:00
royjr c562eca8a1 Merge branch 'master' into hyundai-custom-button 2025-08-22 10:30:38 -04:00
royjr d9a690dac3 Update opendbc_repo 2025-08-22 10:30:28 -04:00
nayan a0eed058d1 bump opendbc 2025-08-13 17:28:11 -04:00
nayan abc9f47a6f Merge remote-tracking branch 'origin/master' into hyundai-custom-button
# Conflicts:
#	opendbc_repo
2025-08-13 17:27:19 -04:00
royjr d101cbb83e Update opendbc_repo 2025-08-13 16:00:04 -04:00
royjr 1536d59633 Update opendbc_repo 2025-08-13 15:28:37 -04:00
royjr dc99b865ae Merge branch 'master' into ccnc-port 2025-08-13 12:31:14 -04:00
royjr e59bc027ff Merge branch 'master' into ccnc-port 2025-08-13 11:58:05 -04:00
royjr cf7e5efaca Update opendbc_repo 2025-08-13 11:57:57 -04:00
Nayan b064f6fcaf Merge branch 'master' into hyundai-custom-button 2025-08-12 14:41:16 -04:00
nayan 0c7d7df2ec keep both 2025-08-12 14:40:11 -04:00
nayan dc86f35957 refactor 2025-08-12 13:29:06 -04:00
nayan 9685b0aa9b lint 2025-08-12 12:49:17 -04:00
nayan f23cc408e3 not needed anymore 2025-08-12 12:48:49 -04:00
nayan cea00a6c14 todo 2025-08-12 12:00:53 -04:00
nayan f2949e1dc2 use custom button in feedbackd 2025-08-10 11:49:35 -04:00
royjr 5f1f34fa7f Update opendbc_repo 2025-08-10 09:31:11 -04:00
royjr 2c041f9025 Merge branch 'master' into hyundai-custom-button 2025-08-10 09:28:05 -04:00
royjr 4b44f2eb31 Merge branch 'master' into ccnc-port 2025-08-10 09:18:24 -04:00
royjr 107d2ab400 Update opendbc_repo 2025-08-10 09:18:15 -04:00
nayan 74b2d519b9 move custom button to cs_sp, add bookmark functionality 2025-08-05 15:52:46 -04:00
royjr 5432d9062c Merge branch 'master' into ccnc-port 2025-08-04 11:52:29 -04:00
royjr f533f6c843 Merge branch 'master' into ccnc-port 2025-08-02 06:53:54 -04:00
royjr 58e9ac763c Update opendbc_repo 2025-08-02 06:53:43 -04:00
royjr cb50d54169 Merge branch 'master-new' into ccnc-port 2025-07-24 20:23:46 -04:00
royjr bd5de4ed0a Merge branch 'master-new' into ccnc-port 2025-07-20 23:49:36 -04:00
royjr 0d4073fadb Merge branch 'master-new' into ccnc-port 2025-07-19 23:18:26 -04:00
royjr ebc70dcb52 Merge branch 'master-new' into ccnc-port 2025-07-19 14:37:17 -04:00
royjr 4d0426999e Update opendbc_repo 2025-07-19 14:36:59 -04:00
royjr 286da42573 Merge branch 'master-new' into ccnc-port 2025-07-16 23:48:23 -04:00
royjr 8a836710a9 Update opendbc_repo 2025-07-16 23:48:06 -04:00
royjr 5d515bcf33 Merge branch 'master-new' into ccnc-port 2025-07-07 05:35:55 -04:00
royjr 1c7f6d5133 Update opendbc_repo 2025-07-03 21:24:01 -04:00
royjr 05d57c7aeb Update opendbc_repo 2025-07-01 18:30:32 -04:00
royjr e4b0eaf352 Update opendbc_repo 2025-06-28 19:30:28 -04:00
royjr a710276472 Merge branch 'master-new' into ccnc-port 2025-06-28 19:22:59 -04:00
royjr af086db671 Merge branch 'master-new' into ccnc-port 2025-06-28 12:13:26 -04:00
royjr 0d9eb0e25e Update opendbc_repo submodule to latest commit
Advanced the opendbc_repo submodule to commit d309f7ec96e37267c94d12fc4bfe2672ad505b06. This pulls in the latest changes from the opendbc repository.
2025-06-26 14:03:56 -04:00
royjr 0616caed6d Merge branch 'master-new' into ccnc-port 2025-06-25 19:33:53 -04:00
royjr 095337b3c1 Update opendbc_repo 2025-06-25 19:33:38 -04:00
royjr 1edec2d22c Update opendbc_repo 2025-06-14 16:14:02 -04:00
royjr affabb9ee0 Update opendbc_repo 2025-06-14 15:55:07 -04:00
royjr dc27e8711c Update opendbc_repo 2025-06-14 14:54:32 -04:00
royjr cf7329a264 Merge branch 'master-new' into ccnc-port 2025-06-11 21:36:06 -04:00
royjr 5ee5ecd820 Merge branch 'master-new' into ccnc-port 2025-06-08 23:25:13 -04:00
royjr b064f730dd Update opendbc_repo 2025-06-08 17:54:43 -04:00
33 changed files with 1461 additions and 600 deletions
+79 -1
View File
@@ -33,7 +33,7 @@ jobs:
- name: LFS Fetch
run: |
git lfs fetch
git lfs fetch || echo "::warning::some objects are missing on commaai/openpilot-lfs; mirroring the rest"
- name: Set up Git
run: |
@@ -68,6 +68,7 @@ jobs:
git config lfs.activitytimeout 300
git config lfs.transfer.maxretries 5
git config lfs.concurrenttransfers 4
git config lfs.allowincompletepush true
- name: Push LFS
id: sync-and-commit
@@ -81,3 +82,80 @@ jobs:
done
echo "All push attempts failed"
exit 1
- name: Checkout sunnypilot
uses: actions/checkout@v4
with:
repository: 'sunnypilot/sunnypilot'
path: sunnypilot
fetch-depth: 0
filter: blob:limit=1k
lfs: false
- name: Reference objects on sunnypilot-lfs
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: |
set -euo pipefail
GIT_LFS_SKIP_SMUDGE=1 git clone --quiet https://huggingface.co/sunnypilot/sunnypilot-lfs hf-lfs
mkdir -p hf-lfs/all
cat > scan_pointers.py <<'EOF'
import re, subprocess, sys, threading
repo = sys.argv[1]
chk = subprocess.run(["git", "-C", repo, "cat-file", "--batch-all-objects",
"--batch-check=%(objectname) %(objecttype) %(objectsize)"],
capture_output=True, text=True, check=True).stdout
small = [f[0] for f in (l.split() for l in chk.splitlines()) if f[1] == "blob" and int(f[2]) < 1024]
p = subprocess.Popen(["git", "-C", repo, "cat-file", "--batch=%(objectsize)"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
def feed():
for sha in small:
p.stdin.write(sha.encode() + b"\n")
p.stdin.close()
threading.Thread(target=feed, daemon=True).start()
rx = re.compile(rb"^version https://git-lfs\.github\.com/spec/v1\noid sha256:([0-9a-f]{64})\nsize ([0-9]+)\n")
seen = {}
for _ in small:
n = int(p.stdout.readline()); body = p.stdout.read(n); p.stdout.read(1)
m = rx.match(body)
if m:
seen.setdefault(m.group(1).decode(), m.group(2).decode())
for oid, size in sorted(seen.items()):
print(oid, size)
EOF
for repo in . sunnypilot; do
python3 scan_pointers.py "$repo"
done | sort -u | while read -r oid size; do
[ -e "hf-lfs/all/$oid" ] && continue
printf 'version https://git-lfs.github.com/spec/v1\noid sha256:%s\nsize %s\n' "$oid" "$size" > "hf-lfs/all/$oid"
echo "$oid"
done > new_oids.txt
echo "new pointer files: $(wc -l < new_oids.txt)"
dropped=0
while mapfile -t -n 100 chunk && ((${#chunk[@]})); do
body=$(for oid in "${chunk[@]}"; do
size=$(awk '/^size /{print $2}' "hf-lfs/all/$oid")
printf '{"oid":"%s","size":%s}\n' "$oid" "$size"
done | jq -s '{operation:"download",transfers:["basic"],objects:.}')
for attempt in 1 2 3 4 5; do
resp=$(curl -sS -u "user:${HF_TOKEN}" -w '\n%{http_code}' \
-H 'Accept: application/vnd.git-lfs+json' -H 'Content-Type: application/vnd.git-lfs+json' \
--data "$body" "${LFS_URL}/objects/batch") && [ "$(tail -n1 <<<"$resp")" = 200 ] && break
echo "batch check attempt $attempt failed ($(tail -n1 <<<"$resp")), retrying in 60s"; sleep 60
done
[ "$(tail -n1 <<<"$resp")" = 200 ] || { echo "batch check failed"; exit 1; }
for oid in $(sed '$d' <<<"$resp" | jq -r '.objects[] | select(.error) | .oid'); do
rm -f "hf-lfs/all/$oid"; dropped=$((dropped+1))
done
done < new_oids.txt
echo "dropped (not on HF): $dropped"
cd hf-lfs
git add -A all
if git diff --cached --quiet; then
echo "nothing new to reference"
exit 0
fi
git commit -q -m "Reference $(git diff --cached --name-only | wc -l | tr -d ' ') new LFS objects"
git push origin HEAD:main
+53
View File
@@ -3,9 +3,62 @@ on:
pull_request:
paths:
- 'tinygrad_repo'
- 'openpilot/sunnypilot/modeld_v2/**'
workflow_dispatch:
env:
CI: 1
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: lfs.fetchexclude
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
jobs:
plain-artifact:
name: Plain artifact round trip and pickle converter
runs-on: ubuntu-24.04
env:
PYTHONPATH: ${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}
TINYGRAD_TESTS: 1
steps:
- uses: actions/checkout@v7
- run: ./tools/op.sh setup
- name: Build openpilot
run: scons -j$(nproc)
- name: Compile, load, cache and fuse a model with the pinned tinygrad
env:
DEV: CPU
run: tools/op.sh test -v openpilot/sunnypilot/modeld_v2/tests/test_plain_artifact.py
- name: Install LLVM 20 and ROCm comgr for the amd mock, as tinygrad_repo/.github/actions/setup-tinygrad does
run: |
. /etc/os-release
curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc > /dev/null
echo "deb http://apt.llvm.org/$VERSION_CODENAME/ llvm-toolchain-$VERSION_CODENAME-20 main" | sudo tee /etc/apt/sources.list.d/llvm.list
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.radeon.com/rocm/rocm.gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/rocm.gpg > /dev/null
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/rocm/apt/7.1 $VERSION_CODENAME main" | sudo tee /etc/apt/sources.list.d/rocm.list
printf 'Package: *\nPin: release o=repo.radeon.com\nPin-Priority: 600\n' | sudo tee /etc/apt/preferences.d/rocm-pin-600
sudo apt-get -qq update && sudo apt-get -qq install -y --no-install-recommends libllvm20 clang-20 lld-20 comgr
printf '/opt/rocm/lib\n/opt/rocm/lib64\n' | sudo tee /etc/ld.so.conf.d/rocm.conf && sudo ldconfig
echo "/usr/lib/llvm-20/bin" >> "$GITHUB_PATH"
- name: Convert a pickled USB+AMD jit and replay it under tinygrad's amd mock
env:
DEV: MOCKUSB+AMD
CHECK_OOB: 0
GMMU: 0
run: tools/op.sh test -v openpilot/sunnypilot/modeld_v2/tests/test_convert_upstream_pkl.py
- name: Where tinygrad moved, for the helper or failure point named above
if: failure() && github.event_name == 'pull_request'
run: |
git fetch -q --depth=1 origin "${{ github.base_ref }}"
old=$(git rev-parse "origin/${{ github.base_ref }}:tinygrad_repo") new=$(git rev-parse HEAD:tinygrad_repo)
git -C tinygrad_repo fetch -q origin "$old" || true
echo "tinygrad_repo $old -> $new, in what helpers.py and plain_artifact_compiler.py read:"
git -C tinygrad_repo diff "$old" "$new" -- tinygrad/uop/ops.py tinygrad/engine/jit.py tinygrad/engine/realize.py tinygrad/device.py \
tinygrad/helpers.py tinygrad/dtype.py tinygrad/renderer/__init__.py
echo "and in what convert_upstream_pkl.py decodes:"
git -C tinygrad_repo diff --stat "$old" "$new" -- tinygrad/runtime/autogen/am/pm4_nv.py tinygrad/runtime/autogen/amd_gpu.py
git -C tinygrad_repo diff "$old" "$new" -- tinygrad/runtime/support/hcq2.py tinygrad/runtime/support/usb.py tinygrad/runtime/ops_amd.py
generate-matrix:
runs-on: ubuntu-latest
outputs:
+30
View File
@@ -50,6 +50,12 @@ jobs:
timeout-minutes: 3
run: TARGET_DIR=$STRIPPED_DIR tools/release/build_stripped.sh
- run: ./tools/op.sh setup
- name: Restore scons cache
uses: actions/cache@v6
with:
path: /tmp/scons_cache
key: scons-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
restore-keys: scons-${{ runner.os }}-${{ github.job }}-
- name: Build openpilot and run checks
timeout-minutes: 30
working-directory: ${{ env.STRIPPED_DIR }}
@@ -89,6 +95,12 @@ jobs:
FILTERED=$(echo "$PATH" | tr ':' '\n' | grep -v '/opt/homebrew' | tr '\n' ':')
echo "PATH=${FILTERED}/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" >> $GITHUB_ENV
- run: ./tools/op.sh setup
- name: Restore scons cache
uses: actions/cache@v6
with:
path: /tmp/scons_cache
key: scons-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
restore-keys: scons-${{ runner.os }}-${{ github.job }}-
- name: Building openpilot
run: scons
@@ -121,6 +133,12 @@ jobs:
- name: Install raylib headless dependencies
if: ${{ !contains(runner.name, 'nsc') }}
run: sudo apt-get install -y --no-install-recommends libgles2 libegl1
- name: Restore scons cache
uses: actions/cache@v6
with:
path: /tmp/scons_cache
key: scons-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
restore-keys: scons-${{ runner.os }}-${{ github.job }}-
- name: Build openpilot
run: scons
- name: Run unit tests
@@ -140,6 +158,12 @@ jobs:
steps:
- uses: actions/checkout@v7
- run: ./tools/op.sh setup
- name: Restore scons cache
uses: actions/cache@v6
with:
path: /tmp/scons_cache
key: scons-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
restore-keys: scons-${{ runner.os }}-${{ github.job }}-
- name: Build openpilot
run: scons
- name: Run replay
@@ -228,6 +252,12 @@ jobs:
- name: Install raylib headless dependencies
if: ${{ !contains(runner.name, 'nsc') }}
run: sudo apt-get install -y --no-install-recommends libgles2 libegl1
- name: Restore scons cache
uses: actions/cache@v6
with:
path: /tmp/scons_cache
key: scons-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
restore-keys: scons-${{ runner.os }}-${{ github.job }}-
- name: Build openpilot
run: scons
- name: Create UI Report
+3
View File
@@ -138,6 +138,9 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
status @0 :DownloadStatus;
progress @1 :Float32;
eta @2 :UInt32;
speed @3 :Float32;
canceling @4 :Bool;
resuming @5 :Bool;
}
struct Chunk {
+2
View File
@@ -200,6 +200,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
{"CustomButtonAction", {PERSISTENT | BACKUP, INT, "0"}},
// MADS params
{"Mads", {PERSISTENT | BACKUP, BOOL, "1"}},
@@ -246,6 +247,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}},
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaTSS2Smooth", {PERSISTENT | BACKUP, BOOL, "0"}},
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -64,16 +64,19 @@ class DesireHelper:
((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or
(carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right))
blindspot_detected = (((carstate.leftBlindspot or left_edge_detected) and self.lane_change_direction == LaneChangeDirection.left) or
((carstate.rightBlindspot or right_edge_detected) and self.lane_change_direction == LaneChangeDirection.right))
blindspot_detected = ((carstate.leftBlindspot and self.lane_change_direction == LaneChangeDirection.left) or
(carstate.rightBlindspot and self.lane_change_direction == LaneChangeDirection.right))
self.alc.update_lane_change(blindspot_detected, carstate.brakePressed)
edge_detected = ((left_edge_detected and self.lane_change_direction == LaneChangeDirection.left) or
(right_edge_detected and self.lane_change_direction == LaneChangeDirection.right))
self.alc.update_lane_change(blindspot_detected, edge_detected, carstate.brakePressed, torque_applied)
if not one_blinker or below_lane_change_speed:
self.lane_change_state = LaneChangeState.off
self.lane_change_direction = LaneChangeDirection.none
self.lane_change_timer = 0.0
elif (torque_applied or self.alc.auto_lane_change_allowed) and not blindspot_detected:
elif (torque_applied or self.alc.auto_lane_change_allowed) and not self.alc.lane_change_blocked:
self.lane_change_state = LaneChangeState.laneChangeStarting
self.lane_change_timer = 0.0
+19
View File
@@ -3,6 +3,7 @@ from enum import IntEnum
import openpilot.cereal.messaging as messaging
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.sunnypilot.custom_button import CustomButtonAction, handle_custom_button
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
from openpilot.selfdrive.ui.layouts.home import HomeLayout
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType
@@ -27,6 +28,7 @@ class MainLayout(Widget):
super().__init__()
self._pm = messaging.PubMaster(['bookmarkButton', 'userBookmark'])
self._custom_button_sock = messaging.sub_sock('carState')
self._sidebar = Sidebar()
self._current_mode = MainState.HOME
@@ -40,6 +42,10 @@ class MainLayout(Widget):
MainState.SETTINGS: SettingsLayout(),
MainState.ONROAD: AugmentedRoadView(),
}
self._custom_button_callbacks = {
CustomButtonAction.BOOKMARK: self._on_bookmark_clicked,
CustomButtonAction.CYCLE_UI: self._cycle_ui,
}
self._sidebar_rect = rl.Rectangle(0, 0, 0, 0)
self._content_rect = rl.Rectangle(0, 0, 0, 0)
@@ -55,6 +61,7 @@ class MainLayout(Widget):
gui_app.push_widget(self._onboarding_window)
def _render(self, _):
handle_custom_button(messaging.drain_sock(self._custom_button_sock), ui_state.params, self._custom_button_callbacks)
self._handle_onroad_transition()
self._render_main_content()
@@ -113,6 +120,18 @@ class MainLayout(Widget):
def _on_settings_clicked(self):
self.open_settings(PanelType.DEVICE)
def _show_onroad(self):
self._set_current_layout(MainState.ONROAD)
self._sidebar.set_visible(False)
def _cycle_ui(self):
if self._current_mode == MainState.ONROAD and not self._sidebar.is_visible:
self._sidebar.set_visible(True)
elif self._current_mode == MainState.SETTINGS:
self._show_onroad()
else:
self._on_settings_clicked()
def _on_bookmark_clicked(self):
for service in ('bookmarkButton', 'userBookmark'):
msg = messaging.new_message(service, valid=True)
@@ -4,6 +4,7 @@ from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout
from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
from openpilot.selfdrive.ui.sunnypilot.custom_button import CustomButtonAction, handle_custom_button
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.selfdrive.ui.mici.layouts.onboarding import OnboardingWindow
from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout
@@ -23,6 +24,7 @@ class MiciMainLayout(Scroller):
super().__init__(snap_items=True, spacing=0, pad=0, scroll_indicator=False, edge_shadows=False)
self._pm = messaging.PubMaster(['bookmarkButton', 'userBookmark'])
self._custom_button_sock = messaging.sub_sock('carState')
self._prev_onroad = False
self._prev_standstill = False
@@ -35,6 +37,10 @@ class MiciMainLayout(Scroller):
self._settings_layout = SettingsLayout()
self._car_onroad_layout = AugmentedRoadView(bookmark_callback=self._on_bookmark_clicked)
self._body_onroad_layout = BodyLayout()
self._custom_button_callbacks = {
CustomButtonAction.BOOKMARK: self._on_bookmark_clicked,
CustomButtonAction.CYCLE_UI: self._cycle_ui,
}
# Initialize widget rects
for widget in (self._home_layout, self._alerts_layout, self._settings_layout,
@@ -97,6 +103,8 @@ class MiciMainLayout(Scroller):
self._alerts_layout._update_state()
def _render(self, _):
handle_custom_button(messaging.drain_sock(self._custom_button_sock), ui_state.params, self._custom_button_callbacks)
if not self._setup:
if self._alerts_layout.active_alerts() > 0:
self._scroller.scroll_to(self._alerts_layout.rect.x)
@@ -152,6 +160,23 @@ class MiciMainLayout(Scroller):
msg = messaging.new_message(service, valid=True)
self._pm.send(service, msg)
def _show_layout(self, layout: Widget):
if gui_app.widget_in_stack(self._onboarding_window):
return
gui_app.pop_widgets_to(self, lambda: self._scroll_to(layout))
def _layout_visible(self, layout: Widget) -> bool:
return abs(layout.rect.x - self._rect.x) < self._rect.width / 2
def _cycle_ui(self):
if gui_app.widget_in_stack(self._settings_layout):
self._show_layout(self._onroad_layout)
elif gui_app.get_active_widget() is self and self._layout_visible(self._home_layout):
if not gui_app.widget_in_stack(self._onboarding_window):
gui_app.push_widget(self._settings_layout)
else:
self._show_layout(self._home_layout)
def _on_body_changed(self):
self._car_onroad_layout.set_visible(not ui_state.is_body)
self._body_onroad_layout.set_visible(bool(ui_state.is_body))
@@ -0,0 +1,19 @@
from enum import IntEnum
from opendbc.car.structs import car
class CustomButtonAction(IntEnum):
NONE = 0
BOOKMARK = 1
CYCLE_UI = 3
def handle_custom_button(messages, params, callbacks):
for msg in messages:
custom_pressed = any(be.type == car.CarState.ButtonEvent.Type.altButton2 and be.pressed
for be in msg.carState.buttonEvents)
if custom_pressed:
action = CustomButtonAction(params.get('CustomButtonAction', return_default=True))
if callback := callbacks.get(action):
callback()
@@ -235,13 +235,17 @@ class ModelsLayout(Widget):
progress = sum(p.progress for p in progresses) / len(progresses)
ds = custom.ModelManagerSP.DownloadStatus
if any(p.canceling for p in progresses):
return {"name": name, "downloading": True, "progress": progress, "status_text": tr("canceling...")}
if ds.failed in statuses:
# close.png is authored black and a tint cannot lift it, hence close2
return {"name": name, "status_text": tr("download failed"), "text_color": rl.RED, "icon": "icons/close2.png"}
if ds.verifying in statuses:
return {"name": name, "downloading": True, "progress": progress, "status_text": tr("verifying")}
if ds.downloading in statuses:
return {"name": name, "downloading": True, "progress": progress}
speed = max((p.speed for p in progresses), default=0.0)
status_text = f"{speed / 1024 ** 2:.1f} MB/s" if speed > 0 else "--.- MB/s"
return {"name": name, "downloading": True, "progress": progress, "status_text": status_text}
if statuses <= {ds.downloaded, ds.cached}:
return {"name": name, "text_color": ON_COLOR, "icon": "icons/checkmark.png"}
# circled_slash is authored grey; tinting it again only darkens it
@@ -11,10 +11,12 @@ from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
from opendbc.car.toyota.values import ToyotaFlags
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
SNG_HACK_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control must be available and enabled for your vehicle to use this feature.")
PLATFORM_UNAVAILABLE = tr_noop("This feature is currently not available on this platform.")
DESCRIPTIONS = {
'enforce_stock_longitudinal': tr_noop(
@@ -23,7 +25,11 @@ DESCRIPTIONS = {
'stop_and_go_hack': tr_noop(
'sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. ' +
'This feature is only applicable to certain models that are able to use longitudinal control. This is an alpha feature. Use at your own risk.'
)
),
'tss2_smooth': tr_noop(
'Enable smoother longitudinal control for TSS2 vehicles. ' +
'Uses a custom speed-dependent integral gain curve for smoother traffic driving.'
),
}
@@ -47,9 +53,18 @@ class ToyotaSettings(BrandSettings):
enabled=lambda: not ui_state.engaged,
)
self.tss2_smooth = toggle_item_sp(
lambda: tr("TSS2 Smooth Longitudinal (Alpha)"),
description=lambda: tr(DESCRIPTIONS["tss2_smooth"]),
initial_state=ui_state.params.get_bool("ToyotaTSS2Smooth"),
callback=self._on_enable_tss2_smooth,
enabled=lambda: not ui_state.engaged,
)
self.items = [
self.enforce_stock_longitudinal,
self.stop_and_go_hack,
self.tss2_smooth,
]
def _on_enable_enforce_stock_longitudinal(self, state: bool):
@@ -61,6 +76,8 @@ class ToyotaSettings(BrandSettings):
ui_state.params.put_bool("AlphaLongitudinalEnabled", False)
ui_state.params.put_bool("ToyotaStopAndGoHack", False)
self.stop_and_go_hack.action_item.set_state(False)
ui_state.params.put_bool("ToyotaTSS2Smooth", False)
self.tss2_smooth.action_item.set_state(False)
ui_state.params.put_bool("OnroadCycleRequested", True)
else:
self.enforce_stock_longitudinal.action_item.set_state(False)
@@ -75,6 +92,25 @@ class ToyotaSettings(BrandSettings):
ui_state.params.put_bool("ToyotaEnforceStockLongitudinal", False)
ui_state.params.put_bool("OnroadCycleRequested", True)
def _on_enable_tss2_smooth(self, state: bool):
if state:
def confirm_callback(result: int):
if result == DialogResult.CONFIRM:
ui_state.params.put_bool("ToyotaTSS2Smooth", True)
ui_state.params.put_bool("OnroadCycleRequested", True)
else:
self.tss2_smooth.action_item.set_state(False)
content = (f"<h1>{self.tss2_smooth.title}</h1><br>" +
f"<p>{self.tss2_smooth.description}</p>")
dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback)
gui_app.push_widget(dlg)
else:
ui_state.params.put_bool("ToyotaTSS2Smooth", False)
ui_state.params.put_bool("OnroadCycleRequested", True)
def _on_enable_stop_and_go_hack(self, state: bool):
if state:
def confirm_callback(result: int):
@@ -113,9 +149,32 @@ class ToyotaSettings(BrandSettings):
self.stop_and_go_hack.set_description(new_desc)
if show_desc:
self.stop_and_go_hack.show_description(True)
tss2 = bool(ui_state.CP.flags & ToyotaFlags.TSS2)
if longitudinal and not enforce_stock and tss2:
self.tss2_smooth.action_item.set_enabled(not ui_state.engaged)
new_desc = tr(DESCRIPTIONS["tss2_smooth"])
show_desc = False
else:
self.tss2_smooth.action_item.set_enabled(False)
self.tss2_smooth.action_item.set_state(False)
unavailable = SNG_HACK_UNAVAILABLE if tss2 else PLATFORM_UNAVAILABLE
new_desc = "<b>" + tr(unavailable) + "</b>\n\n" + tr(DESCRIPTIONS["tss2_smooth"])
show_desc = True
if self.tss2_smooth.description != new_desc:
self.tss2_smooth.set_description(new_desc)
if show_desc:
self.tss2_smooth.show_description(True)
else:
self.stop_and_go_hack.action_item.set_enabled(False)
new_desc = "<b>" + tr(ONROAD_ONLY_DESCRIPTION) + "</b>\n\n" + tr(DESCRIPTIONS["stop_and_go_hack"])
if self.stop_and_go_hack.description != new_desc:
self.stop_and_go_hack.set_description(new_desc)
self.stop_and_go_hack.show_description(True)
self.tss2_smooth.action_item.set_enabled(False)
new_desc = "<b>" + tr(ONROAD_ONLY_DESCRIPTION) + "</b>\n\n" + tr(DESCRIPTIONS["tss2_smooth"])
if self.tss2_smooth.description != new_desc:
self.tss2_smooth.set_description(new_desc)
self.tss2_smooth.show_description(True)
@@ -251,11 +251,13 @@ class ModelsLayoutMici(NavScroller):
self.current_model_info.info_text.set_text(tr("download failed"))
elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading:
self.cancel_download_btn.set_visible(True)
self.cancel_download_btn.set_visible(ui_state.params.get("ModelManager_DownloadRef") is not None)
device.set_override_interactive_timeout(5)
progress = 0.0
count = 0
verifying = False
canceling = False
speed = 0.0
for model in manager.selectedBundle.models:
count += 1
p = model.artifact.downloadProgress
@@ -263,11 +265,14 @@ class ModelsLayoutMici(NavScroller):
custom.ModelManagerSP.DownloadStatus.verifying):
progress += p.progress
verifying = verifying or p.status == custom.ModelManagerSP.DownloadStatus.verifying
canceling = canceling or p.canceling
speed = max(speed, p.speed) # only one file downloads at a time, so the non-zero speed is the live one
elif p.status in (custom.ModelManagerSP.DownloadStatus.downloaded,
custom.ModelManagerSP.DownloadStatus.cached):
progress += 100.0
self.current_model_info.current_model_header.set_text(tr("verifying") if verifying else tr("downloading"))
header = tr("canceling...") if canceling else tr("verifying") if verifying else tr("downloading")
self.current_model_info.current_model_header.set_text(header)
self.cancel_download_btn.set_text(tr("cancel verification") if verifying else tr("cancel download"))
self.current_model_info.current_model_header._shimmer = True
name_text = manager.selectedBundle.internalName.lower()
@@ -276,7 +281,10 @@ class ModelsLayoutMici(NavScroller):
self.current_model_info.current_model_text.set_text(name_text)
self.current_model_info.info_header.set_text(tr("progress") + self._download_progress)
self.current_model_info.info_header._shimmer = True
self.current_model_info.info_text.set_text(f"{progress/count:.2f}%")
progress_text = f"{progress/count:.2f}%"
if not verifying and not canceling:
progress_text += f" | {speed / 1024 ** 2:.1f} MB/s" if speed > 0 else " | --.- MB/s"
self.current_model_info.info_text.set_text(progress_text)
elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloaded:
self.current_model_info.info_header.set_text(tr("downloaded"))
@@ -0,0 +1,21 @@
from types import SimpleNamespace
from unittest.mock import Mock
from opendbc.car.structs import car
from openpilot.selfdrive.ui.sunnypilot.custom_button import CustomButtonAction, handle_custom_button
def test_custom_button_actions():
params = Mock()
press = SimpleNamespace(carState=SimpleNamespace(buttonEvents=[SimpleNamespace(
type=car.CarState.ButtonEvent.Type.altButton2,
pressed=True,
)]))
messages = [press, SimpleNamespace(carState=SimpleNamespace(buttonEvents=[])), press]
callbacks = {action: Mock() for action in CustomButtonAction if action != CustomButtonAction.NONE}
for action, callback in callbacks.items():
params.get.return_value = action
handle_custom_button(messages, params, callbacks)
assert callback.call_count == 2
@@ -7,6 +7,7 @@ See the LICENSE.md file in the root directory for more details.
"""
import argparse
import dataclasses
import inspect
import itertools
import mmap
import re
@@ -45,6 +46,11 @@ def _address(v):
return _root(v.src[0]), add + start, size
def _buffer(u):
from tinygrad.uop.ops import Ops
return u.buffer if u.op is Ops.BUFFER else getattr(u.arg, "buffer", None)
def _patched(buf, topo) -> tuple[bytearray, dict]:
from tinygrad.uop.ops import Ops
blob, rows = bytearray(buf.max_numel() * buf.dtype.itemsize), {}
@@ -72,7 +78,7 @@ def _evaluate(u, env):
table, index = _root(u.src[0].src[0]), _evaluate(u.src[0].src[1], env)
return struct.unpack_from(u.dtype.fmt, table.arg, index * u.dtype.itemsize)[0] if table.op is Ops.BINARY and index is not None else None
if u.op in GroupOp.ALU and None not in (values := [_evaluate(x, env) for x in u.src]):
return exec_alu(u.op, u.dtype, values, False)
return exec_alu(u.op, u.dtype, values, *([False] if "truncate_output" in inspect.signature(exec_alu).parameters else []))
return None
@@ -112,7 +118,7 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
from tinygrad.uop.ops import Ops
submit = jit.captured._linear.src[0].without_after
kernels, topo = submit.arg.aux.kernels, list(jit.captured._linear.toposort(enter_calls=True))
allocs = {str(u.tag): u for u in topo if u.op is Ops.ALLOC and u.tag is not None}
allocs = {str(name): u for u in topo if u.op is Ops.ALLOC and (name := getattr(u, "tag", None) or u.arg.name) is not None}
(commands, patched), kernargs = _patched(allocs["cmdbuf_compute_0"], topo), allocs["kernargs_compute_0"]
argument_words, pokes = _patched(kernargs, topo)[1], _input_pokes(submit, kernargs)
sh = {name: getattr(amd_gpu, f"reg{name}") + amd_gpu.GC_BASE__INST0_SEG0 - pm4.PACKET3_SET_SH_REG_START
@@ -158,7 +164,7 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
args.append({"input": pokes[slot][0]})
elif slot in argument_words:
base, at, nbytes = _address(argument_words[slot])
args.append({"buffer": buffers.setdefault(base, len(buffers)), "offset": at, "nbytes": nbytes or base.arg.buffer.nbytes, "dtype": u8})
args.append({"buffer": buffers.setdefault(base, len(buffers)), "offset": at, "nbytes": nbytes or base.max_numel() * base.dtype.itemsize, "dtype": u8})
outs, ins = kernel[6]
calls.append({"kind": "program", "name": re.sub(r"\x1b\[[0-9;]*m", "", kernel[1]), "lib": programs[image_alloc], "target": dataclasses.asdict(target),
"signature": [[f"data{i}", i, u8, [a.get("nbytes", 1)]] for i, a in enumerate(args)], "global_size": grid, "local_size": local,
@@ -166,12 +172,13 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
needs_init |= {args[i]["buffer"] for i in ins if "buffer" in args[i] and args[i]["buffer"] not in written}
written |= {args[i]["buffer"] for i in outs if "buffer" in args[i]}
assert all(_buffer(b) is not None for b, i in buffers.items() if i in needs_init), "a kernel reads a scratch before any kernel writes it"
names = list(jit.captured.expected_names)
inputs = [{"slot": slot, "name": names[slot] if slot < len(names) else None, "dtype": repr(u.dtype), "numel": int(u.max_numel()), "device": u.device}
for slot, u in enumerate(_prepare_jit_inputs((), kwargs)[0])]
header = {"input_names": names, "calls": calls, "returns": {"kind": "NoneType", "tensors": []}, "inputs": inputs,
"buffers": [{"nbytes": b.arg.buffer.nbytes, "device": b.arg.buffer.device, "dtype": u8}
| ({"init": blob.add(b.arg.buffer.ensure_allocated().as_memoryview())} if i in needs_init else {}) for b, i in buffers.items()],
"buffers": [{"nbytes": bb.nbytes if (bb := _buffer(b)) else b.max_numel() * b.dtype.itemsize, "device": bb.device if bb else b.device, "dtype": u8}
| ({"init": blob.add(bb.ensure_allocated().as_memoryview())} if i in needs_init else {}) for b, i in buffers.items()],
"check": {"inputs": [[path, store(array), device] for path, array, device in frame["inputs"]],
"outputs": [store(array) for array in frame["outputs"]], "output_container": frame["output_container"]}}
with sink:
+9 -3
View File
@@ -250,6 +250,12 @@ def _allocate_buffers(header: dict, blob: memoryview, device_map: dict | None, s
return buffers
def _fields(what, /, **values): # a tinygrad dataclass built or replaced with the fields it has today: a dropped field is fine, a new required one is not
known = {field.name for field in dataclasses.fields(what)}
values = {name: value for name, value in values.items() if name in known}
return what(**values) if isinstance(what, type) else dataclasses.replace(what, **values)
def _program_uop(call: dict, lib: bytes):
from tinygrad.helpers import Target
from tinygrad.uop.ops import KernelInfo, Ops, ProgramInfo, UOp
@@ -257,9 +263,9 @@ def _program_uop(call: dict, lib: bytes):
for name, compact_slot, dtype, shape in call["signature"]:
parameter = UOp.param(call["globals"][compact_slot], _dtype(dtype), math.prod(shape))
image = (shape[0], shape[1]) if len(shape) == 3 and shape[2] == 4 else None
parameters.append(parameter.replace(arg=dataclasses.replace(parameter.arg, name=name, image=image)))
info = ProgramInfo(global_size=tuple(call["global_size"]), local_size=tuple(call["local_size"]), vars=(), globals=tuple(call["globals"]),
outs=tuple(call["outs"]), ins=tuple(call["ins"]), target=Target(**call["target"]))
parameters.append(parameter.replace(arg=_fields(parameter.arg, name=name, image=image)))
info = _fields(ProgramInfo, global_size=tuple(call["global_size"]), local_size=tuple(call["local_size"]), globals=tuple(call["globals"]),
outs=tuple(call["outs"]), ins=tuple(call["ins"]), target=_fields(Target, **call["target"]))
return UOp(Ops.PROGRAM, src=(UOp(Ops.SINK, arg=KernelInfo(name=call["name"])), UOp(Ops.LINEAR, src=tuple(parameters)),
UOp(Ops.SOURCE, arg=""), UOp(Ops.BINARY, arg=lib)), arg=info)
@@ -23,7 +23,7 @@ def record_kernel_linear():
recorded, hcq_compile, compile_linear = [], realize.hcq_compile, jit.compile_linear
def recording_compile_linear(*args, **kwargs):
with mock.patch.object(realize, "hcq_compile", lambda linear, *a, **k: (recorded.append(linear), hcq_compile(linear, *a, **k))[1]):
with mock.patch.object(realize, "hcq_compile", lambda linear, *a, **k: (linear.src and recorded.append(linear), hcq_compile(linear, *a, **k))[1]):
return compile_linear(*args, **kwargs)
with mock.patch.object(jit, "compile_linear", recording_compile_linear):
@@ -38,6 +38,18 @@ def capture_frame(jit, kwargs: dict, output_container: str | None = None):
return {"inputs": inputs, "outputs": [tensor.numpy().copy() for tensor in outputs], "output_container": output_container}, result
def kernel_signature(ast, elf) -> list:
from tinygrad.dtype import AddrSpace
if hasattr(ast, "kernel_params"):
if any(p.arg.addrspace is AddrSpace.ALU for p in ast.kernel_params):
raise ValueError(f"{elf.name} takes a variable, which plain artifacts don't support")
compact = {slot: i for i, slot in enumerate(ast.arg.globals)}
return [[p.arg.name, compact[p.arg.slot], repr(p.dtype), [int(x) for x in p._shape]] for p in ast.kernel_params]
if not all(isinstance(slot, int) for _, slot, _, _ in elf.signature):
raise ValueError("TinyELF.signature lost its slots and this tinygrad has no UOp.kernel_params: update kernel_signature")
return [[name, slot, repr(dtype), [int(x) for x in shape]] for name, slot, dtype, shape in elf.signature]
def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict, memoryview]:
from tinygrad.uop.ops import Ops
sink = tempfile.TemporaryFile(dir=".")
@@ -51,6 +63,12 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
if uop.op is Ops.PARAM:
input_specs.setdefault(uop.arg.slot, {"dtype": repr(uop.dtype), "numel": int(uop.max_numel()), "device": uop.device})
return {"input": uop.arg.slot}
root, offset = uop, 0
while root.op in (Ops.AFTER, Ops.BITCAST, Ops.SHRINK):
offset, root = offset + (root.src[1].arg * root.src[0].dtype.itemsize if root.op is Ops.SHRINK else 0), root.src[0]
if root.op is Ops.ALLOC:
base = bases.setdefault(id(root), (len(bases), root, root.dtype))
return {"buffer": base[0], "offset": offset, "nbytes": int(uop.max_numel()) * uop.dtype.itemsize, "dtype": repr(uop.dtype)}
buffer = uop.buffer
root = buffer._base if buffer._base is not None else buffer
base = bases.setdefault(id(root), (len(bases), root, uop.base.dtype if buffer._base is None else uop.dtype))
@@ -63,7 +81,7 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
elf, info = ast.to_elf(), ast.arg
global_size, local_size = info.launch_dims({})
calls.append({"kind": "program", "name": elf.name, "lib": blob.add(bytes(elf.lib)), "target": dataclasses.asdict(elf.target),
"signature": [[name, slot, repr(dtype), [int(x) for x in shape]] for name, slot, dtype, shape in elf.signature],
"signature": kernel_signature(ast, elf),
"global_size": [int(x) for x in global_size], "local_size": [int(x) for x in local_size],
"globals": list(info.globals), "outs": list(info.outs), "ins": list(info.ins), "args": arguments})
outs, ins = info.outs, info.ins
@@ -77,7 +95,9 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
ret = jit.captured.ret
returns = {"kind": type(ret).__name__, "tensors": [describe(t.uop.base) | {"shape": [int(x) for x in t.shape]} for t in flatten(ret)]}
buffers = [{"nbytes": base.nbytes, "device": base.device, "dtype": repr(dtype)}
from tinygrad.device import Buffer
assert not [base for index, base, _ in bases.values() if index in needs_init and not isinstance(base, Buffer)], "a kernel reads a scratch before writing it"
buffers = [{"nbytes": base.nbytes if isinstance(base, Buffer) else int(base.max_numel()) * dtype.itemsize, "device": base.device, "dtype": repr(dtype)}
| ({"init": blob.add(base.as_memoryview())} if index in needs_init else {}) for index, base, dtype in bases.values()]
names = list(jit.captured.expected_names)
if check is not None:
@@ -0,0 +1,121 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import os
import re
import sys
import tempfile
import unittest
from unittest import mock
import numpy as np
os.environ.setdefault("CHECK_OOB", "0")
os.environ.setdefault("GMMU", "0")
if sys.platform == "darwin" and os.environ.get("DEV", "").startswith("MOCKUSB"): # the mock's VRAM is a memfd, which macOS lacks: an unlinked file is the same
from tinygrad.runtime.autogen import libc
def _memfd_create(name, flags):
fd, path = tempfile.mkstemp()
os.unlink(path)
return fd
libc.memfd_create = _memfd_create
from openpilot.common.test import OpenpilotTestCase
from openpilot.sunnypilot.modeld_v2 import convert_upstream_pkl
from openpilot.sunnypilot.modeld_v2.convert_upstream_pkl import convert
from openpilot.sunnypilot.modeld_v2.helpers import PlainJit, load_model
from openpilot.sunnypilot.modeld_v2.plain_artifact_compiler import capture_frame
IMG, DESIRE, STATE, OUT = (6, 8, 8), (8,), (4, 16), (1, 32)
class TestConvertUpstreamPkl(OpenpilotTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
if not os.environ.get("TINYGRAD_TESTS"):
raise unittest.SkipTest("TINYGRAD_TESTS not set. skipping converter test")
from tinygrad import Device, Tensor
assert Device.DEFAULT == "AMD" and getattr(Device["AMD"], "is_usb", False), f"{Device.DEFAULT} is not an AMD device behind the USB bridge"
assert (Tensor([1, 2, 3]) * 2).tolist() == [2, 4, 6]
cls.tmp = tempfile.TemporaryDirectory()
cls.rng = np.random.default_rng(0)
cls.weights = Tensor(cls.rng.standard_normal((16, 32)).astype(np.float32)).realize()
cls.bias = Tensor(cls.rng.standard_normal((1, 32)).astype(np.float32)).realize()
cls.input_specs = {"input_imgs": (IMG, "uint8", Device.DEFAULT), "desire": (DESIRE, "float32", Device.DEFAULT),
"features": (STATE, "float32", Device.DEFAULT)}
cls.output_specs = {"outputs": (OUT, "float32", Device.DEFAULT), "next_features": (STATE, "float32", Device.DEFAULT)}
@classmethod
def tearDownClass(cls):
cls.tmp.cleanup()
super().tearDownClass()
def model(self, input_imgs, desire, features, output_buffers):
from tinygrad import Tensor
feat = features.mean(axis=0).reshape(1, 16) + input_imgs.float().mean() + desire.sum()
Tensor.realize(output_buffers["outputs"].assign((feat @ self.weights + self.bias).relu()),
output_buffers["next_features"].assign(features[1:].cat(feat * 2)))
def inputs(self, seed: int) -> dict:
from examples.openpilot.helpers import allocate_inputs
rng = np.random.default_rng(seed)
def initialize(arrays: dict):
for value in arrays.values():
value[...] = rng.standard_normal(value.shape) if value.dtype.kind == "f" else rng.integers(0, 256, value.shape)
return allocate_inputs(self.input_specs, initialize) | {"output_buffers": allocate_inputs(self.output_specs)}
def jit(self):
from tinygrad import TinyJit
run = TinyJit(self.model, prune=True)
kwargs = self.inputs(1)
run(**kwargs)
run(**kwargs)
return run
def assert_matches_eager(self, plain, seed: int):
fresh, expected = self.inputs(seed), self.inputs(seed)
plain(**fresh)
self.model(**expected)
for name in self.output_specs:
assert np.array_equal(fresh["output_buffers"][name].numpy(), expected["output_buffers"][name].numpy()), name
def convert(self, jit) -> tuple[dict, memoryview]:
from tinygrad import Device
kwargs = self.inputs(2)
frame, _ = capture_frame(jit, kwargs, "output_buffers")
header, blob = convert(jit, kwargs, frame, Device[Device.DEFAULT].renderer.target)
kernels = jit.captured._linear.src[0].without_after.arg.aux.kernels
assert [call["name"] for call in header["calls"]] == [re.sub(r"\x1b\[[0-9;]*m", "", kernel[1]) for kernel in kernels]
assert all(re.fullmatch(r"\w+", call["name"]) for call in header["calls"]), "kernel names: failure point 1"
assert [(tuple(call["outs"]), tuple(call["ins"])) for call in header["calls"]] == [tuple(kernel[6]) for kernel in kernels], "outs/ins: failure point 1"
bound = {argument["input"] for call in header["calls"] for argument in call["args"] if "input" in argument}
assert bound == {spec["slot"] for spec in header["inputs"]}, f"input slots bound by pokes {sorted(bound)}: failure point 4"
assert [spec["name"] for spec in header["inputs"]][:len(self.input_specs)] == sorted(self.input_specs)
return header, blob
def test_converted_jit_matches_eager(self): # the submit decoded into plain calls, replayed against its own frame
jit = self.jit()
plain = PlainJit(*self.convert(jit))
self.assert_matches_eager(plain, 3)
self.assert_matches_eager(plain, 4)
def test_pickle_round_trip_through_main(self):
from examples.openpilot.helpers import dump_pickle, load_pickle
from tinygrad.engine.realize import lower_and_compile
pkl, out = os.path.join(self.tmp.name, "tiny_tinygrad.pkl"), os.path.join(self.tmp.name, "tiny_plain.pkl")
dump_pickle({"run": self.jit(), "input_specs": self.input_specs, "output_specs": self.output_specs, "metadata": {"name": "tiny"}}, pkl)
artifact = load_pickle(pkl, out_of_band=True)
artifact["run"].captured._linear = lower_and_compile(artifact["run"].captured._linear)
self.assert_matches_eager(PlainJit(*self.convert(artifact["run"])), 5)
with mock.patch.object(sys, "argv", ["convert_upstream_pkl", pkl, out]):
convert_upstream_pkl.main()
bundle = load_model(out, cache=False, verify=True)
assert bundle["metadata"] == {"name": "tiny"} and bundle["input_specs"] == self.input_specs and bundle["output_specs"] == self.output_specs
self.assert_matches_eager(bundle["run"], 6)
@@ -0,0 +1,162 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import dataclasses
import functools
import inspect
import os
import sys
import tempfile
from unittest import mock
import numpy as np
import unittest
import openpilot.sunnypilot.modeld_v2.helpers as helpers
from openpilot.common.test import OpenpilotTestCase
from openpilot.sunnypilot.modeld_v2 import compile_warp
from openpilot.sunnypilot.modeld_v2.frame_fusion import frame_part, pack_layout
from openpilot.sunnypilot.modeld_v2.helpers import PlainJit, cache_is_fresh, load_model, read_parts, write_bundle
from openpilot.sunnypilot.modeld_v2.plain_artifact_compiler import capture_frame, record_kernel_linear, serialize_jit
CAM_W, CAM_H, MODEL_W, MODEL_H = 32, 16, 16, 8
FRAME = f"{CAM_W},{CAM_H},{CAM_W},{CAM_H},{CAM_H // 2},{CAM_W * CAM_H * 3 // 2}"
FRAME_SIZE = CAM_W * CAM_H * 3 // 2
HOST = {"desire": (8,), "traffic_convention": (2,)}
STATE, OUT = (4, 16), (1, 32)
def compile_jit(fn, inputs: dict, output_container: str | None = None) -> tuple[dict, memoryview]:
from tinygrad import TinyJit
run = TinyJit(fn, prune=True)
with record_kernel_linear() as recorded:
run(**inputs)
run(**inputs)
check, _ = capture_frame(run, inputs, output_container)
assert len(recorded) == 1, f"expected one captured kernel list, got {len(recorded)}"
return serialize_jit(run, recorded[0], check)
class TestPlainArtifact(OpenpilotTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
if not os.environ.get("TINYGRAD_TESTS"):
raise unittest.SkipTest("TINYGRAD_TESTS env var not set, skipping artifact test.")
from tinygrad import Tensor
cls.tmp = tempfile.TemporaryDirectory()
cls.rng = np.random.default_rng(0)
cls.weights = Tensor(cls.rng.standard_normal((16, 32)).astype(np.float32)).realize()
cls.bias = Tensor(cls.rng.standard_normal((1, 32)).astype(np.float32)).realize()
@classmethod
def tearDownClass(cls):
cls.tmp.cleanup()
super().tearDownClass()
def model(self, x, h): # a two input, one weight model: the shape of a policy without the size
return ((x @ self.weights + self.bias).relu() + h.mean()).contiguous()
def tensors(self, seed: int):
from tinygrad import Tensor
rng = np.random.default_rng(seed)
return {"x": Tensor(rng.standard_normal((1, 16)).astype(np.float32)).realize(), "h": Tensor(rng.standard_normal((4, 16)).astype(np.float32)).realize()}
def artifact(self, name: str = "model") -> str:
path = os.path.join(self.tmp.name, f"{name}_tinygrad.pkl")
if not os.path.exists(path):
write_bundle(path, {"run": compile_jit(self.model, self.tensors(1))}, {"metadata": {"name": name}})
return path
def test_round_trip_matches_jit(self): # compiler -> bundle -> PlainJit, replay verified, then fresh inputs bit exact with eager tinygrad
bundle = load_model(self.artifact(), cache=False, verify=True)
assert bundle["metadata"] == {"name": "model"} and isinstance(bundle["run"], PlainJit)
fresh = self.tensors(2)
assert np.array_equal(bundle["run"](**fresh).numpy(), self.model(**fresh).numpy())
def test_replay_check_catches_a_changed_kernel(self):
parts, extras = read_parts(self.artifact())
header, blob = parts["run"]
poisoned = dict(header, check=dict(header["check"], outputs=[header["check"]["inputs"][0][1]]))
with self.assertRaises(helpers.ArtifactMismatch):
PlainJit(poisoned, blob)
def test_cache_round_trip_holds_no_weights_and_is_rebuilt_when_stale(self):
path = self.artifact("cached")
expected = load_model(path, cache=False, verify=False)["run"](**self.tensors(3)).numpy()
load_model(path)
assert cache_is_fresh(path)
with open(f"{path}.cache", "rb") as file:
cache = file.read()
assert self.weights.numpy().tobytes() not in cache and len(cache) < os.path.getsize(path)
with mock.patch.object(helpers, "PlainJit", side_effect=AssertionError("a fresh cache must not rebuild")):
cached = load_model(path)
assert np.array_equal(cached["run"](**self.tensors(3)).numpy(), expected)
with mock.patch.object(helpers, "tinygrad_identity", return_value="another tinygrad"):
assert not cache_is_fresh(path)
assert isinstance(load_model(path)["run"], PlainJit)
assert cache_is_fresh(path)
assert not cache_is_fresh(path) # keyed on the tinygrad that wrote it, so the next boot after a bump rebuilds once
os.utime(path)
with mock.patch.object(helpers, "tinygrad_identity", return_value="another tinygrad"):
assert not cache_is_fresh(path)
def test_fused_with_warp_matches_warp_then_model(self):
from tinygrad import Tensor
warp_path = os.path.join(self.tmp.name, "warp_tinygrad.pkl")
with mock.patch.object(sys, "argv", ["compile_warp", "--frame", FRAME, "--warp-to", f"{MODEL_W}x{MODEL_H}", "--output", warp_path]):
compile_warp.main()
warp = load_model(warp_path, cache=False)["run"]
def native(new_img, desire, traffic_convention, state_feat_q, output_buffers):
feat = state_feat_q.mean(axis=0).reshape(1, 16) + new_img.float().mean() + desire.sum() + traffic_convention[0]
Tensor.realize(output_buffers["outputs"].assign((feat @ self.weights + self.bias).relu()),
output_buffers["next_state_feat_q"].assign(state_feat_q[1:].cat(feat * 2)))
def model_inputs(new_img):
return {"new_img": new_img} | {name: Tensor(np.ones(shape, np.float32) * 0.5).realize() for name, shape in HOST.items()} | \
{"state_feat_q": Tensor(self.rng.standard_normal(STATE).astype(np.float32)).realize(),
"output_buffers": {"outputs": Tensor.zeros(OUT).contiguous().realize(), "next_state_feat_q": Tensor.zeros(STATE).contiguous().realize()}}
frames = Tensor(self.rng.integers(0, 256, (2, FRAME_SIZE), dtype=np.uint8)).realize()
tfm = Tensor(np.tile(np.eye(3, dtype=np.float32), (2, 1, 1))).realize()
path = os.path.join(self.tmp.name, "native_tinygrad.pkl")
part = compile_jit(native, model_inputs(warp(input_frame=frames, M_inv=tfm).realize()), "output_buffers")
extras = {"input_specs": {"new_img": ((2, 6, MODEL_H // 2, MODEL_W // 2), "|u1", "CPU"), "state_feat_q": (STATE, "<f4", "CPU")}
| {name: (shape, "<f4", "CPU") for name, shape in HOST.items()},
"output_specs": {"outputs": (OUT, "<f4", "CPU"), "next_state_feat_q": (STATE, "<f4", "CPU")},
"metadata": {"output_shapes": {"outputs": OUT, "next_state_feat_q": STATE}}}
write_bundle(path, {"run": part}, extras)
fused = load_model(path, cache=False, extend=functools.partial(frame_part, warp_path))
assert fused["run"].input_names == ["host", "state_feat_q"]
offsets, packed = pack_layout(HOST, FRAME_SIZE)
host = np.zeros(packed, np.uint8)
inputs = model_inputs(None)
for name, shape in ({"tfm": (2, 3, 3)} | HOST).items():
np.ndarray(shape, np.float32, host, offsets[name])[...] = tfm.numpy() if name == "tfm" else inputs[name].numpy()
host[offsets["frames"]:] = frames.numpy().ravel()
fused["run"](host=Tensor(host, device="NPY"), state_feat_q=inputs["state_feat_q"], output_buffers=inputs["output_buffers"])
expected = model_inputs(warp(input_frame=frames, M_inv=tfm).realize()) | {k: inputs[k] for k in ("state_feat_q",)}
native(**expected)
for name in ("outputs", "next_state_feat_q"):
assert np.array_equal(inputs["output_buffers"][name].numpy(), expected["output_buffers"][name].numpy()), name
def test_tinygrad_api_the_loader_reads(self):
import tinygrad.engine.jit as jit
from tinygrad.device import Buffer, TinyELF
from tinygrad.helpers import Target
from tinygrad.uop.ops import ParamArg, ProgramInfo, UOp
fields = {cls: {f.name for f in dataclasses.fields(cls)} for cls in (ProgramInfo, ParamArg, Target)}
assert fields[ProgramInfo] >= {"global_size", "local_size", "globals", "outs", "ins", "target"}, "ProgramInfo changed: update helpers._program_uop"
assert fields[ParamArg] >= {"slot", "dtype", "name", "image"}, "ParamArg changed: update helpers._program_uop"
assert fields[Target] >= {"device", "renderer", "arch"}, "Target changed: update the 'target' key in plain_artifact_compiler.serialize_jit"
assert "signature" in getattr(TinyELF, "__annotations__", {}) or hasattr(UOp, "kernel_params"), \
"kernel signatures moved: update plain_artifact_compiler.serialize_jit"
assert callable(getattr(UOp, "param", None)) and callable(getattr(UOp, "from_buffer", None)), "UOp.param/from_buffer: update helpers.PlainJit"
assert callable(getattr(UOp, "store_call", None)) and callable(getattr(UOp, "call", None)), "UOp.call/store_call: update helpers.PlainJit"
assert callable(getattr(Buffer, "view", None)) and callable(getattr(Buffer, "ensure_allocated", None)), "Buffer.view: update helpers._upload"
assert callable(getattr(jit, "compile_linear", None)) and callable(getattr(jit, "_prepare_jit_inputs", None)), "engine.jit: update helpers._Jit"
assert "expected_names" in inspect.signature(jit.CapturedJit).parameters, "CapturedJit: update helpers.PlainJit and plain_artifact_compiler.serialize_jit"
+168 -48
View File
@@ -7,7 +7,10 @@ See the LICENSE.md file in the root directory for more details.
import asyncio
import os
import re
import threading
import time
from concurrent.futures import ThreadPoolExecutor
import requests
from openpilot.common.params import Params
@@ -22,12 +25,101 @@ from openpilot.sunnypilot.models.helpers import (ACTIVE_BUNDLE_KEYS, get_active_
# (connect, read) seconds. read is per-request inactivity, not a total cap
DOWNLOAD_TIMEOUT = (30, 30)
# HuggingFace throttles each connection to ~1-2 MB/s but not the connection count; 12 saturates the device link
MAX_CONCURRENT_PIECES = 12
PIECE_SIZE = 8 * 1024 * 1024
PIECE_ATTEMPTS = 3
PIECE_BACKOFF = 1.0 # seconds, growing linearly: waits 1 s then 2 s between a piece's three attempts
TRANSIENT_HTTP_STATUS = frozenset({408, 429, 500, 502, 503, 504})
REPORT_INTERVAL = 0.5 # seconds between progress publications
# byte ranges count encoded bytes but iter_content yields decoded ones, so ask for an unencoded body
NO_COMPRESSION = {"Accept-Encoding": "identity"}
SPEED_SMOOTHING = 0.7 # weight of the previous speed sample in the published speed
SPEED_FLOOR = 0.05 * 1024 ** 2 # bytes/s; anything slower would only display as 0.0 MB/s, so it reads as no rate
class DownloadCancelled(Exception):
pass
def _is_transient(e: BaseException) -> bool:
if isinstance(e, requests.HTTPError):
return e.response is not None and e.response.status_code in TRANSIENT_HTTP_STATUS
return isinstance(e, (requests.ConnectionError, requests.Timeout, requests.exceptions.ChunkedEncodingError))
class _Progress:
def __init__(self):
self._bytes = 0
self._lock = threading.Lock()
def add_bytes(self, n: int) -> None:
with self._lock:
self._bytes += n
def snapshot(self) -> int:
with self._lock:
return self._bytes
def _piece_ranges(total: int, piece_size: int) -> list[tuple[int, int]]:
return [(start, min(start + piece_size, total)) for start in range(0, total, piece_size)]
def _prepare_target(path: str, total: int) -> None:
with open(path, "wb") as f:
f.truncate(total)
def _content_range_total(response: requests.Response) -> int | None:
if response.status_code != 206:
return None
match = re.fullmatch(r"bytes \d+-\d+/(\d+)", response.headers.get("Content-Range", ""))
return int(match.group(1)) if match else None
def _probe_size(url: str) -> tuple[int, bool]:
with requests.get(url, headers={**NO_COMPRESSION, "Range": "bytes=0-0"}, stream=True, timeout=DOWNLOAD_TIMEOUT) as response:
response.raise_for_status()
total = _content_range_total(response)
if total is not None:
return total, True
return int(response.headers.get("Content-Length", 0)), False
def _fetch_piece(url: str, path: str, index: int, start: int, end: int | None, progress: _Progress,
cancel: threading.Event, block_size: int) -> None:
headers = {**NO_COMPRESSION, "Range": f"bytes={start}-{end - 1}"} if end is not None else NO_COMPRESSION
label = f"{os.path.basename(path)} piece {index}"
for attempt in range(PIECE_ATTEMPTS):
written = 0
try:
with requests.get(url, headers=headers, stream=True, timeout=DOWNLOAD_TIMEOUT) as response:
response.raise_for_status()
if end is not None and response.status_code != 206:
raise ValueError(f"server ignored byte range for {label}")
with open(path, "r+b") as f:
f.seek(start)
for block in response.iter_content(chunk_size=block_size):
if cancel.is_set():
raise DownloadCancelled("Download cancelled")
f.write(block)
written += len(block)
progress.add_bytes(len(block))
if end is None:
f.truncate() # unknown length: the body defines the file size
if end is not None and written != end - start:
raise requests.exceptions.ConnectionError(f"short read: {written} of {end - start} bytes")
return
except requests.RequestException as e:
progress.add_bytes(-written) # the piece restarts from scratch
if not _is_transient(e) or attempt == PIECE_ATTEMPTS - 1:
raise
cloudlog.warning(f"retrying {label} after {type(e).__name__}: {e}")
if cancel.wait(PIECE_BACKOFF * (1 + attempt)):
raise DownloadCancelled("Download cancelled") from None
class ModelManagerSP:
"""Manages model downloads and status reporting"""
@@ -41,7 +133,7 @@ class ModelManagerSP:
self.source_models: dict[str, list[custom.ModelManagerSP.ModelBundle]] = {}
self.selected_bundle: custom.ModelManagerSP.ModelBundle = None
self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params, chestnut=self.chestnut_present)
self._chunk_size = 128 * 1000 # 128 KB chunks
self._block_size = 128 * 1000 # 128 KB network read blocks
self._download_start_times: dict[str, float] = {} # Track start time per model
self._download_ref: bytes | str | None = None
@@ -56,7 +148,6 @@ class ModelManagerSP:
self._download_ref = None
def _sync_artifact_progress(self, source_artifact) -> None:
"""Mirror download progress to all artifacts sharing the same filename in the selected bundle."""
if not self.selected_bundle:
return
for model in self.selected_bundle.models:
@@ -65,9 +156,9 @@ class ModelManagerSP:
artifact.downloadProgress.status = source_artifact.downloadProgress.status
artifact.downloadProgress.progress = source_artifact.downloadProgress.progress
artifact.downloadProgress.eta = source_artifact.downloadProgress.eta
artifact.downloadProgress.speed = source_artifact.downloadProgress.speed
def _calculate_eta(self, filename: str, progress: float) -> int:
"""Calculate ETA based on elapsed time and current progress"""
if filename not in self._download_start_times or progress <= 0:
return 60 # Default ETA for new downloads
@@ -75,39 +166,74 @@ class ModelManagerSP:
if elapsed_time <= 0:
return 60
# If we're at X% after Y seconds, we can estimate total time as (Y / X) * 100
total_estimated_time = (elapsed_time / progress) * 100
eta = total_estimated_time - elapsed_time
return max(1, int(eta)) # Return at least 1 second if download is ongoing
async def _download_file(self, url: str, path: str, model) -> None:
"""Downloads a file with progress tracking"""
self._download_start_times[model.fileName] = time.monotonic()
def _set_progress(self, artifact, status, progress: float, eta: int = 0, speed: float = 0.0) -> None:
artifact.downloadProgress.status = status
artifact.downloadProgress.progress = progress
artifact.downloadProgress.eta = eta
artifact.downloadProgress.speed = speed
self._sync_artifact_progress(artifact)
self._report_status()
with requests.get(url, stream=True, timeout=DOWNLOAD_TIMEOUT) as response: # noqa: ASYNC210
response.raise_for_status()
total_size = int(response.headers.get("content-length", 0))
bytes_downloaded = 0
def _publish_progress(self, artifact, done_bytes: int, total: int, speed: float) -> None:
# held at 99 until the file passes its hash check
progress = min(99.0, done_bytes / total * 100) if total > 0 else 0.0
if speed > 0 and total > 0:
eta = max(1, int((total - done_bytes) / speed))
else:
eta = self._calculate_eta(artifact.fileName, progress)
self._set_progress(artifact, custom.ModelManagerSP.DownloadStatus.downloading, progress, eta, speed)
with open(path, 'wb') as f: # noqa: ASYNC230
for chunk in response.iter_content(chunk_size=self._chunk_size): # type: bytes
f.write(chunk)
bytes_downloaded += len(chunk)
async def _report_until_done(self, tasks: list[asyncio.Future], artifact, progress: _Progress, total: int,
started: tuple[int, float]) -> None:
pending = set(tasks)
last_bytes, last_time = started
speed = 0.0
while pending:
done, pending = await asyncio.wait(pending, timeout=REPORT_INTERVAL)
for task in done:
task.result() # re-raises a worker failure
if self._download_interrupted():
raise DownloadCancelled("Download cancelled")
now = time.monotonic()
done_bytes = progress.snapshot()
if (dt := now - last_time) > 0:
instant = max(0.0, (done_bytes - last_bytes) / dt) # bytes/s this tick; clamped since retried pieces can lower the byte count
speed = instant if speed == 0 else SPEED_SMOOTHING * speed + (1 - SPEED_SMOOTHING) * instant
if speed < SPEED_FLOOR: # a stall decays toward 0 without reaching it, and a near-0 speed overflows the eta
speed = 0.0
last_time, last_bytes = now, done_bytes
self._publish_progress(artifact, done_bytes, total, speed)
if self._download_interrupted():
raise DownloadCancelled("Download cancelled")
async def _download_file(self, url: str, path: str, artifact) -> None:
self._download_start_times[artifact.fileName] = time.monotonic()
loop = asyncio.get_running_loop()
if total_size > 0:
progress = (bytes_downloaded / total_size) * 100
model.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading
model.downloadProgress.progress = progress
model.downloadProgress.eta = self._calculate_eta(model.fileName, progress)
self._sync_artifact_progress(model)
self._report_status()
total, ranged = await loop.run_in_executor(None, _probe_size, url)
pieces: list[tuple[int, int | None]] = list(_piece_ranges(total, PIECE_SIZE)) if ranged else [(0, None)]
await loop.run_in_executor(None, _prepare_target, path, total)
progress = _Progress()
cancel = threading.Event()
started = progress.snapshot(), time.monotonic() # before any worker can land bytes
# Clean up start time after download completes
del self._download_start_times[model.fileName]
with ThreadPoolExecutor(max_workers=MAX_CONCURRENT_PIECES) as pool:
tasks = [loop.run_in_executor(pool, _fetch_piece, url, path, i, start, end, progress, cancel, self._block_size)
for i, (start, end) in enumerate(pieces)]
try:
await self._report_until_done(tasks, artifact, progress, total, started)
except BaseException as e:
cancel.set()
if isinstance(e, DownloadCancelled):
artifact.downloadProgress.canceling = True
self._set_progress(artifact, custom.ModelManagerSP.DownloadStatus.downloading, artifact.downloadProgress.progress)
for task in tasks:
task.cancel() # drops queued pieces; running ones see the event at their next block
await asyncio.gather(*tasks, return_exceptions=True)
raise
del self._download_start_times[artifact.fileName]
async def _process_artifact(self, artifact, destination_path: str) -> None:
if not artifact.downloadUri.uri:
@@ -119,33 +245,31 @@ class ModelManagerSP:
expected_hash = artifact.downloadUri.sha256
filename = artifact.fileName
full_path = os.path.join(destination_path, filename)
status = custom.ModelManagerSP.DownloadStatus
artifact.downloadProgress.canceling = False
try:
self._set_progress(artifact, status.verifying, 0)
if await verify_file(full_path, expected_hash):
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
artifact.downloadProgress.progress = 100
artifact.downloadProgress.eta = 0
self._sync_artifact_progress(artifact)
self._report_status()
self._set_progress(artifact, status.cached, 100)
return
self._set_progress(artifact, status.downloading, 0)
await self._download_file(url, full_path, artifact)
self._set_progress(artifact, status.verifying, 99)
if not await verify_file(full_path, expected_hash):
raise ValueError(f"Hash validation failed for {filename}")
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloaded
artifact.downloadProgress.progress = 100
artifact.downloadProgress.eta = 0
self._sync_artifact_progress(artifact)
self._report_status()
self._set_progress(artifact, status.downloaded, 100)
except DownloadCancelled:
self._download_start_times.pop(artifact.fileName, None)
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed
artifact.downloadProgress.status = status.failed
artifact.downloadProgress.eta = 0
artifact.downloadProgress.speed = 0
self._sync_artifact_progress(artifact)
if self.selected_bundle:
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed
self.selected_bundle.status = status.failed
self._report_status()
raise
@@ -153,11 +277,12 @@ class ModelManagerSP:
cloudlog.error(f"Error downloading {filename}: {str(e)}")
if os.path.isfile(full_path): # noqa: ASYNC240
os.remove(full_path)
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed
artifact.downloadProgress.status = status.failed
artifact.downloadProgress.eta = 0
artifact.downloadProgress.speed = 0
self._sync_artifact_progress(artifact)
if self.selected_bundle:
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed
self.selected_bundle.status = status.failed
self._report_status()
self._download_start_times.pop(artifact.fileName, None)
raise
@@ -197,6 +322,7 @@ class ModelManagerSP:
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
artifact.downloadProgress.progress = 100
artifact.downloadProgress.eta = 0
artifact.downloadProgress.speed = 0
else:
seen_artifacts.add(artifact.fileName)
await self._process_artifact(artifact, destination_path)
@@ -272,12 +398,7 @@ class ModelManagerSP:
rk.keep_time()
def clear_model_cache(self) -> None:
"""
Clears the model cache directory of all files except those in the active model bundle.
"""
# Get list of files used by both slots' selected bundles (either may become
# the truly active bundle depending on hardware availability)
""" Clears the model cache directory of all files except those in the active model bundle."""
active_files = []
for source in ACTIVE_BUNDLE_KEYS:
if selected_bundle := get_selected_bundle(self.params, source):
@@ -285,7 +406,6 @@ class ModelManagerSP:
if model.artifact.fileName:
active_files.append(model.artifact.fileName)
# Remove all files except active ones
model_dir = Paths.model_root()
try:
for filename in os.listdir(model_dir):
File diff suppressed because it is too large Load Diff
@@ -126,6 +126,7 @@ def initialize_params(params) -> list[dict[str, Any]]:
keys.extend([
"ToyotaEnforceStockLongitudinal",
"ToyotaStopAndGoHack",
"ToyotaTSS2Smooth",
])
return [{k: params.get(k, return_default=True)} for k in keys]
@@ -10,6 +10,8 @@ from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL
class AutoLaneChangeMode:
OFF = -1
NUDGE = 0 # default
@@ -31,6 +33,7 @@ AUTO_LANE_CHANGE_TIMER = {
}
ONE_SECOND_DELAY = -1
EDGE_DELAY = -2
class AutoLaneChangeController:
@@ -48,6 +51,7 @@ class AutoLaneChangeController:
self.prev_brake_pressed = False
self.auto_lane_change_allowed = False
self.prev_lane_change = False
self.lane_change_blocked = False
self.read_params()
@@ -68,7 +72,7 @@ class AutoLaneChangeController:
self.read_params()
self.param_read_counter += 1
def update_lane_change_timers(self, blindspot_detected: bool) -> None:
def update_lane_change_timers(self, blindspot_detected: bool, edge_detected: bool) -> None:
self.lane_change_delay = AUTO_LANE_CHANGE_TIMER.get(self.lane_change_set_timer,
AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.NUDGE])
@@ -80,6 +84,12 @@ class AutoLaneChangeController:
else:
self.lane_change_wait_timer = self.lane_change_delay + ONE_SECOND_DELAY
if edge_detected and self.lane_change_delay > 0:
if self.lane_change_delay == AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.NUDGELESS]:
self.lane_change_wait_timer = min(self.lane_change_wait_timer, EDGE_DELAY)
else:
self.lane_change_wait_timer = min(self.lane_change_wait_timer, self.lane_change_delay + EDGE_DELAY)
def update_allowed(self) -> bool:
# Auto lane change allowed if:
# 1. A valid delay is set (non-zero)
@@ -97,14 +107,23 @@ class AutoLaneChangeController:
return bool(self.lane_change_wait_timer > self.lane_change_delay)
def update_lane_change(self, blindspot_detected: bool, brake_pressed: bool) -> None:
def update_lane_change(self, blindspot_detected: bool, edge_detected: bool, brake_pressed: bool,
torque_applied: bool = False) -> None:
if brake_pressed and not self.prev_brake_pressed:
self.prev_brake_pressed = brake_pressed
self.update_lane_change_timers(blindspot_detected)
self.update_lane_change_timers(blindspot_detected, edge_detected)
self.auto_lane_change_allowed = self.update_allowed()
# BSM always blocks. Edge blocks auto mode but nudge can override.
if blindspot_detected:
self.lane_change_blocked = True
elif edge_detected:
self.lane_change_blocked = not torque_applied
else:
self.lane_change_blocked = False
def update_state(self):
if self.DH.lane_change_state == log.LaneChangeState.laneChangeStarting:
self.prev_lane_change = True
@@ -8,7 +8,7 @@ from openpilot.common.parameterized import parameterized
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState, LaneChangeDirection
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode, \
AUTO_LANE_CHANGE_TIMER, ONE_SECOND_DELAY
AUTO_LANE_CHANGE_TIMER, ONE_SECOND_DELAY, EDGE_DELAY
from openpilot.common.test import OpenpilotTestCase
AUTO_LANE_CHANGE_TIMER_COMBOS = [
@@ -61,7 +61,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
# Update controller
num_updates = int(5.0 / DT_MDL)
for _ in range(num_updates): # Run for 5 seconds
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
# Mode should not allow lane change immediately
assert not self.alc.auto_lane_change_allowed
@@ -74,11 +74,11 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
self.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS
# Update controller once to read params
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
# Update multiple times to exceed the timer threshold
for _ in range(1): # Should exceed 0.1s with multiple DT_MDL updates
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
# Now lane change should be allowed
assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay
@@ -91,7 +91,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
self.alc.lane_change_set_timer = timer_state
# Update controller once
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
# The timer should still be below the threshold after one update
assert not self.alc.auto_lane_change_allowed
@@ -99,7 +99,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
# Update enough times to exceed the threshold (seconds / DT_MDL)
num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold
for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
# Now lane change should be allowed
assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay
@@ -116,7 +116,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
# Update with brake pressed for 1 second
for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=True)
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=True)
# Even though it is an auto lane change mode, lane change should be disallowed due to brake pressed prior initiating lane change
assert not self.alc.auto_lane_change_allowed
@@ -126,7 +126,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
# Even releasing brake shouldn't allow auto lane change
for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed
@@ -139,13 +139,13 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
self.alc.lane_change_set_timer = timer_state
# Update with blindspot detected - this should prevent auto lane change
self.alc.update_lane_change(blindspot_detected=True, brake_pressed=False)
self.alc.update_lane_change(blindspot_detected=True, edge_detected=False, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed
# Keep updating with blindspot detected - should still prevent auto lane change
num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold
for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=True, brake_pressed=False)
self.alc.update_lane_change(blindspot_detected=True, edge_detected=False, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
@@ -157,13 +157,13 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
self.alc.lane_change_set_timer = timer_state
# First update with blindspot detected to set the negative timer
self.alc.update_lane_change(blindspot_detected=True, brake_pressed=False)
self.alc.update_lane_change(blindspot_detected=True, edge_detected=False, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed
# Now update with blindspot cleared - should start incrementing timer from negative value
num_updates = int((timer_delay + abs(ONE_SECOND_DELAY)) / DT_MDL) + 1
for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
# After sufficient updates with no blindspot, auto lane change should be allowed
assert self.alc.auto_lane_change_allowed
@@ -177,7 +177,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
# Update enough times to exceed the threshold (seconds / DT_MDL)
for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
# Now lane change should be allowed
assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay
@@ -193,7 +193,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
# Update enough times to exceed the threshold (seconds / DT_MDL)
for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed
@@ -206,7 +206,86 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
# Simulate updates for a long period of time (e.g., 10 seconds)
num_updates = int(10.0 / DT_MDL)
for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
# Lane change should never be allowed
assert not self.alc.auto_lane_change_allowed
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
def test_edge_detected_delays_auto_lane_change(self, timer_state, timer_delay):
"""Test that road edge detection adds delay to auto lane change."""
self._reset_states()
self.alc.lane_change_bsm_delay = False
self.alc.lane_change_set_timer = timer_state
# Update with edge detected - should prevent auto lane change
self.alc.update_lane_change(blindspot_detected=False, edge_detected=True, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed
# Keep updating with edge detected - timer keeps resetting, should stay disallowed
num_updates = int(timer_delay / DT_MDL) + 1
for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, edge_detected=True, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
def test_edge_detected_then_cleared_allows_after_delay(self, timer_state, timer_delay):
"""Test that auto lane change is allowed after edge clears plus extra delay."""
self._reset_states()
self.alc.lane_change_bsm_delay = False
self.alc.lane_change_set_timer = timer_state
# First update with edge detected to set the negative timer
self.alc.update_lane_change(blindspot_detected=False, edge_detected=True, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed
# Now update with edge cleared - should start incrementing timer from negative value
num_updates = int((timer_delay + abs(EDGE_DELAY)) / DT_MDL) + 1
for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
# After sufficient updates with no edge, auto lane change should be allowed
assert self.alc.auto_lane_change_allowed
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
def test_edge_and_bsm_combined_uses_worst_delay(self, timer_state, timer_delay):
"""Test that when both BSM and edge are detected, the worst (longest) delay wins."""
self._reset_states()
self.alc.lane_change_bsm_delay = True
self.alc.lane_change_set_timer = timer_state
# Update with both blindspot and edge detected
self.alc.update_lane_change(blindspot_detected=True, edge_detected=True, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed
# Clear both - need to wait out the worst delay
worst_delay = max(abs(ONE_SECOND_DELAY), abs(EDGE_DELAY))
num_updates = int((timer_delay + worst_delay) / DT_MDL) + 1
for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
assert self.alc.auto_lane_change_allowed
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
def test_nudge_overrides_edge_in_lane_change_blocked(self, timer_state, timer_delay):
"""Test that lane_change_blocked is False when nudge applied with edge-only."""
self._reset_states()
self.alc.lane_change_set_timer = timer_state
# Edge detected, no nudge - should be blocked
self.alc.update_lane_change(blindspot_detected=False, edge_detected=True, brake_pressed=False, torque_applied=False)
assert self.alc.lane_change_blocked
# Edge detected, nudge applied - should NOT be blocked
self.alc.update_lane_change(blindspot_detected=False, edge_detected=True, brake_pressed=False, torque_applied=True)
assert not self.alc.lane_change_blocked
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
def test_nudge_does_not_override_bsm_in_lane_change_blocked(self, timer_state, timer_delay):
"""Test that lane_change_blocked stays True when BSM detected even with nudge."""
self._reset_states()
self.alc.lane_change_set_timer = timer_state
# BSM detected with nudge - should still be blocked
self.alc.update_lane_change(blindspot_detected=True, edge_detected=False, brake_pressed=False, torque_applied=True)
assert self.alc.lane_change_blocked
@@ -113,12 +113,56 @@ class TestDesireHelperIntegration(OpenpilotTestCase):
left_edge_detected=False, right_edge_detected=False)
assert dh.desire == expected_desire
def test_edge_blocks_lane_change(self, set_lane_turn_params):
def test_nudge_overrides_edge_detection(self, set_lane_turn_params):
"""Manual steering nudge should override road edge detection and allow lane change."""
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
carstate = DummyCarState(vEgo=15, leftBlinker=True, steeringPressed=True, steeringTorque=1)
for _ in range(10):
dh.update(carstate, True, 1.0, left_edge_detected=True, right_edge_detected=False)
assert dh.lane_change_state == LaneChangeState.preLaneChange
assert dh.lane_change_state == LaneChangeState.laneChangeStarting
assert dh.lane_change_direction == LaneChangeDirection.left
assert dh.desire == log.Desire.laneChangeLeft
def test_nudge_does_not_override_bsm(self, set_lane_turn_params):
"""Manual steering nudge must NOT override BSM blindspot detection."""
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
carstate = DummyCarState(vEgo=15, leftBlinker=True, leftBlindspot=True, steeringPressed=True, steeringTorque=1)
for _ in range(10):
dh.update(carstate, True, 1.0, left_edge_detected=False, right_edge_detected=False)
assert dh.lane_change_state == LaneChangeState.preLaneChange
assert dh.desire == log.Desire.none
def test_edge_blocks_auto_lane_change(self, set_lane_turn_params):
"""Road edge detection should still block auto (nudgeless) lane changes."""
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS
dh.alc.param_read_counter = 1
carstate = DummyCarState(vEgo=15, leftBlinker=True)
for _ in range(10):
dh.update(carstate, True, 1.0, left_edge_detected=True, right_edge_detected=False)
assert dh.lane_change_state == LaneChangeState.preLaneChange
assert dh.desire == log.Desire.none
def test_auto_lane_change_allowed_without_edge(self, set_lane_turn_params):
"""Auto lane change should proceed when no edge and no BSM detected."""
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS
dh.alc.param_read_counter = 1
carstate = DummyCarState(vEgo=15, leftBlinker=True)
for _ in range(10):
dh.update(carstate, True, 1.0, left_edge_detected=False, right_edge_detected=False)
assert dh.lane_change_state == LaneChangeState.laneChangeStarting
assert dh.desire == log.Desire.laneChangeLeft
def test_right_edge_does_not_block_left_lane_change(self, set_lane_turn_params):
"""Edge on opposite side should not affect lane change direction."""
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS
dh.alc.param_read_counter = 1
carstate = DummyCarState(vEgo=15, leftBlinker=True)
for _ in range(10):
dh.update(carstate, True, 1.0, left_edge_detected=False, right_edge_detected=True)
assert dh.lane_change_state == LaneChangeState.laneChangeStarting
assert dh.desire == log.Desire.laneChangeLeft
@@ -10,6 +10,7 @@ from openpilot.cereal import custom, messaging
from opendbc.car.structs import car
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, UNSUPPORTED_LONGITUDINAL_CAR
from opendbc.car.subaru.values import CAR as SUBARU_CAR, SubaruFlags
from opendbc.car.toyota.values import CAR as TOYOTA_CAR, ToyotaFlags
from opendbc.sunnypilot.car.interfaces import has_bsm
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
from openpilot.common.params import Params
@@ -43,6 +44,7 @@ CAPABILITY_FIELDS = (
"device_type",
"subaru_has_sng",
"hyundai_alpha_long_available",
"toyota_tss2",
)
CAPABILITY_LABELS: dict[str, str] = {
@@ -65,6 +67,7 @@ CAPABILITY_LABELS: dict[str, str] = {
"device_type": "Device type",
"subaru_has_sng": "Subaru Stop-and-Go available",
"hyundai_alpha_long_available": "Hyundai Alpha Longitudinal available",
"toyota_tss2": "Toyota TSS2 platform",
}
# Explicit defaults for non-boolean capability fields
@@ -111,6 +114,15 @@ def _resolve_brand_capabilities(caps: dict, bundle_platform: str, CP) -> None:
caps["subaru_has_sng"] = not bool(CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
caps["has_stop_and_go"] = caps["subaru_has_sng"]
elif brand == "toyota":
if bundle_platform:
try:
caps["toyota_tss2"] = bool(TOYOTA_CAR[bundle_platform].config.flags & ToyotaFlags.TSS2)
except KeyError:
cloudlog.exception(f"capabilities: unknown toyota platform {bundle_platform!r}")
elif CP is not None:
caps["toyota_tss2"] = bool(CP.flags & ToyotaFlags.TSS2)
def generate_capabilities(params: Params | None = None) -> dict:
"""Generate a SettingsCapabilities dict from CarParams + boolean params.
@@ -2161,6 +2161,26 @@
"title": "Hyundai / Kia / Genesis Settings",
"description": "",
"items": [
{
"key": "CustomButtonAction",
"widget": "multiple_button",
"title": "Steering Custom Button",
"description": "Choose the openpilot action for the steering wheel custom/star button. OEM functionality is unchanged.",
"options": [
{
"value": 0,
"label": "None"
},
{
"value": 1,
"label": "Bookmark"
},
{
"value": 3,
"label": "Cycle UI"
}
]
},
{
"key": "HyundaiLongitudinalTuning",
"widget": "multiple_button",
@@ -2324,6 +2344,33 @@
"equals": false
}
]
},
{
"key": "ToyotaTSS2Smooth",
"widget": "toggle",
"needs_onroad_cycle": true,
"title": "TSS2 Smooth Longitudinal (Alpha)",
"description": "Enable smoother longitudinal control for TSS2 vehicles. Uses a custom speed-dependent integral gain curve for smoother traffic driving.",
"enablement": [
{
"type": "not_engaged"
},
{
"type": "capability",
"field": "has_longitudinal_control",
"equals": true
},
{
"type": "param",
"key": "ToyotaEnforceStockLongitudinal",
"equals": false
},
{
"type": "capability",
"field": "toyota_tss2",
"equals": true
}
]
}
]
}
@@ -10,6 +10,17 @@ sections:
title: Hyundai / Kia / Genesis Settings
description: ''
items:
- key: CustomButtonAction
widget: multiple_button
title: Steering Custom Button
description: Choose the openpilot action for the steering wheel custom/star button. OEM functionality is unchanged.
options:
- value: 0
label: None
- value: 1
label: Bookmark
- value: 3
label: Cycle UI
- key: HyundaiLongitudinalTuning
widget: multiple_button
title: Custom Longitudinal Tuning
@@ -101,3 +112,18 @@ sections:
- type: param
key: ToyotaEnforceStockLongitudinal
equals: false
- key: ToyotaTSS2Smooth
widget: toggle
needs_onroad_cycle: true
title: TSS2 Smooth Longitudinal (Alpha)
description: Enable smoother longitudinal control for TSS2 vehicles. Uses a custom speed-dependent integral gain curve
for smoother traffic driving.
enablement:
- $ref: '#/macros/not_engaged'
- $ref: '#/macros/longitudinal'
- type: param
key: ToyotaEnforceStockLongitudinal
equals: false
- type: capability
field: toyota_tss2
equals: true
@@ -75,6 +75,12 @@ class TestOpaquePerBrandFlags(OpenpilotTestCase):
def test_hyundai_alpha_long_available_default_false(self, caps):
assert caps["hyundai_alpha_long_available"] is False
def test_toyota_tss2_field_present(self):
assert "toyota_tss2" in CAPABILITY_FIELDS
def test_toyota_tss2_default_false(self, caps):
assert caps["toyota_tss2"] is False
class TestCapabilitiesShape(OpenpilotTestCase):
def test_all_fields_present(self, caps):
@@ -207,6 +207,7 @@ class TestNotEngagedReplacement(OpenpilotTestCase):
"AlphaLongitudinalEnabled",
"ToyotaEnforceStockLongitudinal",
"ToyotaStopAndGoHack",
"ToyotaTSS2Smooth",
], names=["key"])
def test_offroad_only_replaced_with_not_engaged(self, schema, key):
"""These items should use not_engaged, not offroad_only."""
@@ -286,6 +286,7 @@ class TestKnownVehicleSettings(OpenpilotTestCase):
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("toyota"))}
assert "ToyotaEnforceStockLongitudinal" in keys
assert "ToyotaStopAndGoHack" in keys
assert "ToyotaTSS2Smooth" in keys
def test_tesla_has_coop_steering(self, schema):
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("tesla"))}
@@ -57,7 +57,7 @@ class DownloadStatusAction(ItemAction):
self.icon: str | None = None
self.icon_color: rl.Color | None = None
self._font = gui_app.font(FontWeight.NORMAL)
# raw progress arrives in steps, one per 128KB chunk the manager publishes
# raw progress arrives in steps, one per progress report from the manager
self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps)
# integrated per frame; (t * speed) % span jumps whenever the fill width changes
self._sweep = 0.0