Compare commits

..

206 Commits

Author SHA1 Message Date
royjr 87ea97ce6a Merge branch 'master' into ccnc-port-alert-dedup 2026-09-06 15:46:33 -04:00
royjr 112d2fa559 Merge branch 'master' into ccnc-port-alert-dedup 2026-09-03 08:01:13 -04:00
royjr 82585cd6b2 Update opendbc_repo 2026-09-03 08:01:07 -04:00
royjr ed55ce98f8 move to mads 2026-08-30 16:59:42 -04:00
royjr 42cd751550 one liner 2026-08-24 01:38:50 -04:00
royjr 4950efbf94 Update selfdrived.py 2026-08-24 01:25:52 -04:00
royjr dc0a1b0dca Update selfdrived.py 2026-08-24 01:17:32 -04:00
royjr fa433ae25f alert-dedup 2026-08-24 01:08:56 -04:00
royjr 9648ac2f04 Merge branch 'master' into ccnc-port 2026-08-23 21:48:17 -04:00
royjr a03a11e333 Merge branch 'master' into ccnc-port 2026-08-23 19:36:45 -04:00
royjr 53b1070b09 Merge branch 'master' into ccnc-port 2026-08-23 11:05:40 -04:00
royjr 68bf6d8162 Merge branch 'master' into ccnc-port 2026-08-22 13:19:05 -04:00
royjr 8b472291d0 Merge branch 'master' into ccnc-port 2026-08-21 17:12:13 -04:00
royjr 797c8d4293 Update opendbc_repo 2026-08-21 17:09:31 -04:00
royjr 7568528507 Merge branch 'master' into ccnc-port 2026-08-19 12:53:59 -04:00
royjr 6665c3acf6 Merge branch 'master' into ccnc-port 2026-08-17 10:14:33 -04:00
royjr 3b13eb4d0d Update opendbc_repo 2026-08-17 10:14:19 -04:00
royjr f7eb4c2561 Update opendbc_repo 2026-08-15 01:39:27 -04:00
royjr be9255358c Update opendbc_repo 2026-08-15 01:35:33 -04:00
royjr f48ccb5d7a Merge branch 'master' into ccnc-port 2026-08-14 20:42:45 -04:00
royjr 99559d6749 Update opendbc_repo 2026-08-14 20:42:36 -04:00
royjr 4b6f0ffb46 Merge branch 'master' into ccnc-port 2026-08-11 11:56:46 -04:00
royjr 55415382ce Update opendbc_repo 2026-08-11 11:56:38 -04:00
royjr 9f30901eb6 Merge branch 'master' into ccnc-port 2026-08-10 16:33:50 -04:00
royjr d2045d24fb Update opendbc_repo 2026-08-10 16:33:37 -04:00
royjr 6488a11e4d Merge branch 'master' into ccnc-port 2026-08-02 14:27:34 -04:00
royjr 7b22b65313 Merge branch 'master' into ccnc-port 2026-07-27 15:29:04 -04:00
royjr b59a50101e Update opendbc_repo 2026-07-27 15:28:51 -04:00
royjr 3e30962bd3 Merge branch 'master' into ccnc-port 2026-07-25 03:04:23 -04:00
royjr 3274043063 Update opendbc_repo 2026-07-25 03:04:12 -04:00
royjr c624550d2e Merge branch 'master' into ccnc-port 2026-07-20 15:52:41 -04:00
royjr 6759038671 Merge branch 'master' into ccnc-port 2026-07-20 09:18:18 -04:00
royjr aa636e75c8 Merge branch 'master' into ccnc-port 2026-07-16 00:23:49 -04:00
royjr 5ff3c3bd8d Update opendbc_repo 2026-07-16 00:23:43 -04:00
royjr 1e11b52290 Update opendbc_repo 2026-07-14 00:40:02 -04:00
royjr 76e0204025 Update opendbc_repo 2026-07-14 00:37:13 -04:00
royjr cc88f2cbd6 Update opendbc_repo 2026-07-10 22:00:25 -04:00
royjr ee0ac199a4 Update opendbc_repo 2026-07-03 08:08:29 -04:00
royjr d5ed828eaa Update opendbc_repo 2026-07-01 19:30:06 -04:00
royjr 9fca585f2a Merge branch 'master' into ccnc-port 2026-06-30 22:14:33 -04:00
royjr bb1259303e Update opendbc_repo 2026-06-30 22:14:23 -04:00
royjr e135051ca8 Merge branch 'master' into ccnc-port 2026-06-13 22:38:22 -04:00
royjr 12bef55d8a Update opendbc_repo 2026-06-13 22:12:00 -04:00
royjr 2f7a45e6c8 Merge branch 'master' into ccnc-port 2026-06-08 22:01:57 -04:00
royjr 936ebfc12b Update opendbc_repo 2026-06-08 22:01:44 -04:00
royjr aa0c9dc0eb Merge branch 'master' into ccnc-port 2026-06-04 09:52:35 -04:00
royjr 7476a866e7 Update opendbc_repo 2026-06-04 09:52:06 -04:00
royjr 610d857e33 Merge branch 'master' into ccnc-port 2026-05-28 08:30:40 -04:00
royjr d2f47407d0 Update opendbc_repo 2026-05-12 21:03:18 -04:00
royjr db75ec76ea Merge branch 'master' into ccnc-port 2026-05-12 20:44:57 -04:00
royjr 24066465d7 Update opendbc_repo 2026-05-12 20:44:48 -04:00
royjr a7abbd6e25 Update opendbc_repo 2026-04-20 18:22:12 -04:00
royjr 878982447c Merge branch 'master' into ccnc-port 2026-04-19 12:06:46 -04:00
royjr 576527a36b Merge branch 'master' into ccnc-port 2026-04-17 05:42:35 -04:00
royjr ef8c35da24 Update opendbc_repo 2026-04-17 05:41:21 -04:00
royjr 85688b1040 Merge branch 'master' into ccnc-port 2026-04-16 19:48:39 -04:00
royjr 0fb2199130 Update opendbc_repo 2026-04-16 19:47:57 -04:00
royjr d48d756c1d Update opendbc_repo 2026-04-08 00:56:54 -04:00
royjr 2ed298a0c9 Merge branch 'master' into ccnc-port 2026-04-08 00:51:13 -04:00
royjr d68f038949 Update opendbc_repo 2026-04-08 00:51:11 -04:00
royjr 7231571e57 Merge branch 'master' into ccnc-port 2026-04-03 23:34:00 -04:00
royjr b37f1419d3 Update opendbc_repo 2026-04-03 23:33:19 -04:00
royjr cd85a66790 Merge branch 'master' into ccnc-port 2026-03-26 00:53:07 -04:00
royjr 305ea87daf Update opendbc_repo 2026-03-26 00:52:44 -04:00
royjr 4bbfc793e0 Merge branch 'master' into ccnc-port 2026-03-15 15:44:59 -04:00
royjr d5d983676e Update opendbc_repo 2026-03-13 16:41:05 -04:00
royjr de8a96a398 Merge branch 'master' into ccnc-port 2026-03-13 16:41:00 -04:00
royjr 0cbf45f699 Merge branch 'master' into ccnc-port 2026-03-11 23:28:59 -04:00
royjr 0d68a3a2ab Merge branch 'master' into ccnc-port 2026-03-09 19:58:32 -04:00
royjr 9e85a85059 Update opendbc_repo 2026-03-09 19:58:18 -04:00
royjr 0373c327c0 Update opendbc_repo 2026-03-02 10:10:38 -05:00
royjr efe9e5c200 Update opendbc_repo 2026-03-02 02:15:05 -05:00
royjr 8a249a45dc Update opendbc_repo 2026-03-02 02:06:16 -05:00
royjr bdbefe67f6 Update opendbc_repo 2026-03-02 01:40:31 -05:00
royjr 675bb166ad Merge branch 'master' into ccnc-port 2026-03-01 17:18:11 -05:00
royjr 1b717a7e88 Merge branch 'master' into ccnc-port 2026-03-01 13:23:30 -05:00
royjr 86f55a8ba9 Update opendbc_repo 2026-03-01 13:23:24 -05:00
royjr 629392d2f7 Update opendbc_repo 2026-02-27 16:31:59 -05:00
royjr bc414bdc8b Update opendbc_repo 2026-02-26 23:53:26 -05:00
royjr 7ca5649f2c Merge branch 'master' into ccnc-port 2026-02-26 23:52:46 -05:00
royjr 641ee8fa87 Update opendbc_repo 2026-02-26 23:52:27 -05:00
royjr 56c276158c Merge branch 'master' into ccnc-port 2026-02-24 14:12:12 -05:00
royjr c65308a8bd Update opendbc_repo 2026-02-24 14:12:01 -05:00
royjr 994e526460 Merge branch 'master' into ccnc-port 2026-02-18 12:06:05 -05:00
royjr 1defae36b7 Update opendbc_repo 2026-02-18 12:05:53 -05:00
royjr 8f029fd0ef Merge branch 'master' into ccnc-port 2026-02-13 23:01:41 -05:00
royjr ddb46284dc Update opendbc_repo 2026-02-13 23:01:16 -05:00
royjr 9effc754d9 Merge branch 'master' into ccnc-port 2026-02-06 01:16:06 -05:00
royjr e49ffc2a2d Update opendbc_repo 2026-02-06 01:15:59 -05:00
royjr 2cacd0b3e5 Merge branch 'master' into ccnc-port 2026-01-24 12:50:18 -05:00
royjr c4b8859dff Update opendbc_repo 2026-01-24 12:50:11 -05:00
royjr 8fb0953205 Merge branch 'master' into ccnc-port 2026-01-11 22:17:38 -05:00
royjr 63d1c8835f Merge branch 'master' into ccnc-port 2026-01-10 13:03:48 -05:00
royjr 17a185606d Merge branch 'master' into ccnc-port 2026-01-09 16:40:32 -05:00
royjr da10131392 Merge branch 'master' into ccnc-port 2025-12-28 17:18:51 -05:00
royjr 7107c2ba14 Merge branch 'master' into ccnc-port 2025-12-23 12:13:48 -05:00
royjr 95b6e877ac Update opendbc_repo 2025-12-23 12:13:29 -05:00
royjr eb02c6570e Update opendbc_repo 2025-12-21 15:43:04 -05:00
royjr 1be8ae31c4 Merge branch 'master' into ccnc-port 2025-12-19 01:04:42 -05:00
royjr 04dcd38856 Update opendbc_repo 2025-12-19 01:04:30 -05:00
royjr 22ccf0d72f Merge branch 'master' into ccnc-port 2025-12-15 17:02:50 -05:00
royjr 3c969bb627 Merge branch 'master' into ccnc-port 2025-12-13 23:22:22 -05:00
royjr 20f8011feb Update opendbc_repo 2025-12-13 23:22:11 -05:00
royjr 9cf17e74a1 Merge branch 'master' into ccnc-port 2025-12-12 23:19:56 -05:00
royjr 2c4efdf557 Merge branch 'master' into ccnc-port 2025-12-07 13:29:48 -05:00
royjr 4cd3d3c16c Merge branch 'master' into ccnc-port 2025-12-02 12:56:21 -05:00
royjr 637f3ae9c8 Merge branch 'master' into ccnc-port 2025-12-01 14:41:03 -05:00
royjr 464ee80f71 Merge branch 'master' into ccnc-port 2025-11-26 00:27:53 -05:00
royjr 2743a04613 Merge branch 'master' into ccnc-port 2025-11-24 18:44:13 -05:00
royjr 7f9978d001 Merge branch 'master' into ccnc-port 2025-11-22 00:21:15 -05:00
royjr 4b83961c67 Merge branch 'master' into ccnc-port 2025-11-21 16:23:22 -05:00
royjr c00eaf428a Update opendbc_repo 2025-11-21 16:23:01 -05:00
royjr 0a9993e8d4 Merge branch 'master' into ccnc-port 2025-11-19 16:49:59 -05:00
royjr 0af214a985 Update opendbc_repo 2025-11-19 16:49:51 -05:00
royjr af43385e3a Merge branch 'master' into ccnc-port 2025-11-11 10:19:51 -05:00
royjr 0ab2b8c590 Update opendbc_repo 2025-11-07 19:59:50 -05:00
royjr 67ab18a0de Merge branch 'master' into ccnc-port 2025-11-07 19:23:51 -05:00
royjr e87dc15b30 Update opendbc_repo 2025-11-07 19:23:37 -05:00
royjr 192d08516c Merge branch 'master' into ccnc-port 2025-11-02 19:23:00 -05:00
royjr 3cf001c59c Update opendbc_repo 2025-11-02 19:22:49 -05:00
royjr f2ccd021da Merge branch 'master' into ccnc-port 2025-11-02 14:07:54 -05:00
royjr c9fc900f64 Update opendbc_repo 2025-11-02 14:07:44 -05:00
royjr 3c37c5ce5d Update opendbc_repo 2025-10-30 11:28:49 -04:00
royjr 7c45889e4e Merge branch 'master' into ccnc-port 2025-10-30 11:27:50 -04:00
royjr 2aabb7aee8 Merge branch 'master' into ccnc-port 2025-10-24 14:16:09 -04:00
royjr 3859e9962f Update opendbc_repo 2025-10-24 14:15:51 -04:00
royjr 810efbab72 Merge branch 'master' into ccnc-port 2025-10-18 07:33:11 -04:00
royjr ec27bec326 Update opendbc_repo 2025-10-18 07:32:33 -04:00
royjr 250d553157 Merge branch 'master' into ccnc-port 2025-10-14 21:57:32 -04:00
royjr cea54a0ca8 Update opendbc_repo 2025-10-14 21:57:26 -04:00
royjr 8e72d783bd Update opendbc_repo 2025-10-13 22:41:21 -04:00
royjr 1b0dc103dc Merge branch 'master' into ccnc-port 2025-10-11 23:51:46 -04:00
royjr 6c364d292b Update opendbc_repo 2025-10-11 23:51:31 -04:00
royjr bcdec2ce84 Merge branch 'master' into ccnc-port 2025-10-10 17:29:05 -04:00
royjr 3deaeb3759 Merge branch 'master' into ccnc-port 2025-10-10 15:02:32 -04:00
royjr c669f0984a Update opendbc_repo 2025-10-10 15:02:17 -04:00
royjr 46dd946740 Merge branch 'master' into ccnc-port 2025-10-07 01:36:06 -04:00
royjr 9da4b3653e Update opendbc_repo 2025-10-07 01:35:58 -04:00
royjr 4e21ae7c50 Update opendbc_repo 2025-10-05 06:21:56 -04:00
royjr bb91e92237 Update opendbc_repo 2025-10-05 06:01:15 -04:00
royjr 14b4c4f85b Update opendbc_repo 2025-10-02 09:20:32 -04:00
royjr 0660b542c3 Merge branch 'master' into ccnc-port 2025-10-01 16:01:32 -04:00
royjr 2b893b90c9 Update opendbc_repo 2025-10-01 16:01:26 -04:00
royjr f5139178ed Merge branch 'master' into ccnc-port 2025-09-30 14:39:57 -04:00
royjr fb43b755f2 Update opendbc_repo 2025-09-30 14:39:50 -04:00
royjr 07f5b967d8 Merge branch 'master' into ccnc-port 2025-09-24 21:30:29 -04:00
royjr ea19c7d3bb Update opendbc_repo 2025-09-24 21:30:17 -04:00
royjr e461842cbb Merge branch 'master' into ccnc-port 2025-09-23 05:55:02 -04:00
royjr a73c9659d5 Update opendbc_repo 2025-09-23 05:54:50 -04:00
royjr cb796fbc76 Merge branch 'master' into ccnc-port 2025-09-18 20:10:43 -04:00
royjr 6bf75fc557 Update opendbc_repo 2025-09-18 20:10:37 -04:00
royjr 9a1fc28819 Merge branch 'master' into ccnc-port 2025-09-18 13:54:36 -04:00
royjr 0741d05e92 Update opendbc_repo 2025-09-18 13:54:23 -04:00
royjr 1ad008107d Merge branch 'master' into ccnc-port 2025-09-15 01:40:27 -04:00
royjr feebd9df93 Reapply "UI: Developer UI (#1233)"
This reverts commit 15e5d2efb9.
2025-09-15 01:40:21 -04:00
royjr c2e5ced3e5 Update opendbc_repo 2025-09-15 01:39:51 -04:00
royjr 15e5d2efb9 Revert "UI: Developer UI (#1233)"
This reverts commit 1bb4ca2547.
2025-09-12 02:10:29 -04:00
royjr a3929d0b54 Merge branch 'master' into ccnc-port 2025-09-12 01:40:29 -04:00
royjr 794f8f9991 Update opendbc_repo 2025-09-08 09:27:31 -04:00
royjr 68fa5e3f21 Merge branch 'master' into ccnc-port 2025-09-07 13:13:37 -04:00
royjr 86c6cc1f48 Merge branch 'master' into ccnc-port 2025-09-03 22:22:12 -04:00
royjr eb7ffbf093 Update opendbc_repo 2025-09-03 10:14:58 -04:00
royjr 3919095752 Update opendbc_repo 2025-09-03 10:05:35 -04:00
royjr 74d63be1c3 Merge branch 'master' into ccnc-port 2025-09-03 09:50:55 -04:00
royjr 8894486a1a Update opendbc_repo 2025-09-03 09:50:49 -04:00
royjr 810599315d Merge branch 'master' into ccnc-port 2025-08-31 16:53:30 -04:00
royjr 6f3ab810c8 Update opendbc_repo 2025-08-31 16:53:24 -04:00
royjr 230f78b8d3 Merge branch 'master' into ccnc-port 2025-08-26 12:14:46 -04:00
royjr f1affec088 Update opendbc_repo 2025-08-26 12:14:22 -04:00
royjr 97d8ef242c Merge branch 'master' into ccnc-port 2025-08-24 15:12:57 -04:00
royjr a63fff9b45 Update opendbc_repo 2025-08-24 15:12:46 -04:00
royjr cb3893daaa Merge branch 'master' into ccnc-port 2025-08-23 10:33:24 -04:00
royjr 29f60df74b Merge branch 'master' into ccnc-port 2025-08-22 11:18:27 -04:00
royjr c6c072e1f4 Update opendbc_repo 2025-08-22 11:18:18 -04:00
royjr d101cbb83e Update opendbc_repo 2025-08-13 16:00:04 -04:00
royjr 1536d59633 Update opendbc_repo 2025-08-13 15:28:37 -04:00
royjr dc99b865ae Merge branch 'master' into ccnc-port 2025-08-13 12:31:14 -04:00
royjr e59bc027ff Merge branch 'master' into ccnc-port 2025-08-13 11:58:05 -04:00
royjr cf7e5efaca Update opendbc_repo 2025-08-13 11:57:57 -04:00
royjr 4b44f2eb31 Merge branch 'master' into ccnc-port 2025-08-10 09:18:24 -04:00
royjr 107d2ab400 Update opendbc_repo 2025-08-10 09:18:15 -04:00
royjr 5432d9062c Merge branch 'master' into ccnc-port 2025-08-04 11:52:29 -04:00
royjr f533f6c843 Merge branch 'master' into ccnc-port 2025-08-02 06:53:54 -04:00
royjr 58e9ac763c Update opendbc_repo 2025-08-02 06:53:43 -04:00
royjr cb50d54169 Merge branch 'master-new' into ccnc-port 2025-07-24 20:23:46 -04:00
royjr bd5de4ed0a Merge branch 'master-new' into ccnc-port 2025-07-20 23:49:36 -04:00
royjr 0d4073fadb Merge branch 'master-new' into ccnc-port 2025-07-19 23:18:26 -04:00
royjr ebc70dcb52 Merge branch 'master-new' into ccnc-port 2025-07-19 14:37:17 -04:00
royjr 4d0426999e Update opendbc_repo 2025-07-19 14:36:59 -04:00
royjr 286da42573 Merge branch 'master-new' into ccnc-port 2025-07-16 23:48:23 -04:00
royjr 8a836710a9 Update opendbc_repo 2025-07-16 23:48:06 -04:00
royjr 5d515bcf33 Merge branch 'master-new' into ccnc-port 2025-07-07 05:35:55 -04:00
royjr 1c7f6d5133 Update opendbc_repo 2025-07-03 21:24:01 -04:00
royjr 05d57c7aeb Update opendbc_repo 2025-07-01 18:30:32 -04:00
royjr e4b0eaf352 Update opendbc_repo 2025-06-28 19:30:28 -04:00
royjr a710276472 Merge branch 'master-new' into ccnc-port 2025-06-28 19:22:59 -04:00
royjr af086db671 Merge branch 'master-new' into ccnc-port 2025-06-28 12:13:26 -04:00
royjr 0d9eb0e25e Update opendbc_repo submodule to latest commit
Advanced the opendbc_repo submodule to commit d309f7ec96e37267c94d12fc4bfe2672ad505b06. This pulls in the latest changes from the opendbc repository.
2025-06-26 14:03:56 -04:00
royjr 0616caed6d Merge branch 'master-new' into ccnc-port 2025-06-25 19:33:53 -04:00
royjr 095337b3c1 Update opendbc_repo 2025-06-25 19:33:38 -04:00
royjr 1edec2d22c Update opendbc_repo 2025-06-14 16:14:02 -04:00
royjr affabb9ee0 Update opendbc_repo 2025-06-14 15:55:07 -04:00
royjr dc27e8711c Update opendbc_repo 2025-06-14 14:54:32 -04:00
royjr cf7329a264 Merge branch 'master-new' into ccnc-port 2025-06-11 21:36:06 -04:00
royjr 5ee5ecd820 Merge branch 'master-new' into ccnc-port 2025-06-08 23:25:13 -04:00
royjr b064f730dd Update opendbc_repo 2025-06-08 17:54:43 -04:00
10 changed files with 49 additions and 1182 deletions
-5
View File
@@ -227,11 +227,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"SunnylinkEnabled", {PERSISTENT, BOOL, "1"}},
{"SunnylinkTempFault", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL, "0"}},
{"SunnylinkLocalApps", {PERSISTENT, JSON}},
{"SunnylinkLocalPairingCode", {CLEAR_ON_MANAGER_START, JSON}},
{"SunnylinkLocalDiscoveredApp", {CLEAR_ON_MANAGER_START, JSON}},
{"SunnylinkLocalPairingRequest", {CLEAR_ON_MANAGER_START, BOOL}},
// Backup Manager params
{"BackupManager_CreateBackup", {PERSISTENT, BOOL}},
{"BackupManager_RestoreVersion", {PERSISTENT, STRING}},
+1 -1
View File
@@ -576,7 +576,7 @@ class SelfdriveD(CruiseHelper):
clear_event_types = set()
if ET.WARNING not in self.state_machine.current_alert_types:
clear_event_types.add(ET.WARNING)
if self.enabled:
if self.enabled or self.mads.clear_no_entry:
clear_event_types.add(ET.NO_ENTRY)
pers = LONGITUDINAL_PERSONALITY_MAP[self.personality]
@@ -5,42 +5,22 @@ This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from functools import partial
from openpilot.cereal import custom
from openpilot.common.version import sunnylink_consent_version
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkConsentPage
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
from openpilot.sunnypilot.sunnylink.athena.local_discovery import latest_discovered_app
from openpilot.sunnypilot.sunnylink.athena.local_pairing import (
LocalApp,
arm_pairing,
clear_pairing_request,
get_local_apps,
local_app_display_name,
pairing_requested,
read_pairing_code,
remove_local_app,
)
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP, button_item_sp, toggle_item_sp
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
from openpilot.system.ui.sunnypilot.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.button import ButtonStyle, Button, IconButton
from openpilot.system.ui.widgets.button import ButtonStyle, Button
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog, ConfirmDialog
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.list_view import dual_button_item
from openpilot.system.ui.widgets.network import NavButton
from openpilot.system.ui.widgets.scroller_tici import Scroller, LineSeparator
MAX_LOCAL_APPS = 4
# Read-only value colors used by the local-mode rows.
_LOCAL_DISCOVERED_COLOR = rl.Color(170, 170, 170, 255) # grey: no app in sight
_LOCAL_ACTIVE_COLOR = rl.Color(0, 255, 0, 255) # green: discovered / pairing code
from openpilot.common.version import sunnylink_consent_version
class SunnylinkHeader(Widget):
@@ -212,15 +192,6 @@ class SunnylinkLayout(Widget):
self._backup_btn.set_button_style(ButtonStyle.NORMAL)
self._restore_btn.set_button_style(ButtonStyle.PRIMARY)
self._mobile_app_btn = button_item_sp(
title=tr("Sunnylink Local Connections"),
button_text=tr("CONFIGURE"),
description=tr("Manage the mobile app(s) connected over Wi-Fi: pair a new app ") +
tr("or unpair existing ones."),
callback=self._open_local_apps,
)
self._mobile_app_btn.set_visible(lambda: self._sunnylink_enabled)
items = [
SunnylinkHeader(),
LineSeparator(),
@@ -231,11 +202,9 @@ class SunnylinkLayout(Widget):
LineSeparator(),
self._pair_btn,
LineSeparator(),
self._mobile_app_btn,
LineSeparator(),
self._sunnylink_uploader_toggle,
LineSeparator(),
self._sunnylink_backup_restore_buttons,
self._sunnylink_backup_restore_buttons
]
return items
@@ -348,8 +317,6 @@ class SunnylinkLayout(Widget):
gui_app.push_widget(sl_terms_dlg)
else:
ui_state.params.put_bool("SunnylinkEnabled", state)
if not state:
clear_pairing_request()
self._update_description(state)
def _update_description(self, state: bool):
@@ -385,9 +352,6 @@ class SunnylinkLayout(Widget):
self._pair_btn.action_item.set_text(pair_btn_text)
self._pair_btn.action_item.set_enabled(self._sunnylink_enabled)
def _open_local_apps(self):
gui_app.push_widget(SunnylinkLocalAppLayout())
def _render(self, rect):
self._scroller.render(rect)
@@ -400,157 +364,3 @@ class SunnylinkLayout(Widget):
def hide_event(self):
super().hide_event()
ui_state.sunnylink_state.set_settings_open(False)
class SunnylinkLocalAppLayout(Widget):
def __init__(self):
super().__init__()
self._local_apps_cache: list[LocalApp] = []
self._back_button = NavButton(tr("Back"))
self._back_button.set_click_callback(gui_app.pop_widget)
self._pair_app_btn = button_item_sp(
title=tr("Pair App"),
button_text=tr("PAIR"),
description=tr("Open a 5-minute pairing window and show the code to ") +
tr("type into the app. Closing the dialog cancels pairing."),
callback=self._show_pairing_code_dialog,
)
self._local_app_rows: list[ListItemSP] = []
self._local_app_seps: list[LineSeparator] = []
for i in range(MAX_LOCAL_APPS):
row = button_item_sp(
title=lambda i=i: self._local_app_title(i),
button_text=tr("UNPAIR"),
description=lambda i=i: self._local_app_endpoint(i),
callback=partial(self._unpair_local_app, i),
)
sep = LineSeparator()
row.set_visible(lambda i=i: self._local_row_visible(i))
sep.set_visible(lambda i=i: self._local_row_visible(i))
self._local_app_rows.append(row)
self._local_app_seps.append(sep)
items = [self._pair_app_btn, LineSeparator()]
for row, sep in zip(self._local_app_rows, self._local_app_seps, strict=True):
items.extend((row, sep))
self._scroller = Scroller(items, line_separator=False, spacing=0)
def _local_row_visible(self, i: int) -> bool:
return i < len(self._local_apps_cache)
def _local_app_title(self, i: int) -> str:
if i >= len(self._local_apps_cache):
return ""
return local_app_display_name(self._local_apps_cache[i])
def _local_app_endpoint(self, i: int) -> str:
if i >= len(self._local_apps_cache):
return ""
return self._local_apps_cache[i].endpoint
def _show_pairing_code_dialog(self):
gui_app.push_widget(SunnylinkLocalPairingDialog())
def _unpair_local_app(self, index: int):
apps = self._local_apps_cache
if index >= len(apps):
return
app = apps[index]
name = local_app_display_name(app)
def on_confirm(_dialog_result: int):
remove_local_app(app.app_id)
dialog = ConfirmDialog(
text=tr("Unpair") + f" {name}? " + tr("You will need the pairing code again to reconnect it."),
confirm_text=tr("Unpair"),
callback=on_confirm,
)
gui_app.push_widget(dialog)
def _update_state(self):
super()._update_state()
self._local_apps_cache = get_local_apps()
def _render(self, rect):
self._back_button.set_position(self._rect.x, self._rect.y + 20)
self._back_button.render()
content_rect = rl.Rectangle(rect.x, rect.y + self._back_button.rect.height + 40,
rect.width, rect.height - self._back_button.rect.height - 40)
self._scroller.render(content_rect)
def show_event(self):
super().show_event()
self._scroller.show_event()
def hide_event(self):
super().hide_event()
self._scroller.hide_event()
class SunnylinkLocalPairingDialog(Widget):
def __init__(self):
super().__init__()
self._apps_before = len(get_local_apps())
arm_pairing()
self._close_btn = IconButton(gui_app.texture("icons/close.png", 80, 80))
self._close_btn.set_click_callback(self._cancel)
def _cancel(self):
clear_pairing_request()
gui_app.pop_widget()
def _update_state(self):
if len(get_local_apps()) > self._apps_before:
gui_app.pop_widget() # paired — window already cleared
elif not pairing_requested():
gui_app.pop_widget() # window expired
def _render(self, rect) -> int:
rl.clear_background(rl.Color(224, 224, 224, 255))
margin = 70
content_rect = rl.Rectangle(rect.x + margin, rect.y + margin,
rect.width - 2 * margin, rect.height - 2 * margin)
y = content_rect.y
close_size = 80
pad = 20
close_rect = rl.Rectangle(content_rect.x - pad, y - pad, close_size + pad * 2, close_size + pad * 2)
self._close_btn.render(close_rect)
y += close_size + 40
title_font = gui_app.font(FontWeight.NORMAL)
title_wrapped = wrap_text(title_font, tr("Pair with mobile app"), 75, int(content_rect.width))
rl.draw_text_ex(title_font, "\n".join(title_wrapped), rl.Vector2(content_rect.x, y), 75, 0.0, rl.BLACK)
y += len(title_wrapped) * 75 + 40
code = read_pairing_code() or ""
code_font = gui_app.font(FontWeight.BOLD)
code_size = measure_text_cached(code_font, code, 110)
rl.draw_text_ex(code_font, code, rl.Vector2(content_rect.x + (content_rect.width - code_size.x) / 2, y),
110, 0.0, rl.BLACK)
y += 170
hint_font = gui_app.font(FontWeight.NORMAL)
hint_wrapped = wrap_text(hint_font, tr("Enter this code in the sunnylink app on your phone."), 45,
int(content_rect.width))
rl.draw_text_ex(hint_font, "\n".join(hint_wrapped), rl.Vector2(content_rect.x, y), 45, 0.0, rl.BLACK)
y += len(hint_wrapped) * 45 + 30
discovered = latest_discovered_app()
if discovered is not None:
endpoint, age = discovered
status = endpoint if age < 2 else f"{endpoint} ({age}s)"
color = _LOCAL_ACTIVE_COLOR
else:
status = tr("Waiting for the app…")
color = _LOCAL_DISCOVERED_COLOR
status_font = gui_app.font(FontWeight.NORMAL)
rl.draw_text_ex(status_font, status, rl.Vector2(content_rect.x, y), 40, 0.0, color)
return -1
@@ -5,34 +5,21 @@ This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from functools import partial
from openpilot.cereal import custom
from openpilot.common.version import sunnylink_consent_version, sunnylink_consent_declined
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigDialogBase
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkConsentPage
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
from openpilot.sunnypilot.sunnylink.athena.local_discovery import latest_discovered_app
from openpilot.sunnypilot.sunnylink.athena.local_pairing import (
LocalApp,
arm_pairing,
clear_pairing_request,
get_local_apps,
local_app_display_name,
pairing_requested,
read_pairing_code,
remove_local_app,
)
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.scroller import NavScroller
MAX_LOCAL_APPS = 4
from openpilot.common.version import sunnylink_consent_version, sunnylink_consent_declined
class SunnylinkInfo(Widget):
def __init__(self):
@@ -86,15 +73,11 @@ class SunnylinkLayoutMici(NavScroller):
self._sunnylink_uploader_toggle = BigToggle(text=tr("sunnylink uploader"), initial_state=False,
toggle_callback=self._sunnylink_uploader_callback)
self._mobile_app_btn = BigButton(tr("sunnylink local"), "")
self._mobile_app_btn.set_click_callback(lambda: gui_app.push_widget(LocalAppsPanelMici()))
self._scroller.add_widgets([
self._sunnylink_info,
self._sunnylink_toggle,
self._sunnylink_sponsor_button,
self._sunnylink_pair_button,
self._mobile_app_btn,
self._backup_btn,
self._restore_btn,
self._sunnylink_uploader_toggle
@@ -127,7 +110,6 @@ class SunnylinkLayoutMici(NavScroller):
self._sunnylink_pair_button.set_text(tr("paired"))
else:
self._sunnylink_pair_button.set_text(tr("pair"))
self._mobile_app_btn.set_visible(self._sunnylink_enabled)
def show_event(self):
super().show_event()
@@ -158,8 +140,6 @@ class SunnylinkLayoutMici(NavScroller):
gui_app.push_widget(sl_terms_dlg)
else:
ui_state.params.put_bool("SunnylinkEnabled", state)
if not state:
clear_pairing_request()
ui_state.update_params()
@@ -272,108 +252,3 @@ class SunnylinkPairBigButton(BigButton):
dlg = SunnylinkPairingDialog(sponsor_pairing=False)
if dlg:
gui_app.push_widget(dlg)
class LocalAppsPanelMici(NavScroller):
def __init__(self):
super().__init__()
self._local_apps_cache: list[LocalApp] = []
self._pair_app_btn = BigButton(tr("pair app"), "")
self._pair_app_btn.set_click_callback(lambda: gui_app.push_widget(LocalPairingCodeDialogMici()))
self._local_app_btns: list[BigButton] = []
for i in range(MAX_LOCAL_APPS):
btn = BigButton("", "")
btn.set_click_callback(partial(self._confirm_unpair_local_app, i))
self._local_app_btns.append(btn)
self._scroller.add_widgets([self._pair_app_btn, *self._local_app_btns])
def _update_state(self):
super()._update_state()
self._local_apps_cache = get_local_apps()
for i, btn in enumerate(self._local_app_btns):
btn.set_visible(i < len(self._local_apps_cache))
if i < len(self._local_apps_cache):
app = self._local_apps_cache[i]
btn.set_text(local_app_display_name(app))
btn.set_value(app.endpoint)
def _confirm_unpair_local_app(self, index: int):
apps = self._local_apps_cache
if index >= len(apps):
return
app = apps[index]
def unpair():
remove_local_app(app.app_id)
icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 64)
dlg = BigConfirmationDialog(
tr("slide to unpair"),
icon,
confirm_callback=unpair,
red=True,
)
gui_app.push_widget(dlg)
class LocalPairingCodeDialogMici(BigDialogBase):
def __init__(self):
super().__init__()
self._apps_before = len(get_local_apps())
arm_pairing()
self.set_back_callback(clear_pairing_request)
header_color = rl.Color(255, 255, 255, int(255 * 0.9))
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
self._title = UnifiedLabel(tr("pair with mobile app"), font_size=48, font_weight=FontWeight.BOLD,
text_color=header_color, line_height=0.8)
self._code_label = UnifiedLabel("", font_size=110, font_weight=FontWeight.DISPLAY,
text_color=rl.Color(0, 255, 0, 255))
self._hint = UnifiedLabel(tr("enter this code in the sunnylink app"), font_size=32,
text_color=subheader_color, line_height=0.9)
self._status = UnifiedLabel("", font_size=28,
text_color=rl.Color(255, 255, 255, int(255 * 0.45)), line_height=0.9)
def _update_state(self):
super()._update_state()
if self.is_dismissing:
return
if len(get_local_apps()) > self._apps_before:
self.dismiss() # paired — window already cleared
elif not pairing_requested():
self.dismiss() # window expired
def _render(self, _):
self._code_label.set_text(read_pairing_code() or "")
discovered = latest_discovered_app()
if discovered is not None:
endpoint, age = discovered
self._status.set_text(endpoint if age < 2 else f"{endpoint} ({age}s)")
self._status.set_text_color(rl.Color(0, 255, 0, 255))
else:
self._status.set_text(tr("waiting for the app…"))
self._status.set_text_color(rl.Color(255, 255, 255, int(255 * 0.45)))
x = self._rect.x + 20
width = int(self._rect.width - 40)
self._title.set_max_width(width)
self._title.set_position(x, self._rect.y + 40)
self._title.render()
self._code_label.set_max_width(width)
self._code_label.set_position(x, self._rect.y + 130)
self._code_label.render()
self._hint.set_max_width(width)
self._hint.set_position(x, self._rect.y + 290)
self._hint.render()
self._status.set_max_width(width)
self._status.set_position(x, self._rect.y + 360)
self._status.render()
+5
View File
@@ -10,6 +10,7 @@ from openpilot.cereal import log, custom
from opendbc.car import structs
from opendbc.car.hyundai.values import HyundaiFlags
from openpilot.common.params import Params
from openpilot.selfdrive.selfdrived.events import ET
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param, MADS_NO_ACC_MAIN_BUTTON
from openpilot.sunnypilot.mads.state import StateMachine, GEARS_ALLOW_PAUSED_SILENT
@@ -33,6 +34,7 @@ class ModularAssistiveDrivingSystem:
self.enabled = False
self.active = False
self.available = False
self.clear_no_entry = False
self.lateral_mismatch_counter = 0
self.allow_always = False
self.no_main_cruise = False
@@ -218,5 +220,8 @@ class ModularAssistiveDrivingSystem:
if not self.CP.passive and self.selfdrive.initialized:
self.enabled, self.active = self.state_machine.update()
cat = self.selfdrive.state_machine.current_alert_types
self.clear_no_entry = (ET.ENABLE in cat or ET.USER_DISABLE in cat) and ET.NO_ENTRY not in cat
# Copy of previous SelfdriveD states for MADS events handling
self.selfdrive.enabled_prev = self.selfdrive.enabled
@@ -1,261 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from __future__ import annotations
import json
import socket
import threading
import time
from collections.abc import Callable
from dataclasses import dataclass
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.sunnypilot.sunnylink.athena.local_pairing import (
BEACON_PREFIX,
DISCOVERED_APP_KEY,
SUNNYLINK_LOCAL_UDP_PORT,
format_endpoint,
get_local_apps,
pairing_requested,
update_local_app_endpoint,
)
LOCAL_BEACON_FRESH_S = 30
@dataclass
class AppBeacon:
"""A parsed app beacon — the app announcing it is acting as the local backend."""
app_id: str
ws_port: int
source_ip: str
@property
def endpoint(self) -> str:
return format_endpoint(self.source_ip, self.ws_port)
def parse_beacon(raw: str | bytes, source_ip: str = "") -> AppBeacon | None:
"""
Parse one UDP beacon line from the app.
Wire format: `SUNNYLINK1 {"v":1,"role":"app","app_id":"<uuid>","ws_port":8443}`
Returns None for anything else. Beacons carry ids + addresses only — no secrets.
"""
if isinstance(raw, bytes):
raw = raw.decode("utf-8", errors="replace")
raw = raw.strip()
if not raw.startswith(BEACON_PREFIX + " "):
return None
try:
data = json.loads(raw[len(BEACON_PREFIX) + 1:])
except ValueError:
return None
if not isinstance(data, dict):
return None
if data.get("role") != "app" or data.get("v") != 1:
return None
app_id = data.get("app_id")
ws_port = data.get("ws_port")
if not isinstance(app_id, str) or not app_id:
return None
if not isinstance(ws_port, int) or not (0 < ws_port <= 65535):
return None
return AppBeacon(app_id=app_id, ws_port=ws_port, source_ip=source_ip)
class LocalDiscovery(threading.Thread):
"""
Passive UDP listener
- While a pairing window is armed: track the freshest app beacon so the
daemon can offer pairing to a NEW app, and mirror it into a status param
for the settings UI.
- Independently of any window: a beacon from an app ALREADY in the paired
registry refreshes its cached endpoint — IPs are not identity, the app can
move between networks.
"""
def __init__(self, params: Params | None = None, port: int = SUNNYLINK_LOCAL_UDP_PORT,
sock: socket.socket | None = None, write_interval_s: float = 5.0,
paired_refresh_cb: Callable[[AppBeacon], None] | None = None):
super().__init__(name="local_discovery_listener", daemon=True)
self.params = params or Params()
self.port = port
self._sock = sock
self.paired_refresh_cb = paired_refresh_cb
self._latest_endpoint: str | None = None
self._latest_app_id: str | None = None
self._last_seen_monotonic: float = 0.0
self._latest_paired_endpoint: str | None = None
self._latest_paired_app_id: str | None = None
self._last_paired_seen_monotonic: float = 0.0
self._lock = threading.Lock()
self._stop_event = threading.Event()
self.write_interval_s = write_interval_s
self._last_write_monotonic = 0.0
self._last_written_endpoint: str | None = None
self._last_written_app_id: str | None = None
self._discovered_cleared = False
def stop(self) -> None:
self._stop_event.set()
if self._sock is not None:
try:
self._sock.close()
except OSError:
pass
def latest_endpoint(self) -> str | None:
"""The most recently announced app endpoint (None outside a pairing window)."""
with self._lock:
return self._latest_endpoint
def latest_app_id(self) -> str | None:
"""The app_id of the most recently announced beacon (None outside a window)."""
with self._lock:
return self._latest_app_id
def last_seen_ago(self) -> float | None:
"""Seconds since the last app beacon was heard (None when none heard yet)."""
with self._lock:
if self._last_seen_monotonic == 0.0:
return None
return time.monotonic() - self._last_seen_monotonic
def latest_paired_endpoint(self) -> str | None:
"""The freshest beacon endpoint announced by an ALREADY-PAIRED."""
with self._lock:
return self._latest_paired_endpoint
def latest_paired_app_id(self) -> str | None:
"""The app_id of the freshest paired-app beacon (None until one is heard)."""
with self._lock:
return self._latest_paired_app_id
def latest_paired_seen_ago(self) -> float | None:
"""Seconds since the freshest paired-app beacon was heard (None when none)."""
with self._lock:
if self._last_paired_seen_monotonic == 0.0:
return None
return time.monotonic() - self._last_paired_seen_monotonic
def _handle(self, raw: bytes, source_ip: str) -> None:
beacon = parse_beacon(raw, source_ip)
if beacon is None:
return
if pairing_requested(self.params):
with self._lock:
self._latest_endpoint = beacon.endpoint
self._latest_app_id = beacon.app_id
self._last_seen_monotonic = time.monotonic()
self._write_discovered_param(beacon)
cloudlog.debug(f"local_discovery.app_found {beacon.app_id} at {beacon.endpoint}")
else:
with self._lock:
self._latest_endpoint = None
self._latest_app_id = None
self._last_seen_monotonic = 0.0
self._clear_discovered_param()
self._maybe_refresh_paired_app(beacon)
def _maybe_refresh_paired_app(self, beacon: AppBeacon) -> None:
"""Refresh a paired app's registry endpoint from its beacon."""
if not any(app.app_id == beacon.app_id for app in get_local_apps(self.params)):
return
with self._lock:
self._latest_paired_endpoint = beacon.endpoint
self._latest_paired_app_id = beacon.app_id
self._last_paired_seen_monotonic = time.monotonic()
if update_local_app_endpoint(beacon.app_id, beacon.endpoint, self.params):
cloudlog.debug(f"local_discovery.paired_refresh {beacon.app_id} -> {beacon.endpoint}")
if self.paired_refresh_cb is not None:
try:
self.paired_refresh_cb(beacon)
except Exception:
cloudlog.exception("local_discovery.paired_refresh_cb.exception")
def _clear_discovered_param(self) -> None:
if self._discovered_cleared:
return
self._discovered_cleared = True
try:
self.params.remove(DISCOVERED_APP_KEY)
except Exception:
cloudlog.exception("local_discovery.param_clear.exception")
def _write_discovered_param(self, beacon: AppBeacon) -> None:
"""Mirror the freshest beacon into a param the settings UI can read."""
now = time.monotonic()
changed = beacon.endpoint != self._last_written_endpoint or beacon.app_id != self._last_written_app_id
if not changed and now - self._last_write_monotonic < self.write_interval_s:
return
self._last_write_monotonic = now
self._last_written_endpoint = beacon.endpoint
self._last_written_app_id = beacon.app_id
self._discovered_cleared = False
payload = {
"endpoint": beacon.endpoint,
"app_id": beacon.app_id,
"ts": int(time.monotonic()),
}
try:
self.params.put(DISCOVERED_APP_KEY, payload, block=True)
except Exception:
cloudlog.exception("local_discovery.param_write.exception")
def _bind(self) -> socket.socket:
if self._sock is not None:
return self._sock
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("0.0.0.0", self.port))
sock.settimeout(0.5)
return sock
def run(self) -> None:
sock = self._bind()
try:
while not self._stop_event.is_set():
try:
data, addr = sock.recvfrom(4096)
self._handle(data, addr[0] if len(addr) > 0 else "")
except TimeoutError:
continue
except OSError:
# Socket closed by stop() — exit quietly.
if self._stop_event.is_set():
break
cloudlog.exception("local_discovery.recv.exception")
break
finally:
try:
sock.close()
except OSError:
pass
def latest_discovered_app(params: Params | None = None,
fresh_s: float = LOCAL_BEACON_FRESH_S) -> tuple[str, int] | None:
params = params or Params()
data = params.get(DISCOVERED_APP_KEY)
if not isinstance(data, dict):
return None
endpoint = str(data.get("endpoint", ""))
try:
ts = int(data.get("ts") or 0)
except (ValueError, TypeError):
return None
if not endpoint or ts <= 0:
return None
age = time.monotonic() - ts
# Negative age = written before the last reboot (monotonic restarts at boot).
if age < 0 or age > fresh_s:
return None
return endpoint, max(0, int(age))
@@ -1,257 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from __future__ import annotations
import secrets
import threading
import time
from dataclasses import asdict, dataclass
from datetime import datetime, UTC
from typing import Any
from collections.abc import Callable
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
SUNNYLINK_LOCAL_UDP_PORT = 53133
SUNNYLINK_LOCAL_WS_PORT = 8443
LOCAL_APPS_KEY = "SunnylinkLocalApps"
PAIRING_CODE_KEY = "SunnylinkLocalPairingCode"
PAIRING_REQUEST_KEY = "SunnylinkLocalPairingRequest"
DISCOVERED_APP_KEY = "SunnylinkLocalDiscoveredApp"
PAIRING_CODE_LENGTH = 6
PAIRING_CODE_ALPHABET = "0123456789"
DEFAULT_CODE_ROTATION_S = 10 * 60 # re-roll the displayed code every 10 min
PAIRING_WINDOW_S = 5 * 60
BEACON_PREFIX = "SUNNYLINK1"
@dataclass
class LocalApp:
"""One app paired with this device (the app runs the local "backend")."""
app_id: str
endpoint: str
app_name: str = ""
alias: str = ""
paired_at: int = 0 # epoch seconds
@staticmethod
def from_dict(data: dict[str, Any]) -> LocalApp:
return LocalApp(
app_id=str(data.get("app_id", "")),
endpoint=str(data.get("endpoint", "")),
app_name=str(data.get("app_name", "")),
alias=str(data.get("alias", "")),
paired_at=int(data.get("paired_at") or 0),
)
def local_app_display_name(app: LocalApp) -> str:
return app.alias or app.app_name or app.app_id
def is_locally_paired(params: Params | None = None) -> bool:
return len(get_local_apps(params)) > 0
def get_local_apps(params: Params | None = None) -> list[LocalApp]:
"""The paired-app registry (a JSON list persisted in `SunnylinkLocalApps`)."""
params = params or Params()
data = params.get(LOCAL_APPS_KEY)
if not isinstance(data, list):
return []
return [LocalApp.from_dict(item) for item in data if isinstance(item, dict) and item.get("app_id")]
def _save_local_apps(apps: list[LocalApp], params: Params | None = None) -> None:
params = params or Params()
if apps:
params.put(LOCAL_APPS_KEY, [asdict(app) for app in apps], block=True)
else:
params.remove(LOCAL_APPS_KEY)
def add_local_app(app: LocalApp, params: Params | None = None) -> None:
"""Append (or update by app_id) and persist."""
if not app.paired_at:
app.paired_at = int(datetime.now(UTC).replace(tzinfo=None).timestamp())
apps = [existing for existing in get_local_apps(params) if existing.app_id != app.app_id]
apps.append(app)
_save_local_apps(apps, params)
cloudlog.event("local_pairing.app_paired", app_id=app.app_id, endpoint=app.endpoint)
def update_local_app_endpoint(app_id: str, endpoint: str, params: Params | None = None) -> bool:
"""Refresh a PAIRED app's cached endpoint from its beacon."""
apps = get_local_apps(params)
for i, app in enumerate(apps):
if app.app_id != app_id or app.endpoint == endpoint:
continue
apps[i] = LocalApp(app_id=app.app_id, endpoint=endpoint,
app_name=app.app_name, alias=app.alias, paired_at=app.paired_at)
_save_local_apps(apps, params)
cloudlog.event("local_pairing.app_endpoint_refreshed", app_id=app_id, endpoint=endpoint)
return True
return False
def set_local_app_alias(app_id: str, alias: str, params: Params | None = None) -> bool:
apps = get_local_apps(params)
for i, app in enumerate(apps):
if app.app_id != app_id:
continue
if app.alias == alias:
return False
apps[i] = LocalApp(app_id=app.app_id, endpoint=app.endpoint,
app_name=app.app_name, alias=alias, paired_at=app.paired_at)
_save_local_apps(apps, params)
cloudlog.event("local_pairing.app_alias_updated", app_id=app_id, alias=alias)
return True
return False
def remove_local_app(app_id: str, params: Params | None = None) -> bool:
"""Unpair an app by id. Returns True when an app was removed."""
apps = get_local_apps(params)
remaining = [app for app in apps if app.app_id != app_id]
if len(remaining) == len(apps):
return False
_save_local_apps(remaining, params)
cloudlog.event("local_pairing.app_unpaired", app_id=app_id)
return True
def remove_all_local_apps(params: Params | None = None) -> None:
"""Unpair every app."""
_save_local_apps([], params)
cloudlog.event("local_pairing.all_apps_unpaired")
def generate_pairing_code() -> str:
"""A 6-digit numeric pairing code."""
return "".join(secrets.choice(PAIRING_CODE_ALPHABET) for _ in range(PAIRING_CODE_LENGTH))
def _write_pairing_code(code: str, params: Params) -> None:
"""Persist the code with its armed-at monotonic timestamp — the window is derived from it."""
params.put(PAIRING_CODE_KEY, {"code": code, "ts": int(time.monotonic())}, block=True)
def read_pairing_code(params: Params | None = None) -> str | None:
"""The stored pairing code, or None when cleared / not yet generated."""
params = params or Params()
data = params.get(PAIRING_CODE_KEY)
if not isinstance(data, dict):
return None
code = data.get("code")
return str(code) if code else None
def get_pairing_code(params: Params | None = None) -> str:
"""The displayed pairing code, generating and persisting one on first use."""
params = params or Params()
code = read_pairing_code(params)
if code is None:
code = generate_pairing_code()
_write_pairing_code(code, params)
return code
def pairing_requested(params: Params | None = None) -> bool:
"""True while the pairing window is armed and fresh.
Self-expiring: if the code (which carries the armed-at timestamp) is missing
or older than PAIRING_WINDOW_S, the flag is dropped here."""
params = params or Params()
if not params.get_bool(PAIRING_REQUEST_KEY):
return False
data = params.get(PAIRING_CODE_KEY)
ts = data.get("ts") if isinstance(data, dict) else None
if not isinstance(ts, (int, float)):
clear_pairing_request(params)
return False
age = time.monotonic() - ts
# Negative age = armed before the last reboot (monotonic restarts at boot).
if age < 0 or age > PAIRING_WINDOW_S:
clear_pairing_request(params)
return False
return True
def arm_pairing(params: Params | None = None) -> str:
"""Arm a pairing window and return the code for the app.
Rolls a fresh code first, then sets the flag, so pairing_requested never
sees an armed flag without a valid code."""
params = params or Params()
code = generate_pairing_code()
_write_pairing_code(code, params)
params.put_bool(PAIRING_REQUEST_KEY, True, block=True)
return code
def clear_pairing_request(params: Params | None = None) -> None:
"""Close the pairing window: drop the request flag and the code together."""
params = params or Params()
params.remove(PAIRING_REQUEST_KEY)
params.remove(PAIRING_CODE_KEY)
def verify_pairing_code(code: str, params: Params | None = None) -> bool:
"""Constant-time check of a code typed into the app against the displayed one."""
params = params or Params()
current = read_pairing_code(params)
if current is None:
return False
return secrets.compare_digest(str(code).strip().upper(), current)
class PairingCodeRotator(threading.Thread):
"""Re-roll the displayed code while a pairing window is armed; clear it
otherwise — the code is never generated outside a window."""
def __init__(self, params: Params | None = None, rotation_s: float = DEFAULT_CODE_ROTATION_S,
stop_event: threading.Event | None = None, tick_cb: Callable[[], None] | None = None):
super().__init__(name="local_pairing_code_rotator", daemon=True)
self.params = params or Params()
self.rotation_s = rotation_s
self.stop_event = stop_event or threading.Event()
# Test seam: invoked once per loop iteration after state is updated.
self.tick_cb = tick_cb
def rotate(self) -> None:
"""Re-roll the code while the window is armed, clear it otherwise."""
if pairing_requested(self.params):
_write_pairing_code(generate_pairing_code(), self.params)
else:
self.params.remove(PAIRING_CODE_KEY)
def run(self) -> None:
self.rotate()
while not self.stop_event.wait(self.rotation_s):
try:
self.rotate()
if self.tick_cb is not None:
self.tick_cb()
except Exception:
cloudlog.exception("local_pairing.code_rotator.exception")
def format_endpoint(host: str, ws_port: int = SUNNYLINK_LOCAL_WS_PORT) -> str:
return f"ws://{host}:{ws_port}"
def local_identity(params: Params | None = None) -> str:
"""Identity claim on local connections. DongleId always exists on comma
hardware (SunnylinkDongleId is "UnregisteredDevice" until cloud
registration) and is what the app matches against the backend device list
to dedupe cloud + local entries."""
params = params or Params()
return params.get("DongleId") or params.get("HardwareSerial") or ""
@@ -30,26 +30,10 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce
import openpilot.cereal.messaging as messaging
from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL
from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param
from openpilot.system.athena import rpc as rpc_module
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi
from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string
from openpilot.sunnypilot.sunnylink.capabilities import generate_capabilities, CAPABILITY_LABELS
from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import generate_schema
from openpilot.sunnypilot.sunnylink.athena.local_discovery import LOCAL_BEACON_FRESH_S, AppBeacon, LocalDiscovery
from openpilot.sunnypilot.sunnylink.athena.local_pairing import (
PAIRING_WINDOW_S,
LocalApp,
PairingCodeRotator,
add_local_app,
clear_pairing_request,
get_local_apps,
is_locally_paired,
local_identity,
pairing_requested,
remove_local_app,
set_local_app_alias,
verify_pairing_code,
)
SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://athena.sunnylink.ai')
HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4"))
@@ -58,15 +42,6 @@ SUNNYLINK_LOG_ATTR_NAME = "user.sunny.upload"
SUNNYLINK_RECONNECT_TIMEOUT_S = 70 # FYI changing this will also would require a change on sidebar.cc
DISALLOW_LOG_UPLOAD = threading.Event()
LOCAL_PAIRING_SESSION_TIMEOUT_S = PAIRING_WINDOW_S
LOCAL_PROBE_INTERVAL_S = 60
LOCAL_ENDPOINT_BACKOFF_S = 300
PAIRING_WATCHDOG_INTERVAL_S = 2.0
_active_local_endpoint: str | None = None
_active_ws: WebSocket | None = None
_pairing_in_progress = threading.Event()
params = Params()
# Parameters that should never be remotely modified
@@ -291,302 +266,44 @@ def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local
return start_local_proxy_shim(global_end_event, local_port, ws)
@dispatcher.add_method
def pairLocalApp(code: str, app_id: str = "", app_name: str = "", alias: str = "") -> dict[str, bool | str]:
"""Complete pairing with the app on the CURRENT local connection."""
if _active_local_endpoint is None:
return {"success": False, "error": "not connected to a local app"}
if not verify_pairing_code(code):
cloudlog.warning("sunnylinkd.pairLocalApp.invalid_code")
return {"success": False, "error": "invalid code"}
add_local_app(LocalApp(app_id=app_id or f"app@{_active_local_endpoint}",
endpoint=_active_local_endpoint, app_name=app_name, alias=alias))
clear_pairing_request()
return {"success": True}
@dispatcher.add_method
def updateLocalAppAlias(app_id: str, alias: str) -> dict[str, bool | str]:
if _active_local_endpoint is None:
return {"success": False, "error": "not connected to a local app"}
updated = set_local_app_alias(app_id, alias)
return {"success": True, "updated": updated}
@dispatcher.add_method
def unpairLocalApp(app_id: str) -> dict[str, bool | str]:
if _active_local_endpoint is None:
return {"success": False, "error": "not connected to a local app"}
removed = remove_local_app(app_id)
return {"success": True, "removed": removed}
def _auth_header(is_local: bool) -> dict[str, str]:
"""Bearer header for a dial."""
api = SunnylinkApi(params.get("SunnylinkDongleId"))
payload = {"identity": local_identity()} if is_local else None
return {"Authorization": f"Bearer {api.get_token(payload_extra=payload)}"}
def _pairing_session(ws: WebSocket, timeout_s: float = LOCAL_PAIRING_SESSION_TIMEOUT_S) -> bool:
"""Serve only the pairing RPCs to an app that isn't in the registry yet —
everything else is refused. Returns True if pairing completed (the connection may then
serve normally)."""
cloudlog.info("sunnylinkd.pairing_session.started")
ws.settimeout(10)
deadline = time.monotonic() + timeout_s
try:
while time.monotonic() < deadline and pairing_requested():
try:
raw = ws.recv() # auto-pongs pings; blocks up to the socket timeout
except WebSocketTimeoutException:
continue
except Exception as e:
cloudlog.warning(f"sunnylinkd.pairing_session.{type(e).__name__}")
return is_locally_paired()
try:
msg = rpc_module.loads(raw)
except Exception:
continue
if not rpc_module.is_call(msg):
continue
if msg.get("method") not in ("pairLocalApp", "unpairLocalApp"):
continue # refuse anything but pairing until paired
try:
ws.send(rpc_module.handle(msg, dispatcher))
except Exception as e:
cloudlog.warning(f"sunnylinkd.pairing_session.{type(e).__name__}")
return is_locally_paired()
return is_locally_paired()
finally:
ws.settimeout(SUNNYLINK_RECONNECT_TIMEOUT_S)
def _pick_ws_uri(discovery: LocalDiscovery, backoffs: dict[str, float]) -> tuple[str, str]:
now = time.monotonic()
apps = get_local_apps()
app_ids = {app.app_id for app in apps}
if pairing_requested():
latest = discovery.latest_endpoint()
seen = discovery.last_seen_ago()
app_id = discovery.latest_app_id()
if latest is not None and seen is not None and seen <= LOCAL_BEACON_FRESH_S \
and app_id is not None and app_id not in app_ids \
and backoffs.get(latest, 0.0) <= now:
return latest, "pairing_offer"
return SUNNYLINK_ATHENA_HOST, "cloud"
fresh_endpoint = discovery.latest_paired_endpoint()
fresh_seen = discovery.latest_paired_seen_ago()
fresh_app_id = discovery.latest_paired_app_id()
fresh_ok = (fresh_endpoint is not None and fresh_seen is not None
and fresh_seen <= LOCAL_BEACON_FRESH_S and fresh_app_id is not None
and fresh_app_id in app_ids)
if fresh_ok and backoffs.get(fresh_endpoint, 0.0) <= now:
return fresh_endpoint, "paired_local"
for app in reversed(apps):
if fresh_ok and app.app_id == fresh_app_id:
continue
if backoffs.get(app.endpoint, 0.0) <= now:
return app.endpoint, "paired_local"
return SUNNYLINK_ATHENA_HOST, "cloud"
def _probe_local_apps(active_ws: WebSocket, discovery: LocalDiscovery,
backoffs: dict[str, float], stop_event: threading.Event) -> None:
while not stop_event.wait(LOCAL_PROBE_INTERVAL_S):
if pairing_requested():
# The pairing watchdog owns an armed window; don't migrate mid-window.
continue
now = time.monotonic()
candidate: str | None = None
for app in reversed(get_local_apps()):
if backoffs.get(app.endpoint, 0.0) <= now:
candidate = app.endpoint
break
if candidate is None:
continue
try:
probe = create_connection(candidate, header=_auth_header(is_local=True), timeout=10)
probe.close()
except Exception:
backoffs[candidate] = now + LOCAL_ENDPOINT_BACKOFF_S
continue
cloudlog.event("sunnylinkd.local_probe.reachable", endpoint=candidate)
try:
active_ws.close()
except Exception:
pass
break
def _handle_paired_refresh(backoffs: dict[str, float], force_attempts: dict[str, float],
beacon: AppBeacon) -> None:
if not any(app.app_id == beacon.app_id for app in get_local_apps()):
return
for app in get_local_apps():
if app.app_id == beacon.app_id:
backoffs.pop(app.endpoint, None)
backoffs.pop(beacon.endpoint, None)
if _pairing_in_progress.is_set():
return
if _active_local_endpoint == beacon.endpoint:
return
if _active_local_endpoint is not None:
return
now = time.monotonic()
if force_attempts.get(beacon.endpoint, 0.0) + LOCAL_BEACON_FRESH_S > now:
return
force_attempts[beacon.endpoint] = now
ws = _active_ws
if ws is not None:
cloudlog.event("sunnylinkd.paired_refresh.reconnect",
app_id=beacon.app_id, endpoint=beacon.endpoint)
try:
ws.close()
except Exception:
pass
def _pairing_watchdog(active_ws: WebSocket, backoffs: dict[str, float],
stop_event: threading.Event,
interval_s: float = PAIRING_WATCHDOG_INTERVAL_S) -> None:
"""Watch for the pairing window being armed mid-session and force a
re-selection to the newly-discovered app."""
while not stop_event.wait(interval_s):
if not pairing_requested():
continue
cloudlog.event("sunnylinkd.pairing_watchdog.arm_detected")
for key in list(backoffs):
backoffs.pop(key, None)
try:
active_ws.close()
except Exception:
pass
break
def main(exit_event: threading.Event | None = None):
try:
set_core_affinity([0, 1, 2, 3])
except Exception:
cloudlog.exception("failed to set core affinity")
discovery = LocalDiscovery()
code_rotator = PairingCodeRotator()
discovery.start()
code_rotator.start()
try:
_connection_loop(exit_event, discovery)
finally:
discovery.stop()
code_rotator.stop_event.set()
def _serviceable(params: Params) -> bool:
"""sunnylinkd should run when sunnylink is enabled and not on a temporary
fault. This deliberately includes the unregistered/unpaired state so a
never-registered device can still be discovered and paired over the LAN (the
actual session gates — registration/local pairing — are handled per
connection inside the loop)."""
return params.get_bool("SunnylinkEnabled") and not params.get_bool("SunnylinkTempFault")
def _connection_loop(exit_event: threading.Event | None, discovery: LocalDiscovery) -> None:
"""Local-first, cloud-fallback connection loop: a paired local endpoint
first, cloud when unreachable, and a pairing session to a freshly-discovered
app when a window is armed."""
global _active_local_endpoint, _active_ws
while sunnylink_need_register(params):
cloudlog.info("Waiting for sunnylink registration to complete")
time.sleep(10)
sunnylink_dongle_id = params.get("SunnylinkDongleId")
sunnylink_api = SunnylinkApi(sunnylink_dongle_id)
UploadQueueCache.initialize(upload_queue)
update_car_list_param()
ws_uri = f"{SUNNYLINK_ATHENA_HOST}"
conn_start = None
conn_retries = 0
backoffs: dict[str, float] = {}
force_attempts: dict[str, float] = {}
discovery.paired_refresh_cb = partial(_handle_paired_refresh, backoffs, force_attempts)
while (exit_event is None or not exit_event.is_set()) and _serviceable(params):
ws_uri, kind = _pick_ws_uri(discovery, backoffs)
if kind == "cloud" and pairing_requested():
cloudlog.debug("sunnylinkd.main.pairing_waiting_for_beacon")
time.sleep(3)
continue
if kind == "cloud" and sunnylink_need_register(params):
cloudlog.info("Waiting for sunnylink registration or local pairing to complete")
time.sleep(10)
continue
if conn_start is None:
conn_start = time.monotonic()
cloudlog.event("sunnylinkd.main.connecting_ws", ws_uri=ws_uri, kind=kind, retries=conn_retries)
while (exit_event is None or not exit_event.is_set()) and sunnylink_ready(params):
try:
if conn_start is None:
conn_start = time.monotonic()
cloudlog.event("sunnylinkd.main.connecting_ws", ws_uri=ws_uri, retries=conn_retries)
ws = create_connection(
ws_uri,
header=_auth_header(is_local=kind != "cloud"),
header={"Authorization": f"Bearer {sunnylink_api.get_token()}"},
enable_multithread=True,
sslopt={"cert_reqs": ssl.CERT_NONE if "localhost" in ws_uri else ssl.CERT_REQUIRED},
timeout=SUNNYLINK_RECONNECT_TIMEOUT_S,
)
except Exception as e:
if kind != "cloud":
backoffs[ws_uri] = time.monotonic() + LOCAL_ENDPOINT_BACKOFF_S
conn_retries += 1
params.remove("LastSunnylinkPingTime")
_log_connection_error(e)
time.sleep(backoff(conn_retries))
continue
cloudlog.event("sunnylinkd.main.connected_ws", ws_uri=ws_uri, retries=conn_retries,
duration=time.monotonic() - conn_start)
conn_start = None
cloudlog.event("sunnylinkd.main.connected_ws", ws_uri=ws_uri, kind=kind, retries=conn_retries,
duration=time.monotonic() - conn_start)
conn_start = None
conn_retries = 0
cur_upload_items.clear()
_active_ws = ws
probe_stop: threading.Event | None = None
watch_stop: threading.Event | None = None
session_endpoint: str | None = ws_uri if kind != "cloud" else None
try:
if kind == "pairing_offer":
_active_local_endpoint = ws_uri
_pairing_in_progress.set()
try:
paired_ok = _pairing_session(ws)
finally:
_pairing_in_progress.clear()
if not paired_ok:
backoffs[ws_uri] = time.monotonic() + LOCAL_ENDPOINT_BACKOFF_S
conn_retries += 1
params.remove("LastSunnylinkPingTime")
try:
ws.close()
except Exception:
pass
time.sleep(backoff(conn_retries))
continue
# Paired during the session — this connection may now serve normally.
kind = "paired_local"
if kind == "paired_local":
_active_local_endpoint = ws_uri
else:
_active_local_endpoint = None
# While on the cloud link, watch for the local app and migrate back.
probe_stop = threading.Event()
threading.Thread(target=_probe_local_apps,
args=(ws, discovery, backoffs, probe_stop),
name="sunnylinkd_local_probe", daemon=True).start()
# Started after any pairing session on this connection, so it can never
# close the connection the code is typed over.
watch_stop = threading.Event()
threading.Thread(target=_pairing_watchdog, args=(ws, backoffs, watch_stop),
name="sunnylinkd_pairing_watchdog", daemon=True).start()
conn_retries = 0
cur_upload_items.clear()
handle_long_poll(ws, exit_event)
except (KeyboardInterrupt, SystemExit):
@@ -594,37 +311,23 @@ def _connection_loop(exit_event: threading.Event | None, discovery: LocalDiscove
except Exception as e:
conn_retries += 1
params.remove("LastSunnylinkPingTime")
_log_connection_error(e)
finally:
if probe_stop is not None:
probe_stop.set()
if watch_stop is not None:
watch_stop.set()
if session_endpoint is not None and kind == "paired_local":
backoffs.pop(session_endpoint, None)
if _active_local_endpoint == session_endpoint:
_active_local_endpoint = None
if _active_ws is ws:
_active_ws = None
if isinstance(e, (ConnectionError, TimeoutError, WebSocketException)):
cloudlog.warning(f"sunnylinkd.main.{type(e).__name__}")
elif isinstance(e, OSError):
name = errno.errorcode.get(e.errno or -1, "UNKNOWN")
msg = f"sunnylinkd.main.OSError.{name} ({e.errno})"
is_expected_error = e.errno in (errno.ENETDOWN, errno.ENETRESET, errno.ENETUNREACH)
cloudlog.warning(msg) if is_expected_error else cloudlog.exception(msg)
else:
cloudlog.exception("sunnylinkd.main.exception")
time.sleep(backoff(conn_retries))
if not _serviceable(params):
cloudlog.debug("Reached end of sunnylinkd.main while sunnylink is not serviceable. Waiting 60s before retrying")
if not sunnylink_ready(params):
cloudlog.debug("Reached end of sunnylinkd.main while sunnylink is not ready. Waiting 60s before retrying")
time.sleep(60)
def _log_connection_error(e: Exception) -> None:
if isinstance(e, (ConnectionError, TimeoutError, WebSocketException)):
cloudlog.warning(f"sunnylinkd.main.{type(e).__name__}")
elif isinstance(e, OSError):
name = errno.errorcode.get(e.errno or -1, "UNKNOWN")
msg = f"sunnylinkd.main.OSError.{name} ({e.errno})"
is_expected_error = e.errno in (errno.ENETDOWN, errno.ENETRESET, errno.ENETUNREACH)
cloudlog.warning(msg) if is_expected_error else cloudlog.exception(msg)
else:
cloudlog.exception("sunnylinkd.main.exception")
if __name__ == "__main__":
main()
+4 -7
View File
@@ -2,7 +2,6 @@ import base64
import gzip
import json
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID
from openpilot.sunnypilot.sunnylink.athena.local_pairing import is_locally_paired
from openpilot.common.params import Params, ParamKeyType
from openpilot.common.version import is_prebuilt
@@ -17,11 +16,10 @@ def get_sunnylink_status(params=None) -> tuple[bool, bool, bool]:
def sunnylink_ready(params=None) -> bool:
"""Enabled and (cloud-registered or locally paired), and not on a temporary
fault. Local pairing makes never-registered devices usable over the LAN."""
"""Check if the device is ready to communicate with Sunnylink. That means it is enabled and registered."""
params = params or Params()
is_sunnylink_enabled, is_registered, is_on_temporary_fault = get_sunnylink_status(params)
return is_sunnylink_enabled and (is_registered or is_locally_paired(params)) and not is_on_temporary_fault
return is_sunnylink_enabled and is_registered and not is_on_temporary_fault
def use_sunnylink_uploader(params) -> bool:
@@ -30,11 +28,10 @@ def use_sunnylink_uploader(params) -> bool:
def sunnylink_need_register(params=None) -> bool:
"""Enabled, unregistered, and not locally paired — a locally paired device
works without cloud registration and must not be blocked."""
"""Check if the device needs to be registered with Sunnylink."""
params = params or Params()
is_sunnylink_enabled, is_registered, is_on_temporary_fault = get_sunnylink_status(params)
return is_sunnylink_enabled and not is_registered and not is_locally_paired(params) and not is_on_temporary_fault
return is_sunnylink_enabled and not is_registered and not is_on_temporary_fault
def register_sunnylink():