Compare commits

..

218 Commits

Author SHA1 Message Date
discountchubbs a8ea467e52 precache warp in legacy 2026-08-14 22:44:19 -07:00
discountchubbs 5795c3ba22 support monolith still 2026-08-14 14:41:02 -07:00
discountchubbs 37d743122e this made me cry 2026-08-14 14:35:28 -07:00
discountchubbs 300869431a epgu flag reduce 2026-08-14 14:06:25 -07:00
discountchubbs 25c21c88e5 how?? 2026-08-14 13:16:19 -07:00
discountchubbs 789bbf2f70 Merge remote-tracking branch 'origin/master' into egpu-support
# Conflicts:
#	opendbc_repo
#	openpilot/sunnypilot/modeld_v2/modeld.py
#	openpilot/sunnypilot/modeld_v2/tests/helpers.py
#	openpilot/sunnypilot/modeld_v2/tests/test_recovery_power.py
#	openpilot/sunnypilot/models/tests/test_tinygrad_ref.py
#	tinygrad_repo
2026-08-14 13:15:01 -07:00
Jason Wen 6e69da4037 Sync: commaai/openpilot:mastersunnypilot/sunnypilot:master (#1908) 2026-08-14 15:47:04 -04:00
Jason Wen 6405f15d5a models: use requests instead of aiohttp 2026-08-14 15:35:14 -04:00
Jason Wen ac4ab9a9b8 tests: migrate sunnypilot tests to unittest and remove pytest 2026-08-14 15:35:14 -04:00
Jason Wen 7461f70fdb Merge commit 'b7c333cf3fee117779515c9ebfd7b2beb164fa81' into sync-20260813
# Conflicts:
#	README.md
#	SConstruct
#	conftest.py
#	docs/CARS.md
#	msgq_repo
#	opendbc_repo
#	openpilot/common/params_keys.h
#	openpilot/common/params_pyx.pyx
#	openpilot/common/tests/test_swaglog.cc
#	openpilot/selfdrive/car/card.py
#	openpilot/selfdrive/car/tests/test_car_interfaces.py
#	openpilot/selfdrive/car/tests/test_cruise_speed.py
#	openpilot/selfdrive/car/tests/test_models.py
#	openpilot/selfdrive/controls/controlsd.py
#	openpilot/selfdrive/controls/lib/latcontrol_torque.py
#	openpilot/selfdrive/controls/lib/longitudinal_planner.py
#	openpilot/selfdrive/controls/plannerd.py
#	openpilot/selfdrive/controls/radard.py
#	openpilot/selfdrive/controls/tests/test_longcontrol.py
#	openpilot/selfdrive/locationd/torqued.py
#	openpilot/selfdrive/modeld/modeld.py
#	openpilot/selfdrive/monitoring/dmonitoringd.py
#	openpilot/selfdrive/monitoring/test_monitoring.py
#	openpilot/selfdrive/selfdrived/selfdrived.py
#	openpilot/selfdrive/selfdrived/tests/test_alertmanager.py
#	openpilot/selfdrive/test/longitudinal_maneuvers/plant.py
#	openpilot/selfdrive/test/process_replay/migration.py
#	openpilot/selfdrive/test/process_replay/process_replay.py
#	openpilot/selfdrive/ui/feedback/feedbackd.py
#	openpilot/selfdrive/ui/layouts/settings/device.py
#	openpilot/selfdrive/ui/layouts/settings/toggles.py
#	openpilot/selfdrive/ui/mici/layouts/onboarding.py
#	openpilot/selfdrive/ui/onroad/augmented_road_view.py
#	openpilot/selfdrive/ui/tests/test_soundd.py
#	openpilot/selfdrive/ui/translations/app.pot
#	openpilot/selfdrive/ui/translations/app_de.po
#	openpilot/selfdrive/ui/translations/app_en.po
#	openpilot/selfdrive/ui/translations/app_es.po
#	openpilot/selfdrive/ui/translations/app_fr.po
#	openpilot/selfdrive/ui/translations/app_ja.po
#	openpilot/selfdrive/ui/translations/app_ko.po
#	openpilot/selfdrive/ui/translations/app_pt-BR.po
#	openpilot/selfdrive/ui/translations/app_th.po
#	openpilot/selfdrive/ui/translations/app_tr.po
#	openpilot/selfdrive/ui/translations/app_uk.po
#	openpilot/selfdrive/ui/translations/app_zh-CHS.po
#	openpilot/selfdrive/ui/translations/app_zh-CHT.po
#	openpilot/system/athena/athenad.py
#	openpilot/system/hardware/hardwared.py
#	openpilot/system/loggerd/deleter.py
#	openpilot/system/manager/process_config.py
#	openpilot/system/ui/lib/application.py
#	panda
#	pyproject.toml
#	tinygrad_repo
#	uv.lock
2026-08-14 15:35:10 -04:00
discountchubbs 351df2d92f old 2026-08-14 11:35:03 -07:00
discountchubbs 502bb58c4e needed to say 2026-08-14 10:49:29 -07:00
discountchubbs e571a11e9b bump 2026-08-14 10:46:49 -07:00
discountchubbs b7fba3b300 lint 2026-08-14 10:02:05 -07:00
discountchubbs 7da3de59bf gpu fixes from upstream 2026-08-14 09:54:23 -07:00
discountchubbs 1cf09b8899 fix 2026-08-14 09:49:52 -07:00
discountchubbs 53404a35cf tici 2026-08-14 09:45:57 -07:00
discountchubbs a1983b0985 Merge remote-tracking branch 'origin/sync-20260813' into egpu-support
# Conflicts:
#	openpilot/selfdrive/modeld/helpers.py
#	openpilot/selfdrive/modeld/modeld.py
#	openpilot/sunnypilot/modeld_v2/modeld.py
#	openpilot/sunnypilot/modeld_v2/tests/helpers.py
#	openpilot/sunnypilot/models/manager.py
2026-08-14 09:41:22 -07:00
Jason Wen ea8eb4b14a models: use requests instead of aiohttp 2026-08-14 02:10:23 -04:00
discountchubbs 7b441aa0fd transition to requests 2026-08-13 22:11:09 -07:00
discountchubbs 198ced6f87 fetch latest commit. 2026-08-13 15:43:19 -07:00
discountchubbs dfd87e730c im scared , this may be a bad idea lol 2026-08-13 15:16:13 -07:00
discountchubbs d4f8354be2 red diff mf 2026-08-13 15:05:29 -07:00
Jason Wen a3012de889 tests: migrate sunnypilot tests to unittest and remove pytest 2026-08-13 17:52:45 -04:00
Jason Wen 9fb9b73620 Merge commit 'b7c333cf3fee117779515c9ebfd7b2beb164fa81' into sync-20260813
# Conflicts:
#	README.md
#	SConstruct
#	conftest.py
#	docs/CARS.md
#	msgq_repo
#	opendbc_repo
#	openpilot/common/params_keys.h
#	openpilot/common/params_pyx.pyx
#	openpilot/common/tests/test_swaglog.cc
#	openpilot/selfdrive/car/card.py
#	openpilot/selfdrive/car/tests/test_car_interfaces.py
#	openpilot/selfdrive/car/tests/test_cruise_speed.py
#	openpilot/selfdrive/car/tests/test_models.py
#	openpilot/selfdrive/controls/controlsd.py
#	openpilot/selfdrive/controls/lib/latcontrol_torque.py
#	openpilot/selfdrive/controls/lib/longitudinal_planner.py
#	openpilot/selfdrive/controls/plannerd.py
#	openpilot/selfdrive/controls/radard.py
#	openpilot/selfdrive/controls/tests/test_longcontrol.py
#	openpilot/selfdrive/locationd/torqued.py
#	openpilot/selfdrive/modeld/modeld.py
#	openpilot/selfdrive/monitoring/dmonitoringd.py
#	openpilot/selfdrive/monitoring/test_monitoring.py
#	openpilot/selfdrive/selfdrived/selfdrived.py
#	openpilot/selfdrive/selfdrived/tests/test_alertmanager.py
#	openpilot/selfdrive/test/longitudinal_maneuvers/plant.py
#	openpilot/selfdrive/test/process_replay/migration.py
#	openpilot/selfdrive/test/process_replay/process_replay.py
#	openpilot/selfdrive/ui/feedback/feedbackd.py
#	openpilot/selfdrive/ui/layouts/settings/device.py
#	openpilot/selfdrive/ui/layouts/settings/toggles.py
#	openpilot/selfdrive/ui/mici/layouts/onboarding.py
#	openpilot/selfdrive/ui/onroad/augmented_road_view.py
#	openpilot/selfdrive/ui/tests/test_soundd.py
#	openpilot/selfdrive/ui/translations/app.pot
#	openpilot/selfdrive/ui/translations/app_de.po
#	openpilot/selfdrive/ui/translations/app_en.po
#	openpilot/selfdrive/ui/translations/app_es.po
#	openpilot/selfdrive/ui/translations/app_fr.po
#	openpilot/selfdrive/ui/translations/app_ja.po
#	openpilot/selfdrive/ui/translations/app_ko.po
#	openpilot/selfdrive/ui/translations/app_pt-BR.po
#	openpilot/selfdrive/ui/translations/app_th.po
#	openpilot/selfdrive/ui/translations/app_tr.po
#	openpilot/selfdrive/ui/translations/app_uk.po
#	openpilot/selfdrive/ui/translations/app_zh-CHS.po
#	openpilot/selfdrive/ui/translations/app_zh-CHT.po
#	openpilot/system/athena/athenad.py
#	openpilot/system/hardware/hardwared.py
#	openpilot/system/loggerd/deleter.py
#	openpilot/system/manager/process_config.py
#	openpilot/system/ui/lib/application.py
#	panda
#	pyproject.toml
#	tinygrad_repo
#	uv.lock
2026-08-13 17:47:11 -04:00
discountchubbs c3df6bb617 move build all to hf 2026-08-13 14:20:06 -07:00
discountchubbs 89e536f909 pkl name 2026-08-13 13:22:43 -07:00
discountchubbs 7ad886c765 dude i am missing so much 2026-08-13 12:37:42 -07:00
discountchubbs 0892756d22 smh 2026-08-13 12:14:48 -07:00
discountchubbs 1ee512ca79 fucking validation wants raw to fetch and full to push. grr 2026-08-13 11:51:05 -07:00
discountchubbs 6cf5b084a1 mf its a dataaset. i knew that 2026-08-13 11:27:56 -07:00
discountchubbs fc4734c598 try hf as a trusted publisher :) 2026-08-13 11:00:58 -07:00
discountchubbs 5fd3b4adac reduce flakiness by using artifact-name from build-model to regex, speed up pub b y checking the name before trying to clone and publish again 2026-08-13 10:39:13 -07:00
discountchubbs 85f43de4ed sync 2026-08-13 10:13:25 -07:00
discountchubbs 110cf5822b who put those there. ?? 2026-08-13 10:11:27 -07:00
Adeeb Shihadeh b7c333cf3f pigeond: add comma AGPS source (#38616) 2026-08-12 20:31:41 -07:00
Adeeb Shihadeh fe47e752f6 log USB3 lane (#38617) 2026-08-12 20:15:10 -07:00
Daniel Koepping 10502adf95 AGNOS 19.6 (#38611)
* bump version

* add production
2026-08-11 23:27:16 -07:00
Adeeb Shihadeh 7a76a50c4f Add release-chestnut branch 2026-08-11 21:48:27 -07:00
Daniel Koepping 927e822b5a fix tap to factory reset (#38613)
hardware: lazily import optional SIM support
2026-08-11 21:25:45 -07:00
Daniel Koepping f1746f2e2d fix spinner crash (#38612)
ui: keep spinner independent of build artifacts
2026-08-11 20:42:08 -07:00
Adeeb Shihadeh 473ab17876 add cupra to release notes 2026-08-11 18:15:10 -07:00
Adeeb Shihadeh ebc77e4c90 Revert "camerad: improve IFE robustness to lags (#38548)"
This reverts commit 122a8ca00a.
2026-08-11 18:13:02 -07:00
Daniel Koepping ad5afe222d log chestnut ASM at 10Hz (#38609)
* modeld: only read the smu while the big model runs

* chestnutState at 10Hz

* smu polling
2026-08-11 18:09:25 -07:00
Shane Smiskol 32b518d51d set long maneuvers to actuation limits 2026-08-11 17:48:13 -07:00
Adeeb Shihadeh 062cb672e8 camerad: force BPS clock to max (#38539)
* camerad: account for BPS clock overhead

* force to max
2026-08-11 00:39:51 -07:00
Adeeb Shihadeh 122a8ca00a camerad: improve IFE robustness to lags (#38548)
camerad: keep request IDs monotonic
2026-08-10 22:36:54 -07:00
Daniel Koepping b1cdf387b5 sync after a build (#38606)
* build: sync after a successful build

* sync on failed builds too
2026-08-10 21:23:59 -07:00
Shane Smiskol b5b156825d ui: fix inverted DM head tracking on RHD (#38607)
* ui: fix inverted DM head tracking on RHD

b29d0a17af switched the mici driver state renderer from computing pose
from driverStateV2 itself to consuming driverMonitoringState.visionPolicyState.pose.
That published yaw is mirrored for RHD (policy.py:269) so the asymmetric yaw
offsets/thresholds can be tuned in a single frame, so the UI's unconditional
sign flip left RHD head tracking backwards. LHD was unaffected, which is why
the refactor looked like a no-op.

Undo the mirror on the UI side.

* Apply suggestions from code review

* fold the sign into the RHD multiplier
2026-08-10 20:02:52 -07:00
discountchubbs b2dd5fbd9c cd 2026-08-10 15:21:19 -07:00
discountchubbs 6dacf9a9c6 no i and x 2026-08-10 15:16:51 -07:00
discountchubbs 904ad9548b whoops 2026-08-10 15:13:54 -07:00
discountchubbs de12cba90d dont download big when we dont have big lol 2026-08-10 14:58:41 -07:00
discountchubbs f5d5a8a329 manual seed , reuse memory buffers across runs 2026-08-10 14:06:39 -07:00
discountchubbs 8fa6067c9a egpu ready build all split 2026-08-10 13:36:35 -07:00
discountchubbs 9b5b30ff43 rip 2026-08-10 13:32:43 -07:00
discountchubbs 3b13e81414 lint 2026-08-10 13:25:09 -07:00
discountchubbs 29297bcf59 done in build model now 2026-08-10 13:20:52 -07:00
discountchubbs ce813b86b7 fix test 2026-08-10 13:17:45 -07:00
discountchubbs 5b88ec2c25 god no onnx either 2026-08-10 13:14:41 -07:00
discountchubbs a6c026461e no pkls please 2026-08-10 13:14:05 -07:00
discountchubbs 776965ad3d egpu 2026-08-10 13:10:32 -07:00
discountchubbs ebce78eca2 bump tg 2026-08-10 12:41:29 -07:00
discountchubbs a5b7635e21 Merge remote-tracking branch 'origin/master' into egpu-support 2026-08-10 12:37:42 -07:00
Vraj Parikh eecff73850 Sleep shadow mode (#38578)
* dmvision c703670b-6a4a-4743-8f79-69d5486cadf7 (a62f9948 with new sleep head)
* dmvision c703670b-6a4a-4743-8f79-69d5486cadf7 (a62f9948 with new sleep head and same batchnorms)
2026-08-10 10:21:16 -07:00
Adeeb Shihadeh 6d5d1f6917 Rename "live" cereal services (#38601) 2026-08-10 09:30:22 -07:00
commaci-public 434fbad5fc [bot] Update Python packages (#38599)
* Update Python packages

* Fix package update CI

---------

Co-authored-by: Vehicle Researcher <user@comma.ai>
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2026-08-10 08:36:12 -07:00
Adeeb Shihadeh 65c007a9a2 tools: setup.sh isn't for AGNOS (#38588)
* tools: setup.sh isn't for AGNOS

* Update setup.sh
2026-08-10 08:16:57 -07:00
Adeeb Shihadeh ea4dbc8936 Remove unused code (#38598) 2026-08-09 21:09:50 -07:00
Daniel Koepping 855c99bf92 speedup chestnut probe (#38596)
use link-up in scons
2026-08-09 16:26:55 -07:00
Jason Wen f8d50e062d updated: add fallback mechanism for agnos.json path resolution (#38593)
* updated: add fallback mechanism for agnos.json path resolution

* nvm fallback seems overkill, red diff

* add symlink for tici/agnos.json

---------

Co-authored-by: stefpi <19478336+stefpi@users.noreply.github.com>
2026-08-08 19:55:32 -07:00
Adeeb Shihadeh fedea9fe8d camera names that make sense (#38585) 2026-08-08 09:44:21 -07:00
Adeeb Shihadeh 9738db0e39 Move camera stream types out of msgq (#38591) 2026-08-08 09:32:47 -07:00
Adeeb Shihadeh df0bd5e6e3 update translations (#38590) 2026-08-08 08:51:58 -07:00
stef cf790746cc encoder/webrtc: resize streams to baseline 3.1 H264 profile (#38587)
* add h264 profile option and different profiles for comma3X and comma4

* update to high profile

* resize to be baseline 3.1

* fix

* gop 15 and clean

* revert gop 15

* fix white space

* simplify teleoprtc

* bump teleop
2026-08-07 23:20:17 -07:00
Daniel Koepping 5701fcad05 ui: show gpu icon longer (#38586) 2026-08-07 21:11:34 -07:00
Daniel Koepping 59f01d212a AGNOS 19.5 (#38582)
* bump version

* add staging

* add production

* fix AGNOS manifest symlink
2026-08-07 21:08:33 -07:00
Daniel Koepping e32b1a344d chestnut: update firmware (#38576)
* chestnut: update firmware to b0805491

* update to ed4e39b7
2026-08-07 20:16:13 -07:00
Adeeb Shihadeh d02355b1a5 rename tici hardware platform to comma (#38581)
* rename tici hardware platform to comma

* no /TICI

* lil more

* lil more

* you had a good life larch64

* lil more

* one more
2026-08-07 20:12:34 -07:00
Daniel Koepping b755d32276 fix telemetry hanging big model (#38565)
* modeld: fail fast when the gpu hangs mid run

* modeld: harden chestnut telemetry

* selfdrived: rm the big model lagging trigger
2026-08-07 19:38:13 -07:00
Adeeb Shihadeh cbda3f7994 Rename hardware/tici/ -> hardware/comma/ (#38580) 2026-08-07 18:49:27 -07:00
Fang Li a75cd2d277 Process eSIM notifications after profile operations per SGP.22 spec (#38241)
* Process eSIM notifications after profile operations

* Clarify sleep purpose after operation in execute_and_process_notifications

Added comment to clarify the purpose of the sleep.
2026-08-07 16:56:49 -07:00
Adeeb Shihadeh 8a88608800 rm qrcode (#38575) 2026-08-07 15:41:28 -07:00
ZwX1616 4eb515b32d DM: Zoom Zoom Model (#38554) 2026-08-07 15:39:59 -07:00
Adeeb Shihadeh 612d97cfdb paramsd: don't learn in reverse gear (#38573) 2026-08-07 14:56:18 -07:00
Adeeb Shihadeh edd938105e remove old regen infra (#38570) 2026-08-07 14:11:12 -07:00
PeterPhuTran 5fadc71a3a swaglog: delete oldest logs on rollover, not newest (#38322) 2026-08-07 13:37:57 -07:00
GavinnnK 0819f5c0fd common: clean up failed atomic writes (#38552)
* common: clean up failed atomic writes

* common: remove atomic write regression test

---------

Co-authored-by: GavinnnK <239280171+GavinnnK@users.noreply.github.com>
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2026-08-07 13:34:11 -07:00
eFini 66b3590b63 Use Noto fonts for Asian languages (#36514)
ui: use Noto fonts for Asian languages

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2026-08-07 13:30:58 -07:00
Adeeb Shihadeh b77da00697 rm unused NotoColorEmoji.ttf 2026-08-07 13:20:53 -07:00
Adeeb Shihadeh e6939dbede inline modem serial support (#38564) 2026-08-07 13:19:02 -07:00
Adeeb Shihadeh 96ca1f8ed5 ui: load fonts on the fly (#38567) 2026-08-07 13:10:32 -07:00
Adeeb Shihadeh 6f94f217ff remove PR template 2026-08-07 12:51:07 -07:00
Adeeb Shihadeh 03803d0c82 manager: remove restarter (#38566) 2026-08-07 11:56:48 -07:00
Jimmy be191eb8ac loggerd: only wait to rotate on cameras that are actually streaming (#38362)
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2026-08-07 11:48:13 -07:00
Adeeb Shihadeh 4e9e9190a5 params: fix cleanup hang on exit (#38562) 2026-08-07 11:29:47 -07:00
Zayan Khan ff9c703289 Fix doc links broken by the openpilot/ restructure (#38467) 2026-08-07 10:41:07 -07:00
Andi Radulescu 2f4981045b esim: report when no eUICC is available (#38497)
* esim: report when no eUICC is available

* remove newline
2026-08-07 10:39:45 -07:00
Matt Purnell faf966adb2 ui: fix value text eliding when it fits (#38502) 2026-08-07 10:39:22 -07:00
Matt Purnell bd7b419a4e ui: don't spam logs when NetworkManager is not available (#38504) 2026-08-07 10:36:52 -07:00
Adeeb Shihadeh 9bf834e64e op: handle AGNOS updates (#38543)
* op: handle AGNOS updates

* use common

* don't just reboot
2026-08-07 10:32:28 -07:00
Daniel Koepping 4adf52fc6f bump opendbc (#38561) 2026-08-07 10:32:08 -07:00
commaci-public 76e900d3ad [bot] Update Python packages (#38553)
Update Python packages

Co-authored-by: Vehicle Researcher <user@comma.ai>
2026-08-07 09:48:06 -07:00
Daniel Koepping 155d206b2b ui: hide egpu icon on disengage (#38557) 2026-08-07 02:40:53 -07:00
Daniel Koepping dd2214a78c chestnut updater (#38546)
* chestnut: production flasher with ROM bootloader recovery

* hardwared: flash chestnut firmware offroad

* comments

* single FW version

* hardwared: fix chestnut flasher path

* chestnut: update firmware to 1d368808

* hardwared: log the flasher output

* comment

* hardwared: flash in a thread

* match panda flashing style

* fix

* chestnut: defer signals and harden the flasher

* chestnut: detect bootloader mode and retry

* comments

* chestnut: name the config dir for its content

* chestnut: name the ROM bootloader consistently

* chestnut: clearer name for the device lookup

* chestnut: spell out the low level names

* chestnut: claim the interface, not the device
2026-08-07 02:35:17 -07:00
Daniel Koepping d252ab5b42 modeld: drop non-finite usbgpu output (#38556) 2026-08-07 01:41:46 -07:00
Daniel Koepping 2002a64d3b modelV2: add big model bool (#38555)
modelV2: add big
2026-08-07 01:30:48 -07:00
stef 438035ec17 bump teleoprtc (#38551)
bump
2026-08-06 18:47:31 -07:00
Daniel Koepping 5406aba1a2 more chestnut metrics (#38550)
* more chestnut metrics

* comment
2026-08-06 15:13:44 -07:00
Adeeb Shihadeh 17150b6db0 camerad: better names for request and frame IDs (#38549) 2026-08-06 12:14:31 -07:00
Adeeb Shihadeh e7b1ee3a5b jp: fix segment range parsing 2026-08-05 21:42:11 -07:00
discountchubbs df83374927 modeld_v2: Support eGpu 2026-08-05 13:51:29 -07:00
Daniel Koepping d86686fffd log chestnut metrics (#38527)
* log chestnut state

* chestnut metrics

* more metrics

* fix chestnut metrics

* cache chestnut power limit

* use valid

* move to class
2026-08-05 07:21:58 -07:00
Daniel Koepping a7f32be2fd probe chestnut before compiling big model (#38540)
* probe gpu

* only on uncompiled

* fix USB GPU build probe

* allow cold Chestnut initialization

* retry

* comment

* fix
2026-08-05 03:43:54 -07:00
Shane Smiskol 4db21dd4f8 comma four: experimental mode explainer (#37961)
* exp mode confirm dialog

* full

* base

* clean up

* too diff

* fix?

* rmnl

* only toggle if exp mode is confirmed

---------

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2026-08-05 02:30:00 -07:00
CzokNorris aa0ac919ba pandad: guarantee SPI slave turnaround time (#38464)
pandad: add SPI turnaround delay

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2026-08-04 22:41:16 -07:00
xiaoxx970 2e4de38eb8 timed: set the clock from GPS in UTC, not local time (#38523) 2026-08-04 21:00:05 -07:00
Adeeb Shihadeh ed777cf5e2 agnos 19.4 (#38537) 2026-08-04 20:55:08 -07:00
Adeeb Shihadeh f7bd4002d6 add libdatachannel to agnos (#38535) 2026-08-04 15:43:46 -07:00
Adeeb Shihadeh 38ef0d2398 agnos 19.2 (#38531) 2026-08-04 11:37:25 -07:00
Toby Penner b361e952c9 Revert "Rebellious Hope model" (#38530)
Revert "Rebellious Hope model (#38475)"

This reverts commit 93f5aa469a.
2026-08-04 10:58:56 -07:00
Adeeb Shihadeh d4211790da agnos 19.1 (#38528) 2026-08-04 09:09:06 -07:00
Shane Smiskol b614253eeb bump opendbc (#38525)
* bump

* bump
2026-08-03 22:45:10 -07:00
Adeeb Shihadeh 6f88b59fdd update release notes 2026-08-03 19:35:27 -07:00
Adeeb Shihadeh e04313cc91 agnos 19 (#38520) 2026-08-03 19:34:00 -07:00
Daniel Koepping 7ef4304e2e handle transient sysfs read errors (#38521)
handle transient USB sysfs read error
2026-08-03 16:49:01 -07:00
Adeeb Shihadeh afa2b390ae ci: fix race condition on closed branches check 2026-08-03 14:49:27 -07:00
YassineYousfi 04847f3805 chestnut: add new usb vid (#38517) 2026-08-03 12:46:46 -07:00
commaci-public 9002016ac5 [bot] Update Python packages (#38516)
Update Python packages

Co-authored-by: Vehicle Researcher <user@comma.ai>
2026-08-03 09:57:06 -07:00
Adeeb Shihadeh 54a7e8926f update athena version test 2026-08-01 23:25:35 -07:00
Adeeb Shihadeh 20eb0d733a athena: publish git commit date (#38513) 2026-08-01 23:07:50 -07:00
Adeeb Shihadeh f09211fcc5 clips fixups (#38512) 2026-08-01 22:28:30 -07:00
Adeeb Shihadeh 7dbdff832f encoderd: add fast hardware clip transcoding (#38508) 2026-08-01 21:39:24 -07:00
Adeeb Shihadeh 88c8f5a520 clips: add getClipChunk (#38510)
* clips: upload over webrtc

* revert that

* clips: transfer over athena
2026-08-01 21:26:31 -07:00
Adeeb Shihadeh 5037016901 loggerd: move qcom decoder into v4l_decoder (#38511) 2026-08-01 20:40:08 -07:00
YassineYousfi bf74ce5447 Be Right Here Model 🏃 (big) (#38499)
* 8e649fab-fb3d-4407-b99c-70f27df8b282/400

* bump tg

* TC_OPT
2026-08-01 20:27:29 -07:00
Adeeb Shihadeh 3643dfbef0 simple clips (#38509) 2026-08-01 19:17:44 -07:00
Shane Smiskol cc83d8615d bump opendbc (#38507)
bump
2026-08-01 06:26:49 -07:00
Shane Smiskol d3058de5c1 ldw: fix blinker cooldown using wrong time step (#38501)
Co-authored-by: Shane Smiskol <trivisionzero@gmail.com>
2026-08-01 06:26:31 -07:00
Daniel Koepping a2ee4dfffc AGNOS 18.8 (#38496)
* bump version

* add staging

* update to production
2026-07-31 11:39:39 -07:00
Adeeb Shihadeh b72f6e9f7d we write it ourselves 2026-07-31 11:39:17 -07:00
Daniel Koepping d4e29ec945 fade out eGPU icon (#38506)
Fade GPU logo on engagement
2026-07-31 11:26:03 -07:00
Daniel Koepping e3ba6492f9 fix big model alert (#38500) 2026-07-30 17:33:25 -07:00
Daniel Koepping caee482fe7 usb logging on topology change (#38495)
speedup usb detection
2026-07-30 08:26:02 -07:00
Daniel Koepping 1c54ff853a change big model alerts (#38490)
* fix big model failed transition

* fix eGPU onroad transitions

* don't alert on eGPU hot connect

* green

* big
2026-07-30 08:20:25 -07:00
Shane Smiskol c9f0d296b1 rename CarEvents (#38493) 2026-07-29 20:08:20 -07:00
Shane Smiskol 6ac27d491b Add carNotReady noEntry (#38485)
* car not ready

* missing

* missing

* bump
2026-07-29 20:02:35 -07:00
Daniel Koepping 47a7524595 use GPU icon onroad (#38492)
* rm permanent alert

* use gpu icon instead of BIG text

* add crossed state

* rm state

* opacity
2026-07-29 17:51:13 -07:00
Daniel Koepping 0088cb7ef0 revert tinygrad bump (#38489) 2026-07-29 13:07:49 -07:00
Daniel Koepping 3ec980477d show big model failed (#38487) 2026-07-29 10:22:32 -07:00
Shane Smiskol e4d152e1a7 bump opendbc (#38486)
bump
2026-07-29 02:51:42 -07:00
Shane Smiskol 4cfdcea1d1 cabana: fix macOS build and --zmq bridge path (#38484)
Two macOS-only cabana fixes:

- signalview.h: rename the SignalView::setModel parameter that shadowed
  the `model` member. Newer clang (Apple clang 21) folds
  "parameter shadows a field" into plain -Wshadow, so with -Werror this
  breaks the macOS build. Older clang (CI's image) only flags it under
  -Wshadow-all, which is why CI stays green. The rename is clean under
  any -Wshadow level and gcc.

- devicestream.cc: the --zmq bridge path had a stale `openpilot/`
  segment (`../../openpilot/cereal/messaging/bridge`), a monorepo-layout
  leftover that resolved to a non-existent triple-`openpilot` path, so
  the bridge failed to exec and live streaming was broken. Drop it to
  `../../cereal/messaging/bridge`.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 20:00:25 -07:00
Daniel Koepping 952d2cab8a Revert "profile usb gpu link stability (#38368)" (#38483) 2026-07-28 18:15:11 -07:00
Daniel Koepping 799551c2d6 rm complete sound (#38481)
* rm complete sound for now

* deprecate bigModelReady
2026-07-28 17:25:36 -07:00
Adeeb Shihadeh 0b7bd2561d unittest speedups (#38472)
* unittest speedups

* fix deleter test helper signature

* document small deleter test files

* test uploader clears locks on startup
2026-07-28 16:52:41 -07:00
Adeeb Shihadeh 83000c571c no slow unittests (#38480)
* rm test_regen

* not slow

* skip bridge

* rm that one too

* just op test

* just test
2026-07-28 16:42:38 -07:00
Daniel Koepping 294635beb5 big model failed alert (#38479) 2026-07-28 16:25:34 -07:00
Daniel Koepping 3040f55382 ui: breathe BIG while loading model (#38478) 2026-07-28 15:52:20 -07:00
Adeeb Shihadeh b4c9403211 gitignore intermediate build products 2026-07-28 10:05:13 -07:00
Adeeb Shihadeh e04bb46b68 Update Python packages (#38477)
* Update Python packages

* fix DeviceLike awake protocol

---------

Co-authored-by: Vehicle Researcher <user@comma.ai>
2026-07-28 09:20:33 -07:00
Shane Smiskol e43de73311 bump opendbc (#38476)
bump
2026-07-28 04:18:47 -07:00
stef 02b6cef15c webrtc: cleanup api (#38447)
* add support for multi-track with wideRoad single track as default

* remove stale debug mode

* multi track support

* make cameras list optional

* remove multi camera support from athena, make it only local

* update to teleop master
2026-07-27 20:47:43 -07:00
Toby Penner 93f5aa469a Rebellious Hope model (#38475)
d0c60276-564d-4b97-a59c-f1ab853d8c26/400
2026-07-27 15:35:28 -07:00
Adeeb Shihadeh 74009870ab cleanup banned APIs 2026-07-26 21:28:30 -07:00
Adeeb Shihadeh 1d7b6f0f9f dev is tools 2026-07-26 21:27:34 -07:00
Adeeb Shihadeh 98e7c4f987 replace pytest with lil custom runner (#38471) 2026-07-26 21:26:19 -07:00
stef 4dab10e98b rm pillow package (#38466) 2026-07-26 15:04:56 -07:00
stef df90fc2526 remove PIL (#38320)
* replace PIL in emoji

* replace PIL in test

* fix uv.lock

* fix uv lock

* revert emoji removal from this P R

* revert merge artifact

* remove more merge artifacts

* fix lock

* red diff

* red diff +
2026-07-26 14:57:54 -07:00
stef f29eb5d25c ui: remove emoji support (#38361)
* remove emoji support, replace with png

* upload icons

* fix icon left/right positioning

* simplify rewrite

* simplify firehose

* simplify custom software set up text

* lint

* fix custom software

* move fix to diff pr

---------

Co-authored-by: elkoled <elkoled@gmail.com>
2026-07-26 14:37:19 -07:00
Adeeb Shihadeh 0bf99879d1 docs fixups 2026-07-26 14:34:43 -07:00
Adeeb Shihadeh e1db0f59f1 docs: replace zensical with dumb html template (#38462) 2026-07-26 13:56:32 -07:00
Adeeb Shihadeh 5bcff3f87d bump opendbc: rm jinja (#38463) 2026-07-26 12:18:11 -07:00
Adeeb Shihadeh 9a39ccc66f tools/auth: use any open port (#38458) 2026-07-26 08:52:49 -07:00
Adeeb Shihadeh 27122bbd28 lint: check indentation (#38454)
* lint: check indentation

* cleanup

* happy

* that was too much

* happy
2026-07-25 10:29:37 -07:00
Adeeb Shihadeh c37855d113 op: merge setup and install (#38450) 2026-07-24 11:03:01 -07:00
Toby Penner 9225dac433 Revert "Rebel Legion model (#38164)" (#38449)
This reverts commit 2895346746.
2026-07-24 10:21:01 -07:00
stef ee6f374621 bump teleoprtc (#38446)
bump
2026-07-23 20:45:49 -07:00
stef 0fdfaf737f ui(body): flip eye direction (#38445)
flip eye
2026-07-23 20:42:51 -07:00
stef 71e29583cb Revert "bump teleoprtc" (#38444)
Revert "bump teleoprtc (#38443)"

This reverts commit 171f271d11.
2026-07-23 20:39:22 -07:00
stef 171f271d11 bump teleoprtc (#38443)
bump
2026-07-23 19:59:21 -07:00
Adeeb Shihadeh 1a753ad9ae test_models moved to opendbc (#38441)
* remove test_models

* rm that

* update test_models docs

* restore car model test tool

* leave dependencies unchanged
2026-07-23 19:50:08 -07:00
Adeeb Shihadeh 9c312e91e7 release: include nested packages in PYTHONPATH (#38442) 2026-07-23 19:49:31 -07:00
Adeeb Shihadeh 4d4d6803e5 modeld: remove tinygrad probe on build (#38439) 2026-07-23 19:11:23 -07:00
Harald Schäfer 9c21b92c4d should_stop: use shared helper (#38438) 2026-07-23 18:36:31 -07:00
Daniel Koepping ab7284fc8a AGNOS 18.7 (#38416)
* bump version

* update AGNOS 18.7

* update to production
2026-07-23 15:51:34 -07:00
stef 554d8d2114 webrtcd: timeout after 5 minutes (#38437)
* webrtc timeout after 5 mins

* omit timeout for body
2026-07-23 15:11:22 -07:00
Daniel Koepping b392328c35 big model loading permanent alert (#38436)
show big model loading as permanent alert
2026-07-23 15:00:25 -07:00
Adeeb Shihadeh ddf782f08a big release 2026-07-23 13:36:12 -07:00
Adeeb Shihadeh e04fd3c724 rm private reporter comment (#38431) 2026-07-23 10:23:08 -07:00
Adeeb Shihadeh 6b47a5b6b7 tools: decompress in the python downloader (#38430)
* tools: decompress in the python downloader

* and zstd
2026-07-23 10:06:44 -07:00
Daniel Koepping 3601b850c2 new sounds for 3x (#38404)
rm tizi sounds
2026-07-23 09:57:47 -07:00
Adeeb Shihadeh cfa8e5bf03 gitignore test_swaglog 2026-07-23 09:42:36 -07:00
Adeeb Shihadeh e1c69719c6 bump panda (#38429) 2026-07-23 09:35:05 -07:00
Adeeb Shihadeh a8bb6b5aee rm hypothesis (#38428) 2026-07-23 09:17:18 -07:00
Adeeb Shihadeh 87fde8a058 pyproject cleanup 2026-07-23 08:31:54 -07:00
stef 66300306f4 ui(body): hide body layout if no car fingerprint (#38427)
* hide body layout if car params is none

* reposition

* refactor
2026-07-22 21:03:25 -07:00
Harald Schäfer 2895346746 Rebel Legion model (#38164)
* 3e5b7d32-e114-4517-96b4-b3a2448fd939/400

* 9ffd502c-01d0-40f3-93c2-07a4d3e4445c/400

* 1afc2042-8761-4663-ae6a-d593b13defd0/400

* d4b183da-3b8c-4ff7-8927-e3e7216ddb86/400

* 60a46520-f02f-49c5-8eae-e0f8de92af07/400

* ca63f597-de00-478f-970c-302069a59130/400

* 0322768e-5d7e-46e6-9712-d52de312e1da/400

* d25f38c8-8689-42a7-8326-f0c7ce00625f

* f14f0be4-bf1e-4dc3-914d-3d9a23b5a05a/400

* lil smooth

* 03dfde49-54e6-4129-b05f-24b0b0c7e5f0/400

* 6d9d6f8a-5c82-41f6-92aa-4c1a11eb5645/400

* ci: fix model review tinygrad import

* ci: skip unfetched big model in review

---------

Co-authored-by: Toby Penner <toby@comma.ai>
2026-07-22 18:12:13 -07:00
Shane Smiskol 49c3b8fc34 joystick: add override state (#38423)
fix js joverride
2026-07-22 17:24:38 -07:00
Daniel Koepping 0bac3c9039 big model loaded chime (#38420)
play chime when big model is loaded
2026-07-22 16:44:16 -07:00
Daniel Koepping 6335db69bd eGPU UI (#38419) 2026-07-22 16:23:32 -07:00
Shane Smiskol 05f42f752b joystickd: use should_stop (#38421)
* param

* not really needed now
2026-07-22 16:07:43 -07:00
Daniel Koepping 1791057369 add big model fallback (#38303)
* big model fallback

* reorder
2026-07-22 15:13:46 -07:00
Harald Schäfer 736f3b1a00 longitudinal: move cruise speed limit out of the MPC (#38367)
*
2026-07-22 14:53:56 -07:00
Adeeb Shihadeh 2a1c44c904 rm scons cython tool (#38417) 2026-07-22 13:43:23 -07:00
Adeeb Shihadeh 74ac5ef9a0 try ctypes params (#38411)
* try cffi params

* ctypes

* lil more

* rm cython

* lil more

* that was fine

* lil more

* Drop unrelated Params concurrency changes

* Clean up ctypes Params build integration

* just c

* Clarify Params exception translation

* Expand Params C wrappers

* lil more
2026-07-22 13:26:28 -07:00
Daniel Koepping aac9d9ecf4 ui: show eGPU icon when enumerated (#38407)
* ui: react to egpu hotplug instead of waiting for modeld params

* use chestnutPresent
2026-07-22 10:25:58 -07:00
Adeeb Shihadeh 08fd571272 add -pipe to build (#38410) 2026-07-21 22:35:27 -07:00
Adeeb Shihadeh 75d590fb90 replace catch2 tests for 20% faster builds (#38408)
* replace catch2 tests

* lil more

* lil more

* rm dep!
2026-07-21 22:20:09 -07:00
Adeeb Shihadeh 576de9c7ed speed up jotpluggler build (#38406) 2026-07-21 21:28:21 -07:00
Adeeb Shihadeh 86753ea93d better alerts (#38403)
* better alerts

* just rm that

* lil more
2026-07-21 21:01:19 -07:00
Adeeb Shihadeh c9b9fd56ea gc dead events (#38402) 2026-07-21 18:49:47 -07:00
Adeeb Shihadeh 765d6fff9b remove audio feedback (#38401)
* remove audio feedback

* rm feedbackd

* backwards
2026-07-21 18:30:20 -07:00
Daniel Koepping 9e02936717 AGNOS 18.6 (#38393)
* version

* agnos 18.6

* fix
2026-07-21 16:23:27 -07:00
Armand du Parc Locmaria f1977eaa93 bump tg (#38398)
* bump tg

* slightly nicer
2026-07-21 16:08:19 -07:00
Adeeb Shihadeh eb6c9c7f0d Run device tests directly (#38397) 2026-07-21 12:52:48 -07:00
Adeeb Shihadeh 1262e26899 athena: bring back port migration
This reverts commit d9596fa998.
2026-07-21 12:51:45 -07:00
Adeeb Shihadeh 911f07ee87 convert tests to unittest (#38387) 2026-07-21 11:02:34 -07:00
Adeeb Shihadeh ebb5cc12f5 ci: don't pay the big model tax yet (#38371)
* ci: don't pay the big model tax yet

* skip big in release
2026-07-21 10:55:26 -07:00
459 changed files with 8735 additions and 7504 deletions
+1 -1
View File
@@ -11,4 +11,4 @@
*.wav filter=lfs diff=lfs merge=lfs -text
openpilot/selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text
openpilot/common/hardware/tici/updater filter=lfs diff=lfs merge=lfs -text
openpilot/common/hardware/comma/updater filter=lfs diff=lfs merge=lfs -text
-68
View File
@@ -1,68 +0,0 @@
<!-- Please copy and paste the relevant template -->
<!--- ***** Template: Fingerprint *****
**Car**
Which car (make, model, year) this fingerprint is for
**Route**
A route with the fingerprint
-->
<!--- ***** Template: Car Bugfix *****
**Description**
A description of the bug and the fix. Also link the issue if it exists.
**Verification**
Explain how you tested this bug fix.
**Route**
Route: [a route with the bug fix]
-->
<!--- ***** Template: Bugfix *****
**Description**
A description of the bug and the fix. Also link the issue if it exists.
**Verification**
Explain how you tested this bug fix.
-->
<!--- ***** Template: Car Port *****
**Checklist**
- [ ] added entry to CAR in openpilot/selfdrive/car/*/values.py and ran `openpilot/selfdrive/car/docs.py` to generate new docs
- [ ] test route added to [routes.py](https://github.com/commaai/openpilot/blob/master/openpilot/selfdrive/car/tests/routes.py)
- [ ] route with openpilot:
- [ ] route with stock system:
- [ ] car harness used (if comma doesn't sell it, put N/A):
-->
<!--- ***** Template: Refactor *****
**Description**
A description of the refactor, including the goals it accomplishes.
**Verification**
Explain how you tested the refactor for regressions.
-->
+44 -184
View File
@@ -7,6 +7,19 @@ on:
description: 'Minimum selector version required for the models (see helpers.py or readme.md)'
required: true
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:
setup:
@@ -46,13 +59,14 @@ jobs:
id: get-json
run: |
cd docs/docs
latest=$(ls driving_models_v*.json | sed -E 's/.*_v([0-9]+)\.json/\1/' | sort -n | tail -1)
PREFIX="driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_' || '' }}v"
latest=$(ls ${PREFIX}*.json | sed -E "s/${PREFIX}([0-9]+)\.json/\1/" | sort -n | tail -1)
next=$((latest+1))
json_file="driving_models_v${next}.json"
cp "driving_models_v${latest}.json" "$json_file"
json_file="${PREFIX}${next}.json"
cp "${PREFIX}${latest}.json" "$json_file"
echo "json_file=docs/docs/$json_file" >> $GITHUB_OUTPUT
echo "json_version=$((next+0))" >> $GITHUB_OUTPUT
echo "SRC_JSON_FILE=docs/docs/driving_models_v${latest}.json" >> $GITHUB_ENV
echo "SRC_JSON_FILE=docs/docs/${PREFIX}${latest}.json" >> $GITHUB_ENV
- name: Extract tinygrad models
id: set-matrix
@@ -61,45 +75,23 @@ jobs:
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
- 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
- name: Get next recompiled dir number
id: create-recompiled-dir
env:
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
HF_REPO: ${{ github.event.inputs.hf_repo }}
run: |
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
cd gitlab_docs
git checkout main
git sparse-checkout set --no-cone models/
cd models
latest_dir=$(ls -d recompiled* 2>/dev/null | sed -E 's/recompiled([0-9]+)/\1/' | sort -n | tail -1)
if [[ -z "$latest_dir" ]]; then
next_dir=1
else
next_dir=$((latest_dir+1))
fi
recompiled_dir="${next_dir}"
mkdir -p "recompiled${recompiled_dir}"
touch "recompiled${recompiled_dir}/.gitkeep"
cd ../..
pip install huggingface_hub
recompiled_dir=$(python3 -c "
from huggingface_hub import HfApi
import re, sys
api = HfApi()
files = api.list_repo_files(repo_id=sys.argv[1], repo_type='dataset')
dirs = [re.search(r'models/recompiled([0-9]+)', f) for f in files]
nums = [int(m.group(1)) for m in dirs if m]
print(max(nums) + 1)
" "$HF_REPO")
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
run: |
cd docs
@@ -123,25 +115,30 @@ jobs:
is_20hz: ${{ matrix.model.is_20hz }}
recompiled_dir: ${{ needs.setup.outputs.recompiled_dir }}
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
retry_failed_models:
needs: [setup, get_and_build]
runs-on: ubuntu-latest
if: ${{ needs.setup.result != 'failure' && !cancelled() }}
if: ${{ !cancelled() && needs.setup.result == 'success' && (needs.get_and_build.result == 'success' || needs.get_and_build.result == 'failure') }}
outputs:
retry_matrix: ${{ steps.set-retry-matrix.outputs.retry_matrix }}
steps:
- uses: actions/download-artifact@v4
with:
pattern: model-*
pattern: artifact-name-*
path: output
continue-on-error: true
- id: set-retry-matrix
run: |
echo '${{ needs.setup.outputs.model_matrix }}' > matrix.json
built=(); while IFS= read -r line; do built+=("$line"); done < <(
find output -maxdepth 1 -name 'model-*' -printf "%f\n" | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}'
built=(); while IFS= read -r line; do [ -n "$line" ] && built+=("$line"); done < <(
find output -maxdepth 1 -name 'artifact-name-*' -printf "%f\n" 2>/dev/null | sed -E 's/^artifact-name-//' | awk '{gsub(/^ +| +$/, ""); print}'
)
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
@@ -149,7 +146,7 @@ jobs:
retry_get_and_build:
needs: [setup, get_and_build, retry_failed_models]
if: ${{ needs.get_and_build.result == 'failure' || (needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '') }}
if: ${{ !cancelled() && needs.retry_failed_models.result == 'success' && needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '' }}
strategy:
matrix:
model: ${{ fromJson(needs.retry_failed_models.outputs.retry_matrix) }}
@@ -161,146 +158,9 @@ jobs:
is_20hz: ${{ matrix.model.is_20hz }}
recompiled_dir: ${{ needs.setup.outputs.recompiled_dir }}
json_version: ${{ needs.setup.outputs.json_version }}
target_hardware: ${{ github.event.inputs.target_hardware }}
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
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,11 +29,24 @@ on:
required: false
type: boolean
default: true
bypass_push:
description: 'Bypass pushing to GitLab for build-all'
target_hardware:
description: 'Hardware target to compile for (qcom or usbgpu)'
required: false
default: true
type: boolean
type: string
default: 'qcom'
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:
inputs:
upstream_branch:
@@ -65,8 +78,8 @@ on:
- None
- Master Models
- Release Models
- 2025 World Models
- 2026 World Models
- 2026 Deep RL Models
- Custom Merge Models
- Other
custom_model_folder:
@@ -81,9 +94,22 @@ on:
description: 'Minimum selector version'
required: false
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:
RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }}
JSON_FILE: docs/docs/driving_models_v${{ inputs.json_version }}.json
JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_v' || 'v' }}${{ inputs.json_version }}.json
jobs:
build_model:
@@ -93,38 +119,20 @@ jobs:
custom_name: ${{ inputs.custom_name || inputs.upstream_branch }}
is_20hz: ${{ inputs.is_20hz }}
artifact_suffix: ${{ inputs.artifact_suffix }}
target_hardware: ${{ inputs.target_hardware }}
secrets: inherit
publish_model:
if: ${{ !inputs.bypass_push && !cancelled() }}
if: ${{ !cancelled() && needs.build_model.result == 'success' }}
concurrency:
group: gitlab-push-${{ inputs.recompiled_dir }}
group: hf-push-${{ inputs.recompiled_dir }}
cancel-in-progress: false
needs: build_model
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
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:
@@ -133,16 +141,28 @@ jobs:
path: docs
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
- name: Validate recompiled dir and JSON version
- name: Install huggingface_hub
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: |
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
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
uses: actions/download-artifact@v4
@@ -162,49 +182,26 @@ jobs:
name: ${{ steps.read-artifact-name.outputs.artifact_name }}
path: output
- 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
- name: Create models folder
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
mkdir -p "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}"
cp -r output/* "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/"
rm -f "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/artifact_name.txt"
- name: Push recompiled dir to GitLab
- name: Upload to Hugging Face
env:
GITLAB_SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}
HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }}
ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }}
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 "Create/Update $RECOMPILED_DIR with new/updated model from build-single-tinygrad-model" || echo "No changes to commit"
git push origin main
hf upload ${{ inputs.hf_repo }} \
output/ \
"models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/" \
--repo-type=dataset
- run: |
- name: Pull gh-pages
run: |
cd docs
git pull origin gh-pages
@@ -220,9 +217,11 @@ jobs:
fi
[ -n "${{ inputs.generation }}" ] && ARGS="$ARGS --generation \"${{ inputs.generation }}\""
[ -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 \
--json-path "$JSON_FILE" \
--recompiled-dir "gitlab_docs/models/$RECOMPILED_DIR" \
--recompiled-dir "local_models/$RECOMPILED_DIR" \
--sort-by-date \
$ARGS
+2 -1
View File
@@ -40,6 +40,7 @@ jobs:
echo "run-id=$run_id" >> "$GITHUB_OUTPUT"
- name: Download diff
if: steps.wait.outcome == 'success'
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -48,7 +49,7 @@ jobs:
name: diff_report_${{ github.event.number }}
path: .
- name: Comment on PR
if: steps.wait.outcome == 'success'
if: steps.wait.outcome == 'success' && hashFiles('diff_report.txt') != ''
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b
with:
file-path: diff_report.txt
+7 -3
View File
@@ -15,6 +15,11 @@ concurrency:
group: docs-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
cancel-in-progress: true
env:
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: lfs.fetchexclude
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
jobs:
docs:
name: build docs
@@ -30,8 +35,7 @@ jobs:
- name: Build docs
run: |
git lfs pull
pip install zensical
python scripts/docs.py build
python docs/serve.py --build
# Push to docs.comma.ai
- uses: actions/checkout@v7
@@ -52,7 +56,7 @@ jobs:
git rm -rf .
# copy over docs
cp -r ../docs_site/ docs/
cp -r ../docs/_site/ docs/
# GitHub pages config
touch docs/.nojekyll
-42
View File
@@ -1,42 +0,0 @@
name: "model review"
on:
pull_request:
types: [opened, reopened, synchronize]
paths:
- 'openpilot/selfdrive/modeld/models/*.onnx'
workflow_dispatch:
jobs:
comment:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
if: github.repository == 'commaai/openpilot'
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: true
- name: Checkout master
uses: actions/checkout@v7
with:
ref: master
path: base
- run: git lfs pull
- run: cd base && git lfs pull
- name: scripts/reporter.py
id: report
run: |
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "## Model Review" >> $GITHUB_OUTPUT
PYTHONPATH=${{ github.workspace }} MASTER_PATH=${{ github.workspace }}/base python scripts/reporter.py >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Post model report comment
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0
with:
header: model-review
message: ${{ steps.report.outputs.content }}
+5
View File
@@ -4,6 +4,11 @@ on:
- cron: '0 9 * * *'
workflow_dispatch:
env:
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: lfs.fetchexclude
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
jobs:
build___nightly:
name: build __nightly
+4 -1
View File
@@ -9,6 +9,9 @@ on:
env:
PYTHONPATH: ${{ github.workspace }}
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: lfs.fetchexclude
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
jobs:
package_updates:
@@ -81,7 +84,7 @@ jobs:
labels: bot
cleanup_closed_branches:
if: github.repository == 'commaai/openpilot'
if: github.repository == 'commaai/openpilot' && github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: write
+14 -24
View File
@@ -80,6 +80,7 @@ jobs:
with:
repository: commaai/openpilot
ref: ${{ inputs.upstream_branch }}
fetch-depth: 1
submodules: recursive
path: openpilot
@@ -89,18 +90,25 @@ jobs:
with:
repository: sunnypilot/sunnypilot
ref: ${{ inputs.upstream_branch }}
fetch-depth: 1
submodules: recursive
path: openpilot
- name: Get commit date
id: commit-date
run: |
cd ${{ github.workspace }}/openpilot
cd ${{ github.workspace }}/openpilot/openpilot
commit_date=$(git log -1 --format=%cd --date=format:'%B %d, %Y')
echo "model_date=${commit_date}" >> $GITHUB_OUTPUT
cat $GITHUB_OUTPUT
- run: |
cd ${{ github.workspace }}/openpilot
git lfs pull
cd ${{ github.workspace }}/openpilot/openpilot
if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; then
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'
uses: actions/upload-artifact@v4
with:
@@ -116,24 +124,10 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
submodules: recursive
- 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
id: set-env
@@ -144,7 +138,7 @@ jobs:
export UV_PYTHON_PREFERENCE=managed
export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
uv sync
uv sync --frozen
printenv >> $GITHUB_ENV
if [[ "${{ runner.debug }}" == "1" ]]; then
cat $GITHUB_OUTPUT
@@ -173,8 +167,6 @@ jobs:
with:
name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
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
run: |
@@ -191,7 +183,7 @@ jobs:
if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then
echo "USBGPU build"
export USBGPU=1
TG_FLAGS="DEV=AMD USBGPU=1 IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
TG_FLAGS="DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2"
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
else
echo "QCOM build"
@@ -254,10 +246,8 @@ jobs:
# Copy the model files
rsync -avm \
--include='*.dlc' \
--include='*.pkl' \
--include='*.chunk*' \
--include='*.chunkmanifest' \
--include='*.onnx' \
--exclude='*' \
--delete-excluded \
--chown=comma:comma \
+5 -12
View File
@@ -20,7 +20,9 @@ concurrency:
env:
CI: 1
PYTHONPATH: ${{ github.workspace }}
PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: lfs.fetchexclude
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
jobs:
build_release:
@@ -125,10 +127,7 @@ jobs:
timeout-minutes: ${{ contains(runner.name, 'nsc') && 2 || 999 }}
env:
RAYLIB_BACKEND: headless
run: |
# Pre-compile Python bytecode so each pytest worker doesn't need to
$PYTEST --collect-only -m 'not slow' -qq
MAX_EXAMPLES=1 $PYTEST -m 'not slow'
run: tools/op.sh test
process_replay:
name: process replay
@@ -193,12 +192,6 @@ jobs:
timeout_minutes: 2
max_attempts: 3
command: cd ${{ github.workspace }}/ci-artifacts && git push origin process-replay --force
- name: Run regen
if: false
timeout-minutes: 4
env:
ONNXCPU: 1
run: $PYTEST openpilot/selfdrive/test/process_replay/test_regen.py
simulator_driving:
name: simulator driving
@@ -219,7 +212,7 @@ jobs:
env:
# MetaDrive renders offscreen through panda3d's EGL pipe on llvmpipe
EGL_PLATFORM: surfaceless
run: pytest -s openpilot/tools/sim/tests/test_metadrive_bridge.py
run: python openpilot/tools/sim/tests/test_metadrive_bridge.py
create_ui_report:
name: Create UI Report
+4 -2
View File
@@ -15,7 +15,7 @@ a.out
.cache/
bin/
# created at launch for TICI PYTHONPATH (PC uses editable installs via pyproject.toml)
# created at launch for comma hardware PYTHONPATH (PC uses editable installs via pyproject.toml)
/msgq
/opendbc
/rednose
@@ -39,6 +39,8 @@ bin/
*.os-*
*.so
*.a
st[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]
*.unchunked
*.clb
*.class
*.pyxbldc
@@ -54,7 +56,7 @@ compare_runtime*.html
openpilot/selfdrive/modeld/models/tg_input_devices.json
# build artifacts
docs_site/
docs/_site/
openpilot/selfdrive/pandad/pandad
openpilot/cereal/services.h
openpilot/cereal/gen
Vendored
+14 -14
View File
@@ -30,14 +30,14 @@ export GIT_COMMIT=${env.GIT_COMMIT}
export CI_ARTIFACTS_TOKEN=${env.CI_ARTIFACTS_TOKEN}
export GITHUB_COMMENTS_TOKEN=${env.GITHUB_COMMENTS_TOKEN}
export AZURE_TOKEN='${env.AZURE_TOKEN}'
# only use 1 thread for tici tests since most require HIL
# only use 1 thread since most require real hardware that can't be shared
export PYTEST_ADDOPTS="-n0 -s"
export GIT_SSH_COMMAND="ssh -i /data/gitkey"
source ~/.bash_profile
if [ -f /TICI ]; then
if [ -f /AGNOS ]; then
source /etc/profile
rm -rf /tmp/tmp*
@@ -206,35 +206,35 @@ node {
deviceStage("onroad", "tizi-needs-can", ["UNSAFE=1"], [
step("build openpilot", "cd openpilot/system/manager && ./build.py"),
step("check dirty", "tools/release/check-dirty.sh"),
step("onroad tests", "pytest openpilot/selfdrive/test/test_onroad.py -s", [timeout: 60]),
step("onroad tests", "./openpilot/selfdrive/test/test_onroad.py", [timeout: 60]),
])
},
'HW + Unit Tests': {
deviceStage("tizi-hardware", "tizi-common", ["UNSAFE=1"], [
step("build", "cd openpilot/system/manager && ./build.py"),
step("test power draw", "pytest -s openpilot/selfdrive/test//test_power_draw.py"),
step("test encoder", "pytest openpilot/system/loggerd/tests/test_encoder.py", [diffPaths: ["openpilot/system/loggerd/"]]),
step("test manager", "pytest openpilot/system/manager/test/test_manager.py"),
step("test power draw", "./openpilot/selfdrive/test/test_power_draw.py"),
step("test encoder", "./openpilot/system/loggerd/tests/test_encoder.py", [diffPaths: ["openpilot/system/loggerd/"]]),
step("test manager", "./openpilot/system/manager/test/test_manager.py"),
])
},
'camerad OX03C10': {
deviceStage("OX03C10", "tizi-ox03c10", ["UNSAFE=1"], [
step("build", "cd openpilot/system/manager && ./build.py"),
step("test pandad", "pytest openpilot/selfdrive/pandad/tests/test_pandad.py"),
step("test camerad", "pytest openpilot/system/camerad/test/test_camerad.py", [timeout: 90]),
step("test pandad", "./openpilot/selfdrive/pandad/tests/test_pandad.py"),
step("test camerad", "./openpilot/system/camerad/test/test_camerad.py", [timeout: 90]),
])
},
'camerad OS04C10': {
deviceStage("OS04C10", "tici-os04c10", ["UNSAFE=1"], [
step("build", "cd openpilot/system/manager && ./build.py"),
step("test pandad", "pytest openpilot/selfdrive/pandad/tests/test_pandad.py"),
step("test camerad", "pytest openpilot/system/camerad/test/test_camerad.py", [timeout: 90]),
step("test pandad", "./openpilot/selfdrive/pandad/tests/test_pandad.py"),
step("test camerad", "./openpilot/system/camerad/test/test_camerad.py", [timeout: 90]),
])
},
'sensord': {
deviceStage("LSM + MMC", "tizi-lsmc", ["UNSAFE=1"], [
step("build", "cd openpilot/system/manager && ./build.py"),
step("test sensord", "pytest openpilot/system/sensord/tests/test_sensord.py"),
step("test sensord", "./openpilot/system/sensord/tests/test_sensord.py"),
])
},
'replay': {
@@ -246,9 +246,9 @@ node {
'tizi': {
deviceStage("tizi", "tizi", ["UNSAFE=1"], [
step("build openpilot", "cd openpilot/system/manager && ./build.py"),
step("test pandad loopback", "pytest openpilot/selfdrive/pandad/tests/test_pandad_loopback.py"),
step("test pandad spi", "pytest openpilot/selfdrive/pandad/tests/test_pandad_spi.py"),
step("test amp", "pytest openpilot/common/hardware/tici/tests/test_amplifier.py"),
step("test pandad loopback", "./openpilot/selfdrive/pandad/tests/test_pandad_loopback.py"),
step("test pandad spi", "./openpilot/selfdrive/pandad/tests/test_pandad_spi.py"),
step("test amp", "./openpilot/common/hardware/comma/tests/test_amplifier.py"),
])
},
+11 -3
View File
@@ -1,6 +1,14 @@
Version 0.11.2 (2026-06-15)
========================
Version 0.11.2 (2026-08-12)
=======================
* New driving model
* Big model with 880M parameters
* Support for big models running on an external GPU
* Live stream cameras from comma connect
* Generate dashcam clips from comma connect
* Remote comma body control from comma connect
* New alert sounds
* CUPRA Born 2021-2023 support thanks to DaHansi!
* Volkswagen ID.4 2021-2025 support thanks to DaHansi!
Version 0.11.1 (2026-05-18)
========================
+19 -18
View File
@@ -10,7 +10,7 @@ import numpy as np
import SCons.Errors
from SCons.Defaults import _stripixes
TICI = os.path.isfile('/TICI')
COMMA_HARDWARE = os.path.isfile('/AGNOS')
SCons.Warnings.warningAsException(True)
@@ -24,7 +24,7 @@ release = not os.path.exists(File('#.gitattributes').abspath) # file absent on r
AddOption('--minimal',
action='store_false',
dest='extras',
default=(not TICI and not release),
default=(not COMMA_HARDWARE and not release),
help='the minimum build to run openpilot. no tests, tools, etc.')
submodule_python_paths = [
@@ -46,22 +46,22 @@ if external_pythonpath := os.environ.get("PYTHONPATH"):
arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()
if platform.system() == "Darwin":
arch = "Darwin"
elif arch == "aarch64" and TICI:
arch = "larch64"
elif arch == "aarch64" and COMMA_HARDWARE:
arch = "comma_arm64"
assert arch in [
"larch64", # linux tici arm64
"aarch64", # linux pc arm64
"x86_64", # linux pc x64
"Darwin", # macOS arm64 (x86 not supported)
"comma_arm64", # linux comma hardware (AGNOS) arm64
"aarch64", # linux pc arm64
"x86_64", # linux pc x64
"Darwin", # macOS arm64 (x86 not supported)
]
pkg_names = ['acados', 'bzip2', 'capnproto', 'catch2', 'eigen', 'ffmpeg', 'json11', 'ncurses', 'zeromq', 'zstd']
pkg_names = ['acados', 'capnproto', 'eigen', 'ffmpeg', 'json11', 'ncurses', 'zeromq', 'zstd']
pkgs = [importlib.import_module(name) for name in pkg_names]
acados = pkgs[pkg_names.index('acados')]
ffmpeg = pkgs[pkg_names.index('ffmpeg')]
# Shared package ships .so/.dylib; older device venvs still have static .a only.
# Keep static link deps (x264/z/va/drm) when the installed package is static so
# TICI CI works without upgrading the device venv yet.
# COMMA_HARDWARE CI works without upgrading the device venv yet.
# TODO: drop the static fallback once device venvs have comma-deps-ffmpeg>=7.1.0.post94
_ffmpeg_lib_names = os.listdir(ffmpeg.LIB_DIR) if os.path.isdir(ffmpeg.LIB_DIR) else []
ffmpeg_shared = any(
@@ -129,10 +129,11 @@ env = Environment(
CCFLAGS=[
"-g",
"-fPIC",
"-pipe",
"-O2",
"-Wunused",
"-Werror",
"-Wshadow" if arch in ("Darwin", "larch64") else "-Wshadow=local",
"-Wshadow" if arch in ("Darwin", "comma_arm64") else "-Wshadow=local",
"-Wno-unknown-warning-option",
"-Wno-inconsistent-missing-override",
"-Wno-c99-designator",
@@ -164,24 +165,24 @@ env = Environment(
COMPILATIONDB_USE_ABSPATH=True,
REDNOSE_ROOT="#rednose_repo",
tools=["default", "cython", "compilation_db", "rednose_filter"],
toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
toolpath=["#msgq_repo/site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
)
# SCons' Darwin linker tool doesn't define the variables used to expand RPATH.
if arch == "Darwin":
env["RPATHPREFIX"] = "-Wl,-rpath,"
env["RPATHSUFFIX"] = ""
env["_RPATH"] = "${_concat(RPATHPREFIX, RPATH, RPATHSUFFIX, __env__)}"
if arch != "larch64":
if arch != "comma_arm64":
env['_LIBFLAGS'] = _libflags
# Arch-specific flags and paths
if arch == "larch64":
if arch == "comma_arm64":
env["CC"] = "clang"
env["CXX"] = "clang++"
env.Append(LIBPATH=[
"/usr/lib/aarch64-linux-gnu",
])
arch_flags = ["-D__TICI__", "-mcpu=cortex-a57", "-DQCOM2"]
arch_flags = ["-D__COMMA_HARDWARE__", "-mcpu=cortex-a57"]
env.Append(CCFLAGS=arch_flags)
env.Append(CXXFLAGS=arch_flags)
elif arch == "Darwin":
@@ -233,7 +234,7 @@ Export('envCython', 'np_version')
Export('env', 'arch', 'acados', 'release', 'ffmpeg_libs')
# Setup cache dir
default_cache_dir = '/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache'
default_cache_dir = '/data/scons_cache' if arch == "comma_arm64" else '/tmp/scons_cache'
cache_dir = ARGUMENTS.get('cache_dir', default_cache_dir)
cache_size_limit = 4e9 if "CI" in os.environ else 2e9
CacheDir(cache_dir)
@@ -280,7 +281,7 @@ SConscript([
'openpilot/system/loggerd/SConscript',
])
if arch == "larch64":
if arch == "comma_arm64":
SConscript(['openpilot/system/camerad/SConscript'])
# Build selfdrive
@@ -295,7 +296,7 @@ SConscript([
SConscript(['openpilot/sunnypilot/SConscript'])
# Build desktop-only tools
if GetOption('extras') and arch != "larch64":
if GetOption('extras') and arch != "comma_arm64":
SConscript([
'openpilot/tools/replay/SConscript',
'openpilot/tools/cabana/SConscript',
-99
View File
@@ -1,99 +0,0 @@
import contextlib
import gc
import os
import pytest
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.system.manager import manager
from openpilot.common.hardware import TICI, HARDWARE
# these are heavy CI-only tests, invoked explicitly in .github/workflows/tests.yaml
collect_ignore = [
"openpilot/selfdrive/test/process_replay/test_processes.py",
"openpilot/selfdrive/test/process_replay/test_regen.py",
"openpilot/tools/sim/",
# tinygrad JIT has process-global state. Other test files import modeld → tinygrad,
# which corrupts JIT captures for test_warp.py in the same process. Run separately in CI.
"openpilot/sunnypilot/modeld_v2/tests/test_warp.py",
]
def pytest_sessionstart(session):
# TODO: fix tests and enable test order randomization
if session.config.pluginmanager.hasplugin('randomly'):
session.config.option.randomly_reorganize = False
@pytest.hookimpl(hookwrapper=True, trylast=True)
def pytest_runtest_call(item):
# ensure we run as a hook after capturemanager's
if item.get_closest_marker("nocapture") is not None:
capmanager = item.config.pluginmanager.getplugin("capturemanager")
with capmanager.global_and_fixture_disabled():
yield
else:
yield
@contextlib.contextmanager
def clean_env():
starting_env = dict(os.environ)
yield
os.environ.clear()
os.environ.update(starting_env)
@pytest.fixture(scope="function", autouse=True)
def openpilot_function_fixture(request):
with clean_env():
# setup a clean environment for each test
with OpenpilotPrefix(shared_download_cache=request.node.get_closest_marker("shared_download_cache") is not None) as prefix:
prefix = os.environ["OPENPILOT_PREFIX"]
yield
# ensure the test doesn't change the prefix
assert "OPENPILOT_PREFIX" in os.environ and prefix == os.environ["OPENPILOT_PREFIX"]
# cleanup any started processes
manager.manager_cleanup()
# some processes disable gc for performance, re-enable here
if not gc.isenabled():
gc.enable()
gc.collect()
# If you use setUpClass, the environment variables won't be cleared properly,
# so we need to hook both the function and class pytest fixtures
@pytest.fixture(scope="class", autouse=True)
def openpilot_class_fixture():
with clean_env():
yield
@pytest.fixture(scope="function")
def tici_setup_fixture(request, openpilot_function_fixture):
"""Ensure a consistent state for tests on-device. Needs the openpilot function fixture to run first."""
if 'skip_tici_setup' in request.keywords:
return
HARDWARE.initialize_hardware()
HARDWARE.set_power_save(False)
os.system("pkill -9 -f athena")
@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(config, items):
skipper = pytest.mark.skip(reason="Skipping tici test on PC")
for item in items:
if "tici" in item.keywords:
if not TICI:
item.add_marker(skipper)
else:
item.fixturenames.append('tici_setup_fixture')
if "xdist_group_class_property" in item.keywords:
class_property_name = item.get_closest_marker('xdist_group_class_property').args[0]
class_property_value = getattr(item.cls, class_property_name)
item.add_marker(pytest.mark.xdist_group(class_property_value))
+1 -1
View File
@@ -61,7 +61,7 @@ A good pull request has all of the following:
* Report bugs in GitHub issues.
* Report driving issues in the `#driving-feedback` Discord channel.
* Consider opting into driver camera uploads to improve the driver monitoring model.
* Consider opting into cabin camera uploads to improve the driver monitoring model.
* Connect your device to Wi-Fi regularly, so that we can pull data for training better driving models.
* Run the `nightly` branch and report issues. This branch is like `master` but it's built just like a release.
* Annotate images in the [comma10k dataset](https://github.com/commaai/comma10k).
-24
View File
@@ -1,24 +0,0 @@
# Docs development
The `docs/` tree is the source for [docs.comma.ai](https://docs.comma.ai).
The site is updated on pushes to master by this [workflow](../.github/workflows/docs.yaml).
Those commands must be run in the root directory of openpilot, **not /docs**
**1. Install the docs dependencies**
``` bash
uv pip install .[docs]
```
**2. Build the new site**
``` bash
docs build
```
**3. Run the new site locally**
``` bash
docs serve
```
References:
* https://zensical.org/docs/
+2 -2
View File
@@ -52,7 +52,7 @@ Many factors can impact the performance of openpilot DM, causing it to be unable
* Low light conditions, such as driving at night or in dark tunnels.
* Bright light (due to oncoming headlights, direct sunlight, etc.).
* The driver's face is partially or completely outside field of view of the driver facing camera.
* The driver facing camera is obstructed, covered, or damaged.
* The driver's face is partially or completely outside field of view of the cabin camera.
* The cabin camera is obstructed, covered, or damaged.
The list above does not represent an exhaustive list of situations that may interfere with proper operation of openpilot components. A driver should not rely on openpilot DM to assess their level of attention.
+14
View File
@@ -0,0 +1,14 @@
# Docs development
The `docs/` tree is the source for [docs.comma.ai](https://docs.comma.ai).
The site is updated on pushes to master by this [workflow](../.github/workflows/docs.yaml).
**1. Build the site**
``` bash
python docs/serve.py --build
```
**2. Run the site locally** (rebuilds on change)
``` bash
python docs/serve.py
```
+1 -1
View File
@@ -1 +1 @@
../../selfdrive/assets/icons_mici/settings/comma_icon.png
../../openpilot/selfdrive/assets/icons_mici/settings/comma_icon.png
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9e019ed5af500e8820d05934d47e0380728e1b30e01a179f786dc4edb1eccbd7
size 349
-3
View File
@@ -1,3 +0,0 @@
# openpilot glossary
{{GLOSSARY_DEFINITIONS}}
+3 -3
View File
@@ -10,13 +10,13 @@ For each segment, openpilot records the following log types:
rlogs contain all the messages passed amongst openpilot's processes. See [openpilot/cereal/services.py](https://github.com/commaai/openpilot/blob/master/openpilot/cereal/services.py) for a list of all the logged services. They're a zstd archive of the serialized [Capn Proto](https://capnproto.org/) messages.
## {f,e,d}camera.hevc
## camera video files
Each camera stream is H.265 encoded and written to its respective file.
* `fcamera.hevc` is the road camera
* `fcamera.hevc` is the narrow road camera (the main forward camera)
* `ecamera.hevc` is the wide road camera
* `dcamera.hevc` is the driver camera
* `dcamera.hevc` is the cabin camera
## qlog.zst & qcamera.ts
+3 -3
View File
@@ -21,14 +21,14 @@ In general, driver monitoring feedback is very actionable, and we can fix your c
To post your feedback:
1. Join the [community Discord](https://discord.comma.ai).
2. If driver camera recording is toggled off, temporarily enable driver camera recording in the settings until you reproduce the issue.
3. Using comma connect, identify the relevant segment and upload the segment's logs and driver camera.
2. If cabin camera recording is toggled off, temporarily enable cabin camera recording in the settings until you reproduce the issue.
3. Using comma connect, identify the relevant segment and upload the segment's logs and cabin camera.
4. Post the segment in the `#openpilot-experience` channel on Discord with a good description.
Before posting feedback, please ensure:
- **openpilot is up to date** you should be on the latest openpilot release or nightly
- **the driver camera has a clear view of the driver** ensure nothing blocks view of the driver (e.g. a cable), the lens is clean, etc.
- **the cabin camera has a clear view of the driver** ensure nothing blocks view of the driver (e.g. a cable), the lens is clean, etc.
- **your device is mounted properly** your device must be mounted horizontally center and relatively high on the windshield
## Other bugs
-216
View File
@@ -1,216 +0,0 @@
import posixpath
import re
import tomllib
import xml.etree.ElementTree as ET
from pathlib import Path
from markdown.extensions import Extension
from markdown.preprocessors import Preprocessor
from markdown.treeprocessors import Treeprocessor
from zensical.extensions.links import LinksTreeprocessor
GlossaryTerm = tuple[str, re.Pattern[str], str]
GLOSSARY_FILE = Path(__file__).with_name("glossary.toml")
GLOSSARY_PAGE = "concepts/glossary.md"
GLOSSARY_PLACEHOLDER = "{{GLOSSARY_DEFINITIONS}}"
SKIP_TAGS = {
"a",
"code",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"kbd",
"pre",
"script",
"style",
}
def clean_tooltip(description: str) -> str:
text = re.sub(r"\[([^\]]+)]\([^)]+\)", r"\1", description)
text = re.sub(r"`([^`]+)`", r"\1", text)
text = re.sub(r"[*_~]", "", text)
return re.sub(r"\s+", " ", text).strip()
def load_glossary() -> tuple[list[GlossaryTerm], str]:
with GLOSSARY_FILE.open("rb") as f:
glossary_data = tomllib.load(f).get("glossary", {})
glossary: list[GlossaryTerm] = []
rendered = []
for key, value in glossary_data.items():
label = str(key).strip().replace("_", " ")
description = str(value).strip()
if not description:
continue
slug = label.replace(" ", "-").replace("_", "-").lower()
glossary.append((slug, re.compile(rf"(?<!\w){re.escape(label)}(?!\w)", re.IGNORECASE), clean_tooltip(description)))
rendered.append(f'* <span id="{slug}"></span>**{label}**: {description}')
return glossary, "\n".join(rendered)
class GlossaryPreprocessor(Preprocessor):
def __init__(self, md, glossary: str):
super().__init__(md)
self.glossary = glossary
def run(self, lines: list[str]) -> list[str]:
markdown = "\n".join(lines)
if GLOSSARY_PLACEHOLDER not in markdown:
return lines
return markdown.replace(GLOSSARY_PLACEHOLDER, self.glossary).splitlines()
class GlossaryTreeprocessor(Treeprocessor):
def __init__(self, md, glossary: list[GlossaryTerm]):
super().__init__(md)
self.glossary = glossary
self.seen: set[str] = set()
def run(self, root: ET.Element) -> None:
at = self.md.treeprocessors.get_index_for_name("zrelpath")
processor = self.md.treeprocessors[at]
if not isinstance(processor, LinksTreeprocessor):
raise TypeError("Links processor not registered")
if processor.path == GLOSSARY_PAGE:
return
self.seen.clear()
glossary_href = f"{posixpath.relpath(GLOSSARY_PAGE, posixpath.dirname(processor.path) or '.')}#"
self._walk(root, glossary_href)
def _walk(self, element: ET.Element, glossary_href: str) -> None:
if element.tag in SKIP_TAGS or element.attrib.get("data-glossary-skip") is not None:
return
self._replace(element, glossary_href)
idx = 0
while idx < len(element):
child = element[idx]
self._walk(child, glossary_href)
idx = self._replace(element, glossary_href, idx) + 1
def _replace(self, parent: ET.Element, glossary_href: str, index: int | None = None) -> int:
child = None if index is None else parent[index]
text = parent.text if child is None else child.tail
pieces = self._pieces(text or "", glossary_href)
if not pieces:
return -1 if index is None else index
if child is None:
parent.text = pieces[0] if isinstance(pieces[0], str) else ""
# Insert replacements for parent.text before the first existing child.
insert_at = -1
else:
assert index is not None
child.tail = pieces[0] if isinstance(pieces[0], str) else ""
insert_at = index
start = 1 if isinstance(pieces[0], str) else 0
previous = child
for piece in pieces[start:]:
if isinstance(piece, str):
previous.tail = (previous.tail or "") + piece
continue
insert_at += 1
parent.insert(insert_at, piece)
previous = piece
return insert_at
def _pieces(self, text: str, glossary_href: str) -> list[str | ET.Element]:
if not text.strip():
return []
pieces: list[str | ET.Element] = []
cursor = 0
while True:
best = None
for slug, pattern, tooltip in self.glossary:
if slug in self.seen:
continue
found = pattern.search(text, cursor)
if found is None:
continue
candidate = (slug, tooltip, found.start(), found.end())
if best is None:
best = candidate
continue
_, _, best_start, best_end = best
_, _, current_start, current_end = candidate
if current_start < best_start:
best = candidate
continue
if current_start == best_start and current_end - current_start > best_end - best_start:
best = candidate
if best is None:
break
slug, tooltip, start, end = best
if start > cursor:
pieces.append(text[cursor:start])
link = ET.Element(
"a",
{
"class": "glossary-term",
"data-glossary-term": "",
"href": f"{glossary_href}{slug}",
},
)
ET.SubElement(link, "span", {"class": "glossary-term__label"}).text = text[start:end]
ET.SubElement(
link,
"span",
{
"class": "glossary-term__tooltip",
"data-search-exclude": "",
},
).text = tooltip
pieces.append(link)
self.seen.add(slug)
cursor = end
if not pieces:
return []
if cursor < len(text):
pieces.append(text[cursor:])
return pieces
class GlossaryExtension(Extension):
def extendMarkdown(self, md) -> None:
md.registerExtension(self)
glossary, rendered = load_glossary()
md.preprocessors.register(
GlossaryPreprocessor(md, rendered),
"docs-ext-glossary-preprocessor",
27,
)
md.treeprocessors.register(
GlossaryTreeprocessor(md, glossary),
"docs-ext-glossary-treeprocessor",
0,
)
def makeExtension(**kwargs) -> GlossaryExtension:
return GlossaryExtension(**kwargs)
-8
View File
@@ -1,8 +0,0 @@
[glossary]
onroad = "openpilot's system state while ignition is on."
offroad = "openpilot's system state while ignition is off."
route = "A route is a recording of an onroad session."
segment = "Routes are split into one minute chunks called segments."
"comma connect" = "The web viewer for all your routes; check it out at [connect.comma.ai](https://connect.comma.ai)."
panda = "The secondary processor on the device that implements the functional safety and directly talks to the car over CAN. See the [panda repo](https://github.com/commaai/panda)."
"comma four" = "The latest hardware by comma.ai for running openpilot. More info at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four)."
+555
View File
@@ -0,0 +1,555 @@
import argparse
import functools
import html
import http.server
import json
import posixpath
import re
import shutil
import threading
import time
import urllib.parse
from pathlib import Path
DOCS_DIR = Path(__file__).resolve().parent
SITE_DIR = DOCS_DIR / "_site"
TEMPLATE_FILE = DOCS_DIR / "template.html"
EXCLUDE_DIRS = {"_site", "__pycache__"}
REPO_URL = "https://github.com/commaai/openpilot/"
# (title, target) pairs. target is a page path or an absolute URL.
# A None target marks a section header.
NAV: list[tuple[str, str | None]] = [
("What is openpilot?", "index.md"),
("How-to", None),
("Turn the speed blue", "how-to/turn-the-speed-blue.md"),
("Connect to a comma 3X or four", "how-to/connect-to-comma.md"),
("Add support for a car", "how-to/car-port.md"),
("Concepts", None),
("Logs", "concepts/logs.md"),
("Safety", "concepts/safety.md"),
("Glossary", "concepts/glossary.md"),
("Contributing", None),
("Feedback", "contributing/feedback.md"),
("Roadmap", "contributing/roadmap.md"),
("Contributing Guide →", "https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md"),
("Links", None),
("Blog →", "https://blog.comma.ai"),
("Bounties →", "https://comma.ai/bounties"),
("GitHub →", "https://github.com/commaai"),
("Discord →", "https://discord.comma.ai"),
("X →", "https://x.com/comma_ai"),
]
GLOSSARY_DESCRIPTIONS = {
"onroad": "openpilot's system state while ignition is on.",
"offroad": "openpilot's system state while ignition is off.",
"route": "A route is a recording of an onroad session.",
"segment": "Routes are split into one minute chunks called segments.",
"comma connect": "The web viewer for all your routes; check it out at [connect.comma.ai](https://connect.comma.ai).",
"panda": "The secondary processor on the device that implements the functional safety and directly talks to the car over CAN. See the [panda repo](https://github.com/commaai/panda).",
"comma four": "The latest hardware by comma.ai for running openpilot. More info at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four).",
}
GLOSSARY_PAGE = "concepts/glossary.md"
GLOSSARY_ROUTE = GLOSSARY_PAGE.removesuffix(".md")
GLOSSARY_SKIP = frozenset("a code h1 h2 h3 h4 h5 h6 kbd pre script style".split())
_ENTITY = re.compile(r"&(?:#x?[0-9a-fA-F]+|[a-zA-Z]+);")
_LIST = re.compile(r"^(\s*)([*+-]|\d+\.)\s+(.*)$")
_HEADING = re.compile(r"^(#{1,6})\s+(.*)$")
_HR = re.compile(r"^(-{3,}|\*{3,}|_{3,})$")
_ATTR_URL = re.compile(r"""(?P<pre>\b(?:href|src)=(?P<q>["']))(?P<url>.*?)(?P=q)""")
_VOID = frozenset("br img hr meta link input".split())
_URL = re.compile(r"https?://[^\s<>\[\]\"']+")
_ADMONITION = re.compile(r"^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]$", re.I)
def page_route(path: str) -> str:
path = path.removesuffix(".md")
return posixpath.dirname(path) or "." if posixpath.basename(path) == "index" else path
def page_href(current: str, target: str) -> str:
route = posixpath.relpath(page_route(target), page_route(current))
return ("." if route == "." else route) + "/"
def rewrite_relative_url(value: str, page: str) -> str | None:
url = urllib.parse.urlparse(value)
if value.startswith(("#", "/")) or url.scheme or url.netloc or not url.path:
return None
target = posixpath.normpath(posixpath.join(posixpath.dirname(page), url.path))
if target == ".." or target.startswith("../"):
return None
path = page_href(page, target) if target.endswith(".md") else posixpath.relpath(target, page_route(page))
return url._replace(path=path).geturl()
def rewrite_html_urls(fragment: str, page: str) -> str:
def repl(m: re.Match[str]) -> str:
r = rewrite_relative_url(m.group("url"), page)
return m.group(0) if r is None else f'{m.group("pre")}{r}{m.group("q")}'
return _ATTR_URL.sub(repl, fragment)
def esc(text: str, attr: bool = False) -> str:
held: list[str] = []
def hold(m: re.Match[str]) -> str:
held.append(m.group(0))
return f"\0{len(held) - 1}\0"
return re.sub(r"\0(\d+)\0", lambda m: held[int(m.group(1))], html.escape(_ENTITY.sub(hold, text), quote=attr))
def clean_tooltip(description: str) -> str:
text = re.sub(r"\[([^\]]+)]\([^)]+\)", r"\1", description)
return re.sub(r"\s+", " ", re.sub(r"[*_~]", "", re.sub(r"`([^`]+)`", r"\1", text))).strip()
def glossary_slug(label: str) -> str:
return label.replace(" ", "-").replace("_", "-").lower()
GLOSSARY_TERMS = [(glossary_slug(l), re.compile(rf"(?<!\w){re.escape(l)}(?!\w)", re.I), clean_tooltip(d)) for l, d in GLOSSARY_DESCRIPTIONS.items()]
GLOSSARY_DEFINITIONS = "\n".join(f'* <span id="{glossary_slug(l)}"></span>**{l}**: {d}' for l, d in GLOSSARY_DESCRIPTIONS.items())
def inject_glossary(body: str, page: str) -> str:
if page == GLOSSARY_PAGE:
return body
route = "." if page == "index.md" else page.removesuffix(".md")
base, seen, out, skip, depth = f"{posixpath.relpath(GLOSSARY_ROUTE, route)}/#", set(), [], None, 0
for part in re.split(r"(<[^>]+>)", body):
if not part:
continue
if part.startswith("<"):
out.append(part)
if part.startswith("<!") or not (m := re.match(r"</?\s*([a-zA-Z0-9]+)", part)):
continue
tag = m.group(1).lower()
if tag not in GLOSSARY_SKIP:
continue
closing, void = part.startswith("</"), part.endswith("/>") or tag in _VOID
if closing and skip == tag and depth:
depth -= 1
skip = None if not depth else skip
elif not closing and not void:
skip, depth = (tag, 1) if skip is None else (skip, depth + (skip == tag))
continue
if depth:
out.append(part)
continue
cur, text = 0, part
while True:
best = None
for order, (slug, pat, tip) in enumerate(GLOSSARY_TERMS):
if slug in seen or (found := pat.search(text, cur)) is None:
continue
cand = (found.start(), found.start() - found.end(), order, slug, tip, found.end(), found.group(0))
if best is None or cand[:3] < best[:3]:
best = cand
if best is None:
out.append(text[cur:])
break
start, _, _, slug, tip, end, matched = best
out.append(text[cur:start])
out.append(
f'<a class="glossary-term" data-glossary-term="" href="{base}{slug}">'
+ f'<span class="glossary-term__label">{matched}</span>'
+ f'<span class="glossary-term__tooltip" data-search-exclude="">{esc(tip)}</span></a>'
)
seen.add(slug)
cur = end
return "".join(out)
def slugify(text: str) -> str:
text = html.unescape(re.sub(r"<[^>]+>", "", text)).lower()
return re.sub(r"[-\s]+", "-", re.sub(r"[^\w\s-]", "", text, flags=re.UNICODE)).strip("-")
def _parse_link(text: str, start: int) -> tuple[str, str, int] | None:
if start >= len(text) or text[start] != "[":
return None
depth, i = 0, start
while i < len(text):
depth += (text[i] == "[") - (text[i] == "]")
if text[i] == "]" and depth == 0:
label = text[start + 1 : i]
if i + 1 >= len(text) or text[i + 1] != "(":
return None
j, dp = i + 2, 1
while j < len(text) and dp:
dp += (text[j] == "(") - (text[j] == ")")
j += 1
return None if dp else (label, text[i + 2 : j - 1], j)
i += 1
return None
def autolink_plain(text: str) -> str:
parts: list[str] = []
last = 0
for m in _URL.finditer(text):
start = m.start()
if start > 0 and text[start - 1].isalnum():
continue
parts.append(esc(text[last:start]))
url = m.group(0).rstrip(".,;:!?)]")
parts.append(f'<a href="{esc(url, True)}">{esc(url)}</a>')
last = start + len(url)
parts.append(esc(text[last:]))
return "".join(parts)
def render_inline(text: str, page: str) -> str:
out, i, n = [], 0, len(text)
while i < n:
if text[i] == "\n" and i >= 2 and text[i - 2 : i] == " " and out and out[-1].endswith(" "):
out[-1] = out[-1][:-2]
out.append("<br>\n")
i += 1
continue
if text[i] == "`" and (end := text.find("`", i + 1)) != -1:
out.append(f"<code>{esc(text[i + 1 : end])}</code>")
i = end + 1
continue
if text[i] == "!" and i + 1 < n and text[i + 1] == "[" and (p := _parse_link(text, i + 1)):
label, url, end = p
src = rewrite_relative_url(url, page) or url
out.append(f'<img alt="{esc(label, True)}" src="{esc(src, True)}">')
i = end
continue
if text[i] == "[" and (p := _parse_link(text, i)):
label, url, end = p
href = rewrite_relative_url(url, page) or url
out.append(f'<a href="{esc(href, True)}">{render_inline(label, page)}</a>')
i = end
continue
if text[i] == "<":
if text.startswith("<!--", i):
end = text.find("-->", i + 4)
end = n if end < 0 else end + 3
out.append(rewrite_html_urls(text[i:end], page))
i = end
continue
if m := re.match(r"<[^>]+>", text[i:]):
out.append(rewrite_html_urls(m.group(0), page))
i += len(m.group(0))
continue
if (text.startswith("**", i) or text.startswith("__", i)) and (end := text.find(text[i : i + 2], i + 2)) != -1:
out.append(f"<strong>{render_inline(text[i + 2 : end], page)}</strong>")
i = end + 2
continue
if text[i] in "*_" and i + 1 < n and text[i + 1] not in " \t\n" and (end := text.find(text[i], i + 1)) > i + 1:
out.append(f"<em>{render_inline(text[i + 1 : end], page)}</em>")
i = end + 1
continue
j = i + 1
while j < n and text[j] not in "`[<!*_":
j += 1
out.append(autolink_plain(text[i:j]))
i = j
return "".join(out)
def _trow(line: str) -> list[str]:
return [c.strip() for c in line.strip().removeprefix("|").removesuffix("|").split("|")]
def _is_sep(line: str) -> bool:
return "|" in line and all(re.fullmatch(r":?-{3,}:?", c.strip()) for c in _trow(line))
def _align(sep: str) -> str:
s = sep.strip()
if s.startswith(":") and s.endswith(":"):
return ' style="text-align: center;"'
if s.endswith(":"):
return ' style="text-align: right;"'
if s.startswith(":"):
return ' style="text-align: left;"'
return ""
def _list_info(line: str) -> tuple[int, str, str] | None:
m = _LIST.match(line)
return None if not m else (len(m.group(1)) // 4, "ol" if m.group(2)[-1] == "." else "ul", m.group(3))
def _render_blocks(text: str, page: str) -> str:
lines, out, i, n = text.splitlines(), [], 0, 0
n = len(lines)
while i < n:
line, s = lines[i], lines[i].strip()
if not s:
i += 1
continue
if s.startswith("```"):
lang, body = s[3:].strip(), []
i += 1
while i < n and not lines[i].strip().startswith("```"):
body.append(lines[i])
i += 1
if i < n:
i += 1
code = html.escape("\n".join(body) + ("\n" if body else ""))
cls = f' class="language-{html.escape(lang)}"' if lang else ""
out.append(f"<pre><code{cls}>{code}</code></pre>")
continue
if m := _HEADING.match(s):
content, level = m.group(2).rstrip("#").strip(), len(m.group(1))
sid = slugify(content)
out.append(f'<h{level} id="{sid}">{render_inline(content, page)}<a class="headerlink" href="#{sid}" title="Permanent link">#</a></h{level}>')
i += 1
continue
if _HR.fullmatch(s):
out.append("<hr>")
i += 1
continue
if "|" in line and i + 1 < n and _is_sep(lines[i + 1]):
headers, aligns = _trow(line), [_align(c) for c in _trow(lines[i + 1])]
i += 2
rows = []
while i < n and "|" in lines[i] and lines[i].strip():
rows.append(_trow(lines[i]))
i += 1
parts = (
["<table>", "<thead>", "<tr>"]
+ [f"<th{aligns[j] if j < len(aligns) else ''}>{render_inline(h, page)}</th>" for j, h in enumerate(headers)]
+ ["</tr>", "</thead>", "<tbody>"]
)
for row in rows:
parts.append("<tr>")
for j in range(len(headers)):
parts.append(f"<td{aligns[j] if j < len(aligns) else ''}>{render_inline(row[j] if j < len(row) else '', page)}</td>")
parts.append("</tr>")
out.append("\n".join(parts + ["</tbody>", "</table>"]))
continue
if _list_info(line):
items: list[tuple[int, str, list[str]]] = []
while i < n:
if not lines[i].strip():
if i + 1 < n and _list_info(lines[i + 1]):
i += 1
continue
break
info = _list_info(lines[i])
if not info:
break
level, kind, body = info
chunk = [body]
i += 1
while i < n and lines[i].strip() and _list_info(lines[i]) is None:
t = lines[i].strip()
if t.startswith(("```", ">")) or _HEADING.match(t) or _HR.fullmatch(t):
break
chunk.append(lines[i])
i += 1
items.append((level, kind, chunk))
def render_list(items: list[tuple[int, str, list[str]]], start: int, min_level: int) -> tuple[str, int]:
if start >= len(items) or items[start][0] < min_level:
return "", start
kind, chunks, idx = items[start][1], [f"<{items[start][1]}>"], start
while idx < len(items) and items[idx][0] >= min_level:
level, ikind, body_lines = items[idx]
if level > min_level:
nested, idx = render_list(items, idx, level)
chunks[-1] = (chunks[-1][:-5] + nested + "</li>") if chunks[-1].endswith("</li>") else chunks[-1] + nested
continue
if ikind != kind:
chunks += [f"</{kind}>", f"<{ikind}>"]
kind = ikind
idx += 1
body = render_inline("\n".join(body_lines), page)
nested = ""
if idx < len(items) and items[idx][0] > min_level:
nested, idx = render_list(items, idx, min_level + 1)
chunks.append(f"<li>{body}{nested}\n</li>" if nested else f"<li>{body}</li>")
chunks.append(f"</{kind}>")
return "\n".join(chunks), idx
out.append(render_list(items, 0, items[0][0])[0])
continue
if s.startswith(">"):
q = []
while i < n and lines[i].strip().startswith(">"):
q.append(re.sub(r"^>\s?", "", lines[i].strip()))
i += 1
m = _ADMONITION.match(q[0].strip()) if q else None
if m:
kind = m.group(1).lower()
title = m.group(1).capitalize()
body = _render_blocks("\n".join(q[1:]), page)
out.append(f'<div class="admonition {kind}">\n<p class="admonition-title">{title}</p>\n{body}\n</div>')
else:
out.append(f"<blockquote>\n<p>{render_inline(chr(10).join(q), page)}</p>\n</blockquote>")
continue
if s.startswith("<!--"):
out.append(rewrite_html_urls(line, page))
i += 1
# Preserve a blank line after HTML comments (python-markdown does).
if i < n and not lines[i].strip():
out.append("")
while i < n and not lines[i].strip():
i += 1
continue
buf = [line]
i += 1
while i < n and lines[i].strip():
t = lines[i].strip()
if t.startswith(("```", ">")) or _HEADING.match(t) or _HR.fullmatch(t):
break
if "|" in lines[i] and i + 1 < n and _is_sep(lines[i + 1]):
break
buf.append(lines[i])
i += 1
out.append(f"<p>{render_inline(chr(10).join(buf), page)}</p>")
return "\n".join(out)
def render_markdown(text: str, page: str) -> str:
return inject_glossary(_render_blocks(text, page), page)
def page_title(source: str) -> str:
for line in source.splitlines():
if line.startswith("# "):
return line[2:].strip()
return "openpilot docs"
def write_html_redirect(rel: Path) -> None:
if rel.name == "index.md":
return
target = f"{rel.stem}/"
out = SITE_DIR / rel.with_suffix(".html")
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(
"\n".join(
[
"<!doctype html>",
f'<meta http-equiv="refresh" content="0; url={html.escape(target)}">',
f'<link rel="canonical" href="{html.escape(target)}">',
f"<script>location.replace({json.dumps(target)} + location.search + location.hash)</script>",
]
)
)
def copy_assets() -> None:
for src in DOCS_DIR.rglob("*"):
if not src.is_file():
continue
rel = src.relative_to(DOCS_DIR)
if any(part in EXCLUDE_DIRS for part in rel.parts):
continue
if src.suffix == ".md" or src in (Path(__file__).resolve(), TEMPLATE_FILE):
continue
dest = SITE_DIR / rel
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dest)
def render_nav_html(current_page: str) -> str:
parts: list[str] = []
for title, target in NAV:
if target is None:
parts.append(f'<div class="nav-section">{html.escape(title)}</div>')
elif target.startswith(("http://", "https://")):
parts.append(f'<a href="{html.escape(target)}">{html.escape(title)}</a>')
else:
active = ' class="active"' if target == current_page else ""
parts.append(f'<a href="{html.escape(page_href(current_page, target))}"{active}>{html.escape(title)}</a>')
return "\n".join(parts)
def build() -> None:
template = TEMPLATE_FILE.read_text()
pages = [
(path.relative_to(DOCS_DIR), path.read_text())
for path in sorted(DOCS_DIR.rglob("*.md"))
if path != DOCS_DIR / "README.md" and not any(part in EXCLUDE_DIRS for part in path.relative_to(DOCS_DIR).parts)
]
pages.append((Path(GLOSSARY_PAGE), f"# openpilot glossary\n\n{GLOSSARY_DEFINITIONS}"))
pages.sort()
if SITE_DIR.exists():
shutil.rmtree(SITE_DIR)
SITE_DIR.mkdir(parents=True)
copy_assets()
for rel_path, source in pages:
rel = rel_path.as_posix()
body = render_markdown(source, rel)
title = page_title(source)
route = page_route(rel)
root = "../" * (0 if route == "." else len(route.split("/")))
edit_path = "serve.py" if rel == GLOSSARY_PAGE else rel
page_html = template
for name, value in {
"TITLE": html.escape(title),
"ROOT": root,
"HOME_HREF": page_href(rel, "index.md"),
"NAV": render_nav_html(rel),
"BODY": body,
"EDIT_URL": html.escape(f"{REPO_URL}blob/master/docs/{edit_path}"),
}.items():
page_html = page_html.replace(f"{{{{{name}}}}}", value)
out = SITE_DIR / ("" if route == "." else route) / "index.html"
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(page_html)
write_html_redirect(rel_path)
print(f"docs: built {len(pages)} pages into {SITE_DIR}")
def _watched_files() -> list[Path]:
return [p for p in DOCS_DIR.rglob("*") if p.is_file() and not any(part in EXCLUDE_DIRS for part in p.relative_to(DOCS_DIR).parts)]
def serve() -> None:
build()
mtimes = {p: p.stat().st_mtime for p in _watched_files()}
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(SITE_DIR))
httpd = http.server.ThreadingHTTPServer(("", 0), handler)
print(f"docs: serving on http://localhost:{httpd.server_port}/ (watching for changes)")
try:
threading.Thread(target=httpd.serve_forever, daemon=True).start()
while True:
time.sleep(0.5)
new_mtimes = {p: p.stat().st_mtime for p in _watched_files()}
if new_mtimes != mtimes:
mtimes = new_mtimes
print("docs: change detected, rebuilding...")
try:
build()
except Exception as e:
print(f"docs: build failed: {e}")
except KeyboardInterrupt:
pass
finally:
httpd.shutdown()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Build or serve the openpilot documentation site.")
parser.add_argument("--build", action="store_true", help="Build the site and exit.")
args = parser.parse_args()
if args.build:
build()
else:
serve()
-42
View File
@@ -1,42 +0,0 @@
.md-logo img {
filter: invert(1);
}
.glossary-term {
position: relative;
color: inherit;
text-decoration: none;
}
.glossary-term__label {
border-bottom: 1px dotted currentColor;
}
.glossary-term__tooltip {
position: absolute;
top: calc(100% + 0.4rem);
left: 50%;
width: max-content;
max-width: min(30rem, 80vw);
padding: 0.65rem 0.8rem;
border-radius: 0.6rem;
background: rgb(26 26 26 / 96%);
color: white;
box-shadow: 0 0.6rem 1.8rem rgb(0 0 0 / 22%);
font-size: 0.85rem;
line-height: 1.45;
opacity: 0;
pointer-events: none;
transform: translateX(-50%) translateY(-0.15rem);
transition: opacity 120ms ease, transform 120ms ease;
visibility: hidden;
z-index: 20;
}
.glossary-term:hover .glossary-term__tooltip,
.glossary-term:focus-visible .glossary-term__tooltip,
.glossary-term:focus-within .glossary-term__tooltip {
opacity: 1;
transform: translateX(-50%) translateY(0);
visibility: visible;
}
+224
View File
@@ -0,0 +1,224 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{TITLE}} · openpilot docs</title>
<link rel="icon" href="{{ROOT}}assets/favicon.svg">
<style>
:root {
--bg: #fff;
--bg-elev: #f5f5f5;
--bg-hover: #eef0ff;
--fg: #262626;
--fg-dim: #666;
--accent: #4051b5;
--border: #e5e5e5;
--max-width: 76rem;
}
* { box-sizing: border-box; }
html { scrollbar-gutter: stable; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--fg);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
font-size: 15px;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
header.site {
display: flex; align-items: center; gap: 0.75rem;
min-height: 3rem;
padding: 0.5rem max(1.25rem, calc((100% - var(--max-width)) / 2));
background: var(--bg);
border-bottom: 1px solid var(--border);
position: sticky; top: 0; z-index: 10;
}
header.site img { display: block; height: 24px; filter: invert(1); }
header.site .brand { display: flex; align-items: center; gap: 0.75rem; color: var(--fg); }
header.site .brand:hover { text-decoration: none; }
header.site .site-name { font-weight: 600; font-size: 1.05rem; }
header.site .spacer { flex: 1; }
header.site .social { display: flex; gap: 1rem; }
header.site .social a { display: flex; color: var(--fg); }
header.site .social a:hover { color: var(--accent); }
header.site .social svg { width: 1.75rem; height: 1.75rem; fill: currentColor; }
header.site .social a[aria-label="Discord"] svg { transform: scale(1.15); }
.layout { display: flex; width: 100%; max-width: var(--max-width); margin: 0 auto; }
nav.sidebar {
width: 14rem; flex-shrink: 0;
padding: 3.3rem 1.25rem 2rem 0;
position: sticky; top: 3rem; align-self: start;
max-height: calc(100vh - 3rem); overflow-y: auto;
}
nav.sidebar .nav-section { font-weight: 600; margin: 1.15rem 0.8rem 0.45rem; }
nav.sidebar a {
display: block; padding: 0.28rem 0.8rem; border-radius: 0.45rem;
color: var(--fg); line-height: 1.35;
}
nav.sidebar a:hover { background: var(--bg-hover); text-decoration: none; }
nav.sidebar a.active { background: var(--bg-hover); color: var(--accent); font-weight: 600; }
main.content { width: min(100%, 46.5rem); min-width: 0; padding: 2.7rem 2rem 5rem; }
main.content h1, main.content h2, main.content h3 { line-height: 1.25; margin-top: 1.8rem; }
main.content h1 { margin-top: 0; }
main.content img { max-width: 100%; }
main.content table { border-collapse: collapse; display: block; overflow-x: auto; }
main.content th, main.content td { border: 1px solid var(--border); padding: 0.4rem 0.6rem; text-align: left; }
main.content th { background: var(--bg-elev); }
main.content pre {
position: relative;
background: var(--bg-elev);
padding: 0.9rem 1rem;
border-radius: 0.25rem;
overflow-x: auto;
}
main.content code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
main.content :not(pre) > code { background: var(--bg-elev); padding: 0.1rem 0.35rem; border-radius: 0.2rem; font-size: 0.88em; }
main.content blockquote { border-left: 3px solid var(--border); margin: 1rem 0; padding: 0.2rem 1rem; color: var(--fg-dim); }
main.content .admonition {
margin: 1rem 0; padding: 0.5rem 1rem; border-radius: 0.3rem; border-left: 3px solid var(--border);
background: var(--bg-elev);
}
main.content .admonition .admonition-title { margin: 0.2rem 0 0.4rem; font-weight: 600; }
main.content .admonition.note { border-left-color: #0969da; }
main.content .admonition.note .admonition-title { color: #0969da; }
main.content .admonition.tip { border-left-color: #1a7f37; }
main.content .admonition.tip .admonition-title { color: #1a7f37; }
main.content .admonition.important { border-left-color: #8250df; }
main.content .admonition.important .admonition-title { color: #8250df; }
main.content .admonition.warning { border-left-color: #9a6700; }
main.content .admonition.warning .admonition-title { color: #9a6700; }
main.content .admonition.caution { border-left-color: #cf222e; }
main.content .admonition.caution .admonition-title { color: #cf222e; }
main.content details { margin: 0.5rem 0; }
main.content hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
.headerlink { margin-left: 0.25rem; opacity: 0; font-size: 0.7em; }
h1:hover .headerlink, h2:hover .headerlink, h3:hover .headerlink { opacity: 1; }
.edit-link { margin-top: 3rem; font-size: 0.85rem; }
.copy-btn {
position: absolute; top: 0.4rem; right: 0.4rem;
background: var(--bg); color: var(--fg-dim);
border: 1px solid var(--border); border-radius: 0.35rem;
padding: 0.2rem 0.5rem; font-size: 0.78rem; cursor: pointer;
opacity: 0; transition: opacity 120ms;
}
main.content pre:hover .copy-btn { opacity: 1; }
@media (max-width: 48rem) {
nav.sidebar { display: none; }
main.content { padding: 2rem 1.25rem 4rem; }
}
.glossary-term {
position: relative;
color: inherit;
}
.glossary-term__label {
border-bottom: 1px dotted currentColor;
}
.glossary-term__tooltip {
position: absolute;
top: calc(100% + 0.4rem);
left: 50%;
width: max-content;
max-width: min(30rem, 80vw);
padding: 0.65rem 0.8rem;
border-radius: 0.6rem;
background: rgb(26 26 26 / 96%);
color: white;
box-shadow: 0 0.6rem 1.8rem rgb(0 0 0 / 22%);
font-size: 0.85rem;
line-height: 1.45;
opacity: 0;
pointer-events: none;
transform: translateX(-50%) translateY(-0.15rem);
transition: opacity 120ms ease, transform 120ms ease;
visibility: hidden;
z-index: 20;
}
.glossary-term:hover .glossary-term__tooltip,
.glossary-term:focus-visible .glossary-term__tooltip,
.glossary-term:focus-within .glossary-term__tooltip {
opacity: 1;
transform: translateX(-50%) translateY(0);
visibility: visible;
}
</style>
</head>
<body>
<header class="site">
<a class="brand" href="{{HOME_HREF}}">
<img src="{{ROOT}}assets/comma-logo.png" alt="">
<span class="site-name">openpilot docs</span>
</a>
<span class="spacer"></span>
<div class="social">
<a href="https://github.com/commaai" aria-label="GitHub">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="
M12 .7A11.5 11.5 0 0 0 8.4 23c.6.1.8-.2.8-.5v-2c-3.3.7-4-1.4-4-1.4-.5-1.4-1.3-1.8-1.3-1.8-1.1-.7.1-.7.1-.7
1.2.1 1.8 1.2 1.8 1.2 1.1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.8-1.6-2.7-.3-5.5-1.3-5.5-5.7 0-1.3.5-2.3 1.2-3.1
-.1-.3-.5-1.6.1-3.1 0 0 1-.3 3.2 1.2a11 11 0 0 1 5.8 0C15.8 6 16.8 6.3 16.8 6.3c.6 1.5.2 2.8.1 3.1
.8.8 1.2 1.8 1.2 3.1 0 4.4-2.8 5.4-5.5 5.7.4.4.8 1.1.8 2.2v2.1c0 .3.2.6.8.5A11.5 11.5 0 0 0 12 .7Z"/></svg>
</a>
<a href="https://discord.comma.ai" aria-label="Discord">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="
M20.3 4.4A16 16 0 0 0 16.3 3l-.5 1.1a15 15 0 0 0-7.6 0L7.7 3a16 16 0 0 0-4 1.4C1.1 8.2.4 11.9.8 15.5
a16 16 0 0 0 4.9 2.6l1.2-1.7-1.8-.9.4-.3c3.5 1.6 9.4 1.6 13 0l.4.3-1.8.9 1.2 1.7a16 16 0 0 0 4.9-2.6
c.5-4.2-.8-7.8-2.9-11.1ZM8.3 13.3c-1.1 0-1.9-1-1.9-2.2 0-1.3.8-2.3 1.9-2.3s2 1 1.9 2.3c0 1.2-.8 2.2-1.9 2.2Z
m7.4 0c-1.1 0-1.9-1-1.9-2.2 0-1.3.8-2.3 1.9-2.3s2 1 1.9 2.3c0 1.2-.8 2.2-1.9 2.2Z"/></svg>
</a>
<a href="https://x.com/comma_ai" aria-label="X">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="
M18.2 2.3h3.3l-7.2 8.3 8.5 11.2h-6.7l-5.2-6.8-6 6.8H1.6l7.8-8.9L1.2 2.3h6.8l4.7 6.2 5.5-6.2Z
m-1.2 17.5h1.8L7 4.2H5Z"/></svg>
</a>
</div>
</header>
<div class="layout">
<nav class="sidebar">
{{NAV}}
</nav>
<main class="content">
{{BODY}}
<div class="edit-link">
<a href="{{EDIT_URL}}">Edit this page on GitHub</a>
</div>
</main>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('pre').forEach(function (el) {
var btn = document.createElement('button');
btn.className = 'copy-btn';
btn.textContent = 'copy';
btn.addEventListener('click', function () {
var code = el.querySelector('code');
if (!code) return;
navigator.clipboard.writeText(code.innerText).then(function () {
btn.textContent = 'copied';
setTimeout(function () { btn.textContent = 'copy'; }, 1200);
});
});
el.appendChild(btn);
});
});
</script>
</body>
</html>
+3 -3
View File
@@ -19,12 +19,12 @@ function agnos_init {
# Check if AGNOS update is required
if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then
AGNOS_PY="$DIR/openpilot/common/hardware/tici/agnos.py"
MANIFEST="$DIR/openpilot/system/hardware/tici/agnos.json"
AGNOS_PY="$DIR/openpilot/common/hardware/comma/agnos.py"
MANIFEST="$DIR/openpilot/system/hardware/comma/agnos.json"
if $AGNOS_PY --verify $MANIFEST; then
sudo reboot
fi
$DIR/openpilot/common/hardware/tici/updater $AGNOS_PY $MANIFEST
$DIR/openpilot/common/hardware/comma/updater $AGNOS_PY $MANIFEST
fi
}
+1 -1
View File
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="18.5"
export AGNOS_VERSION="19.6"
fi
export STAGING_ROOT="/data/safe_staging"
+10
View File
@@ -775,3 +775,13 @@ struct GpsTrajectory @0x8cfeb072f5301000 {
x @0 :List(Float32);
y @1 :List(Float32);
}
struct AudioFeedbackDEPRECATED @0xed47e3c075be372a {
audio @0 :AudioData;
blockNum @1 :UInt16;
struct AudioData {
data @0 :Data;
sampleRate @1 :UInt32;
}
}
+59 -51
View File
@@ -75,7 +75,6 @@ struct OnroadEvent @0xc4fa6047f024e718 {
driverUnresponsive2 @37;
driverUnresponsive3 @38;
belowSteerSpeed @39;
lowBattery @40;
accFaulted @41;
sensorDataInvalid @42;
commIssue @43;
@@ -107,18 +106,17 @@ struct OnroadEvent @0xc4fa6047f024e718 {
noGps @68;
wrongCruiseMode @69;
modeldLagging @70;
deviceFalling @71;
fanMalfunction @72;
cameraMalfunction @73;
cameraFrameRate @74;
processNotRunning @75;
dashcamMode @76;
selfdriveInitializing @77;
usbError @78;
cruiseMismatch @79;
canBusMissing @80;
selfdrivedLagging @81;
resumeBlocked @82;
carNotReady @103;
steerTimeLimit @83;
vehicleSensorsInvalid @84;
locationdTemporaryError @85;
@@ -131,9 +129,15 @@ struct OnroadEvent @0xc4fa6047f024e718 {
aeb @92;
userBookmark @95;
excessiveActuation @96;
audioFeedback @97;
bigModelLoading @100;
bigModelFailed @102;
lowBatteryDEPRECATED @40;
soundsUnavailableDEPRECATED @47;
deviceFallingDEPRECATED @71;
usbErrorDEPRECATED @78;
audioFeedbackDEPRECATED @97;
bigModelReadyDEPRECATED @101;
}
}
@@ -176,13 +180,13 @@ struct InitData {
enum DeviceType {
unknown @0;
neo @1;
neo @1; # NEO, EON, & comma two
chffrAndroid @2;
chffrIos @3;
tici @4;
tici @4; # comma three
pc @5;
tizi @6;
mici @7;
tizi @6; # comma 3X
mici @7; # comma four
}
struct PandaInfo {
@@ -700,9 +704,29 @@ struct UsbState {
manufacturer @6 :Text;
product @5 :Text;
linkErrorCount @7 :UInt16;
usb3Lane @8 :Usb3Lane;
enum Usb3Lane {
unknown @0;
a @1;
b @2;
}
}
}
struct ChestnutState {
tempC @0 :Float32;
memoryTempC @1 :Float32;
powerDrawW @2 :Float32;
powerLimitW @3 :Float32;
gpuUsagePercent @4 :UInt8;
gpuClockMhz @5 :UInt16;
fanSpeedRpm @6 :UInt16;
pcieLtssm @7 :UInt8;
supplyVoltage @8 :UInt16; # mV
supplyCurrent @9 :Int16; # mA
}
struct RadarState @0x9a185389d6fdd05f {
mdMonoTime @6 :UInt64; # for debugging
radarErrors @13 :Car.RadarData.Error;
@@ -746,7 +770,7 @@ struct RadarState @0x9a185389d6fdd05f {
}
}
struct LiveCalibrationData {
struct ExtrinsicsCalibration @0x96df70754d8390bc {
calStatus @11 :Status;
calCycle @2 :Int32;
calPerc @3 :Int8;
@@ -1026,6 +1050,7 @@ struct ModelDataV2 {
timestampEof @3 :UInt64;
modelExecutionTime @15 :Float32;
rawPredictions @16 :Data;
big @27 :Bool;
# predicted future position, orientation, etc..
position @4 :XYZTData;
@@ -1366,7 +1391,7 @@ struct LiveLocationKalman {
}
struct LivePose {
struct DeviceMotion @0xc24ca2b57206b44d {
# More info on reference frames:
# https://github.com/commaai/openpilot/tree/master/openpilot/common/transformations
orientationNED @0 :XYZMeasurement;
@@ -2248,7 +2273,7 @@ struct Boot {
}
}
struct LiveParametersData {
struct VehicleParameters @0xd9058dcb967c2753 {
valid @0 :Bool;
gyroBias @1 :Float32;
angleOffsetDeg @2 :Float32;
@@ -2282,8 +2307,8 @@ struct LiveParametersData {
}
}
struct LiveTorqueParametersData {
liveValid @0 :Bool;
struct LateralTorqueParameters @0xe61690eb0b091692 {
valid @0 :Bool;
latAccelFactorRaw @1 :Float32;
latAccelOffsetRaw @2 :Float32;
frictionCoefficientRaw @3 :Float32;
@@ -2299,7 +2324,7 @@ struct LiveTorqueParametersData {
calPerc @13 :Int8;
}
struct LiveDelayData {
struct LateralDelay @0x98dfdb22c44df8d4 {
lateralDelay @0 :Float32;
validBlocks @1 :Int32;
status @2 :Status;
@@ -2309,19 +2334,6 @@ struct LiveDelayData {
points @4 :List(Float32);
calPerc @6 :Int8;
version @7 :Int32;
speedBucket @8 :Int8;
speedBucketEdges @9 :List(Float32);
lateralDelayBuckets @10 :List(Float32);
lateralDelayEstimateStdBuckets @11 :List(Float32);
validBlocksBuckets @12 :List(Int32);
calPercBuckets @13 :List(Int8);
statusBuckets @14 :List(Status);
lateralDelayAppliedBuckets @15 :List(Float32);
blockIdxBuckets @16 :List(Int8);
sampleIdxBuckets @17 :List(Int8);
blockValuesBuckets @18 :List(List(Float32));
learningCountdownBuckets @19 :List(Float32);
learningResetReasonBuckets @20 :List(Text);
enum Status {
unestimated @0;
@@ -2498,11 +2510,6 @@ struct AudioData {
sampleRate @1 :UInt32;
}
struct AudioFeedback {
audio @0 :AudioData;
blockNum @1 :UInt16;
}
struct Touch {
sec @0 :Int64;
usec @1 :Int64;
@@ -2535,9 +2542,9 @@ struct Event {
pandaStates @81 :List(PandaState);
peripheralState @80 :PeripheralState;
radarState @13 :RadarState;
liveTracks @131 :Car.RadarData;
radarTracks @131 :Car.RadarData;
sendcan @17 :List(CanData);
liveCalibration @19 :LiveCalibrationData;
extrinsicsCalibration @19 :ExtrinsicsCalibration;
carState @22 :Car.CarState;
carControl @23 :Car.CarControl;
carOutput @127 :Car.CarOutput;
@@ -2548,31 +2555,31 @@ struct Event {
qcomGnss @31 :QcomGnss;
gpsLocationExternal @48 :GpsLocationData;
gpsLocation @21 :GpsLocationData;
liveParameters @61 :LiveParametersData;
liveTorqueParameters @94 :LiveTorqueParametersData;
liveDelay @146 : LiveDelayData;
vehicleParameters @61 :VehicleParameters;
lateralTorqueParameters @94 :LateralTorqueParameters;
lateralDelay @146 : LateralDelay;
cameraOdometry @63 :CameraOdometry;
thumbnail @66: Thumbnail;
onroadEvents @134: List(OnroadEvent);
carParams @69: Car.CarParams;
driverMonitoringState @151 :DriverMonitoringState;
livePose @129 :LivePose;
deviceMotion @129 :DeviceMotion;
modelV2 @75 :ModelDataV2;
drivingModelData @128 :DrivingModelData;
driverStateV2 @92 :DriverStateV2;
# camera stuff, each camera state has a matching encode idx
roadCameraState @2 :FrameData;
driverCameraState @70: FrameData;
narrowRoadCameraState @2 :FrameData;
cabinCameraState @70: FrameData;
wideRoadCameraState @74: FrameData;
roadEncodeIdx @15 :EncodeIndex;
driverEncodeIdx @76 :EncodeIndex;
narrowRoadEncodeIdx @15 :EncodeIndex;
cabinEncodeIdx @76 :EncodeIndex;
wideRoadEncodeIdx @77 :EncodeIndex;
qRoadEncodeIdx @90 :EncodeIndex;
qNarrowRoadEncodeIdx @90 :EncodeIndex;
livestreamRoadEncodeIdx @117 :EncodeIndex;
livestreamNarrowRoadEncodeIdx @117 :EncodeIndex;
livestreamWideRoadEncodeIdx @118 :EncodeIndex;
livestreamDriverEncodeIdx @119 :EncodeIndex;
livestreamCabinEncodeIdx @119 :EncodeIndex;
# microphone data
soundPressure @103 :SoundPressure;
@@ -2584,6 +2591,7 @@ struct Event {
procLog @33 :ProcLog;
clocks @35 :Clocks;
deviceState @6 :DeviceState;
chestnutState @152 :ChestnutState;
logMessage @18 :Text;
errorLogMessage @85 :Text;
@@ -2596,21 +2604,20 @@ struct Event {
# driving feedback
userBookmark @93 :UserBookmark;
bookmarkButton @148 :UserBookmark;
audioFeedback @149 :AudioFeedback;
lateralManeuverPlan @150 :LateralManeuverPlan;
# *********** debug ***********
testJoystick @52 :Joystick;
roadEncodeData @86 :EncodeData;
driverEncodeData @87 :EncodeData;
narrowRoadEncodeData @86 :EncodeData;
cabinEncodeData @87 :EncodeData;
wideRoadEncodeData @88 :EncodeData;
qRoadEncodeData @89 :EncodeData;
qNarrowRoadEncodeData @89 :EncodeData;
alertDebug @133 :DebugAlert;
livestreamRoadEncodeData @120 :EncodeData;
livestreamNarrowRoadEncodeData @120 :EncodeData;
livestreamWideRoadEncodeData @121 :EncodeData;
livestreamDriverEncodeData @122 :EncodeData;
livestreamCabinEncodeData @122 :EncodeData;
# *********** Custom: reserved for forks ***********
@@ -2646,6 +2653,7 @@ struct Event {
# *********** legacy + deprecated ***********
model @9 :Deprecated.ModelData; # TODO: rename modelV2 and mark this as deprecated
audioFeedbackDEPRECATED @149 :Deprecated.AudioFeedbackDEPRECATED;
liveMpcDEPRECATED @36 :Deprecated.LiveMpcData;
liveLongitudinalMpcDEPRECATED @37 :Deprecated.LiveLongitudinalMpcData;
liveLocationKalmanDeprecatedDEPRECATED @51 :Deprecated.LiveLocationData;
@@ -4,6 +4,7 @@ import numbers
import random
import threading
import time
from openpilot.common.test import OpenpilotTestCase
from openpilot.common.parameterized import parameterized
from openpilot.cereal import log
@@ -46,7 +47,7 @@ def delayed_send(delay, sock, dat):
threading.Timer(delay, send_func).start()
class TestMessaging:
class TestMessaging(OpenpilotTestCase):
@parameterized.expand(events)
def test_new_message(self, evt):
try:
@@ -80,10 +81,11 @@ class TestMessaging:
assert len(msgs) == 0
# no wait but msgs are queued up
pub_sock.send(messaging.new_message(sock).to_bytes())
assert sub_sock.receive() is not None # synchronize the PUB/SUB connection
num_msgs = random.randrange(3, 10)
for _ in range(num_msgs):
pub_sock.send(messaging.new_message(sock).to_bytes())
time.sleep(0.1)
msgs = func(sub_sock)
assert isinstance(msgs, list)
assert all(isinstance(msg, expected_type) for msg in msgs)
@@ -110,7 +112,7 @@ class TestMessaging:
def test_recv_one(self):
sock = "carState"
pub_sock = messaging.pub_sock(sock)
sub_sock = messaging.sub_sock(sock, timeout=1000)
sub_sock = messaging.sub_sock(sock, timeout=10)
# no msg in queue, socket should timeout
recvd = messaging.recv_one(sub_sock)
@@ -141,7 +143,7 @@ class TestMessaging:
def test_recv_one_retry(self):
sock = "carState"
sock_timeout = 0.1
sock_timeout = 0.005
pub_sock = messaging.pub_sock(sock)
sub_sock = messaging.sub_sock(sock, timeout=round(sock_timeout*1000))
@@ -3,13 +3,14 @@ import time
from typing import cast
from collections.abc import Sized
from openpilot.common.test import OpenpilotTestCase
import openpilot.cereal.messaging as messaging
from openpilot.cereal.messaging.tests.test_messaging import events, random_sock, random_socks, \
random_bytes, random_carstate, assert_carstate
from openpilot.cereal.services import SERVICE_LIST
class TestSubMaster:
class TestSubMaster(OpenpilotTestCase):
def test_init(self):
sm = messaging.SubMaster(events)
@@ -59,17 +60,17 @@ class TestSubMaster:
def test_update_timeout(self):
sock = random_sock()
sm = messaging.SubMaster([sock,])
timeout = random.randrange(1000, 3000)
timeout = random.randrange(10, 30)
start_time = time.monotonic()
sm.update(timeout)
t = time.monotonic() - start_time
assert t >= timeout/1000.
assert t < 3
assert t < 0.1
assert not any(sm.updated.values())
def test_avg_frequency_checks(self):
for poll in (True, False):
sm = messaging.SubMaster(["modelV2", "carParams", "carState", "cameraOdometry", "liveCalibration"],
sm = messaging.SubMaster(["modelV2", "carParams", "carState", "cameraOdometry", "extrinsicsCalibration"],
poll=("modelV2" if poll else None),
frequency=(20. if not poll else None))
@@ -77,7 +78,7 @@ class TestSubMaster:
"carState": (20, 20),
"modelV2": (20, 20 if poll else 10),
"cameraOdometry": (20, 10),
"liveCalibration": (4, 4),
"extrinsicsCalibration": (4, 4),
"carParams": (None, None),
"userBookmark": (None, None),
}
@@ -97,17 +98,19 @@ class TestSubMaster:
pub_sock = messaging.pub_sock(sock)
sm = messaging.SubMaster([sock,])
pub_sock.send(messaging.new_message(sock).to_bytes())
sm.update(1000) # synchronize the PUB/SUB connection
n = 10
for i in range(n+1):
msg = messaging.new_message(sock)
msg.carState.vEgo = i
pub_sock.send(msg.to_bytes())
time.sleep(0.01)
sm.update(1000)
assert sm[sock].vEgo == n
class TestPubMaster:
class TestPubMaster(OpenpilotTestCase):
def test_init(self):
messaging.PubMaster(events)
@@ -1,13 +1,14 @@
import subprocess
import tempfile
from openpilot.common.test import OpenpilotTestCase
from openpilot.common.parameterized import parameterized
import openpilot.cereal.services as services
from openpilot.cereal.services import SERVICE_LIST
class TestServices:
class TestServices(OpenpilotTestCase):
@parameterized.expand(SERVICE_LIST.keys())
def test_services(self, s):
+19 -19
View File
@@ -25,6 +25,7 @@ _services: dict[str, tuple] = {
"accelerometer": (True, 104., 104),
"temperatureSensor": (True, 2., 200),
"deviceState": (True, 2., 1),
"chestnutState": (True, 10., 10),
"touch": (True, 20., 1),
"can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment
"controlsState": (True, 100., 10, QueueSize.MEDIUM),
@@ -32,14 +33,14 @@ _services: dict[str, tuple] = {
"pandaStates": (True, 10., 1),
"peripheralState": (True, 2., 1),
"radarState": (True, 20., 5),
"roadEncodeIdx": (False, 20., 1),
"liveTracks": (True, 20.),
"narrowRoadEncodeIdx": (False, 20., 1),
"radarTracks": (True, 20.),
"sendcan": (True, 100., 139, QueueSize.MEDIUM),
"logMessage": (True, 0., None, QueueSize.BIG),
"errorLogMessage": (True, 0., 1, QueueSize.BIG),
"liveCalibration": (True, 4., 4),
"liveTorqueParameters": (True, 4., 1),
"liveDelay": (True, 4., 1),
"extrinsicsCalibration": (True, 4., 4),
"lateralTorqueParameters": (True, 4., 1),
"lateralDelay": (True, 4., 1),
"operatingSystemLog": (True, 0.),
"carState": (True, 100., 10),
"carControl": (True, 100., 10),
@@ -54,15 +55,15 @@ _services: dict[str, tuple] = {
"qcomGnss": (True, 2.),
"clocks": (True, 0.1, 1),
"ubloxRaw": (True, 20.),
"livePose": (True, 20., 4),
"liveParameters": (True, 20., 5),
"deviceMotion": (True, 20., 4),
"vehicleParameters": (True, 20., 5),
"cameraOdometry": (True, 20., 10),
"thumbnail": (True, 1 / 60., 1),
"onroadEvents": (True, 1., 1),
"carParams": (True, 0.02, 1),
"roadCameraState": (True, 20., 20),
"driverCameraState": (True, 20., 20),
"driverEncodeIdx": (False, 20., 1),
"narrowRoadCameraState": (True, 20., 20),
"cabinCameraState": (True, 20., 20),
"cabinEncodeIdx": (False, 20., 1),
"driverStateV2": (True, 20., 10),
"driverMonitoringState": (True, 20., 10),
"wideRoadEncodeIdx": (False, 20., 1),
@@ -70,16 +71,15 @@ _services: dict[str, tuple] = {
"drivingModelData": (True, 20., 10),
"modelV2": (True, 20., None, QueueSize.BIG),
"managerState": (True, 2., 1),
"qRoadEncodeIdx": (False, 20.),
"qNarrowRoadEncodeIdx": (False, 20.),
"userBookmark": (True, 0., 1),
"soundPressure": (True, 10., 10),
"rawAudioData": (False, 20.),
"bookmarkButton": (True, 0., 1),
"audioFeedback": (True, 0., 1),
"roadEncodeData": (False, 20., None, QueueSize.BIG),
"driverEncodeData": (False, 20., None, QueueSize.BIG),
"narrowRoadEncodeData": (False, 20., None, QueueSize.BIG),
"cabinEncodeData": (False, 20., None, QueueSize.BIG),
"wideRoadEncodeData": (False, 20., None, QueueSize.BIG),
"qRoadEncodeData": (False, 20., None, QueueSize.BIG),
"qNarrowRoadEncodeData": (False, 20., None, QueueSize.BIG),
# sunnypilot
"modelManagerSP": (False, 1., 1, QueueSize.BIG),
@@ -99,11 +99,11 @@ _services: dict[str, tuple] = {
"testJoystick": (True, 0.),
"alertDebug": (True, 20., 5),
"livestreamWideRoadEncodeIdx": (False, 20.),
"livestreamRoadEncodeIdx": (False, 20.),
"livestreamDriverEncodeIdx": (False, 20.),
"livestreamNarrowRoadEncodeIdx": (False, 20.),
"livestreamCabinEncodeIdx": (False, 20.),
"livestreamWideRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
"livestreamRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
"livestreamDriverEncodeData": (False, 20., None, QueueSize.MEDIUM),
"livestreamNarrowRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
"livestreamCabinEncodeData": (False, 20., None, QueueSize.MEDIUM),
"customReservedRawData0": (True, 0.),
}
SERVICE_LIST = {name: Service(*vals) for
+8
View File
@@ -0,0 +1,8 @@
from enum import IntEnum
class VisionStreamType(IntEnum):
VISION_STREAM_NARROW_ROAD = 0
VISION_STREAM_CABIN = 1
VISION_STREAM_WIDE_ROAD = 2
VISION_STREAM_MAP = 3
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include "msgq/visionipc/visionbuf.h"
enum VisionStreamValues : VisionStreamType {
VISION_STREAM_NARROW_ROAD = 0,
VISION_STREAM_CABIN = 1,
VISION_STREAM_WIDE_ROAD = 2,
VISION_STREAM_MAP = 3,
};
+5 -10
View File
@@ -1,4 +1,4 @@
Import('env', 'envCython')
Import('env')
common_libs = [
'params.cc',
@@ -11,14 +11,9 @@ common_libs = [
_common = env.Library('common', common_libs, LIBS="json11")
Export('_common')
if GetOption('extras'):
env.Program('tests/test_common',
['tests/test_runner.cc', 'tests/test_util.cc', 'tests/test_swaglog.cc'],
LIBS=[_common, 'json11', 'zmq', 'pthread'])
# Cython bindings
params_python = envCython.Program('params_pyx.so', 'params_pyx.pyx', LIBS=envCython['LIBS'] + [_common, 'zmq', 'json11'])
params_python = env.SharedLibrary('params_c', 'params_c.cc', LIBS=[_common, 'zmq', 'json11'])
common_python = [params_python]
Export('common_python')
if GetOption('extras'):
env.Program('tests/test_swaglog', 'tests/test_swaglog.cc', LIBS=[_common, 'json11', 'zmq', 'pthread'])
+19 -3
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
import argparse
import sys
import time
from openpilot.common.hardware import HARDWARE
from openpilot.common.esim.base import LPABase, Profile
@@ -27,6 +29,17 @@ def print_profiles(lpa: LPABase) -> None:
print(f'{i}. {p.iccid} (nickname: {p.nickname or "<none provided>"}) (provider: {p.provider}) - {"enabled" if p.enabled else "disabled"}')
def execute_and_process_notifications(lpa: LPABase, operation) -> None:
try:
operation()
finally:
time.sleep(1) # Need to wait for 1s after the operation is finished so the eUICC/modem can settle down.
try:
lpa.process_notifications()
except Exception as e:
print(f'failed to process eSIM notifications: {e}', file=sys.stderr)
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='esim.py', description='manage eSIM profiles on your comma device', epilog='comma.ai')
sub = parser.add_subparsers(dest='cmd')
@@ -50,18 +63,21 @@ if __name__ == '__main__':
args = parser.parse_args()
lpa = HARDWARE.get_sim_lpa()
if not lpa.is_euicc():
raise SystemExit("no eUICC detected")
if args.cmd == 'switch':
lpa.switch_profile(resolve_iccid(lpa, args.profile))
iccid = resolve_iccid(lpa, args.profile)
execute_and_process_notifications(lpa, lambda: lpa.switch_profile(iccid))
elif args.cmd == 'delete':
iccid = resolve_iccid(lpa, args.profile)
confirm = input(f'are you sure you want to delete profile {iccid}? (y/N) ')
if confirm == 'y':
lpa.delete_profile(iccid)
execute_and_process_notifications(lpa, lambda: lpa.delete_profile(iccid))
else:
print('cancelled')
exit(0)
elif args.cmd == 'download':
lpa.download_profile(args.qr, args.name)
execute_and_process_notifications(lpa, lambda: lpa.download_profile(args.qr, args.name))
elif args.cmd == 'nickname':
lpa.nickname_profile(resolve_iccid(lpa, args.profile), args.name)
else:
+2 -2
View File
@@ -451,7 +451,7 @@ def process_notifications(client: AtClient) -> None:
response = es10x_command(client, request)
content = require_tag(require_tag(response, TAG_RETRIEVE_NOTIFICATION, "RetrieveNotificationsListResponse"),
TAG_OK, "RetrieveNotificationsListResponse")
pending_notif = next((v for t, v in iter_tlv(content) if t in (TAG_PROFILE_INSTALL_RESULT, 0x30)), None)
pending_notif = next((content[start:end] for t, _, start, end in iter_tlv(content, with_positions=True) if t in (TAG_PROFILE_INSTALL_RESULT, 0x30)), None)
if pending_notif is None:
raise RuntimeError("Missing PendingNotification")
@@ -682,7 +682,7 @@ def download_profile(client: AtClient, activation_code: str) -> str:
session.close()
class TiciLPA(LPABase):
class LPA(LPABase):
def __init__(self):
if hasattr(self, '_client'):
return
+240
View File
@@ -0,0 +1,240 @@
import math
import os
import random
import secrets
import struct
from collections.abc import Callable, Sequence
from functools import wraps
from typing import Any, TypeVar
import capnp
T = TypeVar("T")
_EDGE_SLOTS = 16
_MINIMAL_EXAMPLES = 10
_INTEGER_RANGES = {
"int8": (-2**7, 2**7 - 1),
"int16": (-2**15, 2**15 - 1),
"int32": (-2**31, 2**31 - 1),
"int64": (-2**63, 2**63 - 1),
"uint8": (0, 2**8 - 1),
"uint16": (0, 2**16 - 1),
"uint32": (0, 2**32 - 1),
"uint64": (0, 2**64 - 1),
}
# One seed is shared by the whole test process. Individual tests derive their seed
# from their unittest ID, so FUZZ_SEED is reproducible under the parallel runner too.
FUZZ_SEED = int(os.environ.get("FUZZ_SEED", secrets.randbits(64)))
class Fuzzy:
"""Fast, deterministic data generator with systematic boundary coverage."""
def __init__(self, seed: int | str, example_index: int):
self.example_index = example_index
self._random = random.Random(seed)
self._draw_index = 0
def _draw(self, edges: Sequence[T], random_value: Callable[[], T]) -> T:
draw_index = self._draw_index
self._draw_index += 1
# Preserve the cheap minimal prefix Hypothesis produced, then interleave
# systematic boundaries and random values at every draw site.
if self.example_index < _MINIMAL_EXAMPLES:
return edges[0]
search_example = self.example_index - _MINIMAL_EXAMPLES
if search_example < _EDGE_SLOTS * 2 and search_example % 2 == 0:
return edges[(search_example // 2 + draw_index) % len(edges)]
if self._random.randrange(4) == 0:
return self._random.choice(edges)
return random_value()
def boolean(self) -> bool:
return self._draw((False, True), lambda: bool(self._random.getrandbits(1)))
def choice(self, values: Sequence[T]) -> T:
if not values:
raise ValueError("cannot choose from an empty sequence")
return self._draw(values, lambda: self._random.choice(values))
def integer(self, min_value: int, max_value: int) -> int:
if min_value > max_value:
raise ValueError(f"{min_value=} must not exceed {max_value=}")
edges = [
0, 1, -1, min_value, max_value,
min_value + 1, max_value - 1,
]
edges.extend(1 << bit for bit in range(max_value.bit_length()))
edges.extend(-(1 << bit) for bit in range((-min_value).bit_length()))
valid_edges = tuple(dict.fromkeys(v for v in edges if min_value <= v <= max_value))
return self._draw(valid_edges, lambda: self._random.randint(min_value, max_value))
def floating(self, width: int = 64, *, allow_nan: bool = True, allow_infinity: bool = True) -> float:
if width not in (32, 64):
raise ValueError("float width must be 32 or 64")
if width == 32:
unpack_format = "!f"
finite_edges = (
0.0, -0.0, 1.0, -1.0,
struct.unpack(unpack_format, b"\x00\x00\x00\x01")[0],
struct.unpack(unpack_format, b"\x80\x00\x00\x01")[0],
struct.unpack(unpack_format, b"\x7f\x7f\xff\xff")[0],
struct.unpack(unpack_format, b"\xff\x7f\xff\xff")[0],
struct.unpack(unpack_format, b"\x00\x80\x00\x00")[0],
struct.unpack(unpack_format, b"\x80\x80\x00\x00")[0],
)
else:
unpack_format = "!d"
finite_edges = (
0.0, -0.0, 1.0, -1.0,
math.ulp(0.0), -math.ulp(0.0),
float.fromhex("0x1.fffffffffffffp+1023"), -float.fromhex("0x1.fffffffffffffp+1023"),
float.fromhex("0x1p-1022"), -float.fromhex("0x1p-1022"),
)
edges = list(finite_edges)
if allow_infinity:
edges.extend((math.inf, -math.inf))
if allow_nan:
edges.append(math.nan)
def random_float() -> float:
while True:
value = struct.unpack(unpack_format, self._random.randbytes(width // 8))[0]
if (allow_nan or not math.isnan(value)) and (allow_infinity or not math.isinf(value)):
return value
return self._draw(tuple(edges), random_float)
def _length(self, min_length: int, max_length: int | None) -> int:
if min_length < 0:
raise ValueError("minimum length must be non-negative")
if max_length is not None and min_length > max_length:
raise ValueError(f"{min_length=} must not exceed {max_length=}")
if max_length == min_length:
return min_length
offsets = (0, 1, 2, 4, 8, 16, 32)
edges = tuple(min_length + offset for offset in offsets if max_length is None or min_length + offset <= max_length)
def random_length() -> int:
# A geometric tail keeps ordinary examples small without placing an
# artificial ceiling on an unbounded list.
length = min_length
while max_length is None or length < max_length:
if self._random.randrange(8) == 0:
break
length += 1
return length
return self._draw(edges, random_length)
def binary(self, min_size: int = 0, max_size: int | None = None) -> bytes:
size = self._length(min_size, max_size)
patterns = (
bytes(size),
b"\xff" * size,
(b"\xaa\x55" * ((size + 1) // 2))[:size],
bytes(i & 0xff for i in range(size)),
)
return self._draw(patterns, lambda: self._random.randbytes(size))
def text(self, min_size: int = 0, max_size: int | None = None) -> str:
size = self._length(min_size, max_size)
def scalar() -> str:
value = self._random.randrange(0x110000 - 0x800)
if value >= 0xd800:
value += 0x800
return chr(value)
patterns = (
"",
"a" * size,
"\0" * size,
"\U0010ffff" * size,
)
valid_patterns = tuple(value for value in patterns if len(value) == size)
return self._draw(valid_patterns, lambda: "".join(scalar() for _ in range(size)))
def list(self, generate: Callable[[], T], min_size: int = 0, max_size: int | None = None) -> list[T]:
return [generate() for _ in range(self._length(min_size, max_size))]
def fuzzy_test(max_examples: int) -> Callable[[Callable[..., None]], Callable[..., None]]:
"""Run a unittest method repeatedly with independent, reproducible fuzzy data."""
max_examples = int(os.environ.get("MAX_EXAMPLES", max_examples))
assert max_examples >= 1
def decorator(fn: Callable[..., None]) -> Callable[..., None]:
@wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> None:
test_seed = f"{FUZZ_SEED}:{args[0].id()}"
selected_example = os.environ.get("FUZZ_EXAMPLE")
examples = [int(selected_example, 0)] if selected_example is not None else range(max_examples)
for example_index in examples:
if not 0 <= example_index < max_examples:
raise ValueError(f"FUZZ_EXAMPLE={example_index} is outside [0, {max_examples})")
try:
fn(*args, **kwargs, fuzzy=Fuzzy(f"{test_seed}:{example_index}", example_index))
except Exception as exc:
exc.add_note(f"reproduce with FUZZ_SEED={FUZZ_SEED} FUZZ_EXAMPLE={example_index}")
raise
return wrapper
return decorator
def capnp_random_dict(fuzzy: Fuzzy, schema: Any, event: str | None = None, *, real_floats: bool = False) -> dict[str, Any]:
"""Generate a dictionary accepted by a pycapnp struct constructor."""
def native(type_name: str) -> bool | int | float | str | bytes:
if type_name == "bool":
return fuzzy.boolean()
if type_name in _INTEGER_RANGES:
return fuzzy.integer(*_INTEGER_RANGES[type_name])
if type_name in ("float32", "float64"):
return fuzzy.floating(width=int(type_name[-2:]), allow_nan=not real_floats, allow_infinity=not real_floats)
if type_name == "text":
return fuzzy.text(max_size=1000)
if type_name == "anyPointer":
return fuzzy.text()
if type_name == "data":
return fuzzy.binary(max_size=1000)
raise NotImplementedError(f"invalid Cap'n Proto type: {type_name}")
def generate_field(field: Any) -> Any:
def rec(field_type: Any, base_type: str) -> Any:
type_name = field_type.which()
if type_name == "struct":
struct_schema = field.schema.elementType if base_type == "list" else field.schema
return capnp_random_dict(fuzzy, struct_schema, real_floats=real_floats)
if type_name == "list":
return fuzzy.list(lambda: rec(field_type.list.elementType, "list"))
if type_name == "enum":
enum_schema = field.schema.elementType if base_type == "list" else field.schema
return fuzzy.choice(tuple(enum_schema.enumerants))
return native(type_name)
try:
if hasattr(field.proto, "slot"):
slot_type = field.proto.slot.type
return rec(slot_type, slot_type.which())
return capnp_random_dict(fuzzy, field.schema, real_floats=real_floats)
except capnp.lib.capnp.KjException:
return capnp_random_dict(fuzzy, field.schema, real_floats=real_floats)
union_field = event or (fuzzy.choice(tuple(schema.union_fields)) if schema.union_fields else None)
fields = schema.non_union_fields + ((union_field,) if union_field else ())
return {
field_name: generate_field(schema.fields[field_name])
for field_name in fields
if not field_name.endswith("DEPRECATED") and field_name != "deprecated"
}
-9
View File
@@ -31,12 +31,3 @@ def get_origin(cwd: str | None = None) -> str:
return run_cmd(["git", "config", "remote." + tracking_remote + ".url"], cwd=cwd)
except subprocess.CalledProcessError: # Not on a branch, fallback
return run_cmd_default(["git", "config", "--get", "remote.origin.url"], cwd=cwd)
@cache
def get_normalized_origin(cwd: str | None = None) -> str:
return get_origin(cwd) \
.replace("git@", "", 1) \
.replace(".git", "", 1) \
.replace("https://", "", 1) \
.replace(":", "/", 1)
+7 -7
View File
@@ -2,15 +2,15 @@ import os
from typing import cast
from openpilot.common.hardware.base import HardwareBase
from openpilot.common.hardware.tici.hardware import Tici
from openpilot.common.hardware.pc.hardware import Pc
from openpilot.common.hardware.comma.hardware import HardwareComma
from openpilot.common.hardware.pc.hardware import HardwarePc
TICI = os.path.isfile('/TICI')
AGNOS = os.path.isfile('/AGNOS')
PC = not TICI
COMMA_HARDWARE = AGNOS
PC = not COMMA_HARDWARE
if TICI:
HARDWARE = cast(HardwareBase, Tici())
if COMMA_HARDWARE:
HARDWARE = cast(HardwareBase, HardwareComma())
else:
HARDWARE = cast(HardwareBase, Pc())
HARDWARE = cast(HardwareBase, HardwarePc())
@@ -23,14 +23,14 @@
},
{
"name": "abl",
"url": "https://commadist.azureedge.net/agnosupdate/abl-b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c.img.xz",
"hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c",
"hash_raw": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c",
"url": "https://commadist.azureedge.net/agnosupdate/abl-29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35.img.xz",
"hash": "29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35",
"hash_raw": "29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35",
"size": 274432,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c"
"ondevice_hash": "29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35"
},
{
"name": "aop",
@@ -56,29 +56,29 @@
},
{
"name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39.img.xz",
"hash": "19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39",
"hash_raw": "19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39",
"size": 17487872,
"url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz",
"hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
"hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
"size": 46897152,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "ddfe93cc6a8531af92ee331d9bbaeae2f1d933bdb38e579769dc9fe7998eb626"
"ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e"
},
{
"name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724.img.xz",
"hash": "4dc41c2c072f5f5d5cd484cd6173049cd96acfb9a67bc20049775585fe881539",
"hash_raw": "a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724",
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz",
"hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b",
"hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
"size": 4718592000,
"sparse": true,
"full_check": false,
"has_ab": true,
"ondevice_hash": "cf1229630b7a2b8497705bca4ba947dbf0c217418ff4febff571aa4f4a878134",
"ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468",
"alt": {
"hash": "a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724",
"url": "https://commadist.azureedge.net/agnosupdate/system-a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724.img",
"hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img",
"size": 4718592000
}
}
]
]
@@ -12,7 +12,7 @@ import requests
SPARSE_CHUNK_FMT = struct.Struct('H2xI4x')
AGNOS_MANIFEST_FILE = "openpilot/system/hardware/tici/agnos.json"
AGNOS_MANIFEST_FILE = "openpilot/system/hardware/comma/agnos.json"
class StreamingDecompressor:
@@ -339,51 +339,51 @@
},
{
"name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8.img.xz",
"hash": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8",
"hash_raw": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8",
"size": 17487872,
"url": "https://commadist.azureedge.net/agnosupdate/boot-5463317ca7a231f2ddd55a667a8c2b0d6692982359773ca875a99a0e6fd67fb4.img.xz",
"hash": "5463317ca7a231f2ddd55a667a8c2b0d6692982359773ca875a99a0e6fd67fb4",
"hash_raw": "5463317ca7a231f2ddd55a667a8c2b0d6692982359773ca875a99a0e6fd67fb4",
"size": 46897152,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "edca8bee1531e66953d107eeceeed2dc7b3ca46417e49d55508f94e58bf95db8"
"ondevice_hash": "daefa6f897bdcc277e50dbf562fd63e57d50d9fc7a599f2d171208a490e725d9"
},
{
"name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img.xz",
"hash": "78acfe16a7b62a3a91fc7a81f40a693e4468cec1c69df7d0b1e550aacc646113",
"hash_raw": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f",
"url": "https://commadist.azureedge.net/agnosupdate/system-2e1ca22762e66898aae384fcc21f488cb015c020b6b588862a64fc8128f0ad1d.img.xz",
"hash": "14ab660bb955604ab6c482b3f59e5239e2c40c54e798201afcaed43d5cb0dcfd",
"hash_raw": "2e1ca22762e66898aae384fcc21f488cb015c020b6b588862a64fc8128f0ad1d",
"size": 4718592000,
"sparse": true,
"full_check": false,
"has_ab": true,
"ondevice_hash": "743142c5a898f27b2a1029cca42c8a5d5d1fc0096414422b850fe84c8d0b8342",
"ondevice_hash": "18666ca3b529304f5e7a8fc2e3c9dce715b55941e3965a7af43c613f8e18cfce",
"alt": {
"hash": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f",
"url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img",
"hash": "2e1ca22762e66898aae384fcc21f488cb015c020b6b588862a64fc8128f0ad1d",
"url": "https://commadist.azureedge.net/agnosupdate/system-2e1ca22762e66898aae384fcc21f488cb015c020b6b588862a64fc8128f0ad1d.img",
"size": 4718592000
}
},
{
"name": "userdata_90",
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-14a3fc6e9bd148b9deebf6ae9df2f1b3b759e629b337e41b6895864cdd51f630.img.xz",
"hash": "52160dd01b30b3dc572226e8d549a034b03bc328e80f1f4cd6a857b6dd447687",
"hash_raw": "14a3fc6e9bd148b9deebf6ae9df2f1b3b759e629b337e41b6895864cdd51f630",
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-871adca25cb254ad1704ce427f3faaae1027f74aff43a3e150188c8a1c3d3cb8.img.xz",
"hash": "e4f59e5dcf4758759da9284479163e420febf1c118bf33c83b402e80e241608d",
"hash_raw": "871adca25cb254ad1704ce427f3faaae1027f74aff43a3e150188c8a1c3d3cb8",
"size": 96636764160,
"sparse": true,
"full_check": true,
"has_ab": false,
"ondevice_hash": "3bbc052c7793087946b0cd668c1778f930084f6b00896aeebd193dfce53fa518"
"ondevice_hash": "17b4ec7449e62c52be5df551542684b953e5c7ffc95103008043602f79ae7112"
},
{
"name": "userdata_89",
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-425c69d021f4ee2f767963bf7b991d1a492485fa465c5f5d001e1cf7de3d62a0.img.xz",
"hash": "02a8c5512754d7781d930d242be3fad01fbce652297c69dfe8722dc92b18dc09",
"hash_raw": "425c69d021f4ee2f767963bf7b991d1a492485fa465c5f5d001e1cf7de3d62a0",
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-9d84bd64ef237a32675f936594c16932f8acbb52cf2dd82373a0e718cd32dfcf.img.xz",
"hash": "652e61ecb5b12250dbfeb9f12fd74af16b867b5ea9bf8a08b5731ffd63a76317",
"hash_raw": "9d84bd64ef237a32675f936594c16932f8acbb52cf2dd82373a0e718cd32dfcf",
"size": 95563022336,
"sparse": true,
"full_check": true,
"has_ab": false,
"ondevice_hash": "3667d500f91b08a4671d7c09623eb6ae8fc905d9273893b50689e214005c7fa6"
"ondevice_hash": "acad63821bb48823c871910b1a363ab924d2635a7574cc5045e877c438d3012f"
}
]
@@ -9,7 +9,7 @@
#include "common/util.h"
#include "common/hardware/base.h"
class HardwareTici : public HardwareNone {
class HardwareComma : public HardwareNone {
public:
static std::string get_name() {
static const std::string name = []() {
@@ -12,9 +12,8 @@ from openpilot.common.utils import sudo_read, sudo_write
from openpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action
from openpilot.common.esim.base import LPABase
from openpilot.common.hardware.base import HardwareBase, ThermalConfig, ThermalZone
from openpilot.common.esim.lpa import TiciLPA
from openpilot.common.hardware.tici.pins import GPIO
from openpilot.common.hardware.tici.amplifier import Amplifier
from openpilot.common.hardware.comma.pins import GPIO
from openpilot.common.hardware.comma.amplifier import Amplifier
MODEM_STATE_PATH = "/dev/shm/modem"
@@ -58,7 +57,17 @@ def get_default_route_iface():
routes = [(int(route[6]), route[0]) for line in f.readlines()[1:] if (route := line.split())[1] == "00000000" and int(route[3], 16) & 0x1]
return min(routes)[1] if routes else None
class Tici(HardwareBase):
class HardwareComma(HardwareBase):
"""
This platform covers the Snapdragon 845-based comma devices:
- tici = comma three
- tizi = comma 3X
- mici = comma four
We strictly use only the device codenames in this codebase for
consistency, though all user-facing UI should use the product names.
"""
@cached_property
def amplifier(self):
if self.get_device_type() == "mici":
@@ -145,7 +154,8 @@ class Tici(HardwareBase):
}
def get_sim_lpa(self) -> LPABase:
return TiciLPA()
from openpilot.common.esim.lpa import LPA
return LPA()
def get_imei(self):
return self.get_modem_state().get('imei', '')
@@ -413,7 +423,7 @@ class Tici(HardwareBase):
return True
if __name__ == "__main__":
t = Tici()
t = HardwareComma()
t.initialize_hardware()
t.set_power_save(False)
print(t.get_sim_info())
@@ -3,17 +3,19 @@ import fcntl
import json
import logging
import os
import select
import signal
import struct
import subprocess
import tempfile
import termios
import time
from contextlib import contextmanager
from ipaddress import IPv4Address, AddressValueError
from enum import Enum
from openpilot.common.serial import Serial
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s.%(msecs)03d %(levelname)-7s modem: %(message)s",
@@ -64,6 +66,39 @@ INITIAL_STATE: dict[str, object] = {
}
@contextmanager
def _serial_port(port: str, baudrate: int):
fd = os.open(port, os.O_RDWR | os.O_NOCTTY)
try:
attrs = termios.tcgetattr(fd)
attrs[0] = 0
attrs[1] = 0
attrs[2] = termios.CLOCAL | termios.CREAD | termios.CS8
attrs[3] = 0
attrs[4] = attrs[5] = getattr(termios, f"B{baudrate}")
attrs[6][termios.VMIN] = 0
attrs[6][termios.VTIME] = 0
termios.tcsetattr(fd, termios.TCSANOW, attrs)
yield fd
finally:
os.close(fd)
def _read_line(fd: int, timeout: float) -> bytes:
data = bytearray()
deadline = time.monotonic() + timeout
while True:
readable, _, _ = select.select([fd], [], [], max(0.0, deadline - time.monotonic()))
if not readable:
return bytes(data)
byte = os.read(fd, 1)
if not byte:
return bytes(data)
data.extend(byte)
if byte == b"\n":
return bytes(data)
class State(Enum):
INITIALIZING = "INITIALIZING"
SEARCHING = "SEARCHING"
@@ -97,10 +132,11 @@ class PPPSession:
def reset_data_port():
"""Drop DTR on PPP_PORT so the modem terminates any stuck PPP session."""
try:
with Serial(PPP_PORT, baudrate=460800, timeout=1) as s:
s.dtr = False
with _serial_port(PPP_PORT, 460800) as fd:
dtr = struct.pack("I", termios.TIOCM_DTR)
fcntl.ioctl(fd, termios.TIOCMBIC, dtr)
time.sleep(0.2)
s.dtr = True
fcntl.ioctl(fd, termios.TIOCMBIS, dtr)
except Exception as e:
logging.warning(f"data port reset failed: {e}")
@@ -221,12 +257,14 @@ class Modem:
os.close(fd)
return []
try:
with Serial(AT_PORT, baudrate=9600, timeout=5) as ser:
ser.reset_input_buffer()
ser.write((cmd + "\r").encode())
with _serial_port(AT_PORT, 9600) as serial_fd:
termios.tcflush(serial_fd, termios.TCIFLUSH)
command = (cmd + "\r").encode()
while command:
command = command[os.write(serial_fd, command):]
lines = []
while True:
raw = ser.readline()
raw = _read_line(serial_fd, 5)
if not raw:
raise TimeoutError("AT timeout")
line = raw.decode(errors="ignore").strip()
@@ -6,7 +6,8 @@ TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
MANIFEST = os.path.join(TEST_DIR, "../agnos.json")
class TestAgnosUpdater:
from openpilot.common.test import OpenpilotTestCase
class TestAgnosUpdater(OpenpilotTestCase):
def test_manifest(self):
with open(MANIFEST) as f:
@@ -1,18 +1,17 @@
import pytest
#!/usr/bin/env python3
import time
import subprocess
import unittest
from panda import Panda
from openpilot.common.hardware import TICI, HARDWARE
from openpilot.common.hardware.tici.amplifier import Amplifier
from openpilot.common.test import OpenpilotTestCase
from openpilot.common.hardware import HARDWARE
from openpilot.common.hardware.comma.amplifier import Amplifier
class TestAmplifier:
@classmethod
def setup_class(cls):
if not TICI:
pytest.skip()
class TestAmplifier(OpenpilotTestCase):
COMMA_HARDWARE_TEST = True
def setup_method(self):
# clear dmesg
@@ -65,4 +64,8 @@ class TestAmplifier:
if self._check_for_i2c_errors(True):
break
else:
pytest.fail("didn't hit any i2c errors")
self.fail("didn't hit any i2c errors")
if __name__ == "__main__":
unittest.main()
+3 -3
View File
@@ -5,9 +5,9 @@
#include "common/hardware/base.h"
#include "common/util.h"
#if __TICI__
#include "common/hardware/tici/hardware.h"
#define Hardware HardwareTici
#if __COMMA_HARDWARE__
#include "common/hardware/comma/hardware.h"
#define Hardware HardwareComma
#else
#include "common/hardware/pc/hardware.h"
#define Hardware HardwarePC
+1 -1
View File
@@ -1,7 +1,7 @@
from openpilot.cereal import log
from openpilot.common.hardware.base import HardwareBase
class Pc(HardwareBase):
class HardwarePc(HardwareBase):
def get_device_type(self):
return "pc"
+18 -4
View File
@@ -1,8 +1,19 @@
import os
from pathlib import Path
CHESTNUT_VENDOR_ID = 0xADD1
CHESTNUT_PRODUCT_ID = 0x0001
CHESTNUT_FW_VERSION = "ed4e39b7"
CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
PRIMARY_USB_CONTROLLER = "a600000.ssusb"
def get_usb_topology() -> set[str]:
try:
return set(os.listdir(USB_DEVICES_PATH))
except OSError:
return set()
def read(path: Path) -> str | None:
@@ -15,7 +26,7 @@ def read(path: Path) -> str | None:
def read_int(path: Path, base: int = 10) -> int:
try:
return int(path.read_text(), base)
except (OSError, ValueError):
except (OSError, ValueError, TypeError):
return 0
@@ -36,6 +47,7 @@ def controller(device: Path) -> Path | None:
def get_usb_state() -> list[dict]:
devices = []
typec_orientation = read_int(TYPEC_CC_ORIENTATION_PATH)
for device in usb_devices():
vendor_id = read_int(device / "idVendor", 16)
product_id = read_int(device / "idProduct", 16)
@@ -49,6 +61,7 @@ def get_usb_state() -> list[dict]:
"manufacturer": read(device / "manufacturer") or "",
"product": read(device / "product") or "",
"linkErrorCount": read_int(ctrl / "portli", 0) & 0xFFFF if ctrl is not None else 0,
"usb3Lane": {1: "a", 2: "b"}.get(typec_orientation, "unknown") if ctrl is not None and ctrl.name == PRIMARY_USB_CONTROLLER else "unknown",
})
return devices
@@ -66,8 +79,9 @@ def set_usb_state(device_state, devices: list[dict]) -> None:
entry.manufacturer = device["manufacturer"]
entry.product = device["product"]
entry.linkErrorCount = device["linkErrorCount"]
entry.usb3Lane = device.get("usb3Lane", "unknown")
if (entry.vendorId, entry.productId) == (CHESTNUT_VENDOR_ID, CHESTNUT_PRODUCT_ID):
if (entry.vendorId, entry.productId) in CHESTNUT_USB_IDS:
chestnut_present = True
device_state.chestnutPresent = chestnut_present
+2 -2
View File
@@ -8,12 +8,12 @@ import functools
import threading
from openpilot.cereal.messaging import PubMaster
from openpilot.cereal.services import SERVICE_LIST
from openpilot.common.mock.generators import generate_livePose
from openpilot.common.mock.generators import generate_deviceMotion
from openpilot.common.realtime import Ratekeeper
MOCK_GENERATOR = {
"livePose": generate_livePose
"deviceMotion": generate_deviceMotion
}
+9 -9
View File
@@ -1,14 +1,14 @@
from openpilot.cereal import messaging
def generate_livePose():
msg = messaging.new_message('livePose')
def generate_deviceMotion():
msg = messaging.new_message('deviceMotion')
meas = {'x': 0.0, 'y': 0.0, 'z': 0.0, 'xStd': 0.0, 'yStd': 0.0, 'zStd': 0.0, 'valid': True}
msg.livePose.orientationNED = meas
msg.livePose.velocityDevice = meas
msg.livePose.angularVelocityDevice = meas
msg.livePose.accelerationDevice = meas
msg.livePose.inputsOK = True
msg.livePose.posenetOK = True
msg.livePose.sensorsOK = True
msg.deviceMotion.orientationNED = meas
msg.deviceMotion.velocityDevice = meas
msg.deviceMotion.angularVelocityDevice = meas
msg.deviceMotion.accelerationDevice = meas
msg.deviceMotion.inputsOK = True
msg.deviceMotion.posenetOK = True
msg.deviceMotion.sensorsOK = True
return msg
+56 -17
View File
@@ -1,7 +1,7 @@
import re
import sys
import pytest
import inspect
import unittest
def _to_safe_name(s):
@@ -10,22 +10,60 @@ def _to_safe_name(s):
class parameterized:
@staticmethod
def expand(cases):
def expand(cases, names=None, ids=None, serial=False):
cases = list(cases)
if not cases:
return lambda func: pytest.mark.skip("no parameterized cases")(func)
return lambda func: unittest.skip("no parameterized cases")(func)
def decorator(func):
params = [p for p in inspect.signature(func).parameters if p != 'self']
normalized = [c if isinstance(c, tuple) else (c,) for c in cases]
# Infer arg count from first case so extra params (e.g. from @given) are left untouched
expand_params = params[: len(normalized[0])]
if len(expand_params) == 1:
return pytest.mark.parametrize(expand_params[0], [c[0] for c in normalized])(func)
return pytest.mark.parametrize(', '.join(expand_params), normalized)(func)
if serial:
def decorator(func):
normalized = [case if isinstance(case, tuple) else (case,) for case in cases]
return decorator
def wrapper(self):
for case in normalized:
with self.subTest():
func(self, *case)
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper
return decorator
return lambda func: _Expanded(func, cases, names, ids)
class _Expanded:
"""Descriptor that turns every parameter case into a real unittest method."""
def __init__(self, func, cases, names, ids):
self.func = func
self.cases = [c if isinstance(c, tuple) else (c,) for c in cases]
self.names = names
self.ids = ids
def __set_name__(self, owner, name):
params = [p for p in inspect.signature(self.func).parameters if p != "self"]
for index, case in enumerate(self.cases):
label = self.ids(*case) if self.ids is not None else None
method_name = f"{name}_{index}" + (f"_{_to_safe_name(label)}" if label is not None else "")
def test_method(test_case, current_case=case):
if self.names is None:
self.func(test_case, *current_case)
else:
values = dict(zip(self.names, current_case, strict=True))
values.update({param: test_case._fixture(param) for param in params if param not in values})
self.func(test_case, **values)
test_method.__name__ = method_name
test_method.__doc__ = self.func.__doc__
setattr(owner, method_name, test_method)
# The descriptor itself is only a method factory, not a test.
setattr(owner, name, None)
def parameterized_class(attrs, input_list=None):
@@ -39,16 +77,17 @@ def parameterized_class(attrs, input_list=None):
def decorator(cls):
globs = sys._getframe(1).f_globals
for i, params in enumerate(params_list):
# append sanitized string param values so pytest -k can filter by them
# Append sanitized values so unittest's -k can filter by them.
suffix = "_".join(filter(None, (_to_safe_name(v) for v in params.values() if isinstance(v, str))))
name = f"{cls.__name__}_{i}" + (f"_{suffix}" if suffix else "")
new_cls = type(name, (cls,), dict(params))
new_cls.__module__ = cls.__module__
new_cls.__test__ = True # override inherited False so pytest collects this subclass
new_cls.__unittest_skip__ = False
new_cls.__unittest_skip_why__ = "" # else inherited from the base and the collector drops it
globs[name] = new_cls
# Don't collect the un-parametrised base, but return it so outer decorators
# (e.g. @pytest.mark.skip) land on it and propagate to subclasses via MRO.
cls.__test__ = False
# Don't collect the un-parametrised base.
cls.__unittest_skip__ = True
cls.__unittest_skip_why__ = "parameterized base class"
return cls
return decorator
+209 -7
View File
@@ -1,16 +1,218 @@
from openpilot.common.params_pyx import Params, ParamKeyFlag, ParamKeyType, UnknownKeyName
assert Params
assert ParamKeyFlag
assert ParamKeyType
assert UnknownKeyName
import sys
import json
import ctypes
import weakref
import builtins
import datetime
from pathlib import Path
from enum import IntEnum, IntFlag
from openpilot.common.swaglog import cloudlog
class ParamKeyFlag(IntFlag):
PERSISTENT = 0x02
CLEAR_ON_MANAGER_START = 0x04
CLEAR_ON_ONROAD_TRANSITION = 0x08
CLEAR_ON_OFFROAD_TRANSITION = 0x10
DEVELOPMENT_ONLY = 0x40
CLEAR_ON_IGNITION_ON = 0x80
BACKUP = 0x100
ALL = 0xFFFFFFFF
class ParamKeyType(IntEnum):
STRING = 0
BOOL = 1
INT = 2
FLOAT = 3
TIME = 4
JSON = 5
BYTES = 6
_suffix = ".dylib" if sys.platform == "darwin" else ".so"
lib = ctypes.CDLL(Path(__file__).with_name(f"libparams_c{_suffix}"))
ParamsHandle = ctypes.c_void_p
class ParamsBuffer(ctypes.Structure):
_fields_ = [("data", ctypes.c_void_p), ("size", ctypes.c_size_t)]
def _bind_raw(name, args, result=None):
function = getattr(lib, name)
function.argtypes = args
function.restype = result
return function
params_last_error = _bind_raw("params_last_error", [], ctypes.c_char_p)
def _bind(name, args, result=None):
function = _bind_raw(name, args, result)
def checked(*call_args):
value = function(*call_args)
if error := params_last_error():
raise RuntimeError(error.decode())
return value
return checked
params_create = _bind("params_create", [ctypes.c_char_p, ctypes.c_size_t], ParamsHandle)
params_destroy = _bind("params_destroy", [ParamsHandle])
params_clear_all = _bind("params_clear_all", [ParamsHandle, ctypes.c_uint])
params_check_key = _bind("params_check_key", [ParamsHandle, ctypes.c_char_p], ctypes.c_bool)
params_get_key_type = _bind("params_get_key_type", [ParamsHandle, ctypes.c_char_p], ctypes.c_int)
params_get_default = _bind("params_get_default", [ParamsHandle, ctypes.c_char_p], ParamsBuffer)
params_get = _bind("params_get", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool], ParamsBuffer)
params_get_bool = _bind("params_get_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool], ctypes.c_bool)
params_put = _bind("params_put", [ParamsHandle, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool], ctypes.c_int)
params_put_bool = _bind("params_put_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool, ctypes.c_bool], ctypes.c_int)
params_remove = _bind("params_remove", [ParamsHandle, ctypes.c_char_p], ctypes.c_int)
params_get_path = _bind("params_get_path", [ParamsHandle, ctypes.c_char_p, ctypes.c_size_t], ParamsBuffer)
params_keys_size = _bind("params_keys_size", [ParamsHandle], ctypes.c_size_t)
params_key_at = _bind("params_key_at", [ParamsHandle, ctypes.c_size_t], ParamsBuffer)
params_keys_by_flag = _bind("params_keys_by_flag", [ParamsHandle, ctypes.c_uint, ctypes.POINTER(ParamsBuffer), ctypes.c_size_t], ctypes.c_size_t)
PYTHON_2_CPP = {
(str, ParamKeyType.STRING): lambda v: v,
(builtins.bool, ParamKeyType.BOOL): lambda v: "1" if v else "0",
(int, ParamKeyType.INT): str,
(float, ParamKeyType.FLOAT): str,
(datetime.datetime, ParamKeyType.TIME): lambda v: v.isoformat(),
(dict, ParamKeyType.JSON): json.dumps,
(list, ParamKeyType.JSON): json.dumps,
(bytes, ParamKeyType.BYTES): lambda v: v,
}
CPP_2_PYTHON = {
ParamKeyType.STRING: lambda v: v.decode("utf-8"),
ParamKeyType.BOOL: lambda v: v == b"1",
ParamKeyType.INT: int,
ParamKeyType.FLOAT: float,
ParamKeyType.TIME: lambda v: datetime.datetime.fromisoformat(v.decode("utf-8")),
ParamKeyType.JSON: json.loads,
ParamKeyType.BYTES: lambda v: v,
}
def ensure_bytes(v):
return v.encode() if isinstance(v, str) else v
def _copy_string(value):
if value.data is None:
return None
return ctypes.string_at(value.data, value.size)
class UnknownKeyName(Exception):
pass
class Params:
def __init__(self, d=""):
path = ensure_bytes(d)
self.p = params_create(path, len(path))
self._finalizer = weakref.finalize(self, params_destroy, self.p)
self._finalizer.atexit = False # daemon threads can still be using live Params handles during interpreter shutdown
self.d = d
def __reduce__(self):
return (type(self), (self.d,))
def clear_all(self, tx_flag=ParamKeyFlag.ALL):
params_clear_all(self.p, int(tx_flag))
def check_key(self, key):
key = ensure_bytes(key)
if b"\0" in key or not params_check_key(self.p, key):
raise UnknownKeyName(key)
return key
def python2cpp(self, proposed_type, expected_type, value, key):
cast = PYTHON_2_CPP.get((proposed_type, expected_type))
if cast:
return cast(value)
raise TypeError(f"Type mismatch while writing param {key}: {proposed_type=} {expected_type=} {value=}")
def _cpp2python(self, t, value, default, key):
if value is None:
return None
try:
return CPP_2_PYTHON[t](value)
except (KeyError, TypeError, ValueError):
cloudlog.warning(f"Failed to cast param {key} with {value=} from type {t=}")
return self._cpp2python(t, default, None, key)
def _default(self, key):
return _copy_string(params_get_default(self.p, key))
def get(self, key, block=False, return_default=False):
k = self.check_key(key)
t = self.get_type(k)
default = self._default(k) if return_default else None
value = _copy_string(params_get(self.p, k, block))
if value == b"":
if block:
raise KeyboardInterrupt
return self._cpp2python(t, default, None, key)
return self._cpp2python(t, value, default, key)
def get_bool(self, key, block=False):
return bool(params_get_bool(self.p, self.check_key(key), block))
def _put_cast(self, key, dat):
return ensure_bytes(self.python2cpp(type(dat), self.get_type(key), dat, key))
def put(self, key, dat, block=False):
"""Write a parameter. block=True waits until it is persisted to disk."""
k = self.check_key(key)
value = self._put_cast(k, dat)
params_put(self.p, k, value, len(value), block)
def put_bool(self, key, val, block=False):
params_put_bool(self.p, self.check_key(key), val, block)
def remove(self, key):
params_remove(self.p, self.check_key(key))
def get_param_path(self, key=""):
key = ensure_bytes(key)
return _copy_string(params_get_path(self.p, key, len(key))).decode()
def get_type(self, key):
return ParamKeyType(params_get_key_type(self.p, self.check_key(key)))
def all_keys(self, flag=ParamKeyFlag.ALL):
if flag == ParamKeyFlag.ALL:
keys = []
for i in range(params_keys_size(self.p)):
keys.append(_copy_string(params_key_at(self.p, i)))
return keys
max_keys = 1024
buf = (ParamsBuffer * max_keys)()
count = params_keys_by_flag(self.p, int(flag), buf, max_keys)
return [_copy_string(buf[i]) for i in range(min(count, max_keys))]
def get_default_value(self, key):
k = self.check_key(key)
return self._cpp2python(self.get_type(k), self._default(k), None, key)
def cpp2python(self, key, value):
return self._cpp2python(self.get_type(key), value, None, key)
if __name__ == "__main__":
import sys
params = Params()
key = sys.argv[1]
assert params.check_key(key), f"unknown param: {key}"
params.check_key(key)
if len(sys.argv) == 3:
val = sys.argv[2]
print(f"SET: {key} = {val}")
+174
View File
@@ -0,0 +1,174 @@
#include <cstddef>
#include <cstdio>
#include <exception>
#include <string>
#include <utility>
#include <vector>
#include "common/params.h"
typedef struct {
const char *data;
size_t size;
} ParamsBuffer;
struct ParamsHandle {
ParamsHandle(const char *path, size_t path_size) : params(std::string(path, path_size)), keys(params.allKeys()) {
}
Params params;
const std::vector<std::string> keys;
};
namespace {
thread_local char last_error[512] = {};
thread_local std::string result;
void set_error(const char *error) {
snprintf(last_error, sizeof(last_error), "%s", error);
}
ParamsBuffer return_string(std::string value) {
result = std::move(value);
return {result.data(), result.size()};
}
template <typename Result, typename Callable>
Result translate_exceptions(Result failure, Callable &&callable) noexcept {
last_error[0] = '\0';
try {
return callable();
} catch (const std::exception &e) {
set_error(e.what());
} catch (...) {
set_error("unknown C++ exception");
}
return failure;
}
template <typename Callable>
void translate_exceptions(Callable &&callable) noexcept {
translate_exceptions(false, [&]() {
callable();
return true;
});
}
} // namespace
extern "C" {
ParamsHandle *params_create(const char *path, size_t path_size) noexcept {
return translate_exceptions(static_cast<ParamsHandle *>(nullptr), [&]() {
return new ParamsHandle(path, path_size);
});
}
void params_destroy(ParamsHandle *handle) noexcept {
translate_exceptions([&]() {
delete handle;
});
}
const char *params_last_error() noexcept {
return last_error;
}
void params_clear_all(ParamsHandle *handle, unsigned int flag) noexcept {
translate_exceptions([&]() {
handle->params.clearAll(static_cast<ParamKeyFlag>(flag));
});
}
bool params_check_key(ParamsHandle *handle, const char *key) noexcept {
return translate_exceptions(false, [&]() {
return handle->params.checkKey(key);
});
}
int params_get_key_type(ParamsHandle *handle, const char *key) noexcept {
return translate_exceptions(-1, [&]() {
return static_cast<int>(handle->params.getKeyType(key));
});
}
ParamsBuffer params_get_default(ParamsHandle *handle, const char *key) noexcept {
return translate_exceptions(ParamsBuffer{nullptr, 0}, [&]() {
auto value = handle->params.getKeyDefaultValue(key);
if (!value.has_value()) {
return ParamsBuffer{nullptr, 0};
}
return return_string(*value);
});
}
ParamsBuffer params_get(ParamsHandle *handle, const char *key, bool block) noexcept {
return translate_exceptions(ParamsBuffer{nullptr, 0}, [&]() {
return return_string(handle->params.get(key, block));
});
}
bool params_get_bool(ParamsHandle *handle, const char *key, bool block) noexcept {
return translate_exceptions(false, [&]() {
return handle->params.getBool(key, block);
});
}
int params_put(ParamsHandle *handle, const char *key, const char *value, size_t size, bool block) noexcept {
return translate_exceptions(-1, [&]() {
if (block) {
return handle->params.put(key, value, size);
}
handle->params.putNonBlocking(key, std::string(value, size));
return 0;
});
}
int params_put_bool(ParamsHandle *handle, const char *key, bool value, bool block) noexcept {
return translate_exceptions(-1, [&]() {
if (block) {
return handle->params.putBool(key, value);
}
handle->params.putBoolNonBlocking(key, value);
return 0;
});
}
int params_remove(ParamsHandle *handle, const char *key) noexcept {
return translate_exceptions(-1, [&]() {
return handle->params.remove(key);
});
}
ParamsBuffer params_get_path(ParamsHandle *handle, const char *key, size_t key_size) noexcept {
return translate_exceptions(ParamsBuffer{nullptr, 0}, [&]() {
return return_string(handle->params.getParamPath(std::string(key, key_size)));
});
}
size_t params_keys_size(ParamsHandle *handle) noexcept {
return translate_exceptions(size_t{0}, [&]() {
return handle->keys.size();
});
}
ParamsBuffer params_key_at(ParamsHandle *handle, size_t index) noexcept {
return translate_exceptions(ParamsBuffer{nullptr, 0}, [&]() {
if (index >= handle->keys.size()) {
return ParamsBuffer{nullptr, 0};
}
return return_string(handle->keys[index]);
});
}
size_t params_keys_by_flag(ParamsHandle *handle, uint32_t flag, ParamsBuffer *out, size_t out_size) noexcept {
return translate_exceptions(size_t{0}, [&]() {
auto filtered = handle->params.allKeys(static_cast<ParamKeyFlag>(flag));
size_t count = std::min(filtered.size(), out_size);
for (size_t i = 0; i < count; i++) {
out[i] = return_string(filtered[i]);
}
return filtered.size();
});
}
} // extern "C"
+4 -3
View File
@@ -106,7 +106,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"PandaHeartbeatLost", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"PrimeType", {PERSISTENT, INT}},
{"RecordAudio", {PERSISTENT | BACKUP, BOOL}},
{"RecordAudioFeedback", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RecordFront", {PERSISTENT | BACKUP, BOOL}},
{"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet
{"SecOCKey", {PERSISTENT | DONT_LOG | BACKUP, STRING}},
@@ -131,8 +130,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"UpdaterLastFetchTime", {PERSISTENT, TIME}},
{"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}},
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
{"UsbGpuPresent", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"UsbGpuCompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"UsbGpuActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"UsbGpuLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"Version", {PERSISTENT, STRING}},
// --- sunnypilot params --- //
@@ -200,7 +199,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT}},
{"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}},
{"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_USBGPU", {PERSISTENT | BACKUP, JSON}},
// Neural Network Lateral Control
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
-192
View File
@@ -1,192 +0,0 @@
# distutils: language = c++
# cython: language_level = 3
import builtins
import datetime
import json
from libcpp cimport bool
from libcpp.string cimport string
from libcpp.vector cimport vector
from libcpp.optional cimport optional
from openpilot.common.swaglog import cloudlog
cdef extern from "common/params.h":
cpdef enum ParamKeyFlag:
PERSISTENT
CLEAR_ON_MANAGER_START
CLEAR_ON_ONROAD_TRANSITION
CLEAR_ON_OFFROAD_TRANSITION
DEVELOPMENT_ONLY
CLEAR_ON_IGNITION_ON
BACKUP
ALL
cpdef enum ParamKeyType:
STRING
BOOL
INT
FLOAT
TIME
JSON
BYTES
cdef cppclass c_Params "Params":
c_Params(string) except + nogil
string get(string, bool) nogil
bool getBool(string, bool) nogil
int remove(string) nogil
int put(string, string) nogil
void putNonBlocking(string, string) nogil
void putBoolNonBlocking(string, bool) nogil
int putBool(string, bool) nogil
bool checkKey(string) nogil
ParamKeyType getKeyType(string) nogil
optional[string] getKeyDefaultValue(string) nogil
string getParamPath(string) nogil
void clearAll(ParamKeyFlag)
vector[string] allKeys(ParamKeyFlag)
PYTHON_2_CPP = {
(str, STRING): lambda v: v,
(builtins.bool, BOOL): lambda v: "1" if v else "0",
(int, INT): str,
(float, FLOAT): str,
(datetime.datetime, TIME): lambda v: v.isoformat(),
(dict, JSON): json.dumps,
(list, JSON): json.dumps,
(bytes, BYTES): lambda v: v,
}
CPP_2_PYTHON = {
STRING: lambda v: v.decode("utf-8"),
BOOL: lambda v: v == b"1",
INT: int,
FLOAT: float,
TIME: lambda v: datetime.datetime.fromisoformat(v.decode("utf-8")),
JSON: json.loads,
BYTES: lambda v: v,
}
def ensure_bytes(v):
return v.encode() if isinstance(v, str) else v
class UnknownKeyName(Exception):
pass
cdef class Params:
cdef c_Params* p
cdef str d
def __cinit__(self, d=""):
cdef string path = <string>d.encode()
with nogil:
self.p = new c_Params(path)
self.d = d
def __reduce__(self):
return (type(self), (self.d,))
def __dealloc__(self):
del self.p
def clear_all(self, tx_flag=ParamKeyFlag.ALL):
self.p.clearAll(tx_flag)
def check_key(self, key):
key = ensure_bytes(key)
if not self.p.checkKey(key):
raise UnknownKeyName(key)
return key
def python2cpp(self, proposed_type, expected_type, value, key):
cast = PYTHON_2_CPP.get((proposed_type, expected_type))
if cast:
return cast(value)
raise TypeError(f"Type mismatch while writing param {key}: {proposed_type=} {expected_type=} {value=}")
def _cpp2python(self, t, value, default, key):
if value is None:
return None
try:
return CPP_2_PYTHON[t](value)
except (KeyError, TypeError, ValueError):
cloudlog.warning(f"Failed to cast param {key} with {value=} from type {t=}")
return self._cpp2python(t, default, None, key)
def get(self, key, bool block=False, bool return_default=False):
cdef string k = self.check_key(key)
cdef ParamKeyType t = self.p.getKeyType(k)
cdef optional[string] default = self.p.getKeyDefaultValue(k)
cdef string val
with nogil:
val = self.p.get(k, block)
default_val = (default.value() if default.has_value() else None) if return_default else None
if val == b"":
if block:
# If we got no value while running in blocked mode
# it means we got an interrupt while waiting
raise KeyboardInterrupt
else:
return self._cpp2python(t, default_val, None, key)
return self._cpp2python(t, val, default_val, key)
def get_bool(self, key, bool block=False):
cdef string k = self.check_key(key)
cdef bool r
with nogil:
r = self.p.getBool(k, block)
return r
def _put_cast(self, key, dat):
cdef string k = self.check_key(key)
cdef ParamKeyType t = self.p.getKeyType(k)
return ensure_bytes(self.python2cpp(type(dat), t, dat, key))
def put(self, key, dat, bool block = False):
"""
Warning: block=True blocks until the param is written to disk!
In very rare cases this can take over a second, and your code will hang.
Use block=False in time sensitive code, but in general try to avoid
writing params as much as possible.
"""
cdef string k = self.check_key(key)
cdef string dat_bytes = self._put_cast(key, dat)
with nogil:
if block:
self.p.put(k, dat_bytes)
else:
self.p.putNonBlocking(k, dat_bytes)
def put_bool(self, key, bool val, bool block = False):
cdef string k = self.check_key(key)
with nogil:
if block:
self.p.putBool(k, val)
else:
self.p.putBoolNonBlocking(k, val)
def remove(self, key):
cdef string k = self.check_key(key)
with nogil:
self.p.remove(k)
def get_param_path(self, key=""):
cdef string key_bytes = ensure_bytes(key)
return self.p.getParamPath(key_bytes).decode("utf-8")
def get_type(self, key):
return self.p.getKeyType(self.check_key(key))
def all_keys(self, flag=ParamKeyFlag.ALL):
return self.p.allKeys(flag)
def get_default_value(self, key):
cdef string k = self.check_key(key)
cdef ParamKeyType t = self.p.getKeyType(k)
cdef optional[string] default = self.p.getKeyDefaultValue(k)
return self._cpp2python(t, default.value(), None, key) if default.has_value() else None
def cpp2python(self, key, value):
cdef string k = self.check_key(key)
cdef ParamKeyType t = self.p.getKeyType(k)
return self._cpp2python(t, value, None, key)
+218
View File
@@ -0,0 +1,218 @@
"""Small QR encoder for the UI's byte-mode, error-correction-level-L codes."""
import numpy as np
import pyray as rl
# Indexes are QR versions. These are the only two Reed-Solomon parameters needed
# for error-correction level L.
_ECC_LEN = (0, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28)
_NUM_BLOCKS = (0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8)
# 15 format-info bits for level L (01) with mask 0: ((0x08 << 10) | bch_remainder) ^ 0x5412
_FORMAT_BITS = 0b111011111000100
def _raw_modules(version: int) -> int:
result = (16 * version + 128) * version + 64
if version >= 2:
align = version // 7 + 2
result -= (25 * align - 10) * align - 55
return result - (36 if version >= 7 else 0)
def _capacity(version: int) -> int:
return _raw_modules(version) // 8 - _ECC_LEN[version] * _NUM_BLOCKS[version]
def _append_bits(bits: list[int], value: int, length: int) -> None:
bits.extend((value >> i) & 1 for i in range(length - 1, -1, -1))
def _data_codewords(data: bytes, version: int) -> bytes:
"""Byte-mode-encode the payload, terminated and padded to the version's capacity."""
capacity = _capacity(version)
bits: list[int] = []
_append_bits(bits, 4, 4) # byte mode
_append_bits(bits, len(data), 8 if version <= 9 else 16)
for value in data:
_append_bits(bits, value, 8)
bits.extend([0] * min(4, capacity * 8 - len(bits))) # terminator
bits.extend([0] * (-len(bits) % 8)) # byte alignment
result = bytearray(sum(bits[i + j] << (7 - j) for j in range(8)) for i in range(0, len(bits), 8))
pad = (0xEC, 0x11)
while len(result) < capacity:
result.append(pad[(len(result) - (len(bits) // 8)) & 1])
return bytes(result)
def _codewords(data: bytes, version: int) -> bytes:
"""Split data codewords into Reed-Solomon blocks and interleave data + ECC."""
data = _data_codewords(data, version)
num_blocks = _NUM_BLOCKS[version]
ecc_len = _ECC_LEN[version]
raw_codewords = _raw_modules(version) // 8
short_len = raw_codewords // num_blocks
num_short = num_blocks - raw_codewords % num_blocks
divisor = _divisor(ecc_len)
blocks: list[tuple[bytes, bytes]] = []
offset = 0
for i in range(num_blocks):
length = short_len - ecc_len + (0 if i < num_short else 1)
block = data[offset:offset + length]
blocks.append((block, _remainder(block, divisor)))
offset += length
result = bytearray()
for i in range(short_len - ecc_len + 1):
for block, _ in blocks:
result.extend(block[i:i + 1])
for i in range(ecc_len):
for _, ecc in blocks:
result.append(ecc[i])
return bytes(result)
def _multiply(x: int, y: int) -> int:
result = 0
for _ in range(8):
result = (result << 1) ^ (0x11D if result & 0x80 else 0)
if y & 0x80:
result ^= x
y <<= 1
return result
def _divisor(degree: int) -> bytes:
result = bytearray([0] * (degree - 1) + [1])
root = 1
for _ in range(degree):
for j in range(degree):
result[j] = _multiply(result[j], root)
if j + 1 < degree:
result[j] ^= result[j + 1]
root = _multiply(root, 2)
return bytes(result)
def _remainder(data: bytes, divisor: bytes) -> bytes:
result = bytearray(len(divisor))
for value in data:
factor = value ^ result.pop(0)
result.append(0)
for i, coefficient in enumerate(divisor):
result[i] ^= _multiply(coefficient, factor)
return bytes(result)
def _alignment_positions(version: int) -> list[int]:
if version == 1:
return []
count = version // 7 + 2
step = ((version * 4 + count * 2 + 1) // (count * 2 - 2)) * 2
return [6] + [version * 4 + 10 - step * i for i in range(count - 1)][::-1]
class _Qr:
def __init__(self, version: int, data: bytes):
self.version = version
self.size = version * 4 + 17
self.modules = [[False] * self.size for _ in range(self.size)]
self.function = [[False] * self.size for _ in range(self.size)]
self._draw_functions()
self._draw_data(_codewords(data, version))
for y in range(self.size):
for x in range(self.size):
if not self.function[y][x]:
self.modules[y][x] ^= (x + y) % 2 == 0
self._format()
def _set_function(self, x: int, y: int, dark: bool) -> None:
if 0 <= x < self.size and 0 <= y < self.size:
self.modules[y][x] = dark
self.function[y][x] = True
def _finder(self, x: int, y: int) -> None:
for dy in range(-4, 5):
for dx in range(-4, 5):
distance = max(abs(dx), abs(dy))
self._set_function(x + dx, y + dy, distance != 2 and distance != 4)
def _alignment(self, x: int, y: int) -> None:
for dy in range(-2, 3):
for dx in range(-2, 3):
self._set_function(x + dx, y + dy, max(abs(dx), abs(dy)) != 1)
def _draw_functions(self) -> None:
for i in range(self.size):
self._set_function(6, i, i % 2 == 0)
self._set_function(i, 6, i % 2 == 0)
self._finder(3, 3)
self._finder(self.size - 4, 3)
self._finder(3, self.size - 4)
positions = _alignment_positions(self.version)
for y in positions:
for x in positions:
if not ((x == 6 and y in (6, self.size - 7)) or (x == self.size - 7 and y == 6)):
self._alignment(x, y)
# reserve the format-info modules before the data is placed; the real
# values are written by the second _format call after masking
self._format()
if self.version >= 7:
value = self.version
for _ in range(12):
value = (value << 1) ^ ((value >> 11) * 0x1F25)
value = self.version << 12 | value
for i in range(18):
bit = ((value >> i) & 1) != 0
a = self.size - 11 + i % 3
b = i // 3
self._set_function(a, b, bit)
self._set_function(b, a, bit)
def _format(self) -> None:
for i in range(15):
bit = ((_FORMAT_BITS >> i) & 1) != 0
y_pos = i if i < 6 else i + 1 if i < 8 else self.size - 15 + i
self._set_function(8, y_pos, bit)
x_pos = self.size - 1 - i if i < 8 else 15 - i if i < 9 else 14 - i
self._set_function(x_pos, 8, bit)
self._set_function(8, self.size - 8, True)
def _draw_data(self, data: bytes) -> None:
bits = ((byte >> s) & 1 for byte in data for s in reversed(range(8)))
upward = True
right = self.size - 1
while right >= 1:
if right == 6: # skip the vertical timing column
right = 5
for vert in range(self.size):
y = self.size - 1 - vert if upward else vert
for x in (right, right - 1):
if not self.function[y][x]:
self.modules[y][x] = bool(next(bits, 0))
upward = not upward
right -= 2
def make_texture(data: str, inverted: bool = False) -> rl.Texture:
"""Render a URL as the RGBA QR texture used by the UI. The texture upload
copies the pixels, so the intermediate image/array don't need to outlive it."""
raw = data.encode()
for version in range(1, 21):
count_bits = 8 if version <= 9 else 16
if 4 + count_bits + len(raw) * 8 <= _capacity(version) * 8:
break
else:
raise ValueError("QR URL is too long")
modules = np.pad(_Qr(version, raw).modules, 0 if inverted else 4)
modules = np.repeat(np.repeat(modules, 10, axis=0), 10, axis=1)
gray = ((modules == inverted) * 255).astype(np.uint8)
img_array = np.dstack((gray, gray, gray, np.full_like(gray, 255)))
rl_image = rl.Image()
rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data)
rl_image.width = img_array.shape[1]
rl_image.height = img_array.shape[0]
rl_image.mipmaps = 1
rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
return rl.load_texture_from_image(rl_image)
-3
View File
@@ -48,9 +48,6 @@ class RunningStat:
def std(self):
return np.sqrt(self.variance())
def params_to_save(self):
return [self.M, self.S, self.n]
class RunningStatFilter:
def __init__(self, raw_priors=None, filtered_priors=None, max_trackable=-1):
self.raw_stat = RunningStat(raw_priors, -1)
+2 -1
View File
@@ -45,7 +45,8 @@ class SwaglogRotatingFileHandler(BaseRotatingHandler):
fp = os.path.join(base_dir, fn)
if fp.startswith(self.base_filename) and os.path.isfile(fp):
log_files.append(fp)
return sorted(log_files)
# newest first, matching _open()'s insert(0, ...) so doRollover()'s pop() deletes the oldest
return sorted(log_files, reverse=True)
def shouldRollover(self, record):
size_exceeded = self.max_bytes > 0 and self.stream.tell() >= self.max_bytes
+166
View File
@@ -0,0 +1,166 @@
import contextlib
import gc
import inspect
import os
import subprocess
import unittest
from unittest import mock
from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.system.manager import manager
@contextlib.contextmanager
def clean_env():
starting_env = dict(os.environ)
try:
yield
finally:
os.environ.clear()
os.environ.update(starting_env)
class OpenpilotTestCase(unittest.TestCase):
"""TestCase with openpilot's per-test isolation."""
COMMA_HARDWARE_TEST = False
SHARED_DOWNLOAD_CACHE = False
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Preserve legacy xunit hooks and invoke them inside the prefix below.
for name in ("setup_method", "teardown_method"):
hook = cls.__dict__.get(name)
if hook is not None:
setattr(cls, f"openpilot_{name}", hook)
setattr(cls, name, None)
def _fixture(self, name):
if name == "mocker":
return Mocker(self.addCleanup)
if name == "monkeypatch":
return MonkeyPatch(self.addCleanup)
if name == "subtests":
return SubTests(self)
fixture = getattr(inspect.getmodule(type(self)), name)
kwargs = {p: self._fixture(p) for p in inspect.signature(fixture).parameters}
value = fixture(**kwargs)
if inspect.isgenerator(value):
generator = value
value = next(generator)
self.addCleanup(lambda: next(generator, None))
return value
def _callTestMethod(self, method):
params = [name for name, param in inspect.signature(method).parameters.items()
if param.default is inspect.Parameter.empty]
return method(**{name: self._fixture(name) for name in params})
def run(self, result=None):
# This boundary cannot live in setUp/tearDown: existing unittest classes
# are allowed to override those hooks without calling super().
if (self.COMMA_HARDWARE_TEST and not COMMA_HARDWARE) or getattr(type(self), "__unittest_skip__", False):
return super().run(result)
test_env = clean_env()
test_env.__enter__()
prefix = OpenpilotPrefix(shared_download_cache=self.SHARED_DOWNLOAD_CACHE)
prefix.__enter__()
try:
return super().run(result)
finally:
prefix.__exit__(None, None, None)
manager.manager_cleanup()
if not gc.isenabled():
gc.enable()
gc.collect()
test_env.__exit__(None, None, None)
@classmethod
def setUpClass(cls):
super().setUpClass()
if cls.COMMA_HARDWARE_TEST and not COMMA_HARDWARE:
raise unittest.SkipTest("Skipping comma hardware test on PC")
cls._class_env = clean_env()
cls._class_env.__enter__()
setup_class = getattr(cls, "setup_class", None)
if setup_class is not None:
setup_class()
@classmethod
def tearDownClass(cls):
try:
teardown_class = getattr(cls, "teardown_class", None)
if teardown_class is not None:
teardown_class()
finally:
cls._class_env.__exit__(None, None, None)
super().tearDownClass()
def setUp(self):
super().setUp()
if self.COMMA_HARDWARE_TEST and not COMMA_HARDWARE:
self.skipTest("Skipping comma hardware test on PC")
if self.COMMA_HARDWARE_TEST:
HARDWARE.initialize_hardware()
HARDWARE.set_power_save(False)
subprocess.run(["pkill", "-9", "-f", "athena"], check=False)
setup_method = getattr(self, "openpilot_setup_method", None)
if setup_method is not None:
setup_method()
def tearDown(self):
try:
teardown_method = getattr(self, "openpilot_teardown_method", None)
if teardown_method is not None:
teardown_method()
finally:
super().tearDown()
class Mocker:
Mock = mock.Mock
MagicMock = mock.MagicMock
call = mock.call
ANY = mock.ANY
def __init__(self, add_cleanup):
self._add_cleanup = add_cleanup
self.patch = Patch(self._start)
def _start(self, patcher):
value = patcher.start()
self._add_cleanup(patcher.stop)
return value
class Patch:
def __init__(self, start):
self._start = start
def __call__(self, *args, **kwargs):
return self._start(mock.patch(*args, **kwargs))
def object(self, *args, **kwargs):
return self._start(mock.patch.object(*args, **kwargs))
class MonkeyPatch:
def __init__(self, add_cleanup):
self._add_cleanup = add_cleanup
def setattr(self, target, name, value):
patcher = mock.patch.object(target, name, value)
patcher.start()
self._add_cleanup(patcher.stop)
class SubTests:
def __init__(self, test_case):
self._test_case = test_case
def test(self, label=None, **kwargs):
return self._test_case.subTest(**kwargs) if label is None else self._test_case.subTest(label, **kwargs)
+1
View File
@@ -1 +1,2 @@
test_common
test_swaglog
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <iostream>
#include <stdexcept>
#include <string>
inline void native_test_check(bool condition, const char *expression, const char *file, int line) {
if (!condition) {
throw std::runtime_error(std::string(file) + ":" + std::to_string(line) + ": check failed: " + expression);
}
}
#define CHECK(condition) native_test_check(static_cast<bool>(condition), #condition, __FILE__, __LINE__)
#define REQUIRE(...) CHECK((__VA_ARGS__))
template <typename Function>
int run_native_test(Function &&function) {
try {
function();
return 0;
} catch (const std::exception &error) {
std::cerr << error.what() << '\n';
return 1;
}
}
+2 -1
View File
@@ -1,10 +1,11 @@
import os
from uuid import uuid4
from openpilot.common.test import OpenpilotTestCase
from openpilot.common.utils import atomic_write
class TestFileHelpers:
class TestFileHelpers(OpenpilotTestCase):
def run_atomic_write_func(self, atomic_write_func):
path = f"/tmp/tmp{uuid4()}"
with atomic_write_func(path) as f:
+2 -1
View File
@@ -1,10 +1,11 @@
import os
from openpilot.common.test import OpenpilotTestCase
from openpilot.common.basedir import BASEDIR
from openpilot.common.markdown import parse_markdown
class TestMarkdown:
class TestMarkdown(OpenpilotTestCase):
def test_all_release_notes(self):
with open(os.path.join(BASEDIR, "CHANGELOG.md")) as f:
release_notes = f.read().split("\n\n")
+11 -6
View File
@@ -1,13 +1,13 @@
import pytest
import datetime
import os
import threading
import time
import uuid
from openpilot.common.test import OpenpilotTestCase
from openpilot.common.params import Params, ParamKeyFlag, UnknownKeyName
class TestParams:
class TestParams(OpenpilotTestCase):
def setup_method(self):
self.params = Params()
@@ -50,18 +50,23 @@ class TestParams:
assert self.params.get("CarParams", block=True) == b"test"
def test_params_unknown_key_fails(self):
with pytest.raises(UnknownKeyName):
with self.assertRaises(UnknownKeyName):
self.params.get("swag")
with pytest.raises(UnknownKeyName):
with self.assertRaises(UnknownKeyName):
self.params.get_bool("swag")
with pytest.raises(UnknownKeyName):
with self.assertRaises(UnknownKeyName):
self.params.put("swag", "abc", block=True)
with pytest.raises(UnknownKeyName):
with self.assertRaises(UnknownKeyName):
self.params.put_bool("swag", True, block=True)
with self.assertRaises(UnknownKeyName):
self.params.put(b"DongleId\0suffix", "abc", block=True)
assert self.params.get_param_path(b"key\0suffix").endswith("/key\0suffix")
def test_remove_not_there(self):
assert self.params.get("CarParams") is None
self.params.remove("CarParams")
-2
View File
@@ -1,2 +0,0 @@
#define CATCH_CONFIG_MAIN
#include "catch2/catch.hpp"
+2 -1
View File
@@ -1,7 +1,8 @@
from openpilot.common.test import OpenpilotTestCase
from openpilot.common.simple_kalman import KF1D
class TestSimpleKalman:
class TestSimpleKalman(OpenpilotTestCase):
def setup_method(self):
dt = 0.01
x0_0 = 0.0
+52 -40
View File
@@ -1,12 +1,11 @@
#include <cstdlib>
#include <string>
#include <zmq.h>
#include <iostream>
#include "catch2/catch.hpp"
#include "common/swaglog.h"
#include "common/util.h"
#include "common/version.h"
#include "common/hardware/hw.h"
#include "common/swaglog.h"
#include "common/tests/native_test.h"
#include "json11/json11.hpp"
#include "sunnypilot/common/version.h"
@@ -15,12 +14,38 @@ std::string daemon_name = "testy";
std::string dongle_id = "test_dongle_id";
int LINE_NO = 0;
void log_thread(int thread_id, int msg_cnt) {
for (int i = 0; i < msg_cnt; ++i) {
LOGD("%d", thread_id);
LINE_NO = __LINE__ - 1;
usleep(1);
}
void test_swaglog() {
setenv("MANAGER_DAEMON", "swaglog_test", 1);
setenv("DONGLE_ID", "test_dongle_id", 1);
setenv("CLEAN", "1", 1);
void *context = zmq_ctx_new();
CHECK(context != nullptr);
void *socket = zmq_socket(context, ZMQ_PULL);
CHECK(socket != nullptr);
int timeout = 5000;
CHECK(zmq_setsockopt(socket, ZMQ_RCVTIMEO, &timeout, sizeof(timeout)) == 0);
CHECK(zmq_bind(socket, Path::swaglog_ipc().c_str()) == 0);
LOGD("native-cpp-log");
char buffer[4096] = {};
const int size = zmq_recv(socket, buffer, sizeof(buffer), 0);
CHECK(size > 1);
CHECK(buffer[0] == CLOUDLOG_DEBUG);
std::string error;
const auto message = json11::Json::parse(std::string(buffer + 1, size - 1), error);
CHECK(error.empty());
CHECK(message["levelnum"].int_value() == CLOUDLOG_DEBUG);
CHECK(message["msg"].string_value() == "native-cpp-log");
CHECK(message["funcname"].string_value() == "test_swaglog");
CHECK(message["filename"].string_value().find("test_swaglog.cc") != std::string::npos);
CHECK(message["ctx"]["daemon"].string_value() == "swaglog_test");
CHECK(message["ctx"]["dongle_id"].string_value() == "test_dongle_id");
CHECK(message["ctx"]["dirty"].bool_value() == false);
CHECK(zmq_close(socket) == 0);
CHECK(zmq_ctx_destroy(context) == 0);
}
void recv_log(int thread_cnt, int thread_msg_cnt) {
@@ -39,52 +64,39 @@ void recv_log(int thread_cnt, int thread_msg_cnt) {
break;
}
REQUIRE(buf[0] == CLOUDLOG_DEBUG);
CHECK(buf[0] == CLOUDLOG_DEBUG);
std::string err;
auto msg = json11::Json::parse(buf + 1, err);
REQUIRE(!msg.is_null());
CHECK(!msg.is_null());
REQUIRE(msg["levelnum"].int_value() == CLOUDLOG_DEBUG);
REQUIRE_THAT(msg["filename"].string_value(), Catch::Contains("test_swaglog.cc"));
REQUIRE(msg["funcname"].string_value() == "log_thread");
REQUIRE(msg["lineno"].int_value() == LINE_NO);
CHECK(msg["levelnum"].int_value() == CLOUDLOG_DEBUG);
CHECK(msg["filename"].string_value().find("test_swaglog.cc") != std::string::npos);
CHECK(msg["funcname"].string_value() == "log_thread");
CHECK(msg["lineno"].int_value() == LINE_NO);
auto ctx = msg["ctx"];
REQUIRE(ctx["daemon"].string_value() == daemon_name);
REQUIRE(ctx["dongle_id"].string_value() == dongle_id);
REQUIRE(ctx["dirty"].bool_value() == true);
CHECK(ctx["daemon"].string_value() == daemon_name);
CHECK(ctx["dongle_id"].string_value() == dongle_id);
CHECK(ctx["dirty"].bool_value() == true);
REQUIRE(ctx["version"].string_value() == SUNNYPILOT_VERSION);
CHECK(ctx["version"].string_value() == SUNNYPILOT_VERSION);
std::string device = Hardware::get_name();
REQUIRE(ctx["device"].string_value() == device);
CHECK(ctx["device"].string_value() == device);
int thread_id = atoi(msg["msg"].string_value().c_str());
REQUIRE((thread_id >= 0 && thread_id < thread_cnt));
CHECK((thread_id >= 0 && thread_id < thread_cnt));
thread_msgs[thread_id]++;
total_count++;
}
for (int i = 0; i < thread_cnt; ++i) {
INFO("thread :" << i);
REQUIRE(thread_msgs[i] == thread_msg_cnt);
CHECK(thread_msgs[i] == thread_msg_cnt);
}
zmq_close(sock);
zmq_ctx_destroy(zctx);
}
TEST_CASE("swaglog") {
setenv("MANAGER_DAEMON", daemon_name.c_str(), 1);
setenv("DONGLE_ID", dongle_id.c_str(), 1);
setenv("dirty", "1", 1);
const int thread_cnt = 5;
const int thread_msg_cnt = 100;
std::vector<std::thread> log_threads;
for (int i = 0; i < thread_cnt; ++i) {
log_threads.push_back(std::thread(log_thread, i, thread_msg_cnt));
}
for (auto &t : log_threads) t.join();
recv_log(thread_cnt, thread_msg_cnt);
int main() {
return run_native_test(test_swaglog);
}
-147
View File
@@ -1,147 +0,0 @@
#include <dirent.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <algorithm>
#include <climits>
#include <fstream>
#include <random>
#include <string>
#include "catch2/catch.hpp"
#include "common/util.h"
std::string random_bytes(int size) {
std::random_device rd;
std::independent_bits_engine<std::default_random_engine, CHAR_BIT, unsigned char> rbe(rd());
std::string bytes(size + 1, '\0');
std::generate(bytes.begin(), bytes.end(), std::ref(rbe));
return bytes;
}
TEST_CASE("util::read_file") {
SECTION("read /proc/version") {
std::string ret = util::read_file("/proc/version");
REQUIRE(ret.find("Linux version") != std::string::npos);
}
SECTION("read from sysfs") {
std::string ret = util::read_file("/sys/power/wakeup_count");
REQUIRE(!ret.empty());
}
SECTION("read file") {
char filename[] = "/tmp/test_read_XXXXXX";
int fd = mkstemp(filename);
REQUIRE(util::read_file(filename).empty());
std::string content = random_bytes(64 * 1024);
REQUIRE(write(fd, content.c_str(), content.size()) == (ssize_t)content.size());
std::string ret = util::read_file(filename);
bool equal = (ret == content);
REQUIRE(equal);
close(fd);
}
SECTION("read directory") {
REQUIRE(util::read_file(".").empty());
}
SECTION("read non-existent file") {
std::string ret = util::read_file("does_not_exist");
REQUIRE(ret.empty());
}
SECTION("read non-permission") {
REQUIRE(util::read_file("/proc/kmsg").empty());
}
}
TEST_CASE("util::file_exists") {
char filename[] = "/tmp/test_file_exists_XXXXXX";
int fd = mkstemp(filename);
REQUIRE(fd != -1);
close(fd);
SECTION("existent file") {
REQUIRE(util::file_exists(filename));
REQUIRE(util::file_exists("/tmp"));
}
SECTION("nonexistent file") {
std::string fn = filename;
REQUIRE(!util::file_exists(fn + "/nonexistent"));
}
SECTION("file has no access permissions") {
std::string fn = "/proc/kmsg";
std::ifstream f(fn);
REQUIRE(f.good() == false);
REQUIRE(util::file_exists(fn));
}
::remove(filename);
}
TEST_CASE("util::read_files_in_dir") {
char tmp_path[] = "/tmp/test_XXXXXX";
const std::string test_path = mkdtemp(tmp_path);
const std::string files[] = {".test1", "'test2'", "test3"};
for (auto fn : files) {
std::ofstream{test_path + "/" + fn} << fn;
}
mkdir((test_path + "/dir").c_str(), 0777);
std::map<std::string, std::string> result = util::read_files_in_dir(test_path);
REQUIRE(result.find("dir") == result.end());
REQUIRE(result.size() == std::size(files));
for (auto& [k, v] : result) {
REQUIRE(k == v);
}
}
TEST_CASE("util::safe_fwrite") {
char filename[] = "/tmp/XXXXXX";
int fd = mkstemp(filename);
close(fd);
std::string dat = random_bytes(1024 * 1024);
FILE *f = util::safe_fopen(filename, "wb");
REQUIRE(f != nullptr);
size_t size = util::safe_fwrite(dat.data(), 1, dat.size(), f);
REQUIRE(size == dat.size());
int ret = util::safe_fflush(f);
REQUIRE(ret == 0);
ret = fclose(f);
REQUIRE(ret == 0);
bool equal = (dat == util::read_file(filename));
REQUIRE(equal);
}
TEST_CASE("util::create_directories") {
REQUIRE(system("rm /tmp/test_create_directories -rf") == 0);
std::string dir = "/tmp/test_create_directories/a/b/c/d/e/f";
auto check_dir_permissions = [](const std::string &path, mode_t mode) -> bool {
struct stat st = {};
return stat(path.c_str(), &st) == 0 && (st.st_mode & S_IFMT) == S_IFDIR && (st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)) == mode;
};
SECTION("create_directories") {
REQUIRE(util::create_directories(dir, 0755));
REQUIRE(check_dir_permissions(dir, 0755));
}
SECTION("dir already exists") {
REQUIRE(util::create_directories(dir, 0755));
REQUIRE(util::create_directories(dir, 0755));
}
SECTION("a file exists with the same name") {
REQUIRE(util::create_directories(dir, 0755));
int f = open((dir + "/file").c_str(), O_RDWR | O_CREAT, 0644);
REQUIRE(f != -1);
close(f);
REQUIRE(util::create_directories(dir + "/file", 0755) == false);
REQUIRE(util::create_directories(dir + "/file/1/2/3", 0755) == false);
}
SECTION("end with slashes") {
REQUIRE(util::create_directories(dir + "/", 0755));
}
SECTION("empty") {
REQUIRE(util::create_directories("", 0755) == false);
}
}
+4 -4
View File
@@ -37,12 +37,12 @@ class _NoneCameraConfig(CameraConfig):
@dataclass(frozen=True)
class DeviceCameraConfig:
fcam: CameraConfig
dcam: CameraConfig
ecam: CameraConfig
narrow_road: CameraConfig
cabin: CameraConfig
wide_road: CameraConfig
def all_cams(self):
for cam in ['fcam', 'dcam', 'ecam']:
for cam in ['narrow_road', 'cabin', 'wide_road']:
if not isinstance(getattr(self, cam), _NoneCameraConfig):
yield cam, getattr(self, cam)
@@ -1,5 +1,6 @@
import numpy as np
from openpilot.common.test import OpenpilotTestCase
import openpilot.common.transformations.coordinates as coord
geodetic_positions = np.array([[37.7610403, -122.4778699, 115],
@@ -41,7 +42,7 @@ ned_offsets_batch = np.array([[ 53.88103168, 43.83445935, -46.27488057],
[ 78.56272609, 18.53100158, -43.25290759]])
class TestNED:
class TestNED(OpenpilotTestCase):
def test_small_distances(self):
start_geodetic = np.array([33.8042184, -117.888593, 0.0])
local_coord = coord.LocalCoord.from_geodetic(start_geodetic)
@@ -1,6 +1,6 @@
import numpy as np
import pytest
from openpilot.common.test import OpenpilotTestCase
from openpilot.common.transformations.orientation import euler2quat, quat2euler, euler2rot, rot2euler, \
rot2quat, quat2rot, \
ned_euler_from_ecef
@@ -30,7 +30,7 @@ ned_eulers = np.array([[ 0.46806039, -0.4881889 , 1.65697808],
[ 2.50450101, 0.36304151, 0.33136365]])
class TestOrientation:
class TestOrientation(OpenpilotTestCase):
def test_quat_euler(self):
for i, eul in enumerate(eulers):
np.testing.assert_allclose(quats[i], euler2quat(eul), rtol=1e-7)
@@ -62,13 +62,13 @@ class TestOrientation:
# np.testing.assert_allclose(ned_eulers, ned_euler_from_ecef(ecef_positions, eulers), rtol=1e-7)
def test_inputs(self):
with pytest.raises(ValueError):
with self.assertRaises(ValueError):
euler2quat([1, 2])
with pytest.raises(ValueError):
with self.assertRaises(ValueError):
quat2rot([1, 2, 3])
with pytest.raises(IndexError):
with self.assertRaises(IndexError):
rot2quat(np.zeros((2, 2)))
def test_euler_rot_consistency(self):
+8 -4
View File
@@ -108,10 +108,14 @@ def atomic_write(path: str, mode: str = 'w', buffering: int = -1, encoding: str
if not overwrite and os.path.exists(path):
raise FileExistsError(f"File '{path}' already exists. To overwrite it, set 'overwrite' to True.")
with tempfile.NamedTemporaryFile(mode=mode, buffering=buffering, encoding=encoding, newline=newline, dir=dir_name, delete=False) as tmp_file:
yield tmp_file
tmp_file_name = tmp_file.name
os.replace(tmp_file_name, path)
tmp_file = tempfile.NamedTemporaryFile(mode=mode, buffering=buffering, encoding=encoding, newline=newline, dir=dir_name, delete=False)
try:
with tmp_file:
yield tmp_file
os.replace(tmp_file.name, path)
finally:
with contextlib.suppress(FileNotFoundError):
os.unlink(tmp_file.name)
def get_upload_stream(filepath: str, should_compress: bool) -> tuple[io.BufferedIOBase, int]:
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:93cdc4ee9aa40e2afceecc63da0ca05ec7aab4bec991ece51a6b52389f48a477
size 10788068
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:80135e7ab71496c3b744047ef9100f5d24edaa275d248a3e0488bc769e453baa
size 240500
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e86fb33e127558b845aec3016d46c36118811a1e30fd9dacc374840ad3001221
size 109356
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:71933d4233b691fb499524cbf9b3d74ccdf6465c81791687e661667aefa8e660
size 196068
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:292f8ad096fd0a47e4016bd3607cb4edcd3b1f97634f67a115c0b0973ba5002f
size 245884
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8656eb9e4e182333e58c59cd07ae0dd82c913d31c29b0d2726887ffbbef137a2
size 26536
-136
View File
@@ -1,136 +0,0 @@
#!/usr/bin/env python3
from pathlib import Path
import json
import pyray as rl
FONT_DIR = Path(__file__).resolve().parent
SELFDRIVE_DIR = FONT_DIR.parents[1]
TRANSLATIONS_DIR = SELFDRIVE_DIR / "ui" / "translations"
LANGUAGES_FILE = TRANSLATIONS_DIR / "languages.json"
GLYPH_PADDING = 6
EXTRA_CHARS = "–‑✓×°§•X⚙✕◀▶✔⌫⇧␣○●↳çêüñ–‑✓×°§•€£¥"
UNIFONT_LANGUAGES = {"th", "zh-CHT", "zh-CHS", "ko", "ja"}
def _languages():
if not LANGUAGES_FILE.exists():
return {}
with LANGUAGES_FILE.open(encoding="utf-8") as f:
return json.load(f)
def _char_sets():
base = set(map(chr, range(32, 127))) | set(EXTRA_CHARS)
unifont = set(base)
for language, code in _languages().items():
unifont.update(language)
po_path = TRANSLATIONS_DIR / f"app_{code}.po"
try:
chars = set(po_path.read_text(encoding="utf-8"))
except FileNotFoundError:
continue
(unifont if code in UNIFONT_LANGUAGES else base).update(chars)
return tuple(sorted(ord(c) for c in base)), tuple(sorted(ord(c) for c in unifont))
def _glyph_metrics(glyphs, rects, glyph_count: int):
entries = []
min_offset_y, max_extent = None, 0
for idx in range(glyph_count):
glyph = glyphs[idx]
rect = rects[idx]
width = int(round(rect.width))
height = int(round(rect.height))
offset_y = int(round(glyph.offsetY))
min_offset_y = offset_y if min_offset_y is None else min(min_offset_y, offset_y)
max_extent = max(max_extent, offset_y + height)
entries.append({
"id": glyph.value,
"x": int(round(rect.x)),
"y": int(round(rect.y)),
"width": width,
"height": height,
"xoffset": int(round(glyph.offsetX)),
"yoffset": offset_y,
"xadvance": int(round(glyph.advanceX)),
})
if min_offset_y is None:
raise RuntimeError("No glyphs were generated")
line_height = int(round(max_extent - min_offset_y))
base = int(round(max_extent))
return entries, line_height, base
def _write_bmfont(path: Path, font_size: int, face: str, atlas_name: str, line_height: int, base: int, atlas_size, entries):
# TODO: why doesn't raylib calculate these metrics correctly?
if line_height != font_size:
print("using font size for line height", atlas_name)
line_height = font_size
lines = [
f"info face=\"{face}\" size=-{font_size} bold=0 italic=0 charset=\"\" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=0,0 outline=0",
f"common lineHeight={line_height} base={base} scaleW={atlas_size[0]} scaleH={atlas_size[1]} pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4",
f"page id=0 file=\"{atlas_name}\"",
f"chars count={len(entries)}",
]
for entry in entries:
lines.append(
("char id={id:<4} x={x:<5} y={y:<5} width={width:<5} height={height:<5} " +
"xoffset={xoffset:<5} yoffset={yoffset:<5} xadvance={xadvance:<5} page=0 chnl=15").format(**entry)
)
path.write_text("\n".join(lines) + "\n")
def _process_font(font_path: Path, codepoints: tuple[int, ...]):
print(f"Processing {font_path.name}...")
font_size = {
"unifont.otf": 16, # unifont is only 16x8 or 16x16 pixels per glyph
}.get(font_path.name, 200)
data = font_path.read_bytes()
file_buf = rl.ffi.new("unsigned char[]", data)
cp_buffer = rl.ffi.new("int[]", codepoints)
cp_ptr = rl.ffi.cast("int *", cp_buffer)
glyph_count = rl.ffi.new("int *", len(codepoints))
glyphs = rl.load_font_data(
rl.ffi.cast("unsigned char *", file_buf), len(data), font_size, cp_ptr, len(codepoints),
rl.FontType.FONT_DEFAULT, glyph_count
)
if glyphs == rl.ffi.NULL:
raise RuntimeError("raylib failed to load font data")
rects_ptr = rl.ffi.new("Rectangle **")
image = rl.gen_image_font_atlas(glyphs, rects_ptr, glyph_count[0], font_size, GLYPH_PADDING, 0)
if image.width == 0 or image.height == 0:
raise RuntimeError("raylib returned an empty atlas")
rects = rects_ptr[0]
atlas_name = f"{font_path.stem}.png"
atlas_path = FONT_DIR / atlas_name
entries, line_height, base = _glyph_metrics(glyphs, rects, glyph_count[0])
if not rl.export_image(image, atlas_path.as_posix()):
raise RuntimeError("Failed to export atlas image")
_write_bmfont(FONT_DIR / f"{font_path.stem}.fnt", font_size, font_path.stem, atlas_name, line_height, base, (image.width, image.height), entries)
def main():
base_cp, unifont_cp = _char_sets()
fonts = sorted(FONT_DIR.glob("*.ttf")) + sorted(FONT_DIR.glob("*.otf"))
for font in fonts:
if "emoji" in font.name.lower():
continue
glyphs = unifont_cp if font.stem.lower().startswith("unifont") else base_cp
_process_font(font, glyphs)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4ae6b5f666de71c8af94e8dcace3c4ca0259070cb6e8b2bbb9ba648c28a80e1
size 5926
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a3f14ff386f5e3e8e01ac64231b77154729516aa96a8baf85fdd98fa6be20839
size 2341
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ec3dcf64cbc34251d8423cb8b3b31d743e93d14002dec43c389a857cb7e8eb17
size 10875
oid sha256:d05f85ba6e53417260335bb52e2f4c492311a1eb4136e7fa1fc49aa14095bcaf
size 1914

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