Compare commits

...

189 Commits

Author SHA1 Message Date
royjr 0d710c3451 bett 2026-08-13 17:58:47 -04:00
royjr b7a8cf8777 more 2026-08-12 13:38:47 -04:00
royjr 57d490e5c0 buckets 2026-08-11 19:25:44 -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
Kumar 2d859a8ca6 Controls: Road Edge Lane Change Controller (#1085)
* relc

* so picky

* clean up

* switch to distance based

* same calls

* all you brother i'm just here to red diff

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-11 10:29:14 -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
20 changed files with 744 additions and 65 deletions
+3
View File
@@ -351,6 +351,7 @@ struct OnroadEventSP @0xda96579883444c35 {
speedLimitChanged @21;
speedLimitPending @22;
e2eChime @23;
laneChangeRoadEdge @24;
}
}
@@ -457,6 +458,8 @@ struct LiveMapDataSP @0xf416ec09499d9d19 {
struct ModelDataV2SP @0xa1680744031fdb2d {
laneTurnDirection @0 :TurnDirection;
leftLaneChangeEdgeBlock @1 :Bool;
rightLaneChangeEdgeBlock @2 :Bool;
enum TurnDirection {
none @0;
+13
View File
@@ -2309,6 +2309,19 @@ 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;
+1
View File
@@ -179,6 +179,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RoadEdgeLaneChangeEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ScreenSaverEnabled", {PERSISTENT | BACKUP, BOOL, "1"}},
{"ScreenSaverTimeout", {PERSISTENT | BACKUP, INT, "300"}},
@@ -33,7 +33,7 @@ class DesireHelper:
def get_lane_change_direction(CS):
return LaneChangeDirection.left if CS.leftBlinker else LaneChangeDirection.right
def update(self, carstate, lateral_active, lane_change_prob):
def update(self, carstate, lateral_active, lane_change_prob, left_edge_detected=False, right_edge_detected=False):
self.alc.update_params()
self.lane_turn_controller.update_params()
v_ego = carstate.vEgo
@@ -64,8 +64,8 @@ class DesireHelper:
((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or
(carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right))
blindspot_detected = ((carstate.leftBlindspot and self.lane_change_direction == LaneChangeDirection.left) or
(carstate.rightBlindspot and self.lane_change_direction == LaneChangeDirection.right))
blindspot_detected = (((carstate.leftBlindspot or left_edge_detected) and self.lane_change_direction == LaneChangeDirection.left) or
((carstate.rightBlindspot or right_edge_detected) and self.lane_change_direction == LaneChangeDirection.right))
self.alc.update_lane_change(blindspot_detected, carstate.brakePressed)
+135 -44
View File
@@ -16,13 +16,21 @@ from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose, fft_next_good_size, parabolic_peak_interp
from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle
BLOCK_SIZE = 100
BLOCK_NUM = 50
BLOCK_NUM_NEEDED = 5
MOVING_WINDOW_SEC = 60.0
MIN_OKAY_WINDOW_SEC = 25.0
BLOCK_SIZE = 8
BLOCK_NUM = 30
BLOCK_NUM_NEEDED = 3
MOVING_WINDOW_SEC = 20.0
MIN_OKAY_WINDOW_SEC = 5.0
MIN_RECOVERY_BUFFER_SEC = 2.0
MIN_VEGO = 50.0 * CV.MPH_TO_MS
SPEED_BUCKET_EDGES = np.arange(0.0, 90.0, 10.0) * CV.MPH_TO_MS
def interpolate_bucket_values(speed: float, edges: np.ndarray, values: list[float]) -> float:
if len(values) == 1:
return values[0]
widths = np.append(np.diff(edges), edges[-1] - edges[-2])
return float(np.interp(speed, edges + widths / 2, values))
MIN_ABS_YAW_RATE = 0.0
MAX_YAW_RATE_SANITY_CHECK = 1.0
MIN_NCC = 0.95
@@ -31,14 +39,14 @@ MIN_LAG = 0.15
MAX_LAG_STD = 0.1
MAX_LAT_ACCEL = 2.0
MAX_LAT_ACCEL_DIFF = 0.6
MIN_LAT_ACCEL_RANGE = 0.5
MIN_LAT_ACCEL_RANGE = 0.1
MIN_CONFIDENCE = 0.7
CORR_BORDER_OFFSET = 5
LAG_CANDIDATE_CORR_THRESHOLD = 0.9
SMOOTH_K = 5
SMOOTH_SIGMA = 1.0
VERSION = 1 # bump this to invalidate old parameter caches
VERSION = 3 # bump this to invalidate old parameter caches
def masked_symmetric_moving_average(x: np.ndarray, mask: np.ndarray, k: int, sigma: float) -> np.ndarray:
@@ -152,6 +160,12 @@ class BlockAverage:
self.block_idx = (self.block_idx + 1) % self.num_blocks
self.valid_blocks = min(self.valid_blocks + 1, self.num_blocks)
def restore(self, values: list[float], block_idx: int, idx: int):
assert len(values) == self.num_blocks and 0 <= block_idx < self.num_blocks and 0 <= idx < self.block_size
self.values = np.asarray(values, dtype=float).reshape(self.num_blocks, 1)
self.block_idx = block_idx
self.idx = idx
def get(self) -> tuple[float, float, float, float]:
valid_block_idx = [i for i in range(self.valid_blocks) if i != self.block_idx]
valid_and_current_idx = valid_block_idx + ([self.block_idx] if self.idx > 0 else [])
@@ -178,7 +192,8 @@ class LateralLagEstimator:
block_count: int = BLOCK_NUM, min_valid_block_count: int = BLOCK_NUM_NEEDED, block_size: int = BLOCK_SIZE,
window_sec: float = MOVING_WINDOW_SEC, okay_window_sec: float = MIN_OKAY_WINDOW_SEC, min_recovery_buffer_sec: float = MIN_RECOVERY_BUFFER_SEC,
min_vego: float = MIN_VEGO, min_yr: float = MIN_ABS_YAW_RATE, min_ncc: float = MIN_NCC,
max_lat_accel: float = MAX_LAT_ACCEL, max_lat_accel_diff: float = MAX_LAT_ACCEL_DIFF, min_confidence: float = MIN_CONFIDENCE):
max_lat_accel: float = MAX_LAT_ACCEL, max_lat_accel_diff: float = MAX_LAT_ACCEL_DIFF, min_confidence: float = MIN_CONFIDENCE,
speed_bucket_edges: np.ndarray | None = None):
self.dt = dt
self.window_sec = window_sec
self.okay_window_sec = okay_window_sec
@@ -193,6 +208,8 @@ class LateralLagEstimator:
self.min_confidence = min_confidence
self.max_lat_accel = max_lat_accel
self.max_lat_accel_diff = max_lat_accel_diff
self.speed_bucket_edges = np.asarray(speed_bucket_edges if speed_bucket_edges is not None else [0.0], dtype=float)
assert len(self.speed_bucket_edges) > 0 and self.speed_bucket_edges[0] == 0.0 and np.all(np.diff(self.speed_bucket_edges) > 0)
self.t = 0.0
self.lat_active = False
@@ -208,7 +225,9 @@ class LateralLagEstimator:
self.last_steering_pressed_t = 0.0
self.last_steering_saturated_t = 0.0
self.last_pose_invalid_t = 0.0
self.last_estimate_t = 0.0
self.last_estimate_ts = [0.0] * len(self.speed_bucket_edges)
self.consecutive_okay = [0] * len(self.speed_bucket_edges)
self.learning_reset_reasons = ["not started"] * len(self.speed_bucket_edges)
self.calibrator = PoseCalibrator()
@@ -216,8 +235,12 @@ class LateralLagEstimator:
def reset(self, initial_lag: float, valid_blocks: int):
window_len = int(self.window_sec / self.dt)
self.points = Points(window_len)
self.block_avg = BlockAverage(self.block_count, self.block_size, valid_blocks, initial_lag)
self.points = [Points(window_len) for _ in self.speed_bucket_edges]
self.block_avgs = [BlockAverage(self.block_count, self.block_size, valid_blocks, initial_lag) for _ in self.speed_bucket_edges]
@property
def speed_bucket(self) -> int:
return max(0, int(np.searchsorted(self.speed_bucket_edges, self.v_ego, side="right") - 1))
def get_msg(self, valid: bool, debug: bool = False) -> capnp._DynamicStructBuilder:
msg = messaging.new_message('liveDelay')
@@ -226,19 +249,23 @@ class LateralLagEstimator:
liveDelay = msg.liveDelay
valid_mean_lag, valid_std, current_mean_lag, current_std = self.block_avg.get()
if self.block_avg.valid_blocks >= self.min_valid_block_count and not np.isnan(valid_mean_lag) and not np.isnan(valid_std):
if valid_std > MAX_LAG_STD:
liveDelay.status = log.LiveDelayData.Status.invalid
bucket_values = [avg.get() for avg in self.block_avgs]
bucket_statuses = []
bucket_applied = []
for avg, (valid_mean, valid_std, _, _) in zip(self.block_avgs, bucket_values, strict=True):
if avg.valid_blocks < self.min_valid_block_count or np.isnan(valid_mean) or np.isnan(valid_std):
status = "unestimated"
elif valid_std > MAX_LAG_STD:
status = "invalid"
else:
liveDelay.status = log.LiveDelayData.Status.estimated
else:
liveDelay.status = log.LiveDelayData.Status.unestimated
status = "estimated"
bucket_statuses.append(status)
bucket_applied.append(min(MAX_LAG, max(MIN_LAG, valid_mean)) if status == "estimated" else self.initial_lag)
if liveDelay.status == log.LiveDelayData.Status.estimated:
liveDelay.lateralDelay = min(MAX_LAG, max(MIN_LAG, valid_mean_lag))
else:
liveDelay.lateralDelay = self.initial_lag
block_avg = self.block_avgs[self.speed_bucket]
_, _, current_mean_lag, current_std = bucket_values[self.speed_bucket]
liveDelay.status = bucket_statuses[self.speed_bucket]
liveDelay.lateralDelay = interpolate_bucket_values(self.v_ego, self.speed_bucket_edges, bucket_applied)
if not np.isnan(current_mean_lag) and not np.isnan(current_std):
liveDelay.lateralDelayEstimate = current_mean_lag
@@ -247,11 +274,25 @@ class LateralLagEstimator:
liveDelay.lateralDelayEstimate = self.initial_lag
liveDelay.lateralDelayEstimateStd = 0.0
liveDelay.validBlocks = self.block_avg.valid_blocks
liveDelay.calPerc = min(100 * (self.block_avg.valid_blocks * self.block_size + self.block_avg.idx) //
liveDelay.validBlocks = block_avg.valid_blocks
liveDelay.calPerc = min(100 * (block_avg.valid_blocks * self.block_size + block_avg.idx) //
(self.min_valid_block_count * self.block_size), 100)
liveDelay.speedBucket = self.speed_bucket
liveDelay.speedBucketEdges = self.speed_bucket_edges.tolist()
liveDelay.lateralDelayBuckets = [min(MAX_LAG, max(MIN_LAG, value[2])) if not np.isnan(value[2]) else self.initial_lag for value in bucket_values]
liveDelay.lateralDelayEstimateStdBuckets = [value[3] if not np.isnan(value[3]) else 0.0 for value in bucket_values]
liveDelay.validBlocksBuckets = [avg.valid_blocks for avg in self.block_avgs]
liveDelay.calPercBuckets = [min(100 * (avg.valid_blocks * self.block_size + avg.idx) //
(self.min_valid_block_count * self.block_size), 100) for avg in self.block_avgs]
liveDelay.statusBuckets = bucket_statuses
liveDelay.lateralDelayAppliedBuckets = bucket_applied
liveDelay.blockIdxBuckets = [avg.block_idx for avg in self.block_avgs]
liveDelay.sampleIdxBuckets = [avg.idx for avg in self.block_avgs]
liveDelay.blockValuesBuckets = [avg.values.flatten().tolist() for avg in self.block_avgs]
liveDelay.learningCountdownBuckets = [max(0.0, self.okay_window_sec - count * self.dt) for count in self.consecutive_okay]
liveDelay.learningResetReasonBuckets = self.learning_reset_reasons
if debug:
liveDelay.points = self.block_avg.values.flatten().tolist()
liveDelay.points = np.concatenate([avg.values.flatten() for avg in self.block_avgs]).tolist()
liveDelay.version = VERSION
return msg
@@ -275,11 +316,12 @@ class LateralLagEstimator:
self.pose_valid = msg.angularVelocityDevice.valid and msg.posenetOK and msg.inputsOK
self.t = t
def points_enough(self):
return self.points.num_points >= int(self.okay_window_sec / self.dt)
def points_enough(self, points: Points):
return points.num_points >= int(self.okay_window_sec / self.dt)
def points_valid(self):
return self.points.num_okay >= int(self.okay_window_sec / self.dt)
def points_valid(self, points: Points, speed_bucket: int):
required_points = int(self.okay_window_sec / self.dt)
return points.num_okay >= required_points and self.consecutive_okay[speed_bucket] >= required_points
def update_points(self):
la_desired = self.desired_curvature * self.v_ego * self.v_ego
@@ -307,17 +349,47 @@ class LateralLagEstimator:
okay = self.lat_active and not self.steering_pressed and not self.steering_saturated and \
fast and turning and has_recovered and calib_valid and sensors_valid and la_valid
self.points.update(self.t, la_desired, la_actual_pose, okay)
speed_bucket = self.speed_bucket
for i, points in enumerate(self.points):
in_bucket_okay = okay and i == speed_bucket
points.update(self.t, la_desired, la_actual_pose, in_bucket_okay)
if in_bucket_okay:
self.consecutive_okay[i] += 1
if self.consecutive_okay[i] >= int(self.okay_window_sec / self.dt):
self.learning_reset_reasons[i] = ""
else:
self.consecutive_okay[i] = 0
if i != speed_bucket:
self.learning_reset_reasons[i] = "outside speed band"
elif self.steering_pressed:
self.learning_reset_reasons[i] = "driver steering"
elif not self.lat_active:
self.learning_reset_reasons[i] = "lateral inactive"
elif self.steering_saturated:
self.learning_reset_reasons[i] = "steering saturated"
elif not calib_valid:
self.learning_reset_reasons[i] = "calibration invalid"
elif not sensors_valid:
self.learning_reset_reasons[i] = "pose invalid"
elif not la_valid:
self.learning_reset_reasons[i] = "lateral error"
elif not fast:
self.learning_reset_reasons[i] = "speed too low"
elif not turning:
self.learning_reset_reasons[i] = "insufficient yaw"
def update_estimate(self):
if not self.points_enough():
speed_bucket = self.speed_bucket
points = self.points[speed_bucket]
if not self.points_enough(points):
return
times, desired, actual, okay = self.points.get()
times, desired, actual, okay = points.get()
# check if there are any new valid data points since the last update
is_valid = self.points_valid() and (actual.max() - actual.min() >= MIN_LAT_ACCEL_RANGE)
if self.last_estimate_t != 0 and times[0] <= self.last_estimate_t:
new_values_start_idx = next(-i for i, t in enumerate(reversed(times)) if t <= self.last_estimate_t)
is_valid = self.points_valid(points, speed_bucket) and (actual.max() - actual.min() >= MIN_LAT_ACCEL_RANGE)
last_estimate_t = self.last_estimate_ts[speed_bucket]
if last_estimate_t != 0 and times[0] <= last_estimate_t:
new_values_start_idx = next(-i for i, t in enumerate(reversed(times)) if t <= last_estimate_t)
is_valid = is_valid and not (new_values_start_idx == 0 or not np.any(okay[new_values_start_idx:]))
desired = masked_symmetric_moving_average(desired, okay, SMOOTH_K, SMOOTH_SIGMA)
@@ -327,8 +399,8 @@ class LateralLagEstimator:
if corr < self.min_ncc or confidence < self.min_confidence or not is_valid:
return
self.block_avg.update(delay)
self.last_estimate_t = self.t
self.block_avgs[speed_bucket].update(delay)
self.last_estimate_ts[speed_bucket] = self.t
@staticmethod
def actuator_delay(expected_sig: np.ndarray, actual_sig: np.ndarray, mask: np.ndarray,
@@ -373,11 +445,24 @@ def retrieve_initial_lag(params: Params, CP: car.CarParams):
if last_CP.carFingerprint != CP.carFingerprint:
raise Exception("Car model mismatch")
lag, valid_blocks, status, version = ld.lateralDelayEstimate, ld.validBlocks, ld.status, ld.version
assert valid_blocks <= BLOCK_NUM, "Invalid number of valid blocks"
lags, valid_blocks, status, version = list(ld.lateralDelayBuckets), list(ld.validBlocksBuckets), ld.status, ld.version
assert len(lags) == len(SPEED_BUCKET_EDGES) and len(valid_blocks) == len(SPEED_BUCKET_EDGES), "Invalid number of speed buckets"
assert all(blocks <= BLOCK_NUM for blocks in valid_blocks), "Invalid number of valid blocks"
assert status != log.LiveDelayData.Status.invalid, "Lag estimate is invalid"
assert version == VERSION, f"Lag estimate is from a different version (got {version}, expected {VERSION})"
return lag, valid_blocks
values = [list(v) for v in ld.blockValuesBuckets]
if len(values) == len(SPEED_BUCKET_EDGES):
block_indices, sample_indices = list(ld.blockIdxBuckets), list(ld.sampleIdxBuckets)
assert len(block_indices) == len(values) and len(sample_indices) == len(values), "Invalid saved bucket state"
assert all(len(v) == BLOCK_NUM for v in values), "Invalid saved block values"
else: # migrate version 3 state saved before per-bucket progress persistence
values = [[lag] * BLOCK_NUM for lag in lags]
block_indices = [blocks % BLOCK_NUM for blocks in valid_blocks]
sample_indices = [0] * len(valid_blocks)
active_bucket = ld.speedBucket
accepted = round(ld.calPerc * (BLOCK_NUM_NEEDED * BLOCK_SIZE) / 100) - valid_blocks[active_bucket] * BLOCK_SIZE
sample_indices[active_bucket] = min(max(accepted, 0), BLOCK_SIZE - 1)
return list(zip(values, block_indices, sample_indices, valid_blocks, strict=True))
except Exception as e:
cloudlog.error(f"Failed to retrieve initial lag: {e}")
params.remove("LiveDelay")
@@ -396,12 +481,15 @@ def main():
params = Params()
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
lag_learner = LateralLagEstimator(CP, 1. / SERVICE_LIST['livePose'].frequency)
lag_learner = LateralLagEstimator(CP, 1. / SERVICE_LIST['livePose'].frequency, min_vego=0.0, speed_bucket_edges=SPEED_BUCKET_EDGES)
if (initial_lag_params := retrieve_initial_lag(params, CP)) is not None:
lag, valid_blocks = initial_lag_params
lag_learner.reset(lag, valid_blocks)
for avg, (values, block_idx, idx, valid_blocks) in zip(lag_learner.block_avgs, initial_lag_params, strict=True):
avg.valid_blocks = valid_blocks
avg.restore(values, block_idx, idx)
lagd_toggle = LagdToggle(CP)
last_cache_t = 0.0
last_cached_progress = None
while True:
sm.update()
@@ -419,8 +507,11 @@ def main():
lag_msg_dat = lag_msg.to_bytes()
pm.send('liveDelay', lag_msg_dat)
if sm.frame % 1200 == 0: # cache every 60 seconds
progress = tuple((avg.valid_blocks, avg.idx) for avg in lag_learner.block_avgs)
if progress != last_cached_progress and lag_learner.t - last_cache_t >= 5.0:
params.put("LiveDelay", lag_msg_dat)
last_cache_t = lag_learner.t
last_cached_progress = progress
if sm.frame % 60 == 0: # read from and write to params every 3 seconds
lagd_toggle.update(lag_msg)
+81 -10
View File
@@ -1,3 +1,4 @@
import math
import random
import numpy as np
import time
@@ -6,11 +7,13 @@ import pytest
from openpilot.cereal import messaging, log
from opendbc.car.structs import car
from openpilot.selfdrive.locationd.lagd import LateralLagEstimator, retrieve_initial_lag, masked_normalized_cross_correlation, \
BLOCK_NUM_NEEDED, BLOCK_SIZE, MIN_OKAY_WINDOW_SEC, VERSION, MIN_LAG, MAX_LAG
BLOCK_NUM, BLOCK_NUM_NEEDED, BLOCK_SIZE, MIN_OKAY_WINDOW_SEC, VERSION, MIN_LAG, MAX_LAG
from openpilot.selfdrive.locationd.lagd import SPEED_BUCKET_EDGES, interpolate_bucket_values
from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_carParams
from openpilot.selfdrive.locationd.test.test_locationd_scenarios import TEST_ROUTE
from openpilot.common.params import Params
from openpilot.tools.lib.logreader import LogReader
from openpilot.tools.lagd_buckets import line_graph, ping_pong_metrics
from openpilot.common.hardware import PC
MAX_ERR_FRAMES = 1
@@ -18,9 +21,9 @@ DT = 0.05
LAGD_MIN_LAG_FRAMES, LAGD_MAX_LAG_FRAMES = int(round(MIN_LAG / DT)), int(round(MAX_LAG / DT))
def process_messages(estimator, lag_frames, n_frames, vego=25.0, rejection_threshold=0.0):
def process_messages(estimator, lag_frames, n_frames, vego=25.0, rejection_threshold=0.0, start_frame=0):
for i in range(n_frames):
t = i * estimator.dt
t = (start_frame + i) * estimator.dt
desired_la = np.cos(10 * t) * 0.3
actual_la = np.cos(10 * (t - lag_frames * estimator.dt)) * 0.3
@@ -46,6 +49,30 @@ def process_messages(estimator, lag_frames, n_frames, vego=25.0, rejection_thres
class TestLagd:
def test_interpolate_bucket_values(self):
edges = np.array([0.0, 10.0, 20.0])
values = [.2, .4, .3]
assert interpolate_bucket_values(0, edges, values) == .2
assert interpolate_bucket_values(10, edges, values) == pytest.approx(.3)
assert interpolate_bucket_values(20, edges, values) == pytest.approx(.35)
assert interpolate_bucket_values(30, edges, values) == .3
def test_ping_pong_metrics(self):
samples = [(i / 20, 2.5 * math.sin(2 * math.pi * 1.2 * i / 20)) for i in range(101)]
severity, amplitude, frequency, duration = ping_pong_metrics(samples)
assert severity == "MODERATE"
assert 2.4 < amplitude < 2.6
assert 1.0 < frequency < 1.3
assert duration == 5
def test_line_graph(self):
graph = line_graph([0.15, 0.4, 0.65], ["estimated", "unestimated", "invalid"], 1)
assert all(marker in graph for marker in ["", "", "×"])
assert "0.65s" in graph and "0.15s" in graph
assert "centers" in graph and "5" in graph
assert any("" < char <= "" for char in graph)
assert "" in line_graph([0.3, 0.3], ["estimated", "estimated"], -1)
def test_read_saved_params(self):
params = Params()
@@ -53,8 +80,11 @@ class TestLagd:
CP = next(m for m in lr if m.which() == "carParams").carParams
msg = messaging.new_message('liveDelay')
msg.liveDelay.lateralDelayEstimate = random.random()
msg.liveDelay.validBlocks = random.randint(1, 10)
msg.liveDelay.lateralDelayBuckets = [random.random() for _ in SPEED_BUCKET_EDGES]
msg.liveDelay.validBlocksBuckets = [random.randint(1, 10) for _ in SPEED_BUCKET_EDGES]
msg.liveDelay.blockValuesBuckets = [[random.random() for _ in range(BLOCK_NUM)] for _ in SPEED_BUCKET_EDGES]
msg.liveDelay.blockIdxBuckets = [random.randrange(BLOCK_NUM) for _ in SPEED_BUCKET_EDGES]
msg.liveDelay.sampleIdxBuckets = [random.randrange(BLOCK_SIZE) for _ in SPEED_BUCKET_EDGES]
msg.liveDelay.version = VERSION
params.put("LiveDelay", msg.to_bytes(), block=True)
params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True)
@@ -62,9 +92,20 @@ class TestLagd:
saved_lag_params = retrieve_initial_lag(params, CP)
assert saved_lag_params is not None
lag, valid_blocks = saved_lag_params
assert lag == msg.liveDelay.lateralDelayEstimate
assert valid_blocks == msg.liveDelay.validBlocks
for state, values, block_idx, sample_idx, valid_blocks in zip(saved_lag_params, msg.liveDelay.blockValuesBuckets,
msg.liveDelay.blockIdxBuckets, msg.liveDelay.sampleIdxBuckets,
msg.liveDelay.validBlocksBuckets, strict=True):
assert np.allclose(state[0], values)
assert state[1:] == (block_idx, sample_idx, valid_blocks)
old_msg = messaging.new_message('liveDelay')
old_msg.liveDelay.version = VERSION
old_msg.liveDelay.speedBucket = 4
old_msg.liveDelay.calPerc = 12
old_msg.liveDelay.lateralDelayBuckets = [0.3] * len(SPEED_BUCKET_EDGES)
old_msg.liveDelay.validBlocksBuckets = [0] * len(SPEED_BUCKET_EDGES)
params.put("LiveDelay", old_msg.to_bytes(), block=True)
assert retrieve_initial_lag(params, CP)[4][2] == 3
def test_read_invalid_saved_params(self, subtests):
params = Params()
@@ -72,7 +113,9 @@ class TestLagd:
lr = migrate(LogReader(TEST_ROUTE), [migrate_carParams])
CP = next(m for m in lr if m.which() == "carParams").carParams
for msg_dict in [{'version': 0}, {'status': 'invalid'}, {'validBlocks': 100}]:
valid = {'version': VERSION, 'lateralDelayBuckets': [0.3] * len(SPEED_BUCKET_EDGES),
'validBlocksBuckets': [1] * len(SPEED_BUCKET_EDGES)}
for msg_dict in [valid | {'version': 0}, valid | {'status': 'invalid'}, valid | {'validBlocksBuckets': [100] * 4}]:
with subtests.test(msg=f"liveDelay={msg_dict}"):
msg = messaging.new_message('liveDelay')
msg.liveDelay = msg_dict
@@ -131,12 +174,40 @@ class TestLagd:
def test_estimator_masking(self):
mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.5), random.randint(LAGD_MIN_LAG_FRAMES, LAGD_MAX_LAG_FRAMES - 1)
estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, min_valid_block_count=1)
process_messages(estimator, lag_frames, (int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_SIZE) * 2, rejection_threshold=0.4)
masked_frames = int(MIN_OKAY_WINDOW_SEC / DT)
process_messages(estimator, lag_frames, masked_frames, rejection_threshold=0.4)
process_messages(estimator, lag_frames, masked_frames + BLOCK_SIZE * 2, start_frame=masked_frames)
msg = estimator.get_msg(True)
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
assert msg.liveDelay.calPerc == 100
def test_learning_countdown_resets(self):
mocked_CP = car.CarParams(steerActuatorDelay=0.3)
estimator = LateralLagEstimator(mocked_CP, DT, window_sec=10.0, okay_window_sec=5.0,
min_recovery_buffer_sec=0.0, min_vego=0.0, min_yr=0.0)
process_messages(estimator, 5, 80)
assert np.allclose(estimator.get_msg(True).liveDelay.learningCountdownBuckets, [1.0])
process_messages(estimator, 5, 1, rejection_threshold=1.0, start_frame=80)
msg = estimator.get_msg(True).liveDelay
assert np.allclose(msg.learningCountdownBuckets, [5.0])
assert list(msg.learningResetReasonBuckets) == ["lateral inactive"]
def test_speed_buckets(self):
mocked_CP = car.CarParams(steerActuatorDelay=0.3)
estimator = LateralLagEstimator(mocked_CP, DT, block_size=10, min_valid_block_count=1, window_sec=5.0,
okay_window_sec=2.0, min_recovery_buffer_sec=0.0, min_vego=0.0, min_yr=0.0,
speed_bucket_edges=np.array([0.0, 20.0]))
frame_count = int(5.0 / DT) + 10
process_messages(estimator, 5, frame_count, vego=15.0)
process_messages(estimator, 9, frame_count, vego=25.0, start_frame=frame_count)
msg = estimator.get_msg(True).liveDelay
assert msg.speedBucket == 1
assert np.allclose(msg.lateralDelayBuckets, [5 * DT, 9 * DT], atol=0.01)
assert all(blocks > 0 for blocks in msg.validBlocksBuckets)
@pytest.mark.skipif(PC, reason="only on device")
def test_estimator_performance(self):
mocked_CP = car.CarParams(steerActuatorDelay=0.5)
+4 -1
View File
@@ -29,6 +29,7 @@ from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld"
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
@@ -213,6 +214,7 @@ def main(demo=False):
prev_action = log.ModelDataV2.Action()
DH = DesireHelper()
RELC = RoadEdgeLaneChangeController()
while True:
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
@@ -313,7 +315,8 @@ def main(demo=False):
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
lane_change_prob = l_lane_change_prob + r_lane_change_prob
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
@@ -327,9 +327,16 @@ class SelfdriveD(CruiseHelper):
# Handle lane change
if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange:
direction = self.sm['modelV2'].meta.laneChangeDirection
mdv2sp = self.sm['modelDataV2SP']
if (CS.leftBlindspot and direction == LaneChangeDirection.left) or \
(CS.rightBlindspot and direction == LaneChangeDirection.right):
self.events.add(EventName.laneChangeBlocked)
elif (mdv2sp.leftLaneChangeEdgeBlock and direction == LaneChangeDirection.left) or \
(mdv2sp.rightLaneChangeEdgeBlock and direction == LaneChangeDirection.right):
self.events_sp.add(custom.OnroadEventSP.EventName.laneChangeRoadEdge)
else:
if direction == LaneChangeDirection.left:
self.events.add(EventName.preLaneChangeLeft)
@@ -51,11 +51,18 @@ class LaneChangeSettingsLayout(Widget):
description=lambda: tr("Toggle to enable a delay timer for seamless lane changes when blind spot monitoring " +
"(BSM) detects a obstructing vehicle, ensuring safe maneuvering."),
)
self._road_edge_block = toggle_item_sp(
param="RoadEdgeLaneChangeEnabled",
title=lambda: tr("Block Lane Change: Road Edge Detection"),
description=lambda: tr("Blocks the lane change if the model sees a road edge on your signaled side."),
)
items = [
self._lane_change_timer,
LineSeparatorSP(40),
self._bsm_delay,
LineSeparatorSP(40),
self._road_edge_block,
]
return items
+4 -1
View File
@@ -49,6 +49,7 @@ from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, mak
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
from openpilot.sunnypilot.models.helpers import get_active_bundle
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld_tinygrad"
@@ -367,6 +368,7 @@ def main(demo=False):
DH = DesireHelper()
meta_constants = load_meta_constants()
RELC = RoadEdgeLaneChangeController()
while True:
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
@@ -479,7 +481,8 @@ def main(demo=False):
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
lane_change_prob = l_lane_change_prob + r_lane_change_prob
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
@@ -1,5 +1,5 @@
"""
Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors.
Copyright (c) 2021-, rav4kumar, 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.
@@ -0,0 +1,98 @@
"""
Copyright (c) 2021-, rav4kumar, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import numpy as np
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
from openpilot.common.params import Params
NEARSIDE_PROB = 0.2
EDGE_PROB = 0.35
EDGE_REACTION_TIME = 1.0
EDGE_CLEAR_TIME = 0.3
MIN_SPEED = 20 * CV.MPH_TO_MS
VEHICLE_EDGE_MARGIN = 1.08
EDGE_CLEARANCE = 3.7
class RoadEdgeLaneChangeController:
def __init__(self):
self.params = Params()
self.enabled = self.params.get_bool("RoadEdgeLaneChangeEnabled")
self.param_read_counter = 0
self.left_edge_detected = False
self.right_edge_detected = False
self.left_edge_timer = 0.0
self.right_edge_timer = 0.0
self.left_clear_timer = 0.0
self.right_clear_timer = 0.0
def read_params(self) -> None:
self.enabled = self.params.get_bool("RoadEdgeLaneChangeEnabled")
def update_params(self) -> None:
if self.param_read_counter % 50 == 0:
self.read_params()
self.param_read_counter += 1
def reset(self) -> None:
self.left_edge_detected = False
self.right_edge_detected = False
self.left_edge_timer = 0.0
self.right_edge_timer = 0.0
self.left_clear_timer = 0.0
self.right_clear_timer = 0.0
def update(self, road_edge_stds, lane_line_probs, v_ego: float, road_edges=None) -> None:
self.update_params()
if not self.enabled or v_ego < MIN_SPEED:
self.reset()
return
left_edge_prob = np.clip(1.0 - road_edge_stds[0], 0.0, 1.0)
right_edge_prob = np.clip(1.0 - road_edge_stds[1], 0.0, 1.0)
left_lane_prob = lane_line_probs[0]
right_lane_prob = lane_line_probs[3]
if road_edges is not None and len(road_edges) == 2 and len(road_edges[0].y) > 0 and len(road_edges[1].y) > 0:
left_clearance = abs(road_edges[0].y[0]) - VEHICLE_EDGE_MARGIN
right_clearance = abs(road_edges[1].y[0]) - VEHICLE_EDGE_MARGIN
else:
left_clearance = 0.0
right_clearance = 0.0
left_cond = left_edge_prob > EDGE_PROB and left_lane_prob < NEARSIDE_PROB and left_clearance < EDGE_CLEARANCE
right_cond = right_edge_prob > EDGE_PROB and right_lane_prob < NEARSIDE_PROB and right_clearance < EDGE_CLEARANCE
if left_cond:
self.left_edge_timer = min(self.left_edge_timer + DT_MDL, EDGE_REACTION_TIME + EDGE_CLEAR_TIME)
self.left_clear_timer = 0.0
if self.left_edge_timer > EDGE_REACTION_TIME:
self.left_edge_detected = True
else:
self.left_clear_timer += DT_MDL
if self.left_clear_timer > EDGE_CLEAR_TIME:
self.left_edge_timer = 0.0
self.left_edge_detected = False
if right_cond:
self.right_edge_timer = min(self.right_edge_timer + DT_MDL, EDGE_REACTION_TIME + EDGE_CLEAR_TIME)
self.right_clear_timer = 0.0
if self.right_edge_timer > EDGE_REACTION_TIME:
self.right_edge_detected = True
else:
self.right_clear_timer += DT_MDL
if self.right_clear_timer > EDGE_CLEAR_TIME:
self.right_edge_timer = 0.0
self.right_edge_detected = False
def update_and_fill(self, modelv2, mdv2sp, v_ego):
self.update(modelv2.roadEdgeStds, modelv2.laneLineProbs, v_ego, modelv2.roadEdges)
mdv2sp.leftLaneChangeEdgeBlock = self.left_edge_detected
mdv2sp.rightLaneChangeEdgeBlock = self.right_edge_detected
return self.left_edge_detected, self.right_edge_detected
@@ -2,10 +2,11 @@ import pytest
from openpilot.cereal import log, custom
from openpilot.common.params import Params
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState, LaneChangeDirection
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeMode
TurnDirection = custom.ModelDataV2SP.TurnDirection
@@ -109,5 +110,17 @@ def test_desire_helper_integration(carstate, lateral_active, lane_change_prob, e
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
for _ in range(10):
dh.update(carstate, lateral_active, lane_change_prob)
assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers
dh.update(carstate, lateral_active, lane_change_prob,
left_edge_detected=False, right_edge_detected=False)
assert dh.desire == expected_desire
def test_edge_blocks_lane_change(set_lane_turn_params):
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
carstate = DummyCarState(vEgo=15, leftBlinker=True, steeringPressed=True, steeringTorque=1)
for _ in range(10):
dh.update(carstate, True, 1.0, left_edge_detected=True, right_edge_detected=False)
assert dh.lane_change_state == LaneChangeState.preLaneChange
assert dh.lane_change_direction == LaneChangeDirection.left
assert dh.desire == log.Desire.none
@@ -0,0 +1,169 @@
"""
Copyright (c) 2021-, rav4kumar, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pytest
from openpilot.common.realtime import DT_MDL
from openpilot.sunnypilot.selfdrive.controls.lib.relc import (
RoadEdgeLaneChangeController, EDGE_REACTION_TIME, EDGE_CLEAR_TIME, MIN_SPEED,
VEHICLE_EDGE_MARGIN, EDGE_CLEARANCE,
)
V_HIGH = MIN_SPEED + 2.0
V_LOW = MIN_SPEED - 1.0
class MockEdge:
def __init__(self, y_val):
self.y = [y_val] * 33
def edges(left_y, right_y):
return [MockEdge(left_y), MockEdge(right_y)]
CLOSE_EDGES = edges(-2.0, 1.5)
FAR_EDGES = edges(-10.0, 10.0)
@pytest.fixture
def relc(mocker):
mocker.patch("openpilot.sunnypilot.selfdrive.controls.lib.relc.Params")
controller = RoadEdgeLaneChangeController()
controller.enabled = True
return controller
def drive(controller, road_edge_stds, lane_line_probs, seconds, v_ego=V_HIGH, road_edges=CLOSE_EDGES):
for _ in range(int(seconds / DT_MDL) + 1):
controller.update(road_edge_stds, lane_line_probs, v_ego, road_edges)
@pytest.mark.parametrize("road_edge_stds,lane_line_probs,attr", [
([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], "left_edge_detected"),
([0.9, 0.0], [0.8, 0.8, 0.8, 0.0], "right_edge_detected"),
])
def test_edge_detection(relc, road_edge_stds, lane_line_probs, attr):
drive(relc, road_edge_stds, lane_line_probs, EDGE_REACTION_TIME + 0.1)
assert getattr(relc, attr)
def test_edge_detection_requires_time(relc):
drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME - 0.05)
assert not relc.left_edge_detected
def test_both_edges_detected(relc):
drive(relc, [0.0, 0.0], [0.0, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1)
assert relc.left_edge_detected
assert relc.right_edge_detected
def test_noise_doesnt_clear(relc):
edge = ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8])
clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8])
drive(relc, *edge, EDGE_REACTION_TIME + 0.1)
assert relc.left_edge_detected
relc.update(*clear, V_HIGH, CLOSE_EDGES)
relc.update(*edge, V_HIGH, CLOSE_EDGES)
assert relc.left_edge_detected
def test_clears_after_window(relc):
edge = ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8])
clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8])
drive(relc, *edge, EDGE_REACTION_TIME + 0.1)
assert relc.left_edge_detected
drive(relc, *clear, EDGE_CLEAR_TIME + 0.05)
assert not relc.left_edge_detected
assert relc.left_edge_timer == 0.0
def test_low_speed_skips(relc):
drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1, v_ego=V_LOW)
assert not relc.left_edge_detected
assert relc.left_edge_timer == 0.0
def test_speed_drop_resets(relc):
drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1)
assert relc.left_edge_detected
relc.update([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], V_LOW, CLOSE_EDGES)
assert not relc.left_edge_detected
def test_param_off_resets(relc):
drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1)
assert relc.left_edge_detected
relc.params.get_bool.return_value = False
relc.read_params()
relc.update([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], V_HIGH, CLOSE_EDGES)
assert not relc.left_edge_detected
assert not relc.right_edge_detected
def test_lane_line_prevents_detection(relc):
drive(relc, [0.0, 0.9], [0.8, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1)
assert not relc.left_edge_detected
def test_one_side_blocks_other_allows(relc):
drive(relc, [0.9, 0.0], [0.8, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1)
assert relc.right_edge_detected
assert not relc.left_edge_detected
def test_disabled_no_detection(relc):
relc.enabled = False
relc.params.get_bool.return_value = False
drive(relc, [0.0, 0.0], [0.0, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1)
assert not relc.left_edge_detected
assert not relc.right_edge_detected
def test_far_edge_no_block(relc):
drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1, road_edges=FAR_EDGES)
assert not relc.left_edge_detected
def test_close_edge_blocks(relc):
drive(relc, [0.9, 0.0], [0.05, 0.8, 0.8, 0.05], EDGE_REACTION_TIME + 0.1,
road_edges=edges(-8.0, 1.5))
assert relc.right_edge_detected
assert not relc.left_edge_detected
def test_wide_road_no_lines_no_block(relc):
drive(relc, [0.0, 0.0], [0.05, 0.4, 0.4, 0.05], EDGE_REACTION_TIME + 0.1,
road_edges=edges(-8.0, 8.0))
assert not relc.left_edge_detected
assert not relc.right_edge_detected
def test_narrow_road_both_block(relc):
drive(relc, [0.0, 0.0], [0.02, 0.4, 0.4, 0.02], EDGE_REACTION_TIME + 0.1,
road_edges=edges(-2.5, 2.5))
assert relc.left_edge_detected
assert relc.right_edge_detected
def test_clearance_boundary(relc):
boundary = VEHICLE_EDGE_MARGIN + EDGE_CLEARANCE # 4.78m
drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1,
road_edges=edges(-(boundary - 0.1), 10.0))
assert relc.left_edge_detected
relc.reset()
drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1,
road_edges=edges(-(boundary + 0.1), 10.0))
assert not relc.left_edge_detected
@@ -244,4 +244,12 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
AlertStatus.normal, AlertSize.none,
Priority.MID, VisualAlert.none, AudibleAlert.prompt, 3.),
},
EventNameSP.laneChangeRoadEdge: {
ET.WARNING: Alert(
"Lane Change Unavailable: Road Edge",
"",
AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.1),
},
}
@@ -545,6 +545,12 @@
}
]
},
{
"key": "RoadEdgeLaneChangeEnabled",
"widget": "toggle",
"title": "Block Lane Change: Road Edge Detection",
"description": "Blocks lane change when the model sees a road edge on the side you signal."
},
{
"key": "AutoLaneChangeBsmDelay",
"widget": "toggle",
@@ -257,6 +257,10 @@ sections:
label: 2 seconds
- value: 5
label: 3 seconds
- key: RoadEdgeLaneChangeEnabled
widget: toggle
title: 'Block Lane Change: Road Edge Detection'
description: Blocks lane change when the model sees a road edge on the side you signal.
- key: AutoLaneChangeBsmDelay
widget: toggle
title: 'Auto Lane Change: Delay with Blind Spot'
+1 -1
View File
@@ -88,7 +88,7 @@ def use_sunnylink_uploader_shim(started, params, CP: car.CarParams) -> bool:
return use_sunnylink_uploader(params)
def is_tinygrad_model(started, params, CP: car.CarParams) -> bool:
"""Check if the active model runner is SNPE."""
"""Check if the active model runner is tinygrad."""
return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.tinygrad)
def is_stock_model(started, params, CP: car.CarParams) -> bool:
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
from collections import deque
import math
import time
from openpilot.cereal import messaging
from openpilot.common.constants import CV
from openpilot.common.params import Params
from opendbc.car.structs import car
from opendbc.car.vehicle_model import VehicleModel
PING_PONG_WINDOW = 5.0
PING_PONG_MIN_WINDOW = 3.0
PING_PONG_ERROR_DEG = 1.0
def ping_pong_metrics(samples: list[tuple[float, float]]) -> tuple[str, float, float, float]:
duration = samples[-1][0] - samples[0][0] if len(samples) > 1 else 0.0
if duration < PING_PONG_MIN_WINDOW:
return "COLLECTING", 0.0, 0.0, duration
values = [sample[1] for sample in samples]
center = sum(values) / len(values)
residual = [value - center for value in values]
amplitude = (max(residual) - min(residual)) / 2
state = 0
transition_times = []
for (sample_time, _), value in zip(samples, residual, strict=True):
new_state = 1 if value >= PING_PONG_ERROR_DEG else -1 if value <= -PING_PONG_ERROR_DEG else state
if state and new_state != state:
transition_times.append(sample_time)
state = new_state
transition_duration = transition_times[-1] - transition_times[0] if len(transition_times) > 1 else 0.0
frequency = (len(transition_times) - 1) / (2 * transition_duration) if transition_duration else 0.0
if frequency < 1 or amplitude < PING_PONG_ERROR_DEG:
severity = "NONE"
elif amplitude < 2:
severity = "MILD"
elif amplitude < 3:
severity = "MODERATE"
else:
severity = "SEVERE"
return severity, amplitude, frequency, duration
def line_graph(values, statuses, active_bucket: int, edges=None, low: float = .15, high: float = .65, height: int = 11, step: int = 8) -> str:
width = step * len(values) + 1
dot_height = height * 4
dots = set()
value_rows = [round((high - max(low, min(high, value))) / (high - low) * (dot_height - 1)) for value in values]
marker_rows = [min(row // 4, height - 1) for row in value_rows]
rows = [row * 4 + 1 for row in marker_rows]
point_cols = [i * step + step // 2 for i in range(len(rows))]
point_xs = [col * 2 for col in point_cols]
knot_xs = [0, *point_xs, width * 2 - 2]
knot_rows = [rows[0], *rows, rows[-1]]
for i in range(len(knot_rows) - 1):
x0, x1 = knot_xs[i], knot_xs[i + 1]
for x in range(x0, x1 + 1):
row = round(knot_rows[i] + (knot_rows[i + 1] - knot_rows[i]) * (x - x0) / (x1 - x0))
dots.add((x, row))
braille_bits = ((0, 3), (1, 4), (2, 5), (6, 7))
grid = [[" " for _ in range(width)] for _ in range(height)]
for cell_row in range(height):
for cell_col in range(width):
bits = 0
for dot_row in range(4):
for dot_col in range(2):
if (cell_col * 2 + dot_col, cell_row * 4 + dot_row) in dots:
bits |= 1 << braille_bits[dot_row][dot_col]
if bits:
grid[cell_row][cell_col] = chr(0x2800 + bits)
markers = {"unestimated": "", "estimated": "", "invalid": "×"}
for i, (col, row, status) in enumerate(zip(point_cols, marker_rows, statuses, strict=True)):
grid[row][col] = "" if i == active_bucket else markers[str(status)]
chart = []
for row, cells in enumerate(grid):
value = high - row * (high - low) / (height - 1)
chart.append(f" {value:.2f}s │{''.join(cells)}")
axis_edges = edges if edges is not None else [i * 10 for i in range(len(values))]
axis = ["" for _ in range(width)]
for i in range(1, len(axis_edges)):
axis[i * step] = ""
chart.append("" + "".join(axis))
labels = [" " for _ in range(width)]
for i, edge in enumerate(axis_edges):
label = f"{edge:.0f}+" if i == len(axis_edges) - 1 else f"{edge:.0f}"
start = min(i * step, width - len(label))
labels[start:start + len(label)] = label
chart.append(" edges " + "".join(labels) + " mph")
center_labels = [" " for _ in range(width)]
widths = [axis_edges[i + 1] - edge for i, edge in enumerate(axis_edges[:-1])]
widths.append(widths[-1] if widths else 0)
for col, edge, bucket_width in zip(point_cols, axis_edges, widths, strict=True):
label = f"{edge + bucket_width / 2:.0f}"
start = max(0, min(col - len(label) // 2, width - len(label)))
center_labels[start:start + len(label)] = label
chart.append(" centers" + "".join(center_labels) + " mph (dots)")
chart.append(" ● READY ○ LEARNING × UNSTABLE ◆ ACTIVE")
return "\n".join(chart)
def main() -> None:
CP = messaging.log_from_bytes(Params().get("CarParams", block=True), car.CarParams)
VM = VehicleModel(CP)
sm = messaging.SubMaster(["carControl", "carState", "controlsState", "liveCalibration", "liveDelay", "liveParameters"])
ping_pong_samples: deque[tuple[float, float]] = deque()
ping_pong_waiting = "lateral inactive"
last_sample = 0.0
next_redraw = 0.0
print("\033[2J", end="")
while True:
sm.update(500)
now = time.monotonic()
speed_mps = sm["carState"].vEgo
lp = sm["liveParameters"]
VM.update_params(max(lp.stiffnessFactor, .1), max(lp.steerRatio, .1))
desired_angle = math.degrees(VM.get_steer_from_curvature(-sm["controlsState"].desiredCurvature, speed_mps, lp.roll))
actual_angle = sm["carState"].steeringAngleDeg - lp.angleOffsetDeg
if now - last_sample >= .05:
if not sm["carControl"].latActive:
ping_pong_waiting = "lateral inactive"
elif sm["carState"].steeringPressed:
ping_pong_waiting = "driver steering"
elif speed_mps < 2:
ping_pong_waiting = "speed below 5 mph"
else:
ping_pong_waiting = ""
ping_pong_samples.append((now, desired_angle - actual_angle))
while ping_pong_samples and now - ping_pong_samples[0][0] > PING_PONG_WINDOW:
ping_pong_samples.popleft()
if ping_pong_waiting:
ping_pong_samples.clear()
last_sample = now
if not sm.alive["liveDelay"] or now < next_redraw:
continue
next_redraw = now + .25
ld = sm["liveDelay"]
edges = [edge * CV.MS_TO_MPH for edge in ld.speedBucketEdges]
speed = sm["carState"].vEgo * CV.MS_TO_MPH
calibration = sm["liveCalibration"].calStatus
lines = [f"speed {speed:5.1f} mph calibration {calibration} status {ld.status} applied {ld.lateralDelay:.3f} s"]
severity, amplitude, frequency, duration = ping_pong_metrics(list(ping_pong_samples))
ping_pong = f"PING-PONG {severity:<10} angle-error amplitude ±{amplitude:.2f}° {frequency:.2f} Hz window {duration:.0f}s"
if ping_pong_waiting:
ping_pong = f"PING-PONG WAITING {ping_pong_waiting}"
elif severity == "COLLECTING":
ping_pong = f"PING-PONG COLLECTING {duration:.0f}/{PING_PONG_MIN_WINDOW:.0f}s of steering-angle error"
lines.extend([ping_pong, "", "APPLIED LAG BY SPEED (INTERPOLATED)",
*line_graph(ld.lateralDelayAppliedBuckets, ld.statusBuckets, ld.speedBucket, edges).splitlines(), "",
" range estimate applied std blocks progress starts in state last reset"])
for i, (estimate, applied, std, blocks, percent, countdown, status, reason) in enumerate(zip(ld.lateralDelayBuckets,
ld.lateralDelayAppliedBuckets,
ld.lateralDelayEstimateStdBuckets,
ld.validBlocksBuckets,
ld.calPercBuckets,
ld.learningCountdownBuckets,
ld.statusBuckets,
ld.learningResetReasonBuckets,
strict=True)):
speed_range = f"{edges[i]:.0f}-{edges[i + 1]:.0f} mph" if i + 1 < len(edges) else f"{edges[i]:.0f}+ mph"
active = "*" if i == ld.speedBucket else " "
status_name = str(status)
state = {"unestimated": "LEARNING", "estimated": "READY", "invalid": "UNSTABLE"}[status_name]
row = f" {active} {speed_range:<9} {estimate:.3f} s {applied:.3f} s {std:.3f}"
waiting = " --" if status_name == "estimated" else f"{countdown:4.1f}s"
reset_reason = "--" if status_name == "estimated" else (reason or "--")
row += f" {blocks:2} {percent:3}% {waiting} {state:<9} {reset_reason}"
lines.append(row)
print("\033[H" + "\033[K\n".join(lines) + "\033[K\033[J", end="", flush=True)
if __name__ == "__main__":
main()