Compare commits

..

241 Commits

Author SHA1 Message Date
royjr 7be3eca113 ui: preserve custom button presses 2026-08-16 21:02:03 -04:00
royjr 9e9918124d Merge branch 'hyundai-custom-button' into ccnc-port-custom-button 2026-08-16 20:45:36 -04:00
royjr 4621e9a560 Update opendbc_repo 2026-08-16 20:45:25 -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
63 changed files with 992 additions and 965 deletions
+184 -44
View File
@@ -7,19 +7,6 @@ on:
description: 'Minimum selector version required for the models (see helpers.py or readme.md)' description: 'Minimum selector version required for the models (see helpers.py or readme.md)'
required: true required: true
type: string type: string
target_hardware:
description: 'Hardware target to compile for (qcom or usbgpu)'
required: true
type: choice
default: 'qcom'
options:
- qcom
- usbgpu
hf_repo:
description: 'Hugging Face dataset repository'
required: false
type: string
default: 'sunnypilot/sunnypilot_models_v1'
jobs: jobs:
setup: setup:
@@ -59,14 +46,13 @@ jobs:
id: get-json id: get-json
run: | run: |
cd docs/docs cd docs/docs
PREFIX="driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_' || '' }}v" latest=$(ls driving_models_v*.json | sed -E 's/.*_v([0-9]+)\.json/\1/' | sort -n | tail -1)
latest=$(ls ${PREFIX}*.json | sed -E "s/${PREFIX}([0-9]+)\.json/\1/" | sort -n | tail -1)
next=$((latest+1)) next=$((latest+1))
json_file="${PREFIX}${next}.json" json_file="driving_models_v${next}.json"
cp "${PREFIX}${latest}.json" "$json_file" cp "driving_models_v${latest}.json" "$json_file"
echo "json_file=docs/docs/$json_file" >> $GITHUB_OUTPUT echo "json_file=docs/docs/$json_file" >> $GITHUB_OUTPUT
echo "json_version=$((next+0))" >> $GITHUB_OUTPUT echo "json_version=$((next+0))" >> $GITHUB_OUTPUT
echo "SRC_JSON_FILE=docs/docs/${PREFIX}${latest}.json" >> $GITHUB_ENV echo "SRC_JSON_FILE=docs/docs/driving_models_v${latest}.json" >> $GITHUB_ENV
- name: Extract tinygrad models - name: Extract tinygrad models
id: set-matrix id: set-matrix
@@ -75,23 +61,45 @@ jobs:
jq -c '[.bundles[] | select(.runner=="tinygrad") | {ref, display_name: (.display_name | gsub(" \\([^)]*\\)"; "")), is_20hz}]' "$(basename "${SRC_JSON_FILE}")" > matrix.json jq -c '[.bundles[] | select(.runner=="tinygrad") | {ref, display_name: (.display_name | gsub(" \\([^)]*\\)"; "")), is_20hz}]' "$(basename "${SRC_JSON_FILE}")" > matrix.json
echo "model_matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT echo "model_matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT
- name: Get next recompiled dir number - name: Set up SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}
- run: |
mkdir -p ~/.ssh
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
- name: Clone GitLab docs repo and create new recompiled dir
id: create-recompiled-dir id: create-recompiled-dir
env: env:
HF_REPO: ${{ github.event.inputs.hf_repo }} GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
run: | run: |
pip install huggingface_hub git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
recompiled_dir=$(python3 -c " cd gitlab_docs
from huggingface_hub import HfApi git checkout main
import re, sys git sparse-checkout set --no-cone models/
api = HfApi() cd models
files = api.list_repo_files(repo_id=sys.argv[1], repo_type='dataset') latest_dir=$(ls -d recompiled* 2>/dev/null | sed -E 's/recompiled([0-9]+)/\1/' | sort -n | tail -1)
dirs = [re.search(r'models/recompiled([0-9]+)', f) for f in files] if [[ -z "$latest_dir" ]]; then
nums = [int(m.group(1)) for m in dirs if m] next_dir=1
print(max(nums) + 1) else
" "$HF_REPO") next_dir=$((latest_dir+1))
fi
recompiled_dir="${next_dir}"
mkdir -p "recompiled${recompiled_dir}"
touch "recompiled${recompiled_dir}/.gitkeep"
cd ../..
echo "recompiled_dir=$recompiled_dir" >> $GITHUB_OUTPUT echo "recompiled_dir=$recompiled_dir" >> $GITHUB_OUTPUT
- name: Push empty recompiled dir to GitLab
run: |
cd gitlab_docs
git add models/recompiled${{ steps.create-recompiled-dir.outputs.recompiled_dir }}
git config --global user.name "GitHub Action"
git config --global user.email "action@github.com"
git commit -m "Add recompiled${{ steps.create-recompiled-dir.outputs.recompiled_dir }} for build-all" || echo "No changes to commit"
git push origin main
- name: Push new JSON to GitHub docs repo - name: Push new JSON to GitHub docs repo
run: | run: |
cd docs cd docs
@@ -115,30 +123,25 @@ jobs:
is_20hz: ${{ matrix.model.is_20hz }} is_20hz: ${{ matrix.model.is_20hz }}
recompiled_dir: ${{ needs.setup.outputs.recompiled_dir }} recompiled_dir: ${{ needs.setup.outputs.recompiled_dir }}
json_version: ${{ needs.setup.outputs.json_version }} json_version: ${{ needs.setup.outputs.json_version }}
target_hardware: ${{ github.event.inputs.target_hardware }}
hf_repo: ${{ github.event.inputs.hf_repo }}
set_min_version: ${{ github.event.inputs.set_min_version }}
tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }}
secrets: inherit secrets: inherit
retry_failed_models: retry_failed_models:
needs: [setup, get_and_build] needs: [setup, get_and_build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ !cancelled() && needs.setup.result == 'success' && (needs.get_and_build.result == 'success' || needs.get_and_build.result == 'failure') }} if: ${{ needs.setup.result != 'failure' && !cancelled() }}
outputs: outputs:
retry_matrix: ${{ steps.set-retry-matrix.outputs.retry_matrix }} retry_matrix: ${{ steps.set-retry-matrix.outputs.retry_matrix }}
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
pattern: artifact-name-* pattern: model-*
path: output path: output
continue-on-error: true
- id: set-retry-matrix - id: set-retry-matrix
run: | run: |
echo '${{ needs.setup.outputs.model_matrix }}' > matrix.json echo '${{ needs.setup.outputs.model_matrix }}' > matrix.json
built=(); while IFS= read -r line; do [ -n "$line" ] && built+=("$line"); done < <( built=(); while IFS= read -r line; do built+=("$line"); done < <(
find output -maxdepth 1 -name 'artifact-name-*' -printf "%f\n" 2>/dev/null | sed -E 's/^artifact-name-//' | awk '{gsub(/^ +| +$/, ""); print}' find output -maxdepth 1 -name 'model-*' -printf "%f\n" | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}'
) )
jq -c --argjson built "$(printf '%s\n' "${built[@]}" | jq -R . | jq -s .)" \ jq -c --argjson built "$(printf '%s\n' "${built[@]}" | jq -R . | jq -s .)" \
'map(select(.display_name as $n | ($built | index($n | gsub("^ +| +$"; "")) | not)))' matrix.json > retry_matrix.json 'map(select(.display_name as $n | ($built | index($n | gsub("^ +| +$"; "")) | not)))' matrix.json > retry_matrix.json
@@ -146,7 +149,7 @@ jobs:
retry_get_and_build: retry_get_and_build:
needs: [setup, get_and_build, retry_failed_models] needs: [setup, get_and_build, retry_failed_models]
if: ${{ !cancelled() && needs.retry_failed_models.result == 'success' && needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '' }} if: ${{ needs.get_and_build.result == 'failure' || (needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '') }}
strategy: strategy:
matrix: matrix:
model: ${{ fromJson(needs.retry_failed_models.outputs.retry_matrix) }} model: ${{ fromJson(needs.retry_failed_models.outputs.retry_matrix) }}
@@ -158,9 +161,146 @@ jobs:
is_20hz: ${{ matrix.model.is_20hz }} is_20hz: ${{ matrix.model.is_20hz }}
recompiled_dir: ${{ needs.setup.outputs.recompiled_dir }} recompiled_dir: ${{ needs.setup.outputs.recompiled_dir }}
json_version: ${{ needs.setup.outputs.json_version }} json_version: ${{ needs.setup.outputs.json_version }}
target_hardware: ${{ github.event.inputs.target_hardware }}
artifact_suffix: -retry artifact_suffix: -retry
hf_repo: ${{ github.event.inputs.hf_repo }}
set_min_version: ${{ github.event.inputs.set_min_version }}
tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }}
secrets: inherit secrets: inherit
publish_models:
name: Publish models sequentially
needs: [setup, get_and_build, retry_failed_models, retry_get_and_build]
if: ${{ !cancelled() && (needs.get_and_build.result != 'failure' || needs.retry_get_and_build.result == 'success' || (needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '')) }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 1
matrix:
model: ${{ fromJson(needs.setup.outputs.model_matrix) }}
env:
RECOMPILED_DIR: recompiled${{ needs.setup.outputs.recompiled_dir }}
JSON_FILE: ${{ needs.setup.outputs.json_file }}
ARTIFACT_NAME_INPUT: ${{ matrix.model.display_name }}
steps:
- name: Set up SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}
- name: Add GitLab.com SSH key to known_hosts
run: |
mkdir -p ~/.ssh
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
- name: Clone GitLab docs repo
env:
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
run: |
echo "Cloning GitLab"
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
cd gitlab_docs
echo "checkout models/${RECOMPILED_DIR}"
git sparse-checkout set --no-cone models/${RECOMPILED_DIR}
git checkout main
cd ..
- name: Checkout docs repo
uses: actions/checkout@v4
with:
repository: sunnypilot/sunnypilot-models
ref: gh-pages
path: docs
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
- name: Validate recompiled dir and JSON version
run: |
if [ ! -d "gitlab_docs/models/$RECOMPILED_DIR" ]; then
echo "Recompiled dir $RECOMPILED_DIR does not exist in GitLab repo"
exit 1
fi
if [ ! -f "$JSON_FILE" ]; then
echo "JSON file $JSON_FILE does not exist!"
exit 1
fi
- name: Download artifact name file
uses: actions/download-artifact@v4
with:
name: artifact-name-${{ env.ARTIFACT_NAME_INPUT }}
path: artifact_name
- name: Read artifact name
id: read-artifact-name
run: |
ARTIFACT_NAME=$(cat artifact_name/artifact_name.txt)
echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
- name: Download model artifact
uses: actions/download-artifact@v4
with:
name: ${{ steps.read-artifact-name.outputs.artifact_name }}
path: output
- name: Remove onnx files bc not needed for recompiled dir since they already exist from single build
run: |
find output -type f -name '*.onnx' -delete
find output -type f -name 'big_*.pkl' -delete
find output -type f -name 'dmonitoring_model_tinygrad.pkl' -delete
- name: Copy model artifacts to gitlab
env:
ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }}
run: |
ARTIFACT_DIR="gitlab_docs/models/${RECOMPILED_DIR}/${ARTIFACT_NAME}"
mkdir -p "$ARTIFACT_DIR"
for path in output/*; do
if [ "$(basename "$path")" = "artifact_name.txt" ]; then
continue
fi
name="$(basename "$path")"
if [ -d "$path" ]; then
mkdir -p "$ARTIFACT_DIR/$name"
cp -r "$path"/* "$ARTIFACT_DIR/$name/"
echo "Copied dir $name -> $ARTIFACT_DIR/$name"
else
cp "$path" "$ARTIFACT_DIR/"
echo "Copied file $name -> $ARTIFACT_DIR/"
fi
done
- name: Push recompiled dir to GitLab
env:
GITLAB_SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}
run: |
cd gitlab_docs
git checkout main
git pull origin main
for d in models/"$RECOMPILED_DIR"/*/; do
git sparse-checkout add "$d"
done
git add models/"$RECOMPILED_DIR"
git config --global user.name "GitHub Action"
git config --global user.email "action@github.com"
git commit -m "Update $RECOMPILED_DIR with model from build-all-tinygrad-models" || echo "No changes to commit"
git push origin main
- run: |
cd docs
git pull origin gh-pages
- name: update json
run: |
ARGS=""
[ -n "${{ inputs.set_min_version }}" ] && ARGS="$ARGS --set-min-version \"${{ inputs.set_min_version }}\""
ARGS="$ARGS --sort-by-date"
ARGS="$ARGS --tinygrad-ref \"${{ needs.setup.outputs.tinygrad_ref }}\""
eval python3 docs/json_parser.py \
--json-path "$JSON_FILE" \
--recompiled-dir "gitlab_docs/models/$RECOMPILED_DIR" \
$ARGS
- name: Push updated json to GitHub
run: |
cd docs
git config --global user.name "GitHub Action"
git config --global user.email "action@github.com"
git checkout gh-pages
git add docs/"$(basename $JSON_FILE)"
git commit -m "Update $(basename $JSON_FILE) after recompiling model" || echo "No changes to commit"
git push origin gh-pages
@@ -29,24 +29,11 @@ on:
required: false required: false
type: boolean type: boolean
default: true default: true
target_hardware: bypass_push:
description: 'Hardware target to compile for (qcom or usbgpu)' description: 'Bypass pushing to GitLab for build-all'
required: false required: false
type: string default: true
default: 'qcom' type: boolean
hf_repo:
description: 'Hugging Face dataset repository (e.g. sunnypilot/sunnypilot_models_v1)'
required: false
type: string
default: 'sunnypilot/sunnypilot_models_v1'
set_min_version:
description: 'Minimum selector version'
required: false
type: string
tinygrad_ref:
description: 'Tinygrad reference'
required: false
type: string
workflow_dispatch: workflow_dispatch:
inputs: inputs:
upstream_branch: upstream_branch:
@@ -78,8 +65,8 @@ on:
- None - None
- Master Models - Master Models
- Release Models - Release Models
- 2025 World Models
- 2026 World Models - 2026 World Models
- 2026 Deep RL Models
- Custom Merge Models - Custom Merge Models
- Other - Other
custom_model_folder: custom_model_folder:
@@ -94,22 +81,9 @@ on:
description: 'Minimum selector version' description: 'Minimum selector version'
required: false required: false
type: string type: string
target_hardware:
description: 'Hardware target to compile for'
required: false
type: choice
default: 'qcom'
options:
- qcom
- usbgpu
hf_repo:
description: 'Hugging Face dataset repository'
required: false
type: string
default: 'sunnypilot/sunnypilot_models_v1'
env: env:
RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }} RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }}
JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_v' || 'v' }}${{ inputs.json_version }}.json JSON_FILE: docs/docs/driving_models_v${{ inputs.json_version }}.json
jobs: jobs:
build_model: build_model:
@@ -119,20 +93,38 @@ jobs:
custom_name: ${{ inputs.custom_name || inputs.upstream_branch }} custom_name: ${{ inputs.custom_name || inputs.upstream_branch }}
is_20hz: ${{ inputs.is_20hz }} is_20hz: ${{ inputs.is_20hz }}
artifact_suffix: ${{ inputs.artifact_suffix }} artifact_suffix: ${{ inputs.artifact_suffix }}
target_hardware: ${{ inputs.target_hardware }}
secrets: inherit secrets: inherit
publish_model: publish_model:
if: ${{ !cancelled() && needs.build_model.result == 'success' }} if: ${{ !inputs.bypass_push && !cancelled() }}
concurrency: concurrency:
group: hf-push-${{ inputs.recompiled_dir }} group: gitlab-push-${{ inputs.recompiled_dir }}
cancel-in-progress: false cancel-in-progress: false
needs: build_model needs: build_model
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
steps: steps:
- name: Set up SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}
- name: Add GitLab.com SSH key to known_hosts
run: |
mkdir -p ~/.ssh
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
- name: Clone GitLab docs repo
env:
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
run: |
echo "Cloning GitLab"
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
cd gitlab_docs
echo "checkout models/${RECOMPILED_DIR}"
git sparse-checkout set --no-cone models/${RECOMPILED_DIR}
git checkout main
cd ..
- name: Checkout docs repo - name: Checkout docs repo
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@@ -141,28 +133,16 @@ jobs:
path: docs path: docs
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
- name: Install huggingface_hub - name: Validate recompiled dir and JSON version
run: pip install --upgrade "huggingface_hub>=0.22.0"
- name: Validate hf_repo and JSON version
env:
HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }}
run: | run: |
if [ ! -d "gitlab_docs/models/$RECOMPILED_DIR" ]; then
echo "Recompiled dir $RECOMPILED_DIR does not exist in GitLab repo"
exit 1
fi
if [ ! -f "$JSON_FILE" ]; then if [ ! -f "$JSON_FILE" ]; then
echo "JSON file $JSON_FILE does not exist!" echo "JSON file $JSON_FILE does not exist!"
exit 1 exit 1
fi fi
python3 -c "
import sys
from huggingface_hub import HfApi
try:
api = HfApi()
api.repo_info(repo_id=sys.argv[1], repo_type='dataset')
print(f'Success: Repo {sys.argv[1]} exists.')
except Exception as e:
print('HF validation failed:', e)
sys.exit(1)
" "${{ inputs.hf_repo }}"
- name: Download artifact name file - name: Download artifact name file
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
@@ -182,26 +162,49 @@ jobs:
name: ${{ steps.read-artifact-name.outputs.artifact_name }} name: ${{ steps.read-artifact-name.outputs.artifact_name }}
path: output path: output
- name: Create models folder - name: Remove unwanted files
run: |
find output -type f -name 'dmonitoring_model_tinygrad.pkl' -delete
find output -type f -name 'dmonitoring_model.onnx' -delete
- name: Copy model artifact(s) to GitLab recompiled dir
env: env:
ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }} ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }}
run: | run: |
mkdir -p "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}" ARTIFACT_DIR="gitlab_docs/models/${RECOMPILED_DIR}/${ARTIFACT_NAME}"
cp -r output/* "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/" mkdir -p "$ARTIFACT_DIR"
rm -f "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/artifact_name.txt" for path in output/*; do
if [ "$(basename "$path")" = "artifact_name.txt" ]; then
continue
fi
name="$(basename "$path")"
if [ -d "$path" ]; then
mkdir -p "$ARTIFACT_DIR/$name"
cp -r "$path"/* "$ARTIFACT_DIR/$name/"
echo "Copied dir $name -> $ARTIFACT_DIR/$name"
else
cp "$path" "$ARTIFACT_DIR/"
echo "Copied file $name -> $ARTIFACT_DIR/"
fi
done
- name: Upload to Hugging Face - name: Push recompiled dir to GitLab
env: env:
HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }} GITLAB_SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}
ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }}
run: | run: |
hf upload ${{ inputs.hf_repo }} \ cd gitlab_docs
output/ \ git checkout main
"models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/" \ git pull origin main
--repo-type=dataset for d in models/"$RECOMPILED_DIR"/*/; do
git sparse-checkout add "$d"
done
git add models/"$RECOMPILED_DIR"
git config --global user.name "GitHub Action"
git config --global user.email "action@github.com"
git commit -m "Create/Update $RECOMPILED_DIR with new/updated model from build-single-tinygrad-model" || echo "No changes to commit"
git push origin main
- name: Pull gh-pages - run: |
run: |
cd docs cd docs
git pull origin gh-pages git pull origin gh-pages
@@ -217,11 +220,9 @@ jobs:
fi fi
[ -n "${{ inputs.generation }}" ] && ARGS="$ARGS --generation \"${{ inputs.generation }}\"" [ -n "${{ inputs.generation }}" ] && ARGS="$ARGS --generation \"${{ inputs.generation }}\""
[ -n "${{ inputs.version }}" ] && ARGS="$ARGS --version \"${{ inputs.version }}\"" [ -n "${{ inputs.version }}" ] && ARGS="$ARGS --version \"${{ inputs.version }}\""
[ -n "${{ inputs.set_min_version }}" ] && ARGS="$ARGS --set-min-version \"${{ inputs.set_min_version }}\""
[ -n "${{ inputs.tinygrad_ref }}" ] && ARGS="$ARGS --tinygrad-ref \"${{ inputs.tinygrad_ref }}\""
eval python3 docs/json_parser.py \ eval python3 docs/json_parser.py \
--json-path "$JSON_FILE" \ --json-path "$JSON_FILE" \
--recompiled-dir "local_models/$RECOMPILED_DIR" \ --recompiled-dir "gitlab_docs/models/$RECOMPILED_DIR" \
--sort-by-date \ --sort-by-date \
$ARGS $ARGS
+24 -14
View File
@@ -80,7 +80,6 @@ jobs:
with: with:
repository: commaai/openpilot repository: commaai/openpilot
ref: ${{ inputs.upstream_branch }} ref: ${{ inputs.upstream_branch }}
fetch-depth: 1
submodules: recursive submodules: recursive
path: openpilot path: openpilot
@@ -90,25 +89,18 @@ jobs:
with: with:
repository: sunnypilot/sunnypilot repository: sunnypilot/sunnypilot
ref: ${{ inputs.upstream_branch }} ref: ${{ inputs.upstream_branch }}
fetch-depth: 1
submodules: recursive submodules: recursive
path: openpilot path: openpilot
- name: Get commit date - name: Get commit date
id: commit-date id: commit-date
run: | run: |
cd ${{ github.workspace }}/openpilot/openpilot cd ${{ github.workspace }}/openpilot
commit_date=$(git log -1 --format=%cd --date=format:'%B %d, %Y') commit_date=$(git log -1 --format=%cd --date=format:'%B %d, %Y')
echo "model_date=${commit_date}" >> $GITHUB_OUTPUT echo "model_date=${commit_date}" >> $GITHUB_OUTPUT
cat $GITHUB_OUTPUT cat $GITHUB_OUTPUT
- run: | - run: |
cd ${{ github.workspace }}/openpilot/openpilot cd ${{ github.workspace }}/openpilot
if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; then git lfs pull
git lfs pull -X "selfdrive/modeld/models/big_*.onnx" -X "selfdrive/modeld/models/dmonitoring_*.onnx"
rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx
else
git lfs pull -I "selfdrive/modeld/models/big_*.onnx"
find selfdrive/modeld/models -name "*.onnx" ! -name "big_*.onnx" -delete
fi
- name: 'Upload Artifact' - name: 'Upload Artifact'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@@ -124,10 +116,24 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 1
submodules: recursive submodules: recursive
- run: git lfs pull - run: git lfs pull
- name: Cache SCons
uses: actions/cache@v4
with:
path: ${{env.SCONS_CACHE_DIR}}
key: scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model-${{ github.sha }}
# Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.)
# for security. Only caches from the default branch are shared across all builds. This is by design and cannot be overridden.
restore-keys: |
scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model
scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}
scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_NEW_BRANCH }}-model
scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_BRANCH }}-model
scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_NEW_BRANCH }}
scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_BRANCH }}
scons-${{ runner.os }}-${{ runner.arch }}
- name: Set environment variables - name: Set environment variables
id: set-env id: set-env
@@ -138,7 +144,7 @@ jobs:
export UV_PYTHON_PREFERENCE=managed export UV_PYTHON_PREFERENCE=managed
export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
uv sync --frozen uv sync
printenv >> $GITHUB_ENV printenv >> $GITHUB_ENV
if [[ "${{ runner.debug }}" == "1" ]]; then if [[ "${{ runner.debug }}" == "1" ]]; then
cat $GITHUB_OUTPUT cat $GITHUB_OUTPUT
@@ -167,6 +173,8 @@ jobs:
with: with:
name: models-${{ env.REF }}${{ inputs.artifact_suffix }} name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
path: ${{ env.MODELS_DIR }} path: ${{ env.MODELS_DIR }}
- run: |
rm -f ${{ env.MODELS_DIR }}/{dmonitoring_model,big_driving_policy,big_driving_vision,big_driving_supercombo}.onnx
- name: Build Model - name: Build Model
run: | run: |
@@ -183,7 +191,7 @@ jobs:
if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then
echo "USBGPU build" echo "USBGPU build"
export USBGPU=1 export USBGPU=1
TG_FLAGS="DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2" TG_FLAGS="DEV=AMD USBGPU=1 IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl" OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
else else
echo "QCOM build" echo "QCOM build"
@@ -246,8 +254,10 @@ jobs:
# Copy the model files # Copy the model files
rsync -avm \ rsync -avm \
--include='*.dlc' \ --include='*.dlc' \
--include='*.pkl' \
--include='*.chunk*' \ --include='*.chunk*' \
--include='*.chunkmanifest' \ --include='*.chunkmanifest' \
--include='*.onnx' \
--exclude='*' \ --exclude='*' \
--delete-excluded \ --delete-excluded \
--chown=comma:comma \ --chown=comma:comma \
@@ -4,8 +4,12 @@ env:
BUILD_DIR: "/data/openpilot" BUILD_DIR: "/data/openpilot"
OUTPUT_DIR: ${{ github.workspace }}/output OUTPUT_DIR: ${{ github.workspace }}/output
CI_DIR: ${{ github.workspace }}/release/ci CI_DIR: ${{ github.workspace }}/release/ci
SCONS_CACHE_DIR: ${{ github.workspace }}/release/ci/scons_cache
PUBLIC_REPO_URL: "https://github.com/sunnypilot/sunnypilot" PUBLIC_REPO_URL: "https://github.com/sunnypilot/sunnypilot"
# Branch configurations
STAGING_SOURCE_BRANCH: 'master'
# Runtime configuration # Runtime configuration
SOURCE_BRANCH: "${{ github.head_ref || github.ref_name }}" SOURCE_BRANCH: "${{ github.head_ref || github.ref_name }}"
@@ -105,6 +109,11 @@ jobs:
group: build-${{ github.head_ref || github.ref_name }} group: build-${{ github.head_ref || github.ref_name }}
cancel-in-progress: false cancel-in-progress: false
runs-on: [self-hosted, tici] runs-on: [self-hosted, tici]
outputs:
new_branch: ${{ needs.prepare_strategy.outputs.new_branch }}
version: ${{ needs.prepare_strategy.outputs.version }}
extra_version_identifier: ${{ needs.prepare_strategy.outputs.extra_version_identifier }}
commit_sha: ${{ github.sha }}
if: ${{ if: ${{
(always() && !cancelled() && !failure()) && (always() && !cancelled() && !failure()) &&
needs.prepare_strategy.result == 'success' && needs.prepare_strategy.result == 'success' &&
@@ -120,8 +129,26 @@ jobs:
repository: ${{ github.event.pull_request.head.repo.fork && github.event.pull_request.head.repo.full_name || github.repository }} repository: ${{ github.event.pull_request.head.repo.fork && github.event.pull_request.head.repo.full_name || github.repository }}
- run: git lfs pull - run: git lfs pull
- name: Cache SCons
uses: actions/cache@v4
with:
path: ${{env.SCONS_CACHE_DIR}}
key: scons-${{ runner.os }}-${{ runner.arch }}-${{ env.SOURCE_BRANCH }}-${{ github.sha }}
# Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.)
# for security. Only caches from the default branch are shared across all builds. This is by design and cannot be overridden.
restore-keys: |
scons-${{ runner.os }}-${{ runner.arch }}-${{ env.SOURCE_BRANCH }}
scons-${{ runner.os }}-${{ runner.arch }}-${{ env.STAGING_SOURCE_BRANCH }}
scons-${{ runner.os }}-${{ runner.arch }}
- name: Set environment variables - name: Set environment variables
id: set-env
run: | run: |
echo "new_branch=${{ needs.prepare_strategy.outputs.new_branch }}" >> $GITHUB_OUTPUT
echo "version=${{ needs.prepare_strategy.outputs.version }}" >> $GITHUB_OUTPUT
echo "extra_version_identifier=${{ needs.prepare_strategy.outputs.extra_version_identifier }}" >> $GITHUB_OUTPUT
echo "commit_sha=${{ github.sha }}" >> $GITHUB_OUTPUT
# Set up common environment # Set up common environment
source /etc/profile; source /etc/profile;
export UV_PROJECT_ENVIRONMENT=${HOME}/venv export UV_PROJECT_ENVIRONMENT=${HOME}/venv
@@ -130,6 +157,9 @@ jobs:
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
uv sync uv sync
printenv >> $GITHUB_ENV printenv >> $GITHUB_ENV
if [[ "${{ runner.debug }}" == "1" ]]; then
cat $GITHUB_OUTPUT
fi
- name: Setup build environment - name: Setup build environment
run: | run: |
@@ -138,7 +168,7 @@ jobs:
echo "Starting build stage..." echo "Starting build stage..."
echo "BUILD_DIR: ${BUILD_DIR}" echo "BUILD_DIR: ${BUILD_DIR}"
echo "CI_DIR: ${CI_DIR}" echo "CI_DIR: ${CI_DIR}"
echo "VERSION: ${{ needs.prepare_strategy.outputs.version }}" echo "VERSION: ${{ steps.set-env.outputs.version }}"
echo "UV_PROJECT_ENVIRONMENT: ${UV_PROJECT_ENVIRONMENT}" echo "UV_PROJECT_ENVIRONMENT: ${UV_PROJECT_ENVIRONMENT}"
echo "VIRTUAL_ENV: ${VIRTUAL_ENV}" echo "VIRTUAL_ENV: ${VIRTUAL_ENV}"
echo "-------" echo "-------"
@@ -150,44 +180,61 @@ jobs:
- name: Build Main Project - name: Build Main Project
run: | run: |
export PYTHONPATH="$BUILD_DIR:$BUILD_DIR/msgq_repo:$BUILD_DIR/opendbc_repo:$BUILD_DIR/rednose_repo:$BUILD_DIR/teleoprtc_repo:$BUILD_DIR/tinygrad_repo" export PYTHONPATH="$BUILD_DIR"
./tools/release/release_files.py | xargs -0 cp -pR --parents -t "$BUILD_DIR" -- ./tools/release/release_files.py | sort | uniq | rsync -rRl${RUNNER_DEBUG:+v} --files-from=- . $BUILD_DIR/
# outside the checkout, which is wiped each run. /data/scons_cache is the device's, not ours.
SCONS_CACHE="$RUNNER_WORKSPACE/scons_cache"
mkdir -p "$SCONS_CACHE"
cd $BUILD_DIR cd $BUILD_DIR
echo "Building locationd..." ln -sfn msgq_repo/msgq msgq
# -j1: parallel rednose generators OOM the device ln -sfn opendbc_repo/opendbc opendbc
scons -j1 cache_dir="$SCONS_CACHE" --minimal \ ln -sfn rednose_repo/rednose rednose
openpilot/selfdrive/locationd openpilot/sunnypilot/selfdrive/locationd ln -sfn teleoprtc_repo/teleoprtc teleoprtc
ln -sfn tinygrad_repo/tinygrad tinygrad
sed -i '/from .board.jungle import PandaJungle, PandaJungleDFU/s/^/#/' panda/__init__.py
echo "Building sunnypilot's modeld_v2..."
scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal openpilot/sunnypilot/modeld_v2
echo "Building sunnypilot's locationd..."
scons -j2 cache_dir=${{env.SCONS_CACHE_DIR}} --minimal openpilot/sunnypilot/selfdrive/locationd
echo "Building openpilot's locationd..."
scons -j1 cache_dir=${{env.SCONS_CACHE_DIR}} --minimal openpilot/selfdrive/locationd
echo "Building rest of sunnypilot" echo "Building rest of sunnypilot"
/usr/bin/time -v scons -j$(nproc) cache_dir="$SCONS_CACHE" --minimal scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal
touch ${BUILD_DIR}/prebuilt touch ${BUILD_DIR}/prebuilt
if [[ "${{ runner.debug }}" == "1" ]]; then if [[ "${{ runner.debug }}" == "1" ]]; then
ls -la ${BUILD_DIR} ls -la ${BUILD_DIR}
fi fi
- name: Strip release tree - name: Prepare Output
run: | run: |
cd $BUILD_DIR sudo rm -rf ${OUTPUT_DIR}
find . -name '*.a' -delete mkdir -p ${OUTPUT_DIR}
find . -name '*.o' -delete rsync -am${RUNNER_DEBUG:+v} \
find . -name '*.os' -delete --exclude='.sconsign.dblite' \
find . -name '*.pyc' -delete --exclude='*.a' \
find . -name 'moc_*' -delete --exclude='*.o' \
find . -name '__pycache__' -type d -exec rm -rf {} + --exclude='*.os' \
find . -name 'SConstruct' -delete --exclude='*.pyc' \
find . -name 'SConscript' -delete --exclude='moc_*' \
rm -rf .sconsign.dblite Jenkinsfile tools/release/ release/ --exclude='__pycache__' \
rm -f openpilot/selfdrive/modeld/models/*.onnx* --exclude='Jenkinsfile' \
rm -f openpilot/sunnypilot/modeld*/models/*.onnx* --exclude='**/release/' \
find openpilot/third_party/ -name '*x86*' -exec rm -r {} + --exclude='**/.github/' \
find openpilot/third_party/ -name '*Darwin*' -exec rm -r {} + --exclude='**/openpilot/selfdrive/ui/replay/' \
cd - --exclude='**/__pycache__/' \
--exclude='${{env.SCONS_CACHE_DIR}}' \
--exclude='**/.git/' \
--exclude='**/SConstruct' \
--exclude='**/SConscript' \
--exclude='**/.venv/' \
--exclude='openpilot/selfdrive/modeld/models/*.onnx*' \
--exclude='openpilot/sunnypilot/modeld*/models/*.onnx*' \
--exclude='openpilot/third_party/*x86*' \
--exclude='openpilot/third_party/*Darwin*' \
--delete-excluded \
--chown=comma:comma \
${BUILD_DIR}/ ${OUTPUT_DIR}/
- name: 'Tar.gz files' - name: 'Tar.gz files'
run: | run: |
tar czf prebuilt.tar.gz -C ${{ env.BUILD_DIR }} . tar czf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }} .
ls -la prebuilt.tar.gz ls -la prebuilt.tar.gz
- name: 'Upload Artifact' - name: 'Upload Artifact'
@@ -195,7 +242,6 @@ jobs:
with: with:
name: prebuilt name: prebuilt
path: prebuilt.tar.gz path: prebuilt.tar.gz
compression-level: 0
- name: Re-enable powersave - name: Re-enable powersave
if: always() if: always()
@@ -237,22 +283,29 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
echo '${{ toJSON(needs.prepare_strategy.outputs) }}' echo '${{ toJSON(needs.build.outputs) }}'
ls -la ${{ env.OUTPUT_DIR }} ls -la ${{ env.OUTPUT_DIR }}
${{ env.CI_DIR }}/publish.sh \ ${{ env.CI_DIR }}/publish.sh \
"${{ github.workspace }}" \ "${{ github.workspace }}" \
"${{ env.OUTPUT_DIR }}" \ "${{ env.OUTPUT_DIR }}" \
"${{ needs.prepare_strategy.outputs.new_branch }}" \ "${{ needs.build.outputs.new_branch }}" \
"${{ needs.prepare_strategy.outputs.version }}" \ "${{ needs.build.outputs.version }}" \
"https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \ "https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \
"${{ needs.prepare_strategy.outputs.extra_version_identifier }}" "${{ needs.build.outputs.extra_version_identifier }}"
echo ""
echo "---- ️ To update the list of branches that auto deploy prebuilts -----"
echo ""
echo "1. Go to: ${{ github.server_url }}/${{ github.repository }}/settings/variables/actions/AUTO_DEPLOY_PREBUILT_BRANCHES"
echo "2. Current value: ${{ vars.AUTO_DEPLOY_PREBUILT_BRANCHES }}"
echo "3. Update as needed (JSON array with no spaces)"
- name: Tag ${{ needs.prepare_strategy.outputs.environment }} - name: Tag ${{ needs.prepare_strategy.outputs.environment }}
if: ${{ needs.prepare_strategy.outputs.is_stable_branch == 'true' && (github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')) }} if: ${{ needs.prepare_strategy.outputs.is_stable_branch == 'true' && (github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')) }}
run: | run: |
TAG="${{ needs.prepare_strategy.outputs.environment }}/${{ needs.prepare_strategy.outputs.version }}/${{ needs.prepare_strategy.outputs.build }}" TAG="${{ needs.prepare_strategy.outputs.environment }}/${{ needs.prepare_strategy.outputs.version }}/${{ needs.prepare_strategy.outputs.build }}"
git tag -f -a ${TAG} -m "${{ needs.prepare_strategy.outputs.environment }} @ ${{ needs.prepare_strategy.outputs.version }} of build ${{ needs.prepare_strategy.outputs.build }}." git tag -f -a ${TAG} -m "${{ needs.prepare_strategy.outputs.environment }} @ ${{ needs.prepare_strategy.outputs.version }} of build ${{ needs.build.outputs.build }}."
git push -f origin ${TAG} git push -f origin ${TAG}
notify: notify:
@@ -271,6 +324,7 @@ jobs:
- name: Prepare notification message - name: Prepare notification message
id: message id: message
run: | run: |
TEMPLATE='${{ vars.DISCOURSE_GENERAL_UPDATE_NOTICE }}'
export VERSION="${{ needs.prepare_strategy.outputs.version }}" export VERSION="${{ needs.prepare_strategy.outputs.version }}"
export branch_name="${{ env.SOURCE_BRANCH }}" export branch_name="${{ env.SOURCE_BRANCH }}"
export new_branch="${{ needs.prepare_strategy.outputs.new_branch }}" export new_branch="${{ needs.prepare_strategy.outputs.new_branch }}"
@@ -319,7 +373,7 @@ jobs:
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
issue_number: prNumber, issue_number: prNumber,
name: process.env.LABEL name: process.env.LABELf
}); });
console.log(`Removed '${process.env.LABEL}' label from PR #${prNumber}`); console.log(`Removed '${process.env.LABEL}' label from PR #${prNumber}`);
+1 -2
View File
@@ -25,8 +25,7 @@ env:
jobs: jobs:
preview: preview:
if: false # tmp disable due to GH API rate limiting flakiness if: github.repository == 'sunnypilot/sunnypilot'
#if: github.repository == 'sunnypilot/sunnypilot'
name: preview name: preview
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 timeout-minutes: 20
Vendored
-1
View File
@@ -22,7 +22,6 @@ shopt -s huponexit # kill all child processes when the shell exits
export CI=1 export CI=1
export PYTHONWARNINGS=error export PYTHONWARNINGS=error
export COMMA_CACHE=/data/tmp/comma_download_cache
#export LOGPRINT=debug # this has gotten too spammy... #export LOGPRINT=debug # this has gotten too spammy...
export TEST_DIR=${env.TEST_DIR} export TEST_DIR=${env.TEST_DIR}
export SOURCE_DIR=${env.SOURCE_DIR} export SOURCE_DIR=${env.SOURCE_DIR}
+11 -14
View File
@@ -4,7 +4,7 @@
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
# 345 Supported Cars # 342 Supported Cars
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|Video|Setup Video| |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|Video|Setup Video|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
@@ -34,7 +34,6 @@ A supported vehicle is one that just works when you install a comma device. All
|Chrysler|Pacifica Hybrid 2019-25|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica Hybrid 2019-25">Buy Here</a></sub></details>||| |Chrysler|Pacifica Hybrid 2019-25|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica Hybrid 2019-25">Buy Here</a></sub></details>|||
|comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None|<a href="https://youtu.be/VT-i3yRsX2s?t=2736" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None|<a href="https://youtu.be/VT-i3yRsX2s?t=2736" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|CUPRA[<sup>12</sup>](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Ateca 2018-23">Buy Here</a></sub></details>||| |CUPRA[<sup>12</sup>](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Ateca 2018-23">Buy Here</a></sub></details>|||
|CUPRA[<sup>12</sup>](#footnotes)|Born 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Born 2021-23">Buy Here</a></sub></details>|||
|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Dodge Durango 2020-21">Buy Here</a></sub></details>||| |Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Dodge Durango 2020-21">Buy Here</a></sub></details>|||
|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Bronco Sport 2021-24">Buy Here</a></sub></details>||| |Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Bronco Sport 2021-24">Buy Here</a></sub></details>|||
|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2020-22">Buy Here</a></sub></details>||| |Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2020-22">Buy Here</a></sub></details>|||
@@ -83,7 +82,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Honda|City (Brazil only) 2023-25|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|14 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda City (Brazil only) 2023-25">Buy Here</a></sub></details>||| |Honda|City (Brazil only) 2023-25|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|14 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda City (Brazil only) 2023-25">Buy Here</a></sub></details>|||
|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2016-18">Buy Here</a></sub></details>|<a href="https://youtu.be/-IkImTe1NYE" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2016-18">Buy Here</a></sub></details>|<a href="https://youtu.be/-IkImTe1NYE" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Honda|Civic 2019-21|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|2 mph[<sup>4</sup>](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=4Iz1Mz5LGF8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Honda|Civic 2019-21|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|2 mph[<sup>4</sup>](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=4Iz1Mz5LGF8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Honda|Civic 2022-26|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2022-26">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Honda|Civic 2022-24|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2017-18">Buy Here</a></sub></details>||| |Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2017-18">Buy Here</a></sub></details>|||
|Honda|Civic Hatchback 2019-21|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2019-21">Buy Here</a></sub></details>||| |Honda|Civic Hatchback 2019-21|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2019-21">Buy Here</a></sub></details>|||
|Honda|Civic Hatchback 2022-24|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Honda|Civic Hatchback 2022-24|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
@@ -159,7 +158,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Hyundai|Tucson Plug-in Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Tucson Plug-in Hybrid 2024">Buy Here</a></sub></details>||| |Hyundai|Tucson Plug-in Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Tucson Plug-in Hybrid 2024">Buy Here</a></sub></details>|||
|Hyundai|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Veloster 2019-20">Buy Here</a></sub></details>||| |Hyundai|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Veloster 2019-20">Buy Here</a></sub></details>|||
|Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Jeep Grand Cherokee 2016-18">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=eLR9o2JkuRk" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Jeep Grand Cherokee 2016-18">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=eLR9o2JkuRk" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Jeep Grand Cherokee 2019-21">Buy Here</a></sub></details>||| |Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Jeep Grand Cherokee 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=jBe4lWnRSu4" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Kia|Carnival 2022-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Carnival 2022-24">Buy Here</a></sub></details>||| |Kia|Carnival 2022-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Carnival 2022-24">Buy Here</a></sub></details>|||
|Kia|Carnival (China only) 2023|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Carnival (China only) 2023">Buy Here</a></sub></details>||| |Kia|Carnival (China only) 2023|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Carnival (China only) 2023">Buy Here</a></sub></details>|||
|Kia|Ceed 2019-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Ceed 2019-21">Buy Here</a></sub></details>||| |Kia|Ceed 2019-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Ceed 2019-21">Buy Here</a></sub></details>|||
@@ -243,20 +242,20 @@ A supported vehicle is one that just works when you install a comma device. All
|Rivian|R1T 2025|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2025">Buy Here</a></sub></details>||| |Rivian|R1T 2025|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2025">Buy Here</a></sub></details>|||
|SEAT[<sup>12</sup>](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Ateca 2016-23">Buy Here</a></sub></details>||| |SEAT[<sup>12</sup>](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Ateca 2016-23">Buy Here</a></sub></details>|||
|SEAT[<sup>12</sup>](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Leon 2014-20">Buy Here</a></sub></details>||| |SEAT[<sup>12</sup>](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Leon 2014-20">Buy Here</a></sub></details>|||
|Subaru|Ascent 2019-21|All[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>||| |Subaru|Ascent 2019-21|All[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2020-23">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>||| |Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2020-23">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Forester 2017-18|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2017-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>||| |Subaru|Forester 2017-18|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2017-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Forester 2019-21|All[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>||| |Subaru|Forester 2019-21|All[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Impreza 2017-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2017-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>||| |Subaru|Impreza 2017-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2017-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Impreza 2020-22|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>||| |Subaru|Impreza 2020-22|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Legacy 2015-18|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2015-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>||| |Subaru|Legacy 2015-18|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2015-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Legacy 2020-22|All[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>||| |Subaru|Legacy 2020-22|All[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Outback 2015-17|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2015-17">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>||| |Subaru|Outback 2015-17|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2015-17">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Outback 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>||| |Subaru|Outback 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Outback 2020-22|All[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>||| |Subaru|Outback 2020-22|All[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2020-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>||| |Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2020-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Škoda|Fabia 2022-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Fabia 2022-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)||| |Škoda|Fabia 2022-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Fabia 2022-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|Škoda|Kamiq 2021-23[<sup>13,15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Kamiq 2021-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)||| |Škoda|Kamiq 2021-23[<sup>13,15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Kamiq 2021-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|Škoda[<sup>12</sup>](#footnotes)|Karoq 2019-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Karoq 2019-23">Buy Here</a></sub></details>||| |Škoda[<sup>12</sup>](#footnotes)|Karoq 2019-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Karoq 2019-23">Buy Here</a></sub></details>|||
@@ -335,8 +334,6 @@ A supported vehicle is one that just works when you install a comma device. All
|Volkswagen[<sup>12</sup>](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf R 2015-19">Buy Here</a></sub></details>||| |Volkswagen[<sup>12</sup>](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf R 2015-19">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf SportsVan 2015-20">Buy Here</a></sub></details>||| |Volkswagen[<sup>12</sup>](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf SportsVan 2015-20">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Grand California 2019-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|| |Volkswagen[<sup>12</sup>](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Grand California 2019-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Volkswagen[<sup>12</sup>](#footnotes)|ID.4 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen ID.4 2021-23">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|ID.4 2024-25|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen ID.4 2024-25">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta 2019-23">Buy Here</a></sub></details>||| |Volkswagen[<sup>12</sup>](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta 2019-23">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta GLI 2021-23">Buy Here</a></sub></details>||| |Volkswagen[<sup>12</sup>](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta GLI 2021-23">Buy Here</a></sub></details>|||
|Volkswagen|Passat 2015-22[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat 2015-22">Buy Here</a></sub></details>||| |Volkswagen|Passat 2015-22[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat 2015-22">Buy Here</a></sub></details>|||
+1 -1
View File
@@ -15,7 +15,7 @@ public:
static std::string get_serial() { return "cccccc"; } static std::string get_serial() { return "cccccc"; }
static std::map<std::string, std::string> get_init_logs(bool route_log = false) { static std::map<std::string, std::string> get_init_logs() {
return {}; return {};
} }
+7 -9
View File
@@ -59,7 +59,7 @@ public:
std::ofstream("/sys/class/leds/led:switch_2/brightness") << value << "\n"; std::ofstream("/sys/class/leds/led:switch_2/brightness") << value << "\n";
} }
static std::map<std::string, std::string> get_init_logs(bool route_log = false) { static std::map<std::string, std::string> get_init_logs() {
std::map<std::string, std::string> ret = { std::map<std::string, std::string> ret = {
{"/BUILD", util::read_file("/BUILD")}, {"/BUILD", util::read_file("/BUILD")},
{"lsblk", util::check_output("lsblk -o NAME,SIZE,STATE,VENDOR,MODEL,REV,SERIAL")}, {"lsblk", util::check_output("lsblk -o NAME,SIZE,STATE,VENDOR,MODEL,REV,SERIAL")},
@@ -73,14 +73,12 @@ public:
temp.erase(temp.find_last_not_of(std::string("\0\r\n", 3))+1); temp.erase(temp.find_last_not_of(std::string("\0\r\n", 3))+1);
ret["boot temp"] = temp; ret["boot temp"] = temp;
// TODO: these are too slow to do on route log inits. need to do it async? // TODO: log something from system and boot
if (!route_log) { for (std::string part : {"xbl", "abl", "aop", "devcfg", "xbl_config"}) {
for (std::string part : {"xbl", "abl", "aop", "devcfg", "xbl_config"}) { for (std::string slot : {"a", "b"}) {
for (std::string slot : {"a", "b"}) { std::string partition = part + "_" + slot;
std::string partition = part + "_" + slot; std::string hash = util::check_output("sha256sum /dev/disk/by-partlabel/" + partition);
std::string hash = util::check_output("sha256sum /dev/disk/by-partlabel/" + partition); ret[partition] = hash.substr(0, hash.find_first_of(" "));
ret[partition] = hash.substr(0, hash.find_first_of(" "));
}
} }
} }
+2 -4
View File
@@ -91,10 +91,10 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ObdMultiplexingChanged", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"ObdMultiplexingChanged", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ExcessiveActuation", {PERSISTENT, JSON}}, {"Offroad_ExcessiveActuation", {PERSISTENT, JSON}},
{"Offroad_NeosUpdate", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_NoFirmware", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_NoFirmware", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_Recalibration", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_Recalibration", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_TemperatureTooHigh", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_TemperatureTooHigh", {CLEAR_ON_MANAGER_START, JSON}},
@@ -186,6 +186,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}}, {"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}}, {"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
{"CustomButtonAction", {PERSISTENT | BACKUP, INT, "0"}},
// MADS params // MADS params
{"Mads", {PERSISTENT | BACKUP, BOOL, "1"}}, {"Mads", {PERSISTENT | BACKUP, BOOL, "1"}},
@@ -195,14 +196,11 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
// Model Manager params // Model Manager params
{"ModelManager_ActiveBundle", {PERSISTENT, JSON}}, {"ModelManager_ActiveBundle", {PERSISTENT, JSON}},
{"ModelManager_ActiveJson", {CLEAR_ON_MANAGER_START, STRING}},
{"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}}, {"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}},
{"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT}}, {"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT}},
{"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}}, {"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}},
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, {"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_LastSyncTime_USBGPU", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}}, {"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
{"ModelManager_ModelsCache_USBGPU", {PERSISTENT | BACKUP, JSON}},
// Neural Network Lateral Control // Neural Network Lateral Control
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}}, {"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:190e196eba6feffec125ac66cf7e77620b759e346fa69b80e3a3884a5694cb15 oid sha256:c4c38772e6080aa4b8bf5212d3619e949775468c64f3edb88a1a426d767c38d2
size 3225 size 1579
@@ -44,7 +44,8 @@ class LongControl:
self.CP = CP self.CP = CP
self.CP_SP = CP_SP self.CP_SP = CP_SP
self.long_control_state = LongCtrlState.off self.long_control_state = LongCtrlState.off
self.pid = PIDController(0.0, (CP.longitudinalTuning.kiBP, CP.longitudinalTuning.kiV), self.pid = PIDController((CP.longitudinalTuning.kpBP, CP.longitudinalTuning.kpV),
(CP.longitudinalTuning.kiBP, CP.longitudinalTuning.kiV),
rate=1 / DT_CTRL) rate=1 / DT_CTRL)
self.last_output_accel = 0.0 self.last_output_accel = 0.0
@@ -35,7 +35,6 @@ build_files = [f'{gen}/acados_solver_long.c'] + casadi_model + casadi_cost_y + c
# extra generated files used to trigger a rebuild # extra generated files used to trigger a rebuild
generated_files = [ generated_files = [
'acados_ocp_long.json',
f'{gen}/Makefile', f'{gen}/Makefile',
f'{gen}/main_long.c', f'{gen}/main_long.c',
@@ -49,8 +49,9 @@ def get_cruise_accel(e2e, v_cruise, v_ego, a_cruise_prev, angle_steers, CP, dt,
max_accel = min(max_accel, coast_limit) max_accel = min(max_accel, coast_limit)
target_accel = np.clip(v_cruise - v_ego, A_CRUISE_MIN, max_accel) target_accel = np.clip(v_cruise - v_ego, A_CRUISE_MIN, max_accel)
j_cruise = np.interp(v_ego, A_CRUISE_MAX_BP, J_CRUISE_VALS) if not e2e:
target_accel = float(np.clip(target_accel, a_cruise_prev - j_cruise * dt, a_cruise_prev + j_cruise * dt)) j_cruise = np.interp(v_ego, A_CRUISE_MAX_BP, J_CRUISE_VALS)
target_accel = float(np.clip(target_accel, a_cruise_prev - j_cruise * dt, a_cruise_prev + j_cruise * dt))
return target_accel return target_accel
@@ -64,9 +65,10 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
self.dt = dt self.dt = dt
self.allow_throttle = True self.allow_throttle = True
self.a_desired = init_a
self.v_desired_filter = FirstOrderFilter(init_v, 2.0, self.dt) self.v_desired_filter = FirstOrderFilter(init_v, 2.0, self.dt)
self.a_cruise = init_a self.a_cruise = 0.0
self.output_a_target = init_a self.output_a_target = 0.0
self.output_should_stop = False self.output_should_stop = False
self.v_desired_trajectory = np.zeros(CONTROL_N) self.v_desired_trajectory = np.zeros(CONTROL_N)
@@ -103,8 +105,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
if reset_state: if reset_state:
self.v_desired_filter.x = v_ego self.v_desired_filter.x = v_ego
self.output_a_target = np.clip(sm['carState'].aEgo, ACCEL_MIN, ACCEL_MAX) self.a_desired = np.clip(sm['carState'].aEgo, ACCEL_MIN, ACCEL_MAX)
self.a_cruise = self.output_a_target
# Prevent divergence, smooth in current v_ego # Prevent divergence, smooth in current v_ego
self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego)) self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego))
@@ -112,11 +113,11 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
# No change cost when user is controlling the speed, or when standstill # No change cost when user is controlling the speed, or when standstill
prev_accel_constraint = not (reset_state or sm['carState'].standstill) prev_accel_constraint = not (reset_state or sm['carState'].standstill)
# Get new v_cruise and a_target from Smart Cruise Control and Speed Limit Assist # Get new v_cruise and a_desired from Smart Cruise Control and Speed Limit Assist
v_cruise, self.output_a_target = LongitudinalPlannerSP.update_targets(self, sm, self.v_desired_filter.x, self.output_a_target, v_cruise) v_cruise, self.a_desired = LongitudinalPlannerSP.update_targets(self, sm, self.v_desired_filter.x, self.a_desired, v_cruise)
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality) self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
self.mpc.set_cur_state(self.v_desired_filter.x, self.output_a_target) self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired)
self.mpc.update(sm['radarState'], personality=sm['selfdriveState'].personality) self.mpc.update(sm['radarState'], personality=sm['selfdriveState'].personality)
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution) self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
@@ -129,7 +130,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
cloudlog.info("FCW triggered") cloudlog.info("FCW triggered")
# Save starting point for next iteration # Save starting point for next iteration
a_prev = self.output_a_target a_prev = self.a_desired
action_t = self.CP.longitudinalActuatorDelay + DT_MDL action_t = self.CP.longitudinalActuatorDelay + DT_MDL
output_a_target_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX, output_a_target_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX,
@@ -154,6 +155,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
self.output_should_stop = any(should_stop for _, _, should_stop in candidates) self.output_should_stop = any(should_stop for _, _, should_stop in candidates)
self.output_a_target = np.clip(output_a_target, ACCEL_MIN, ACCEL_MAX) self.output_a_target = np.clip(output_a_target, ACCEL_MIN, ACCEL_MAX)
self.a_desired = float(self.output_a_target)
self.v_desired_filter.x = self.v_desired_filter.x + self.dt * (self.output_a_target + a_prev) / 2.0 self.v_desired_filter.x = self.v_desired_filter.x + self.dt * (self.output_a_target + a_prev) / 2.0
def publish(self, sm, pm): def publish(self, sm, pm):
+1 -3
View File
@@ -79,9 +79,7 @@ for usbgpu in [False, True] if USBGPU else [False]:
file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags) file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags)
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath) driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS) camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS)
# CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it. cmd = (f'{cmd_flags} {mac_brew_string} python3 {modeld_dir}/compile_modeld.py '
taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else ''
cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py '
f'--model-size {model_w}x{model_h} ' f'--model-size {model_w}x{model_h} '
f'--camera-resolutions {camera_res_args} ' f'--camera-resolutions {camera_res_args} '
f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} ' f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} '
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a501760a9d1d5fef0eab2b8c5d122d06124fc26dc8e0782e0aa94b82a208f0ff oid sha256:10926f2c0911821ca0e72439c1c3bf3ec11f0a08789aa14b7ee8f25379b2afa4
size 1757355221 size 1753235978
@@ -102,6 +102,7 @@ class TestBoarddSpi(OpenpilotTestCase):
edt = 1e3 / SERVICE_LIST[service].frequency edt = 1e3 / SERVICE_LIST[service].frequency
assert edt*0.9 < np.mean(dts) < edt*1.1 assert edt*0.9 < np.mean(dts) < edt*1.1
assert np.max(dts) < edt*8 assert np.max(dts) < edt*8
assert np.min(dts) < edt
assert len(dts) >= ((et-0.5)*SERVICE_LIST[service].frequency*0.8) assert len(dts) >= ((et-0.5)*SERVICE_LIST[service].frequency*0.8)
with subtests.test(msg="CAN traffic"): with subtests.test(msg="CAN traffic"):
@@ -17,8 +17,8 @@
"severity": 1, "severity": 1,
"_comment": "Set extra field to the failed reason." "_comment": "Set extra field to the failed reason."
}, },
"Offroad_ChestnutBranch": { "Offroad_NeosUpdate": {
"text": "Chestnut detected! Switch to the release-chestnut branch to use chestnut-class models.", "text": "An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install.",
"severity": 0 "severity": 0
}, },
"Offroad_UnregisteredHardware": { "Offroad_UnregisteredHardware": {
+1 -2
View File
@@ -234,8 +234,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
}, },
EventName.bigModelFailed: { EventName.bigModelFailed: {
ET.SOFT_DISABLE: soft_disable_alert("Big Model Failed"), ET.PERMANENT: NormalPermanentAlert("Big Model Failed ", "Restart the car to retry,\nnow driving on small model", duration=20.),
ET.PERMANENT: NormalPermanentAlert("Big Model Failed ", "Restart the car to retry,\nsmall model is still available", duration=20.),
}, },
EventName.lateralManeuver: { EventName.lateralManeuver: {
+1 -4
View File
@@ -337,7 +337,7 @@ class SelfdriveD(CruiseHelper):
device_motion = Pose.from_device_motion(self.sm['deviceMotion']) device_motion = Pose.from_device_motion(self.sm['deviceMotion'])
self.calibrated_pose = self.pose_calibrator.build_calibrated_pose(device_motion) self.calibrated_pose = self.pose_calibrator.build_calibrated_pose(device_motion)
if self.calibrated_pose is not None and not self.CP.notCar: if self.calibrated_pose is not None:
excessive_actuation = self.excessive_actuation_check.update(self.sm, CS, self.calibrated_pose) excessive_actuation = self.excessive_actuation_check.update(self.sm, CS, self.calibrated_pose)
if not self.excessive_actuation and excessive_actuation is not None: if not self.excessive_actuation and excessive_actuation is not None:
set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text=str(excessive_actuation)) set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text=str(excessive_actuation))
@@ -397,9 +397,6 @@ class SelfdriveD(CruiseHelper):
# All events here should at least have NO_ENTRY and SOFT_DISABLE. # All events here should at least have NO_ENTRY and SOFT_DISABLE.
num_events = len(self.events) num_events = len(self.events)
if self.big_model_active and big_failed:
self.events.add(EventName.bigModelFailed)
not_running = {p.name for p in self.sm['managerState'].processes if not p.running and p.shouldBeRunning} not_running = {p.name for p in self.sm['managerState'].processes if not p.running and p.shouldBeRunning}
if self.sm.recv_frame['managerState'] and len(not_running): if self.sm.recv_frame['managerState'] and len(not_running):
if not_running != self.not_running_prev: if not_running != self.not_running_prev:
@@ -7,7 +7,7 @@ import traceback
from collections import defaultdict from collections import defaultdict
from tqdm import tqdm from tqdm import tqdm
from typing import Any from typing import Any
from opendbc.car.car_helpers import interface_names, interfaces from opendbc.car.car_helpers import interface_names
from openpilot.common.git import get_commit from openpilot.common.git import get_commit
from openpilot.tools.lib.openpilotci import get_url from openpilot.tools.lib.openpilotci import get_url
from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs, format_diff from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs, format_diff
@@ -64,8 +64,7 @@ segments = [
] ]
# dashcamOnly makes don't need to be tested until a full port is done # dashcamOnly makes don't need to be tested until a full port is done
excluded_interfaces = {brand for brand, platforms in interface_names.items() excluded_interfaces = ["mock", "body", "psa"]
if all(interfaces[platform].get_non_essential_params(platform).dashcamOnly for platform in platforms)} | {"body"}
BASE_URL = "https://raw.githubusercontent.com/sunnypilot/ci-artifacts/refs/heads/process-replay/" BASE_URL = "https://raw.githubusercontent.com/sunnypilot/ci-artifacts/refs/heads/process-replay/"
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit") REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
+11 -23
View File
@@ -67,7 +67,7 @@ PROCS = {
"openpilot.selfdrive.pandad.pandad": 0, "openpilot.selfdrive.pandad.pandad": 0,
"openpilot.system.loggerd.uploader": 15.0, "openpilot.system.loggerd.uploader": 15.0,
"openpilot.system.loggerd.deleter": 1.0, "openpilot.system.loggerd.deleter": 1.0,
"./pandad": 40.0, "./pandad": 19.0,
"openpilot.system.qcomgpsd.qcomgpsd": 1.0, "openpilot.system.qcomgpsd.qcomgpsd": 1.0,
"openpilot.common.hardware.comma.modem": 10.0, "openpilot.common.hardware.comma.modem": 10.0,
} }
@@ -107,10 +107,6 @@ def cputime_total(ct):
class TestOnroad(OpenpilotTestCase): class TestOnroad(OpenpilotTestCase):
COMMA_HARDWARE_TEST = True COMMA_HARDWARE_TEST = True
def setUp(self):
# Hardware setup is handled once for the full onroad test in setup_class.
unittest.TestCase.setUp(self)
@classmethod @classmethod
def setup_class(cls): def setup_class(cls):
if "DEBUG" in os.environ: if "DEBUG" in os.environ:
@@ -336,34 +332,26 @@ class TestOnroad(OpenpilotTestCase):
assert np.all(eof_sof_diff < 50*1e6) assert np.all(eof_sof_diff < 50*1e6)
first_fid = {min(self.ts[c]['frameId']) for c in cams} first_fid = {min(self.ts[c]['frameId']) for c in cams}
if cams[0].endswith('CameraState'): assert len(first_fid) == 1, "Cameras don't start on same frame ID"
if cam.endswith('CameraState'):
# camerad guarantees that all cams start on frame ID 0 # camerad guarantees that all cams start on frame ID 0
# (note loggerd also needs to start up fast enough to catch it) # (note loggerd also needs to start up fast enough to catch it)
assert min(first_fid) < 100, "Cameras start on frame ID too high" assert next(iter(first_fid)) < 100, "Cameras start on frame ID too high"
else:
# encoderd synchronizes all camera encoders to the same starting frame
assert len(first_fid) == 1, "Camera encoders don't start on same frame ID"
# we don't do a full segment rotation, so these might not match exactly # we don't do a full segment rotation, so these might not match exactly
last_fid = {max(self.ts[c]['frameId']) for c in cams} last_fid = {max(self.ts[c]['frameId']) for c in cams}
assert max(last_fid) - min(last_fid) < 10 assert max(last_fid) - min(last_fid) < 10
timestamps = { start, end = min(first_fid), min(last_fid)
cam: dict(zip(self.ts[cam]['frameId'], self.ts[cam]['timestampSof'], strict=True)) for i in range(end-start):
for cam in cams
}
common_frame_ids = set.intersection(*(set(ts) for ts in timestamps.values()))
assert common_frame_ids, "Cameras have no overlapping frame IDs"
for frame_id in sorted(common_frame_ids):
# road and wide cameras (first two) should be synced within 2ms # road and wide cameras (first two) should be synced within 2ms
ts = {cam: timestamps[cam][frame_id] / 1e6 for cam in cams[:2]} ts = {c: round(self.ts[c]['timestampSof'][i]/1e6, 1) for c in cams[:2]}
diff = max(ts.values()) - min(ts.values()) diff = (max(ts.values()) - min(ts.values()))
assert diff < 2, f"Cameras not synced properly: {frame_id=}, {diff=:.1f}ms, {ts=}" assert diff < 2, f"Cameras not synced properly: frame_id={start+i}, {diff=:.1f}ms, {ts=}"
# cabin camera should be staggered ~25ms from road camera # cabin camera should be staggered ~25ms from road camera
offset_ms = abs(timestamps[cams[2]][frame_id] - timestamps[cams[0]][frame_id]) / 1e6 offset_ms = abs(self.ts[cams[2]]['timestampSof'][i] - self.ts[cams[0]]['timestampSof'][i]) / 1e6
assert 20 < offset_ms < 30, f"cabin camera stagger out of range at frame {frame_id}: {offset_ms:.1f}ms" assert 20 < offset_ms < 30, f"cabin camera stagger out of range at frame {start+i}: {offset_ms:.1f}ms"
def test_camera_encoder_matches(self, subtests): def test_camera_encoder_matches(self, subtests):
# sanity check that the frame metadata is consistent with the encoded frames # sanity check that the frame metadata is consistent with the encoded frames
+19
View File
@@ -3,6 +3,7 @@ from enum import IntEnum
import openpilot.cereal.messaging as messaging import openpilot.cereal.messaging as messaging
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import Widget 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.sidebar import Sidebar, SIDEBAR_WIDTH
from openpilot.selfdrive.ui.layouts.home import HomeLayout from openpilot.selfdrive.ui.layouts.home import HomeLayout
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType
@@ -27,6 +28,7 @@ class MainLayout(Widget):
super().__init__() super().__init__()
self._pm = messaging.PubMaster(['bookmarkButton', 'userBookmark']) self._pm = messaging.PubMaster(['bookmarkButton', 'userBookmark'])
self._custom_button_sock = messaging.sub_sock('carState')
self._sidebar = Sidebar() self._sidebar = Sidebar()
self._current_mode = MainState.HOME self._current_mode = MainState.HOME
@@ -40,6 +42,10 @@ class MainLayout(Widget):
MainState.SETTINGS: SettingsLayout(), MainState.SETTINGS: SettingsLayout(),
MainState.ONROAD: AugmentedRoadView(), 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._sidebar_rect = rl.Rectangle(0, 0, 0, 0)
self._content_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) gui_app.push_widget(self._onboarding_window)
def _render(self, _): 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._handle_onroad_transition()
self._render_main_content() self._render_main_content()
@@ -114,6 +121,18 @@ class MainLayout(Widget):
def _on_settings_clicked(self): def _on_settings_clicked(self):
self.open_settings(PanelType.DEVICE) 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): def _on_bookmark_clicked(self):
for service in ('bookmarkButton', 'userBookmark'): for service in ('bookmarkButton', 'userBookmark'):
msg = messaging.new_message(service, valid=True) 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.settings.settings import SettingsLayout
from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts 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.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.ui_state import device, ui_state
from openpilot.selfdrive.ui.mici.layouts.onboarding import OnboardingWindow from openpilot.selfdrive.ui.mici.layouts.onboarding import OnboardingWindow
from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout 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) super().__init__(snap_items=True, spacing=0, pad=0, scroll_indicator=False, edge_shadows=False)
self._pm = messaging.PubMaster(['bookmarkButton', 'userBookmark']) self._pm = messaging.PubMaster(['bookmarkButton', 'userBookmark'])
self._custom_button_sock = messaging.sub_sock('carState')
self._prev_onroad = False self._prev_onroad = False
self._prev_standstill = False self._prev_standstill = False
@@ -35,6 +37,10 @@ class MiciMainLayout(Scroller):
self._settings_layout = SettingsLayout() self._settings_layout = SettingsLayout()
self._car_onroad_layout = AugmentedRoadView(bookmark_callback=self._on_bookmark_clicked) self._car_onroad_layout = AugmentedRoadView(bookmark_callback=self._on_bookmark_clicked)
self._body_onroad_layout = BodyLayout() self._body_onroad_layout = BodyLayout()
self._custom_button_callbacks = {
CustomButtonAction.BOOKMARK: self._on_bookmark_clicked,
CustomButtonAction.CYCLE_UI: self._cycle_ui,
}
# Initialize widget rects # Initialize widget rects
for widget in (self._home_layout, self._alerts_layout, self._settings_layout, for widget in (self._home_layout, self._alerts_layout, self._settings_layout,
@@ -95,6 +101,8 @@ class MiciMainLayout(Scroller):
self._alerts_layout._update_state() self._alerts_layout._update_state()
def _render(self, _): 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 not self._setup:
if self._alerts_layout.active_alerts() > 0: if self._alerts_layout.active_alerts() > 0:
self._scroller.scroll_to(self._alerts_layout.rect.x) self._scroller.scroll_to(self._alerts_layout.rect.x)
@@ -150,6 +158,23 @@ class MiciMainLayout(Scroller):
msg = messaging.new_message(service, valid=True) msg = messaging.new_message(service, valid=True)
self._pm.send(service, msg) 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): def _on_body_changed(self):
self._car_onroad_layout.set_visible(not ui_state.is_body) self._car_onroad_layout.set_visible(not ui_state.is_body)
self._body_onroad_layout.set_visible(bool(ui_state.is_body)) self._body_onroad_layout.set_visible(bool(ui_state.is_body))
@@ -47,7 +47,7 @@ class SoftwareInfoLayoutMici(Widget):
self._branch_label = UnifiedLabel("branch", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) self._branch_label = UnifiedLabel("branch", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False)
self._branch_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color, self._branch_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color,
font_weight=FontWeight.ROMAN, wrap_text=False, scroll=True) font_weight=FontWeight.ROMAN, wrap_text=False)
def _update_state(self): def _update_state(self):
desc = _split_description(ui_state.params.get("UpdaterCurrentDescription") or "") desc = _split_description(ui_state.params.get("UpdaterCurrentDescription") or "")
@@ -132,7 +132,7 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
# 1. Never received selfdriveState since going onroad # 1. Never received selfdriveState since going onroad
waiting_for_startup = recv_frame < ui_state.started_frame waiting_for_startup = recv_frame < ui_state.started_frame
if waiting_for_startup and time_since_onroad > 10: if waiting_for_startup and time_since_onroad > 5:
return ALERT_STARTUP_PENDING return ALERT_STARTUP_PENDING
# 2. Lost communication with selfdriveState after receiving it # 2. Lost communication with selfdriveState after receiving it
@@ -150,8 +150,8 @@ class BigButton(Widget):
super().set_touch_valid_callback(lambda: touch_callback() and self._grow_animation_until is None) super().set_touch_valid_callback(lambda: touch_callback() and self._grow_animation_until is None)
def _width_hint(self) -> int: def _width_hint(self) -> int:
# A value moves the title to the top, where it shares space with the icon. # Single line if scrolling, so hide behind icon if exists
icon_size = self._txt_icon.width if self._txt_icon and self.value else 0 icon_size = self._txt_icon.width if self._txt_icon and self._scroll and self.value else 0
return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - icon_size) return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - icon_size)
def _get_label_font_size(self): def _get_label_font_size(self):
@@ -92,7 +92,7 @@ class AlertRenderer(Widget):
# 1. Never received selfdriveState since going onroad # 1. Never received selfdriveState since going onroad
waiting_for_startup = recv_frame < ui_state.started_frame waiting_for_startup = recv_frame < ui_state.started_frame
if waiting_for_startup and time_since_onroad > 10: if waiting_for_startup and time_since_onroad > 5:
return ALERT_STARTUP_PENDING return ALERT_STARTUP_PENDING
# 2. Lost communication with selfdriveState after receiving it # 2. Lost communication with selfdriveState after receiving it
@@ -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()
@@ -22,9 +22,9 @@ from openpilot.system.ui.widgets.toggle import ON_COLOR
from openpilot.sunnypilot.models.runners.constants import CUSTOM_MODEL_PATH from openpilot.sunnypilot.models.runners.constants import CUSTOM_MODEL_PATH
from openpilot.system.ui.sunnypilot.lib.styles import style from openpilot.system.ui.sunnypilot.lib.styles import style
from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction, ScrollingButtonAction from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP, toggle_item_sp, option_item_sp from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP, toggle_item_sp, option_item_sp
from openpilot.system.ui.sunnypilot.widgets.download_status import download_status_item from openpilot.system.ui.sunnypilot.widgets.progress_bar import progress_item
from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeNode, TreeFolder from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeNode, TreeFolder
if gui_app.sunnypilot_ui(): if gui_app.sunnypilot_ui():
@@ -35,8 +35,9 @@ class ModelsLayout(Widget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.model_manager = None self.model_manager = None
self.download_status = None
self.prev_download_status = None
self.model_dialog = None self.model_dialog = None
self._downloading = False
self.last_cache_calc_time = 0 self.last_cache_calc_time = 0
self._initialize_items() self._initialize_items()
@@ -51,11 +52,15 @@ class ModelsLayout(Widget):
self.current_model_item = ListItemSP( self.current_model_item = ListItemSP(
title=tr("Current Model"), title=tr("Current Model"),
description="", description="",
action_item=ScrollingButtonAction(tr("SELECT")), action_item=NoElideButtonAction(tr("SELECT")),
callback=self._handle_current_model_clicked callback=self._handle_current_model_clicked
) )
self.download_item = download_status_item(lambda: tr("Download") if self._downloading else tr("Model Status")) self.supercombo_label = progress_item(tr("Driving Model"))
self.vision_label = progress_item(tr("Vision Model"))
self.policy_label = progress_item(tr("Policy Model"))
self.off_policy_label = progress_item(tr("Off-Policy Model"))
self.on_policy_label = progress_item(tr("On-Policy Model"))
self.refresh_item = button_item(tr("Refresh Model List"), tr("REFRESH"), "", self.refresh_item = button_item(tr("Refresh Model List"), tr("REFRESH"), "",
lambda: (ui_state.params.put("ModelManager_LastSyncTime", 0), lambda: (ui_state.params.put("ModelManager_LastSyncTime", 0),
@@ -93,7 +98,8 @@ class ModelsLayout(Widget):
1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True, 1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True,
lambda v: f"{v / 100:.2f} m") lambda v: f"{v / 100:.2f} m")
self.items = [self.current_model_item, self.cancel_download_item, self.download_item, self.refresh_item, self.clear_cache_item, self.items = [self.current_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label,
self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item,
self.lane_turn_desire_toggle, self.lane_turn_value_control, self.lagd_toggle, self.delay_control, self.camera_offset] self.lane_turn_desire_toggle, self.lane_turn_value_control, self.lagd_toggle, self.delay_control, self.camera_offset]
def _update_lagd_description(self, lagd_toggle: bool): def _update_lagd_description(self, lagd_toggle: bool):
@@ -129,9 +135,14 @@ class ModelsLayout(Widget):
gui_app.push_widget(dialog) gui_app.push_widget(dialog)
def _handle_bundle_download_progress(self): def _handle_bundle_download_progress(self):
self.download_item.set_visible(False) labels = {custom.ModelManagerSP.Model.Type.supercombo: self.supercombo_label,
custom.ModelManagerSP.Model.Type.vision: self.vision_label,
custom.ModelManagerSP.Model.Type.policy: self.policy_label,
custom.ModelManagerSP.Model.Type.offPolicy: self.off_policy_label,
custom.ModelManagerSP.Model.Type.onPolicy: self.on_policy_label}
for label in labels.values():
label.set_visible(False)
self.cancel_download_item.set_visible(False) self.cancel_download_item.set_visible(False)
self._downloading = False
if not self.model_manager or (not self.model_manager.selectedBundle and not self.model_manager.activeBundle): if not self.model_manager or (not self.model_manager.selectedBundle and not self.model_manager.activeBundle):
return return
@@ -142,41 +153,32 @@ class ModelsLayout(Widget):
if not bundle: if not bundle:
return return
self.download_status = bundle.status
status_changed = self.prev_download_status != self.download_status
self.prev_download_status = self.download_status
self.cancel_download_item.set_visible(bool(self.model_manager.selectedBundle) and ui_state.params.get("ModelManager_DownloadIndex") is not None) self.cancel_download_item.set_visible(bool(self.model_manager.selectedBundle) and ui_state.params.get("ModelManager_DownloadIndex") is not None)
if (current_time := time.monotonic()) - self.last_cache_calc_time > 0.5: if (current_time := time.monotonic()) - self.last_cache_calc_time > 0.5:
self.last_cache_calc_time = current_time self.last_cache_calc_time = current_time
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB") self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
if bundle.status == custom.ModelManagerSP.DownloadStatus.downloading: if self.download_status == custom.ModelManagerSP.DownloadStatus.downloading:
device._reset_interactive_timeout() device._reset_interactive_timeout()
# every bundle is a single chunked artifact now for model in bundle.models:
progresses = [model.artifact.downloadProgress for model in bundle.models if model.artifact.fileName] if label := labels.get(getattr(model.type, 'raw', model.type)):
if not progresses: label.set_visible(True)
return p = model.artifact.downloadProgress
text, show, color = f"pending - {bundle.displayName}", False, rl.GRAY
self.download_item.set_visible(True) if p.status == custom.ModelManagerSP.DownloadStatus.downloading:
self.download_item.action_item.update(**self._download_row_state(progresses, bundle.internalName)) text, show = f"{int(p.progress)}% - {bundle.displayName}", True
self._downloading = self.download_item.action_item.downloading elif p.status in (custom.ModelManagerSP.DownloadStatus.downloaded, custom.ModelManagerSP.DownloadStatus.cached):
status_text = tr("from cache" if p.status == custom.ModelManagerSP.DownloadStatus.cached else "downloaded")
@staticmethod text, color = f"{bundle.displayName} - {status_text if status_changed else tr('ready')}", ON_COLOR
def _download_row_state(progresses, name: str) -> dict: elif p.status == custom.ModelManagerSP.DownloadStatus.failed:
"""Maps a bundle's artifact progress to DownloadStatusAction.update kwargs.""" text, color = f"download failed - {bundle.displayName}", rl.RED
# .raw: _DynamicEnum equals its int but does not hash like it label.action_item.update(p.progress, text, show, color)
statuses = {getattr(p.status, 'raw', p.status) for p in progresses}
progress = sum(p.progress for p in progresses) / len(progresses)
ds = custom.ModelManagerSP.DownloadStatus
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.downloading in statuses:
return {"name": name, "downloading": True, "progress": progress}
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
return {"name": name, "text_color": rl.GRAY, "icon": "icons/circled_slash.png", "icon_color": rl.WHITE}
@staticmethod @staticmethod
def _show_reset_params_dialog(): def _show_reset_params_dialog():
@@ -249,7 +251,7 @@ class ModelsLayout(Widget):
self._update_lagd_description(live_delay) self._update_lagd_description(live_delay)
self.model_manager = ui_state.sm["modelManagerSP"] self.model_manager = ui_state.sm["modelManagerSP"]
self._handle_bundle_download_progress() self._handle_bundle_download_progress()
active_name = self.model_manager.activeBundle.displayName if self.model_manager and self.model_manager.activeBundle.ref else f"{DEFAULT_MODEL} (Default)" active_name = self.model_manager.activeBundle.internalName if self.model_manager and self.model_manager.activeBundle.ref else f"{DEFAULT_MODEL} (Default)"
self.current_model_item.action_item.set_value(active_name) self.current_model_item.action_item.set_value(active_name)
if not ui_state.is_offroad(): if not ui_state.is_offroad():
@@ -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
+4 -4
View File
@@ -20,14 +20,14 @@ class TestSoundd(OpenpilotTestCase):
sm.update(100) sm.update(100)
assert sm.updated['selfdriveState'] assert sm.updated['selfdriveState']
sm.recv_time['selfdriveState'] = 0 received_at = sm.recv_time['selfdriveState']
clock = mocker.patch("openpilot.selfdrive.ui.soundd.time.monotonic", return_value=SELFDRIVE_STATE_TIMEOUT) clock = mocker.patch("openpilot.selfdrive.ui.soundd.time.monotonic", return_value=received_at + SELFDRIVE_STATE_TIMEOUT)
assert not check_selfdrive_timeout_alert(sm) assert not check_selfdrive_timeout_alert(sm)
clock.return_value = SELFDRIVE_STATE_TIMEOUT + 0.1 clock.return_value = received_at + SELFDRIVE_STATE_TIMEOUT + 0.1
assert check_selfdrive_timeout_alert(sm) assert check_selfdrive_timeout_alert(sm)
clock.return_value = SELFDRIVE_STATE_TIMEOUT + 10 clock.return_value = received_at + SELFDRIVE_STATE_TIMEOUT + 10
assert not check_selfdrive_timeout_alert(sm) assert not check_selfdrive_timeout_alert(sm)
def test_check_selfdrive_timeout_alert_mads_lateral_only(self): def test_check_selfdrive_timeout_alert_mads_lateral_only(self):
+1
View File
@@ -1,2 +1,3 @@
SConscript(['common/transformations/SConscript']) SConscript(['common/transformations/SConscript'])
SConscript(['modeld_v2/SConscript'])
SConscript(['selfdrive/locationd/SConscript']) SConscript(['selfdrive/locationd/SConscript'])
+84
View File
@@ -0,0 +1,84 @@
import os
import glob
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE
from openpilot.common.hardware import HARDWARE, PC
Import('env', 'arch', 'release')
lenv = env.Clone()
tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath) if 'pycache' not in x]
def get_camera_configs():
DEVICE_RESOLUTIONS = {
"tici": (_ar_ox_fisheye.width, _ar_ox_fisheye.height),
"tizi": (_ar_ox_fisheye.width, _ar_ox_fisheye.height),
"mici": (_os_fisheye.width, _os_fisheye.height),
}
if release or PC or 'CI' in os.environ:
return set(DEVICE_RESOLUTIONS.values())
return [DEVICE_RESOLUTIONS[HARDWARE.get_device_type()]]
CAMERA_CONFIGS = get_camera_configs()
tg_flags = {
'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0',
'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")}',
}.get(arch, 'DEV=CPU:LLVM')
image_flag = {
'larch64': 'IMAGE=2',
}.get(arch, 'IMAGE=0')
model_w, model_h = MEDMODEL_INPUT_SIZE
from openpilot.selfdrive.modeld.constants import ModelConstants
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS)
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + ':' + env.Dir("#").abspath + '"'
compile_modeld_script = File("compile_modeld.py").abspath
upstream_compile_script = File(Dir("#openpilot/selfdrive/modeld").File("compile_modeld.py").abspath)
script_deps = [File("compile_modeld.py"), upstream_compile_script]
def compile_combined(model_type, onnx_args, output_name):
output_pkl = File(f"models/{output_name}").abspath
cmd = (f'{pythonpath_string} {tg_flags} {image_flag} python3 {compile_modeld_script} '
f'--model-type {model_type} '
f'--model-size {model_w}x{model_h} '
f'--camera-resolutions {camera_res_args} '
f'{onnx_args} '
f'--frame-skip {frame_skip} '
f'--output {output_pkl}')
onnx_files = [f for f in onnx_args.split() if f.endswith('.onnx')]
return lenv.Command(output_pkl, tinygrad_files + script_deps + [File(f) for f in onnx_files if os.path.isfile(f)], cmd)
# Vision + Policy (stock default model)
vision_onnx = File("models/driving_vision.onnx").abspath
policy_onnx = File("models/driving_policy.onnx").abspath
if os.path.isfile(vision_onnx) and os.path.isfile(policy_onnx):
compile_combined('vision_policy',
f'--vision-onnx {vision_onnx} --policy-onnx {policy_onnx}',
'driving_combined_tinygrad.pkl')
# Vision + Off-Policy
off_policy_onnx = File("models/driving_off_policy.onnx").abspath
if os.path.isfile(vision_onnx) and os.path.isfile(off_policy_onnx):
policy_arg = f'--policy-onnx {policy_onnx}' if os.path.isfile(policy_onnx) else ''
compile_combined('vision_multi_policy',
f'--vision-onnx {vision_onnx} {policy_arg} --off-policy-onnx {off_policy_onnx}',
'driving_combined_multi_tinygrad.pkl')
# Vision + On-Policy + Off-Policy
on_policy_onnx = File("models/driving_on_policy.onnx").abspath
if os.path.isfile(vision_onnx) and os.path.isfile(on_policy_onnx) and os.path.isfile(off_policy_onnx):
compile_combined('vision_multi_policy',
f'--vision-onnx {vision_onnx} --off-policy-onnx {off_policy_onnx} --on-policy-onnx {on_policy_onnx}',
'driving_combined_tri_tinygrad.pkl')
# Supercombo
supercombo_onnx = File("models/supercombo.onnx").abspath
if os.path.isfile(supercombo_onnx):
compile_combined('supercombo',
f'--supercombo-onnx {supercombo_onnx}',
'driving_combined_supercombo_tinygrad.pkl')
+117 -139
View File
@@ -8,10 +8,10 @@ See the LICENSE.md file in the root directory for more details.
import argparse import argparse
import os import os
import tempfile import pickle
import time import time
from collections import defaultdict
from functools import partial from functools import partial
from openpilot.selfdrive.modeld.helpers import dump_oob, load_oob
import numpy as np import numpy as np
os.environ['GMMU'] = '0' os.environ['GMMU'] = '0'
@@ -38,9 +38,6 @@ from tinygrad.engine.jit import TinyJit
from tinygrad.tensor import Tensor from tinygrad.tensor import Tensor
MODEL_TYPES = ('vision_policy', 'supercombo', 'vision_multi_policy') MODEL_TYPES = ('vision_policy', 'supercombo', 'vision_multi_policy')
WARP_INPUTS = ['tfm', 'big_tfm']
POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
WARP_DEV = os.getenv('WARP_DEV')
def _detect_desire_key(shapes: dict) -> str | None: def _detect_desire_key(shapes: dict) -> str | None:
@@ -79,7 +76,7 @@ def get_policy_npy_shapes(input_shapes: dict, is_supercombo: bool = False) -> tu
def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = Device.DEFAULT, def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = Device.DEFAULT,
is_supercombo: bool = False) -> tuple[dict, dict]: is_supercombo: bool = False, use_packed: bool = True) -> tuple[dict, dict]:
road_key, _ = _detect_vision_keys(input_shapes) road_key, _ = _detect_vision_keys(input_shapes)
if not road_key: if not road_key:
raise ValueError("Vision road key missing from input shapes.") raise ValueError("Vision road key missing from input shapes.")
@@ -95,76 +92,74 @@ def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = D
desire_shape = input_shapes[desire_key] desire_shape = input_shapes[desire_key]
features_buffer = input_shapes.get('features_buffer') features_buffer = input_shapes.get('features_buffer')
npy_arrays = { if use_packed: # remove packed detection block after all models are recompiled
'tfm': np.zeros((3, 3), dtype=np.float32), npy_arrays = {
'big_tfm': np.zeros((3, 3), dtype=np.float32) 'tfm': np.zeros((3, 3), dtype=np.float32),
} 'big_tfm': np.zeros((3, 3), dtype=np.float32)
}
shapes, sizes = get_policy_npy_shapes(input_shapes, is_supercombo=is_supercombo) shapes, sizes = get_policy_npy_shapes(input_shapes, is_supercombo=is_supercombo)
packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32) packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32)
split_indices = np.cumsum(sizes[:-1]) if len(sizes) > 1 else [] split_indices = np.cumsum(sizes[:-1]) if len(sizes) > 1 else []
split_views = np.split(packed_npy_inputs, split_indices) if len(sizes) > 0 else [] split_views = np.split(packed_npy_inputs, split_indices) if len(sizes) > 0 else []
for (k, s), v in zip(shapes.items(), split_views, strict=True): for (k, s), v in zip(shapes.items(), split_views, strict=True):
npy_arrays[k] = v.reshape(s) npy_arrays[k] = v.reshape(s)
queues = { queues = {
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), 'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), 'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
'desire_q': Tensor(np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]), 'desire_q': Tensor(np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]),
dtype=np.float32), device=device).contiguous().realize(), dtype=np.float32), device=device).contiguous().realize(),
'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(), 'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(),
} }
if features_buffer: if features_buffer:
feat_q_len = frame_skip * features_buffer[1] if is_supercombo else frame_skip * (features_buffer[1] - 1) + 1 queues['feat_q'] = Tensor(np.zeros((frame_skip * (features_buffer[1] - 1) + 1, features_buffer[0], features_buffer[2]),
queues['feat_q'] = Tensor(np.zeros((feat_q_len, features_buffer[0], features_buffer[2]), dtype=np.float32), device=device).contiguous().realize()
dtype=np.float32), device=device).contiguous().realize()
queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items() if key in ('tfm', 'big_tfm')}) queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items() if key in ('tfm', 'big_tfm')})
else:
# TODO-SP: Remove legacy queuing fallback else block after all models are recompiled
npy_arrays = {
'desire': np.zeros(desire_shape[2], dtype=np.float32),
'tfm': np.zeros((3, 3), dtype=np.float32),
'big_tfm': np.zeros((3, 3), dtype=np.float32)
}
for key, shape in input_shapes.items():
if key not in npy_arrays and 'img' not in key and key not in ('features_buffer', desire_key):
npy_arrays[key] = np.zeros(shape, dtype=np.float32)
queues = {
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
'desire_q': Tensor(np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]),
dtype=np.float32), device=device).contiguous().realize()
}
if features_buffer:
queues['feat_q'] = Tensor(np.zeros((frame_skip * (features_buffer[1] - 1) + 1, features_buffer[0], features_buffer[2]),
dtype=np.float32), device=device).contiguous().realize()
queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items()})
return queues, npy_arrays return queues, npy_arrays
def make_split_input_queues(vision_input_shapes: dict, policy_input_shapes: dict, def make_split_input_queues(vision_input_shapes: dict, policy_input_shapes: dict,
frame_skip: int, device: str = Device.DEFAULT) -> tuple[dict, dict]: frame_skip: int, device: str = Device.DEFAULT, use_packed: bool = True) -> tuple[dict, dict]:
return generate_queues_and_npy({**vision_input_shapes, **policy_input_shapes}, frame_skip, device, is_supercombo=False) return generate_queues_and_npy({**vision_input_shapes, **policy_input_shapes}, frame_skip, device, is_supercombo=False, use_packed=use_packed)
def make_supercombo_input_queues(input_shapes: dict, frame_skip: int, def make_supercombo_input_queues(input_shapes: dict, frame_skip: int,
device: str = Device.DEFAULT) -> tuple[dict, dict]: device: str = Device.DEFAULT, use_packed: bool = True) -> tuple[dict, dict]:
return generate_queues_and_npy(input_shapes, frame_skip, device, is_supercombo=True) return generate_queues_and_npy(input_shapes, frame_skip, device, is_supercombo=True, use_packed=use_packed)
def make_random_images(keys, shape, device): def create_jit_runner(vision_runner, policy_runners: list, nv12: NV12Frame, model_size: tuple[int, int],
return {k: Tensor.randint(shape, low=0, high=256, dtype=dtypes.uint8, device=device).realize() for k in keys} features_slice: slice, frame_skip: int, input_shapes: dict, prepare_only: bool):
frame_prepare = make_frame_prepare(nv12, *model_size)
def make_warp_queues(device=Device.DEFAULT):
npy = {
'tfm': np.zeros((3, 3), dtype=np.float32),
'big_tfm': np.zeros((3, 3), dtype=np.float32),
}
queues = {k: Tensor(v, device='NPY').realize() for k, v in npy.items()}
return queues, npy
def make_warp(nv12: NV12Frame, model_w: int, model_h: int):
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
WARP_DEV = os.getenv('WARP_DEV', Device.DEFAULT)
def warp(tfm, big_tfm, frame, big_frame):
tfm = tfm.to(WARP_DEV)
big_tfm = big_tfm.to(WARP_DEV)
Tensor.realize(tfm, big_tfm)
warped_frame = frame_prepare(frame, tfm).unsqueeze(0)
warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0)
return Tensor.cat(warped_frame, warped_big_frame)
return warp
def make_run_policy(vision_runner, policy_runners: list, features_slice: slice, frame_skip: int, input_shapes: dict):
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip) sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip) sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
@@ -177,14 +172,20 @@ def make_run_policy(vision_runner, policy_runners: list, features_slice: slice,
is_supercombo = vision_runner is None is_supercombo = vision_runner is None
npy_shapes, npy_sizes = get_policy_npy_shapes(input_shapes, is_supercombo=is_supercombo) npy_shapes, npy_sizes = get_policy_npy_shapes(input_shapes, is_supercombo=is_supercombo)
def run_policy(warped, img_q, big_img_q, feat_q, packed_npy_inputs, **kwargs): def runner(img_q, big_img_q, feat_q, packed_npy_inputs, frame, big_frame, tfm, big_tfm, **kwargs):
desire_q = kwargs['desire_q'] desire_q = kwargs['desire_q']
packed_npy_inputs_dev = packed_npy_inputs.to(Device.DEFAULT)
warped_dev = warped.to(Device.DEFAULT)
Tensor.realize(packed_npy_inputs_dev, warped_dev)
img = shift_and_sample(img_q, warped_dev[0:1], sample_skip_fn).realize() packed_npy_inputs_dev = packed_npy_inputs.to(Device.DEFAULT)
big_img = shift_and_sample(big_img_q, warped_dev[1:2], sample_skip_fn).realize() tfm_dev = tfm.to(Device.DEFAULT)
big_tfm_dev = big_tfm.to(Device.DEFAULT)
Tensor.realize(packed_npy_inputs_dev, tfm_dev, big_tfm_dev)
img = shift_and_sample(img_q, frame_prepare(frame, tfm_dev).unsqueeze(0), sample_skip_fn).realize()
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm_dev).unsqueeze(0), sample_skip_fn).realize()
if prepare_only:
return img, big_img
unpacked_tensors = [tensor.reshape(shape) for tensor, shape in zip(packed_npy_inputs_dev.split(npy_sizes), npy_shapes.values(), strict=True)] unpacked_tensors = [tensor.reshape(shape) for tensor, shape in zip(packed_npy_inputs_dev.split(npy_sizes), npy_shapes.values(), strict=True)]
unpacked_dict = dict(zip(npy_shapes.keys(), unpacked_tensors, strict=True)) unpacked_dict = dict(zip(npy_shapes.keys(), unpacked_tensors, strict=True))
@@ -219,52 +220,42 @@ def make_run_policy(vision_runner, policy_runners: list, features_slice: slice,
shift_and_sample(feat_q, new_feat, sample_skip_fn).realize() shift_and_sample(feat_q, new_feat, sample_skip_fn).realize()
return policy_out return policy_out
return run_policy return runner
def compile_jit(jit, make_random_inputs, input_keys, make_queues): def compile_and_warmup(nv12: NV12Frame, model_size: tuple[int, int], prepare_only: bool, frame_skip: int, vision_runner, policy_runners: list, metadata: dict):
SEED = 42 print(f"Compiling combined JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...")
def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True):
input_queues, npy = make_queues(Device.DEFAULT)
rng = np.random.default_rng(seed)
Tensor.manual_seed(seed)
testing = test_val is not None or test_buffers is not None all_shapes = {key: value for meta in metadata.values() for key, value in meta['input_shapes'].items()}
n_runs = 1 if testing else 3
for i in range(n_runs): feat_meta = metadata.get('vision') or metadata.get('model') or metadata.get('policy')
for v in npy.values(): if not feat_meta:
v[:] = rng.standard_normal(v.shape).astype(v.dtype) raise ValueError("Could not find vision, model, or policy metadata.")
Device.default.synchronize()
random_inputs = make_random_inputs()
st = time.perf_counter()
outs = fn(**{k: input_queues[k] for k in input_keys if k in input_queues}, **random_inputs)
mt = time.perf_counter()
Device.default.synchronize()
et = time.perf_counter()
print(f" [{i+1}/{n_runs}] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
if i == 0: features_slice = feat_meta['output_slices']['hidden_state']
val = [np.copy(v.numpy()) for v in (outs if isinstance(outs, tuple) else [outs])] if outs is not None else [] WARP_DEV = 'CPU' if "USBGPU" in os.environ else Device.DEFAULT
buffers = [np.copy(v.numpy().copy()) for v in input_queues.values()]
if test_val is not None: is_supercombo = vision_runner is None
match = all(np.array_equal(a, b) for a, b in zip(val, test_val, strict=True)) run_func = create_jit_runner(vision_runner, policy_runners, nv12, model_size, features_slice, frame_skip, all_shapes, prepare_only)
assert match == expect_match, f"outputs {'differ from' if expect_match else 'match'} baseline (seed={seed})" run_jit = TinyJit(run_func, prune=True)
if test_buffers is not None: queues, npy_arrays = generate_queues_and_npy(all_shapes, frame_skip, Device.DEFAULT, is_supercombo=is_supercombo)
match = all(np.array_equal(a, b) for a, b in zip(buffers, test_buffers, strict=True))
assert match == expect_match, f"buffers {'differ from' if expect_match else 'match'} baseline (seed={seed})"
return val, buffers
print('capture + replay') for i in range(3):
test_val, test_buffers = random_inputs_run(jit, SEED) rng = np.random.default_rng(42 + i)
print('pickle round trip') frame = Tensor.randint(nv12.size, low=0, high=256, dtype=dtypes.uint8, device=WARP_DEV).realize()
with tempfile.TemporaryFile(dir=".") as f: big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype=dtypes.uint8, device=WARP_DEV).realize()
dump_oob(jit, f) for arr in npy_arrays.values():
f.seek(0) arr[:] = rng.standard_normal(arr.shape).astype(arr.dtype)
deserialized_jit = load_oob(f)
random_inputs_run(deserialized_jit, SEED, test_val=test_val, test_buffers=test_buffers) Device.default.synchronize()
return deserialized_jit start_time = time.perf_counter()
run_jit(**queues, frame=frame, big_frame=big_frame)
mid_time = time.perf_counter()
Device.default.synchronize()
print(f" [{i + 1}/3] enqueue {(mid_time - start_time) * 1e3:6.2f} ms -- total {(time.perf_counter() - start_time) * 1e3:6.2f} ms")
# TODO-SP: switch to dump_oob/load_oob on next full recompile of all models
return pickle.loads(pickle.dumps(run_jit)) if not prepare_only else run_jit
def _parse_size(size_str: str) -> tuple[int, int]: def _parse_size(size_str: str) -> tuple[int, int]:
@@ -286,6 +277,19 @@ def read_file_chunked_to_shm(path):
return shm_path return shm_path
def _compile_for_resolutions(camera_resolutions: list, model_size: tuple[int, int], frame_skip: int,
vision_runner, policy_runners: list, metadata: dict) -> dict:
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
return {
(cam_w, cam_h): {
name: compile_and_warmup(NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)), model_size, prepare_only,
frame_skip, vision_runner, policy_runners, metadata)
for name, prepare_only in [('warp_enqueue', True), ('run_policy', False)]
}
for cam_w, cam_h in camera_resolutions
}
def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]: def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]:
runners, keys = [], [] runners, keys = [], []
for name, onnx_arg in [('policy', args.policy_onnx), ('off_policy', args.off_policy_onnx), ('on_policy', args.on_policy_onnx)]: for name, onnx_arg in [('policy', args.policy_onnx), ('off_policy', args.off_policy_onnx), ('on_policy', args.on_policy_onnx)]:
@@ -296,18 +300,7 @@ def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]:
if __name__ == "__main__": if __name__ == "__main__":
if 'USB' in os.getenv('DEV', '') or os.getenv('USBGPU'):
from openpilot.system.hardware.chestnut.flash import link_up
for _ in range(10):
if link_up():
break
time.sleep(1)
else:
raise RuntimeError("Chestnut not ready, skipping big model build")
from openpilot.common.file_chunker import chunk_file, get_chunk_targets
from openpilot.selfdrive.modeld.get_model_metadata import make_metadata_dict from openpilot.selfdrive.modeld.get_model_metadata import make_metadata_dict
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from tinygrad.nn.onnx import OnnxRunner from tinygrad.nn.onnx import OnnxRunner
parser = argparse.ArgumentParser(description="Compile combined JIT pkl for sunnypilot modeld_v2") parser = argparse.ArgumentParser(description="Compile combined JIT pkl for sunnypilot modeld_v2")
@@ -324,8 +317,7 @@ if __name__ == "__main__":
parser.add_argument('--supercombo-onnx', help='supercombo ONNX (for supercombo)') parser.add_argument('--supercombo-onnx', help='supercombo ONNX (for supercombo)')
args = parser.parse_args() args = parser.parse_args()
model_w, model_h = args.model_size output_data = defaultdict(dict)
output_data = {}
args.vision_onnx = read_file_chunked_to_shm(args.vision_onnx) args.vision_onnx = read_file_chunked_to_shm(args.vision_onnx)
args.policy_onnx = read_file_chunked_to_shm(args.policy_onnx) args.policy_onnx = read_file_chunked_to_shm(args.policy_onnx)
@@ -356,31 +348,17 @@ if __name__ == "__main__":
vision_meta = output_data['metadata'].get('vision', {}) vision_meta = output_data['metadata'].get('vision', {})
derived_frame_skip = args.frame_skip or derive_frame_skip(vision_meta.get('input_shapes', {}), first_policy_meta.get('input_shapes', {})) derived_frame_skip = args.frame_skip or derive_frame_skip(vision_meta.get('input_shapes', {}), first_policy_meta.get('input_shapes', {}))
all_shapes = {key: value for meta in output_data['metadata'].values() for key, value in meta['input_shapes'].items()} output_data.update(_compile_for_resolutions(args.camera_resolutions, args.model_size, derived_frame_skip,
feat_meta = output_data['metadata'].get('vision') or output_data['metadata'].get('model') or output_data['metadata'].get('policy') vision_runner, policy_runners, output_data['metadata']))
assert feat_meta is not None
features_slice = feat_meta['output_slices']['hidden_state']
is_supercombo = vision_runner is None
print(f"Compiling run_policy JIT (model_size={model_w}x{model_h}, frame_skip={derived_frame_skip})...")
run_policy_func = make_run_policy(vision_runner, policy_runners, features_slice, derived_frame_skip, all_shapes)
run_policy_jit = TinyJit(run_policy_func, prune=True)
make_policy_queues = partial(generate_queues_and_npy, all_shapes, derived_frame_skip, is_supercombo=is_supercombo)
make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, model_h // 2, model_w // 2), device=WARP_DEV)
output_data['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS, make_policy_queues)
for cam_w, cam_h in args.camera_resolutions:
print(f"Compiling warp JIT for {cam_w}x{cam_h}...")
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=WARP_DEV)
warp = TinyJit(make_warp(nv12, model_w, model_h), prune=True)
output_data[(cam_w, cam_h)] = compile_jit(warp, make_random_warp_inputs, WARP_INPUTS, make_warp_queues)
with open(args.output, "wb") as file: with open(args.output, "wb") as file:
dump_oob(output_data, file) # TODO-SP: switch to dump_oob from openpilot/selfdrive/helpers on next full recompile of all models
pickle.dump(output_data, file)
pkl_size = os.path.getsize(args.output) pkl_size = os.path.getsize(args.output)
print(f"Saved combined JIT to {args.output} ({pkl_size / 1e6:.2f} MB)") print(f"Saved combined JIT to {args.output} ({pkl_size / 1e6:.2f} MB)")
from openpilot.common.file_chunker import chunk_file, get_chunk_targets
chunk_targets = get_chunk_targets(args.output, pkl_size) chunk_targets = get_chunk_targets(args.output, pkl_size)
chunk_file(args.output, chunk_targets) chunk_file(args.output, chunk_targets)
print(f"Chunked into {len(chunk_targets) - 1} file(s)") print(f"Chunked into {len(chunk_targets) - 1} file(s)")
+52 -101
View File
@@ -9,13 +9,17 @@ See the LICENSE.md file in the root directory for more details.
import os import os
os.environ['GMMU'] = '0' os.environ['GMMU'] = '0'
from openpilot.common.hardware import COMMA_HARDWARE from openpilot.common.hardware import COMMA_HARDWARE
from openpilot.selfdrive.modeld.helpers import usbgpu_present, load_oob os.environ['DEV'] = 'QCOM' if COMMA_HARDWARE else 'CPU'
USBGPU = "USBGPU" in os.environ
if USBGPU:
os.environ['DEV'] = 'AMD'
os.environ['AMD_IFACE'] = 'USB'
import pickle
import time import time
import numpy as np import numpy as np
import openpilot.cereal.messaging as messaging import openpilot.cereal.messaging as messaging
from openpilot.cereal import log from openpilot.cereal import log
from opendbc.car.structs import car from opendbc.car.structs import car
from openpilot.cereal.services import SERVICE_LIST
from setproctitle import setproctitle from setproctitle import setproctitle
from openpilot.cereal.messaging import PubMaster, SubMaster from openpilot.cereal.messaging import PubMaster, SubMaster
from openpilot.cereal.visionipc import VisionStreamType from openpilot.cereal.visionipc import VisionStreamType
@@ -23,6 +27,7 @@ from msgq.visionipc import VisionIpcClient, VisionBuf
from opendbc.car.car_helpers import get_demo_car_params from opendbc.car.car_helpers import get_demo_car_params
from tinygrad.tensor import Tensor from tinygrad.tensor import Tensor
from tinygrad.device import Device
from openpilot.common.file_chunker import open_file_chunked from openpilot.common.file_chunker import open_file_chunked
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
@@ -35,13 +40,12 @@ from openpilot.system import sentry
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value
from openpilot.selfdrive.modeld.modeld import ChestnutState
from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState, get_curvature_from_output from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState, get_curvature_from_output
from openpilot.sunnypilot.modeld_v2.constants import Plan from openpilot.sunnypilot.modeld_v2.constants import Plan
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues, make_supercombo_input_queues, WARP_INPUTS, POLICY_INPUTS from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
@@ -84,7 +88,7 @@ class ModelState(ModelStateBase):
inputs: dict[str, np.ndarray] inputs: dict[str, np.ndarray]
prev_desire: np.ndarray prev_desire: np.ndarray
def __init__(self, cam_w: int, cam_h: int, usbgpu: bool = False): def __init__(self, cam_w: int, cam_h: int):
ModelStateBase.__init__(self) ModelStateBase.__init__(self)
env_pkl = os.environ.get('COMBINED_MODEL_PKL') env_pkl = os.environ.get('COMBINED_MODEL_PKL')
@@ -99,7 +103,6 @@ class ModelState(ModelStateBase):
self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0")) self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0"))
self.MIN_LAT_CONTROL_SPEED = 0.3 self.MIN_LAT_CONTROL_SPEED = 0.3
self.PLANPLUS_CONTROL: float = 1.0 self.PLANPLUS_CONTROL: float = 1.0
self.usbgpu = usbgpu
pkl_path = _find_driving_pkl(model_bundle) pkl_path = _find_driving_pkl(model_bundle)
assert pkl_path is not None, "No driving pkl found — all models must be compiled with compile_modeld.py" assert pkl_path is not None, "No driving pkl found — all models must be compiled with compile_modeld.py"
@@ -107,20 +110,24 @@ class ModelState(ModelStateBase):
def _init_combined(self, pkl_path, cam_w, cam_h, bundle): def _init_combined(self, pkl_path, cam_w, cam_h, bundle):
cloudlog.warning(f"loading combined pkl: {pkl_path}") cloudlog.warning(f"loading combined pkl: {pkl_path}")
jits = load_oob(open_file_chunked(pkl_path)) # TODO-SP: switch to load_oob from openpilot/selfdrive/helpers on next full recompile of all models
jits = pickle.load(open_file_chunked(pkl_path))
self.WARP_DEV = 'QCOM' if COMMA_HARDWARE else 'CPU' self.DEV = Device.DEFAULT
self.DEV = 'AMD' if self.usbgpu else self.WARP_DEV self.WARP_DEV = 'CPU' if USBGPU else self.DEV
self.QUEUE_DEV = self.DEV self.QUEUE_DEV = self.DEV
metadata = jits['metadata'] metadata = jits['metadata']
self.is_legacy_model = 'run_policy' not in jits # remove after next recompile self._run_policy = jits[(cam_w, cam_h)]['run_policy']
if self.is_legacy_model: self._warp_enqueue = jits[(cam_w, cam_h)]['warp_enqueue']
self.warp = jits[(cam_w, cam_h)]['warp_enqueue']
self.run_policy = jits[(cam_w, cam_h)]['run_policy'] # TODO-SP: Remove legacy use_packed detection block after all models are recompiled
captured = getattr(self._run_policy, 'captured', None)
if captured is not None:
use_packed = 'packed_npy_inputs' in getattr(captured, 'expected_names', [])
else: else:
self.run_policy = jits['run_policy'] use_packed = True
self.warp = jits[(cam_w, cam_h)]
if 'model' in metadata: if 'model' in metadata:
model_metadata = metadata['model'] model_metadata = metadata['model']
@@ -129,9 +136,10 @@ class ModelState(ModelStateBase):
self._policy_slices_list = [] self._policy_slices_list = []
self._combined_model_type = 'supercombo' self._combined_model_type = 'supercombo'
self._vision_input_names = [key for key in model_metadata['input_shapes'] if 'img' in key] self._vision_input_names = [key for key in model_metadata['input_shapes'] if 'img' in key]
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_supercombo_input_queues
frame_skip = derive_frame_skip({}, model_metadata['input_shapes']) frame_skip = derive_frame_skip({}, model_metadata['input_shapes'])
self.input_queues, self.numpy_inputs = make_supercombo_input_queues(model_metadata['input_shapes'], self.input_queues, self.numpy_inputs = make_supercombo_input_queues(model_metadata['input_shapes'],
frame_skip, device=self.QUEUE_DEV) frame_skip, device=self.QUEUE_DEV, use_packed=use_packed)
else: else:
vision_metadata = metadata['vision'] vision_metadata = metadata['vision']
policy_keys = [k for k in metadata if k != 'vision'] policy_keys = [k for k in metadata if k != 'vision']
@@ -144,12 +152,13 @@ class ModelState(ModelStateBase):
self._policy_slices_list = [metadata[k]['output_slices'] for k in policy_keys] self._policy_slices_list = [metadata[k]['output_slices'] for k in policy_keys]
self.policy_output_slices = self._policy_slices_list[0] self.policy_output_slices = self._policy_slices_list[0]
self._has_on_policy = any('on' in k.lower() for k in policy_keys) self._has_on_policy = any('on' in k.lower() for k in policy_keys)
self._vision_input_names = [key for key in vision_metadata['input_shapes'] if 'img' in key] first_policy_metadata = metadata[policy_keys[0]]
first_policy_meta = metadata[policy_keys[0]] vision_input_shapes = vision_metadata['input_shapes']
frame_skip = derive_frame_skip(vision_metadata['input_shapes'], first_policy_meta['input_shapes']) policy_input_shapes = first_policy_metadata['input_shapes']
self.input_queues, self.numpy_inputs = make_split_input_queues(vision_metadata['input_shapes'], self._vision_input_names = [k for k in vision_input_shapes if 'img' in k]
first_policy_meta['input_shapes'], frame_skip = derive_frame_skip(vision_input_shapes, policy_input_shapes)
frame_skip, device=self.QUEUE_DEV) self.input_queues, self.numpy_inputs = make_split_input_queues(vision_input_shapes, policy_input_shapes,
frame_skip, device=self.QUEUE_DEV, use_packed=use_packed)
self._desire_key = next(key for key in self.numpy_inputs if key.startswith('desire')) self._desire_key = next(key for key in self.numpy_inputs if key.startswith('desire'))
self._road_key = next(key for key in self._vision_input_names if 'big' not in key) self._road_key = next(key for key in self._vision_input_names if 'big' not in key)
@@ -177,33 +186,10 @@ class ModelState(ModelStateBase):
self.frame_buf_params = dict.fromkeys(self._vision_input_names, nv12_info) self.frame_buf_params = dict.fromkeys(self._vision_input_names, nv12_info)
yuv_size = self.frame_buf_params[self._road_key][3] yuv_size = self.frame_buf_params[self._road_key][3]
frame_tensor = Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize() self._warp_enqueue(
big_frame_tensor = Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize() **self.input_queues,
frame=Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize(),
if self.is_legacy_model: # Remove this conditional hack after recompile big_frame=Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize())
self.warp(**self.input_queues, frame=frame_tensor, big_frame=big_frame_tensor)
else:
self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=frame_tensor, big_frame=big_frame_tensor)
if self.usbgpu:
self.warmup()
def warmup(self) -> None:
dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self._vision_input_names}
transforms = {k: np.eye(3, dtype=np.float32) for k in [self._road_key, self._wide_key] if k}
dummy_inputs = {}
for k, v in self.numpy_inputs.items():
if k not in ['tfm', 'big_tfm', 'prev_feat']:
dummy_inputs[k] = np.zeros(v.shape, dtype=v.dtype)
self.run(dummy_frames, transforms, dummy_inputs, prepare_only=False)
for v in self.numpy_inputs.values():
v[:] = 0
self.prev_desire[:] = 0
self.full_frames.clear()
self._blob_cache.clear()
@property @property
@@ -241,17 +227,11 @@ class ModelState(ModelStateBase):
self.numpy_inputs['tfm'][:, :] = transforms[road_key].reshape(3, 3) self.numpy_inputs['tfm'][:, :] = transforms[road_key].reshape(3, 3)
self.numpy_inputs['big_tfm'][:, :] = transforms[wide_key].reshape(3, 3) self.numpy_inputs['big_tfm'][:, :] = transforms[wide_key].reshape(3, 3)
if self.is_legacy_model: # remove after next recompile if prepare_only:
if prepare_only: self._warp_enqueue(**self.input_queues, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key])
self.warp(**self.input_queues, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) return None
return None
raw_outputs = self.run_policy(**self.input_queues, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) raw_outputs = self._run_policy(**self.input_queues, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key])
else:
if prepare_only:
self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key])
return None
warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key])
raw_outputs = self.run_policy(**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped)
if self._combined_model_type == 'supercombo': if self._combined_model_type == 'supercombo':
model_output = raw_outputs.numpy().flatten() model_output = raw_outputs.numpy().flatten()
@@ -287,9 +267,10 @@ class ModelState(ModelStateBase):
buf[0, :-1] = buf[0, 1:] buf[0, :-1] = buf[0, 1:]
buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0 buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0
if self.usbgpu and not np.all(np.isfinite(outputs.get('plan', np.array([0.])))): # TODO-SP: This is a hack to prevent GPU corruption by calculating in CPU space, it can be removed on next recompile
cloudlog.error("model output not finite, dropping frame") if 'prev_feat' not in self.numpy_inputs and 'feat_q' in self.input_queues:
return None feat_val = self.input_queues['feat_q'].numpy()
self.input_queues['feat_q'].assign(feat_val).realize()
return outputs return outputs
@@ -297,8 +278,8 @@ class ModelState(ModelStateBase):
lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action: lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action:
if 'action' not in model_output: if 'action' not in model_output:
plan = model_output['plan'][0] plan = model_output['plan'][0]
desired_accel = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS, desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS,
action_t=long_action_t) action_t=long_action_t)
curvature_plan = (plan + (self.PLANPLUS_CONTROL - 1.0) * model_output['planplus'][0] curvature_plan = (plan + (self.PLANPLUS_CONTROL - 1.0) * model_output['planplus'][0]
if 'planplus' in model_output and self.PLANPLUS_CONTROL != 1.0 else plan) if 'planplus' in model_output and self.PLANPLUS_CONTROL != 1.0 else plan)
@@ -306,8 +287,8 @@ class ModelState(ModelStateBase):
else: else:
desired_accel = model_output['action'][0, 1] desired_accel = model_output['action'][0, 1]
desired_curvature = model_output['action'][0, 0] / (max(1.0, v_ego))**2 desired_curvature = model_output['action'][0, 0] / (max(1.0, v_ego))**2
should_stop = (v_ego < 0.3 and desired_accel < 0.1)
stop = v_ego < 0.3 and desired_accel < 0.1
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS) desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models
@@ -316,7 +297,7 @@ class ModelState(ModelStateBase):
else: else:
desired_curvature = prev_action.desiredCurvature desired_curvature = prev_action.desiredCurvature
return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature), desiredAcceleration=float(desired_accel), shouldStop=bool(stop)) return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature),desiredAcceleration=float(desired_accel), shouldStop=bool(should_stop))
def main(demo=False): def main(demo=False):
@@ -327,14 +308,6 @@ def main(demo=False):
setproctitle(PROCESS_NAME) setproctitle(PROCESS_NAME)
config_realtime_process(7, 54) config_realtime_process(7, 54)
USBGPU = usbgpu_present()
if USBGPU:
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
params = Params()
params.put_bool("UsbGpuLoading", USBGPU)
params.remove("UsbGpuActive")
# visionipc clients # visionipc clients
while True: while True:
available_streams = VisionIpcClient.available_streams("camerad", block=False) available_streams = VisionIpcClient.available_streams("camerad", block=False)
@@ -359,34 +332,15 @@ def main(demo=False):
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})") cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
cloudlog.warning("loading model") cloudlog.warning("loading model")
st = time.monotonic() model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height)
cloudlog.warning("models loaded, modeld starting")
model = None
if USBGPU:
import threading
def load():
nonlocal model
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, usbgpu=True)
t = threading.Thread(target=load, daemon=True)
t.start()
t.join(60)
if model is None:
params.put_bool("UsbGpuActive", False)
raise RuntimeError("eGPU model load failed or timed out (60s)")
params.put_bool("UsbGpuActive", True)
else:
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, usbgpu=False)
params.put_bool("UsbGpuLoading", False)
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
# messaging # messaging
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if USBGPU else []) pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
pm = PubMaster(pub_socks)
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"])
publish_state = PublishState() publish_state = PublishState()
chestnut_state = ChestnutState(pm, USBGPU) if USBGPU else None params = Params()
# setup filter to track dropped frames # setup filter to track dropped frames
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / model.constants.MODEL_FREQ) frame_dropped_filter = FirstOrderFilter(0., 10., 1. / model.constants.MODEL_FREQ)
@@ -524,7 +478,6 @@ def main(demo=False):
fill_model_msg(drivingdata_send, modelv2_send, model_output, action, fill_model_msg(drivingdata_send, modelv2_send, model_output, action,
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, meta_constants) frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, meta_constants)
modelv2_send.modelV2.big = model.usbgpu
desire_state = modelv2_send.modelV2.meta.desireState desire_state = modelv2_send.modelV2.meta.desireState
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
@@ -545,8 +498,6 @@ def main(demo=False):
pm.send('modelDataV2SP', mdv2sp_send) pm.send('modelDataV2SP', mdv2sp_send)
last_vipc_frame_id = meta_main.frame_id last_vipc_frame_id = meta_main.frame_id
if chestnut_state is not None and run_count % round(model.constants.MODEL_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0:
chestnut_state.send()
if __name__ == "__main__": if __name__ == "__main__":
try: try:
@@ -6,6 +6,7 @@ See the LICENSE.md file in the root directory for more details.
""" """
import pathlib import pathlib
import pickle
import tempfile import tempfile
import openpilot.sunnypilot.models.helpers as helpers import openpilot.sunnypilot.models.helpers as helpers
@@ -163,16 +164,14 @@ ARCHETYPES = {
def make_pkl_data(archetype): def make_pkl_data(archetype):
return { return {
'metadata': archetype.metadata_structure, 'metadata': archetype.metadata_structure,
'run_policy': _noop_jit, (CAM_W, CAM_H): {'run_policy': _noop_jit, 'warp_enqueue': _noop_jit},
(CAM_W, CAM_H): _noop_jit,
} }
def write_pkl(tmp_path, archetype): def write_pkl(tmp_path, archetype):
from openpilot.selfdrive.modeld.helpers import dump_oob
pkl_path = tmp_path / 'driving_test_tinygrad.pkl' pkl_path = tmp_path / 'driving_test_tinygrad.pkl'
with open(pkl_path, 'wb') as f: with open(pkl_path, 'wb') as f:
dump_oob(make_pkl_data(archetype), f) pickle.dump(make_pkl_data(archetype), f)
return pkl_path return pkl_path
@@ -33,7 +33,7 @@ class TestRecoveryPower(OpenpilotTestCase):
def mock_accel(plan_vel, plan_accel, t_idxs, action_t=0.0): def mock_accel(plan_vel, plan_accel, t_idxs, action_t=0.0):
recorded_vel.append(plan_vel.copy()) recorded_vel.append(plan_vel.copy())
return 0.0 return 0.0, False
def mock_curvature(output, plan, vego, lat_action_t, mlsim): def mock_curvature(output, plan, vego, lat_action_t, mlsim):
recorded_curv_plans.append(plan.copy()) recorded_curv_plans.append(plan.copy())
+11 -23
View File
@@ -13,7 +13,6 @@ from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.common.hardware.hw import Paths from openpilot.common.hardware.hw import Paths
from openpilot.sunnypilot.models.helpers import is_bundle_version_compatible from openpilot.sunnypilot.models.helpers import is_bundle_version_compatible
from openpilot.selfdrive.modeld.helpers import usbgpu_present
from openpilot.cereal import custom from openpilot.cereal import custom
@@ -104,11 +103,11 @@ class ModelParser:
class ModelCache: class ModelCache:
"""Handles caching of model data to avoid frequent remote fetches""" """Handles caching of model data to avoid frequent remote fetches"""
def __init__(self, params: Params, cache_timeout: int = int(3600 * 1e9), suffix: str = ""): def __init__(self, params: Params, cache_timeout: int = int(3600 * 1e9)):
self.params = params self.params = params
self.cache_timeout = cache_timeout self.cache_timeout = cache_timeout
self._LAST_SYNC_KEY = f"ModelManager_LastSyncTime{suffix}" self._LAST_SYNC_KEY = "ModelManager_LastSyncTime"
self._CACHE_KEY = f"ModelManager_ModelsCache{suffix}" self._CACHE_KEY = "ModelManager_ModelsCache"
def _is_expired(self) -> bool: def _is_expired(self) -> bool:
"""Checks if the cache has expired""" """Checks if the cache has expired"""
@@ -140,37 +139,24 @@ class ModelCache:
class ModelFetcher: class ModelFetcher:
"""Handles fetching and caching of model data from remote source""" """Handles fetching and caching of model data from remote source"""
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v20.json" MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v18.json"
MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v20.json"
def __init__(self, params: Params): def __init__(self, params: Params):
self.params = params self.params = params
self.model_parser = ModelParser()
self._is_usbgpu: bool | None = None
self.model_cache = ModelCache(params) self.model_cache = ModelCache(params)
self.model_url = self.MODEL_URL self.model_parser = ModelParser()
self._update_model_source()
def _update_model_source(self) -> None:
"""Updates what json to use based on usbgpu availability"""
is_usbgpu = usbgpu_present()
if is_usbgpu != self._is_usbgpu:
self._is_usbgpu = is_usbgpu
self.model_cache = ModelCache(self.params, suffix="_USBGPU" if is_usbgpu else "")
self.model_url = self.MODEL_URL_USBGPU if is_usbgpu else self.MODEL_URL
self.params.put("ModelManager_ActiveJson", self.model_url, block=True)
def _fetch_and_cache_models(self) -> list[custom.ModelManagerSP.ModelBundle] | None: def _fetch_and_cache_models(self) -> list[custom.ModelManagerSP.ModelBundle] | None:
"""Fetches fresh model data from remote and updates cache. """Fetches fresh model data from remote and updates cache.
Returns None on transport errors. Raises on 404 and other fatal HTTP errors. Returns None on transport errors. Raises on 404 and other fatal HTTP errors.
""" """
try: try:
response = requests.get(self.model_url, timeout=10) response = requests.get(self.MODEL_URL, timeout=10)
# Explicitly handle 404 differently # Explicitly handle 404 differently
if response.status_code == 404: if response.status_code == 404:
cloudlog.error(f"Models URL returned 404 Not Found: {self.model_url}") cloudlog.error(f"Models URL returned 404 Not Found: {self.MODEL_URL}")
raise HTTPError(f"404 Not Found: {self.model_url}", response=response) raise HTTPError(f"404 Not Found: {self.MODEL_URL}", response=response)
# Raise for any other 4xx/5xx # Raise for any other 4xx/5xx
response.raise_for_status() response.raise_for_status()
@@ -193,7 +179,6 @@ class ModelFetcher:
def get_available_bundles(self) -> list[custom.ModelManagerSP.ModelBundle]: def get_available_bundles(self) -> list[custom.ModelManagerSP.ModelBundle]:
"""Gets the list of available models, with smart cache handling""" """Gets the list of available models, with smart cache handling"""
self._update_model_source()
cached_data, is_expired = self.model_cache.get() cached_data, is_expired = self.model_cache.get()
if cached_data and not is_expired: if cached_data and not is_expired:
@@ -217,7 +202,10 @@ if __name__ == "__main__":
for bundle in bundles: for bundle in bundles:
for model in bundle.models: for model in bundle.models:
model_overrides = {override.key: override.value for override in bundle.overrides} model_overrides = {override.key: override.value for override in bundle.overrides}
# Print model details
print(f"Bundle: {bundle.internalName}, Type: {model.type}, Status: {bundle.status}, Overrides: {model_overrides}") print(f"Bundle: {bundle.internalName}, Type: {model.type}, Status: {bundle.status}, Overrides: {model_overrides}")
# Print artifact details
print(f"Artifact: {model.artifact.fileName}, Download URI: {model.artifact.downloadUri.uri}") print(f"Artifact: {model.artifact.fileName}, Download URI: {model.artifact.downloadUri.uri}")
# Print metadata details
if model.artifact.chunks: if model.artifact.chunks:
print(f"Contains {len(model.artifact.chunks)} chunks.") print(f"Contains {len(model.artifact.chunks)} chunks.")
+1 -1
View File
@@ -18,7 +18,7 @@ from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRai
from openpilot.common.hardware.hw import Paths from openpilot.common.hardware.hw import Paths
# SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO # SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO
REQUIRED_JSON_VERSION = 17 REQUIRED_JSON_VERSION = 16
CUSTOM_MODEL_PATH = Paths.model_root() CUSTOM_MODEL_PATH = Paths.model_root()
METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl' METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl'
@@ -1,13 +1,12 @@
import requests import requests
from openpilot.common.params import Params
from openpilot.sunnypilot.models.tinygrad_ref import get_tinygrad_ref from openpilot.sunnypilot.models.tinygrad_ref import get_tinygrad_ref
from openpilot.sunnypilot.models.fetcher import ModelFetcher from openpilot.sunnypilot.models.fetcher import ModelFetcher
from openpilot.common.test import OpenpilotTestCase from openpilot.common.test import OpenpilotTestCase
def fetch_tinygrad_ref(): def fetch_tinygrad_ref():
fetcher = ModelFetcher(Params()) response = requests.get(ModelFetcher.MODEL_URL, timeout=10)
response = requests.get(fetcher.model_url, timeout=10)
response.raise_for_status() response.raise_for_status()
json_data = response.json() json_data = response.json()
return json_data.get("tinygrad_ref") return json_data.get("tinygrad_ref")
@@ -2172,6 +2172,26 @@
"title": "Hyundai / Kia / Genesis Settings", "title": "Hyundai / Kia / Genesis Settings",
"description": "", "description": "",
"items": [ "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", "key": "HyundaiLongitudinalTuning",
"widget": "multiple_button", "widget": "multiple_button",
@@ -10,6 +10,17 @@ sections:
title: Hyundai / Kia / Genesis Settings title: Hyundai / Kia / Genesis Settings
description: '' description: ''
items: 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 - key: HyundaiLongitudinalTuning
widget: multiple_button widget: multiple_button
title: Custom Longitudinal Tuning title: Custom Longitudinal Tuning
+2 -7
View File
@@ -33,7 +33,6 @@ USBDEVFS_SETCONFIGURATION = 0x80045505
USBDEVFS_CLAIMINTERFACE = 0x8004550F USBDEVFS_CLAIMINTERFACE = 0x8004550F
USBDEVFS_RESET = 0x5514 USBDEVFS_RESET = 0x5514
USBDEVFS_CLEAR_HALT = 0x80045515 USBDEVFS_CLEAR_HALT = 0x80045515
MAX_REGISTER_READ_SIZE = 255
_deadline = float("inf") _deadline = float("inf")
@@ -147,7 +146,6 @@ def claim_interface(path, setup=False):
class Flash: class Flash:
def __init__(self): def __init__(self):
self.fd = -1 self.fd = -1
self.max_register_read_size = MAX_REGISTER_READ_SIZE
def close(self): def close(self):
if self.fd >= 0: if self.fd >= 0:
@@ -162,9 +160,6 @@ class Flash:
if in_rom_bootloader(vid_pid, product): if in_rom_bootloader(vid_pid, product):
raise RomFallback("chestnut fell back to the ROM bootloader") raise RomFallback("chestnut fell back to the ROM bootloader")
if path is not None: if path is not None:
speed = int(open(path + "/speed").read())
# USB2 firmware truncates larger reads to one full packet without a terminating ZLP.
self.max_register_read_size = 64 if speed < 5000 else MAX_REGISTER_READ_SIZE
self.fd = claim_interface(path) self.fd = claim_interface(path)
return return
time.sleep(0.1) time.sleep(0.1)
@@ -236,8 +231,8 @@ class Flash:
while len(out) < length: while len(out) < length:
n = min(4096, length - len(out)) n = min(4096, length - len(out))
self.transaction(0x03, addr + len(out), max(4096, n)) self.transaction(0x03, addr + len(out), max(4096, n))
for off in range(0, n, self.max_register_read_size): for off in range(0, n, 255):
out += self.reg_read(0x7000 + off, min(self.max_register_read_size, n - off)) out += self.reg_read(0x7000 + off, min(255, n - off))
return bytes(out) return bytes(out)
def erase_sector(self, addr): def erase_sector(self, addr):
-3
View File
@@ -16,7 +16,6 @@ from openpilot.common.utils import strip_deprecated_keys
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.realtime import DT_HW from openpilot.common.realtime import DT_HW
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, usbgpu_compiled
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
@@ -239,7 +238,6 @@ def hardware_thread(end_event, hw_queue) -> None:
fan_controller = FanController(int(1./DT_HW)) fan_controller = FanController(int(1./DT_HW))
chestnut = Chestnut() chestnut = Chestnut()
big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or usbgpu_compiled()
while not end_event.is_set(): while not end_event.is_set():
sm.update(PANDA_STATES_TIMEOUT) sm.update(PANDA_STATES_TIMEOUT)
@@ -301,7 +299,6 @@ def hardware_thread(end_event, hw_queue) -> None:
set_usb_state(msg.deviceState, last_hw_state.usb_state) set_usb_state(msg.deviceState, last_hw_state.usb_state)
chestnut.update(started_ts is None, last_hw_state.usb_state) chestnut.update(started_ts is None, last_hw_state.usb_state)
set_offroad_alert_if_changed("Offroad_ChestnutBranch", msg.deviceState.chestnutPresent and not big_model_available)
# this subset is only used for offroad # this subset is only used for offroad
temp_sources = [ temp_sources = [
+3 -3
View File
@@ -14,7 +14,7 @@
#include "sunnypilot/common/version.h" #include "sunnypilot/common/version.h"
// ***** log metadata ***** // ***** log metadata *****
kj::Array<capnp::word> logger_build_init_data(bool route_log) { kj::Array<capnp::word> logger_build_init_data() {
uint64_t wall_time = nanos_since_epoch(); uint64_t wall_time = nanos_since_epoch();
MessageBuilder msg; MessageBuilder msg;
@@ -72,7 +72,7 @@ kj::Array<capnp::word> logger_build_init_data(bool route_log) {
"df -h", // usage for all filesystems "df -h", // usage for all filesystems
}; };
auto hw_logs = Hardware::get_init_logs(route_log); auto hw_logs = Hardware::get_init_logs();
auto commands = init.initCommands().initEntries(log_commands.size() + hw_logs.size()); auto commands = init.initCommands().initEntries(log_commands.size() + hw_logs.size());
for (int i = 0; i < log_commands.size(); i++) { for (int i = 0; i < log_commands.size(); i++) {
@@ -166,7 +166,7 @@ static void log_sentinel(LoggerState *log, SentinelType type, int exit_signal =
LoggerState::LoggerState(const std::string &log_root) { LoggerState::LoggerState(const std::string &log_root) {
route_name = logger_get_identifier("RouteCount"); route_name = logger_get_identifier("RouteCount");
route_path = log_root + "/" + route_name; route_path = log_root + "/" + route_name;
init_data = logger_build_init_data(true); init_data = logger_build_init_data();
} }
LoggerState::~LoggerState() { LoggerState::~LoggerState() {
+1 -1
View File
@@ -32,6 +32,6 @@ protected:
std::unique_ptr<ZstdFileWriter> rlog, qlog; std::unique_ptr<ZstdFileWriter> rlog, qlog;
}; };
kj::Array<capnp::word> logger_build_init_data(bool route_log = false); kj::Array<capnp::word> logger_build_init_data();
std::string logger_get_identifier(std::string key); std::string logger_get_identifier(std::string key);
std::string zstd_decompress(const std::string &in); std::string zstd_decompress(const std::string &in);
+10 -28
View File
@@ -3,12 +3,11 @@ import sys
import time import time
import signal import signal
import struct import struct
import threading
import requests import requests
import urllib.parse import urllib.parse
from datetime import datetime, UTC from datetime import datetime, UTC
from openpilot.cereal import log, messaging from openpilot.cereal import messaging
from openpilot.common.api import Api from openpilot.common.api import Api
from openpilot.common.time_helpers import system_time_valid from openpilot.common.time_helpers import system_time_valid
from openpilot.common.params import Params from openpilot.common.params import Params
@@ -47,6 +46,7 @@ def get_assistnow_messages() -> list[bytes]:
params = Params() params = Params()
if token := params.get('AssistNowToken'): if token := params.get('AssistNowToken'):
cloudlog.warning("Downloading AssistNow data directly from u-blox") cloudlog.warning("Downloading AssistNow data directly from u-blox")
# TODO: implement adding the last known location
r = requests.get("https://online-live2.services.u-blox.com/GetOnlineData.ashx", params=urllib.parse.urlencode({ r = requests.get("https://online-live2.services.u-blox.com/GetOnlineData.ashx", params=urllib.parse.urlencode({
'token': token, 'token': token,
'gnss': 'gps,glo', 'gnss': 'gps,glo',
@@ -240,6 +240,14 @@ def init_pigeon(pigeon: TTYPigeon) -> bool:
)) ))
pigeon.send_with_ack(msg, ack=UBLOX_ASSIST_ACK) pigeon.send_with_ack(msg, ack=UBLOX_ASSIST_ACK)
# A configured u-blox token takes precedence over comma's AGPS proxy.
try:
for msg in get_assistnow_messages():
pigeon.send_with_ack(msg, ack=UBLOX_ASSIST_ACK)
cloudlog.warning("AssistNow messages sent")
except Exception:
cloudlog.warning("failed to get AssistNow messages")
cloudlog.warning("Pigeon GPS on!") cloudlog.warning("Pigeon GPS on!")
break break
except TimeoutError: except TimeoutError:
@@ -279,38 +287,12 @@ def run_receiving(duration: int = 0):
start_time = time.monotonic() start_time = time.monotonic()
last_almanac_save = time.monotonic() last_almanac_save = time.monotonic()
assist_attempted = False
assist_messages = None
def download_assistnow() -> None:
nonlocal assist_messages
sm = messaging.SubMaster(['deviceState'])
while assist_messages is None:
sm.update(1000)
if system_time_valid() and sm['deviceState'].networkType != log.DeviceState.NetworkType.none:
try:
assist_messages = get_assistnow_messages()
except Exception:
cloudlog.warning("failed to get AssistNow messages")
time.sleep(10.)
threading.Thread(target=download_assistnow, daemon=True).start()
while (duration == 0) or (time.monotonic() - start_time < duration): while (duration == 0) or (time.monotonic() - start_time < duration):
if assist_messages is not None and not assist_attempted:
assist_attempted = True
try:
for msg in assist_messages:
pigeon.send_with_ack(msg, ack=UBLOX_ASSIST_ACK)
cloudlog.warning("AssistNow messages sent")
except Exception:
cloudlog.warning("failed to send AssistNow messages")
dat = pigeon.receive() dat = pigeon.receive()
if len(dat) > 0: if len(dat) > 0:
if dat[0] == 0x00: if dat[0] == 0x00:
cloudlog.warning("received invalid data from ublox, re-initing!") cloudlog.warning("received invalid data from ublox, re-initing!")
init(pigeon) init(pigeon)
assist_attempted = False
continue continue
# send out to socket # send out to socket
@@ -4,15 +4,7 @@ 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. 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. See the LICENSE.md file in the root directory for more details.
""" """
from collections.abc import Callable
import pyray as rl
from openpilot.system.ui.lib.application import FontWeight
from openpilot.system.ui.sunnypilot.lib.styles import style
from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.list_view import BUTTON_WIDTH, BUTTON_HEIGHT, TEXT_PADDING, _resolve_value
class NoElideButtonAction(ButtonActionSP): class NoElideButtonAction(ButtonActionSP):
@@ -20,38 +12,6 @@ class NoElideButtonAction(ButtonActionSP):
return super().get_width_hint() + 1 return super().get_width_hint() + 1
class ScrollingButtonAction(ButtonActionSP):
"""ButtonActionSP whose value scrolls instead of eliding when it doesn't fit."""
def __init__(self, text: str | Callable[[], str], width: int = style.BUTTON_ACTION_WIDTH,
enabled: bool | Callable[[], bool] = True):
super().__init__(text=text, width=width, enabled=enabled)
self._value_label = UnifiedLabel("", font_size=style.ITEM_TEXT_FONT_SIZE, font_weight=FontWeight.NORMAL,
text_color=self._value_color, scroll=True,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
def set_value(self, value: str | Callable[[], str], color: rl.Color = style.ITEM_TEXT_VALUE_COLOR):
if self.value != _resolve_value(value, ""):
self._value_label.reset_scroll()
super().set_value(value, color)
self._value_label.set_text(value)
self._value_label.set_text_color(color)
def _render(self, rect: rl.Rectangle) -> bool:
"""Duplicate of ButtonActionSP._render, with the value drawn by a scrolling label"""
self._button.set_text(self.text)
self._button.set_enabled(_resolve_value(self.enabled))
button_rect = rl.Rectangle(rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT)
self._button.render(button_rect)
if self.value:
self._value_label.render(rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height))
pressed = self._pressed
self._pressed = False
return pressed
class AlertFadeAnimator: class AlertFadeAnimator:
def __init__(self, target_fps: int, duration_on: float = 0.75, rc: float = 0.05): def __init__(self, target_fps: int, duration_on: float = 0.75, rc: float = 0.05):
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
@@ -1,166 +0,0 @@
"""
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 math
import numpy as np
import pyray as rl
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.sunnypilot.lib.styles import style
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.list_view import ItemAction
FONT_SIZE = style.ITEM_TEXT_FONT_SIZE
ICON_SIZE = 56
ICON_PADDING = 12
BAR_WIDTH = 1100
BAR_HEIGHT = 20
BAR_GAP = 16
BAR_RADIUS = BAR_HEIGHT / 2
CAPSULE_POINTS = 24
RAIL_COLOR = rl.Color(60, 60, 60, 255)
FILL_COLOR = rl.Color(30, 121, 232, 255)
# rl.WHITE is a tuple; the shimmer path reads .a off the color
TEXT_COLOR = rl.Color(255, 255, 255, 255)
SWEEP_SPEED = 550.0 # px/s
SWEEP_BAND = 240.0 # highlight half-width, px
SWEEP_DIM = 0.65
class DownloadStatusAction(ItemAction):
"""Model download row: a name + percent over a progress rail while downloading, a name + icon otherwise."""
def __init__(self):
super().__init__(width=BAR_WIDTH)
self.name = ""
self.status_text = ""
self.downloading = False
self.text_color = rl.GRAY
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
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
self._name_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
self._percent_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
def update(self, name, downloading=False, progress=0.0, status_text="", text_color=rl.GRAY, icon=None, icon_color=None):
if downloading and not self.downloading:
self._name_label.reset_shimmer()
self._progress.x = progress
self._sweep = 0.0
self.name = name
self.downloading = downloading
self.status_text = status_text
self.text_color = text_color
self.icon = icon
self.icon_color = icon_color
self._name_label._shimmer = downloading
if downloading:
self._progress.update(progress)
self._sweep += SWEEP_SPEED / gui_app.target_fps
@property
def _idle_text(self) -> str:
return f"{self.name} - {self.status_text}" if self.status_text else self.name
def get_width_hint(self) -> float:
if self.downloading:
return BAR_WIDTH
width = measure_text_cached(self._font, self._idle_text, FONT_SIZE).x
if self.icon:
width += ICON_SIZE + ICON_PADDING
return width
def _render(self, rect: rl.Rectangle):
if self.downloading:
self._render_downloading(rect)
else:
self._render_idle(rect)
def _sweep_gradient(self, width: float) -> Gradient:
# clearance at both ends keeps the wrap offscreen
center = (self._sweep % (width + 2 * SWEEP_BAND)) - SWEEP_BAND
def band(x: float) -> float:
return max(0.0, 1.0 - abs(x - center) / SWEEP_BAND)
# sampling the corners is exact for a piecewise linear band
xs = sorted({0.0, width} | {min(max(center + o, 0.0), width) for o in (-SWEEP_BAND, 0.0, SWEEP_BAND)}, reverse=True)
# the gradient axis runs right-to-left in screen space
stops = [1.0 - x / width for x in xs]
# alpha here is the lift over the SWEEP_DIM base, not the final opacity
colors = [rl.Color(FILL_COLOR.r, FILL_COLOR.g, FILL_COLOR.b, int(255 * band(x))) for x in xs]
return Gradient(start=(0.0, 0.0), end=(1.0, 0.0), colors=colors, stops=stops)
@staticmethod
def _capsule(rect: rl.Rectangle) -> np.ndarray:
"""Rounded-end ribbon so the gradient covers the caps."""
r = rect.height / 2
cy = rect.y + r
top, bottom = [], []
for i in range(CAPSULE_POINTS):
x = rect.x + rect.width * i / (CAPSULE_POINTS - 1)
d = min(x - rect.x, rect.x + rect.width - x, r)
h = math.sqrt(max(r * r - (r - d) ** 2, 0.0))
top.append((x, cy - h))
bottom.append((x, cy + h))
return np.array(top + bottom[::-1], dtype=np.float32)
def _draw_fill(self, rail: rl.Rectangle, fill_width: float):
if fill_width <= 0:
return
fill = rl.Rectangle(rail.x, rail.y, fill_width, rail.height)
rl.draw_rectangle_rounded(fill, 1.0, 10, rl.Color(FILL_COLOR.r, FILL_COLOR.g, FILL_COLOR.b, int(255 * SWEEP_DIM)))
draw_polygon(fill, self._capsule(fill), gradient=self._sweep_gradient(fill_width))
def _render_downloading(self, rect: rl.Rectangle):
percent = f"{int(self._progress.x)}%"
text_height = measure_text_cached(self._font, percent, FONT_SIZE).y
top = rect.y + (rect.height - (text_height + BAR_GAP + BAR_HEIGHT)) / 2
text_rect = rl.Rectangle(rect.x, top, rect.width, text_height)
self._name_label.set_text(self.name)
self._name_label.render(text_rect)
self._percent_label.set_text(percent)
self._percent_label.render(text_rect)
rail = rl.Rectangle(rect.x, top + text_height + BAR_GAP, rect.width, BAR_HEIGHT)
rl.draw_rectangle_rounded(rail, 1.0, 10, RAIL_COLOR)
self._draw_fill(rail, max(0.0, min(rect.width, rect.width * (self._progress.x / 100.0))))
def _render_idle(self, rect: rl.Rectangle):
text = self._idle_text
text_size = measure_text_cached(self._font, text, FONT_SIZE)
right = rect.x + rect.width
if self.icon:
texture = gui_app.texture(self.icon, ICON_SIZE, ICON_SIZE, keep_aspect_ratio=True)
rl.draw_texture_v(texture, rl.Vector2(right - texture.width, rect.y + (rect.height - texture.height) / 2),
self.icon_color or self.text_color)
right -= texture.width + ICON_PADDING
rl.draw_text_ex(self._font, text, rl.Vector2(right - text_size.x, rect.y + (rect.height - text_size.y) / 2),
FONT_SIZE, 0, self.text_color)
def download_status_item(title):
return ListItemSP(title=title, action_item=DownloadStatusAction(), title_color=style.ITEM_TEXT_COLOR)
+3
View File
@@ -207,10 +207,13 @@ def handle_agnos_update() -> None:
set_consistent_flag(False) set_consistent_flag(False)
cloudlog.info(f"Beginning background installation for AGNOS {updated_version}") cloudlog.info(f"Beginning background installation for AGNOS {updated_version}")
set_offroad_alert("Offroad_NeosUpdate", True)
manifest_path = os.path.join(OVERLAY_MERGED, "openpilot/system/hardware/comma/agnos.json") manifest_path = os.path.join(OVERLAY_MERGED, "openpilot/system/hardware/comma/agnos.json")
target_slot_number = get_target_slot_number() target_slot_number = get_target_slot_number()
flash_agnos_update(manifest_path, target_slot_number, cloudlog) flash_agnos_update(manifest_path, target_slot_number, cloudlog)
set_offroad_alert("Offroad_NeosUpdate", False)
class Updater: class Updater:
@@ -7,7 +7,7 @@ from openpilot.common.test import OpenpilotTestCase
from openpilot.cereal import messaging, log from openpilot.cereal import messaging, log
from teleoprtc.tracks import VIDEO_CLOCK_RATE from teleoprtc.tracks import VIDEO_CLOCK_RATE
from openpilot.system.webrtc.webrtcd import CerealOutgoingMessageProxy, CerealIncomingMessageProxy, ServerState, handle_get_stream from openpilot.system.webrtc.webrtcd import CerealOutgoingMessageProxy, CerealIncomingMessageProxy
from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack
@@ -80,8 +80,3 @@ class TestStreamSession(OpenpilotTestCase):
start_pts = packet.pts start_pts = packet.pts
assert abs(i + packet.pts - (start_pts + (((time.monotonic_ns() - start_ns) * VIDEO_CLOCK_RATE) // 1_000_000_000))) < 450 #5ms assert abs(i + packet.pts - (start_pts + (((time.monotonic_ns() - start_ns) * VIDEO_CLOCK_RATE) // 1_000_000_000))) < 450 #5ms
assert bytes(packet) == b"" assert bytes(packet) == b""
def test_stream_rejects_non_json_content_type(self):
response = self.loop.run_until_complete(handle_get_stream(ServerState(), b"{}", "text/plain"))
assert response == (415, b'{"error": "unsupported media type"}', "application/json; charset=utf-8")
+3 -6
View File
@@ -395,10 +395,7 @@ def _text_response(text: str, status: int = 200) -> tuple[int, bytes, str]:
return (status, text.encode(), "text/plain; charset=utf-8") return (status, text.encode(), "text/plain; charset=utf-8")
async def handle_get_stream(state: ServerState, raw_body: bytes, content_type: str) -> tuple[int, bytes, str]: async def handle_get_stream(state: ServerState, raw_body: bytes) -> tuple[int, bytes, str]:
if content_type != "application/json":
return _json_response({"error": "unsupported media type"}, status=415)
stream_dict = state.streams stream_dict = state.streams
body = StreamRequestBody(**json.loads(raw_body)) body = StreamRequestBody(**json.loads(raw_body))
@@ -511,7 +508,7 @@ class WebrtcdHandler(BaseHTTPRequestHandler):
services = parse_qs(parsed.query).get("services", [""])[0] services = parse_qs(parsed.query).get("services", [""])[0]
result = self._run(handle_get_schema(self.server.state, services)) result = self._run(handle_get_schema(self.server.state, services))
elif parsed.path == "/stream": elif parsed.path == "/stream":
result = self._run(handle_get_stream(self.server.state, self._read_body(), self.headers.get_content_type())) result = self._run(handle_get_stream(self.server.state, self._read_body()))
else: # /notify else: # /notify
try: try:
payload = json.loads(self._read_body()) payload = json.loads(self._read_body())
@@ -614,7 +611,7 @@ def webrtcd_thread(host: str, port: int):
def main(): def main():
parser = argparse.ArgumentParser(description="WebRTC daemon") parser = argparse.ArgumentParser(description="WebRTC daemon")
parser.add_argument("--host", type=str, default="127.0.0.1", help="Host to listen on") parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to listen on")
parser.add_argument("--port", type=int, default=5001, help="Port to listen on") parser.add_argument("--port", type=int, default=5001, help="Port to listen on")
args = parser.parse_args() args = parser.parse_args()
+1 -1
View File
@@ -21,7 +21,7 @@ dependencies = [
"tqdm", # cars (fw_versions.py) on start + many one-off uses "tqdm", # cars (fw_versions.py) on start + many one-off uses
# core # core
"scons==4.10.1", # 4.11 removed the qt3 tool still used to build Cabana "scons",
"pycapnp==2.1.0", # 2.2 introduces a memory leak due to cyclic references "pycapnp==2.1.0", # 2.2 introduces a memory leak due to cyclic references
"numpy >=2.0", "numpy >=2.0",
+2 -8
View File
@@ -48,11 +48,6 @@ def create_short_name(full_name: str) -> str:
return result[:8] return result[:8]
def create_pkl_name(full_name: str) -> str:
pkl = re.sub(r'[^a-zA-Z0-9]+', '_', full_name).strip('_').lower()
return pkl
def _read_pkl_bytes(pkl_path: Path) -> bytes: def _read_pkl_bytes(pkl_path: Path) -> bytes:
manifest = Path(f"{pkl_path}.chunkmanifest") manifest = Path(f"{pkl_path}.chunkmanifest")
if manifest.exists(): if manifest.exists():
@@ -159,15 +154,14 @@ if __name__ == "__main__":
_output_dir = Path(args.output_dir) _output_dir = Path(args.output_dir)
_output_dir.mkdir(exist_ok=True, parents=True) _output_dir.mkdir(exist_ok=True, parents=True)
_short_name = create_short_name(args.custom_name) if args.custom_name else None _short_name = create_short_name(args.custom_name) if args.custom_name else None
_pkl = create_pkl_name(args.custom_name) if args.custom_name else None
_driving_pkl = _find_driving_pkl(_output_dir) _driving_pkl = _find_driving_pkl(_output_dir)
if not _driving_pkl: if not _driving_pkl:
print(f"No driving_tinygrad.pkl found in {_output_dir}", file=sys.stderr) print(f"No driving_tinygrad.pkl found in {_output_dir}", file=sys.stderr)
sys.exit(1) sys.exit(1)
if _pkl: if _short_name:
new_pkl = _output_dir / f"driving_{_pkl}_tinygrad.pkl" new_pkl = _output_dir / f"driving_{_short_name.lower()}_tinygrad.pkl"
if not new_pkl.exists(): if not new_pkl.exists():
_driving_pkl = _rename_pkl_with_chunks(_driving_pkl, new_pkl) _driving_pkl = _rename_pkl_with_chunks(_driving_pkl, new_pkl)
else: else:
-6
View File
@@ -52,12 +52,6 @@ git fetch origin $DEV_BRANCH || (git checkout -b $DEV_BRANCH && git commit --all
echo "[-] committing version $VERSION T=$SECONDS" echo "[-] committing version $VERSION T=$SECONDS"
git add -f . git add -f .
# gitlinks break the release tree on device
if git ls-files -s | awk '$1 == "160000" { found = 1; print } END { exit !found }'; then
echo "Error: submodules found in release tree."
exit 1
fi
# include source commit hash and build date in commit # include source commit hash and build date in commit
GIT_HASH=$(git --git-dir=$SOURCE_DIR/.git rev-parse HEAD) GIT_HASH=$(git --git-dir=$SOURCE_DIR/.git rev-parse HEAD)
DATETIME=$(date '+%Y-%m-%dT%H:%M:%S') DATETIME=$(date '+%Y-%m-%dT%H:%M:%S')
+23 -27
View File
@@ -2,14 +2,15 @@
set -e set -e
set -x set -x
# git diff --name-status origin/release3-staging | grep "^A" | less
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
cd $DIR cd $DIR
BUILD_DIR=/data/openpilot BUILD_DIR=/data/openpilot
SOURCE_DIR="$(git rev-parse --show-toplevel)" SOURCE_DIR="$(git rev-parse --show-toplevel)"
export PYTHONPATH="$BUILD_DIR:$BUILD_DIR/msgq_repo:$BUILD_DIR/opendbc_repo:$BUILD_DIR/rednose_repo:$BUILD_DIR/teleoprtc_repo:$BUILD_DIR/tinygrad_repo"
if [ -z "$RELEASE_BRANCH" ]; then if [ -z "$RELEASE_BRANCH" ]; then
echo "RELEASE_BRANCH is not set" echo "RELEASE_BRANCH is not set"
exit 1 exit 1
@@ -22,36 +23,33 @@ BUILD_BRANCH=release-mici-staging
source $DIR/identity.sh source $DIR/identity.sh
echo "[-] Setting up repo T=$SECONDS" echo "[-] Setting up repo T=$SECONDS"
if ! git -C "$SOURCE_DIR" worktree remove --force "$BUILD_DIR" 2>/dev/null; then rm -rf $BUILD_DIR
rm -rf $BUILD_DIR mkdir -p $BUILD_DIR
fi
git -C "$SOURCE_DIR" worktree prune
git -C "$SOURCE_DIR" worktree add --detach --no-checkout "$BUILD_DIR"
cd $BUILD_DIR cd $BUILD_DIR
git update-ref -d "refs/heads/$BUILD_BRANCH" git init
git symbolic-ref HEAD "refs/heads/$BUILD_BRANCH" git remote add origin git@github.com:commaai/openpilot.git
git read-tree --empty git checkout --orphan $BUILD_BRANCH
# do the files copy # do the files copy
echo "[-] copying files T=$SECONDS" echo "[-] copying files T=$SECONDS"
cd $SOURCE_DIR cd $SOURCE_DIR
./tools/release/release_files.py | xargs -0 cp -pR --parents -t "$BUILD_DIR" -- cp -pR --parents $(./tools/release/release_files.py) $BUILD_DIR/
# in the directory # in the directory
cd $BUILD_DIR cd $BUILD_DIR
# use the full CPU available for speeding up the build. rm -f panda/board/obj/panda.bin.signed
# openpilot resets the CPU frequencies when test_onroad.py runs below. rm -f panda/board/obj/panda_h7.bin.signed
for policy in /sys/devices/system/cpu/cpufreq/policy*; do
[ -d "$policy" ] || continue
hardware_max="$(cat "$policy/cpuinfo_max_freq")"
echo "$hardware_max" | sudo tee "$policy/scaling_max_freq" >/dev/null
done
VERSION=$(cat openpilot/sunnypilot/common/version.h | awk -F[\"-] '{print $2}')
echo "[-] committing version $VERSION T=$SECONDS"
git add -f .
git commit -a -m "openpilot v$VERSION release"
# Build and test before launch_chffrplus.sh creates the on-device package
# symlinks. SConstruct uses the same package roots for build subprocesses.
export PYTHONPATH="$BUILD_DIR:$BUILD_DIR/msgq_repo:$BUILD_DIR/opendbc_repo:$BUILD_DIR/rednose_repo:$BUILD_DIR/teleoprtc_repo:$BUILD_DIR/tinygrad_repo"
scons scons
if [ -n "$INCLUDE_BIG_MODEL" ]; then
test -f openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl.chunkmanifest
fi
if [ -z "$PANDA_DEBUG_BUILD" ]; then if [ -z "$PANDA_DEBUG_BUILD" ]; then
# release panda fw # release panda fw
@@ -74,6 +72,7 @@ find . -name '*.a' -delete
find . -name '*.o' -delete find . -name '*.o' -delete
find . -name '*.os' -delete find . -name '*.os' -delete
find . -name '*.pyc' -delete find . -name '*.pyc' -delete
find . -name 'moc_*' -delete
find . -name '__pycache__' -delete find . -name '__pycache__' -delete
rm -rf .sconsign.dblite Jenkinsfile tools/release/ rm -rf .sconsign.dblite Jenkinsfile tools/release/
rm -f openpilot/selfdrive/modeld/models/*.onnx* rm -f openpilot/selfdrive/modeld/models/*.onnx*
@@ -89,11 +88,9 @@ git checkout openpilot/third_party/
# Mark as prebuilt release # Mark as prebuilt release
touch prebuilt touch prebuilt
VERSION=$(cat openpilot/sunnypilot/common/version.h | awk -F[\"-] '{print $2}')
# Add built files to git # Add built files to git
# writing larger objects is faster than compressing them on-device git add -f .
git -c core.compression=0 add -f . git commit --amend -m "openpilot v$VERSION"
git -c core.compression=0 -c gc.auto=0 commit -m "openpilot v$VERSION"
# Run tests # Run tests
cd $BUILD_DIR cd $BUILD_DIR
@@ -105,7 +102,6 @@ REFS=()
for branch in ${RELEASE_BRANCH//,/ }; do for branch in ${RELEASE_BRANCH//,/ }; do
REFS+=("$BUILD_BRANCH:$branch") REFS+=("$BUILD_BRANCH:$branch")
done done
# uploading the larger pack is faster than spending CPU to optimize it git push -f origin "${REFS[@]}"
git -c pack.window=0 -c pack.depth=0 -c pack.compression=0 push -f origin "${REFS[@]}"
echo "[-] done T=$SECONDS" echo "[-] done T=$SECONDS"
+10 -6
View File
@@ -30,14 +30,20 @@ git submodule deinit -f --all
git rm -rf --cached . git rm -rf --cached .
find . -maxdepth 1 -not -path './.git' -not -name '.' -not -name '..' -exec rm -rf '{}' \; find . -maxdepth 1 -not -path './.git' -not -name '.' -not -name '..' -exec rm -rf '{}' \;
# cleanup before the copy
cd $SOURCE_DIR
git clean -xdff
git submodule foreach --recursive git clean -xdff
# do the files copy # do the files copy
echo "[-] copying files T=$SECONDS" echo "[-] copying files T=$SECONDS"
cd $SOURCE_DIR cd $SOURCE_DIR
./tools/release/release_files.py | xargs -0 cp -pR --parents -t "$TARGET_DIR" -- ./tools/release/release_files.py | xargs -d '\n' cp -pR --parents -t "$TARGET_DIR"
# in the directory # in the directory
cd $TARGET_DIR cd $TARGET_DIR
rm -rf .git/modules/ rm -rf .git/modules/
rm -f panda/board/obj/panda.bin.signed
find openpilot/selfdrive/modeld/models -name '*.onnx' -size +95M -exec ./openpilot/common/file_chunker.py {} \; find openpilot/selfdrive/modeld/models -name '*.onnx' -size +95M -exec ./openpilot/common/file_chunker.py {} \;
@@ -51,10 +57,9 @@ echo -n "$GIT_HASH" > git_src_commit
echo -n "$GIT_COMMIT_DATE" > git_src_commit_date echo -n "$GIT_COMMIT_DATE" > git_src_commit_date
echo "[-] committing version $VERSION T=$SECONDS" echo "[-] committing version $VERSION T=$SECONDS"
# writing larger objects is faster than compressing them on-device git add -f .
git -c core.compression=0 add -f .
git status git status
git -c core.compression=0 commit -a -m "sunnypilot v$VERSION release git commit -a -m "sunnypilot v$VERSION release
date: $DATETIME date: $DATETIME
master commit: $GIT_HASH master commit: $GIT_HASH
@@ -78,8 +83,7 @@ fi
if [ ! -z "$BRANCH" ]; then if [ ! -z "$BRANCH" ]; then
echo "[-] Pushing to $BRANCH T=$SECONDS" echo "[-] Pushing to $BRANCH T=$SECONDS"
# uploading the larger pack is faster than spending CPU to optimize it git push -f origin tmp:$BRANCH
git -c pack.window=0 -c pack.depth=0 -c pack.compression=0 push -f origin tmp:$BRANCH
fi fi
echo "[-] done T=$SECONDS, ready at $TARGET_DIR" echo "[-] done T=$SECONDS, ready at $TARGET_DIR"
+8 -10
View File
@@ -1,19 +1,20 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import re import re
import subprocess from pathlib import Path
import sys
HERE = os.path.abspath(os.path.dirname(__file__)) HERE = os.path.abspath(os.path.dirname(__file__))
ROOT = os.path.abspath(os.path.join(HERE, "../..")) ROOT = os.path.abspath(os.path.join(HERE, "../.."))
blacklist = [ blacklist = [
".git/", ".git/",
".venv/",
".github/workflows/", ".github/workflows/",
"matlab.*.md", "matlab.*.md",
# skip big model for now
"openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx",
# no LFS or submodules in release # no LFS or submodules in release
".lfsconfig", ".lfsconfig",
".gitattributes", ".gitattributes",
@@ -29,17 +30,14 @@ whitelist: list[str] = [
if __name__ == "__main__": if __name__ == "__main__":
tracked_files = subprocess.check_output(["git", "ls-files", "-z", "--recurse-submodules"], cwd=ROOT).split(b"\0") for f in Path(ROOT).rglob("**/*"):
for tracked_file in tracked_files: if not (f.is_file() or f.is_symlink()):
if not tracked_file:
continue continue
rf = os.fsdecode(tracked_file) rf = str(f.relative_to(ROOT))
if not os.getenv("INCLUDE_BIG_MODEL") and rf.startswith("openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx"):
continue
blacklisted = any(re.search(p, rf) for p in blacklist) blacklisted = any(re.search(p, rf) for p in blacklist)
whitelisted = any(re.search(p, rf) for p in whitelist) whitelisted = any(re.search(p, rf) for p in whitelist)
if blacklisted and not whitelisted: if blacklisted and not whitelisted:
continue continue
sys.stdout.buffer.write(tracked_file + b"\0") print(rf)
Generated
+60 -79
View File
@@ -52,43 +52,24 @@ wheels = [
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "3.5.1" version = "3.4.9"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" },
{ url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" },
{ url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" },
{ url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" },
{ url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" },
{ url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" },
{ url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" },
{ url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" },
{ url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" },
{ url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" },
{ url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" },
{ url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" },
{ url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" },
{ url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" },
{ url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" },
{ url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" },
{ url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" },
{ url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" },
{ url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" },
{ url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" },
{ url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" },
{ url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" },
{ url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" },
{ url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" },
{ url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" },
{ url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" },
{ url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" },
{ url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" },
{ url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" },
{ url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" },
{ url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" },
] ]
[[package]] [[package]]
@@ -847,7 +828,7 @@ requires-dist = [
{ name = "rednose", marker = "extra == 'submodules'", editable = "rednose_repo" }, { name = "rednose", marker = "extra == 'submodules'", editable = "rednose_repo" },
{ name = "requests" }, { name = "requests" },
{ name = "ruff", marker = "extra == 'testing'" }, { name = "ruff", marker = "extra == 'testing'" },
{ name = "scons", specifier = "==4.10.1" }, { name = "scons" },
{ name = "sentry-sdk" }, { name = "sentry-sdk" },
{ name = "setproctitle" }, { name = "setproctitle" },
{ name = "sounddevice" }, { name = "sounddevice" },
@@ -1111,27 +1092,27 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.16.3" version = "0.16.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } sdist = { url = "https://files.pythonhosted.org/packages/73/e1/4508a569211b35599016e84ba65c1a992b7a4004b4b6c4bea02a851cba1b/ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c", size = 4885811, upload-time = "2026-08-07T13:31:01.432Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, { url = "https://files.pythonhosted.org/packages/14/57/db19951540f98859c956b50bdb4d31089b4d91e9f15e2968e7d5193806d5/ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318", size = 10847925, upload-time = "2026-08-07T13:30:14.468Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" }, { url = "https://files.pythonhosted.org/packages/13/5a/995fe85a8470d3e391ac0f7fa8054bb454eaf33ee138196d6172ed1079c0/ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344", size = 11072662, upload-time = "2026-08-07T13:30:18.143Z" },
{ url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" }, { url = "https://files.pythonhosted.org/packages/32/53/370d767c61c71a971a4ace36703a7ecd8c393956349a7325d7fab2b56827/ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700", size = 10566771, upload-time = "2026-08-07T13:30:20.899Z" },
{ url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, { url = "https://files.pythonhosted.org/packages/85/d6/9d96948caf5a632be62d62202d5ec914d6856f204fd79eb036e5915e79ea/ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe", size = 10975825, upload-time = "2026-08-07T13:30:23.517Z" },
{ url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, { url = "https://files.pythonhosted.org/packages/3b/92/ea87129b3414acb0b5770563779c51804d37ac67675c7ba35447ddb14773/ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f", size = 10649437, upload-time = "2026-08-07T13:30:26.097Z" },
{ url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, { url = "https://files.pythonhosted.org/packages/ac/43/f8f291dcd4af5bb7872b74fdfa41a7cd7c856ca1d4069670971cf1b9f5cb/ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f", size = 11446761, upload-time = "2026-08-07T13:30:28.752Z" },
{ url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, { url = "https://files.pythonhosted.org/packages/71/4a/ef991fb2fcf516ab71f0808adcdd8da5e18c8cde447f4ceaf5f47a5132a5/ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12", size = 12336364, upload-time = "2026-08-07T13:30:31.468Z" },
{ url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, { url = "https://files.pythonhosted.org/packages/f3/24/f615e74f307e6ca0e56a482872477b856c70d530aa356abfb6dfe5ca8a80/ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140", size = 11630720, upload-time = "2026-08-07T13:30:34.426Z" },
{ url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, { url = "https://files.pythonhosted.org/packages/c5/d3/8ef50149e8412a77f7ab409efdef0e2b23803707a3863da4fc64cb23d459/ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f", size = 11466130, upload-time = "2026-08-07T13:30:36.958Z" },
{ url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, { url = "https://files.pythonhosted.org/packages/dd/a7/a19334985c4dea8c381981fa252cd854c7ee52dc4b1686dc16f4a911c702/ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0", size = 11523634, upload-time = "2026-08-07T13:30:39.822Z" },
{ url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, { url = "https://files.pythonhosted.org/packages/6e/6c/96d192b0e742412ceda08c0a50f9669b253dde9fd6a60ea1a10c9fa79a63/ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690", size = 10949807, upload-time = "2026-08-07T13:30:42.745Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, { url = "https://files.pythonhosted.org/packages/fa/51/e26599ceca11e79ee255c7df515995561edf87e9ca1893284e44d98f5a86/ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1", size = 10646891, upload-time = "2026-08-07T13:30:45.522Z" },
{ url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, { url = "https://files.pythonhosted.org/packages/68/01/800c4b1f97bc8d7c6029e06b1f20473a3cf1e13c4933d8f3342add83fc55/ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8", size = 11162063, upload-time = "2026-08-07T13:30:48.131Z" },
{ url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, { url = "https://files.pythonhosted.org/packages/e4/d0/1477ea50fc5a0d4b0b71d1d63d50770bdd794d90b43e37a7618e63ec9894/ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd", size = 11556038, upload-time = "2026-08-07T13:30:50.686Z" },
{ url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" }, { url = "https://files.pythonhosted.org/packages/b8/76/a7776f32048d991e16d4fa8ff91790b877342d3596cc3ed04acdbf1aaedc/ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b", size = 10872850, upload-time = "2026-08-07T13:30:53.471Z" },
{ url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" }, { url = "https://files.pythonhosted.org/packages/00/0d/929c800d920e61397d82a01b60bffc68da3052c17d31de59efaad2e4ed75/ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa", size = 12023338, upload-time = "2026-08-07T13:30:56.193Z" },
{ url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" },
] ]
[[package]] [[package]]
@@ -1145,15 +1126,15 @@ wheels = [
[[package]] [[package]]
name = "sentry-sdk" name = "sentry-sdk"
version = "2.68.0" version = "2.67.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/5b/94/23b7dd072acb9628907bd3f4fbf61794a7b12a9db8f33c1276f70ae5ac92/sentry_sdk-2.68.0.tar.gz", hash = "sha256:648c58e9887311a03470a41539e24bdbbf64a30ca4f5336f7e3dcc87276400b3", size = 1008854, upload-time = "2026-08-13T09:06:21.268Z" } sdist = { url = "https://files.pythonhosted.org/packages/ad/8a/b2eec40df8a67bf073e244d29001d04ee365d163bc4f15efdfce35f53090/sentry_sdk-2.67.1.tar.gz", hash = "sha256:f263d8c9aa4137750640de8fb0ed5404df6bb564e20e4b59cb16a6eeba18d4ed", size = 990599, upload-time = "2026-08-10T13:05:55.892Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/9b/e2421d08956d0bc4691d995393d835e563886bff499d8fb10fdefae85a8d/sentry_sdk-2.68.0-py3-none-any.whl", hash = "sha256:538e56c2d03679d42f7c0cb5f1af73a7a510b00abc7e296c13ac49b107b713a4", size = 518670, upload-time = "2026-08-13T09:06:19.735Z" }, { url = "https://files.pythonhosted.org/packages/34/e2/70692eba662037cddf93391cbbf98297159f3038612e9b9a8129e16feb7a/sentry_sdk-2.67.1-py3-none-any.whl", hash = "sha256:a66bfbce1cd8a93c51c369d642ad85b46253ea7a6f7938141315b83e2823cda5", size = 515591, upload-time = "2026-08-10T13:05:54.213Z" },
] ]
[[package]] [[package]]
@@ -1285,6 +1266,7 @@ requires-dist = [
{ name = "pylint", marker = "extra == 'linting'" }, { name = "pylint", marker = "extra == 'linting'" },
{ name = "pytest", marker = "extra == 'testing-minimal'" }, { name = "pytest", marker = "extra == 'testing-minimal'" },
{ name = "pytest-split", marker = "extra == 'testing-minimal'" }, { name = "pytest-split", marker = "extra == 'testing-minimal'" },
{ name = "pytest-timeout", marker = "extra == 'testing-minimal'" },
{ name = "pytest-xdist", marker = "extra == 'testing-minimal'" }, { name = "pytest-xdist", marker = "extra == 'testing-minimal'" },
{ name = "ruff", marker = "extra == 'linting'", specifier = "==0.14.10" }, { name = "ruff", marker = "extra == 'linting'", specifier = "==0.14.10" },
{ name = "safetensors", marker = "extra == 'testing-unit'" }, { name = "safetensors", marker = "extra == 'testing-unit'" },
@@ -1293,7 +1275,6 @@ requires-dist = [
{ name = "tiktoken", marker = "extra == 'testing'" }, { name = "tiktoken", marker = "extra == 'testing'" },
{ name = "tinygrad", extras = ["testing-minimal"], marker = "extra == 'testing-unit'" }, { name = "tinygrad", extras = ["testing-minimal"], marker = "extra == 'testing-unit'" },
{ name = "tinygrad", extras = ["testing-unit"], marker = "extra == 'testing'" }, { name = "tinygrad", extras = ["testing-unit"], marker = "extra == 'testing'" },
{ name = "tinymesa", marker = "extra == 'mesa'", specifier = "==25.2.7.2" },
{ name = "torch", marker = "extra == 'testing-minimal'", specifier = "==2.9.1" }, { name = "torch", marker = "extra == 'testing-minimal'", specifier = "==2.9.1" },
{ name = "tqdm", marker = "extra == 'testing-unit'" }, { name = "tqdm", marker = "extra == 'testing-unit'" },
{ name = "transformers", marker = "extra == 'testing'" }, { name = "transformers", marker = "extra == 'testing'" },
@@ -1301,7 +1282,7 @@ requires-dist = [
{ name = "typing-extensions", marker = "extra == 'linting'" }, { name = "typing-extensions", marker = "extra == 'linting'" },
{ name = "z3-solver", marker = "extra == 'testing-minimal'", specifier = "<4.15.4" }, { name = "z3-solver", marker = "extra == 'testing-minimal'", specifier = "<4.15.4" },
] ]
provides-extras = ["linting", "testing-minimal", "testing-unit", "testing", "docs", "mesa"] provides-extras = ["linting", "testing-minimal", "testing-unit", "testing", "docs"]
[[package]] [[package]]
name = "tomli" name = "tomli"
@@ -1335,27 +1316,27 @@ wheels = [
[[package]] [[package]]
name = "ty" name = "ty"
version = "0.0.72" version = "0.0.69"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d5/df/656e684bafb13c1d146e7d5b5f3e7978ca177232acc84998ff36427e9462/ty-0.0.72.tar.gz", hash = "sha256:ec2b8066b618df18cab4cb8e992f8da45d360332acb23fa34df7fa29cd1b9d3a", size = 6654939, upload-time = "2026-08-14T21:35:42.612Z" } sdist = { url = "https://files.pythonhosted.org/packages/8e/5b/7a618632dfe9373b7df572ecd7a08c8f799d772fbc317da82dd3aa363207/ty-0.0.69.tar.gz", hash = "sha256:b65106e9ff24fa76e25e1142fb09c85244e815c40450e3021d2bf652c231bb43", size = 6565094, upload-time = "2026-08-06T10:04:25.667Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/3b/f51461239a4e66565d4b362f97a3b55fe7fdba2e944068341f87c62f6743/ty-0.0.72-py3-none-linux_armv6l.whl", hash = "sha256:fda86db153ffd85ee52000cf175d6a3f1c0223772cf7c5b6f726200bf92c7b44", size = 12621989, upload-time = "2026-08-14T21:35:01.676Z" }, { url = "https://files.pythonhosted.org/packages/06/60/6534092f4d2c15e2491807edd609c2e50d527c1fed957acf40b9f110b64a/ty-0.0.69-py3-none-linux_armv6l.whl", hash = "sha256:98bfd383b273540829af673e7f98b9c1c4bcc8547d12a1a3806cd0bec7f0e087", size = 12364185, upload-time = "2026-08-06T10:03:47.137Z" },
{ url = "https://files.pythonhosted.org/packages/ca/fb/79ddf683affc679ca856f3510b5640ec3a88a842ba5f654f5d4bc78f1786/ty-0.0.72-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ceb944c612529b9023acfdc9cf4c0dcbb722549f9d17d46baecd1141baf01d7f", size = 12233910, upload-time = "2026-08-14T21:35:04.334Z" }, { url = "https://files.pythonhosted.org/packages/34/2b/5c29689bd4f74c2e3394d983d85e4011b629f2ce3730c9442553b8554bf8/ty-0.0.69-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:964621ddd05771660017c51b4e74078d861d9fc863c21ef2a500db1ab62c9ccf", size = 12042510, upload-time = "2026-08-06T10:03:49.481Z" },
{ url = "https://files.pythonhosted.org/packages/5d/45/10562a0d84802158db8fa4ec46de54aa9fdcecdeeaabbfe3639ae7042b66/ty-0.0.72-py3-none-macosx_11_0_arm64.whl", hash = "sha256:108d76218333d6c092e5f1cebf8e9b06f25738613a0236a28e2dd47c936ee52c", size = 12084108, upload-time = "2026-08-14T21:35:06.686Z" }, { url = "https://files.pythonhosted.org/packages/09/46/fa085bde4d23516d7ef14b24736fc5dd7dc498f60f52b3d077e59ffdea20/ty-0.0.69-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3ffea4048dd0da4c9c97393b4be0901098a9065b06fa81be2477cbde65d8a151", size = 11549397, upload-time = "2026-08-06T10:03:51.747Z" },
{ url = "https://files.pythonhosted.org/packages/a1/dc/1fe1aef8d697e3509face271a5331700c7aa1d1e44a4b622707bdfa41d4b/ty-0.0.72-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f3943f186f741a2499a31053872169250c9264a9a49684920e48d8fcf4ef4f5", size = 12132640, upload-time = "2026-08-14T21:35:09.305Z" }, { url = "https://files.pythonhosted.org/packages/25/cc/97b9efb2061dcab6fef1e94a4ad99df0bb45bd2cc15d4f5794c787ee0552/ty-0.0.69-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8684d4a70aadd1eab0f41bdba835e3288ef49db8402a8e6ca81bab52ed5d610", size = 12115567, upload-time = "2026-08-06T10:03:53.79Z" },
{ url = "https://files.pythonhosted.org/packages/14/46/41ceb265e96969487311a2014bd0e53abb4fbc1395efb2ebe411fcb4db62/ty-0.0.72-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf283c07dc3cc52ca48a3ad8ab100fb5aec3aebbd03ef6a12d5f910b8e596fc5", size = 12402489, upload-time = "2026-08-14T21:35:11.555Z" }, { url = "https://files.pythonhosted.org/packages/e0/c1/a5e0404965093835f3e62544e661784ec0aa8ef0b006ed50af50b19c107e/ty-0.0.69-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:afaaba240ab4122e2069a796836d10be81b4ddb053ae268b3dff962a0b4ca5c7", size = 12149770, upload-time = "2026-08-06T10:03:55.993Z" },
{ url = "https://files.pythonhosted.org/packages/2b/45/30bf43cb4fd505c5c2dd30fda27dde5f05208686cd21217adec77c954204/ty-0.0.72-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95f3b6462c38f9f115d10cee21f47fedf715fcf2040daf36eef210359300bc7c", size = 13130835, upload-time = "2026-08-14T21:35:13.746Z" }, { url = "https://files.pythonhosted.org/packages/e2/39/8cad6b205a4abe8a044ca0c84aea71e8ccda29b07a75a5f090e310605580/ty-0.0.69-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11ea63ef07d4e33aeb1a775cf5f2c736b3ed22fa6f8b1b608591612c36795044", size = 12941278, upload-time = "2026-08-06T10:03:58.324Z" },
{ url = "https://files.pythonhosted.org/packages/31/2f/03bba754d2613f640df168335c41f83f41db150bb515839c60d80e3a7880/ty-0.0.72-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30caf658feb8ffb250d9e9e47107657a78f5f3425c227df1664d8df2ebe38880", size = 13590392, upload-time = "2026-08-14T21:35:16.839Z" }, { url = "https://files.pythonhosted.org/packages/d6/8b/8766d96b732c2a060d70dc8ccafcc4d6a54109a2a95f1deb0705de88892b/ty-0.0.69-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb3730b1268e92a2907d7aea3afe8dd1b360ae65862f0557080cf479d481b424", size = 13426509, upload-time = "2026-08-06T10:04:00.621Z" },
{ url = "https://files.pythonhosted.org/packages/04/c7/03c67f00e63005ec41585653dc3096064570b1e6273742baae2798cd242f/ty-0.0.72-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27bdc012ddfbeec8948e4a6036c0dc39ac7cf2c8ec7c7d48dc7d2fd56d57b399", size = 13309629, upload-time = "2026-08-14T21:35:19.169Z" }, { url = "https://files.pythonhosted.org/packages/02/1f/e991b2cde953ea5b94d6a9a4c45c87937bd916bc09235f764407bf471c0a/ty-0.0.69-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a544ff57a752ef186ed40b5a2f44c17402af4cdefeb74a311ca02ebd57c4fca0", size = 13106582, upload-time = "2026-08-06T10:04:02.818Z" },
{ url = "https://files.pythonhosted.org/packages/c1/df/102d3b264eb7f2a58dd11952f229bb5150bb5668d176a6154976a6675981/ty-0.0.72-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:802c5970a77d7739e6f499921fbb6984fb7ad8a31d95e1ff42fd46f3642e4f3b", size = 12734028, upload-time = "2026-08-14T21:35:22.099Z" }, { url = "https://files.pythonhosted.org/packages/ea/bb/73538f1b99e3558fd9db87b98698426f0f60fc8666da0b1efd0e70e275eb/ty-0.0.69-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87ed2cbca20caddfdf8e3e14d213ce91b67e75feed78900f4aaf3ef884954028", size = 12708931, upload-time = "2026-08-06T10:04:05.233Z" },
{ url = "https://files.pythonhosted.org/packages/61/85/d0737c8c54d0ba67366ddfb9f31d88edf0b02299e65923e6945ae60ebcb5/ty-0.0.72-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:47dce65114fdc615c68ca0edb393b433df0956447e4267df0e264137a789598d", size = 13174832, upload-time = "2026-08-14T21:35:24.71Z" }, { url = "https://files.pythonhosted.org/packages/87/cd/484a5208d74c4ad1155933906295ccdce9aa81a257d8df2ab9e41bd60133/ty-0.0.69-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2684efcbce5b6fe45045faf610b377b50781b6d2aa7e61ea23ecf5b3d2bce421", size = 12985322, upload-time = "2026-08-06T10:04:07.587Z" },
{ url = "https://files.pythonhosted.org/packages/1e/31/497f5a96c36d9b586ab6afe0574986835c6fd5b835a89773d2bec4711b49/ty-0.0.72-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:325144fa07e2675d0faa337fcc864213c272a499eb0cfe5bde2fdc62282d27bc", size = 12215005, upload-time = "2026-08-14T21:35:26.892Z" }, { url = "https://files.pythonhosted.org/packages/6e/81/b75003f0d4da9ab3bc8fd4f4802f836cb9921ff7e70f460604f7b769a0b5/ty-0.0.69-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:da9aeb26fdac1d2214937542b59e0d4d1ba94ec7a3f45444f33c846de1eb1d63", size = 12063910, upload-time = "2026-08-06T10:04:09.835Z" },
{ url = "https://files.pythonhosted.org/packages/df/7d/46e65b17b4966c7cd0140f134380d33d8e84fe6efccd761533ce793dc502/ty-0.0.72-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5c9f15d0f58e43707d8848274be1821a0ef408eccb8aa7dda28a4a9eddf7640", size = 12421298, upload-time = "2026-08-14T21:35:29.301Z" }, { url = "https://files.pythonhosted.org/packages/8a/76/088469f547ef63dceefc4a75826aedee5014f9371dc5171cde931896a82c/ty-0.0.69-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:00e7677cd14ede381f705f71104ea7b8ea0ce217a8634e19a89781953de0e9ad", size = 12166823, upload-time = "2026-08-06T10:04:12.114Z" },
{ url = "https://files.pythonhosted.org/packages/08/2a/12ada4ec17700b3cb1d4fd3bc3e5b1852df9e6885288429318cade87b3c1/ty-0.0.72-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ee508d64b381871529cc22c412b41071bf5e908b7aa5d66a38f3f6b2573a806", size = 12669242, upload-time = "2026-08-14T21:35:31.444Z" }, { url = "https://files.pythonhosted.org/packages/0a/c9/ce88a0bec0d46d8ae180b99c6ec014866fecc4cba1727b5feec8877b2765/ty-0.0.69-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d91965eb799649833d0d6042db09cd03d15289125245337cc46a2606effb7bda", size = 12483136, upload-time = "2026-08-06T10:04:14.33Z" },
{ url = "https://files.pythonhosted.org/packages/1c/1a/4692536880790fb550ed6d44a6096778dc71bb112f2c6d615cebb01a57e5/ty-0.0.72-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3699e2ec7921d44da79d6b089f7bf239b2cc53c4e45a5a38430adc34ee9e9a55", size = 12988199, upload-time = "2026-08-14T21:35:33.749Z" }, { url = "https://files.pythonhosted.org/packages/63/9e/6fae0ff225a0012642cf72c077e20f8f448c0a80771bc3360e8178fe2f32/ty-0.0.69-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1f03359cd8e5c412aa0c181118fa9b9061a4dddaedbb61bac0a424fb0814d402", size = 12799025, upload-time = "2026-08-06T10:04:16.445Z" },
{ url = "https://files.pythonhosted.org/packages/9a/0d/f5e5a50322e9c45865e7b7a428ba6cd6527387cf0f2472492ac3cf746243/ty-0.0.72-py3-none-win32.whl", hash = "sha256:f25f72a67bd36cd247707c4784e52fad0b6b4f42a1b7dd14804110fa95c486ed", size = 11939708, upload-time = "2026-08-14T21:35:36.006Z" }, { url = "https://files.pythonhosted.org/packages/e4/43/78a658d18b2a4ccf35b053392f2213bf12e3c63b2abea512d3b6751d1f4c/ty-0.0.69-py3-none-win32.whl", hash = "sha256:ec460e01586b1eb91894c4a8403bee3e045a47e7a4ada943cc27ce8e348e88cf", size = 11787774, upload-time = "2026-08-06T10:04:18.622Z" },
{ url = "https://files.pythonhosted.org/packages/3f/4e/8af3534b2e4214e6184a5a59c34101e94a68d578f081f97b995866bab1bf/ty-0.0.72-py3-none-win_amd64.whl", hash = "sha256:cdeee869341717e1736cea2e2d7856738c6957c320f584ed2f68c8f90100d2f5", size = 12643876, upload-time = "2026-08-14T21:35:38.141Z" }, { url = "https://files.pythonhosted.org/packages/3a/5e/88db1f674403f2b81316a853a44a81ed220621fa96f8f7ae586fb6ca7513/ty-0.0.69-py3-none-win_amd64.whl", hash = "sha256:18976ca26a4e28fc3249477f79a695d5502e670803f2e080d89ac905baef3c6e", size = 12864038, upload-time = "2026-08-06T10:04:20.748Z" },
{ url = "https://files.pythonhosted.org/packages/ff/ea/a2606e654c7276bd08586391a2525b0af3f3bf60228a8c57b2d248f273f9/ty-0.0.72-py3-none-win_arm64.whl", hash = "sha256:1bd3ac3ed4424a6d6990a85dc388556aea012bd752de21349a84b685951de0d8", size = 12394857, upload-time = "2026-08-14T21:35:40.277Z" }, { url = "https://files.pythonhosted.org/packages/4d/7b/6fc6efd00c69103d70f2bdbe824343089cd70b17b3079170057d3e5a3ac0/ty-0.0.69-py3-none-win_arm64.whl", hash = "sha256:7d4ca3bb74d91cb9947ba3f3b4cb131ad6a2b3ecc76d34040c4ec6092d2e411d", size = 12196693, upload-time = "2026-08-06T10:04:22.902Z" },
] ]
[[package]] [[package]]