Compare commits

..

188 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
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
9 changed files with 418 additions and 61 deletions
+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;
+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)
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a501760a9d1d5fef0eab2b8c5d122d06124fc26dc8e0782e0aa94b82a208f0ff
size 1757355221
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2b85e82079a2d31c5ce8616f2b429ccfcdcb8ebb5aefd72a62b8bd78aa9c7621
size 15583592
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:659727c4d4839adc4992a254409a54259a8756a743f2d567bf5fdc6579f8009b
size 60881999
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c824f68646a3b94f117f01c70dc8316fb466e05fbd42ccdba440b8a8dc86914b
size 46265993
+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()