Compare commits

...

196 Commits

Author SHA1 Message Date
Michael Tawata ea53286972 readme overhaul 2026-03-24 23:42:43 -07:00
firestar5683 d2a6a60ea0 Handle MapsSelected downgrade from StarPilot-Development 2026-03-22 15:12:11 -05:00
firestar5683 ecec3afbd6 tinygrad bump 2026-03-21 13:59:45 -05:00
firestar5683 5a67663069 Fix Galaxy 2026-03-20 17:52:16 -05:00
firestar5683 54b307378b Long Maneuvers Resume 2026-03-18 21:10:09 -05:00
firestar5683 9b4c0a7151 Fix Lead departure issue 2026-03-18 19:36:08 -05:00
firestarsdog 299c42040a Fix UI Respring 2026-03-15 22:18:46 -04:00
firestar5683 2d50c43309 Backwards Compatible 2026-03-15 00:58:38 -05:00
firestar5683 5965d2389e offset 2026-03-11 18:10:54 -05:00
firestar5683 43eeca188f button 2026-03-11 17:59:50 -05:00
firestar5683 7986f23b1c SLC Bug 2026-03-11 17:56:34 -05:00
firestar5683 d4ce22deaf Update plots.js 2026-03-11 17:51:52 -05:00
firestar5683 782cef4c8b no blend 2026-03-11 17:07:28 -05:00
firestar5683 3ffe607452 v3 2026-03-11 16:56:46 -05:00
firestar5683 fc79fe1248 LongPlanV2 2026-03-11 15:18:14 -05:00
firestar5683 79e28d6f2e back to zero 2026-03-11 02:43:22 -05:00
firestar5683 335e7cf74e patches 2026-03-11 02:06:02 -05:00
firestar5683 978d57a674 Planner 2026-03-11 02:06:02 -05:00
firestarsdog 33c0f0ef3a GLXY Client Changes 2026-03-10 23:20:46 -05:00
firestar5683 c540998591 long press 2026-03-10 17:37:04 -05:00
firestar5683 445e89e14c BUTTON 2026-03-10 17:23:31 -05:00
firestar5683 46e5eaa232 Remove experimentalgmtune 2026-03-10 13:57:49 -05:00
firestar5683 d4d426f94e malibu 2026-03-10 13:55:20 -05:00
firestar5683 9364ffd6a8 malibu 2026-03-10 13:31:14 -05:00
firestar5683 fcf8c3feb2 Malibu Exception 2026-03-10 01:20:59 -05:00
firestar5683 2881deb510 Updates 2026-03-10 00:55:50 -05:00
firestar5683 082418b012 BUTTONS YEEEAAAA 2026-03-10 00:54:56 -05:00
firestar5683 f8eab92141 stop distance 2026-03-09 18:42:17 -05:00
firestar5683 208630f5ca personalities 2026-03-09 14:01:27 -05:00
firestar5683 0e0909cbd7 Goat scream 2026-03-09 13:52:40 -05:00
firestar5683 4320525df6 disable lagd 2026-03-09 13:11:05 -05:00
firestar5683 813e3183fc Troubleshoot 2026-03-09 13:11:05 -05:00
firestar5683 b4cf18cdc2 High Speed Follow 2026-03-09 13:11:05 -05:00
firestar5683 c8f18d1497 allow uploads 2026-03-08 21:09:13 -05:00
firestar5683 5ca3f503ee Pond Updates 2026-03-08 21:06:31 -05:00
firestar5683 283e9b1d78 Refine GM pedal/paddle tuning and EV accel profile behavior 2026-03-08 21:06:31 -05:00
firestar5683 404922e80d Add Pond long maneuvers page and live status wiring 2026-03-08 21:06:31 -05:00
firestar5683 77f314a170 Backport longitudinal maneuvers tooling and runtime hooks 2026-03-08 21:06:31 -05:00
firestar5683 b0426254be Revert "offset"
This reverts commit 3bd7152f0a.
2026-03-08 01:44:13 -06:00
firestar5683 0d911b2c31 Update Lateral 2026-03-07 19:52:37 -06:00
firestar5683 bd7f47eb20 rename sidebar and tmux remote 2026-03-07 13:29:37 -06:00
firestar5683 f14ac063b3 Revert "General2"
This reverts commit cc700d9b81.
2026-03-07 00:06:54 -06:00
firestar5683 b169d93464 general 2026-03-07 00:06:54 -06:00
firestar5683 14a3cb7025 steppers 2026-03-06 23:47:40 -06:00
firestar5683 9e6ef37d96 fix stale labels 2026-03-06 23:25:57 -06:00
firestar5683 aa36b2133e offset 2026-03-06 23:25:56 -06:00
firestar5683 8eb6d5b447 Revert "Testing Ground: add Gen1 Bolt lateral tune (slot 2)"
This reverts commit a43027c1ad.
2026-03-06 13:02:00 -06:00
firestar5683 f2b9cc51a0 Testing Ground: add Gen1 Bolt lateral tune (slot 2) 2026-03-06 11:32:40 -06:00
firestar5683 f2d754e96d Testing Ground: add GM Long Tune (slot 1) 2026-03-06 11:32:40 -06:00
firestar5683 cca4a277f1 Testing Ground: framework and UI scaffolding (unused slots) 2026-03-06 11:32:40 -06:00
firestar5683 cd7cb862fd plots 2026-03-05 23:32:43 -06:00
firestar5683 e8009ad9b9 Updater 2026-03-05 01:25:47 -06:00
firestar5683 cc700d9b81 General2 2026-03-04 23:14:57 -06:00
firestar5683 c24dd3552c Galaxy Optimization 2026-03-04 23:12:12 -06:00
firestarsdog 2eced5b72c Revert "General"
This reverts commit 77ffd98a1a.
2026-03-04 18:50:27 -05:00
firestar5683 77ffd98a1a General 2026-03-04 11:18:23 -06:00
firestar5683 d62d7dd9fc Moar Toggles 2026-03-03 22:46:49 -06:00
firestarsdog c84acea993 Galaxy Pair Locally 2026-03-03 22:45:38 -05:00
firestar5683 876a45122f Model Manager/Galaxy 2026-03-03 21:44:07 -06:00
firestarsdog f538eddb87 Pin to Pass 2026-03-03 11:37:52 -06:00
firestarsdog 7c51e8aac1 Galaxy 2026-03-03 00:10:16 -06:00
firestar5683 4ab8b0e874 2017 Tuning 2026-03-02 22:41:47 -06:00
firestar5683 b6f9f8180e Try old regen tuning 2026-03-02 16:39:38 -06:00
firestar5683 d07852b2bc low speed 2026-03-02 16:38:53 -06:00
firestar5683 a70746918b paddle refactor 2026-03-02 00:40:15 -06:00
firestar5683 0a6e03b7ca 2017 2026-03-02 00:40:15 -06:00
firestar5683 f0f95d5d10 Update parse_model_outputs.py 2026-02-27 15:04:29 -06:00
firestar5683 cf735574a6 Test handoff 2026-02-27 13:59:28 -06:00
firestarsdog d466d36429 Nothing to see 2026-02-24 20:05:23 -05:00
firestar5683 33cb78cfa9 Lat Tuning 2026-02-24 17:42:10 -06:00
firestar5683 e2a15a0985 Update tinygrad_modeld.py 2026-02-24 17:33:52 -06:00
firestar5683 984fd6a6cb Update latcontrol_torque.py 2026-02-23 21:30:52 -06:00
firestar5683 7ef35817fc SC 2026-02-23 13:23:55 -06:00
firestar5683 26e0b2406e 7mph engage 2026-02-22 21:10:30 -06:00
firestarsdog fed12740f2 Spoof CruiseSpeed Non-Acc 2026-02-22 21:10:02 -06:00
firestar5683 030cf80148 Update carcontroller.py 2026-02-20 02:11:08 -06:00
firestar5683 d082aa8a3c blaze 2026-02-19 13:19:35 -06:00
firestar5683 fdcab8d127 blaze 2026-02-19 11:13:07 -06:00
firestar5683 266fda2c91 CEM Transition 2026-02-18 21:32:01 -06:00
firestar5683 97a2b1522e oops 2026-02-18 15:35:56 -06:00
firestar5683 d59ab46e91 Cleanup fingerprint 2026-02-18 15:15:06 -06:00
firestar5683 948cdf21d0 fix general 2026-02-18 14:49:48 -06:00
firestar5683 f10391d9ce Revert "no sascm"
This reverts commit 1a77897c3a.
2026-02-18 14:30:09 -06:00
firestar5683 444edbe71b Update interface.py 2026-02-18 10:51:47 -06:00
firestar5683 bb41c76c62 Revert "GMC Sierra Print"
This reverts commit a7f5f3b21e.
2026-02-18 10:48:42 -06:00
firestar5683 1978e5fda1 GMC Sierra Print 2026-02-18 08:38:36 -06:00
firestar5683 85906ceb77 JC's follow changes 2026-02-18 08:03:05 -06:00
firestar5683 259ac19499 Honda 2026-02-17 21:02:15 -06:00
firestar5683 1a77897c3a no sascm
Revert "no sascm"

This reverts commit 147e752007c6a4f80eec551934bc1cf86510e6dc.

Update interface.py
2026-02-17 20:49:33 -06:00
firestar5683 b5d39c4bc1 Truck Tuning 2026-02-17 19:57:35 -06:00
firestar5683 d4d78c9435 Update frogpilot_acceleration.py 2026-02-17 12:28:29 -06:00
firestar5683 2f29d7ee04 EV Tuning 2026-02-16 23:43:46 -06:00
firestar5683 338a07973b Truck Tuning 2026-02-16 23:39:45 -06:00
firestar5683 f4da6a3e3b silverado 2026-02-16 17:28:35 -06:00
firestar5683 5fbaebc91c brake mismatches 2026-02-16 13:25:23 -06:00
firestarsdog 7dbdb33ed1 Gen2ACC Distance Button Sync for CC_ONLY_CAR 2026-02-15 23:33:15 -06:00
firestar5683 a1cd4954b1 cancel acc bolt with pedal 2026-02-15 23:15:48 -06:00
firestar5683 2bbffc2a38 cruisestatus acc pedal 2026-02-14 22:44:33 -06:00
firestar5683 4d7aee4935 pedal 2026-02-14 15:58:32 -06:00
firestarsdog 41b055d140 Update values.py 2026-02-14 16:24:10 -05:00
firestarsdog bec0c04334 Evil +? 2026-02-14 13:02:22 -05:00
firestar5683 a8499d20fc Separate Non-ACC vs ACC + Pedal 2026-02-13 23:48:41 -06:00
firestar5683 6d0c17f43a Holiday 2026-02-13 22:45:30 -06:00
firestar5683 6dec58237b Fingerprinting 2026-02-13 22:35:10 -06:00
firestarsdog 6a47be1631 Hondadays 2026-02-13 21:38:01 -05:00
firestarsdog 8ea080b4cb Honda attempt from bugsink 2026-02-13 15:35:17 -05:00
firestarsdog a51503f2e4 BugSink Catch - human_lane_changes 2026-02-13 11:16:12 -05:00
firestar5683 07c722553d The Great Merge 2026-02-13 01:52:05 -06:00
firestar5683 14b1793c79 User adjustable offsets 2026-02-07 23:32:26 -06:00
firestar5683 055c223ded Integrator Smooth On Handoff 2026-02-06 15:46:56 -06:00
firestar5683 4d406112f7 Lights 2026-02-05 22:39:42 -06:00
firestar5683 e03f28bba0 New Models 2026-02-05 15:05:18 -06:00
firestar5683 f0cc963c0e Revert "merry christmas"
This reverts commit 8a9d0e5e6a.
2026-02-05 13:33:40 -06:00
firestar5683 cd39ce0305 Increase Fault Resilience 2026-02-05 13:15:39 -06:00
firestarsdog 749b5e06fb Stats 2026-02-02 01:04:19 -05:00
firestarsdog 23121cca0e Stats 2026-02-01 22:46:13 -06:00
firestar5683 6f0502e862 friction threshold 2026-01-29 11:28:30 -06:00
firestar5683 04c59fb18e Lat Updates 2026-01-28 23:51:36 -06:00
firestar5683 844dce2030 Update interface.py 2026-01-20 22:08:07 -06:00
firestar5683 aa77db1fc9 Update override.toml 2026-01-19 21:18:53 -06:00
firestar5683 bee9b2b046 Revert "Mac Update"
This reverts commit 1b5d776464.
2026-01-19 11:43:31 -06:00
firestarsdog 469cd5e528 Add SASCM to vehicle settings detection/stats 2026-01-19 10:32:21 -06:00
firestar5683 23f52644ae Update interface.py 2026-01-18 23:45:59 -06:00
firestar5683 e76b82603b Update interface.py 2026-01-18 23:44:42 -06:00
firestar5683 1b5d776464 Mac Update 2026-01-18 22:22:06 -06:00
firestarsdog 61f4e8b142 Stats 2026-01-18 22:17:42 -06:00
firestar5683 f0fefb6e13 Torque Controller Update 2026-01-18 21:58:43 -06:00
firestar5683 25a7090c3d Update redneck 2026-01-15 22:54:15 -06:00
firestar5683 b0de4d64b4 Split Tune 2026-01-15 19:17:55 -06:00
firestar5683 77ae5b176a fix redneck v2 2026-01-14 13:58:22 -06:00
firestar5683 89de28682c Remove lat smooth seconds 2026-01-14 13:49:35 -06:00
firestar5683 a1d4dcb9a0 frogpilot migration 2026-01-12 22:10:40 -06:00
firestar5683 c8deae75b7 More defaults 2026-01-12 22:04:19 -06:00
firestar5683 8653fb41f2 Update defaults 2026-01-12 22:00:04 -06:00
firestar5683 c064197fde Big Mac 2026-01-11 16:58:25 -06:00
firestar5683 7f878ec1dc Try Higher Friction 2026-01-10 14:04:32 -06:00
firestar5683 140bdd59fb Autotune Off 2026-01-09 22:40:45 -06:00
firestar5683 6f728f935a Revert "Pedal Braking Limits"
This reverts commit 568b72095f.
2025-12-31 20:55:13 -06:00
firestar5683 568b72095f Pedal Braking Limits 2025-12-30 20:17:59 -06:00
firestar5683 0aaf4d1336 merry christmas 2025-12-24 22:04:58 -06:00
firestar5683 bb3f0a3354 ds2 2025-12-24 21:28:40 -06:00
firestar5683 f2aba355b9 Try friction adjustment 2025-12-16 14:38:29 -06:00
firestar5683 e6513efb9b Update frogpilot_tracking.py 2025-12-15 00:55:51 -06:00
firestar5683 872a2c58aa minsteer speed 2025-12-14 14:46:27 -06:00
firestar5683 1630560715 Update Percentages 2025-12-13 19:13:44 -06:00
firestar5683 4cfd893837 Trailer Load Gas Tuning 2025-12-12 10:50:11 -06:00
firestar5683 0e93ceef00 Live Friction 2025-12-11 16:54:11 -06:00
firestar5683 d626f0fe49 Zero error 2025-12-11 15:12:29 -06:00
firestar5683 8dae3d375c Updates
Update latcontrol_torque.py

interp friction threshold
2025-12-09 19:58:46 -06:00
firestar5683 721d39ef1f Change NoEntryAlert message to 'Reverse Gear' 2025-12-06 16:44:30 -06:00
firestar5683 a97c5742b7 Update events.py 2025-12-05 20:13:49 -06:00
firestar5683 488f821983 LattyBoi2.0 2025-12-03 23:30:35 -06:00
firestar5683 190dc98180 Latty Boi
Revert "Latty Boi"

This reverts commit af687e501cc4bdcda7840453d06595d7ea674148.

Reapply "Latty Boi"

This reverts commit ff5566d4439f5e4997fe83f4f21d0be62b75d75d.
2025-12-02 20:29:48 -06:00
firestar5683 0b459d9f0b Recovery Power 2025-12-02 17:18:43 -06:00
firestar5683 3cd594bbe4 New planplus 2025-12-02 12:09:05 -06:00
Woohyun Rho f31082ce2b Update 2025-11-28 17:10:45 -06:00
firestar5683 b51d7ae35d Update interface.py 2025-11-18 20:48:48 -06:00
firestar5683 884fcb8315 New Lateral Changes 2025-11-18 20:48:21 -06:00
firestar5683 77597e60b2 Update 2025-11-15 14:44:07 -06:00
firestar5683 f2fbc8f30b Accel Tests 2025-11-14 20:32:47 -06:00
firestar5683 fffea614a4 Paddle Planner 2025-11-14 01:26:06 -06:00
firestar5683 e7e593cdb2 Revert "Upstream Lateral"
This reverts commit 97f8736605.
2025-11-10 23:07:02 -06:00
firestar5683 97f8736605 Upstream Lateral
Revert "Upstream Lateral"

This reverts commit 20f7d6631bb152860781b46533bb0e96223be132.

Reapply "Upstream Lateral"

This reverts commit 2a7a563e219a1b688529d341919fc377fed5038e.

Update latcontrol_torque.py

more lateral

Update ui
2025-11-07 00:13:34 -06:00
firestar5683 48efc5b97a Update manager.py 2025-10-27 00:58:28 -05:00
firestar5683 4cca6553e4 Update 2025-10-25 15:35:55 -05:00
firestar5683 6eda52b562 Medium Fanta 2025-10-22 22:18:08 -05:00
firestar5683 de9cbc5de6 Update interfaces.py 2025-10-20 13:49:08 -05:00
firestar5683 fee241dc0b no nnff 2025-10-19 17:59:36 -05:00
firestar5683 2094be9a7a Scene Complexity 2025-10-18 14:24:48 -05:00
firestar5683 6ce476025d Update carcontroller.py 2025-10-17 19:10:41 -05:00
firestar5683 dd6c62dbf6 Update interface.py 2025-10-17 18:13:24 -05:00
firestar5683 2ab19dbd4c CEM 2025-10-17 17:39:06 -05:00
firestar5683 3241fbdd8c lite 2025-10-17 17:13:37 -05:00
firestar5683 1f0350e66a Redneck 2.0 2025-10-17 16:55:56 -05:00
firestar5683 ce46776439 Modify torque tuning parameters in interfaces.py
Adjusted torque tuning parameters for improved performance.
2025-10-17 16:53:54 -05:00
firestar5683 afb209a1a8 Hurts Donut 2025-10-16 23:57:08 -05:00
firestar5683 b4e29d4ed6 Smoothy Boi 2025-10-16 23:35:26 -05:00
firestar5683 dada0f2ae6 New change 2025-10-15 22:59:04 -05:00
firestar5683 4532e7f5d1 lat3 2025-10-15 22:20:35 -05:00
firestar5683 78de442ee1 New Lateral Changes 2025-10-10 20:59:23 -05:00
firestar5683 953b343e06 Fix Standard 2025-10-10 19:24:24 -05:00
firestar5683 5b26be2e3c error? 2025-10-08 23:13:32 -05:00
firestar5683 4abe50405c Update interface.py 2025-10-08 23:07:37 -05:00
firestar5683 f2e705413c Fix New Devices 2025-10-08 22:19:43 -05:00
firestar5683 da56f26105 No positive P-response for long control if user-selected parameter set 2025-10-08 07:42:32 -05:00
firestar5683 ab39266180 Automatic updates 2025-10-04 16:04:35 -05:00
firestar5683 dac85dfd3a Steer Alerts 2025-10-04 01:49:22 -05:00
firestar5683 7e539cde38 Update frogpilot_acceleration.py 2025-10-03 22:42:08 -05:00
firestar5683 16a79fbc3d SteerAlerts
Revert "SteerAlerts"

This reverts commit cbaba399c5b9caac5a8faf3917f0929d747a0acc.

Update controlsd.py
2025-10-03 22:14:25 -05:00
firestar5683 9cdf0120cf Donut DM 2025-10-03 20:50:00 -05:00
firestar5683 aa0f0aaac8 Update gm_global_a_powertrain_generated.dbc 2025-10-03 16:41:22 -05:00
firestar5683 aefaafb616 Update 2025-10-03 00:43:40 -05:00
firestar5683 388f63168f firehose 2025-09-30 20:45:47 -05:00
firestar5683 7fe40f6e7f Update 2025-09-30 14:30:04 -05:00
firestar5683 eb2218e733 Dom 2025-09-30 09:25:14 -05:00
firestar5683 aa9364a311 Tinygrad 2025-09-30 08:56:38 -05:00
1372 changed files with 355213 additions and 227157 deletions
+74 -252
View File
@@ -1,252 +1,74 @@
<div align="center" style="text-align: center;">
<h1>openpilot</h1>
<p>
<b>openpilot is an operating system for robotics.</b>
<br>
Currently, it upgrades the driver assistance system in 300+ supported cars.
</p>
<h3>
<a href="https://docs.comma.ai">Docs</a>
<span> · </span>
<a href="https://docs.comma.ai/contributing/roadmap/">Roadmap</a>
<span> · </span>
<a href="https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md">Contribute</a>
<span> · </span>
<a href="https://discord.comma.ai">Community</a>
<span> · </span>
<a href="https://comma.ai/shop">Try it on a comma 3X</a>
</h3>
Quick start: `bash <(curl -fsSL openpilot.comma.ai)`
[![openpilot tests](https://github.com/commaai/openpilot/actions/workflows/selfdrive_tests.yaml/badge.svg)](https://github.com/commaai/openpilot/actions/workflows/selfdrive_tests.yaml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![X Follow](https://img.shields.io/twitter/follow/comma_ai)](https://x.com/comma_ai)
[![Discord](https://img.shields.io/discord/469524606043160576)](https://discord.comma.ai)
</div>
<table>
<tr>
<td><a href="https://youtu.be/NmBfgOanCyk" title="Video By Greer Viau"><img src="https://github.com/commaai/openpilot/assets/8762862/2f7112ae-f748-4f39-b617-fabd689c3772"></a></td>
<td><a href="https://youtu.be/VHKyqZ7t8Gw" title="Video By Logan LeGrand"><img src="https://github.com/commaai/openpilot/assets/8762862/92351544-2833-40d7-9e0b-7ef7ae37ec4c"></a></td>
<td><a href="https://youtu.be/SUIZYzxtMQs" title="A drive to Taco Bell"><img src="https://github.com/commaai/openpilot/assets/8762862/05ceefc5-2628-439c-a9b2-89ce77dc6f63"></a></td>
</tr>
</table>
Using openpilot in a car
------
To use openpilot in a car, you need four things:
1. **Supported Device:** a comma 3/3X, available at [comma.ai/shop](https://comma.ai/shop/comma-3x).
2. **Software:** The setup procedure for the comma 3/3X allows users to enter a URL for custom software. Use the URL `openpilot.comma.ai` to install the release version.
3. **Supported Car:** Ensure that you have one of [the 275+ supported cars](docs/CARS.md).
4. **Car Harness:** You will also need a [car harness](https://comma.ai/shop/car-harness) to connect your comma 3/3X to your car.
We have detailed instructions for [how to install the harness and device in a car](https://comma.ai/setup). Note that it's possible to run openpilot on [other hardware](https://blog.comma.ai/self-driving-car-for-free/), although it's not plug-and-play.
------
<div align="center" style="text-align: center;">
<h1>FrogPilot 🐸</h1>
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/FrogAi/FrogPilot)
[![Discord](https://img.shields.io/discord/1137853399715549214?label=Discord)](https://discord.frogpilot.download)
[![Last Updated](https://img.shields.io/badge/Last%20Updated-August%209th%2C%202025-brightgreen)](https://github.com/FrogAi/FrogPilot/releases/latest)
[![Wiki](https://img.shields.io/badge/Wiki-FrogPilot-blue?logo=wiki)](https://frogpilot.wiki.gg/)
</div>
------
**FrogPilot** is a custom frog-themed fork of openpilot that embraces a collaborative, community-driven approach to push the project forward. It delivers bleeding-edge features and experimental improvements far ahead of official releases. As an unofficial and highly experimental version of openpilot, **FrogPilot** should always be used with caution!
openpilot vs **FrogPilot**
------
#### Community
| Feature | openpilot | **FrogPilot** |
|---------|:---------:|:---------:|
| A Welcoming Community | ❌ | ✅ |
| Erich / Primary Moderators / 🦇 | ✅ | ❌ |
#### Core Features
| Feature | openpilot | **FrogPilot** |
|---------|:---------:|:---------:|
| Always On Lateral (Steering) | ❌ | ✅ |
| Blind Spot Integration | ✅ | ✅ |
| Conditional Experimental Mode | ❌ | ✅ |
| Custom Themes | ❌ | ✅ |
| Driver Monitoring | ✅ | ✅ |
| Driving Model Selector | ❌ | ✅ |
| Holiday Themes | ❌ | ✅ |
| Speed Limit Support | ❌ | ✅ |
| Weather Detection | ❌ | ✅ |
#### Device & Hardware
| Feature | openpilot | **FrogPilot** |
|---------|:---------:|:---------:|
| Advanced Volume Controller | ❌ | ✅ |
| Automatic Version Backups | ❌ | ✅ |
| C3 Support | ❌ | ✅ |
| comma Pedal Support | ❌ | ✅ |
| High Quality Recordings | ❌ | ✅ |
| SDSU Support | ❌ | ✅ |
| ZSS Support | ❌ | ✅ |
#### Gas/Brake
| Feature | openpilot | **FrogPilot** |
|---------|:---------:|:---------:|
| Adaptive Cruise Control (ACC) | ✅ | ✅ |
| Advanced Live Tuning | ❌ | ✅ |
| Custom Following Distances | ❌ | ✅ |
| Faster Human-Like Acceleration | ❌ | ✅ |
| Human-Like Speed Control in Curves | ❌ | ✅ |
| Smoother Human-Like Braking | ❌ | ✅ |
#### Steering
| Feature | openpilot | **FrogPilot** |
|---------|:---------:|:---------:|
| Advanced Live Tuning | ❌ | ✅ |
| Automatic Lane Changes | ❌ | ✅ |
| Increased Steering Torque* | ❌ | ✅ |
| Lane Centering (LKAS) | ✅ | ✅ |
| Lane Change Assist | ✅ | ✅ |
*Select vehicles only
And much much more!
🌟 Highlight Features
------
### 🚗 Always On Lateral (AOL)
With **"Always On Lateral"**, lane-centering stays active whenever cruise control is on, even when you press the accelerator or brake. This means steering assist won't cut out during manual speed adjustments giving you continuous support through curves, traffic, or mountain roads!
---
### 🧠 Conditional Experimental Mode (CEM)
**["Experimental Mode"](https://blog.comma.ai/090release/#experimental-mode)** lets openpilot drive at the speed it thinks a human would to allow slowing for curves, stopping at stoplights/stop signs, and adapting to traffic. This makes it powerful in complex scenarios, but it's still, well, "experimental" and less predictable than **"Chill Mode"**. But **"Conditional Experimental Mode"** gives you the best of both worlds by automatically switching between **"Chill Mode"** for steady cruising and **"Experimental Mode"** for more advanced situations to help fully automate your driving experience!
**"Conditional Experimental Mode"** switches into **"Experimental Mode"** when conditions like these are met:
- Approaching curves and turns
- Detecting slower or stopped lead vehicles
- Driving below a set speed
- Predicting an upcoming stop (e.g. stoplight or stop sign)
Once conditions clear it returns to **"Chill Mode"** for stability and predictability.
**Note: Stay attentive as "Experimental Mode" is an alpha feature and mistakes are expected!**
---
### 🎭 Driving Personalities
With **"Driving Personalities"**, you choose how the vehicle behaves with four adjustable profiles:
- **Traffic:** Catered towards stop-and-go traffic by minimizing gaps and delays
- **Aggressive:** Aimed to provide tighter following distances and quicker reactions
- **Standard:** Useful for a balanced, all-purpose driving
- **Relaxed:** A smoother driving experience with larger following distance gaps
Each profile can be fine-tuned to change the desired following distance, acceleration, and braking style letting you shape **FrogPilot**'s behavior to match your own driving preferences! Profiles can be switched instantly using the following distance button on the steering wheel, while **"Traffic Mode"** can be enabled by simply holding down the following distance button.
---
### 📏 Speed Limit Controller (SLC)
With **"Speed Limit Controller"**, **FrogPilot** automatically adapts to the road's posted speed using information from downloaded **["OpenStreetMap"](https://www.openstreetmap.org)** maps, online **["Mapbox"](https://www.mapbox.com)** data, and the vehicle's dashboard (if supported).
Offsets let you fine-tune how closely **FrogPilot** follows posted limits across different speed ranges allowing you to cruise slightly above or below for a more natural driving experience. If no speed limit is available, you can choose whether **FrogPilot** drives at the set speed, falls back to the last known speed limit, or uses **"Experimental Mode"** to estimate one with the driving model.
Maps can be downloaded directly in settings and updated automatically on a schedule ensuring your device always has the latest speed limits!
**Note: Speed limits are only as accurate as the available speed limit data. Always stay attentive and adjust your speed when necessary!**
---
### 🎨 Themes
With **"Themes"**, you can personalize **FrogPilot**'s driving screen to make it uniquely yours! Choose from:
- **Color Schemes**
- **Icon Packs**
- **Sound Packs**
- **Turn Signal Animations**
- **Steering Wheel Icons**
Enjoy pre-existing **FrogPilot** and seasonal holiday themes, or you can create your own with the **"Theme Maker"** and even share them with the community! For extra fun, enable features like the Mario Kartstyle **"Rainbow Path"** or **"Random Events"** that add playful visual effects while you drive!
---
And lots more! From safety enhancements to personalization options, **FrogPilot** continues to evolve with features that put you in control. Check it out today for yourself!
---
🔧 Branches
------
| Branch | Install&nbsp;URL | Description | Recommended&nbsp;For |
|----------------------------|---------------------------|--------------------------------------------------------|--------------------------|
| FrogPilot | frogpilot.download | The main release branch. | Everyone |
| FrogPilot&#8209;Staging | staging.frogpilot.download| Beta branch with upcoming features. Expect bugs! | Early&nbsp;Adopters |
| FrogPilot&#8209;Testing | testing.frogpilot.download| Alpha branch with bleeding-edge features. Breaks often!| Advanced&nbsp;Testers |
| FrogPilot&#8209;Development| No :) | Active development branch. Don't use! | **FrogPilot**&nbsp;Developers|
| MAKE&#8209;PRS&#8209;HERE | No :) | Workspace for pull requests. Don't use! | Contributors |
🧰 How to Install
------
The easiest way to install **FrogPilot** is by entering this URL on the installation screen:
```
frogpilot.download
```
**DO NOT** install the **FrogPilot-Development** branch. I'm constantly breaking things on there, so unless you don't want to use **FrogPilot**, **NEVER** install it!
![](https://i.imgur.com/LTCqRqB.png)
🐞 Bug Reports / Feature Requests
------
If you run into bugs, issues, or have ideas for new features, please post about it on the **[FrogPilot Discord](https://discord.gg/frogpilot)**! Feedback helps improve **FrogPilot** and create a better experience for everyone!
To report a bug, please post it in [**#bug-reports**](https://discord.com/channels/1137853399715549214/1162100167110053888).
To request a feature, please post it in [**#feature-requests**](https://discord.com/channels/1137853399715549214/1160318669839147259).
Please include as much detail as possible! Photos, videos, log files, or anything that can help explain the issue or idea are very helpful!
I'll do my best to respond promptly, but not every request can be addressed right away. Your feedback is always appreciated and helps make **FrogPilot** the best it can be!
📋 Credits
------
* [Aidenir](https://github.com/Aidenir)
* [AlexandreSato](https://github.com/AlexandreSato)
* [cfranyota](https://github.com/cfranyota)
* [cydia2020](https://github.com/cydia2020)
* [dragonpilot-community](https://github.com/dragonpilot-community)
* [ErichMoraga](https://github.com/ErichMoraga)
* [garrettpall](https://github.com/garrettpall)
* [jakethesnake420](https://github.com/jakethesnake420)
* [jyoung8607](https://github.com/jyoung8607)
* [mike8643](https://github.com/mike8643)
* [neokii](https://github.com/neokii)
* [OPGM](https://github.com/opgm)
* [OPKR](https://github.com/openpilotkr)
* [pfeiferj](https://github.com/pfeiferj)
* [realfast](https://github.com/realfast)
* [syncword](https://github.com/syncword)
* [twilsonco](https://github.com/twilsonco)
Star History
------
[![Star History Chart](https://api.star-history.com/svg?repos=FrogAi/FrogPilot&type=Date)](https://www.star-history.com/#FrogAi/FrogPilot&Date)
# StarPilot
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/firestar5683/StarPilot)
[![Discord](https://img.shields.io/discord/1137853399715549214?label=Discord)](https://firestar.link/discord)
[![Last Updated](https://img.shields.io/github/last-commit/firestar5683/StarPilot/StarPilot)](https://github.com/firestar5683/StarPilot)
[![Wiki](https://img.shields.io/badge/Wiki-StarPilot-blue?logo=wiki)](https://wiki.firestar.link)
**StarPilot** is a custom fork of [comma.ai's openpilot](https://comma.ai/openpilot),
an open source driver assistance system.
Openpilot provides
* Automated Lane Centering
* Adaptive Cruise Control
* Lane Change Assist
* Driver Monitoring *without wheel nags*
StarPilot adds support for many GM vehicles along with improved tuning,
especially for radar-less (camera only) vehicles.
StarPilot is built off of [FrogPilot](https://github.com/FrogAi/FrogPilot)
and supports the major features FrogPilot offers.
StarPilot has a vibrant, welcoming community [discord](https://firestar.link/discord).
Stop by to chat or ask questions!
## Documentation
Please see [https://wiki.firestar.link](https://wiki.firestar.link) for hardware lists,
installation guides, and software configuration.
## Features
* Full support for Comma C3, C3X, and C4
* C4 is currently in release testing. Join our fleet of C4 testers!
* Model switcher with all of comma's tinygrad driving models
* Special longitudinal planner tuning for VoACC (visual only, radar-less) vehicles
* Galaxy: StarPilot's portal to configure your comma device using your phone from anywhere.
Download models, change settings, update software, visualize live model outputs for tuning.
* Always On Lateral (full time steering assist)*
* Learning Speed Limit Controller*
* Conditional Experimental Mode (CEM)*
* Driving Profiles*
* Custom themes*
* Alert Volume Controller*
* Comma Pedal Interceptor support*
* Toyota SDSU support*
* ZSS support*
* High quality dashcam recordings*
* Enhanced tuning for CEM (dynamic experimental mode switching)
\* [Inherited from FrogPilot](https://github.com/FrogAi/FrogPilot#openpilot-vs-frogpilot)
## GM-only Features
* Increased LKAS fault resiliency
* ASCM_INT and SASCM support
* Custom lateral torque controller, with special tuning for Bolts
* 50% extra torque on 2017 Chevy Bolt
* Improved lateral and longitudinal tuning
* Dashboard cruise control display speed spoofing for vehicles with pedal interceptor
* Extra steering wheel button functionality for vehicles with pedal interceptor
* Optional toggle to boot comma when remote starting your vehicle
## Developer Features
* Native and cross compilation for Windows, Mac, and Ubuntu
* Custom AGNOS to support C3, C3X, and C4
* To run UI on PC:
* `./c3` for large UI
* `./c4` for small UI
* `./build` to produce cross compiled binaries for comma devices.
Uses your comma's sysroot/toolchain
* Toggle: "Use Precompiled Binaries" to allow switching between fast boot / editable builds
* Custom long maneuver tests, specifically designed for regen-only vehicles
+3 -2
View File
@@ -532,11 +532,12 @@ struct CarParams {
useSteeringAngle @0 :Bool;
kp @1 :Float32;
ki @2 :Float32;
kd @8 : Float32;
friction @3 :Float32;
kf @4 :Float32;
steeringAngleDeadzoneDeg @5 :Float32;
latAccelFactor @6 :Float32;
latAccelOffset @7 :Float32;
kfDEPRECATED @4 :Float32;
}
struct LongitudinalPIDTuning {
@@ -544,7 +545,7 @@ struct CarParams {
kpV @1 :List(Float32);
kiBP @2 :List(Float32);
kiV @3 :List(Float32);
kf @6 :Float32;
kfDEPRECATED @6 :Float32;
deadzoneBP @4 :List(Float32);
deadzoneV @5 :List(Float32);
}
+30 -33
View File
@@ -105,11 +105,7 @@ struct FrogPilotCarParams @0xf35cc4560bbf6ec2 {
isHDA2 @3 :Bool;
openpilotLongitudinalControlDisabled @4 :Bool;
safetyConfigs @5 :List(SafetyConfig);
lateralTuning :union {
pid @6 :Car.CarParams.LateralPIDTuning;
torque @7 :Car.CarParams.LateralTorqueTuning;
}
canUseSASCM @6 :Bool;
struct SafetyConfig {
safetyParam @0 :UInt16;
@@ -193,34 +189,35 @@ struct FrogPilotPlan @0xa1680744031fdb2d {
cscTraining @4 :Bool;
dangerJerk @5 :Float32;
desiredFollowDistance @6 :Int64;
experimentalMode @7 :Bool;
forcingStop @8 :Bool;
forcingStopLength @9 :Float32;
frogpilotEvents @10 :List(FrogPilotCarEvent);
increasedStoppedDistance @11 :Float32;
lateralCheck @12 :Bool;
laneWidthLeft @13 :Float32;
laneWidthRight @14 :Float32;
maxAcceleration @15 :Float32;
minAcceleration @16 :Float32;
redLight @17 :Bool;
roadCurvature @18 :Float32;
slcMapSpeedLimit @19 :Float32;
slcMapboxSpeedLimit @20 :Float32;
slcNextSpeedLimit @21 :Float32;
slcOverriddenSpeed @22 :Float32;
slcSpeedLimit @23 :Float32;
slcSpeedLimitOffset @24 :Float32;
slcSpeedLimitSource @25 :Text;
speedJerk @26 :Float32;
speedJerkStock @27 :Float32;
speedLimitChanged @28 :Bool;
tFollow @29 :Float32;
themeUpdated @30 :Bool;
togglesUpdated @31 :Bool;
trackingLead @32 :Bool;
unconfirmedSlcSpeedLimit @33 :Float32;
vCruise @34 :Float32;
disableThrottle @7 :Bool;
experimentalMode @8 :Bool;
forcingStop @9 :Bool;
forcingStopLength @10 :Float32;
frogpilotEvents @11 :List(FrogPilotCarEvent);
increasedStoppedDistance @12 :Float32;
lateralCheck @13 :Bool;
laneWidthLeft @14 :Float32;
laneWidthRight @15 :Float32;
maxAcceleration @16 :Float32;
minAcceleration @17 :Float32;
redLight @18 :Bool;
roadCurvature @19 :Float32;
slcMapSpeedLimit @20 :Float32;
slcMapboxSpeedLimit @21 :Float32;
slcNextSpeedLimit @22 :Float32;
slcOverriddenSpeed @23 :Float32;
slcSpeedLimit @24 :Float32;
slcSpeedLimitOffset @25 :Float32;
slcSpeedLimitSource @26 :Text;
speedJerk @27 :Float32;
speedJerkStock @28 :Float32;
speedLimitChanged @29 :Bool;
tFollow @30 :Float32;
themeUpdated @31 :Bool;
togglesUpdated @32 :Bool;
trackingLead @33 :Bool;
unconfirmedSlcSpeedLimit @34 :Float32;
vCruise @35 :Float32;
}
struct FrogPilotRadarState @0xcb9fd56c7057593a {
+66 -49
View File
@@ -5262,17 +5262,17 @@ const ::capnp::_::RawSchema s_9622723fcbd14c2e = {
0, 5, i_9622723fcbd14c2e, nullptr, nullptr, { &s_9622723fcbd14c2e, nullptr, nullptr, 0, 0, nullptr }, false
};
#endif // !CAPNP_LITE
static const ::capnp::_::AlignedData<147> b_80366e0e804ecc1d = {
static const ::capnp::_::AlignedData<163> b_80366e0e804ecc1d = {
{ 0, 0, 0, 0, 5, 0, 6, 0,
29, 204, 78, 128, 14, 110, 54, 128,
20, 0, 0, 0, 1, 0, 4, 0,
20, 0, 0, 0, 1, 0, 5, 0,
218, 169, 170, 144, 36, 55, 105, 140,
0, 0, 7, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
21, 0, 0, 0, 66, 1, 0, 0,
37, 0, 0, 0, 7, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
33, 0, 0, 0, 199, 1, 0, 0,
33, 0, 0, 0, 255, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
99, 97, 114, 46, 99, 97, 112, 110,
@@ -5281,63 +5281,70 @@ static const ::capnp::_::AlignedData<147> b_80366e0e804ecc1d = {
114, 97, 108, 84, 111, 114, 113, 117,
101, 84, 117, 110, 105, 110, 103, 0,
0, 0, 0, 0, 1, 0, 1, 0,
32, 0, 0, 0, 3, 0, 4, 0,
36, 0, 0, 0, 3, 0, 4, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
209, 0, 0, 0, 138, 0, 0, 0,
237, 0, 0, 0, 138, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
212, 0, 0, 0, 3, 0, 1, 0,
224, 0, 0, 0, 2, 0, 1, 0,
240, 0, 0, 0, 3, 0, 1, 0,
252, 0, 0, 0, 2, 0, 1, 0,
1, 0, 0, 0, 1, 0, 0, 0,
0, 0, 1, 0, 1, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
221, 0, 0, 0, 26, 0, 0, 0,
249, 0, 0, 0, 26, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
216, 0, 0, 0, 3, 0, 1, 0,
228, 0, 0, 0, 2, 0, 1, 0,
244, 0, 0, 0, 3, 0, 1, 0,
0, 1, 0, 0, 2, 0, 1, 0,
2, 0, 0, 0, 2, 0, 0, 0,
0, 0, 1, 0, 2, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
225, 0, 0, 0, 26, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
220, 0, 0, 0, 3, 0, 1, 0,
232, 0, 0, 0, 2, 0, 1, 0,
3, 0, 0, 0, 3, 0, 0, 0,
0, 0, 1, 0, 3, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
229, 0, 0, 0, 74, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
228, 0, 0, 0, 3, 0, 1, 0,
240, 0, 0, 0, 2, 0, 1, 0,
4, 0, 0, 0, 4, 0, 0, 0,
0, 0, 1, 0, 4, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
237, 0, 0, 0, 26, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
232, 0, 0, 0, 3, 0, 1, 0,
244, 0, 0, 0, 2, 0, 1, 0,
5, 0, 0, 0, 5, 0, 0, 0,
0, 0, 1, 0, 5, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
241, 0, 0, 0, 202, 0, 0, 0,
253, 0, 0, 0, 26, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
248, 0, 0, 0, 3, 0, 1, 0,
4, 1, 0, 0, 2, 0, 1, 0,
6, 0, 0, 0, 6, 0, 0, 0,
0, 0, 1, 0, 6, 0, 0, 0,
4, 0, 0, 0, 3, 0, 0, 0,
0, 0, 1, 0, 3, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 0, 0, 122, 0, 0, 0,
1, 1, 0, 0, 74, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 3, 0, 1, 0,
12, 1, 0, 0, 2, 0, 1, 0,
7, 0, 0, 0, 7, 0, 0, 0,
0, 0, 1, 0, 7, 0, 0, 0,
8, 0, 0, 0, 4, 0, 0, 0,
0, 0, 1, 0, 4, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
9, 1, 0, 0, 122, 0, 0, 0,
9, 1, 0, 0, 106, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
8, 1, 0, 0, 3, 0, 1, 0,
20, 1, 0, 0, 2, 0, 1, 0,
5, 0, 0, 0, 5, 0, 0, 0,
0, 0, 1, 0, 5, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
17, 1, 0, 0, 202, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
24, 1, 0, 0, 3, 0, 1, 0,
36, 1, 0, 0, 2, 0, 1, 0,
6, 0, 0, 0, 6, 0, 0, 0,
0, 0, 1, 0, 6, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
33, 1, 0, 0, 122, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
32, 1, 0, 0, 3, 0, 1, 0,
44, 1, 0, 0, 2, 0, 1, 0,
7, 0, 0, 0, 7, 0, 0, 0,
0, 0, 1, 0, 7, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
41, 1, 0, 0, 122, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
40, 1, 0, 0, 3, 0, 1, 0,
52, 1, 0, 0, 2, 0, 1, 0,
3, 0, 0, 0, 8, 0, 0, 0,
0, 0, 1, 0, 8, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
49, 1, 0, 0, 26, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
44, 1, 0, 0, 3, 0, 1, 0,
56, 1, 0, 0, 2, 0, 1, 0,
117, 115, 101, 83, 116, 101, 101, 114,
105, 110, 103, 65, 110, 103, 108, 101,
0, 0, 0, 0, 0, 0, 0, 0,
@@ -5373,7 +5380,8 @@ static const ::capnp::_::AlignedData<147> b_80366e0e804ecc1d = {
10, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
107, 102, 0, 0, 0, 0, 0, 0,
107, 102, 68, 69, 80, 82, 69, 67,
65, 84, 69, 68, 0, 0, 0, 0,
10, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
@@ -5403,6 +5411,14 @@ static const ::capnp::_::AlignedData<147> b_80366e0e804ecc1d = {
0, 0, 0, 0, 0, 0, 0, 0,
108, 97, 116, 65, 99, 99, 101, 108,
79, 102, 102, 115, 101, 116, 0, 0,
10, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
10, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
107, 100, 0, 0, 0, 0, 0, 0,
10, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
@@ -5413,14 +5429,14 @@ static const ::capnp::_::AlignedData<147> b_80366e0e804ecc1d = {
};
::capnp::word const* const bp_80366e0e804ecc1d = b_80366e0e804ecc1d.words;
#if !CAPNP_LITE
static const uint16_t m_80366e0e804ecc1d[] = {3, 4, 2, 1, 6, 7, 5, 0};
static const uint16_t i_80366e0e804ecc1d[] = {0, 1, 2, 3, 4, 5, 6, 7};
static const uint16_t m_80366e0e804ecc1d[] = {3, 8, 4, 2, 1, 6, 7, 5, 0};
static const uint16_t i_80366e0e804ecc1d[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
const ::capnp::_::RawSchema s_80366e0e804ecc1d = {
0x80366e0e804ecc1d, b_80366e0e804ecc1d.words, 147, nullptr, m_80366e0e804ecc1d,
0, 8, i_80366e0e804ecc1d, nullptr, nullptr, { &s_80366e0e804ecc1d, nullptr, nullptr, 0, 0, nullptr }, false
0x80366e0e804ecc1d, b_80366e0e804ecc1d.words, 163, nullptr, m_80366e0e804ecc1d,
0, 9, i_80366e0e804ecc1d, nullptr, nullptr, { &s_80366e0e804ecc1d, nullptr, nullptr, 0, 0, nullptr }, false
};
#endif // !CAPNP_LITE
static const ::capnp::_::AlignedData<151> b_c342cefc303e9b8e = {
static const ::capnp::_::AlignedData<152> b_c342cefc303e9b8e = {
{ 0, 0, 0, 0, 5, 0, 6, 0,
142, 155, 62, 48, 252, 206, 66, 195,
20, 0, 0, 0, 1, 0, 1, 0,
@@ -5486,10 +5502,10 @@ static const ::capnp::_::AlignedData<151> b_c342cefc303e9b8e = {
4, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 0, 6, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
53, 1, 0, 0, 26, 0, 0, 0,
53, 1, 0, 0, 106, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
48, 1, 0, 0, 3, 0, 1, 0,
60, 1, 0, 0, 2, 0, 1, 0,
52, 1, 0, 0, 3, 0, 1, 0,
64, 1, 0, 0, 2, 0, 1, 0,
107, 112, 66, 80, 0, 0, 0, 0,
14, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
@@ -5564,7 +5580,8 @@ static const ::capnp::_::AlignedData<151> b_c342cefc303e9b8e = {
14, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
107, 102, 0, 0, 0, 0, 0, 0,
107, 102, 68, 69, 80, 82, 69, 67,
65, 84, 69, 68, 0, 0, 0, 0,
10, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
@@ -5578,7 +5595,7 @@ static const ::capnp::_::AlignedData<151> b_c342cefc303e9b8e = {
static const uint16_t m_c342cefc303e9b8e[] = {4, 5, 6, 2, 3, 0, 1};
static const uint16_t i_c342cefc303e9b8e[] = {0, 1, 2, 3, 4, 5, 6};
const ::capnp::_::RawSchema s_c342cefc303e9b8e = {
0xc342cefc303e9b8e, b_c342cefc303e9b8e.words, 151, nullptr, m_c342cefc303e9b8e,
0xc342cefc303e9b8e, b_c342cefc303e9b8e.words, 152, nullptr, m_c342cefc303e9b8e,
0, 7, i_c342cefc303e9b8e, nullptr, nullptr, { &s_c342cefc303e9b8e, nullptr, nullptr, 0, 0, nullptr }, false
};
#endif // !CAPNP_LITE
+32 -13
View File
@@ -626,7 +626,7 @@ struct CarParams::LateralTorqueTuning {
class Pipeline;
struct _capnpPrivate {
CAPNP_DECLARE_STRUCT_HEADER(80366e0e804ecc1d, 4, 0)
CAPNP_DECLARE_STRUCT_HEADER(80366e0e804ecc1d, 5, 0)
#if !CAPNP_LITE
static constexpr ::capnp::_::RawBrandedSchema const* brand() { return &schema->defaultBrand; }
#endif // !CAPNP_LITE
@@ -3140,7 +3140,7 @@ public:
inline float getFriction() const;
inline float getKf() const;
inline float getKfDEPRECATED() const;
inline float getSteeringAngleDeadzoneDeg() const;
@@ -3148,6 +3148,8 @@ public:
inline float getLatAccelOffset() const;
inline float getKd() const;
private:
::capnp::_::StructReader _reader;
template <typename, ::capnp::Kind>
@@ -3188,8 +3190,8 @@ public:
inline float getFriction();
inline void setFriction(float value);
inline float getKf();
inline void setKf(float value);
inline float getKfDEPRECATED();
inline void setKfDEPRECATED(float value);
inline float getSteeringAngleDeadzoneDeg();
inline void setSteeringAngleDeadzoneDeg(float value);
@@ -3200,6 +3202,9 @@ public:
inline float getLatAccelOffset();
inline void setLatAccelOffset(float value);
inline float getKd();
inline void setKd(float value);
private:
::capnp::_::StructBuilder _builder;
template <typename, ::capnp::Kind>
@@ -3261,7 +3266,7 @@ public:
inline bool hasDeadzoneV() const;
inline ::capnp::List<float, ::capnp::Kind::PRIMITIVE>::Reader getDeadzoneV() const;
inline float getKf() const;
inline float getKfDEPRECATED() const;
private:
::capnp::_::StructReader _reader;
@@ -3339,8 +3344,8 @@ public:
inline void adoptDeadzoneV(::capnp::Orphan< ::capnp::List<float, ::capnp::Kind::PRIMITIVE>>&& value);
inline ::capnp::Orphan< ::capnp::List<float, ::capnp::Kind::PRIMITIVE>> disownDeadzoneV();
inline float getKf();
inline void setKf(float value);
inline float getKfDEPRECATED();
inline void setKfDEPRECATED(float value);
private:
::capnp::_::StructBuilder _builder;
@@ -7791,16 +7796,16 @@ inline void CarParams::LateralTorqueTuning::Builder::setFriction(float value) {
::capnp::bounded<3>() * ::capnp::ELEMENTS, value);
}
inline float CarParams::LateralTorqueTuning::Reader::getKf() const {
inline float CarParams::LateralTorqueTuning::Reader::getKfDEPRECATED() const {
return _reader.getDataField<float>(
::capnp::bounded<4>() * ::capnp::ELEMENTS);
}
inline float CarParams::LateralTorqueTuning::Builder::getKf() {
inline float CarParams::LateralTorqueTuning::Builder::getKfDEPRECATED() {
return _builder.getDataField<float>(
::capnp::bounded<4>() * ::capnp::ELEMENTS);
}
inline void CarParams::LateralTorqueTuning::Builder::setKf(float value) {
inline void CarParams::LateralTorqueTuning::Builder::setKfDEPRECATED(float value) {
_builder.setDataField<float>(
::capnp::bounded<4>() * ::capnp::ELEMENTS, value);
}
@@ -7847,6 +7852,20 @@ inline void CarParams::LateralTorqueTuning::Builder::setLatAccelOffset(float val
::capnp::bounded<7>() * ::capnp::ELEMENTS, value);
}
inline float CarParams::LateralTorqueTuning::Reader::getKd() const {
return _reader.getDataField<float>(
::capnp::bounded<8>() * ::capnp::ELEMENTS);
}
inline float CarParams::LateralTorqueTuning::Builder::getKd() {
return _builder.getDataField<float>(
::capnp::bounded<8>() * ::capnp::ELEMENTS);
}
inline void CarParams::LateralTorqueTuning::Builder::setKd(float value) {
_builder.setDataField<float>(
::capnp::bounded<8>() * ::capnp::ELEMENTS, value);
}
inline bool CarParams::LongitudinalPIDTuning::Reader::hasKpBP() const {
return !_reader.getPointerField(
::capnp::bounded<0>() * ::capnp::POINTERS).isNull();
@@ -8075,16 +8094,16 @@ inline ::capnp::Orphan< ::capnp::List<float, ::capnp::Kind::PRIMITIVE>> CarPara
::capnp::bounded<5>() * ::capnp::POINTERS));
}
inline float CarParams::LongitudinalPIDTuning::Reader::getKf() const {
inline float CarParams::LongitudinalPIDTuning::Reader::getKfDEPRECATED() const {
return _reader.getDataField<float>(
::capnp::bounded<0>() * ::capnp::ELEMENTS);
}
inline float CarParams::LongitudinalPIDTuning::Builder::getKf() {
inline float CarParams::LongitudinalPIDTuning::Builder::getKfDEPRECATED() {
return _builder.getDataField<float>(
::capnp::bounded<0>() * ::capnp::ELEMENTS);
}
inline void CarParams::LongitudinalPIDTuning::Builder::setKf(float value) {
inline void CarParams::LongitudinalPIDTuning::Builder::setKfDEPRECATED(float value) {
_builder.setDataField<float>(
::capnp::bounded<0>() * ::capnp::ELEMENTS, value);
}
+197 -252
View File
@@ -642,12 +642,12 @@ const ::capnp::_::RawSchema s_aedffd8f31e7b55d = {
};
#endif // !CAPNP_LITE
CAPNP_DEFINE_ENUM(EventName_aedffd8f31e7b55d, aedffd8f31e7b55d);
static const ::capnp::_::AlignedData<132> b_f35cc4560bbf6ec2 = {
static const ::capnp::_::AlignedData<139> b_f35cc4560bbf6ec2 = {
{ 0, 0, 0, 0, 5, 0, 6, 0,
194, 110, 191, 11, 86, 196, 92, 243,
13, 0, 0, 0, 1, 0, 1, 0,
89, 10, 85, 29, 102, 186, 38, 181,
2, 0, 7, 0, 0, 0, 0, 0,
1, 0, 7, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
21, 0, 0, 0, 2, 1, 0, 0,
33, 0, 0, 0, 23, 0, 0, 0,
@@ -707,13 +707,13 @@ static const ::capnp::_::AlignedData<132> b_f35cc4560bbf6ec2 = {
0, 0, 0, 0, 0, 0, 0, 0,
224, 0, 0, 0, 3, 0, 1, 0,
252, 0, 0, 0, 2, 0, 1, 0,
6, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0,
73, 118, 234, 203, 210, 73, 201, 252,
249, 0, 0, 0, 114, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
6, 0, 0, 0, 4, 0, 0, 0,
0, 0, 1, 0, 6, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
249, 0, 0, 0, 98, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
248, 0, 0, 0, 3, 0, 1, 0,
4, 1, 0, 0, 2, 0, 1, 0,
99, 97, 110, 85, 115, 101, 80, 101,
100, 97, 108, 0, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0,
@@ -773,20 +773,26 @@ static const ::capnp::_::AlignedData<132> b_f35cc4560bbf6ec2 = {
14, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
108, 97, 116, 101, 114, 97, 108, 84,
117, 110, 105, 110, 103, 0, 0, 0, }
99, 97, 110, 85, 115, 101, 83, 65,
83, 67, 77, 0, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, }
};
::capnp::word const* const bp_f35cc4560bbf6ec2 = b_f35cc4560bbf6ec2.words;
#if !CAPNP_LITE
static const ::capnp::_::RawSchema* const d_f35cc4560bbf6ec2[] = {
&s_8d65dd40bad40951,
&s_fcc949d2cbea7649,
};
static const uint16_t m_f35cc4560bbf6ec2[] = {0, 1, 2, 3, 6, 4, 5};
static const uint16_t m_f35cc4560bbf6ec2[] = {0, 6, 1, 2, 3, 4, 5};
static const uint16_t i_f35cc4560bbf6ec2[] = {0, 1, 2, 3, 4, 5, 6};
const ::capnp::_::RawSchema s_f35cc4560bbf6ec2 = {
0xf35cc4560bbf6ec2, b_f35cc4560bbf6ec2.words, 132, d_f35cc4560bbf6ec2, m_f35cc4560bbf6ec2,
2, 7, i_f35cc4560bbf6ec2, nullptr, nullptr, { &s_f35cc4560bbf6ec2, nullptr, nullptr, 0, 0, nullptr }, false
0xf35cc4560bbf6ec2, b_f35cc4560bbf6ec2.words, 139, d_f35cc4560bbf6ec2, m_f35cc4560bbf6ec2,
1, 7, i_f35cc4560bbf6ec2, nullptr, nullptr, { &s_f35cc4560bbf6ec2, nullptr, nullptr, 0, 0, nullptr }, false
};
#endif // !CAPNP_LITE
static const ::capnp::_::AlignedData<36> b_8d65dd40bad40951 = {
@@ -836,71 +842,6 @@ const ::capnp::_::RawSchema s_8d65dd40bad40951 = {
0, 1, i_8d65dd40bad40951, nullptr, nullptr, { &s_8d65dd40bad40951, nullptr, nullptr, 0, 0, nullptr }, false
};
#endif // !CAPNP_LITE
static const ::capnp::_::AlignedData<49> b_fcc949d2cbea7649 = {
{ 0, 0, 0, 0, 5, 0, 6, 0,
73, 118, 234, 203, 210, 73, 201, 252,
32, 0, 0, 0, 1, 0, 1, 0,
194, 110, 191, 11, 86, 196, 92, 243,
2, 0, 7, 0, 1, 0, 2, 0,
1, 0, 0, 0, 0, 0, 0, 0,
21, 0, 0, 0, 114, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
33, 0, 0, 0, 119, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
99, 117, 115, 116, 111, 109, 46, 99,
97, 112, 110, 112, 58, 70, 114, 111,
103, 80, 105, 108, 111, 116, 67, 97,
114, 80, 97, 114, 97, 109, 115, 46,
108, 97, 116, 101, 114, 97, 108, 84,
117, 110, 105, 110, 103, 0, 0, 0,
8, 0, 0, 0, 3, 0, 4, 0,
0, 0, 255, 255, 1, 0, 0, 0,
0, 0, 1, 0, 6, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
41, 0, 0, 0, 34, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
36, 0, 0, 0, 3, 0, 1, 0,
48, 0, 0, 0, 2, 0, 1, 0,
1, 0, 254, 255, 1, 0, 0, 0,
0, 0, 1, 0, 7, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
45, 0, 0, 0, 58, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
40, 0, 0, 0, 3, 0, 1, 0,
52, 0, 0, 0, 2, 0, 1, 0,
112, 105, 100, 0, 0, 0, 0, 0,
16, 0, 0, 0, 0, 0, 0, 0,
46, 76, 209, 203, 63, 114, 34, 150,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
16, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
116, 111, 114, 113, 117, 101, 0, 0,
16, 0, 0, 0, 0, 0, 0, 0,
29, 204, 78, 128, 14, 110, 54, 128,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
16, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, }
};
::capnp::word const* const bp_fcc949d2cbea7649 = b_fcc949d2cbea7649.words;
#if !CAPNP_LITE
static const ::capnp::_::RawSchema* const d_fcc949d2cbea7649[] = {
&s_80366e0e804ecc1d,
&s_9622723fcbd14c2e,
&s_f35cc4560bbf6ec2,
};
static const uint16_t m_fcc949d2cbea7649[] = {0, 1};
static const uint16_t i_fcc949d2cbea7649[] = {0, 1};
const ::capnp::_::RawSchema s_fcc949d2cbea7649 = {
0xfcc949d2cbea7649, b_fcc949d2cbea7649.words, 49, d_fcc949d2cbea7649, m_fcc949d2cbea7649,
3, 2, i_fcc949d2cbea7649, nullptr, nullptr, { &s_fcc949d2cbea7649, nullptr, nullptr, 0, 0, nullptr }, false
};
#endif // !CAPNP_LITE
static const ::capnp::_::AlignedData<268> b_da96579883444c35 = {
{ 0, 0, 0, 0, 5, 0, 6, 0,
53, 76, 68, 131, 152, 87, 150, 218,
@@ -1739,7 +1680,7 @@ const ::capnp::_::RawSchema s_f416ec09499d9d19 = {
0, 3, i_f416ec09499d9d19, nullptr, nullptr, { &s_f416ec09499d9d19, nullptr, nullptr, 0, 0, nullptr }, false
};
#endif // !CAPNP_LITE
static const ::capnp::_::AlignedData<597> b_a1680744031fdb2d = {
static const ::capnp::_::AlignedData<613> b_a1680744031fdb2d = {
{ 0, 0, 0, 0, 5, 0, 6, 0,
45, 219, 31, 3, 68, 7, 104, 161,
13, 0, 0, 0, 1, 0, 13, 0,
@@ -1749,7 +1690,7 @@ static const ::capnp::_::AlignedData<597> b_a1680744031fdb2d = {
21, 0, 0, 0, 218, 0, 0, 0,
33, 0, 0, 0, 7, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
29, 0, 0, 0, 175, 7, 0, 0,
29, 0, 0, 0, 231, 7, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
99, 117, 115, 116, 111, 109, 46, 99,
@@ -1757,252 +1698,259 @@ static const ::capnp::_::AlignedData<597> b_a1680744031fdb2d = {
103, 80, 105, 108, 111, 116, 80, 108,
97, 110, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 0, 1, 0,
140, 0, 0, 0, 3, 0, 4, 0,
144, 0, 0, 0, 3, 0, 4, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
197, 3, 0, 0, 138, 0, 0, 0,
225, 3, 0, 0, 138, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
200, 3, 0, 0, 3, 0, 1, 0,
212, 3, 0, 0, 2, 0, 1, 0,
228, 3, 0, 0, 3, 0, 1, 0,
240, 3, 0, 0, 2, 0, 1, 0,
1, 0, 0, 0, 1, 0, 0, 0,
0, 0, 1, 0, 1, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
209, 3, 0, 0, 178, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
212, 3, 0, 0, 3, 0, 1, 0,
224, 3, 0, 0, 2, 0, 1, 0,
2, 0, 0, 0, 64, 0, 0, 0,
0, 0, 1, 0, 2, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
221, 3, 0, 0, 162, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
224, 3, 0, 0, 3, 0, 1, 0,
236, 3, 0, 0, 2, 0, 1, 0,
3, 0, 0, 0, 3, 0, 0, 0,
0, 0, 1, 0, 3, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
233, 3, 0, 0, 74, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
232, 3, 0, 0, 3, 0, 1, 0,
244, 3, 0, 0, 2, 0, 1, 0,
4, 0, 0, 0, 65, 0, 0, 0,
0, 0, 1, 0, 4, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
241, 3, 0, 0, 98, 0, 0, 0,
237, 3, 0, 0, 178, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
240, 3, 0, 0, 3, 0, 1, 0,
252, 3, 0, 0, 2, 0, 1, 0,
5, 0, 0, 0, 4, 0, 0, 0,
0, 0, 1, 0, 5, 0, 0, 0,
2, 0, 0, 0, 64, 0, 0, 0,
0, 0, 1, 0, 2, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
249, 3, 0, 0, 90, 0, 0, 0,
249, 3, 0, 0, 162, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
248, 3, 0, 0, 3, 0, 1, 0,
4, 4, 0, 0, 2, 0, 1, 0,
6, 0, 0, 0, 3, 0, 0, 0,
0, 0, 1, 0, 6, 0, 0, 0,
252, 3, 0, 0, 3, 0, 1, 0,
8, 4, 0, 0, 2, 0, 1, 0,
3, 0, 0, 0, 3, 0, 0, 0,
0, 0, 1, 0, 3, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
1, 4, 0, 0, 178, 0, 0, 0,
5, 4, 0, 0, 74, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
4, 4, 0, 0, 3, 0, 1, 0,
16, 4, 0, 0, 2, 0, 1, 0,
4, 0, 0, 0, 65, 0, 0, 0,
0, 0, 1, 0, 4, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
13, 4, 0, 0, 98, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
12, 4, 0, 0, 3, 0, 1, 0,
24, 4, 0, 0, 2, 0, 1, 0,
5, 0, 0, 0, 4, 0, 0, 0,
0, 0, 1, 0, 5, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
21, 4, 0, 0, 90, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
20, 4, 0, 0, 3, 0, 1, 0,
32, 4, 0, 0, 2, 0, 1, 0,
6, 0, 0, 0, 3, 0, 0, 0,
0, 0, 1, 0, 6, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
29, 4, 0, 0, 178, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
32, 4, 0, 0, 3, 0, 1, 0,
44, 4, 0, 0, 2, 0, 1, 0,
7, 0, 0, 0, 66, 0, 0, 0,
0, 0, 1, 0, 7, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
13, 4, 0, 0, 138, 0, 0, 0,
41, 4, 0, 0, 130, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
16, 4, 0, 0, 3, 0, 1, 0,
28, 4, 0, 0, 2, 0, 1, 0,
40, 4, 0, 0, 3, 0, 1, 0,
52, 4, 0, 0, 2, 0, 1, 0,
8, 0, 0, 0, 67, 0, 0, 0,
0, 0, 1, 0, 8, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
25, 4, 0, 0, 98, 0, 0, 0,
49, 4, 0, 0, 138, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
24, 4, 0, 0, 3, 0, 1, 0,
36, 4, 0, 0, 2, 0, 1, 0,
9, 0, 0, 0, 5, 0, 0, 0,
52, 4, 0, 0, 3, 0, 1, 0,
64, 4, 0, 0, 2, 0, 1, 0,
9, 0, 0, 0, 68, 0, 0, 0,
0, 0, 1, 0, 9, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
33, 4, 0, 0, 146, 0, 0, 0,
61, 4, 0, 0, 98, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
36, 4, 0, 0, 3, 0, 1, 0,
48, 4, 0, 0, 2, 0, 1, 0,
10, 0, 0, 0, 0, 0, 0, 0,
60, 4, 0, 0, 3, 0, 1, 0,
72, 4, 0, 0, 2, 0, 1, 0,
10, 0, 0, 0, 5, 0, 0, 0,
0, 0, 1, 0, 10, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
45, 4, 0, 0, 130, 0, 0, 0,
69, 4, 0, 0, 146, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
44, 4, 0, 0, 3, 0, 1, 0,
72, 4, 0, 0, 2, 0, 1, 0,
11, 0, 0, 0, 8, 0, 0, 0,
72, 4, 0, 0, 3, 0, 1, 0,
84, 4, 0, 0, 2, 0, 1, 0,
11, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 0, 11, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
69, 4, 0, 0, 202, 0, 0, 0,
81, 4, 0, 0, 130, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
76, 4, 0, 0, 3, 0, 1, 0,
88, 4, 0, 0, 2, 0, 1, 0,
12, 0, 0, 0, 68, 0, 0, 0,
80, 4, 0, 0, 3, 0, 1, 0,
108, 4, 0, 0, 2, 0, 1, 0,
12, 0, 0, 0, 8, 0, 0, 0,
0, 0, 1, 0, 12, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
85, 4, 0, 0, 106, 0, 0, 0,
105, 4, 0, 0, 202, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
84, 4, 0, 0, 3, 0, 1, 0,
96, 4, 0, 0, 2, 0, 1, 0,
13, 0, 0, 0, 9, 0, 0, 0,
112, 4, 0, 0, 3, 0, 1, 0,
124, 4, 0, 0, 2, 0, 1, 0,
13, 0, 0, 0, 69, 0, 0, 0,
0, 0, 1, 0, 13, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
93, 4, 0, 0, 114, 0, 0, 0,
121, 4, 0, 0, 106, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
92, 4, 0, 0, 3, 0, 1, 0,
104, 4, 0, 0, 2, 0, 1, 0,
14, 0, 0, 0, 10, 0, 0, 0,
120, 4, 0, 0, 3, 0, 1, 0,
132, 4, 0, 0, 2, 0, 1, 0,
14, 0, 0, 0, 9, 0, 0, 0,
0, 0, 1, 0, 14, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
101, 4, 0, 0, 122, 0, 0, 0,
129, 4, 0, 0, 114, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
100, 4, 0, 0, 3, 0, 1, 0,
112, 4, 0, 0, 2, 0, 1, 0,
15, 0, 0, 0, 11, 0, 0, 0,
128, 4, 0, 0, 3, 0, 1, 0,
140, 4, 0, 0, 2, 0, 1, 0,
15, 0, 0, 0, 10, 0, 0, 0,
0, 0, 1, 0, 15, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
109, 4, 0, 0, 130, 0, 0, 0,
137, 4, 0, 0, 122, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
108, 4, 0, 0, 3, 0, 1, 0,
120, 4, 0, 0, 2, 0, 1, 0,
16, 0, 0, 0, 12, 0, 0, 0,
136, 4, 0, 0, 3, 0, 1, 0,
148, 4, 0, 0, 2, 0, 1, 0,
16, 0, 0, 0, 11, 0, 0, 0,
0, 0, 1, 0, 16, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
117, 4, 0, 0, 130, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
116, 4, 0, 0, 3, 0, 1, 0,
128, 4, 0, 0, 2, 0, 1, 0,
17, 0, 0, 0, 69, 0, 0, 0,
0, 0, 1, 0, 17, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
125, 4, 0, 0, 74, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
124, 4, 0, 0, 3, 0, 1, 0,
136, 4, 0, 0, 2, 0, 1, 0,
18, 0, 0, 0, 13, 0, 0, 0,
0, 0, 1, 0, 18, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
133, 4, 0, 0, 114, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
132, 4, 0, 0, 3, 0, 1, 0,
144, 4, 0, 0, 2, 0, 1, 0,
19, 0, 0, 0, 14, 0, 0, 0,
0, 0, 1, 0, 19, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
141, 4, 0, 0, 138, 0, 0, 0,
145, 4, 0, 0, 130, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
144, 4, 0, 0, 3, 0, 1, 0,
156, 4, 0, 0, 2, 0, 1, 0,
20, 0, 0, 0, 15, 0, 0, 0,
0, 0, 1, 0, 20, 0, 0, 0,
17, 0, 0, 0, 12, 0, 0, 0,
0, 0, 1, 0, 17, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
153, 4, 0, 0, 162, 0, 0, 0,
153, 4, 0, 0, 130, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
156, 4, 0, 0, 3, 0, 1, 0,
168, 4, 0, 0, 2, 0, 1, 0,
21, 0, 0, 0, 16, 0, 0, 0,
0, 0, 1, 0, 21, 0, 0, 0,
152, 4, 0, 0, 3, 0, 1, 0,
164, 4, 0, 0, 2, 0, 1, 0,
18, 0, 0, 0, 70, 0, 0, 0,
0, 0, 1, 0, 18, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
165, 4, 0, 0, 146, 0, 0, 0,
161, 4, 0, 0, 74, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
160, 4, 0, 0, 3, 0, 1, 0,
172, 4, 0, 0, 2, 0, 1, 0,
19, 0, 0, 0, 13, 0, 0, 0,
0, 0, 1, 0, 19, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
169, 4, 0, 0, 114, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
168, 4, 0, 0, 3, 0, 1, 0,
180, 4, 0, 0, 2, 0, 1, 0,
22, 0, 0, 0, 17, 0, 0, 0,
0, 0, 1, 0, 22, 0, 0, 0,
20, 0, 0, 0, 14, 0, 0, 0,
0, 0, 1, 0, 20, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
177, 4, 0, 0, 154, 0, 0, 0,
177, 4, 0, 0, 138, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
180, 4, 0, 0, 3, 0, 1, 0,
192, 4, 0, 0, 2, 0, 1, 0,
23, 0, 0, 0, 18, 0, 0, 0,
21, 0, 0, 0, 15, 0, 0, 0,
0, 0, 1, 0, 21, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
189, 4, 0, 0, 162, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
192, 4, 0, 0, 3, 0, 1, 0,
204, 4, 0, 0, 2, 0, 1, 0,
22, 0, 0, 0, 16, 0, 0, 0,
0, 0, 1, 0, 22, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
201, 4, 0, 0, 146, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
204, 4, 0, 0, 3, 0, 1, 0,
216, 4, 0, 0, 2, 0, 1, 0,
23, 0, 0, 0, 17, 0, 0, 0,
0, 0, 1, 0, 23, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
189, 4, 0, 0, 114, 0, 0, 0,
213, 4, 0, 0, 154, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
188, 4, 0, 0, 3, 0, 1, 0,
200, 4, 0, 0, 2, 0, 1, 0,
24, 0, 0, 0, 19, 0, 0, 0,
216, 4, 0, 0, 3, 0, 1, 0,
228, 4, 0, 0, 2, 0, 1, 0,
24, 0, 0, 0, 18, 0, 0, 0,
0, 0, 1, 0, 24, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
197, 4, 0, 0, 162, 0, 0, 0,
225, 4, 0, 0, 114, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
200, 4, 0, 0, 3, 0, 1, 0,
212, 4, 0, 0, 2, 0, 1, 0,
25, 0, 0, 0, 1, 0, 0, 0,
224, 4, 0, 0, 3, 0, 1, 0,
236, 4, 0, 0, 2, 0, 1, 0,
25, 0, 0, 0, 19, 0, 0, 0,
0, 0, 1, 0, 25, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
209, 4, 0, 0, 162, 0, 0, 0,
233, 4, 0, 0, 162, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
212, 4, 0, 0, 3, 0, 1, 0,
224, 4, 0, 0, 2, 0, 1, 0,
26, 0, 0, 0, 20, 0, 0, 0,
236, 4, 0, 0, 3, 0, 1, 0,
248, 4, 0, 0, 2, 0, 1, 0,
26, 0, 0, 0, 1, 0, 0, 0,
0, 0, 1, 0, 26, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
221, 4, 0, 0, 82, 0, 0, 0,
245, 4, 0, 0, 162, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
220, 4, 0, 0, 3, 0, 1, 0,
232, 4, 0, 0, 2, 0, 1, 0,
27, 0, 0, 0, 21, 0, 0, 0,
248, 4, 0, 0, 3, 0, 1, 0,
4, 5, 0, 0, 2, 0, 1, 0,
27, 0, 0, 0, 20, 0, 0, 0,
0, 0, 1, 0, 27, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
229, 4, 0, 0, 122, 0, 0, 0,
1, 5, 0, 0, 82, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
228, 4, 0, 0, 3, 0, 1, 0,
240, 4, 0, 0, 2, 0, 1, 0,
28, 0, 0, 0, 70, 0, 0, 0,
0, 5, 0, 0, 3, 0, 1, 0,
12, 5, 0, 0, 2, 0, 1, 0,
28, 0, 0, 0, 21, 0, 0, 0,
0, 0, 1, 0, 28, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
237, 4, 0, 0, 146, 0, 0, 0,
9, 5, 0, 0, 122, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
240, 4, 0, 0, 3, 0, 1, 0,
252, 4, 0, 0, 2, 0, 1, 0,
29, 0, 0, 0, 22, 0, 0, 0,
8, 5, 0, 0, 3, 0, 1, 0,
20, 5, 0, 0, 2, 0, 1, 0,
29, 0, 0, 0, 71, 0, 0, 0,
0, 0, 1, 0, 29, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
249, 4, 0, 0, 66, 0, 0, 0,
17, 5, 0, 0, 146, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
244, 4, 0, 0, 3, 0, 1, 0,
0, 5, 0, 0, 2, 0, 1, 0,
30, 0, 0, 0, 71, 0, 0, 0,
20, 5, 0, 0, 3, 0, 1, 0,
32, 5, 0, 0, 2, 0, 1, 0,
30, 0, 0, 0, 22, 0, 0, 0,
0, 0, 1, 0, 30, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
253, 4, 0, 0, 106, 0, 0, 0,
29, 5, 0, 0, 66, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
252, 4, 0, 0, 3, 0, 1, 0,
8, 5, 0, 0, 2, 0, 1, 0,
24, 5, 0, 0, 3, 0, 1, 0,
36, 5, 0, 0, 2, 0, 1, 0,
31, 0, 0, 0, 72, 0, 0, 0,
0, 0, 1, 0, 31, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
5, 5, 0, 0, 122, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
4, 5, 0, 0, 3, 0, 1, 0,
16, 5, 0, 0, 2, 0, 1, 0,
32, 0, 0, 0, 73, 0, 0, 0,
0, 0, 1, 0, 32, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
13, 5, 0, 0, 106, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
12, 5, 0, 0, 3, 0, 1, 0,
24, 5, 0, 0, 2, 0, 1, 0,
33, 0, 0, 0, 23, 0, 0, 0,
0, 0, 1, 0, 33, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
21, 5, 0, 0, 202, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
28, 5, 0, 0, 3, 0, 1, 0,
40, 5, 0, 0, 2, 0, 1, 0,
34, 0, 0, 0, 24, 0, 0, 0,
0, 0, 1, 0, 34, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
37, 5, 0, 0, 66, 0, 0, 0,
33, 5, 0, 0, 106, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
32, 5, 0, 0, 3, 0, 1, 0,
44, 5, 0, 0, 2, 0, 1, 0,
32, 0, 0, 0, 73, 0, 0, 0,
0, 0, 1, 0, 32, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
41, 5, 0, 0, 122, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
40, 5, 0, 0, 3, 0, 1, 0,
52, 5, 0, 0, 2, 0, 1, 0,
33, 0, 0, 0, 74, 0, 0, 0,
0, 0, 1, 0, 33, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
49, 5, 0, 0, 106, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
48, 5, 0, 0, 3, 0, 1, 0,
60, 5, 0, 0, 2, 0, 1, 0,
34, 0, 0, 0, 23, 0, 0, 0,
0, 0, 1, 0, 34, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
57, 5, 0, 0, 202, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
64, 5, 0, 0, 3, 0, 1, 0,
76, 5, 0, 0, 2, 0, 1, 0,
35, 0, 0, 0, 24, 0, 0, 0,
0, 0, 1, 0, 35, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
73, 5, 0, 0, 66, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
68, 5, 0, 0, 3, 0, 1, 0,
80, 5, 0, 0, 2, 0, 1, 0,
97, 99, 99, 101, 108, 101, 114, 97,
116, 105, 111, 110, 74, 101, 114, 107,
0, 0, 0, 0, 0, 0, 0, 0,
@@ -2070,6 +2018,15 @@ static const ::capnp::_::AlignedData<597> b_a1680744031fdb2d = {
5, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
100, 105, 115, 97, 98, 108, 101, 84,
104, 114, 111, 116, 116, 108, 101, 0,
1, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
101, 120, 112, 101, 114, 105, 109, 101,
110, 116, 97, 108, 77, 111, 100, 101,
0, 0, 0, 0, 0, 0, 0, 0,
@@ -2343,11 +2300,11 @@ static const ::capnp::_::AlignedData<597> b_a1680744031fdb2d = {
static const ::capnp::_::RawSchema* const d_a1680744031fdb2d[] = {
&s_81c2f05a394cf4af,
};
static const uint16_t m_a1680744031fdb2d[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 12, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34};
static const uint16_t i_a1680744031fdb2d[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34};
static const uint16_t m_a1680744031fdb2d[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 13, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35};
static const uint16_t i_a1680744031fdb2d[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35};
const ::capnp::_::RawSchema s_a1680744031fdb2d = {
0xa1680744031fdb2d, b_a1680744031fdb2d.words, 597, d_a1680744031fdb2d, m_a1680744031fdb2d,
1, 35, i_a1680744031fdb2d, nullptr, nullptr, { &s_a1680744031fdb2d, nullptr, nullptr, 0, 0, nullptr }, false
0xa1680744031fdb2d, b_a1680744031fdb2d.words, 613, d_a1680744031fdb2d, m_a1680744031fdb2d,
1, 36, i_a1680744031fdb2d, nullptr, nullptr, { &s_a1680744031fdb2d, nullptr, nullptr, 0, 0, nullptr }, false
};
#endif // !CAPNP_LITE
static const ::capnp::_::AlignedData<55> b_cb9fd56c7057593a = {
@@ -2761,18 +2718,6 @@ constexpr ::capnp::_::RawSchema const* FrogPilotCarParams::SafetyConfig::_capnpP
#endif // !CAPNP_NEED_REDUNDANT_CONSTEXPR_DECL
#endif // !CAPNP_LITE
// FrogPilotCarParams::LateralTuning
#if CAPNP_NEED_REDUNDANT_CONSTEXPR_DECL
constexpr uint16_t FrogPilotCarParams::LateralTuning::_capnpPrivate::dataWordSize;
constexpr uint16_t FrogPilotCarParams::LateralTuning::_capnpPrivate::pointerCount;
#endif // !CAPNP_NEED_REDUNDANT_CONSTEXPR_DECL
#if !CAPNP_LITE
#if CAPNP_NEED_REDUNDANT_CONSTEXPR_DECL
constexpr ::capnp::Kind FrogPilotCarParams::LateralTuning::_capnpPrivate::kind;
constexpr ::capnp::_::RawSchema const* FrogPilotCarParams::LateralTuning::_capnpPrivate::schema;
#endif // !CAPNP_NEED_REDUNDANT_CONSTEXPR_DECL
#endif // !CAPNP_LITE
// FrogPilotCarState
#if CAPNP_NEED_REDUNDANT_CONSTEXPR_DECL
constexpr uint16_t FrogPilotCarState::_capnpPrivate::dataWordSize;
+68 -287
View File
@@ -84,7 +84,6 @@ enum class EventName_aedffd8f31e7b55d: uint16_t {
CAPNP_DECLARE_ENUM(EventName, aedffd8f31e7b55d);
CAPNP_DECLARE_SCHEMA(f35cc4560bbf6ec2);
CAPNP_DECLARE_SCHEMA(8d65dd40bad40951);
CAPNP_DECLARE_SCHEMA(fcc949d2cbea7649);
CAPNP_DECLARE_SCHEMA(da96579883444c35);
CAPNP_DECLARE_SCHEMA(ccb4d6b0dc102d40);
CAPNP_DECLARE_SCHEMA(8033e8e60d6a0edb);
@@ -185,10 +184,9 @@ struct FrogPilotCarParams {
class Builder;
class Pipeline;
struct SafetyConfig;
struct LateralTuning;
struct _capnpPrivate {
CAPNP_DECLARE_STRUCT_HEADER(f35cc4560bbf6ec2, 1, 2)
CAPNP_DECLARE_STRUCT_HEADER(f35cc4560bbf6ec2, 1, 1)
#if !CAPNP_LITE
static constexpr ::capnp::_::RawBrandedSchema const* brand() { return &schema->defaultBrand; }
#endif // !CAPNP_LITE
@@ -210,25 +208,6 @@ struct FrogPilotCarParams::SafetyConfig {
};
};
struct FrogPilotCarParams::LateralTuning {
LateralTuning() = delete;
class Reader;
class Builder;
class Pipeline;
enum Which: uint16_t {
PID,
TORQUE,
};
struct _capnpPrivate {
CAPNP_DECLARE_STRUCT_HEADER(fcc949d2cbea7649, 1, 2)
#if !CAPNP_LITE
static constexpr ::capnp::_::RawBrandedSchema const* brand() { return &schema->defaultBrand; }
#endif // !CAPNP_LITE
};
};
struct FrogPilotCarState {
FrogPilotCarState() = delete;
@@ -690,7 +669,7 @@ public:
inline bool hasSafetyConfigs() const;
inline ::capnp::List< ::cereal::FrogPilotCarParams::SafetyConfig, ::capnp::Kind::STRUCT>::Reader getSafetyConfigs() const;
inline typename LateralTuning::Reader getLateralTuning() const;
inline bool getCanUseSASCM() const;
private:
::capnp::_::StructReader _reader;
@@ -742,8 +721,8 @@ public:
inline void adoptSafetyConfigs(::capnp::Orphan< ::capnp::List< ::cereal::FrogPilotCarParams::SafetyConfig, ::capnp::Kind::STRUCT>>&& value);
inline ::capnp::Orphan< ::capnp::List< ::cereal::FrogPilotCarParams::SafetyConfig, ::capnp::Kind::STRUCT>> disownSafetyConfigs();
inline typename LateralTuning::Builder getLateralTuning();
inline typename LateralTuning::Builder initLateralTuning();
inline bool getCanUseSASCM();
inline void setCanUseSASCM(bool value);
private:
::capnp::_::StructBuilder _builder;
@@ -763,7 +742,6 @@ public:
inline explicit Pipeline(::capnp::AnyPointer::Pipeline&& typeless)
: _typeless(kj::mv(typeless)) {}
inline typename LateralTuning::Pipeline getLateralTuning();
private:
::capnp::AnyPointer::Pipeline _typeless;
friend class ::capnp::PipelineHook;
@@ -848,103 +826,6 @@ private:
};
#endif // !CAPNP_LITE
class FrogPilotCarParams::LateralTuning::Reader {
public:
typedef LateralTuning Reads;
Reader() = default;
inline explicit Reader(::capnp::_::StructReader base): _reader(base) {}
inline ::capnp::MessageSize totalSize() const {
return _reader.totalSize().asPublic();
}
#if !CAPNP_LITE
inline ::kj::StringTree toString() const {
return ::capnp::_::structString(_reader, *_capnpPrivate::brand());
}
#endif // !CAPNP_LITE
inline Which which() const;
inline bool isPid() const;
inline bool hasPid() const;
inline ::cereal::CarParams::LateralPIDTuning::Reader getPid() const;
inline bool isTorque() const;
inline bool hasTorque() const;
inline ::cereal::CarParams::LateralTorqueTuning::Reader getTorque() const;
private:
::capnp::_::StructReader _reader;
template <typename, ::capnp::Kind>
friend struct ::capnp::ToDynamic_;
template <typename, ::capnp::Kind>
friend struct ::capnp::_::PointerHelpers;
template <typename, ::capnp::Kind>
friend struct ::capnp::List;
friend class ::capnp::MessageBuilder;
friend class ::capnp::Orphanage;
};
class FrogPilotCarParams::LateralTuning::Builder {
public:
typedef LateralTuning Builds;
Builder() = delete; // Deleted to discourage incorrect usage.
// You can explicitly initialize to nullptr instead.
inline Builder(decltype(nullptr)) {}
inline explicit Builder(::capnp::_::StructBuilder base): _builder(base) {}
inline operator Reader() const { return Reader(_builder.asReader()); }
inline Reader asReader() const { return *this; }
inline ::capnp::MessageSize totalSize() const { return asReader().totalSize(); }
#if !CAPNP_LITE
inline ::kj::StringTree toString() const { return asReader().toString(); }
#endif // !CAPNP_LITE
inline Which which();
inline bool isPid();
inline bool hasPid();
inline ::cereal::CarParams::LateralPIDTuning::Builder getPid();
inline void setPid( ::cereal::CarParams::LateralPIDTuning::Reader value);
inline ::cereal::CarParams::LateralPIDTuning::Builder initPid();
inline void adoptPid(::capnp::Orphan< ::cereal::CarParams::LateralPIDTuning>&& value);
inline ::capnp::Orphan< ::cereal::CarParams::LateralPIDTuning> disownPid();
inline bool isTorque();
inline bool hasTorque();
inline ::cereal::CarParams::LateralTorqueTuning::Builder getTorque();
inline void setTorque( ::cereal::CarParams::LateralTorqueTuning::Reader value);
inline ::cereal::CarParams::LateralTorqueTuning::Builder initTorque();
inline void adoptTorque(::capnp::Orphan< ::cereal::CarParams::LateralTorqueTuning>&& value);
inline ::capnp::Orphan< ::cereal::CarParams::LateralTorqueTuning> disownTorque();
private:
::capnp::_::StructBuilder _builder;
template <typename, ::capnp::Kind>
friend struct ::capnp::ToDynamic_;
friend class ::capnp::Orphanage;
template <typename, ::capnp::Kind>
friend struct ::capnp::_::PointerHelpers;
};
#if !CAPNP_LITE
class FrogPilotCarParams::LateralTuning::Pipeline {
public:
typedef LateralTuning Pipelines;
inline Pipeline(decltype(nullptr)): _typeless(nullptr) {}
inline explicit Pipeline(::capnp::AnyPointer::Pipeline&& typeless)
: _typeless(kj::mv(typeless)) {}
private:
::capnp::AnyPointer::Pipeline _typeless;
friend class ::capnp::PipelineHook;
template <typename, ::capnp::Kind>
friend struct ::capnp::ToDynamic_;
};
#endif // !CAPNP_LITE
class FrogPilotCarState::Reader {
public:
typedef FrogPilotCarState Reads;
@@ -1557,6 +1438,8 @@ public:
inline ::int64_t getDesiredFollowDistance() const;
inline bool getDisableThrottle() const;
inline bool getExperimentalMode() const;
inline bool getForcingStop() const;
@@ -1664,6 +1547,9 @@ public:
inline ::int64_t getDesiredFollowDistance();
inline void setDesiredFollowDistance( ::int64_t value);
inline bool getDisableThrottle();
inline void setDisableThrottle(bool value);
inline bool getExperimentalMode();
inline void setExperimentalMode(bool value);
@@ -2339,22 +2225,20 @@ inline ::capnp::Orphan< ::capnp::List< ::cereal::FrogPilotCarParams::SafetyConfi
::capnp::bounded<0>() * ::capnp::POINTERS));
}
inline typename FrogPilotCarParams::LateralTuning::Reader FrogPilotCarParams::Reader::getLateralTuning() const {
return typename FrogPilotCarParams::LateralTuning::Reader(_reader);
inline bool FrogPilotCarParams::Reader::getCanUseSASCM() const {
return _reader.getDataField<bool>(
::capnp::bounded<4>() * ::capnp::ELEMENTS);
}
inline typename FrogPilotCarParams::LateralTuning::Builder FrogPilotCarParams::Builder::getLateralTuning() {
return typename FrogPilotCarParams::LateralTuning::Builder(_builder);
inline bool FrogPilotCarParams::Builder::getCanUseSASCM() {
return _builder.getDataField<bool>(
::capnp::bounded<4>() * ::capnp::ELEMENTS);
}
#if !CAPNP_LITE
inline typename FrogPilotCarParams::LateralTuning::Pipeline FrogPilotCarParams::Pipeline::getLateralTuning() {
return typename FrogPilotCarParams::LateralTuning::Pipeline(_typeless.noop());
}
#endif // !CAPNP_LITE
inline typename FrogPilotCarParams::LateralTuning::Builder FrogPilotCarParams::Builder::initLateralTuning() {
_builder.setDataField< ::uint16_t>(::capnp::bounded<1>() * ::capnp::ELEMENTS, 0);
_builder.getPointerField(::capnp::bounded<1>() * ::capnp::POINTERS).clear();
return typename FrogPilotCarParams::LateralTuning::Builder(_builder);
inline void FrogPilotCarParams::Builder::setCanUseSASCM(bool value) {
_builder.setDataField<bool>(
::capnp::bounded<4>() * ::capnp::ELEMENTS, value);
}
inline ::uint16_t FrogPilotCarParams::SafetyConfig::Reader::getSafetyParam() const {
return _reader.getDataField< ::uint16_t>(
::capnp::bounded<0>() * ::capnp::ELEMENTS);
@@ -2369,123 +2253,6 @@ inline void FrogPilotCarParams::SafetyConfig::Builder::setSafetyParam( ::uint16_
::capnp::bounded<0>() * ::capnp::ELEMENTS, value);
}
inline ::cereal::FrogPilotCarParams::LateralTuning::Which FrogPilotCarParams::LateralTuning::Reader::which() const {
return _reader.getDataField<Which>(
::capnp::bounded<1>() * ::capnp::ELEMENTS);
}
inline ::cereal::FrogPilotCarParams::LateralTuning::Which FrogPilotCarParams::LateralTuning::Builder::which() {
return _builder.getDataField<Which>(
::capnp::bounded<1>() * ::capnp::ELEMENTS);
}
inline bool FrogPilotCarParams::LateralTuning::Reader::isPid() const {
return which() == FrogPilotCarParams::LateralTuning::PID;
}
inline bool FrogPilotCarParams::LateralTuning::Builder::isPid() {
return which() == FrogPilotCarParams::LateralTuning::PID;
}
inline bool FrogPilotCarParams::LateralTuning::Reader::hasPid() const {
if (which() != FrogPilotCarParams::LateralTuning::PID) return false;
return !_reader.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS).isNull();
}
inline bool FrogPilotCarParams::LateralTuning::Builder::hasPid() {
if (which() != FrogPilotCarParams::LateralTuning::PID) return false;
return !_builder.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS).isNull();
}
inline ::cereal::CarParams::LateralPIDTuning::Reader FrogPilotCarParams::LateralTuning::Reader::getPid() const {
KJ_IREQUIRE((which() == FrogPilotCarParams::LateralTuning::PID),
"Must check which() before get()ing a union member.");
return ::capnp::_::PointerHelpers< ::cereal::CarParams::LateralPIDTuning>::get(_reader.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS));
}
inline ::cereal::CarParams::LateralPIDTuning::Builder FrogPilotCarParams::LateralTuning::Builder::getPid() {
KJ_IREQUIRE((which() == FrogPilotCarParams::LateralTuning::PID),
"Must check which() before get()ing a union member.");
return ::capnp::_::PointerHelpers< ::cereal::CarParams::LateralPIDTuning>::get(_builder.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS));
}
inline void FrogPilotCarParams::LateralTuning::Builder::setPid( ::cereal::CarParams::LateralPIDTuning::Reader value) {
_builder.setDataField<FrogPilotCarParams::LateralTuning::Which>(
::capnp::bounded<1>() * ::capnp::ELEMENTS, FrogPilotCarParams::LateralTuning::PID);
::capnp::_::PointerHelpers< ::cereal::CarParams::LateralPIDTuning>::set(_builder.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS), value);
}
inline ::cereal::CarParams::LateralPIDTuning::Builder FrogPilotCarParams::LateralTuning::Builder::initPid() {
_builder.setDataField<FrogPilotCarParams::LateralTuning::Which>(
::capnp::bounded<1>() * ::capnp::ELEMENTS, FrogPilotCarParams::LateralTuning::PID);
return ::capnp::_::PointerHelpers< ::cereal::CarParams::LateralPIDTuning>::init(_builder.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS));
}
inline void FrogPilotCarParams::LateralTuning::Builder::adoptPid(
::capnp::Orphan< ::cereal::CarParams::LateralPIDTuning>&& value) {
_builder.setDataField<FrogPilotCarParams::LateralTuning::Which>(
::capnp::bounded<1>() * ::capnp::ELEMENTS, FrogPilotCarParams::LateralTuning::PID);
::capnp::_::PointerHelpers< ::cereal::CarParams::LateralPIDTuning>::adopt(_builder.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS), kj::mv(value));
}
inline ::capnp::Orphan< ::cereal::CarParams::LateralPIDTuning> FrogPilotCarParams::LateralTuning::Builder::disownPid() {
KJ_IREQUIRE((which() == FrogPilotCarParams::LateralTuning::PID),
"Must check which() before get()ing a union member.");
return ::capnp::_::PointerHelpers< ::cereal::CarParams::LateralPIDTuning>::disown(_builder.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS));
}
inline bool FrogPilotCarParams::LateralTuning::Reader::isTorque() const {
return which() == FrogPilotCarParams::LateralTuning::TORQUE;
}
inline bool FrogPilotCarParams::LateralTuning::Builder::isTorque() {
return which() == FrogPilotCarParams::LateralTuning::TORQUE;
}
inline bool FrogPilotCarParams::LateralTuning::Reader::hasTorque() const {
if (which() != FrogPilotCarParams::LateralTuning::TORQUE) return false;
return !_reader.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS).isNull();
}
inline bool FrogPilotCarParams::LateralTuning::Builder::hasTorque() {
if (which() != FrogPilotCarParams::LateralTuning::TORQUE) return false;
return !_builder.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS).isNull();
}
inline ::cereal::CarParams::LateralTorqueTuning::Reader FrogPilotCarParams::LateralTuning::Reader::getTorque() const {
KJ_IREQUIRE((which() == FrogPilotCarParams::LateralTuning::TORQUE),
"Must check which() before get()ing a union member.");
return ::capnp::_::PointerHelpers< ::cereal::CarParams::LateralTorqueTuning>::get(_reader.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS));
}
inline ::cereal::CarParams::LateralTorqueTuning::Builder FrogPilotCarParams::LateralTuning::Builder::getTorque() {
KJ_IREQUIRE((which() == FrogPilotCarParams::LateralTuning::TORQUE),
"Must check which() before get()ing a union member.");
return ::capnp::_::PointerHelpers< ::cereal::CarParams::LateralTorqueTuning>::get(_builder.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS));
}
inline void FrogPilotCarParams::LateralTuning::Builder::setTorque( ::cereal::CarParams::LateralTorqueTuning::Reader value) {
_builder.setDataField<FrogPilotCarParams::LateralTuning::Which>(
::capnp::bounded<1>() * ::capnp::ELEMENTS, FrogPilotCarParams::LateralTuning::TORQUE);
::capnp::_::PointerHelpers< ::cereal::CarParams::LateralTorqueTuning>::set(_builder.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS), value);
}
inline ::cereal::CarParams::LateralTorqueTuning::Builder FrogPilotCarParams::LateralTuning::Builder::initTorque() {
_builder.setDataField<FrogPilotCarParams::LateralTuning::Which>(
::capnp::bounded<1>() * ::capnp::ELEMENTS, FrogPilotCarParams::LateralTuning::TORQUE);
return ::capnp::_::PointerHelpers< ::cereal::CarParams::LateralTorqueTuning>::init(_builder.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS));
}
inline void FrogPilotCarParams::LateralTuning::Builder::adoptTorque(
::capnp::Orphan< ::cereal::CarParams::LateralTorqueTuning>&& value) {
_builder.setDataField<FrogPilotCarParams::LateralTuning::Which>(
::capnp::bounded<1>() * ::capnp::ELEMENTS, FrogPilotCarParams::LateralTuning::TORQUE);
::capnp::_::PointerHelpers< ::cereal::CarParams::LateralTorqueTuning>::adopt(_builder.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS), kj::mv(value));
}
inline ::capnp::Orphan< ::cereal::CarParams::LateralTorqueTuning> FrogPilotCarParams::LateralTuning::Builder::disownTorque() {
KJ_IREQUIRE((which() == FrogPilotCarParams::LateralTuning::TORQUE),
"Must check which() before get()ing a union member.");
return ::capnp::_::PointerHelpers< ::cereal::CarParams::LateralTorqueTuning>::disown(_builder.getPointerField(
::capnp::bounded<1>() * ::capnp::POINTERS));
}
inline bool FrogPilotCarState::Reader::getAccelPressed() const {
return _reader.getDataField<bool>(
::capnp::bounded<0>() * ::capnp::ELEMENTS);
@@ -3036,34 +2803,48 @@ inline void FrogPilotPlan::Builder::setDesiredFollowDistance( ::int64_t value) {
::capnp::bounded<3>() * ::capnp::ELEMENTS, value);
}
inline bool FrogPilotPlan::Reader::getExperimentalMode() const {
inline bool FrogPilotPlan::Reader::getDisableThrottle() const {
return _reader.getDataField<bool>(
::capnp::bounded<66>() * ::capnp::ELEMENTS);
}
inline bool FrogPilotPlan::Builder::getExperimentalMode() {
inline bool FrogPilotPlan::Builder::getDisableThrottle() {
return _builder.getDataField<bool>(
::capnp::bounded<66>() * ::capnp::ELEMENTS);
}
inline void FrogPilotPlan::Builder::setExperimentalMode(bool value) {
inline void FrogPilotPlan::Builder::setDisableThrottle(bool value) {
_builder.setDataField<bool>(
::capnp::bounded<66>() * ::capnp::ELEMENTS, value);
}
inline bool FrogPilotPlan::Reader::getForcingStop() const {
inline bool FrogPilotPlan::Reader::getExperimentalMode() const {
return _reader.getDataField<bool>(
::capnp::bounded<67>() * ::capnp::ELEMENTS);
}
inline bool FrogPilotPlan::Builder::getForcingStop() {
inline bool FrogPilotPlan::Builder::getExperimentalMode() {
return _builder.getDataField<bool>(
::capnp::bounded<67>() * ::capnp::ELEMENTS);
}
inline void FrogPilotPlan::Builder::setForcingStop(bool value) {
inline void FrogPilotPlan::Builder::setExperimentalMode(bool value) {
_builder.setDataField<bool>(
::capnp::bounded<67>() * ::capnp::ELEMENTS, value);
}
inline bool FrogPilotPlan::Reader::getForcingStop() const {
return _reader.getDataField<bool>(
::capnp::bounded<68>() * ::capnp::ELEMENTS);
}
inline bool FrogPilotPlan::Builder::getForcingStop() {
return _builder.getDataField<bool>(
::capnp::bounded<68>() * ::capnp::ELEMENTS);
}
inline void FrogPilotPlan::Builder::setForcingStop(bool value) {
_builder.setDataField<bool>(
::capnp::bounded<68>() * ::capnp::ELEMENTS, value);
}
inline float FrogPilotPlan::Reader::getForcingStopLength() const {
return _reader.getDataField<float>(
::capnp::bounded<5>() * ::capnp::ELEMENTS);
@@ -3128,16 +2909,16 @@ inline void FrogPilotPlan::Builder::setIncreasedStoppedDistance(float value) {
inline bool FrogPilotPlan::Reader::getLateralCheck() const {
return _reader.getDataField<bool>(
::capnp::bounded<68>() * ::capnp::ELEMENTS);
::capnp::bounded<69>() * ::capnp::ELEMENTS);
}
inline bool FrogPilotPlan::Builder::getLateralCheck() {
return _builder.getDataField<bool>(
::capnp::bounded<68>() * ::capnp::ELEMENTS);
::capnp::bounded<69>() * ::capnp::ELEMENTS);
}
inline void FrogPilotPlan::Builder::setLateralCheck(bool value) {
_builder.setDataField<bool>(
::capnp::bounded<68>() * ::capnp::ELEMENTS, value);
::capnp::bounded<69>() * ::capnp::ELEMENTS, value);
}
inline float FrogPilotPlan::Reader::getLaneWidthLeft() const {
@@ -3198,16 +2979,16 @@ inline void FrogPilotPlan::Builder::setMinAcceleration(float value) {
inline bool FrogPilotPlan::Reader::getRedLight() const {
return _reader.getDataField<bool>(
::capnp::bounded<69>() * ::capnp::ELEMENTS);
::capnp::bounded<70>() * ::capnp::ELEMENTS);
}
inline bool FrogPilotPlan::Builder::getRedLight() {
return _builder.getDataField<bool>(
::capnp::bounded<69>() * ::capnp::ELEMENTS);
::capnp::bounded<70>() * ::capnp::ELEMENTS);
}
inline void FrogPilotPlan::Builder::setRedLight(bool value) {
_builder.setDataField<bool>(
::capnp::bounded<69>() * ::capnp::ELEMENTS, value);
::capnp::bounded<70>() * ::capnp::ELEMENTS, value);
}
inline float FrogPilotPlan::Reader::getRoadCurvature() const {
@@ -3372,16 +3153,16 @@ inline void FrogPilotPlan::Builder::setSpeedJerkStock(float value) {
inline bool FrogPilotPlan::Reader::getSpeedLimitChanged() const {
return _reader.getDataField<bool>(
::capnp::bounded<70>() * ::capnp::ELEMENTS);
::capnp::bounded<71>() * ::capnp::ELEMENTS);
}
inline bool FrogPilotPlan::Builder::getSpeedLimitChanged() {
return _builder.getDataField<bool>(
::capnp::bounded<70>() * ::capnp::ELEMENTS);
::capnp::bounded<71>() * ::capnp::ELEMENTS);
}
inline void FrogPilotPlan::Builder::setSpeedLimitChanged(bool value) {
_builder.setDataField<bool>(
::capnp::bounded<70>() * ::capnp::ELEMENTS, value);
::capnp::bounded<71>() * ::capnp::ELEMENTS, value);
}
inline float FrogPilotPlan::Reader::getTFollow() const {
@@ -3400,46 +3181,46 @@ inline void FrogPilotPlan::Builder::setTFollow(float value) {
inline bool FrogPilotPlan::Reader::getThemeUpdated() const {
return _reader.getDataField<bool>(
::capnp::bounded<71>() * ::capnp::ELEMENTS);
::capnp::bounded<72>() * ::capnp::ELEMENTS);
}
inline bool FrogPilotPlan::Builder::getThemeUpdated() {
return _builder.getDataField<bool>(
::capnp::bounded<71>() * ::capnp::ELEMENTS);
::capnp::bounded<72>() * ::capnp::ELEMENTS);
}
inline void FrogPilotPlan::Builder::setThemeUpdated(bool value) {
_builder.setDataField<bool>(
::capnp::bounded<71>() * ::capnp::ELEMENTS, value);
}
inline bool FrogPilotPlan::Reader::getTogglesUpdated() const {
return _reader.getDataField<bool>(
::capnp::bounded<72>() * ::capnp::ELEMENTS);
}
inline bool FrogPilotPlan::Builder::getTogglesUpdated() {
return _builder.getDataField<bool>(
::capnp::bounded<72>() * ::capnp::ELEMENTS);
}
inline void FrogPilotPlan::Builder::setTogglesUpdated(bool value) {
_builder.setDataField<bool>(
::capnp::bounded<72>() * ::capnp::ELEMENTS, value);
}
inline bool FrogPilotPlan::Reader::getTrackingLead() const {
inline bool FrogPilotPlan::Reader::getTogglesUpdated() const {
return _reader.getDataField<bool>(
::capnp::bounded<73>() * ::capnp::ELEMENTS);
}
inline bool FrogPilotPlan::Builder::getTrackingLead() {
inline bool FrogPilotPlan::Builder::getTogglesUpdated() {
return _builder.getDataField<bool>(
::capnp::bounded<73>() * ::capnp::ELEMENTS);
}
inline void FrogPilotPlan::Builder::setTrackingLead(bool value) {
inline void FrogPilotPlan::Builder::setTogglesUpdated(bool value) {
_builder.setDataField<bool>(
::capnp::bounded<73>() * ::capnp::ELEMENTS, value);
}
inline bool FrogPilotPlan::Reader::getTrackingLead() const {
return _reader.getDataField<bool>(
::capnp::bounded<74>() * ::capnp::ELEMENTS);
}
inline bool FrogPilotPlan::Builder::getTrackingLead() {
return _builder.getDataField<bool>(
::capnp::bounded<74>() * ::capnp::ELEMENTS);
}
inline void FrogPilotPlan::Builder::setTrackingLead(bool value) {
_builder.setDataField<bool>(
::capnp::bounded<74>() * ::capnp::ELEMENTS, value);
}
inline float FrogPilotPlan::Reader::getUnconfirmedSlcSpeedLimit() const {
return _reader.getDataField<float>(
::capnp::bounded<23>() * ::capnp::ELEMENTS);
+103 -71
View File
@@ -9336,17 +9336,17 @@ const ::capnp::_::RawSchema s_f28c5dc9e09375e3 = {
0, 10, i_f28c5dc9e09375e3, nullptr, nullptr, { &s_f28c5dc9e09375e3, nullptr, nullptr, 0, 0, nullptr }, false
};
#endif // !CAPNP_LITE
static const ::capnp::_::AlignedData<191> b_e774a050cbf689a4 = {
static const ::capnp::_::AlignedData<223> b_e774a050cbf689a4 = {
{ 0, 0, 0, 0, 5, 0, 6, 0,
164, 137, 246, 203, 80, 160, 116, 231,
24, 0, 0, 0, 1, 0, 5, 0,
24, 0, 0, 0, 1, 0, 6, 0,
241, 171, 1, 54, 197, 105, 255, 151,
0, 0, 7, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
21, 0, 0, 0, 90, 1, 0, 0,
41, 0, 0, 0, 7, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
37, 0, 0, 0, 111, 2, 0, 0,
37, 0, 0, 0, 223, 2, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
108, 111, 103, 46, 99, 97, 112, 110,
@@ -9356,84 +9356,98 @@ static const ::capnp::_::AlignedData<191> b_e774a050cbf689a4 = {
111, 114, 113, 117, 101, 83, 116, 97,
116, 101, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 0, 1, 0,
44, 0, 0, 0, 3, 0, 4, 0,
52, 0, 0, 0, 3, 0, 4, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
37, 1, 0, 0, 58, 0, 0, 0,
93, 1, 0, 0, 58, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
32, 1, 0, 0, 3, 0, 1, 0,
44, 1, 0, 0, 2, 0, 1, 0,
88, 1, 0, 0, 3, 0, 1, 0,
100, 1, 0, 0, 2, 0, 1, 0,
1, 0, 0, 0, 1, 0, 0, 0,
0, 0, 1, 0, 1, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
41, 1, 0, 0, 50, 0, 0, 0,
97, 1, 0, 0, 50, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
36, 1, 0, 0, 3, 0, 1, 0,
48, 1, 0, 0, 2, 0, 1, 0,
92, 1, 0, 0, 3, 0, 1, 0,
104, 1, 0, 0, 2, 0, 1, 0,
3, 0, 0, 0, 2, 0, 0, 0,
0, 0, 1, 0, 2, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
45, 1, 0, 0, 18, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
40, 1, 0, 0, 3, 0, 1, 0,
52, 1, 0, 0, 2, 0, 1, 0,
4, 0, 0, 0, 3, 0, 0, 0,
0, 0, 1, 0, 3, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
49, 1, 0, 0, 18, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
44, 1, 0, 0, 3, 0, 1, 0,
56, 1, 0, 0, 2, 0, 1, 0,
5, 0, 0, 0, 4, 0, 0, 0,
0, 0, 1, 0, 4, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
53, 1, 0, 0, 18, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
48, 1, 0, 0, 3, 0, 1, 0,
60, 1, 0, 0, 2, 0, 1, 0,
6, 0, 0, 0, 5, 0, 0, 0,
0, 0, 1, 0, 5, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
57, 1, 0, 0, 18, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
52, 1, 0, 0, 3, 0, 1, 0,
64, 1, 0, 0, 2, 0, 1, 0,
7, 0, 0, 0, 6, 0, 0, 0,
0, 0, 1, 0, 6, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
61, 1, 0, 0, 58, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
56, 1, 0, 0, 3, 0, 1, 0,
68, 1, 0, 0, 2, 0, 1, 0,
8, 0, 0, 0, 1, 0, 0, 0,
0, 0, 1, 0, 7, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
65, 1, 0, 0, 82, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
64, 1, 0, 0, 3, 0, 1, 0,
76, 1, 0, 0, 2, 0, 1, 0,
2, 0, 0, 0, 7, 0, 0, 0,
0, 0, 1, 0, 8, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
73, 1, 0, 0, 82, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
72, 1, 0, 0, 3, 0, 1, 0,
84, 1, 0, 0, 2, 0, 1, 0,
9, 0, 0, 0, 8, 0, 0, 0,
0, 0, 1, 0, 9, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
81, 1, 0, 0, 154, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
84, 1, 0, 0, 3, 0, 1, 0,
96, 1, 0, 0, 2, 0, 1, 0,
10, 0, 0, 0, 9, 0, 0, 0,
0, 0, 1, 0, 10, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
93, 1, 0, 0, 162, 0, 0, 0,
101, 1, 0, 0, 18, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
96, 1, 0, 0, 3, 0, 1, 0,
108, 1, 0, 0, 2, 0, 1, 0,
4, 0, 0, 0, 3, 0, 0, 0,
0, 0, 1, 0, 3, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
105, 1, 0, 0, 18, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
100, 1, 0, 0, 3, 0, 1, 0,
112, 1, 0, 0, 2, 0, 1, 0,
5, 0, 0, 0, 4, 0, 0, 0,
0, 0, 1, 0, 4, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
109, 1, 0, 0, 18, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
104, 1, 0, 0, 3, 0, 1, 0,
116, 1, 0, 0, 2, 0, 1, 0,
6, 0, 0, 0, 5, 0, 0, 0,
0, 0, 1, 0, 5, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
113, 1, 0, 0, 18, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
108, 1, 0, 0, 3, 0, 1, 0,
120, 1, 0, 0, 2, 0, 1, 0,
7, 0, 0, 0, 6, 0, 0, 0,
0, 0, 1, 0, 6, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
117, 1, 0, 0, 58, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
112, 1, 0, 0, 3, 0, 1, 0,
124, 1, 0, 0, 2, 0, 1, 0,
8, 0, 0, 0, 1, 0, 0, 0,
0, 0, 1, 0, 7, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
121, 1, 0, 0, 82, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
120, 1, 0, 0, 3, 0, 1, 0,
132, 1, 0, 0, 2, 0, 1, 0,
2, 0, 0, 0, 7, 0, 0, 0,
0, 0, 1, 0, 8, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
129, 1, 0, 0, 82, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
128, 1, 0, 0, 3, 0, 1, 0,
140, 1, 0, 0, 2, 0, 1, 0,
9, 0, 0, 0, 8, 0, 0, 0,
0, 0, 1, 0, 9, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
137, 1, 0, 0, 154, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
140, 1, 0, 0, 3, 0, 1, 0,
152, 1, 0, 0, 2, 0, 1, 0,
10, 0, 0, 0, 9, 0, 0, 0,
0, 0, 1, 0, 10, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
149, 1, 0, 0, 162, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
152, 1, 0, 0, 3, 0, 1, 0,
164, 1, 0, 0, 2, 0, 1, 0,
11, 0, 0, 0, 10, 0, 0, 0,
0, 0, 1, 0, 11, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
161, 1, 0, 0, 154, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
164, 1, 0, 0, 3, 0, 1, 0,
176, 1, 0, 0, 2, 0, 1, 0,
12, 0, 0, 0, 11, 0, 0, 0,
0, 0, 1, 0, 12, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
173, 1, 0, 0, 66, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
168, 1, 0, 0, 3, 0, 1, 0,
180, 1, 0, 0, 2, 0, 1, 0,
97, 99, 116, 105, 118, 101, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
@@ -9526,16 +9540,34 @@ static const ::capnp::_::AlignedData<191> b_e774a050cbf689a4 = {
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
10, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
100, 101, 115, 105, 114, 101, 100, 76,
97, 116, 101, 114, 97, 108, 74, 101,
114, 107, 0, 0, 0, 0, 0, 0,
10, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
10, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
118, 101, 114, 115, 105, 111, 110, 0,
4, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
4, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, }
};
::capnp::word const* const bp_e774a050cbf689a4 = b_e774a050cbf689a4.words;
#if !CAPNP_LITE
static const uint16_t m_e774a050cbf689a4[] = {0, 9, 4, 10, 1, 8, 5, 3, 6, 2, 7};
static const uint16_t i_e774a050cbf689a4[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
static const uint16_t m_e774a050cbf689a4[] = {0, 9, 4, 10, 11, 1, 8, 5, 3, 6, 2, 7, 12};
static const uint16_t i_e774a050cbf689a4[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
const ::capnp::_::RawSchema s_e774a050cbf689a4 = {
0xe774a050cbf689a4, b_e774a050cbf689a4.words, 191, nullptr, m_e774a050cbf689a4,
0, 11, i_e774a050cbf689a4, nullptr, nullptr, { &s_e774a050cbf689a4, nullptr, nullptr, 0, 0, nullptr }, false
0xe774a050cbf689a4, b_e774a050cbf689a4.words, 223, nullptr, m_e774a050cbf689a4,
0, 13, i_e774a050cbf689a4, nullptr, nullptr, { &s_e774a050cbf689a4, nullptr, nullptr, 0, 0, nullptr }, false
};
#endif // !CAPNP_LITE
static const ::capnp::_::AlignedData<130> b_9024e2d790c82ade = {
+39 -1
View File
@@ -1076,7 +1076,7 @@ struct ControlsState::LateralTorqueState {
class Pipeline;
struct _capnpPrivate {
CAPNP_DECLARE_STRUCT_HEADER(e774a050cbf689a4, 5, 0)
CAPNP_DECLARE_STRUCT_HEADER(e774a050cbf689a4, 6, 0)
#if !CAPNP_LITE
static constexpr ::capnp::_::RawBrandedSchema const* brand() { return &schema->defaultBrand; }
#endif // !CAPNP_LITE
@@ -7531,6 +7531,10 @@ public:
inline float getDesiredLateralAccel() const;
inline float getDesiredLateralJerk() const;
inline ::int32_t getVersion() const;
private:
::capnp::_::StructReader _reader;
template <typename, ::capnp::Kind>
@@ -7592,6 +7596,12 @@ public:
inline float getDesiredLateralAccel();
inline void setDesiredLateralAccel(float value);
inline float getDesiredLateralJerk();
inline void setDesiredLateralJerk(float value);
inline ::int32_t getVersion();
inline void setVersion( ::int32_t value);
private:
::capnp::_::StructBuilder _builder;
template <typename, ::capnp::Kind>
@@ -30064,6 +30074,34 @@ inline void ControlsState::LateralTorqueState::Builder::setDesiredLateralAccel(f
::capnp::bounded<9>() * ::capnp::ELEMENTS, value);
}
inline float ControlsState::LateralTorqueState::Reader::getDesiredLateralJerk() const {
return _reader.getDataField<float>(
::capnp::bounded<10>() * ::capnp::ELEMENTS);
}
inline float ControlsState::LateralTorqueState::Builder::getDesiredLateralJerk() {
return _builder.getDataField<float>(
::capnp::bounded<10>() * ::capnp::ELEMENTS);
}
inline void ControlsState::LateralTorqueState::Builder::setDesiredLateralJerk(float value) {
_builder.setDataField<float>(
::capnp::bounded<10>() * ::capnp::ELEMENTS, value);
}
inline ::int32_t ControlsState::LateralTorqueState::Reader::getVersion() const {
return _reader.getDataField< ::int32_t>(
::capnp::bounded<11>() * ::capnp::ELEMENTS);
}
inline ::int32_t ControlsState::LateralTorqueState::Builder::getVersion() {
return _builder.getDataField< ::int32_t>(
::capnp::bounded<11>() * ::capnp::ELEMENTS);
}
inline void ControlsState::LateralTorqueState::Builder::setVersion( ::int32_t value) {
_builder.setDataField< ::int32_t>(
::capnp::bounded<11>() * ::capnp::ELEMENTS, value);
}
inline bool ControlsState::LateralLQRState::Reader::getActive() const {
return _reader.getDataField<bool>(
::capnp::bounded<0>() * ::capnp::ELEMENTS);
Binary file not shown.
+2
View File
@@ -795,6 +795,8 @@ struct ControlsState @0x97ff69c53601abf1 {
saturated @7 :Bool;
actualLateralAccel @9 :Float32;
desiredLateralAccel @10 :Float32;
desiredLateralJerk @11 :Float32;
version @12 :Int32;
}
struct LateralLQRState {
+28 -1
View File
@@ -164,6 +164,9 @@ std::unordered_map<std::string, uint32_t> keys = {
{"LiveDelay", PERSISTENT},
{"LiveParameters", PERSISTENT},
{"LiveTorqueParameters", PERSISTENT | DONT_LOG},
{"LongitudinalManeuverMode", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"LongitudinalManeuverPaddleMode", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"LongitudinalManeuverStatus", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"LongitudinalPersonality", PERSISTENT},
{"NavDestination", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"NavDestinationWaypoints", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
@@ -224,6 +227,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"AdvancedLateralTune", PERSISTENT},
{"AdvancedLongitudinalTune", PERSISTENT},
{"AggressiveFollow", PERSISTENT},
{"AggressiveFollowHigh", PERSISTENT},
{"AggressiveJerkAcceleration", PERSISTENT},
{"AggressiveJerkDanger", PERSISTENT},
{"AggressiveJerkDeceleration", PERSISTENT},
@@ -234,17 +238,20 @@ std::unordered_map<std::string, uint32_t> keys = {
{"AlwaysOnLateral", PERSISTENT},
{"AlwaysOnLateralLKAS", PERSISTENT},
{"AlwaysOnLateralMain", PERSISTENT},
{"AlwaysAllowUploads", PERSISTENT},
{"AMapKey1", PERSISTENT | DONT_LOG},
{"AMapKey2", PERSISTENT | DONT_LOG},
{"ApiCache_DriveStats", PERSISTENT},
{"AutomaticallyDownloadModels", PERSISTENT},
{"AutomaticUpdates", PERSISTENT},
{"AvailableModelNames", PERSISTENT},
{"AvailableModelSeries", PERSISTENT},
{"AvailableModels", PERSISTENT},
{"BigMap", PERSISTENT},
{"BlacklistedModels", PERSISTENT},
{"BlindSpotMetrics", PERSISTENT},
{"BlindSpotPath", PERSISTENT},
{"BootLogo", PERSISTENT},
{"BorderMetrics", PERSISTENT},
{"CalibratedLateralAcceleration", PERSISTENT},
{"CalibrationProgress", PERSISTENT},
@@ -270,6 +277,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"CEStatus", CLEAR_ON_OFFROAD_TRANSITION},
{"CEStoppedLead", PERSISTENT},
{"ClusterOffset", PERSISTENT},
{"BootLogoToDownload", CLEAR_ON_MANAGER_START},
{"ColorToDownload", CLEAR_ON_MANAGER_START},
{"Compass", PERSISTENT},
{"ConditionalExperimental", PERSISTENT},
@@ -308,6 +316,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"DisengageVolume", PERSISTENT},
{"DoToggleReset", PERSISTENT},
{"DoToggleResetStock", PERSISTENT},
{"DownloadableBootLogos", PERSISTENT},
{"DownloadableColors", PERSISTENT},
{"DownloadableDistanceIcons", PERSISTENT},
{"DownloadableIcons", PERSISTENT},
@@ -319,7 +328,8 @@ std::unordered_map<std::string, uint32_t> keys = {
{"DynamicPathWidth", PERSISTENT},
{"DynamicPedalsOnUI", PERSISTENT},
{"EngageVolume", PERSISTENT},
{"ExperimentalGMTune", PERSISTENT},
{"EVTuning", PERSISTENT},
{"TruckTuning", PERSISTENT},
{"Fahrenheit", PERSISTENT},
{"FavoriteDestinations", PERSISTENT | DONT_LOG},
{"FlashPanda", CLEAR_ON_MANAGER_START},
@@ -345,7 +355,9 @@ std::unordered_map<std::string, uint32_t> keys = {
{"FrogsGoMoosTweak", PERSISTENT},
{"FullMap", PERSISTENT},
{"GasRegenCmd", PERSISTENT},
{"GMPedalLongitudinal", PERSISTENT},
{"GoatScream", PERSISTENT},
{"GoatScreamCriticalAlerts", PERSISTENT},
{"GreenLightAlert", PERSISTENT},
{"HideAlerts", PERSISTENT},
{"HideLeadMarker", PERSISTENT},
@@ -398,12 +410,17 @@ std::unordered_map<std::string, uint32_t> keys = {
{"MinimumBackupSize", PERSISTENT},
{"MinimumLaneChangeSpeed", PERSISTENT},
{"Model", PERSISTENT},
{"ModelVersion", PERSISTENT},
{"ModelDownloadProgress", CLEAR_ON_MANAGER_START},
{"ModelDrivesAndScores", PERSISTENT},
{"ModelRandomizer", PERSISTENT},
{"ModelToDownload", CLEAR_ON_MANAGER_START},
{"ModelUI", PERSISTENT},
{"ModelVersions", PERSISTENT},
{"ModelReleasedDates", PERSISTENT},
{"CommunityFavorites", PERSISTENT},
{"UserFavorites", PERSISTENT},
{"SortModelsByDate", PERSISTENT},
{"NavigationUI", PERSISTENT},
{"NextMapSpeedLimit", CLEAR_ON_MANAGER_START},
{"NewLongAPI", PERSISTENT},
@@ -449,6 +466,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"RandomThemes", PERSISTENT},
{"RefuseVolume", PERSISTENT},
{"RelaxedFollow", PERSISTENT},
{"RelaxedFollowHigh", PERSISTENT},
{"RelaxedJerkAcceleration", PERSISTENT},
{"RelaxedJerkDanger", PERSISTENT},
{"RelaxedJerkDeceleration", PERSISTENT},
@@ -459,6 +477,9 @@ std::unordered_map<std::string, uint32_t> keys = {
{"RoadEdgesWidth", PERSISTENT},
{"RoadName", CLEAR_ON_MANAGER_START},
{"RoadNameUI", PERSISTENT},
{"RedPanda", PERSISTENT},
{"RemapCancelToDistance", PERSISTENT},
{"RemoteStartBootsComma", PERSISTENT},
{"RotatingWheel", PERSISTENT},
{"ScreenBrightness", PERSISTENT},
{"ScreenBrightnessOnroad", PERSISTENT},
@@ -508,6 +529,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"SpeedLimitsFiltered", PERSISTENT | DONT_LOG},
{"SpeedLimitSources", PERSISTENT},
{"StandardFollow", PERSISTENT},
{"StandardFollowHigh", PERSISTENT},
{"StandardJerkAcceleration", PERSISTENT},
{"StandardJerkDanger", PERSISTENT},
{"StandardJerkDeceleration", PERSISTENT},
@@ -524,6 +546,8 @@ std::unordered_map<std::string, uint32_t> keys = {
{"SteerDelayStock", PERSISTENT},
{"SteerFriction", PERSISTENT},
{"SteerFrictionStock", PERSISTENT},
{"SteerOffset", PERSISTENT},
{"SteerOffsetStock", PERSISTENT},
{"SteerLatAccel", PERSISTENT},
{"SteerLatAccelStock", PERSISTENT},
{"SteerKP", PERSISTENT},
@@ -538,6 +562,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"StoppedTimer", PERSISTENT},
{"TacoTune", PERSISTENT},
{"TacoTuneHacks", PERSISTENT},
{"TrailerLoad", PERSISTENT},
{"TestAlert", CLEAR_ON_MANAGER_START},
{"TetheringEnabled", PERSISTENT},
{"ThemeDownloadProgress", CLEAR_ON_MANAGER_START},
@@ -576,6 +601,8 @@ std::unordered_map<std::string, uint32_t> keys = {
{"WarningSoftVolume", PERSISTENT},
{"WheelIcon", PERSISTENT},
{"WheelSpeed", PERSISTENT},
{"StopDistance", PERSISTENT},
{"RecoveryPower", PERSISTENT},
{"WheelToDownload", CLEAR_ON_MANAGER_START},
};
Binary file not shown.
+12 -1
View File
@@ -6,6 +6,7 @@ from collections import deque
from setproctitle import getproctitle
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware import PC
@@ -34,7 +35,17 @@ def set_realtime_priority(level: int) -> None:
def set_core_affinity(cores: list[int]) -> None:
if not PC:
os.sched_setaffinity(0, cores)
for attempt in range(3): # Retry up to 3 times
try:
os.sched_setaffinity(0, cores)
return
except OSError as e:
if e.errno == 22: # EINVAL
time.sleep(0.1) # Brief delay before retry
else:
raise # Re-raise other errors
# If all retries fail, log and continue without affinity
cloudlog.error(f"Failed to set core affinity after retries: {cores}")
def config_realtime_process(cores: int | list[int], priority: int) -> None:
+52 -40
View File
@@ -6,14 +6,13 @@ from datetime import datetime
from pathlib import Path
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, is_url_pingable
from openpilot.frogpilot.common.frogpilot_variables import RESOURCES_REPO, params_memory
GITHUB_URL = f"https://raw.githubusercontent.com/{RESOURCES_REPO}"
GITLAB_URL = f"https://gitlab.com/{RESOURCES_REPO}/-/raw"
GITHUB_URL = "https://raw.githubusercontent.com/firestar5683/StarPilot-Resources"
GITLAB_URL = "https://gitlab.com/firestar5683/FrogPilot-Resources/-/raw"
def check_github_rate_limit(session):
def check_github_rate_limit():
try:
response = session.get("https://api.github.com/rate_limit", timeout=10)
response = requests.get("https://api.github.com/rate_limit")
response.raise_for_status()
rate_limit_info = response.json()
@@ -26,91 +25,104 @@ def check_github_rate_limit(session):
print("GitHub rate limit reached")
print(f"GitHub Rate Limit Resets At (UTC): {reset_time}")
return False
except requests.exceptions.RequestException as exception:
print(f"Error checking GitHub rate limit: {exception}")
except requests.exceptions.RequestException as error:
print(f"Error checking GitHub rate limit: {error}")
return False
def download_file(cancel_param, destination, progress_param, url, download_param, session, offset_bytes=0, total_bytes=0):
def download_file(cancel_param, destination, progress_param, url, download_param, params_memory, allow_unknown_size=False, suppress_errors=False):
try:
destination.parent.mkdir(parents=True, exist_ok=True)
total_size = get_remote_file_size(url, session)
if total_size == 0:
total_size = get_remote_file_size(url, suppress_errors=suppress_errors or allow_unknown_size)
if total_size == 0 and not allow_unknown_size:
if not url.endswith(".gif"):
handle_error(None, "Download invalid...", "Download invalid...", download_param, progress_param)
if suppress_errors:
return
handle_error(None, "Download invalid...", "Download invalid...", download_param, progress_param, params_memory)
return
with session.get(url, stream=True, timeout=10) as response:
with requests.get(url, stream=True, timeout=10) as response:
response.raise_for_status()
with tempfile.NamedTemporaryFile(delete=False, dir=destination.parent) as temp_file:
with tempfile.NamedTemporaryFile(dir=destination.parent, delete=False) as temp_file:
temp_file_path = Path(temp_file.name)
downloaded_size = 0
for chunk in response.iter_content(chunk_size=16384):
if params_memory.get_bool(cancel_param):
temp_file_path.unlink(missing_ok=True)
handle_error(None, "Download cancelled...", "Download cancelled...", download_param, progress_param)
handle_error(None, "Download cancelled...", "Download cancelled...", download_param, progress_param, params_memory)
return
if chunk:
temp_file.write(chunk)
downloaded_size += len(chunk)
if total_bytes:
overall_progress = (offset_bytes + downloaded_size) / total_bytes * 100
else:
overall_progress = downloaded_size / total_size * 100
if overall_progress != 100:
params_memory.put(progress_param, f"{overall_progress:.0f}%")
else:
if total_size > 0:
progress = (downloaded_size / total_size) * 100
if progress != 100:
params_memory.put(progress_param, f"{progress:.0f}%")
else:
params_memory.put(progress_param, "Verifying authenticity...")
elif downloaded_size > 0:
params_memory.put(progress_param, "Verifying authenticity...")
temp_file_path.rename(destination)
except Exception as exception:
handle_request_error(exception, destination, download_param, progress_param)
except Exception as error:
if suppress_errors:
return
handle_request_error(error, destination, download_param, progress_param, params_memory)
def get_remote_file_size(url, session):
def get_remote_file_size(url, suppress_errors=False):
try:
response = session.head(url, headers={"Accept-Encoding": "identity"}, timeout=10)
response = requests.head(url, headers={"Accept-Encoding": "identity"}, timeout=10)
response.raise_for_status()
return int(response.headers.get("Content-Length", 0))
except Exception as exception:
handle_request_error(exception, None, None, None)
except Exception as error:
if not suppress_errors:
handle_request_error(error, None, None, None, None)
return 0
def get_repository_url(session):
def get_repository_url():
if is_url_pingable("https://github.com"):
if check_github_rate_limit(session):
if check_github_rate_limit():
return GITHUB_URL
if is_url_pingable("https://gitlab.com"):
return GITLAB_URL
return None
def handle_error(destination, error_message, error, download_param, progress_param):
def handle_error(destination, error_message, error, download_param, progress_param, params_memory):
if destination:
delete_file(destination)
if progress_param and "404" not in error_message:
if params_memory and progress_param and "404" not in error_message:
print(f"Error occurred: {error}")
params_memory.put(progress_param, error_message)
params_memory.remove(download_param)
def handle_request_error(error, destination, download_param, progress_param):
def handle_request_error(error, destination, download_param, progress_param, params_memory):
error_map = {
requests.exceptions.ConnectionError: "Connection dropped",
requests.exceptions.HTTPError: lambda error: f"Server error ({error.response.status_code})" if error and getattr(error, "response", None) else "Server error",
requests.exceptions.RequestException: "Network request error. Check connection",
requests.exceptions.Timeout: "Download timed out",
requests.ConnectionError: "Connection dropped",
requests.HTTPError: lambda error: f"Server error ({error.response.status_code})" if error.response else "Server error",
requests.RequestException: "Network request error. Check connection",
requests.Timeout: "Download timed out"
}
error_message = error_map.get(type(error), "Unexpected error")
handle_error(destination, f"Failed: {error_message}", error, download_param, progress_param)
handle_error(destination, f"Failed: {error_message}", error, download_param, progress_param, params_memory)
def verify_download(file_path, url, session):
remote_file_size = get_remote_file_size(url, session)
def verify_download(file_path, url, allow_unknown_size=False):
remote_file_size = get_remote_file_size(url, suppress_errors=allow_unknown_size)
if remote_file_size == 0 and allow_unknown_size:
if not file_path.is_file():
print(f"File not found: {file_path}")
return False
if file_path.stat().st_size == 0:
print(f"File is empty: {file_path}")
return False
return True
if remote_file_size == 0:
print(f"Error fetching remote size for {file_path}")
+362 -462
View File
@@ -5,59 +5,240 @@ import requests
import shutil
import time
import urllib.parse
import urllib.request
from pathlib import Path
from urllib.parse import quote_plus
from openpilot.common.basedir import BASEDIR
from openpilot.frogpilot.assets.download_functions import GITLAB_URL, download_file, get_remote_file_size, get_repository_url, handle_error, handle_request_error, verify_download
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, extract_tar, load_json_file, update_json_file
from openpilot.frogpilot.common.frogpilot_variables import DEFAULT_MODEL, MODELS_PATH, RESOURCES_REPO, TINYGRAD_FILES, params, params_default, params_memory, update_frogpilot_toggles
from openpilot.frogpilot.assets.download_functions import GITLAB_URL, download_file, get_repository_url, handle_error, handle_request_error, verify_download
from openpilot.frogpilot.common.frogpilot_utilities import delete_file
from openpilot.frogpilot.common.frogpilot_variables import DEFAULT_MODEL, MODELS_PATH, params, params_default, params_memory
VERSION = "v16"
VERSION_PATH = MODELS_PATH / "model_version"
VERSION = "v21"
CANCEL_DOWNLOAD_PARAM = "CancelModelDownload"
DOWNLOAD_PROGRESS_PARAM = "ModelDownloadProgress"
MODEL_DOWNLOAD_PARAM = "ModelToDownload"
MODEL_DOWNLOAD_ALL_PARAM = "DownloadAllModels"
UPDATE_TINYGRAD_PARAM = "UpdateTinygrad"
DEFAULT_TINYGRAD_SIZE = 87746736
TAR_FILE_NAME = f"Tinygrad_{VERSION}.tar.gz"
TINYGRAD_MODELD_PATH = Path(BASEDIR) / "frogpilot/tinygrad_modeld"
TINYGRAD_REPO_PATH = Path(BASEDIR) / "tinygrad_repo"
class ModelManager:
def __init__(self, boot_run=False):
def __init__(self):
self.available_models = (params.get("AvailableModels", encoding="utf-8") or "").split(",")
self.model_versions = (params.get("ModelVersions", encoding="utf-8") or "").split(",")
self.model_series = (params.get("AvailableModelSeries", encoding="utf-8") or "").split(",")
self.downloading_model = False
self.available_models = (params.get("AvailableModels", encoding="utf-8") or "").split(",")
self.available_model_names = (params.get("AvailableModelNames", encoding="utf-8") or "").split(",")
self.model_versions = (params.get("ModelVersions", encoding="utf-8") or "").split(",")
@staticmethod
def fetch_models(url):
try:
with urllib.request.urlopen(url, timeout=10) as response:
return json.loads(response.read().decode("utf-8"))["models"]
except Exception as error:
handle_request_error(error, None, None, None, None)
return []
self.model_sizes_path = MODELS_PATH / "model_sizes.json"
self.tinygrad_sizes_path = MODELS_PATH / "tinygrad_sizes.json"
@staticmethod
def fetch_all_model_sizes(repo_url):
project_path = "firestar5683/StarPilot-Resources"
branch = "Models"
self.model_sizes = load_json_file(self.model_sizes_path)
self.tinygrad_sizes = load_json_file(self.tinygrad_sizes_path)
if "github" in repo_url:
api_url = f"https://api.github.com/repos/{project_path}/contents?ref={branch}"
elif "gitlab" in repo_url:
api_url = f"https://gitlab.com/api/v4/projects/{urllib.parse.quote_plus(project_path)}/repository/tree?ref={branch}"
else:
return {}
self.session = requests.Session()
self.session.headers.update({"Accept-Language": "en"})
self.session.headers.update({"User-Agent": "frogpilot-model-downloader/1.0 (https://github.com/FrogAi/FrogPilot)"})
try:
response = requests.get(api_url)
response.raise_for_status()
model_files = [file for file in response.json() if "." in file["name"]]
if boot_run:
self.copy_default_model()
self.validate_models()
if "gitlab" in repo_url:
model_sizes = {}
for file in model_files:
file_path = file["path"]
metadata_url = f"https://gitlab.com/api/v4/projects/{urllib.parse.quote_plus(project_path)}/repository/files/{urllib.parse.quote_plus(file_path)}/raw?ref={branch}"
metadata_response = requests.head(metadata_url)
metadata_response.raise_for_status()
model_sizes[file["name"].rsplit(".", 1)[0]] = int(metadata_response.headers.get("content-length", 0))
return model_sizes
else:
return {file["name"].rsplit(".", 1)[0]: file["size"] for file in model_files if "size" in file}
except Exception as error:
handle_request_error(f"Failed to fetch model sizes from {'GitHub' if 'github' in repo_url else 'GitLab'}: {error}", None, None, None, None)
return {}
def handle_verification_failure(self, model, model_path, file_extension):
print(f"Verification failed for model {model}. Retrying from GitLab...")
model_url = f"{GITLAB_URL}/Models/{model}.{file_extension}"
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, model_url, MODEL_DOWNLOAD_PARAM, params_memory)
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, params_memory)
self.downloading_model = False
return
if verify_download(model_path, model_url):
print(f"Model {model} downloaded and verified successfully!")
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
params_memory.remove(MODEL_DOWNLOAD_PARAM)
self.downloading_model = False
else:
handle_error(model_path, "Verification failed...", "GitLab verification failed", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, params_memory)
self.downloading_model = False
def download_model(self, model_to_download):
self.downloading_model = True
repo_url = get_repository_url()
if not repo_url:
handle_error(None, "GitHub and GitLab are offline...", "Repository unavailable", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, params_memory)
self.downloading_model = False
return
try:
model_index = self.available_models.index(model_to_download)
model_version = self.model_versions[model_index]
except Exception:
handle_error(None, f"Unknown model version for {model_to_download}! Download aborted.", "Model download failed", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, params_memory)
self.downloading_model = False
return
if model_version in ("v8", "v9", "v10", "v11", "v12"):
# Download all PKL and metadata files for multi-file tinygrad models (v8 and v9)
filenames = [
f"{model_to_download}_driving_policy_tinygrad.pkl",
f"{model_to_download}_driving_vision_tinygrad.pkl",
f"{model_to_download}_driving_policy_metadata.pkl",
f"{model_to_download}_driving_vision_metadata.pkl",
]
if model_version == "v12":
filenames += [
f"{model_to_download}_driving_off_policy_tinygrad.pkl",
f"{model_to_download}_driving_off_policy_metadata.pkl",
]
for filename in filenames:
model_path = MODELS_PATH / filename
model_url = f"{repo_url}/Models/{filename}"
print(f"Downloading model file: {filename}")
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, model_url, MODEL_DOWNLOAD_PARAM, params_memory)
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, params_memory)
self.downloading_model = False
return
if verify_download(model_path, model_url):
print(f"File {filename} downloaded and verified successfully!")
params_memory.put(DOWNLOAD_PROGRESS_PARAM, f"Downloaded {filename}!")
else:
self.handle_verification_failure(filename[:-4], model_path, "pkl")
self.downloading_model = False
return
# After all files are downloaded and verified
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
params_memory.remove(MODEL_DOWNLOAD_PARAM)
elif model_version == "v7":
# Download both PKL and metadata for OG tinygrad models
v7_filenames = [
f"{model_to_download}.pkl",
f"{model_to_download}_metadata.pkl"
]
for filename in v7_filenames:
model_path = MODELS_PATH / filename
model_url = f"{repo_url}/Models/{filename}"
print(f"Downloading v7 model file: {filename}")
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, model_url, MODEL_DOWNLOAD_PARAM, params_memory)
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, params_memory)
self.downloading_model = False
return
if verify_download(model_path, model_url):
print(f"File {filename} downloaded and verified successfully!")
params_memory.put(DOWNLOAD_PROGRESS_PARAM, f"Downloaded {filename}!")
else:
self.handle_verification_failure(filename.rsplit('.',1)[0], model_path, "pkl")
self.downloading_model = False
return
# Once both files are fetched
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
params_memory.remove(MODEL_DOWNLOAD_PARAM)
else:
# Classic model: download only the .thneed file
file_extension = "thneed"
model_path = MODELS_PATH / f"{model_to_download}.{file_extension}"
model_url = f"{repo_url}/Models/{model_to_download}.{file_extension}"
print(f"Downloading classic model: {model_to_download}")
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, model_url, MODEL_DOWNLOAD_PARAM, params_memory)
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, params_memory)
self.downloading_model = False
return
if verify_download(model_path, model_url):
print(f"Model {model_to_download} downloaded and verified successfully!")
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
params_memory.remove(MODEL_DOWNLOAD_PARAM)
else:
self.handle_verification_failure(model_to_download, model_path, file_extension)
self.downloading_model = False
return
self.downloading_model = False
@staticmethod
def copy_default_model():
default_model_path = MODELS_PATH / f"{DEFAULT_MODEL}.thneed"
source_path = Path(__file__).parents[2] / "selfdrive/modeld/models/supercombo.thneed"
if source_path.is_file() and not default_model_path.is_file():
shutil.copyfile(source_path, default_model_path)
print(f"Copied the default model from {source_path} to {default_model_path}")
def check_models(self, boot_run, repo_url):
downloaded_models = [
model for model in MODELS_PATH.iterdir()
if (MODELS_PATH / f"{model}.thneed").is_file() or all((MODELS_PATH / f"{model}_{filename}").is_file() for filename, _ in TINYGRAD_FILES)
]
for model_file in downloaded_models:
if not any(model in model_file.name for model in set(self.available_models)):
available_models = set(self.available_models) - {DEFAULT_MODEL}
downloaded_models = set()
for model in available_models:
try:
model_index = self.available_models.index(model)
model_version = self.model_versions[model_index]
except Exception:
model_version = None
if model_version in ("v8", "v9", "v10", "v11", "v12"):
v8_v9_files = [
f"{model}_driving_policy_tinygrad.pkl",
f"{model}_driving_vision_tinygrad.pkl",
f"{model}_driving_policy_metadata.pkl",
f"{model}_driving_vision_metadata.pkl",
]
if model_version == "v12":
v8_v9_files += [
f"{model}_driving_off_policy_tinygrad.pkl",
f"{model}_driving_off_policy_metadata.pkl",
]
if all((MODELS_PATH / f).is_file() for f in v8_v9_files):
downloaded_models.add(model)
elif model_version == "v7":
filename = f"{model}.pkl"
if (MODELS_PATH / filename).is_file():
downloaded_models.add(model)
else:
filename = f"{model}.thneed"
if (MODELS_PATH / filename).is_file():
downloaded_models.add(model)
outdated_models = downloaded_models - available_models
for model in outdated_models:
for model_file in MODELS_PATH.glob(f"{model}*"):
print(f"Removing outdated model: {model_file}")
delete_file(model_file)
@@ -65,466 +246,185 @@ class ModelManager:
if tmp_file.is_file():
delete_file(tmp_file)
if params.get("Model", encoding="utf-8").removesuffix("_default") not in self.available_models:
if params.get("Model", encoding="utf-8") not in self.available_models:
params.put("Model", params_default.get("Model", encoding="utf-8"))
if not (not boot_run and params.get_bool("AutomaticallyDownloadModels")):
automatically_download_models = params.get_bool("AutomaticallyDownloadModels")
if not automatically_download_models:
return
model_sizes = self.fetch_all_model_sizes(repo_url)
if not model_sizes:
print("No model size data available. Skipping model checks...")
return
print("No model size data available. Continuing downloads based on file existence")
# do not return; proceed to download missing files
need_to_update_models = False
for model in self.available_models:
if self.is_tinygrad_model(model):
model_file = MODELS_PATH / f"{model}.thneed"
if not model_file.is_file():
need_to_update_models = True
continue
needs_download = False
expected_size = model_sizes.get(model_file.name)
local_size = self.model_sizes.get(model_file.name)
# Enhanced model file validation per model version
for model in available_models:
model_version = None
try:
model_index = self.available_models.index(model)
model_version = self.model_versions[model_index]
except Exception:
model_version = None
if expected_size > 0 and local_size != expected_size:
print(f"Model {model} is outdated. Deleting {model_file}...")
delete_file(model_file)
need_to_update_models = True
else:
model_missing = False
model_outdated = False
for filename, _ in TINYGRAD_FILES:
expected_file = MODELS_PATH / f"{model}_{filename}"
if not expected_file.is_file():
model_missing = True
need_to_update_models = True
if model_version in ("v8", "v9", "v10", "v11", "v12"):
v8_v9_files = [
f"{model}_driving_policy_tinygrad.pkl",
f"{model}_driving_vision_tinygrad.pkl",
f"{model}_driving_policy_metadata.pkl",
f"{model}_driving_vision_metadata.pkl",
]
if model_version == "v12":
v8_v9_files += [
f"{model}_driving_off_policy_tinygrad.pkl",
f"{model}_driving_off_policy_metadata.pkl",
]
for filename in v8_v9_files:
path = MODELS_PATH / filename
expected_size = model_sizes.get(filename.rsplit(".", 1)[0])
if not path.is_file() or expected_size is None or path.stat().st_size != expected_size:
needs_download = True
break
for filename, _ in TINYGRAD_FILES:
model_file = f"{model}_{filename}"
expected_size = model_sizes.get(model_file)
local_size = self.model_sizes.get(model_file)
if expected_size > 0 and local_size != expected_size:
model_outdated = True
need_to_update_models = True
break
if model_missing or model_outdated:
print(f"Model {model} is either missing required files or outdated. Deleting...")
for filename, _ in TINYGRAD_FILES:
delete_file(MODELS_PATH / f"{model}_{filename}")
if need_to_update_models:
params_memory.put_bool(MODEL_DOWNLOAD_ALL_PARAM, True)
def check_tinygrad(self, repo_url):
tinygrad_url = f"{repo_url}/Tinygrad/{TAR_FILE_NAME}"
expected_size = get_remote_file_size(tinygrad_url, self.session)
local_size = int(self.tinygrad_sizes.get(TAR_FILE_NAME, 0))
if expected_size > 0 and local_size != expected_size:
print(f"Tinygrad version {VERSION} is outdated, expected_size: {expected_size}, local_size: {local_size}, flagging for update...")
params.put_bool("TinygradUpdateAvailable", True)
def copy_default_model(self):
classic_default_model_path = MODELS_PATH / "wd-40.thneed"
source_path = Path(__file__).parents[1] / "classic_modeld/models/supercombo.thneed"
if source_path.is_file() and (not classic_default_model_path.is_file() or source_path.stat().st_size != classic_default_model_path.stat().st_size):
shutil.copyfile(source_path, classic_default_model_path)
print(f"Copied the classic default model from {source_path} to {classic_default_model_path}")
self.update_model_size(classic_default_model_path)
default_model_path = MODELS_PATH / "national-public-radio.thneed"
source_path = Path(__file__).parents[2] / "selfdrive/modeld/models/supercombo.thneed"
if source_path.is_file() and (not default_model_path.is_file() or source_path.stat().st_size != default_model_path.stat().st_size):
shutil.copyfile(source_path, default_model_path)
print(f"Copied the default model from {source_path} to {default_model_path}")
self.update_model_size(default_model_path)
for filename, description in TINYGRAD_FILES:
source = TINYGRAD_MODELD_PATH / "models" / filename
target = MODELS_PATH / f"{DEFAULT_MODEL}_{filename}"
if source.is_file() and (not target.is_file() or source.stat().st_size != target.stat().st_size):
shutil.copyfile(source, target)
print(f"Copied the tinygrad {description} from {source} to {target}")
def download_all_models(self):
repo_url = get_repository_url(self.session)
if not repo_url:
handle_error(None, "GitHub and GitLab are offline...", "Repository unavailable", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
return
self.fetch_models(f"{repo_url}/Versions/model_names_{VERSION}.json", repo_url)
for model in self.available_models:
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_ALL_PARAM, DOWNLOAD_PROGRESS_PARAM)
return
if self.is_tinygrad_model(model):
already_downloaded = (MODELS_PATH / f"{model}.thneed").is_file()
elif model_version == "v7":
filename = f"{model}.pkl"
path = MODELS_PATH / filename
expected_size = model_sizes.get(model)
if not path.is_file() or expected_size is None or path.stat().st_size != expected_size:
needs_download = True
else:
already_downloaded = all((MODELS_PATH / f"{model}_{filename}").is_file() for filename, _ in TINYGRAD_FILES)
filename = f"{model}.thneed"
path = MODELS_PATH / filename
expected_size = model_sizes.get(model)
if not path.is_file() or expected_size is None or path.stat().st_size != expected_size:
needs_download = True
if already_downloaded:
continue
if needs_download:
self.download_all_models()
print(f"Model {model} is not downloaded. Preparing to download...")
params_memory.put(DOWNLOAD_PROGRESS_PARAM, f"Downloading \"{self.available_model_names[self.available_models.index(model)]}\"...")
self.download_model(model)
def update_model_params(self, model_info, repo_url):
self.available_models = [model["id"] for model in model_info]
self.model_versions = [model["version"] for model in model_info]
self.model_series = [model.get("series", "Dom Forgot To Label Me") for model in model_info]
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "All models downloaded!")
params_memory.remove(MODEL_DOWNLOAD_ALL_PARAM)
params.put("AvailableModels", ",".join(self.available_models))
params.put("AvailableModelNames", ",".join([model["name"] for model in model_info]))
params.put("AvailableModelSeries", ",".join(self.model_series))
params.put("CommunityFavorites", ",".join([model["id"] for model in model_info if model.get("community_favorite", False)]))
params.put("ModelReleasedDates", ",".join([model.get("released", "2023-01-01") for model in model_info]))
params.put("ModelVersions", ",".join(self.model_versions))
params.put("CommunityFavorites", ",".join([model["id"] for model in model_info if model.get("community_favorite", False)]))
params.put("AvailableModelSeries", ",".join(self.model_series))
print("Models list updated successfully")
def download_model(self, model_to_download):
self.downloading_model = True
# --- Generate per-model version JSON for offline UI ---
try:
versions_file = MODELS_PATH / ".model_versions.json"
version_map = {model_id: version for model_id, version in zip(self.available_models, self.model_versions)}
with open(versions_file, "w") as vf:
json.dump(version_map, vf)
except Exception as e:
print(f"Failed to write .model_versions.json: {e}")
# --- end JSON generation ---
repo_url = get_repository_url(self.session)
if not repo_url:
handle_error(None, "GitHub and GitLab are offline...", "Repository unavailable", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
self.downloading_model = False
return
# Immediately sync the active ModelVersion param
try:
current = params.get("Model", encoding="utf-8")
if current in version_map:
params.put("ModelVersion", version_map[current])
print(f"Successfully synced ModelVersion to {version_map[current]} for model {current}")
else:
print(f"Warning: Model {current} not found in version map")
except Exception as e:
print(f"Failed to sync ModelVersion for {current}: {e}")
if self.is_tinygrad_model(model_to_download):
model_path = MODELS_PATH / f"{model_to_download}.thneed"
model_url = f"{repo_url}/Models/{model_to_download}.thneed"
# Also ensure ModelVersion is set for the default model if not already set
try:
if not params.get("ModelVersion", encoding="utf-8"):
default_model = params.get("Model", encoding="utf-8") or DEFAULT_MODEL
if default_model in version_map:
params.put("ModelVersion", version_map[default_model])
print(f"Set default ModelVersion to {version_map[default_model]} for model {default_model}")
except Exception as e:
print(f"Failed to set default ModelVersion: {e}")
print(f"Downloading model: {model_to_download}")
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, model_url, MODEL_DOWNLOAD_PARAM, self.session)
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
delete_file(model_path)
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
self.downloading_model = False
return
if verify_download(model_path, model_url, self.session):
print(f"Model {model_to_download} downloaded and verified successfully!")
self.update_model_size(model_path)
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
params_memory.remove(MODEL_DOWNLOAD_PARAM)
self.downloading_model = False
return
print(f"Verification failed for model {model_to_download}. Retrying from GitLab...")
fallback_url = f"{GITLAB_URL}/Models/{model_to_download}.thneed"
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, fallback_url, MODEL_DOWNLOAD_PARAM, self.session)
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
delete_file(model_path)
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
self.downloading_model = False
return
if verify_download(model_path, fallback_url, self.session):
print(f"Model {model_to_download} downloaded and verified successfully from GitLab!")
self.update_model_size(model_path)
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
params_memory.remove(MODEL_DOWNLOAD_PARAM)
self.downloading_model = False
else:
handle_error(model_path, "Verification failed...", "GitLab verification failed", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
self.downloading_model = False
else:
all_model_sizes = self.fetch_all_model_sizes(repo_url) or {}
tinygrad_filenames = [f"{model_to_download}_{file_key}" for file_key, _ in TINYGRAD_FILES]
file_sizes = []
file_sources = []
missing = [name for name in tinygrad_filenames if int(all_model_sizes.get(name, 0)) <= 0]
if missing:
handle_error(None, "Missing size metadata...", f"Sizes not found for: {', '.join(missing)}...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
self.downloading_model = False
return
for filename in tinygrad_filenames:
primary_url = f"{repo_url}/Models/compiled/{filename}"
file_size = int(all_model_sizes.get(filename, 0))
file_sizes.append(file_size)
file_sources.append((primary_url, None))
downloaded_offset_bytes = 0
known_file_sizes = [size for size in file_sizes if size > 0]
total_model_bytes = sum(known_file_sizes) if len(known_file_sizes) == len(file_sizes) else 0
for (file_key, description), part_bytes, (primary_url, fallback_url) in zip(TINYGRAD_FILES, file_sizes, file_sources):
filename = f"{model_to_download}_{file_key}"
model_path = MODELS_PATH / filename
print(f"Downloading {description} for model: {model_to_download}")
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, primary_url, MODEL_DOWNLOAD_PARAM, self.session, offset_bytes=downloaded_offset_bytes, total_bytes=total_model_bytes)
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
delete_file(model_path)
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
self.downloading_model = False
return
if verify_download(model_path, primary_url, self.session):
print(f"{description.capitalize()} for {model_to_download} downloaded and verified successfully!")
if total_model_bytes:
downloaded_offset_bytes += part_bytes
continue
print(f"Verification failed for {filename}. Retrying from GitLab...")
fallback_url = f"{GITLAB_URL}/Models/compiled/{filename}"
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, fallback_url, MODEL_DOWNLOAD_PARAM, self.session, offset_bytes=downloaded_offset_bytes, total_bytes=total_model_bytes)
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
delete_file(model_path)
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
self.downloading_model = False
return
if verify_download(model_path, fallback_url, self.session):
print(f"{description.capitalize()} for {model_to_download} downloaded and verified successfully from GitLab!")
if total_model_bytes:
downloaded_offset_bytes += part_bytes
else:
handle_error(model_path, "Verification failed...", f"GitLab verification failed for {filename}", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
self.downloading_model = False
return
print(f"Updating model sizes for {model_to_download}...")
for filename, _ in TINYGRAD_FILES:
file_path = MODELS_PATH / f"{model_to_download}_{filename}"
self.update_model_size(file_path)
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
params_memory.remove(MODEL_DOWNLOAD_PARAM)
self.downloading_model = False
def fetch_all_model_sizes(self, repo_url):
is_github = "github" in repo_url
is_gitlab = "gitlab" in repo_url
repo_encoded = quote_plus(RESOURCES_REPO)
model_sizes = {}
try:
def fetch_dir_sizes(api_url):
sizes = {}
print(f"Fetching model metadata: {api_url}")
response = self.session.get(api_url, timeout=10)
response.raise_for_status()
content = response.json()
model_files = [file for file in content if "." in file["name"]]
if is_github:
for file in model_files:
sizes[file["name"]] = file.get("size", 0)
else:
for file in model_files:
file_path = quote_plus(file["path"])
metadata_url = f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/files/{file_path}/raw?ref=Models"
head_response = self.session.head(metadata_url, timeout=10)
if head_response.ok:
sizes[file["name"]] = int(head_response.headers.get("content-length", 0))
return sizes
if is_github:
top_api_url = f"https://api.github.com/repos/{RESOURCES_REPO}/contents?ref=Models"
version_api_url = f"https://api.github.com/repos/{RESOURCES_REPO}/contents/compiled?ref=Models"
elif is_gitlab:
top_api_url = f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/tree?ref=Models"
version_api_url = f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/tree?path=compiled&ref=Models"
else:
print(f"Unsupported repository URL: {repo_url}")
return model_sizes
model_sizes.update(fetch_dir_sizes(top_api_url))
model_sizes.update(fetch_dir_sizes(version_api_url))
return model_sizes
except requests.exceptions.RequestException as e:
handle_request_error(f"Failed to fetch model sizes from {'GitHub' if is_github else 'GitLab'}: {e}", None, None, None)
return {}
def fetch_models(self, url, repo_url, boot_run=False):
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
model_info = response.json().get("models", [])
if model_info:
self.update_model_params(model_info)
self.check_models(boot_run, repo_url)
self.check_tinygrad(repo_url)
except Exception as exception:
handle_request_error(exception, None, None, None)
return []
def is_tinygrad_model(self, model):
return self.model_versions[self.available_models.index(model)] in {"v1", "v2", "v3", "v4", "v5", "v6"}
def update_model_params(self, model_info):
self.available_models = [model["id"] for model in model_info]
self.available_model_names = [model["name"] for model in model_info]
self.model_versions = [model["version"] for model in model_info]
params.put("AvailableModels", ",".join(self.available_models))
params.put("AvailableModelNames", ",".join(self.available_model_names))
params.put("ModelVersions", ",".join(self.model_versions))
print("Models list updated successfully!")
def update_models(self, boot_run):
def update_models(self, boot_run=False):
if self.downloading_model:
return
repo_url = get_repository_url(self.session)
repo_url = get_repository_url()
if repo_url is None:
print("GitHub and GitLab are offline...")
return
self.fetch_models(f"{repo_url}/Versions/model_names_{VERSION}.json", repo_url, boot_run)
model_info = self.fetch_models(f"{repo_url}/Versions/model_names_{VERSION}.json")
if model_info:
self.update_model_params(model_info, repo_url)
self.check_models(boot_run, repo_url)
def update_model_size(self, file_path):
self.model_sizes[file_path.name] = file_path.stat().st_size
update_json_file(self.model_sizes_path, self.model_sizes)
print(f"Updated size for {file_path.name} in {self.model_sizes_path.name}")
# Ensure ModelVersion is set immediately after updating model params
if boot_run:
try:
current = params.get("Model", encoding="utf-8")
if current and current in [model["id"] for model in model_info]:
model_index = [model["id"] for model in model_info].index(current)
version = model_info[model_index]["version"]
params.put("ModelVersion", version)
print(f"Boot sync: Set ModelVersion to {version} for model {current}")
except Exception as e:
print(f"Boot sync failed: {e}")
def update_tinygrad_size(self, file_path):
self.tinygrad_sizes[TAR_FILE_NAME] = file_path.stat().st_size
update_json_file(self.tinygrad_sizes_path, self.tinygrad_sizes)
print(f"Updated size for {TAR_FILE_NAME} in {self.tinygrad_sizes_path.name}")
def update_tinygrad(self):
repo_url = get_repository_url(self.session)
def download_all_models(self):
repo_url = get_repository_url()
if not repo_url:
handle_error(None, "GitHub and GitLab are offline...", "Repository unavailable", None, None)
handle_error(None, "GitHub and GitLab are offline...", "Repository unavailable", MODEL_DOWNLOAD_ALL_PARAM, DOWNLOAD_PROGRESS_PARAM, params_memory)
return
primary_url = f"{repo_url}/Tinygrad/{TAR_FILE_NAME}"
fallback_url = f"https://gitlab.com/{RESOURCES_REPO}/-/raw/Tinygrad/{TAR_FILE_NAME}"
model_info = self.fetch_models(f"{repo_url}/Versions/model_names_{VERSION}.json")
if model_info:
available_models = [model["id"] for model in model_info]
available_model_names = [re.sub(r"[🗺️👀📡]", "", model["name"]).strip() for model in model_info]
model_versions = [model["version"] for model in model_info]
model_series = [model.get("series", "Dom Forgot To Label Me") for model in model_info]
tinygrad_tar_path = Path("/data/tmp/tinygrad.tar.gz")
try:
print(f"Attempting to download tinygrad from {primary_url}...")
download_file(CANCEL_DOWNLOAD_PARAM, tinygrad_tar_path, DOWNLOAD_PROGRESS_PARAM, primary_url, UPDATE_TINYGRAD_PARAM, self.session)
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
delete_file(tinygrad_tar_path)
handle_error(None, "Tinygrad update cancelled...", "Tinygrad update cancelled...", UPDATE_TINYGRAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
params_memory.remove("CancelModelDownload")
return
if not verify_download(tinygrad_tar_path, primary_url, self.session):
print(f"Verification failed for {primary_url}. Retrying from GitLab...")
download_file(CANCEL_DOWNLOAD_PARAM, tinygrad_tar_path, DOWNLOAD_PROGRESS_PARAM, fallback_url, UPDATE_TINYGRAD_PARAM, self.session)
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
delete_file(tinygrad_tar_path)
handle_error(None, "Tinygrad update cancelled...", "Tinygrad update cancelled...", UPDATE_TINYGRAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
params_memory.remove("CancelModelDownload")
return
if not verify_download(tinygrad_tar_path, fallback_url, self.session):
handle_error(tinygrad_tar_path, "Verification Failed", "Tinygrad verification failed", UPDATE_TINYGRAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
return
print("Tinygrad downloaded successfully! Proceeding with installation...")
self.update_tinygrad_size(tinygrad_tar_path)
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Installing...")
print("Deleting old tinygrad directories...")
delete_file(TINYGRAD_MODELD_PATH)
print(f"Removed {TINYGRAD_MODELD_PATH}")
delete_file(TINYGRAD_REPO_PATH)
print(f"Removed {TINYGRAD_REPO_PATH}")
extract_tar(tinygrad_tar_path, Path(BASEDIR))
print("Tinygrad update completed successfully!")
params.put_bool("TinygradUpdateAvailable", False)
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Updated!")
params_memory.remove(UPDATE_TINYGRAD_PARAM)
self.update_tinygrad_models(repo_url)
except Exception as exception:
handle_error(tinygrad_tar_path, "Update Failed", f"An unexpected error occurred: {exception}", UPDATE_TINYGRAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
def update_tinygrad_models(self, repo_url=None):
print("Updating old Tinygrad models...")
installed_tinygrad_models = set()
for filename, _ in TINYGRAD_FILES:
suffix = f"_{filename}"
for file_path in MODELS_PATH.glob(f"*{suffix}"):
model_name = file_path.name.rsplit(suffix, 1)[0]
if model_name in set(self.available_models):
installed_tinygrad_models.add(model_name)
delete_file(file_path)
self.copy_default_model()
update_frogpilot_toggles()
if repo_url is None:
return
current_model = params.get("Model", encoding="utf-8").removesuffix("_default")
models_to_redownload = [current_model]
models_to_redownload += [model for model in sorted(installed_tinygrad_models) if model != current_model]
if DEFAULT_MODEL in models_to_redownload:
models_to_redownload.remove(DEFAULT_MODEL)
if models_to_redownload:
print(f"Redownloading the following models: {', '.join(models_to_redownload)}")
self.fetch_models(f"{repo_url}/Versions/model_names_{VERSION}.json", repo_url, boot_run=True)
for model in models_to_redownload:
for model, model_name, model_version in zip(available_models, available_model_names, model_versions):
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_ALL_PARAM, DOWNLOAD_PROGRESS_PARAM)
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_ALL_PARAM, DOWNLOAD_PROGRESS_PARAM, params_memory)
return
params_memory.put(DOWNLOAD_PROGRESS_PARAM, f"Downloading \"{self.available_model_names[self.available_models.index(model)]}\"...")
self.download_model(model)
if model_version in ("v8", "v9", "v10", "v11", "v12"):
required_files = [
f"{model}_driving_policy_tinygrad.pkl",
f"{model}_driving_vision_tinygrad.pkl",
f"{model}_driving_policy_metadata.pkl",
f"{model}_driving_vision_metadata.pkl",
]
if model_version == "v12":
required_files += [
f"{model}_driving_off_policy_tinygrad.pkl",
f"{model}_driving_off_policy_metadata.pkl",
]
missing = [f for f in required_files if not (MODELS_PATH / f).is_file()]
if missing:
print(f"Tinygrad model {model} is missing files. Preparing to download...")
params_memory.put(DOWNLOAD_PROGRESS_PARAM, f"Downloading \"{model_name}\"...")
self.download_model(model)
elif model_version == "v7":
# OG tinygrad: only need PKL file
model_file = MODELS_PATH / f"{model}.pkl"
if not model_file.is_file():
print(f"PKL model {model} is missing. Preparing to download...")
params_memory.put(DOWNLOAD_PROGRESS_PARAM, f"Downloading \"{model_name}\"...")
self.download_model(model)
else:
# Classic: only need .thneed
model_file = MODELS_PATH / f"{model}.thneed"
if not model_file.is_file():
print(f"Classic model {model} is missing. Preparing to download...")
params_memory.put(DOWNLOAD_PROGRESS_PARAM, f"Downloading \"{model_name}\"...")
self.download_model(model)
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "All models downloaded!")
else:
print("No previously installed tinygrad models to redownload")
update_frogpilot_toggles()
def validate_models(self):
current = params.get("Model", encoding="utf-8")
default = params_default.get("Model", encoding="utf-8")
if current.endswith("_default") and current != default:
print(f"Model '{current}' does not match default '{default}', resetting...")
params.put("Model", default)
if VERSION_PATH.is_file():
version_name = VERSION_PATH.read_text().strip()
if version_name != VERSION or int(self.tinygrad_sizes.get(TAR_FILE_NAME, 0)) == 0:
self.update_tinygrad_models()
self.tinygrad_sizes[TAR_FILE_NAME] = DEFAULT_TINYGRAD_SIZE
update_json_file(self.tinygrad_sizes_path, self.tinygrad_sizes)
print(f"Updated size for {TAR_FILE_NAME} in {self.tinygrad_sizes_path.name}")
params.remove("TinygradUpdateAvailable")
VERSION_PATH.write_text(VERSION)
print(f"Updated {VERSION_PATH} to {VERSION}")
handle_error(None, "Unable to fetch models...", "Model list unavailable", MODEL_DOWNLOAD_ALL_PARAM, DOWNLOAD_PROGRESS_PARAM, params_memory)
@@ -0,0 +1 @@
{"input_std":[[9.281861],[1.8477924],[0.7977224],[0.047467366],[1.7969038],[1.8168812],[1.8351218],[1.785757],[1.7335743],[1.6658221],[1.5893887],[0.047346078],[0.0473731],[0.047383286],[0.047291175],[0.047300573],[0.04712479],[0.046799928]],"model_test_loss":0.027355343103408813,"input_size":18,"current_date_and_time":"2023-08-05_05-11-44","input_mean":[[21.655252],[-0.07694559],[-0.006081294],[-0.007598456],[-0.07309746],[-0.075890236],[-0.07804942],[-0.076106496],[-0.07184982],[-0.06528668],[-0.060404416],[-0.0077262702],[-0.0077046235],[-0.0076850485],[-0.007687444],[-0.007708868],[-0.0077959923],[-0.008017422]],"input_vars":["v_ego","lateral_accel","lateral_jerk","roll","lateral_accel_m03","lateral_accel_m02","lateral_accel_m01","lateral_accel_p03","lateral_accel_p06","lateral_accel_p10","lateral_accel_p15","roll_m03","roll_m02","roll_m01","roll_p03","roll_p06","roll_p10","roll_p15"],"output_size":1,"layers":[{"dense_1_b":[[-0.35776415],[-0.126288],[0.007988413],[0.03850029],[-0.056796823],[0.0072075897],[0.17376427]],"dense_1_W":[[0.010538558,6.7293744,-0.007391298,-1.035045,-0.47446147,1.0940393,0.08225446,-0.5795131,0.573115,1.82923,-0.16999252,0.8818023,0.03560901,-0.9470653,-1.042151,1.1532398,1.9009485,-1.2265956],[0.009010456,-1.3618345,0.039026346,0.22943486,-0.6669799,-0.768271,-0.16828564,-0.13406865,0.19760236,-0.18953702,-0.019137766,0.1468623,0.26367718,0.4732375,0.73679143,0.62450147,0.30198866,-0.93340015],[-0.044318866,2.2138615,9.620889,-0.66480356,-0.06849887,-0.038568456,-0.30580127,-0.088089585,0.31187677,0.5968372,-1.1462088,0.13234706,0.29166338,-0.69978577,-0.01790655,-0.097928315,-0.56950736,0.7560642],[1.3758256,0.8700429,-0.24295154,0.20832618,-0.7735097,1.0379355,-0.60753095,-0.5457418,-1.0892137,-0.6708325,0.24982774,0.09251328,0.08596103,-0.58034134,-0.3046114,-0.19754426,0.006461508,0.5989185],[0.06314328,-2.492993,-0.28200585,0.49902886,0.8513198,-0.5704965,1.0960953,-0.08426956,-0.76074624,0.10669376,0.02775254,-0.42194095,-0.34657434,-0.10863868,0.17019278,0.0963825,-0.10169116,0.21243806],[0.007984091,-1.9276509,-0.0013926353,0.04057924,1.4438432,1.3440262,-2.506722,1.700801,0.72410774,0.13471895,-0.18753268,0.7303607,0.018133093,-0.71643597,0.07050904,-0.30161944,0.085108586,0.061202843],[0.9413167,-0.9954305,-0.19510143,-0.4711845,-0.12398665,-0.45175523,1.0616771,0.28550953,-0.55543137,-0.21576759,-0.2364377,-0.012782618,-0.050565187,0.257694,-0.20975965,0.00022543047,-0.08760746,0.4983852]],"activation":"σ"},{"dense_2_W":[[-0.71804935,0.017023304,-0.099854074,-0.3262651,-0.402829,-0.12073083,0.13537839],[-0.44002575,-0.1071093,-0.58886194,-0.17319413,-0.21134079,0.23135148,0.0006880979],[-0.55711204,-0.8693639,-0.6443761,-0.7382846,0.18980889,-0.6082511,-0.63103676],[0.11338112,-0.5327033,0.2856197,0.32239884,-0.72682667,0.5302305,-0.45094746],[-0.35197002,-0.14440043,-0.025249843,-0.48697743,0.3340018,-0.25992322,0.0050561456],[0.34757507,0.15670523,-0.14263277,0.50704384,-0.020171141,0.6408679,-0.3074123],[0.40258837,-0.59363264,0.35948923,-0.060853776,-0.0072541097,0.89743376,-0.49789146],[-0.63476,0.29580453,0.1689314,-0.5061521,0.24901053,-0.23218995,0.57968193],[-0.66173035,0.50861925,-0.5845035,-0.6602214,0.8341883,-0.31437424,0.8046359],[0.038493533,0.15464582,-0.04848341,-0.57820857,-0.25891733,-0.47527292,0.21441916],[-0.15464531,-0.07454202,-0.8215851,-0.12614948,-0.5924209,0.00017916794,0.24154592],[0.6091424,-0.112086505,0.1144111,-0.31035227,-0.9237534,0.041003596,-0.3542808],[0.2989792,-0.23780549,0.116059326,-0.6056522,0.5499526,-0.9001413,0.5200723]],"activation":"σ","dense_2_b":[[-0.2638964],[-0.24607328],[-0.15493082],[-0.0071187904],[-0.23515384],[-0.09098133],[-0.034066215],[0.03609036],[-0.03842933],[-0.042302527],[-0.2439947],[-0.16342077],[-0.01030317]]},{"dense_3_W":[[0.45771673,-0.2084603,0.3570187,0.35835707,0.13684571,-0.582958,0.29889587,0.43543494,0.11841301,-0.26185623,-0.4988437,0.5752996,-0.28057483],[-0.19594137,0.050280698,-0.29057446,-0.2829161,-0.16987291,-0.21278952,-0.59946907,0.21295123,0.7040468,0.53549695,-0.52553934,-0.19560973,0.6233473],[-0.19091003,0.08669418,-0.5792192,0.57799137,-0.36263424,0.6037143,0.27898273,-0.30951327,-0.3572644,0.46720102,-0.5403428,0.32415462,-0.60570025]],"activation":"identity","dense_3_b":[[-0.0047377176],[0.023170695],[-0.021546118]]},{"dense_4_W":[[-0.12453941,-1.006034,0.96776205]],"dense_4_b":[[-0.02351544]],"activation":"identity"}]}
@@ -0,0 +1 @@
{"input_std":[[9.342214],[1.5915664],[0.60113484],[0.048193663],[1.5680411],[1.57577],[1.5836853],[1.5711677],[1.5445132],[1.5007596],[1.4529978],[0.047915205],[0.04800539],[0.04808892],[0.04822398],[0.04817677],[0.047881734],[0.047405947]],"model_test_loss":0.019342588260769844,"input_size":18,"current_date_and_time":"2023-08-05_06-09-11","input_mean":[[22.757933],[-0.016342578],[-0.001405228],[-0.014619173],[-0.018091483],[-0.018382493],[-0.019270267],[-0.018759886],[-0.019559544],[-0.017848592],[-0.020014366],[-0.014564899],[-0.01457757],[-0.014600966],[-0.014757987],[-0.014915743],[-0.015121007],[-0.015359475]],"input_vars":["v_ego","lateral_accel","lateral_jerk","roll","lateral_accel_m03","lateral_accel_m02","lateral_accel_m01","lateral_accel_p03","lateral_accel_p06","lateral_accel_p10","lateral_accel_p15","roll_m03","roll_m02","roll_m01","roll_p03","roll_p06","roll_p10","roll_p15"],"output_size":1,"layers":[{"dense_1_b":[[-0.045577038],[-2.8160777],[-0.21418032],[2.9552083],[-0.06852827],[0.029141279],[-0.053249933]],"dense_1_W":[[0.0010664682,1.3813498,-8.153277,-0.009101015,-0.1484779,1.0008405,-1.5930991,-1.195274,0.6136762,0.3560462,-0.30077773,0.93531257,0.14550218,-0.40074933,-0.21716364,0.13834935,-0.0658936,-0.49427196],[-0.7338253,-0.037501235,-0.49922377,-1.0526338,-0.46141604,-1.3649396,0.7309814,-0.91667104,0.044683233,-0.18628967,-0.20878822,-0.2678598,0.48446324,0.51164204,0.09547851,0.41491362,-0.4524314,0.16594785],[-0.0032175342,3.5451593,-0.11421584,-0.2511033,0.27893174,0.6384996,0.80790603,1.3835542,1.8000495,2.249461,1.4026878,0.7851167,-0.35052946,0.041820426,-0.39790183,0.45703772,-0.30459648,0.18287674],[0.72289664,-0.77138704,-0.5025135,-0.49323097,-0.39893976,-0.8779149,0.6848096,-0.58729875,-0.16643623,-0.14427555,-0.15013328,-0.11467142,-0.27280045,0.48508415,0.52080667,-0.0029569597,-0.28170735,0.06367212],[0.00014026981,-2.0353239,0.0060915067,0.4794941,1.1882906,-1.512865,0.80642134,-0.26044324,-0.56213886,-0.365122,0.55078083,-0.34897435,-0.32031298,0.46106994,0.37792024,-0.14116105,0.15597256,-0.32811671],[0.0011494387,1.9060265,8.3941965,-0.3078165,-0.9530427,0.6589645,-1.2487054,-0.541133,0.13451256,0.2805466,-0.33187166,0.9900705,0.2821258,-0.51270896,-0.5130031,-0.28222615,0.042734932,0.27233443],[-0.0014444552,-1.0933362,0.003359957,-0.15052961,0.37165833,1.7087307,-1.4971043,0.673802,-0.108465396,-0.088505454,0.21561684,0.6252258,-0.15019403,-0.34727478,0.08958758,-0.1723621,-0.25557828,0.29778987]],"activation":"σ"},{"dense_2_W":[[0.20810607,-0.13481373,-0.15003344,-0.6067625,-0.31937903,-0.37380016,0.4666911],[0.71834767,0.15669753,-0.11389502,0.27131546,-0.060118295,0.039282244,0.52961355],[-0.27200606,0.43380752,-0.106262706,-0.49363258,0.35659012,-0.01706296,-1.0807354],[-0.79583454,-0.66428274,-0.42157334,-0.52979547,-0.2803004,-0.57637554,0.084149174],[0.1230394,-0.57023287,0.28639448,-0.506744,-0.9920808,0.7510901,0.8584937],[-0.41256845,0.35423175,0.13009231,0.5198333,0.7576573,-0.7053181,-0.3390053],[-0.15090485,0.33375353,-0.64164793,0.57386595,0.25700107,-0.15245672,-0.49020413],[-0.50120676,0.5210849,-0.3282951,0.4276323,1.0257447,-1.0398436,-0.8911315],[0.11402861,-0.41491523,0.247507,-0.39776865,-0.6089287,0.20646147,0.6692102],[0.09411492,0.31819808,0.41330767,0.024243973,0.2314893,0.08529045,0.14911193],[0.5179265,0.20473345,-0.36762783,-0.009045922,-0.19436747,-0.548099,-0.08656279],[-0.69121784,-0.14201449,0.3887621,0.10787631,1.0371574,-0.64747447,-0.73437464],[-0.07860244,-0.597764,0.019551078,0.010199122,-0.7493642,0.66619915,0.121666186]],"activation":"σ","dense_2_b":[[-0.04259814],[-0.053309314],[-0.28045595],[-0.23801634],[0.00944943],[-0.109921046],[-0.037457183],[0.24627604],[-0.0069056284],[-0.04725389],[-0.03361769],[0.044101644],[-0.0009850285]]},{"dense_3_W":[[-0.19631335,-0.47722518,-0.019853225,-0.29363275,-0.625923,0.35145283,0.62401354,0.14337404,-0.25707108,-0.51310915,0.2565006,0.61742216,-0.07952821],[0.5868974,-0.36786363,-0.0177682,-0.45121866,0.68608487,-0.55178356,-0.35236016,-0.7226455,-0.015394288,-0.10650332,0.5391039,0.17791761,0.34196886],[0.16821232,-0.35580373,0.26906335,0.41736925,-0.6979869,0.41801625,0.34707317,0.7784011,-0.10002936,0.32485273,-0.3644285,0.49092358,-0.43872768]],"activation":"identity","dense_3_b":[[-0.019481273],[0.031026587],[-0.03595322]]},{"dense_4_W":[[-0.72340566,0.21244061,-0.67248434]],"dense_4_b":[[0.026229527]],"activation":"identity"}]}
@@ -0,0 +1 @@
{"input_std":[[9.342214],[1.5915664],[0.60113484],[0.048193663],[1.5680411],[1.57577],[1.5836853],[1.5711677],[1.5445132],[1.5007596],[1.4529978],[0.047915205],[0.04800539],[0.04808892],[0.04822398],[0.04817677],[0.047881734],[0.047405947]],"model_test_loss":0.019342588260769844,"input_size":18,"current_date_and_time":"2023-08-05_06-09-11","input_mean":[[22.757933],[-0.016342578],[-0.001405228],[-0.014619173],[-0.018091483],[-0.018382493],[-0.019270267],[-0.018759886],[-0.019559544],[-0.017848592],[-0.020014366],[-0.014564899],[-0.01457757],[-0.014600966],[-0.014757987],[-0.014915743],[-0.015121007],[-0.015359475]],"input_vars":["v_ego","lateral_accel","lateral_jerk","roll","lateral_accel_m03","lateral_accel_m02","lateral_accel_m01","lateral_accel_p03","lateral_accel_p06","lateral_accel_p10","lateral_accel_p15","roll_m03","roll_m02","roll_m01","roll_p03","roll_p06","roll_p10","roll_p15"],"output_size":1,"layers":[{"dense_1_b":[[-0.045577038],[-2.8160777],[-0.21418032],[2.9552083],[-0.06852827],[0.029141279],[-0.053249933]],"dense_1_W":[[0.0010664682,1.3813498,-8.153277,-0.009101015,-0.1484779,1.0008405,-1.5930991,-1.195274,0.6136762,0.3560462,-0.30077773,0.93531257,0.14550218,-0.40074933,-0.21716364,0.13834935,-0.0658936,-0.49427196],[-0.7338253,-0.037501235,-0.49922377,-1.0526338,-0.46141604,-1.3649396,0.7309814,-0.91667104,0.044683233,-0.18628967,-0.20878822,-0.2678598,0.48446324,0.51164204,0.09547851,0.41491362,-0.4524314,0.16594785],[-0.0032175342,3.5451593,-0.11421584,-0.2511033,0.27893174,0.6384996,0.80790603,1.3835542,1.8000495,2.249461,1.4026878,0.7851167,-0.35052946,0.041820426,-0.39790183,0.45703772,-0.30459648,0.18287674],[0.72289664,-0.77138704,-0.5025135,-0.49323097,-0.39893976,-0.8779149,0.6848096,-0.58729875,-0.16643623,-0.14427555,-0.15013328,-0.11467142,-0.27280045,0.48508415,0.52080667,-0.0029569597,-0.28170735,0.06367212],[0.00014026981,-2.0353239,0.0060915067,0.4794941,1.1882906,-1.512865,0.80642134,-0.26044324,-0.56213886,-0.365122,0.55078083,-0.34897435,-0.32031298,0.46106994,0.37792024,-0.14116105,0.15597256,-0.32811671],[0.0011494387,1.9060265,8.3941965,-0.3078165,-0.9530427,0.6589645,-1.2487054,-0.541133,0.13451256,0.2805466,-0.33187166,0.9900705,0.2821258,-0.51270896,-0.5130031,-0.28222615,0.042734932,0.27233443],[-0.0014444552,-1.0933362,0.003359957,-0.15052961,0.37165833,1.7087307,-1.4971043,0.673802,-0.108465396,-0.088505454,0.21561684,0.6252258,-0.15019403,-0.34727478,0.08958758,-0.1723621,-0.25557828,0.29778987]],"activation":"σ"},{"dense_2_W":[[0.20810607,-0.13481373,-0.15003344,-0.6067625,-0.31937903,-0.37380016,0.4666911],[0.71834767,0.15669753,-0.11389502,0.27131546,-0.060118295,0.039282244,0.52961355],[-0.27200606,0.43380752,-0.106262706,-0.49363258,0.35659012,-0.01706296,-1.0807354],[-0.79583454,-0.66428274,-0.42157334,-0.52979547,-0.2803004,-0.57637554,0.084149174],[0.1230394,-0.57023287,0.28639448,-0.506744,-0.9920808,0.7510901,0.8584937],[-0.41256845,0.35423175,0.13009231,0.5198333,0.7576573,-0.7053181,-0.3390053],[-0.15090485,0.33375353,-0.64164793,0.57386595,0.25700107,-0.15245672,-0.49020413],[-0.50120676,0.5210849,-0.3282951,0.4276323,1.0257447,-1.0398436,-0.8911315],[0.11402861,-0.41491523,0.247507,-0.39776865,-0.6089287,0.20646147,0.6692102],[0.09411492,0.31819808,0.41330767,0.024243973,0.2314893,0.08529045,0.14911193],[0.5179265,0.20473345,-0.36762783,-0.009045922,-0.19436747,-0.548099,-0.08656279],[-0.69121784,-0.14201449,0.3887621,0.10787631,1.0371574,-0.64747447,-0.73437464],[-0.07860244,-0.597764,0.019551078,0.010199122,-0.7493642,0.66619915,0.121666186]],"activation":"σ","dense_2_b":[[-0.04259814],[-0.053309314],[-0.28045595],[-0.23801634],[0.00944943],[-0.109921046],[-0.037457183],[0.24627604],[-0.0069056284],[-0.04725389],[-0.03361769],[0.044101644],[-0.0009850285]]},{"dense_3_W":[[-0.19631335,-0.47722518,-0.019853225,-0.29363275,-0.625923,0.35145283,0.62401354,0.14337404,-0.25707108,-0.51310915,0.2565006,0.61742216,-0.07952821],[0.5868974,-0.36786363,-0.0177682,-0.45121866,0.68608487,-0.55178356,-0.35236016,-0.7226455,-0.015394288,-0.10650332,0.5391039,0.17791761,0.34196886],[0.16821232,-0.35580373,0.26906335,0.41736925,-0.6979869,0.41801625,0.34707317,0.7784011,-0.10002936,0.32485273,-0.3644285,0.49092358,-0.43872768]],"activation":"identity","dense_3_b":[[-0.019481273],[0.031026587],[-0.03595322]]},{"dense_4_W":[[-0.72340566,0.21244061,-0.67248434]],"dense_4_b":[[0.026229527]],"activation":"identity"}]}
@@ -0,0 +1 @@
{"input_std":[[9.342214],[1.5915664],[0.60113484],[0.048193663],[1.5680411],[1.57577],[1.5836853],[1.5711677],[1.5445132],[1.5007596],[1.4529978],[0.047915205],[0.04800539],[0.04808892],[0.04822398],[0.04817677],[0.047881734],[0.047405947]],"model_test_loss":0.019342588260769844,"input_size":18,"current_date_and_time":"2023-08-05_06-09-11","input_mean":[[22.757933],[-0.016342578],[-0.001405228],[-0.014619173],[-0.018091483],[-0.018382493],[-0.019270267],[-0.018759886],[-0.019559544],[-0.017848592],[-0.020014366],[-0.014564899],[-0.01457757],[-0.014600966],[-0.014757987],[-0.014915743],[-0.015121007],[-0.015359475]],"input_vars":["v_ego","lateral_accel","lateral_jerk","roll","lateral_accel_m03","lateral_accel_m02","lateral_accel_m01","lateral_accel_p03","lateral_accel_p06","lateral_accel_p10","lateral_accel_p15","roll_m03","roll_m02","roll_m01","roll_p03","roll_p06","roll_p10","roll_p15"],"output_size":1,"layers":[{"dense_1_b":[[-0.045577038],[-2.8160777],[-0.21418032],[2.9552083],[-0.06852827],[0.029141279],[-0.053249933]],"dense_1_W":[[0.0010664682,1.3813498,-8.153277,-0.009101015,-0.1484779,1.0008405,-1.5930991,-1.195274,0.6136762,0.3560462,-0.30077773,0.93531257,0.14550218,-0.40074933,-0.21716364,0.13834935,-0.0658936,-0.49427196],[-0.7338253,-0.037501235,-0.49922377,-1.0526338,-0.46141604,-1.3649396,0.7309814,-0.91667104,0.044683233,-0.18628967,-0.20878822,-0.2678598,0.48446324,0.51164204,0.09547851,0.41491362,-0.4524314,0.16594785],[-0.0032175342,3.5451593,-0.11421584,-0.2511033,0.27893174,0.6384996,0.80790603,1.3835542,1.8000495,2.249461,1.4026878,0.7851167,-0.35052946,0.041820426,-0.39790183,0.45703772,-0.30459648,0.18287674],[0.72289664,-0.77138704,-0.5025135,-0.49323097,-0.39893976,-0.8779149,0.6848096,-0.58729875,-0.16643623,-0.14427555,-0.15013328,-0.11467142,-0.27280045,0.48508415,0.52080667,-0.0029569597,-0.28170735,0.06367212],[0.00014026981,-2.0353239,0.0060915067,0.4794941,1.1882906,-1.512865,0.80642134,-0.26044324,-0.56213886,-0.365122,0.55078083,-0.34897435,-0.32031298,0.46106994,0.37792024,-0.14116105,0.15597256,-0.32811671],[0.0011494387,1.9060265,8.3941965,-0.3078165,-0.9530427,0.6589645,-1.2487054,-0.541133,0.13451256,0.2805466,-0.33187166,0.9900705,0.2821258,-0.51270896,-0.5130031,-0.28222615,0.042734932,0.27233443],[-0.0014444552,-1.0933362,0.003359957,-0.15052961,0.37165833,1.7087307,-1.4971043,0.673802,-0.108465396,-0.088505454,0.21561684,0.6252258,-0.15019403,-0.34727478,0.08958758,-0.1723621,-0.25557828,0.29778987]],"activation":"σ"},{"dense_2_W":[[0.20810607,-0.13481373,-0.15003344,-0.6067625,-0.31937903,-0.37380016,0.4666911],[0.71834767,0.15669753,-0.11389502,0.27131546,-0.060118295,0.039282244,0.52961355],[-0.27200606,0.43380752,-0.106262706,-0.49363258,0.35659012,-0.01706296,-1.0807354],[-0.79583454,-0.66428274,-0.42157334,-0.52979547,-0.2803004,-0.57637554,0.084149174],[0.1230394,-0.57023287,0.28639448,-0.506744,-0.9920808,0.7510901,0.8584937],[-0.41256845,0.35423175,0.13009231,0.5198333,0.7576573,-0.7053181,-0.3390053],[-0.15090485,0.33375353,-0.64164793,0.57386595,0.25700107,-0.15245672,-0.49020413],[-0.50120676,0.5210849,-0.3282951,0.4276323,1.0257447,-1.0398436,-0.8911315],[0.11402861,-0.41491523,0.247507,-0.39776865,-0.6089287,0.20646147,0.6692102],[0.09411492,0.31819808,0.41330767,0.024243973,0.2314893,0.08529045,0.14911193],[0.5179265,0.20473345,-0.36762783,-0.009045922,-0.19436747,-0.548099,-0.08656279],[-0.69121784,-0.14201449,0.3887621,0.10787631,1.0371574,-0.64747447,-0.73437464],[-0.07860244,-0.597764,0.019551078,0.010199122,-0.7493642,0.66619915,0.121666186]],"activation":"σ","dense_2_b":[[-0.04259814],[-0.053309314],[-0.28045595],[-0.23801634],[0.00944943],[-0.109921046],[-0.037457183],[0.24627604],[-0.0069056284],[-0.04725389],[-0.03361769],[0.044101644],[-0.0009850285]]},{"dense_3_W":[[-0.19631335,-0.47722518,-0.019853225,-0.29363275,-0.625923,0.35145283,0.62401354,0.14337404,-0.25707108,-0.51310915,0.2565006,0.61742216,-0.07952821],[0.5868974,-0.36786363,-0.0177682,-0.45121866,0.68608487,-0.55178356,-0.35236016,-0.7226455,-0.015394288,-0.10650332,0.5391039,0.17791761,0.34196886],[0.16821232,-0.35580373,0.26906335,0.41736925,-0.6979869,0.41801625,0.34707317,0.7784011,-0.10002936,0.32485273,-0.3644285,0.49092358,-0.43872768]],"activation":"identity","dense_3_b":[[-0.019481273],[0.031026587],[-0.03595322]]},{"dense_4_W":[[-0.72340566,0.21244061,-0.67248434]],"dense_4_b":[[0.026229527]],"activation":"identity"}]}
@@ -0,0 +1 @@
{"input_std":[[9.342214],[1.5915664],[0.60113484],[0.048193663],[1.5680411],[1.57577],[1.5836853],[1.5711677],[1.5445132],[1.5007596],[1.4529978],[0.047915205],[0.04800539],[0.04808892],[0.04822398],[0.04817677],[0.047881734],[0.047405947]],"model_test_loss":0.019342588260769844,"input_size":18,"current_date_and_time":"2023-08-05_06-09-11","input_mean":[[22.757933],[-0.016342578],[-0.001405228],[-0.014619173],[-0.018091483],[-0.018382493],[-0.019270267],[-0.018759886],[-0.019559544],[-0.017848592],[-0.020014366],[-0.014564899],[-0.01457757],[-0.014600966],[-0.014757987],[-0.014915743],[-0.015121007],[-0.015359475]],"input_vars":["v_ego","lateral_accel","lateral_jerk","roll","lateral_accel_m03","lateral_accel_m02","lateral_accel_m01","lateral_accel_p03","lateral_accel_p06","lateral_accel_p10","lateral_accel_p15","roll_m03","roll_m02","roll_m01","roll_p03","roll_p06","roll_p10","roll_p15"],"output_size":1,"layers":[{"dense_1_b":[[-0.045577038],[-2.8160777],[-0.21418032],[2.9552083],[-0.06852827],[0.029141279],[-0.053249933]],"dense_1_W":[[0.0010664682,1.3813498,-8.153277,-0.009101015,-0.1484779,1.0008405,-1.5930991,-1.195274,0.6136762,0.3560462,-0.30077773,0.93531257,0.14550218,-0.40074933,-0.21716364,0.13834935,-0.0658936,-0.49427196],[-0.7338253,-0.037501235,-0.49922377,-1.0526338,-0.46141604,-1.3649396,0.7309814,-0.91667104,0.044683233,-0.18628967,-0.20878822,-0.2678598,0.48446324,0.51164204,0.09547851,0.41491362,-0.4524314,0.16594785],[-0.0032175342,3.5451593,-0.11421584,-0.2511033,0.27893174,0.6384996,0.80790603,1.3835542,1.8000495,2.249461,1.4026878,0.7851167,-0.35052946,0.041820426,-0.39790183,0.45703772,-0.30459648,0.18287674],[0.72289664,-0.77138704,-0.5025135,-0.49323097,-0.39893976,-0.8779149,0.6848096,-0.58729875,-0.16643623,-0.14427555,-0.15013328,-0.11467142,-0.27280045,0.48508415,0.52080667,-0.0029569597,-0.28170735,0.06367212],[0.00014026981,-2.0353239,0.0060915067,0.4794941,1.1882906,-1.512865,0.80642134,-0.26044324,-0.56213886,-0.365122,0.55078083,-0.34897435,-0.32031298,0.46106994,0.37792024,-0.14116105,0.15597256,-0.32811671],[0.0011494387,1.9060265,8.3941965,-0.3078165,-0.9530427,0.6589645,-1.2487054,-0.541133,0.13451256,0.2805466,-0.33187166,0.9900705,0.2821258,-0.51270896,-0.5130031,-0.28222615,0.042734932,0.27233443],[-0.0014444552,-1.0933362,0.003359957,-0.15052961,0.37165833,1.7087307,-1.4971043,0.673802,-0.108465396,-0.088505454,0.21561684,0.6252258,-0.15019403,-0.34727478,0.08958758,-0.1723621,-0.25557828,0.29778987]],"activation":"σ"},{"dense_2_W":[[0.20810607,-0.13481373,-0.15003344,-0.6067625,-0.31937903,-0.37380016,0.4666911],[0.71834767,0.15669753,-0.11389502,0.27131546,-0.060118295,0.039282244,0.52961355],[-0.27200606,0.43380752,-0.106262706,-0.49363258,0.35659012,-0.01706296,-1.0807354],[-0.79583454,-0.66428274,-0.42157334,-0.52979547,-0.2803004,-0.57637554,0.084149174],[0.1230394,-0.57023287,0.28639448,-0.506744,-0.9920808,0.7510901,0.8584937],[-0.41256845,0.35423175,0.13009231,0.5198333,0.7576573,-0.7053181,-0.3390053],[-0.15090485,0.33375353,-0.64164793,0.57386595,0.25700107,-0.15245672,-0.49020413],[-0.50120676,0.5210849,-0.3282951,0.4276323,1.0257447,-1.0398436,-0.8911315],[0.11402861,-0.41491523,0.247507,-0.39776865,-0.6089287,0.20646147,0.6692102],[0.09411492,0.31819808,0.41330767,0.024243973,0.2314893,0.08529045,0.14911193],[0.5179265,0.20473345,-0.36762783,-0.009045922,-0.19436747,-0.548099,-0.08656279],[-0.69121784,-0.14201449,0.3887621,0.10787631,1.0371574,-0.64747447,-0.73437464],[-0.07860244,-0.597764,0.019551078,0.010199122,-0.7493642,0.66619915,0.121666186]],"activation":"σ","dense_2_b":[[-0.04259814],[-0.053309314],[-0.28045595],[-0.23801634],[0.00944943],[-0.109921046],[-0.037457183],[0.24627604],[-0.0069056284],[-0.04725389],[-0.03361769],[0.044101644],[-0.0009850285]]},{"dense_3_W":[[-0.19631335,-0.47722518,-0.019853225,-0.29363275,-0.625923,0.35145283,0.62401354,0.14337404,-0.25707108,-0.51310915,0.2565006,0.61742216,-0.07952821],[0.5868974,-0.36786363,-0.0177682,-0.45121866,0.68608487,-0.55178356,-0.35236016,-0.7226455,-0.015394288,-0.10650332,0.5391039,0.17791761,0.34196886],[0.16821232,-0.35580373,0.26906335,0.41736925,-0.6979869,0.41801625,0.34707317,0.7784011,-0.10002936,0.32485273,-0.3644285,0.49092358,-0.43872768]],"activation":"identity","dense_3_b":[[-0.019481273],[0.031026587],[-0.03595322]]},{"dense_4_W":[[-0.72340566,0.21244061,-0.67248434]],"dense_4_b":[[0.026229527]],"activation":"identity"}]}
@@ -0,0 +1 @@
{"input_std":[[9.342214],[1.5915664],[0.60113484],[0.048193663],[1.5680411],[1.57577],[1.5836853],[1.5711677],[1.5445132],[1.5007596],[1.4529978],[0.047915205],[0.04800539],[0.04808892],[0.04822398],[0.04817677],[0.047881734],[0.047405947]],"model_test_loss":0.019342588260769844,"input_size":18,"current_date_and_time":"2023-08-05_06-09-11","input_mean":[[22.757933],[-0.016342578],[-0.001405228],[-0.014619173],[-0.018091483],[-0.018382493],[-0.019270267],[-0.018759886],[-0.019559544],[-0.017848592],[-0.020014366],[-0.014564899],[-0.01457757],[-0.014600966],[-0.014757987],[-0.014915743],[-0.015121007],[-0.015359475]],"input_vars":["v_ego","lateral_accel","lateral_jerk","roll","lateral_accel_m03","lateral_accel_m02","lateral_accel_m01","lateral_accel_p03","lateral_accel_p06","lateral_accel_p10","lateral_accel_p15","roll_m03","roll_m02","roll_m01","roll_p03","roll_p06","roll_p10","roll_p15"],"output_size":1,"layers":[{"dense_1_b":[[-0.045577038],[-2.8160777],[-0.21418032],[2.9552083],[-0.06852827],[0.029141279],[-0.053249933]],"dense_1_W":[[0.0010664682,1.3813498,-8.153277,-0.009101015,-0.1484779,1.0008405,-1.5930991,-1.195274,0.6136762,0.3560462,-0.30077773,0.93531257,0.14550218,-0.40074933,-0.21716364,0.13834935,-0.0658936,-0.49427196],[-0.7338253,-0.037501235,-0.49922377,-1.0526338,-0.46141604,-1.3649396,0.7309814,-0.91667104,0.044683233,-0.18628967,-0.20878822,-0.2678598,0.48446324,0.51164204,0.09547851,0.41491362,-0.4524314,0.16594785],[-0.0032175342,3.5451593,-0.11421584,-0.2511033,0.27893174,0.6384996,0.80790603,1.3835542,1.8000495,2.249461,1.4026878,0.7851167,-0.35052946,0.041820426,-0.39790183,0.45703772,-0.30459648,0.18287674],[0.72289664,-0.77138704,-0.5025135,-0.49323097,-0.39893976,-0.8779149,0.6848096,-0.58729875,-0.16643623,-0.14427555,-0.15013328,-0.11467142,-0.27280045,0.48508415,0.52080667,-0.0029569597,-0.28170735,0.06367212],[0.00014026981,-2.0353239,0.0060915067,0.4794941,1.1882906,-1.512865,0.80642134,-0.26044324,-0.56213886,-0.365122,0.55078083,-0.34897435,-0.32031298,0.46106994,0.37792024,-0.14116105,0.15597256,-0.32811671],[0.0011494387,1.9060265,8.3941965,-0.3078165,-0.9530427,0.6589645,-1.2487054,-0.541133,0.13451256,0.2805466,-0.33187166,0.9900705,0.2821258,-0.51270896,-0.5130031,-0.28222615,0.042734932,0.27233443],[-0.0014444552,-1.0933362,0.003359957,-0.15052961,0.37165833,1.7087307,-1.4971043,0.673802,-0.108465396,-0.088505454,0.21561684,0.6252258,-0.15019403,-0.34727478,0.08958758,-0.1723621,-0.25557828,0.29778987]],"activation":"σ"},{"dense_2_W":[[0.20810607,-0.13481373,-0.15003344,-0.6067625,-0.31937903,-0.37380016,0.4666911],[0.71834767,0.15669753,-0.11389502,0.27131546,-0.060118295,0.039282244,0.52961355],[-0.27200606,0.43380752,-0.106262706,-0.49363258,0.35659012,-0.01706296,-1.0807354],[-0.79583454,-0.66428274,-0.42157334,-0.52979547,-0.2803004,-0.57637554,0.084149174],[0.1230394,-0.57023287,0.28639448,-0.506744,-0.9920808,0.7510901,0.8584937],[-0.41256845,0.35423175,0.13009231,0.5198333,0.7576573,-0.7053181,-0.3390053],[-0.15090485,0.33375353,-0.64164793,0.57386595,0.25700107,-0.15245672,-0.49020413],[-0.50120676,0.5210849,-0.3282951,0.4276323,1.0257447,-1.0398436,-0.8911315],[0.11402861,-0.41491523,0.247507,-0.39776865,-0.6089287,0.20646147,0.6692102],[0.09411492,0.31819808,0.41330767,0.024243973,0.2314893,0.08529045,0.14911193],[0.5179265,0.20473345,-0.36762783,-0.009045922,-0.19436747,-0.548099,-0.08656279],[-0.69121784,-0.14201449,0.3887621,0.10787631,1.0371574,-0.64747447,-0.73437464],[-0.07860244,-0.597764,0.019551078,0.010199122,-0.7493642,0.66619915,0.121666186]],"activation":"σ","dense_2_b":[[-0.04259814],[-0.053309314],[-0.28045595],[-0.23801634],[0.00944943],[-0.109921046],[-0.037457183],[0.24627604],[-0.0069056284],[-0.04725389],[-0.03361769],[0.044101644],[-0.0009850285]]},{"dense_3_W":[[-0.19631335,-0.47722518,-0.019853225,-0.29363275,-0.625923,0.35145283,0.62401354,0.14337404,-0.25707108,-0.51310915,0.2565006,0.61742216,-0.07952821],[0.5868974,-0.36786363,-0.0177682,-0.45121866,0.68608487,-0.55178356,-0.35236016,-0.7226455,-0.015394288,-0.10650332,0.5391039,0.17791761,0.34196886],[0.16821232,-0.35580373,0.26906335,0.41736925,-0.6979869,0.41801625,0.34707317,0.7784011,-0.10002936,0.32485273,-0.3644285,0.49092358,-0.43872768]],"activation":"identity","dense_3_b":[[-0.019481273],[0.031026587],[-0.03595322]]},{"dense_4_W":[[-0.72340566,0.21244061,-0.67248434]],"dense_4_b":[[0.026229527]],"activation":"identity"}]}
@@ -0,0 +1 @@
{"input_std":[[9.342214],[1.5915664],[0.60113484],[0.048193663],[1.5680411],[1.57577],[1.5836853],[1.5711677],[1.5445132],[1.5007596],[1.4529978],[0.047915205],[0.04800539],[0.04808892],[0.04822398],[0.04817677],[0.047881734],[0.047405947]],"model_test_loss":0.019342588260769844,"input_size":18,"current_date_and_time":"2023-08-05_06-09-11","input_mean":[[22.757933],[-0.016342578],[-0.001405228],[-0.014619173],[-0.018091483],[-0.018382493],[-0.019270267],[-0.018759886],[-0.019559544],[-0.017848592],[-0.020014366],[-0.014564899],[-0.01457757],[-0.014600966],[-0.014757987],[-0.014915743],[-0.015121007],[-0.015359475]],"input_vars":["v_ego","lateral_accel","lateral_jerk","roll","lateral_accel_m03","lateral_accel_m02","lateral_accel_m01","lateral_accel_p03","lateral_accel_p06","lateral_accel_p10","lateral_accel_p15","roll_m03","roll_m02","roll_m01","roll_p03","roll_p06","roll_p10","roll_p15"],"output_size":1,"layers":[{"dense_1_b":[[-0.045577038],[-2.8160777],[-0.21418032],[2.9552083],[-0.06852827],[0.029141279],[-0.053249933]],"dense_1_W":[[0.0010664682,1.3813498,-8.153277,-0.009101015,-0.1484779,1.0008405,-1.5930991,-1.195274,0.6136762,0.3560462,-0.30077773,0.93531257,0.14550218,-0.40074933,-0.21716364,0.13834935,-0.0658936,-0.49427196],[-0.7338253,-0.037501235,-0.49922377,-1.0526338,-0.46141604,-1.3649396,0.7309814,-0.91667104,0.044683233,-0.18628967,-0.20878822,-0.2678598,0.48446324,0.51164204,0.09547851,0.41491362,-0.4524314,0.16594785],[-0.0032175342,3.5451593,-0.11421584,-0.2511033,0.27893174,0.6384996,0.80790603,1.3835542,1.8000495,2.249461,1.4026878,0.7851167,-0.35052946,0.041820426,-0.39790183,0.45703772,-0.30459648,0.18287674],[0.72289664,-0.77138704,-0.5025135,-0.49323097,-0.39893976,-0.8779149,0.6848096,-0.58729875,-0.16643623,-0.14427555,-0.15013328,-0.11467142,-0.27280045,0.48508415,0.52080667,-0.0029569597,-0.28170735,0.06367212],[0.00014026981,-2.0353239,0.0060915067,0.4794941,1.1882906,-1.512865,0.80642134,-0.26044324,-0.56213886,-0.365122,0.55078083,-0.34897435,-0.32031298,0.46106994,0.37792024,-0.14116105,0.15597256,-0.32811671],[0.0011494387,1.9060265,8.3941965,-0.3078165,-0.9530427,0.6589645,-1.2487054,-0.541133,0.13451256,0.2805466,-0.33187166,0.9900705,0.2821258,-0.51270896,-0.5130031,-0.28222615,0.042734932,0.27233443],[-0.0014444552,-1.0933362,0.003359957,-0.15052961,0.37165833,1.7087307,-1.4971043,0.673802,-0.108465396,-0.088505454,0.21561684,0.6252258,-0.15019403,-0.34727478,0.08958758,-0.1723621,-0.25557828,0.29778987]],"activation":"σ"},{"dense_2_W":[[0.20810607,-0.13481373,-0.15003344,-0.6067625,-0.31937903,-0.37380016,0.4666911],[0.71834767,0.15669753,-0.11389502,0.27131546,-0.060118295,0.039282244,0.52961355],[-0.27200606,0.43380752,-0.106262706,-0.49363258,0.35659012,-0.01706296,-1.0807354],[-0.79583454,-0.66428274,-0.42157334,-0.52979547,-0.2803004,-0.57637554,0.084149174],[0.1230394,-0.57023287,0.28639448,-0.506744,-0.9920808,0.7510901,0.8584937],[-0.41256845,0.35423175,0.13009231,0.5198333,0.7576573,-0.7053181,-0.3390053],[-0.15090485,0.33375353,-0.64164793,0.57386595,0.25700107,-0.15245672,-0.49020413],[-0.50120676,0.5210849,-0.3282951,0.4276323,1.0257447,-1.0398436,-0.8911315],[0.11402861,-0.41491523,0.247507,-0.39776865,-0.6089287,0.20646147,0.6692102],[0.09411492,0.31819808,0.41330767,0.024243973,0.2314893,0.08529045,0.14911193],[0.5179265,0.20473345,-0.36762783,-0.009045922,-0.19436747,-0.548099,-0.08656279],[-0.69121784,-0.14201449,0.3887621,0.10787631,1.0371574,-0.64747447,-0.73437464],[-0.07860244,-0.597764,0.019551078,0.010199122,-0.7493642,0.66619915,0.121666186]],"activation":"σ","dense_2_b":[[-0.04259814],[-0.053309314],[-0.28045595],[-0.23801634],[0.00944943],[-0.109921046],[-0.037457183],[0.24627604],[-0.0069056284],[-0.04725389],[-0.03361769],[0.044101644],[-0.0009850285]]},{"dense_3_W":[[-0.19631335,-0.47722518,-0.019853225,-0.29363275,-0.625923,0.35145283,0.62401354,0.14337404,-0.25707108,-0.51310915,0.2565006,0.61742216,-0.07952821],[0.5868974,-0.36786363,-0.0177682,-0.45121866,0.68608487,-0.55178356,-0.35236016,-0.7226455,-0.015394288,-0.10650332,0.5391039,0.17791761,0.34196886],[0.16821232,-0.35580373,0.26906335,0.41736925,-0.6979869,0.41801625,0.34707317,0.7784011,-0.10002936,0.32485273,-0.3644285,0.49092358,-0.43872768]],"activation":"identity","dense_3_b":[[-0.019481273],[0.031026587],[-0.03595322]]},{"dense_4_W":[[-0.72340566,0.21244061,-0.67248434]],"dense_4_b":[[0.026229527]],"activation":"identity"}]}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 912 KiB

After

Width:  |  Height:  |  Size: 503 KiB

+145 -55
View File
@@ -37,6 +37,7 @@ HOLIDAY_SLUGS = {
}
THEME_COMPONENT_PARAMS = {
"boot_logos": "BootLogoToDownload",
"colors": "ColorToDownload",
"distance_icons": "DistanceIconToDownload",
"icons": "IconToDownload",
@@ -93,16 +94,29 @@ class ThemeManager:
steering_wheel_save_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(steering_wheel_image_path, steering_wheel_save_path)
default_boot_logo_path = Path(__file__).parent / "other_images/frogpilot_boot_logo.png"
boot_logo_save_path = THEME_SAVE_PATH / "bootlogos/starpilot.png"
boot_logo_save_path.parent.mkdir(parents=True, exist_ok=True)
if not boot_logo_save_path.exists():
shutil.copy2(default_boot_logo_path, boot_logo_save_path)
def download_theme(self, theme_component, theme_name, asset_param, frogpilot_toggles):
self.downloading_theme = True
allow_unknown_size = theme_component in {"boot_logos", "steering_wheels"}
name_candidates = [theme_name]
repo_url = get_repository_url(self.session)
repo_url = get_repository_url()
if not repo_url:
handle_error(None, "GitHub and GitLab are offline...", "Repository unavailable", asset_param, DOWNLOAD_PROGRESS_PARAM)
handle_error(None, "GitHub and GitLab are offline...", "Repository unavailable", asset_param, DOWNLOAD_PROGRESS_PARAM, params_memory)
self.downloading_theme = False
return
if theme_component == "distance_icons":
if theme_component == "boot_logos":
download_link = f"{repo_url}/Themes/bootlogo"
download_path = THEME_SAVE_PATH / "bootlogos" / theme_name
extensions = [".png", ".jpg", ".jpeg"]
name_candidates = list(dict.fromkeys([theme_name, theme_name.replace("_", "-"), theme_name.replace("-", "_")]))
elif theme_component == "distance_icons":
download_link = f"{repo_url}/Distance-Icons/{theme_name}"
download_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component
extensions = [".zip"]
@@ -117,39 +131,42 @@ class ThemeManager:
for extension in extensions:
theme_path = download_path.with_suffix(extension)
theme_url = download_link + extension
theme_urls = [f"{download_link}/{candidate}{extension}" for candidate in name_candidates]
if theme_component != "boot_logos":
theme_urls = [download_link + extension]
delete_file(theme_path)
print(f"Downloading theme from GitHub: {theme_name}")
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, DOWNLOAD_PROGRESS_PARAM, theme_url, asset_param, self.session)
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
for theme_url in theme_urls:
delete_file(theme_path)
handle_error(None, "Download cancelled...", "Download cancelled...", asset_param, DOWNLOAD_PROGRESS_PARAM)
self.downloading_theme = False
return
print(f"Downloading theme from GitHub: {theme_name}")
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, DOWNLOAD_PROGRESS_PARAM, theme_url, asset_param, params_memory, allow_unknown_size=allow_unknown_size, suppress_errors=allow_unknown_size)
if verify_download(theme_path, theme_url, self.session):
print(f"Theme {theme_name} downloaded and verified successfully from GitHub!")
self.update_theme_size(theme_component, theme_name, theme_path.stat().st_size)
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
delete_file(theme_path)
handle_error(None, "Download cancelled...", "Download cancelled...", asset_param, DOWNLOAD_PROGRESS_PARAM, params_memory)
if extension == ".zip":
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
extract_zip(theme_path, download_path)
self.downloading_theme = False
return
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
params_memory.remove(asset_param)
if verify_download(theme_path, theme_url, allow_unknown_size=allow_unknown_size):
print(f"Theme {theme_name} downloaded and verified successfully from GitHub!")
self.update_theme_size(theme_component, theme_name, theme_path.stat().st_size)
self.downloading_theme = False
if extension == ".zip":
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
extract_zip(theme_path, download_path)
self.update_themes(frogpilot_toggles)
return
elif self.handle_verification_failure(extension, theme_component, theme_name, asset_param, theme_path, download_path, frogpilot_toggles):
return
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
params_memory.remove(asset_param)
handle_error(download_path, "Download failed...", "Download failed...", asset_param, DOWNLOAD_PROGRESS_PARAM)
self.downloading_theme = False
self.update_themes(frogpilot_toggles)
return
elif self.handle_verification_failure(extension, theme_component, theme_name, asset_param, theme_path, download_path, frogpilot_toggles):
return
handle_error(download_path, "Download failed...", "Download failed...", asset_param, DOWNLOAD_PROGRESS_PARAM, params_memory)
self.downloading_theme = False
def fetch_assets(self, repo_url, frogpilot_toggles):
@@ -158,7 +175,7 @@ class ThemeManager:
repo_encoded = quote_plus(RESOURCES_REPO)
assets = {"themes": {}, "wheels": []}
assets = {"boot_logos": [], "themes": {}, "wheels": []}
try:
def list_files(branch):
if is_github:
@@ -234,6 +251,20 @@ class ThemeManager:
theme_name, sub_path = item["path"].split("/", 1)
theme_path = sub_path.lower()
if theme_name.lower() == "bootlogo":
if Path(sub_path).suffix.lower() not in (".png", ".jpg", ".jpeg"):
continue
assets["boot_logos"].append(sub_path)
logo_name = Path(sub_path).stem
local_files = list((THEME_SAVE_PATH / "bootlogos").glob(f"{logo_name}.*"))
if local_files and expected_size > 0:
local_size = self.theme_sizes.get("boot_logos", {}).get(logo_name)
if local_size != expected_size:
print(f"boot logo {logo_name} is outdated, redownloading...")
self.download_theme("boot_logos", logo_name, THEME_COMPONENT_PARAMS["boot_logos"], frogpilot_toggles)
continue
for key in ("colors", "icons", "signals", "sounds"):
if key in theme_path:
assets["themes"].setdefault(theme_name, set()).add(key)
@@ -246,13 +277,14 @@ class ThemeManager:
self.download_theme(key, theme_name, THEME_COMPONENT_PARAMS[key], frogpilot_toggles)
break
assets["boot_logos"].sort()
assets["themes"] = {key: sorted(list(value)) for key, value in assets["themes"].items()}
assets["wheels"].sort()
return assets
except requests.exceptions.RequestException as error:
print(f"Request failed: {error}")
handle_request_error(f"Failed to fetch theme sizes from {'GitHub' if is_github else 'GitLab'}: {error}", None, None, None)
handle_request_error(f"Failed to fetch theme sizes from {'GitHub' if is_github else 'GitLab'}: {error}", None, None, None, None)
return {}
@staticmethod
@@ -265,7 +297,7 @@ class ThemeManager:
parts = base.replace("_", "-").split("-")
capitalized_parts = [part.capitalize() for part in parts if part]
if len(capitalized_parts) > 1 and component != "steering_wheels":
if len(capitalized_parts) > 1 and component not in {"boot_logos", "steering_wheels"}:
display = f"{capitalized_parts[0]} ({' '.join(capitalized_parts[1:])})"
else:
display = " ".join(capitalized_parts)
@@ -321,37 +353,44 @@ class ThemeManager:
}
def handle_verification_failure(self, extension, theme_component, theme_name, asset_param, theme_path, download_path, frogpilot_toggles):
if theme_component == "distance_icons":
allow_unknown_size = theme_component in {"boot_logos", "steering_wheels"}
if theme_component == "boot_logos":
download_link = f"{GITLAB_URL}/Themes/bootlogo"
name_candidates = list(dict.fromkeys([theme_name, theme_name.replace("_", "-"), theme_name.replace("-", "_")]))
elif theme_component == "distance_icons":
download_link = f"{GITLAB_URL}/Distance-Icons/{theme_name}"
name_candidates = [theme_name]
elif theme_component == "steering_wheels":
download_link = f"{GITLAB_URL}/Steering-Wheels/{theme_name}"
name_candidates = [theme_name]
else:
download_link = f"{GITLAB_URL}/Themes/{theme_name}/{theme_component}"
name_candidates = [theme_name]
delete_file(theme_path)
for candidate in name_candidates:
delete_file(theme_path)
theme_url = download_link + extension
print(f"Downloading theme from GitLab: {theme_name}")
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, DOWNLOAD_PROGRESS_PARAM, theme_url, asset_param, self.session)
theme_url = f"{download_link}/{candidate}{extension}" if theme_component == "boot_logos" else download_link + extension
print(f"Downloading theme from GitLab: {theme_name}")
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, DOWNLOAD_PROGRESS_PARAM, theme_url, asset_param, params_memory, allow_unknown_size=allow_unknown_size, suppress_errors=allow_unknown_size)
if verify_download(theme_path, theme_url, self.session):
print(f"Theme {theme_name} downloaded and verified successfully from GitLab!")
self.update_theme_size(theme_component, theme_name, theme_path.stat().st_size)
if verify_download(theme_path, theme_url, allow_unknown_size=allow_unknown_size):
print(f"Theme {theme_name} downloaded and verified successfully from GitLab!")
self.update_theme_size(theme_component, theme_name, theme_path.stat().st_size)
if extension == ".zip":
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
extract_zip(theme_path, download_path)
if extension == ".zip":
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
extract_zip(theme_path, download_path)
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
params_memory.remove(asset_param)
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
params_memory.remove(asset_param)
self.downloading_theme = False
self.downloading_theme = False
self.update_themes(frogpilot_toggles)
return True
self.update_themes(frogpilot_toggles)
return True
handle_error(None, "Download failed...", "Download failed...", asset_param, DOWNLOAD_PROGRESS_PARAM)
self.downloading_theme = False
# Let the caller continue trying alternate extensions before surfacing failure.
return False
@staticmethod
@@ -415,6 +454,8 @@ class ThemeManager:
return random.choice(candidates) if candidates else "stock"
def update_active_theme(self, time_validated, frogpilot_toggles, boot_run=False, randomize_theme=False):
boot_logo = getattr(frogpilot_toggles, "boot_logo", "stock")
if time_validated and frogpilot_toggles.holiday_themes:
self.holiday_theme = self.update_holiday()
else:
@@ -422,6 +463,7 @@ class ThemeManager:
if self.holiday_theme != "stock":
asset_mappings = {
"boot_logo": ("boot_logo", boot_logo),
"color_scheme": ("colors", self.holiday_theme),
"distance_icons": ("distance_icons", self.holiday_theme),
"icon_pack": ("icons", self.holiday_theme),
@@ -434,6 +476,7 @@ class ThemeManager:
selected_theme = self.randomize_theme_asset(available_themes)
asset_mappings = {
"boot_logo": ("boot_logo", boot_logo),
"color_scheme": ("colors", selected_theme.replace("-animated", "")),
"distance_icons": ("distance_icons", self.randomize_distance_icons(available_themes, selected_theme.replace("-animated", ""))),
"icon_pack": ("icons", selected_theme),
@@ -444,6 +487,7 @@ class ThemeManager:
elif not frogpilot_toggles.random_themes:
asset_mappings = {
"boot_logo": ("boot_logo", boot_logo),
"color_scheme": ("colors", frogpilot_toggles.color_scheme),
"distance_icons": ("distance_icons", frogpilot_toggles.distance_icons),
"icon_pack": ("icons", frogpilot_toggles.icon_pack),
@@ -458,7 +502,9 @@ class ThemeManager:
for asset, (asset_type, current_value) in asset_mappings.items():
print(f"Updating {asset}: {asset_type} with value {current_value}")
if asset_type == "wheel_image":
if asset_type == "boot_logo":
self.update_boot_logo(current_value)
elif asset_type == "wheel_image":
self.update_wheel_image(current_value, boot_run=boot_run)
else:
self.update_theme_asset(asset_type, current_value, boot_run=boot_run)
@@ -499,9 +545,16 @@ class ThemeManager:
save_location.symlink_to(asset_location, target_is_directory=True)
print(f"Linked {save_location} to {asset_location}")
def update_theme_params(self, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels):
def update_theme_params(self, downloadable_boot_logos, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels):
def update_param(key, assets, subfolder):
if subfolder == "steering_wheels":
if subfolder == "boot_logos":
themes_path = THEME_SAVE_PATH / "bootlogos"
existing_assets = {item.stem.lower() for item in themes_path.glob("*") if item.is_file()}
pending_assets = [asset for asset in assets if asset.lower() not in existing_assets]
params.put(key, ",".join(sorted(set(pending_assets))))
print(f"{key} updated successfully")
return
elif subfolder == "steering_wheels":
themes_path = THEME_SAVE_PATH / subfolder
existing_assets = {self.format_name(item.name, "steering_wheels") for item in themes_path.glob("*") if item.is_file()}
else:
@@ -511,6 +564,7 @@ class ThemeManager:
params.put(key, ",".join(sorted(set(assets) - existing_assets)))
print(f"{key} updated successfully")
update_param("DownloadableBootLogos", downloadable_boot_logos, "boot_logos")
update_param("DownloadableColors", downloadable_colors, "colors")
update_param("DownloadableDistanceIcons", downloadable_distance_icons, "distance_icons")
update_param("DownloadableIcons", downloadable_icons, "icons")
@@ -529,12 +583,20 @@ class ThemeManager:
theme_name = self.format_name(theme_dir.name, "theme_packs")
downloaded_themes[theme_name] = sorted(components)
(THEME_SAVE_PATH / "bootlogos").mkdir(parents=True, exist_ok=True)
downloaded_boot_logos = []
for boot_logo_file in (THEME_SAVE_PATH / "bootlogos").iterdir():
if boot_logo_file.is_file():
downloaded_boot_logos.append(self.format_name(boot_logo_file.name, "boot_logos"))
downloaded_wheels = []
for wheel_file in (THEME_SAVE_PATH / "steering_wheels").iterdir():
if wheel_file.is_file():
downloaded_wheels.append(self.format_name(wheel_file.name, "steering_wheels"))
params.put("ThemesDownloaded", json.dumps({
"boot_logos": sorted(downloaded_boot_logos),
"themes": {key: downloaded_themes[key] for key in sorted(downloaded_themes)},
"steering_wheels": sorted(downloaded_wheels)
}))
@@ -542,7 +604,9 @@ class ThemeManager:
print("ThemesDownloaded updated successfully")
def update_theme_size(self, theme_component, theme_name, file_size):
if theme_component == "steering_wheels":
if theme_component == "boot_logos":
key = "boot_logos"
elif theme_component == "steering_wheels":
key = "wheels"
else:
key = "themes"
@@ -550,7 +614,7 @@ class ThemeManager:
if key not in self.theme_sizes:
self.theme_sizes[key] = {}
if key == "wheels":
if key in {"boot_logos", "wheels"}:
self.theme_sizes[key][theme_name] = file_size
else:
if theme_name not in self.theme_sizes[key]:
@@ -563,7 +627,7 @@ class ThemeManager:
if self.downloading_theme:
return
repo_url = get_repository_url(self.session)
repo_url = get_repository_url()
if repo_url is None:
print("GitHub and GitLab are offline...")
return
@@ -572,6 +636,7 @@ class ThemeManager:
if not assets:
return
downloadable_boot_logos = []
downloadable_colors = []
downloadable_distance_icons = []
downloadable_icons = []
@@ -593,8 +658,10 @@ class ThemeManager:
if "sounds" in available_assets:
downloadable_sounds.append(theme_name)
downloadable_boot_logos = [Path(boot_logo).stem for boot_logo in assets["boot_logos"]]
downloadable_wheels = [self.format_name(wheel, "steering_wheels") for wheel in assets["wheels"]]
print(f"Downloadable Boot Logos: {downloadable_boot_logos}")
print(f"Downloadable Colors: {downloadable_colors}")
print(f"Downloadable Icons: {downloadable_icons}")
print(f"Downloadable Signals: {downloadable_signals}")
@@ -605,7 +672,21 @@ class ThemeManager:
if boot_run:
self.validate_themes(downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels, frogpilot_toggles)
self.update_theme_params(downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels)
self.update_theme_params(downloadable_boot_logos, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels)
@staticmethod
def update_boot_logo(image):
boot_logo_location = Path(__file__).parent / "other_images/frogpilot_boot_logo.png"
image_name = image.replace(" ", "_").lower()
source_file = next((file for file in (THEME_SAVE_PATH / "bootlogos").glob("*") if file.is_file() and file.stem.lower() == image_name), boot_logo_location)
# Avoid SameFileError when the selected/fallback source is already the active boot logo.
if source_file.resolve() == boot_logo_location.resolve():
print(f"Boot logo unchanged: {boot_logo_location}")
return
shutil.copy2(source_file, boot_logo_location)
print(f"Copied {source_file} to {boot_logo_location}")
def update_wheel_image(self, image, boot_run=False, random_event=False):
wheel_save_location = ACTIVE_THEME_PATH / "steering_wheel"
@@ -641,6 +722,15 @@ class ThemeManager:
def validate_themes(self, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels, frogpilot_toggles):
downloaded_data = json.loads(params.get("ThemesDownloaded") or "{}")
boot_logos_path = THEME_SAVE_PATH / "bootlogos"
for display_name in downloaded_data.get("boot_logos", []):
file_stem = display_name.replace(" ", "_").lower()
matching_files = list(boot_logos_path.glob(f"{file_stem}.*"))
if not matching_files:
print(f"Missing boot logo '{display_name}'. Downloading...")
self.download_theme("boot_logos", file_stem, THEME_COMPONENT_PARAMS["boot_logos"], frogpilot_toggles)
self.update_active_theme(True, frogpilot_toggles)
for display_name, components in downloaded_data.get("themes", {}).items():
raw_name = display_name.lower().replace(" ", "_").replace("(", "").replace(")", "")
theme_folder_name = raw_name.replace("_animated", "-animated")
+1 -1
View File
@@ -150,7 +150,7 @@ def frogpilot_boot_functions(build_metadata, params_cache):
params_cache.clear_all()
FrogPilotVariables().update(holiday_theme="stock", started=False)
ModelManager(boot_run=True)
ModelManager()
ThemeManager(boot_run=True).update_active_theme(time_validated=system_time_valid(), frogpilot_toggles=get_frogpilot_toggles(), boot_run=True)
if VIDEO_CACHE_PATH.exists():
+60 -8
View File
@@ -2,6 +2,7 @@
import json
import math
import numpy as np
import os
import requests
import shutil
import subprocess
@@ -22,7 +23,7 @@ from opendbc.can.parser import CANParser
from openpilot.common.realtime import DT_DMON, DT_HW
from openpilot.selfdrive.car.toyota.carcontroller import LOCK_CMD
from openpilot.system.hardware import HARDWARE
from panda import Panda
from panda import Panda, FW_PATH
from openpilot.frogpilot.common.frogpilot_variables import EARTH_RADIUS, KONIK_PATH, MAPD_PATH, MAPS_PATH, params, params_cache, params_memory
@@ -151,11 +152,21 @@ def extract_zip(zip_file, extract_path):
print(f"Extraction completed: {zip_file} has been removed")
def flash_panda():
remote_start = params.get_bool("RemoteStartBootsComma")
for serial in Panda.list():
try:
panda = Panda(serial)
flash_fn = None
if remote_start:
app_fn = panda.get_mcu_type().config.app_fn
remote_fn = "panda_h7_remote.bin.signed" if app_fn == "panda_h7.bin.signed" else "panda_remote.bin.signed"
candidate = os.path.join(FW_PATH, remote_fn)
if os.path.isfile(candidate):
flash_fn = candidate
else:
print(f"Remote-start panda firmware missing: {candidate}. Falling back to default firmware.")
panda.reset(enter_bootstub=True)
panda.flash()
panda.flash(fn=flash_fn)
panda.close()
except Exception as exception:
print(f"Error flashing Panda {serial}: {exception}")
@@ -233,16 +244,57 @@ def update_json_file(path, data):
with open(path, "w") as file:
json.dump(data, file, indent=2, sort_keys=True)
def parse_maps_selected(raw_maps_selected):
if raw_maps_selected is None:
return {}
raw_maps_selected = raw_maps_selected.strip()
if not raw_maps_selected:
return {}
try:
maps_selected = json.loads(raw_maps_selected)
if isinstance(maps_selected, dict):
nations = sorted({str(item) for item in maps_selected.get("nations", []) if item})
states = sorted({str(item) for item in maps_selected.get("states", []) if item})
return {"nations": nations, "states": states}
return {}
except json.JSONDecodeError:
pass
nations = set()
states = set()
for item in raw_maps_selected.split(","):
item = item.strip()
if item.startswith("nation."):
nation = item.removeprefix("nation.").strip()
if nation:
nations.add(nation)
elif item.startswith("us_state."):
state = item.removeprefix("us_state.").strip()
if state:
states.add(state)
if nations or states:
return {"nations": sorted(nations), "states": sorted(states)}
return {}
def update_maps(now):
while not MAPD_PATH.exists():
time.sleep(60)
maps_selected = json.loads(params.get("MapsSelected", encoding="utf-8") or "{}")
if isinstance(maps_selected, int):
raw_maps_selected = params.get("MapsSelected", encoding="utf-8")
maps_selected = parse_maps_selected(raw_maps_selected)
if raw_maps_selected and not maps_selected:
params.remove("MapsSelected")
params_cache.remove("MapsSelected")
return
if maps_selected:
normalized_maps_selected = json.dumps(maps_selected, separators=(",", ":"))
if raw_maps_selected != normalized_maps_selected:
params.put("MapsSelected", normalized_maps_selected)
params_cache.put("MapsSelected", normalized_maps_selected)
if not (maps_selected.get("nations") or maps_selected.get("states")):
return
@@ -296,12 +348,12 @@ def update_openpilot():
if params.get("UpdaterState", encoding="utf-8") != "idle":
return
while params.get_bool("IsOnroad") or params_memory.get_bool("UpdateSpeedLimits") or running_threads.get("lock_doors", threading.Thread()).is_alive():
time.sleep(60)
if not update_available():
return
while params.get_bool("IsOnroad") or params_memory.get_bool("UpdateSpeedLimits") or running_threads.get("lock_doors", threading.Thread()).is_alive():
time.sleep(60)
while True:
if not update_available():
break
+210 -109
View File
@@ -13,7 +13,8 @@ from openpilot.common.conversions import Conversions as CV
from openpilot.common.params import Params
from openpilot.selfdrive.car import gen_empty_fingerprint
from openpilot.selfdrive.car.car_helpers import interfaces
from openpilot.selfdrive.car.gm.values import GMFlags
from openpilot.selfdrive.car.gm.values import EV_CAR as GM_EV_CAR, GMFlags
from openpilot.selfdrive.car.hyundai.values import EV_CAR as HYUNDAI_EV_CAR
from openpilot.selfdrive.car.interfaces import CarInterfaceBase
from openpilot.selfdrive.car.mock.interface import CarInterface
from openpilot.selfdrive.car.mock.values import CAR as MOCK
@@ -32,6 +33,7 @@ params_memory = Params("/dev/shm/params")
GearShifter = car.CarState.GearShifter
SafetyModel = car.CarParams.SafetyModel
TransmissionType = car.CarParams.TransmissionType
CITY_SPEED_LIMIT = 25 # 55mph is typically the minimum speed for highways
CRUISING_SPEED = 5 # Roughly the speed cars go when not touching the gas while in drive
@@ -40,11 +42,16 @@ EARTH_RADIUS = 6378137 # Radius of the Earth in meters
MAX_T_FOLLOW = 3.0 # Maximum allowed following duration. Larger values risk losing track of the lead but may be increased as models improve
MINIMUM_LATERAL_ACCELERATION = 1.3 # m/s^2, typical minimum lateral acceleration when taking curves
PLANNER_TIME = ModelConstants.T_IDXS[-1] # Length of time the model projects out for
THRESHOLD = 0.63 # Requires the condition to be true for ~1 second
def scale_threshold(v_ego):#0 40 60 80 100 0 40 60 80 100
# More aggressive with hysteresis and lead probability: faster activation at higher speeds
return np.interp(v_ego, [0, 17.9, 26.8, 35.8, 44.7], [0.58, 0.60, 0.62, 0.75, 0.9])
NON_DRIVING_GEARS = [GearShifter.neutral, GearShifter.park, GearShifter.reverse, GearShifter.unknown]
RESOURCES_REPO = "FrogAi/FrogPilot-Resources"
RESOURCES_REPO = "firestar5683/StarPilot-Resources"
ACTIVE_THEME_PATH = Path(__file__).parents[1] / "assets/active_theme"
METADATAS_PATH = Path(__file__).parents[1] / "assets/model_metadata"
@@ -55,6 +62,7 @@ THEME_SAVE_PATH = Path("/data/themes")
ERROR_LOGS_PATH = Path("/data/error_logs")
SCREEN_RECORDINGS_PATH = Path("/data/media/screen_recordings")
VIDEO_CACHE_PATH = Path("/data/video_cache")
MODEL_MIGRATION_FLAG_PATH = Path("/data/params/d/ModelMigratedBd2ToSc")
BACKUP_PATH = Path("/cache/on_backup")
@@ -66,12 +74,11 @@ KONIK_PATH = Path("/cache/use_konik")
MAPD_PATH = Path("/data/media/0/osm/mapd")
MAPS_PATH = Path("/data/media/0/osm/offline")
NNFF_MODELS_PATH = Path(BASEDIR) / "frogpilot/assets/nnff_models"
DEFAULT_MODEL = "firehose"
DEFAULT_MODEL_NAME = "Firehose (Default) 👀📡"
DEFAULT_MODEL_VERSION = "v9"
DEFAULT_MODEL = "sc"
DEFAULT_MODEL_NAME = "South Carolina (Default) 👀📡"
DEFAULT_MODEL_VERSION = "v11"
BUTTON_FUNCTIONS = {
"NOTHING": 0,
@@ -90,12 +97,15 @@ EXCLUDED_KEYS = {
}
TINYGRAD_FILES = [
("driving_off_policy_metadata.pkl", "off-policy metadata"),
("driving_off_policy_tinygrad.pkl", "off-policy model"),
("driving_policy_metadata.pkl", "policy metadata"),
("driving_policy_tinygrad.pkl", "policy model"),
("driving_vision_metadata.pkl", "vision metadata"),
("driving_vision_tinygrad.pkl", "vision model"),
]
@cache
def get_nnff_model_files():
model_dir = Path(NNFF_MODELS_PATH)
@@ -117,13 +127,16 @@ def update_frogpilot_toggles():
frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("AccelerationPath", "1", 2, "0"),
("AccelerationProfile", "2", 0, "0"),
("AdjacentLeadsUI", "1", 3, "0"),
("AdjacentLeadsUI", "0", 3, "0"),
("AdjacentPath", "0", 3, "0"),
("AdjacentPathMetrics", "0", 3, "0"),
("AdvancedCustomUI", "0", 2, "0"),
("AdvancedLateralTune", "0", 3, "0"),
("AdvancedLateralTune", "1", 2, "0"),
("AdvancedLongitudinalTune", "0", 3, "0"),
("EVTuning", "", 3, "0"),
("TruckTuning", "0", 3, "0"),
("AggressiveFollow", "1.25", 2, "1.25"),
("AggressiveFollowHigh", "1.25", 2, "1.25"),
("AggressiveJerkAcceleration", "50", 3, "50"),
("AggressiveJerkDanger", "100", 3, "100"),
("AggressiveJerkDeceleration", "50", 3, "50"),
@@ -133,19 +146,24 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("AlertVolumeControl", "0", 2, "0"),
("AlwaysOnDM", "0", 0, "0"),
("AlwaysOnLateral", "1", 0, "0"),
("AlwaysOnLateralLKAS", "1", 2, "0"),
("AlwaysOnLateralMain", "1", 2, "0"),
("AlwaysOnLateralLKAS", "1", 0, "0"),
("AlwaysOnLateralMain", "1", 0, "0"),
("AlwaysAllowUploads", "0", 2, "0"),
("AMapKey1", "", 0, ""),
("AMapKey2", "", 0, ""),
("AutomaticallyDownloadModels", "1", 1, "0"),
("AutomaticUpdates", "1", 0, "1"),
("AvailableModelNames", "", 1, ""),
("AvailableModelSeries", "", 1, ""),
("AvailableModels", "", 1, ""),
("CommunityFavorites", "", 1, ""),
("UserFavorites", "", 0, ""),
("BigMap", "0", 2, "0"),
("BlacklistedModels", "", 2, ""),
("BlindSpotMetrics", "1", 3, "0"),
("BlindSpotPath", "1", 1, "0"),
("BorderMetrics", "0", 3, "0"),
("BootLogo", "starpilot", 0, "stock"),
("BorderMetrics", "1", 3, "0"),
("CalibratedLateralAcceleration", str(DEFAULT_LATERAL_ACCELERATION), 2, str(DEFAULT_LATERAL_ACCELERATION)),
("CalibrationProgress", "0", 3, "0"),
("CameraView", "3", 2, "0"),
@@ -156,17 +174,17 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("CECurves", "0", 1, "0"),
("CECurvesLead", "0", 1, "0"),
("CELead", "0", 1, "0"),
("CEModelStopTime", str(PLANNER_TIME - 2), 2, "0"),
("CENavigation", "1", 2, "0"),
("CENavigationIntersections", "0", 2, "0"),
("CEModelStopTime", "7", 2, "0"),
("CENavigation", "0", 2, "0"),
("CENavigationIntersections", "1", 2, "0"),
("CENavigationLead", "1", 2, "0"),
("CENavigationTurns", "1", 2, "0"),
("CESignalSpeed", "55", 2, "0"),
("CESignalLaneDetection", "1", 2, "0"),
("CESlowerLead", "0", 1, "0"),
("CESlowerLead", "1", 1, "0"),
("CESpeed", "0", 1, "0"),
("CESpeedLead", "0", 1, "0"),
("CEStoppedLead", "0", 1, "0"),
("CEStoppedLead", "1", 1, "0"),
("ClusterOffset", "1.015", 2, "1.015"),
("Compass", "0", 1, "0"),
("ConditionalExperimental", "1", 1, "0"),
@@ -184,7 +202,7 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("CustomUI", "1", 1, "0"),
("DecelerationProfile", "1", 2, "0"),
("DeveloperMetrics", "1", 3, "0"),
("DeveloperSidebar", "1", 3, "0"),
("DeveloperSidebar", "0", 3, "0"),
("DeveloperSidebarMetric1", "1", 3, "0"),
("DeveloperSidebarMetric2", "2", 3, "0"),
("DeveloperSidebarMetric3", "3", 3, "0"),
@@ -193,7 +211,7 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("DeveloperSidebarMetric6", "6", 3, "0"),
("DeveloperSidebarMetric7", "7", 3, "0"),
("DeveloperWidgets", "1", 3, "0"),
("DeveloperUI", "0", 3, "0"),
("DeveloperUI", "1", 3, "0"),
("DeviceManagement", "1", 1, "0"),
("DeviceShutdown", "9", 1, "33"),
("DisableOnroadUploads", "0", 2, "0"),
@@ -203,17 +221,16 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("DistanceButtonControl", "1", 2, "0"),
("DriverCamera", "0", 1, "0"),
("DynamicPathWidth", "0", 2, "0"),
("DynamicPedalsOnUI", "1", 1, "0"),
("DynamicPedalsOnUI", "1", 2, "0"),
("EngageVolume", "101", 2, "101"),
("ExperimentalGMTune", "0", 2, "0"),
("ExperimentalLongitudinalEnabled", "0", 0, "0"),
("ExperimentalModeConfirmed", "0", 0, "0"),
("Fahrenheit", "0", 3, "0"),
("FavoriteDestinations", "", 0, ""),
("ForceAutoTune", "0", 3, "0"),
("ForceAutoTuneOff", "0", 3, "0"),
("ForceAutoTune", "0", 2, "0"),
("ForceAutoTuneOff", "1", 2, "0"),
("ForceFingerprint", "0", 2, "0"),
("ForceMPHDashboard", "0", 3, "0"),
("ForceMPHDashboard", "0", 2, "0"),
("ForceStops", "0", 2, "0"),
("ForceTorqueController", "0", 3, "0"),
("FPSCounter", "1", 3, "0"),
@@ -222,9 +239,14 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("FrogsGoMoosTweak", "1", 2, "0"),
("FullMap", "0", 2, "0"),
("GasRegenCmd", "1", 2, "0"),
("GMPedalLongitudinal", "1", 2, "1"),
("RemapCancelToDistance", "0", 2, "0"),
("RedPanda", "0", 3, "0"),
("RemoteStartBootsComma", "0", 3, "0"),
("GithubSshKeys", "", 0, ""),
("GithubUsername", "", 0, ""),
("GoatScream", "0", 1, "0"),
("GoatScreamCriticalAlerts", "0", 1, "0"),
("GreenLightAlert", "0", 0, "0"),
("GsmApn", "", 0, ""),
("GsmRoaming", "1", 0, "0"),
@@ -235,10 +257,10 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("HideMaxSpeed", "0", 2, "0"),
("HideSpeed", "0", 2, "0"),
("HideSpeedLimit", "0", 2, "0"),
("HigherBitrate", "0", 2, "0"),
("HigherBitrate", "0", 3, "0"),
("HolidayThemes", "1", 0, "0"),
("HumanAcceleration", "1", 2, "0"),
("HumanFollowing", "1", 2, "0"),
("HumanAcceleration", "0", 2, "0"),
("HumanFollowing", "0", 2, "0"),
("IncreasedStoppedDistance", "0", 1, "0"),
("IncreaseThermalLimits", "0", 2, "0"),
("IsLdwEnabled", "0", 0, "0"),
@@ -246,13 +268,13 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("KonikDongleId", "", 0, ""),
("KonikMinutes", "0", 0, "0"),
("LaneChanges", "1", 0, "1"),
("LaneChangeTime", "1.0", 1, "0"),
("LaneDetectionWidth", "0", 1, "0"),
("LaneChangeTime", "2.0", 0, "0"),
("LaneDetectionWidth", "0", 2, "0"),
("LaneLinesWidth", "4", 2, "2"),
("LateralTune", "1", 1, "0"),
("LateralTune", "1", 2, "0"),
("LeadDepartingAlert", "0", 0, "0"),
("LeadDetectionThreshold", "35", 3, "50"),
("LeadInfo", "1", 3, "0"),
("LeadInfo", "1", 2, "0"),
("LiveDelay", "", 0, ""),
("LKASButtonControl", "5", 2, "0"),
("LockDoors", "1", 0, "0"),
@@ -261,33 +283,36 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("LongitudinalActuatorDelay", "", 3, ""),
("LongitudinalActuatorDelayStock", "", 3, ""),
("LongitudinalTune", "1", 0, "0"),
("TrailerLoad", "0", 2, "0"),
("LongPitch", "1", 2, "0"),
("LoudBlindspotAlert", "0", 0, "0"),
("LowVoltageShutdown", str(VBATT_PAUSE_CHARGING), 3, str(VBATT_PAUSE_CHARGING)),
("LowVoltageShutdown", str(VBATT_PAUSE_CHARGING), 2, str(VBATT_PAUSE_CHARGING)),
("MapAcceleration", "0", 1, "0"),
("MapboxPublicKey", "", 0, ""),
("MapboxSecretKey", "", 0, ""),
("MapDeceleration", "0", 1, "0"),
("MapGears", "0", 2, "0"),
("MapGears", "0", 1, "0"),
("MapsSelected", "", 0, ""),
("MapStyle", "1", 2, "0"),
("MaxDesiredAcceleration", "4.0", 2, "2.0"),
("MinimumLaneChangeSpeed", str(LANE_CHANGE_SPEED_MIN / CV.MPH_TO_MS), 2, str(LANE_CHANGE_SPEED_MIN / CV.MPH_TO_MS)),
("Model", DEFAULT_MODEL + "_default", 1, DEFAULT_MODEL + "_default"),
("Model", DEFAULT_MODEL, 1, DEFAULT_MODEL),
("ModelDrivesAndScores", "", 2, ""),
("ModelRandomizer", "0", 2, "0"),
("ModelReleasedDates", "", 1, ""),
("ModelUI", "1", 2, "0"),
("ModelVersions", "", 2, ""),
("SortModelsByDate", "0", 2, "0"),
("NavigationUI", "1", 1, "0"),
("NavSettingLeftSide", "0", 0, "0"),
("NavSettingTime24h", "0", 0, "0"),
("NewLongAPI", "1", 3, "1"),
("NNFF", "1", 2, "0"),
("NNFFLite", "1", 2, "0"),
("NewLongAPI", "0", 2, "1"),
("NNFF", "0", 2, "0"),
("NNFFLite", "0", 2, "0"),
("NoLogging", "0", 2, "0"),
("NoUploads", "0", 2, "0"),
("NudgelessLaneChange", "1", 0, "0"),
("NumericalTemp", "1", 3, "0"),
("NudgelessLaneChange", "0", 0, "0"),
("NumericalTemp", "1", 2, "0"),
("Offset1", "5", 0, "0"),
("Offset2", "5", 0, "0"),
("Offset3", "5", 0, "0"),
@@ -300,15 +325,15 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("openpilotMinutes", "0", 0, "0"),
("PathEdgeWidth", "20", 2, "0"),
("PathWidth", "6.1", 2, "5.9"),
("PauseAOLOnBrake", "0", 1, "0"),
("PauseLateralOnSignal", "0", 1, "0"),
("PauseLateralSpeed", "0", 1, "0"),
("PedalsOnUI", "0", 1, "0"),
("PauseAOLOnBrake", "0", 2, "0"),
("PauseLateralOnSignal", "0", 2, "0"),
("PauseLateralSpeed", "0", 2, "0"),
("PedalsOnUI", "0", 2, "0"),
("PersonalizeOpenpilot", "1", 0, "0"),
("PreferredSchedule", "2", 0, "0"),
("PromptDistractedVolume", "101", 2, "101"),
("PromptVolume", "101", 2, "101"),
("QOLLateral", "1", 1, "0"),
("QOLLateral", "1", 2, "0"),
("QOLLongitudinal", "1", 1, "0"),
("QOLVisuals", "1", 0, "0"),
("RadarTracksUI", "0", 3, "0"),
@@ -318,19 +343,20 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("RecordFront", "0", 0, "0"),
("RefuseVolume", "101", 2, "101"),
("RelaxedFollow", "1.75", 2, "1.75"),
("RelaxedJerkAcceleration", "100", 3, "100"),
("RelaxedFollowHigh", "1.75", 2, "1.75"),
("RelaxedJerkAcceleration", "50", 3, "50"),
("RelaxedJerkDanger", "100", 3, "100"),
("RelaxedJerkDeceleration", "100", 3, "100"),
("RelaxedJerkSpeed", "100", 3, "100"),
("RelaxedJerkSpeedDecrease", "100", 3, "100"),
("RelaxedJerkDeceleration", "50", 3, "50"),
("RelaxedJerkSpeed", "50", 3, "50"),
("RelaxedJerkSpeedDecrease", "50", 3, "50"),
("RelaxedPersonalityProfile", "1", 2, "0"),
("ReverseCruise", "0", 1, "0"),
("ReverseCruise", "1", 1, "0"),
("RoadEdgesWidth", "2", 2, "2"),
("RoadNameUI", "1", 1, "0"),
("RoadNameUI", "1", 2, "0"),
("RotatingWheel", "1", 1, "0"),
("ScreenBrightness", "101", 2, "101"),
("ScreenBrightnessOnroad", "101", 2, "101"),
("ScreenManagement", "1", 1, "0"),
("ScreenManagement", "1", 2, "0"),
("ScreenRecorder", "1", 2, "0"),
("ScreenTimeout", "30", 2, "30"),
("ScreenTimeoutOnroad", "30", 2, "10"),
@@ -348,13 +374,13 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("ShownToggleDescriptions", "", 0, ""),
("ShowSLCOffset", "1", 0, "0"),
("ShowSpeedLimits", "1", 1, "0"),
("ShowSteering", "0", 3, "0"),
("ShowStoppingPoint", "1", 3, "0"),
("ShowStoppingPointMetrics", "1", 3, "0"),
("ShowSteering", "1", 3, "0"),
("ShowStoppingPoint", "0", 2, "0"),
("ShowStoppingPointMetrics", "0", 2, "0"),
("ShowStorageLeft", "0", 3, "0"),
("ShowStorageUsed", "0", 3, "0"),
("Sidebar", "0", 0, "0"),
("SignalMetrics", "0", 3, "0"),
("SignalMetrics", "0", 2, "0"),
("SLCConfirmation", "0", 0, "0"),
("SLCConfirmationHigher", "0", 0, "0"),
("SLCConfirmationLower", "0", 0, "0"),
@@ -367,28 +393,31 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("SLCPriority2", "Map Data", 2, "Map Data"),
("SLCPriority3", "Dashboard", 2, "Dashboard"),
("SNGHack", "1", 2, "0"),
("SpeedLimitChangedAlert", "0", 0, "0"),
("SpeedLimitChangedAlert", "1", 0, "0"),
("SpeedLimitController", "1", 0, "0"),
("SpeedLimitFiller", "0", 0, "0"),
("SpeedLimitSources", "0", 3, "0"),
("SpeedLimitSources", "0", 2, "0"),
("SshEnabled", "0", 0, "0"),
("StartupMessageBottom", "Human-tested, frog-approved 🐸", 0, "Always keep hands on wheel and eyes on road"),
("StartupMessageTop", "Hop in and buckle up!", 0, "Be ready to take over at any time"),
("StandardFollow", "1.45", 2, "1.45"),
("StandardJerkAcceleration", "100", 3, "100"),
("StandardFollowHigh", "1.45", 2, "1.45"),
("StandardJerkAcceleration", "50", 3, "50"),
("StandardJerkDanger", "100", 3, "100"),
("StandardJerkDeceleration", "100", 3, "100"),
("StandardJerkSpeed", "100", 3, "100"),
("StandardJerkSpeedDecrease", "100", 3, "100"),
("StandardJerkDeceleration", "50", 3, "50"),
("StandardJerkSpeed", "50", 3, "50"),
("StandardJerkSpeedDecrease", "50", 3, "50"),
("StandardPersonalityProfile", "1", 2, "0"),
("StandbyMode", "0", 1, "0"),
("StandbyMode", "0", 2, "0"),
("StartAccel", "", 3, ""),
("StartAccelStock", "", 3, ""),
("StaticPedalsOnUI", "0", 1, "0"),
("StaticPedalsOnUI", "0", 2, "0"),
("SteerDelay", "", 3, ""),
("SteerDelayStock", "", 3, ""),
("SteerFriction", "", 3, ""),
("SteerFrictionStock", "", 3, ""),
("SteerOffset", "", 3, ""),
("SteerOffsetStock", "", 3, ""),
("SteerKP", "", 3, ""),
("SteerKPStock", "", 3, ""),
("SteerLatAccel", "", 3, ""),
@@ -403,8 +432,6 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("TacoTune", "0", 2, "0"),
("TacoTuneHacks", "0", 2, "0"),
("TetheringEnabled", "0", 0, "0"),
("ThemesDownloaded", "", 0, ""),
("TinygradUpdateAvailable", "0", 1, "0"),
("ToyotaDoors", "1", 0, "0"),
("TrafficFollow", "0.5", 2, "0.5"),
("TrafficJerkAcceleration", "50", 3, "50"),
@@ -413,8 +440,8 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("TrafficJerkSpeed", "50", 3, "50"),
("TrafficJerkSpeedDecrease", "50", 3, "50"),
("TrafficPersonalityProfile", "1", 2, "0"),
("TuningLevel", "0", 0, "0"),
("TuningLevelConfirmed", "0", 0, "0"),
("TuningLevel", "3", 0, "0"),
("TuningLevelConfirmed", "1", 0, "0"),
("TurnDesires", "0", 2, "0"),
("UnlimitedLength", "1", 2, "0"),
("UnlockDoors", "1", 0, "0"),
@@ -431,7 +458,9 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("WarningImmediateVolume", "101", 2, "101"),
("WarningSoftVolume", "101", 2, "101"),
("WheelIcon", "frog", 0, "stock"),
("WheelSpeed", "0", 2, "0")
("WheelSpeed", "0", 2, "0"),
("StopDistance", "6.0", 3, "6.0"),
("RecoveryPower", "1.0", 2, "1.0")
]
misc_tuning_levels: list[tuple[str, str | bytes, int, str]] = [
@@ -508,6 +537,18 @@ class FrogPilotVariables:
tuning_level = params.get_int("TuningLevel") if params.get_bool("TuningLevelConfirmed") else 3
# One-time migration: move legacy bd2 selections to the new default sc model.
if not MODEL_MIGRATION_FLAG_PATH.is_file():
if params.get("Model", encoding="utf-8") == "bd2":
params.put("Model", DEFAULT_MODEL)
params.put("ModelVersion", DEFAULT_MODEL_VERSION)
params_cache.put("Model", DEFAULT_MODEL)
params_cache.put("ModelVersion", DEFAULT_MODEL_VERSION)
try:
MODEL_MIGRATION_FLAG_PATH.touch(exist_ok=True)
except OSError:
pass
toggle.is_metric = params.get_bool("IsMetric")
distance_conversion = 1 if toggle.is_metric else CV.FOOT_TO_METER
small_distance_conversion = 1 if toggle.is_metric else CV.INCH_TO_CM
@@ -526,6 +567,10 @@ class FrogPilotVariables:
safety_config.safetyModel = car.CarParams.SafetyModel.noOutput
CP.safetyConfigs = [safety_config]
is_torque_car = CP.lateralTuning.which() == "torque"
if not is_torque_car:
CarInterfaceBase.configure_torque_tune(MOCK.MOCK, CP.lateralTuning)
fpmsg_bytes = params.get("FrogPilotCarParams" if started else "FrogPilotCarParamsPersistent", block=started)
if fpmsg_bytes:
with custom.FrogPilotCarParams.from_bytes(fpmsg_bytes) as fpcp_reader:
@@ -534,40 +579,38 @@ class FrogPilotVariables:
CarInterface, _, _ = interfaces[MOCK.MOCK]
FPCP = CarInterface.get_frogpilot_params(MOCK.MOCK, gen_empty_fingerprint(), [], CP, toggle)
is_torque_car = FPCP.lateralTuning.which() == "torque"
if not is_torque_car:
CarInterfaceBase.configure_torque_tune(MOCK.MOCK, FPCP.lateralTuning)
toggle.always_on_lateral_set = bool(CP.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL)
toggle.car_make = CP.carName
toggle.car_model = CP.carFingerprint
toggle.disable_openpilot_long = params.get_bool("DisableOpenpilotLongitudinal") if tuning_level >= level["DisableOpenpilotLongitudinal"] else default.get_bool("DisableOpenpilotLongitudinal")
friction = FPCP.lateralTuning.torque.friction
has_auto_tune = toggle.car_make in {"hyundai", "toyota"} and FPCP.lateralTuning.which() == "torque"
friction = CP.lateralTuning.torque.friction
has_auto_tune = toggle.car_make in {"hyundai", "toyota"} and CP.lateralTuning.which() == "torque"
has_bsm = CP.enableBsm
toggle.has_cc_long = toggle.car_make == "gm" and bool(CP.flags & GMFlags.CC_LONG.value)
has_nnff = nnff_supported(toggle.car_model)
toggle.has_pedal = CP.enableGasInterceptor
params_default.put("IncreasedStoppedDistance", "4" if toggle.has_pedal else "0")
has_radar = not CP.radarUnavailable
toggle.has_sdsu = toggle.car_make == "toyota" and bool(CP.flags & ToyotaFlags.SMART_DSU.value)
toggle.has_sascm = toggle.car_make == "gm" and bool(CP.flags & GMFlags.SASCM.value)
has_sng = CP.autoResumeSng
toggle.has_zss = toggle.car_make == "toyota" and bool(FPCP.fpFlags & ToyotaFrogPilotFlags.ZSS.value)
is_angle_car = CP.steerControlType == car.CarParams.SteerControlType.angle
latAccelFactor = FPCP.lateralTuning.torque.latAccelFactor
latAccelFactor = CP.lateralTuning.torque.latAccelFactor
longitudinalActuatorDelay = CP.longitudinalActuatorDelay
toggle.openpilot_longitudinal = CP.openpilotLongitudinalControl and not toggle.disable_openpilot_long
pcm_cruise = CP.pcmCruise
startAccel = CP.startAccel
stopAccel = CP.stopAccel
steerActuatorDelay = CP.steerActuatorDelay
steerKp = FPCP.lateralTuning.torque.kp
steerKp = CP.lateralTuning.torque.kp
steerRatio = CP.steerRatio
toggle.stoppingDecelRate = CP.stoppingDecelRate
taco_hacks_allowed = CP.safetyConfigs[0].safetyModel == SafetyModel.hyundaiCanfd
toggle.use_lkas_for_aol = not toggle.openpilot_longitudinal and CP.safetyConfigs[0].safetyModel == SafetyModel.hyundaiCanfd
toggle.vEgoStarting = CP.vEgoStarting
toggle.vEgoStopping = CP.vEgoStopping
msg_bytes = params.get("LiveTorqueParameters")
if msg_bytes:
with log.LiveTorqueParametersData.from_bytes(msg_bytes) as LTP:
@@ -594,14 +637,37 @@ class FrogPilotVariables:
toggle.steerActuatorDelay = np.clip(params.get_float("SteerDelay"), 0.01, 1.0) if advanced_lateral_tuning and tuning_level >= level["SteerDelay"] else steerActuatorDelay
toggle.use_custom_steerActuatorDelay = bool(round(toggle.steerActuatorDelay, 2) != round(steerActuatorDelay, 2))
toggle.friction = np.clip(params.get_float("SteerFriction"), 0, 0.5) if advanced_lateral_tuning and tuning_level >= level["SteerFriction"] else friction
toggle.steer_offset = np.clip(params.get_float("SteerOffset"), -0.2, 0.2) if advanced_lateral_tuning and tuning_level >= level["SteerOffset"] and toggle.car_make == "gm" else 0.0
toggle.use_custom_friction = bool(round(toggle.friction, 2) != round(friction, 2)) and is_torque_car and not toggle.force_auto_tune or toggle.force_auto_tune_off
toggle.steerKp = [[0], [np.clip(params.get_float("SteerKP"), steerKp * 0.5, steerKp * 1.5) if advanced_lateral_tuning and is_torque_car and tuning_level >= level["SteerKP"] else steerKp]]
toggle.latAccelFactor = np.clip(params.get_float("SteerLatAccel"), latAccelFactor * 0.75, latAccelFactor * 1.25) if advanced_lateral_tuning and tuning_level >= level["SteerLatAccel"] else latAccelFactor
toggle.latAccelFactor = np.clip(params.get_float("SteerLatAccel"), latAccelFactor * 0.5, latAccelFactor * 1.25) if advanced_lateral_tuning and tuning_level >= level["SteerLatAccel"] else latAccelFactor
toggle.use_custom_latAccelFactor = bool(round(toggle.latAccelFactor, 2) != round(latAccelFactor, 2)) and is_torque_car and not toggle.force_auto_tune or toggle.force_auto_tune_off
toggle.steerRatio = np.clip(params.get_float("SteerRatio"), steerRatio * 0.5, steerRatio * 1.5) if advanced_lateral_tuning and tuning_level >= level["SteerRatio"] else steerRatio
toggle.steerRatio = np.clip(params.get_float("SteerRatio"), steerRatio * 0.25, steerRatio * 1.5) if advanced_lateral_tuning and tuning_level >= level["SteerRatio"] else steerRatio
toggle.use_custom_steerRatio = bool(round(toggle.steerRatio, 2) != round(steerRatio, 2)) and not toggle.force_auto_tune or toggle.force_auto_tune_off
advanced_longitudinal_tuning = params.get_bool("AdvancedLongitudinalTune") if tuning_level >= level["AdvancedLongitudinalTune"] else default.get_bool("AdvancedLongitudinalTune")
gm_ev_vehicle = toggle.car_make == "gm" and CP.carFingerprint in GM_EV_CAR
gm_ev_vehicle &= not (toggle.car_model.startswith("CHEVROLET_VOLT") and not toggle.car_model.endswith("_CC"))
gm_ev_vehicle &= toggle.car_model != "CHEVROLET_MALIBU_HYBRID_CC"
ev_vehicle = gm_ev_vehicle or (toggle.car_make == "hyundai" and CP.carFingerprint in HYUNDAI_EV_CAR)
ev_vehicle |= CP.transmissionType == TransmissionType.direct
if params.get("EVTuning") == b"":
params.put_bool("EVTuning", ev_vehicle)
if params.get("TruckTuning") == b"":
params.put_bool("TruckTuning", False)
ev_tuning_param = params.get_bool("EVTuning")
truck_tuning_param = params.get_bool("TruckTuning")
# Enforce exclusivity between EV and Truck tuning.
if truck_tuning_param and ev_tuning_param:
ev_tuning_param = False
params.put_bool("EVTuning", False)
toggle.ev_tuning = ev_tuning_param if advanced_longitudinal_tuning and tuning_level >= level["EVTuning"] else ev_vehicle
toggle.truck_tuning = truck_tuning_param if advanced_longitudinal_tuning and tuning_level >= level["TruckTuning"] else False
toggle.longitudinalActuatorDelay = np.clip(params.get_float("LongitudinalActuatorDelay"), 0, 1) if advanced_longitudinal_tuning and tuning_level >= level["LongitudinalActuatorDelay"] else longitudinalActuatorDelay
toggle.startAccel = np.clip(params.get_float("StartAccel"), 0, 4) if advanced_longitudinal_tuning and tuning_level >= level["StartAccel"] else startAccel
toggle.stopAccel = np.clip(params.get_float("StopAccel"), -4, 0) if advanced_longitudinal_tuning and tuning_level >= level["StopAccel"] else stopAccel
@@ -609,6 +675,10 @@ class FrogPilotVariables:
toggle.vEgoStarting = np.clip(params.get_float("VEgoStarting"), 0.01, 1) if advanced_longitudinal_tuning and tuning_level >= level["VEgoStarting"] else toggle.vEgoStarting
toggle.vEgoStopping = np.clip(params.get_float("VEgoStopping"), 0.01, 1) if advanced_longitudinal_tuning and tuning_level >= level["VEgoStopping"] else toggle.vEgoStopping
toggle.stop_distance = params.get_float("StopDistance") if advanced_longitudinal_tuning and tuning_level >= level["StopDistance"] else 6.0
toggle.recovery_power = np.clip(params.get_float("RecoveryPower"), 0.5, 2.0) if advanced_longitudinal_tuning and tuning_level >= level["RecoveryPower"] else 1.0
toggle.alert_volume_controller = params.get_bool("AlertVolumeControl") if tuning_level >= level["AlertVolumeControl"] else default.get_bool("AlertVolumeControl")
toggle.disengage_volume = params.get_int("DisengageVolume") if toggle.alert_volume_controller and tuning_level >= level["DisengageVolume"] else default.get_int("DisengageVolume")
toggle.engage_volume = params.get_int("EngageVolume") if toggle.alert_volume_controller and tuning_level >= level["EngageVolume"] else default.get_int("EngageVolume")
@@ -624,11 +694,11 @@ class FrogPilotVariables:
toggle.always_on_lateral_main = toggle.always_on_lateral_set and not toggle.use_lkas_for_aol and (params.get_bool("AlwaysOnLateralMain") if tuning_level >= level["AlwaysOnLateralMain"] else default.get_bool("AlwaysOnLateralMain"))
toggle.always_on_lateral_pause_speed = params.get_int("PauseAOLOnBrake") if toggle.always_on_lateral_set and tuning_level >= level["PauseAOLOnBrake"] else default.get_int("PauseAOLOnBrake")
toggle.automatic_updates = (params.get_bool("AutomaticUpdates") if tuning_level >= level["AutomaticUpdates"] and (self.release_branch or self.vetting_branch) else default.get_bool("AutomaticUpdates")) and not BACKUP_PATH.is_file()
toggle.automatic_updates = (params.get_bool("AutomaticUpdates") if tuning_level >= level["AutomaticUpdates"] else default.get_bool("AutomaticUpdates")) and not BACKUP_PATH.is_file()
toggle.car_model = params.get("CarModel", encoding="utf-8") or toggle.car_model
toggle.cluster_offset = params.get_float("ClusterOffset") if toggle.car_make == "toyota" and tuning_level >= level["ClusterOffset"] else default.get_float("ClusterOffset")
toggle.cluster_offset = params.get_float("ClusterOffset") if toggle.car_make in {"gm", "toyota"} and tuning_level >= level["ClusterOffset"] else default.get_float("ClusterOffset")
toggle.conditional_experimental_mode = toggle.openpilot_longitudinal and (params.get_bool("ConditionalExperimental") if tuning_level >= level["ConditionalExperimental"] else default.get_bool("ConditionalExperimental"))
toggle.conditional_curves = toggle.conditional_experimental_mode and (params.get_bool("CECurves") if tuning_level >= level["CECurves"] else default.get_bool("CECurves"))
@@ -652,6 +722,7 @@ class FrogPilotVariables:
toggle.custom_alerts = params.get_bool("CustomAlerts") if tuning_level >= level["CustomAlerts"] else default.get_bool("CustomAlerts")
toggle.goat_scream_alert = toggle.custom_alerts and (params.get_bool("GoatScream") if tuning_level >= level["GoatScream"] else default.get_bool("GoatScream"))
toggle.goat_scream_critical_alerts = toggle.custom_alerts and (params.get_bool("GoatScreamCriticalAlerts") if tuning_level >= level["GoatScreamCriticalAlerts"] else default.get_bool("GoatScreamCriticalAlerts"))
toggle.green_light_alert = toggle.custom_alerts and (params.get_bool("GreenLightAlert") if tuning_level >= level["GreenLightAlert"] else default.get_bool("GreenLightAlert"))
toggle.lead_departing_alert = toggle.custom_alerts and (params.get_bool("LeadDepartingAlert") if tuning_level >= level["LeadDepartingAlert"] else default.get_bool("LeadDepartingAlert"))
toggle.loud_blindspot_alert = has_bsm and toggle.custom_alerts and (params.get_bool("LoudBlindspotAlert") if tuning_level >= level["LoudBlindspotAlert"] else default.get_bool("LoudBlindspotAlert"))
@@ -664,28 +735,31 @@ class FrogPilotVariables:
toggle.aggressive_jerk_danger = np.clip(params.get_int("AggressiveJerkDanger") / 100, 0.25, 2) if aggressive_profile and tuning_level >= level["AggressiveJerkDanger"] else default.get_int("AggressiveJerkDanger") / 100
toggle.aggressive_jerk_speed = np.clip(params.get_int("AggressiveJerkSpeed") / 100, 0.25, 2) if aggressive_profile and tuning_level >= level["AggressiveJerkSpeed"] else default.get_int("AggressiveJerkSpeed") / 100
toggle.aggressive_jerk_speed_decrease = np.clip(params.get_int("AggressiveJerkSpeedDecrease") / 100, 0.25, 2) if aggressive_profile and tuning_level >= level["AggressiveJerkSpeedDecrease"] else default.get_int("AggressiveJerkSpeedDecrease") / 100
toggle.aggressive_follow = np.clip(params.get_float("AggressiveFollow"), 1, MAX_T_FOLLOW) if aggressive_profile and tuning_level >= level["AggressiveFollow"] else default.get_float("AggressiveFollow")
toggle.aggressive_follow = [np.clip(params.get_float("AggressiveFollow"), 1, MAX_T_FOLLOW) if aggressive_profile and tuning_level >= level["AggressiveFollow"] else default.get_float("AggressiveFollow"),
np.clip(params.get_float("AggressiveFollowHigh"), 1, MAX_T_FOLLOW) if aggressive_profile and tuning_level >= level["AggressiveFollowHigh"] else default.get_float("AggressiveFollowHigh")]
standard_profile = toggle.custom_personalities and (params.get_bool("StandardPersonalityProfile") if tuning_level >= level["StandardPersonalityProfile"] else default.get_bool("StandardPersonalityProfile"))
toggle.standard_jerk_acceleration = np.clip(params.get_int("StandardJerkAcceleration") / 100, 0.25, 2) if standard_profile and tuning_level >= level["StandardJerkAcceleration"] else default.get_int("StandardJerkAcceleration") / 100
toggle.standard_jerk_deceleration = np.clip(params.get_int("StandardJerkDeceleration") / 100, 0.25, 2) if standard_profile and tuning_level >= level["StandardJerkDeceleration"] else default.get_int("StandardJerkDeceleration") / 100
toggle.standard_jerk_danger = np.clip(params.get_int("StandardJerkDanger") / 100, 0.25, 2) if standard_profile and tuning_level >= level["StandardJerkDanger"] else default.get_int("StandardJerkDanger") / 100
toggle.standard_jerk_speed = np.clip(params.get_int("StandardJerkSpeed") / 100, 0.25, 2) if standard_profile and tuning_level >= level["StandardJerkSpeed"] else default.get_int("StandardJerkSpeed") / 100
toggle.standard_jerk_speed_decrease = np.clip(params.get_int("StandardJerkSpeedDecrease") / 100, 0.25, 2) if standard_profile and tuning_level >= level["StandardJerkSpeedDecrease"] else default.get_int("StandardJerkSpeedDecrease") / 100
toggle.standard_follow = np.clip(params.get_float("StandardFollow"), 1, MAX_T_FOLLOW) if standard_profile and tuning_level >= level["StandardFollow"] else default.get_float("StandardFollow")
toggle.standard_follow = [np.clip(params.get_float("StandardFollow"), 1, MAX_T_FOLLOW) if standard_profile and tuning_level >= level["StandardFollow"] else default.get_float("StandardFollow"),
np.clip(params.get_float("StandardFollowHigh"), 1, MAX_T_FOLLOW) if standard_profile and tuning_level >= level["StandardFollowHigh"] else default.get_float("StandardFollowHigh")]
relaxed_profile = toggle.custom_personalities and (params.get_bool("RelaxedPersonalityProfile") if tuning_level >= level["RelaxedPersonalityProfile"] else default.get_bool("RelaxedPersonalityProfile"))
toggle.relaxed_jerk_acceleration = np.clip(params.get_int("RelaxedJerkAcceleration") / 100, 0.25, 2) if relaxed_profile and tuning_level >= level["RelaxedJerkAcceleration"] else default.get_int("RelaxedJerkAcceleration") / 100
toggle.relaxed_jerk_deceleration = np.clip(params.get_int("RelaxedJerkDeceleration") / 100, 0.25, 2) if relaxed_profile and tuning_level >= level["RelaxedJerkDeceleration"] else default.get_int("RelaxedJerkDeceleration") / 100
toggle.relaxed_jerk_danger = np.clip(params.get_int("RelaxedJerkDanger") / 100, 0.25, 2) if relaxed_profile and tuning_level >= level["RelaxedJerkDanger"] else default.get_int("RelaxedJerkDanger") / 100
toggle.relaxed_jerk_speed = np.clip(params.get_int("RelaxedJerkSpeed") / 100, 0.25, 2) if relaxed_profile and tuning_level >= level["RelaxedJerkSpeed"] else default.get_int("RelaxedJerkSpeed") / 100
toggle.relaxed_jerk_speed_decrease = np.clip(params.get_int("RelaxedJerkSpeedDecrease") / 100, 0.25, 2) if relaxed_profile and tuning_level >= level["RelaxedJerkSpeedDecrease"] else default.get_int("RelaxedJerkSpeedDecrease") / 100
toggle.relaxed_follow = np.clip(params.get_float("RelaxedFollow"), 1, MAX_T_FOLLOW) if relaxed_profile and tuning_level >= level["RelaxedFollow"] else default.get_float("RelaxedFollow")
toggle.relaxed_follow = [np.clip(params.get_float("RelaxedFollow"), 1, MAX_T_FOLLOW) if relaxed_profile and tuning_level >= level["RelaxedFollow"] else default.get_float("RelaxedFollow"),
np.clip(params.get_float("RelaxedFollowHigh"), 1, MAX_T_FOLLOW) if relaxed_profile and tuning_level >= level["RelaxedFollowHigh"] else default.get_float("RelaxedFollowHigh")]
traffic_profile = toggle.custom_personalities and (params.get_bool("TrafficPersonalityProfile") if tuning_level >= level["TrafficPersonalityProfile"] else default.get_bool("TrafficPersonalityProfile"))
toggle.traffic_mode_jerk_acceleration = [np.clip(params.get_int("TrafficJerkAcceleration") / 100, 0.25, 2) if traffic_profile and tuning_level >= level["TrafficJerkAcceleration"] else default.get_int("TrafficJerkAcceleration") / 100, toggle.aggressive_jerk_acceleration]
toggle.traffic_mode_jerk_deceleration = [np.clip(params.get_int("TrafficJerkDeceleration") / 100, 0.25, 2) if traffic_profile and tuning_level >= level["TrafficJerkDeceleration"] else default.get_int("TrafficJerkDeceleration") / 100, toggle.aggressive_jerk_deceleration]
toggle.traffic_mode_jerk_danger = [np.clip(params.get_int("TrafficJerkDanger") / 100, 0.25, 2) if traffic_profile and tuning_level >= level["TrafficJerkDanger"] else default.get_int("TrafficJerkDanger") / 100, toggle.aggressive_jerk_danger]
toggle.traffic_mode_jerk_speed = [np.clip(params.get_int("TrafficJerkSpeed") / 100, 0.25, 2) if traffic_profile and tuning_level >= level["TrafficJerkSpeed"] else default.get_int("TrafficJerkSpeed") / 100, toggle.aggressive_jerk_speed]
toggle.traffic_mode_jerk_speed_decrease = [np.clip(params.get_int("TrafficJerkSpeedDecrease") / 100, 0.25, 2) if traffic_profile and tuning_level >= level["TrafficJerkSpeedDecrease"] else default.get_int("TrafficJerkSpeedDecrease") / 100, toggle.aggressive_jerk_speed_decrease]
toggle.traffic_mode_follow = [np.clip(params.get_float("TrafficFollow"), 0.5, MAX_T_FOLLOW) if traffic_profile and tuning_level >= level["TrafficFollow"] else default.get_float("TrafficFollow"), toggle.aggressive_follow]
toggle.traffic_mode_follow = [np.clip(params.get_float("TrafficFollow"), 0.5, MAX_T_FOLLOW) if traffic_profile and tuning_level >= level["TrafficFollow"] else default.get_float("TrafficFollow"), toggle.aggressive_follow[0]]
custom_ui = params.get_bool("CustomUI") if tuning_level >= level["CustomUI"] else default.get_bool("CustomUI")
toggle.acceleration_path = toggle.openpilot_longitudinal and (custom_ui and (params.get_bool("AccelerationPath") if tuning_level >= level["AccelerationPath"] else default.get_bool("AccelerationPath")) or toggle.debug_mode)
@@ -734,6 +808,7 @@ class FrogPilotVariables:
toggle.device_shutdown_time = (device_shutdown_setting - 3) * 3600 if device_shutdown_setting >= 4 else device_shutdown_setting * (60 * 15)
toggle.increase_thermal_limits = device_management and (params.get_bool("IncreaseThermalLimits") if tuning_level >= level["IncreaseThermalLimits"] else default.get_bool("IncreaseThermalLimits"))
toggle.low_voltage_shutdown = np.clip(params.get_float("LowVoltageShutdown"), VBATT_PAUSE_CHARGING, 12.5) if device_management and tuning_level >= level["LowVoltageShutdown"] else default.get_float("LowVoltageShutdown")
toggle.always_allow_uploads = device_management and (params.get_bool("AlwaysAllowUploads") if tuning_level >= level["AlwaysAllowUploads"] else default.get_bool("AlwaysAllowUploads"))
toggle.no_logging = device_management and (params.get_bool("NoLogging") if tuning_level >= level["NoLogging"] else default.get_bool("NoLogging")) and not self.vetting_branch or toggle.force_onroad
toggle.no_uploads = device_management and (params.get_bool("NoUploads") if tuning_level >= level["NoUploads"] else default.get_bool("NoUploads")) and not self.vetting_branch or toggle.use_higher_bitrate
toggle.no_onroad_uploads = toggle.no_uploads and (params.get_bool("DisableOnroadUploads") if tuning_level >= level["DisableOnroadUploads"] else default.get_bool("DisableOnroadUploads")) and not toggle.use_higher_bitrate
@@ -765,10 +840,11 @@ class FrogPilotVariables:
toggle.personality_profile_via_distance_very_long = toggle.openpilot_longitudinal and distance_button_control_very_long == BUTTON_FUNCTIONS["PERSONALITY_PROFILE"]
toggle.traffic_mode_via_distance_very_long = toggle.openpilot_longitudinal and distance_button_control_very_long == BUTTON_FUNCTIONS["TRAFFIC_MODE"]
toggle.experimental_gm_tune = toggle.openpilot_longitudinal and toggle.car_make == "gm" and (params.get_bool("ExperimentalGMTune") if tuning_level >= level["ExperimentalGMTune"] else default.get_bool("ExperimentalGMTune"))
toggle.stoppingDecelRate = 0.3 if toggle.experimental_gm_tune else toggle.stoppingDecelRate
toggle.vEgoStarting = 0.15 if toggle.experimental_gm_tune else toggle.vEgoStarting
toggle.vEgoStopping = 0.15 if toggle.experimental_gm_tune else toggle.vEgoStopping
toggle.red_panda = toggle.car_make == "gm" and (params.get_bool("RedPanda") if tuning_level >= level["RedPanda"] else default.get_bool("RedPanda"))
toggle.remote_start_boots_comma = toggle.car_make == "gm" and (params.get_bool("RemoteStartBootsComma") if tuning_level >= level["RemoteStartBootsComma"] else default.get_bool("RemoteStartBootsComma"))
toggle.remap_cancel_to_distance = toggle.car_make == "gm" and toggle.openpilot_longitudinal and (
params.get_bool("RemapCancelToDistance") if tuning_level >= level["RemapCancelToDistance"] else default.get_bool("RemapCancelToDistance")
)
toggle.force_fingerprint = (params.get_bool("ForceFingerprint") if tuning_level >= level["ForceFingerprint"] else default.get_bool("ForceFingerprint")) and toggle.car_model is not None
@@ -814,33 +890,49 @@ class FrogPilotVariables:
toggle.human_following = longitudinal_tuning and (params.get_bool("HumanFollowing") if tuning_level >= level["HumanFollowing"] else default.get_bool("HumanFollowing"))
toggle.lead_detection_probability = np.clip(params.get_int("LeadDetectionThreshold") / 100, 0.25, 0.50) if longitudinal_tuning and tuning_level >= level["LeadDetectionThreshold"] else default.get_int("LeadDetectionThreshold") / 100
toggle.max_desired_acceleration = np.clip(params.get_float("MaxDesiredAcceleration"), 0.1, 4.0) if longitudinal_tuning and tuning_level >= level["MaxDesiredAcceleration"] else default.get_float("MaxDesiredAcceleration")
toggle.trailer_load_kg = (np.clip(params.get_int("TrailerLoad"), 0, 15000) if longitudinal_tuning and tuning_level >= level["TrailerLoad"] else default.get_int("TrailerLoad")) * CV.LB_TO_KG
toggle.taco_tune = longitudinal_tuning and (params.get_bool("TacoTune") if tuning_level >= level["TacoTune"] else default.get_bool("TacoTune"))
toggle.available_models = (params.get("AvailableModels", encoding="utf-8") or "") + f",{DEFAULT_MODEL}"
toggle.available_model_names = (params.get("AvailableModelNames", encoding="utf-8") or "") + f",{DEFAULT_MODEL_NAME}"
downloaded_models = [model for model in toggle.available_models.split(",") if (MODELS_PATH / f"{model}.thneed").is_file() or all((MODELS_PATH / f"{model}_{filename}").is_file() for filename, _ in TINYGRAD_FILES)]
model_versions = (params.get("ModelVersions", encoding="utf-8") or "") + f",{DEFAULT_MODEL_VERSION}"
toggle.model_randomizer = params.get_bool("ModelRandomizer") if tuning_level >= level["ModelRandomizer"] else default.get_bool("ModelRandomizer")
if toggle.model_randomizer:
if not started:
blacklisted_models = (params.get("BlacklistedModels", encoding="utf-8") or "").split(",")
selectable_models = [model for model in downloaded_models if model not in blacklisted_models]
toggle.model = random.choice(selectable_models) if selectable_models else DEFAULT_MODEL
toggle.model_name = "Mystery Model 👻"
toggle.model_version = model_versions.split(",")[toggle.available_models.split(",").index(toggle.model)]
else:
model = ((params.get("Model", encoding="utf-8") if tuning_level >= level["Model"] else default.get("Model", encoding="utf-8")) or DEFAULT_MODEL).removesuffix("_default")
if model in downloaded_models:
toggle.model = model
toggle.model_name = dict(zip(toggle.available_models.split(","), toggle.available_model_names.split(",")))[toggle.model]
toggle.model_version = dict(zip(toggle.available_models.split(","), model_versions.split(",")))[toggle.model]
toggle.available_models = params.get("AvailableModels", encoding="utf-8") or ""
toggle.available_model_names = params.get("AvailableModelNames", encoding="utf-8") or ""
toggle.available_model_series = params.get("AvailableModelSeries", encoding="utf-8") or ""
toggle.community_favorites = params.get("CommunityFavorites", encoding="utf-8") or ""
toggle.model_released_dates = params.get("ModelReleasedDates", encoding="utf-8") or ""
toggle.model_versions = params.get("ModelVersions", encoding="utf-8") or ""
toggle.sort_models_by_date = params.get_bool("SortModelsByDate") if tuning_level >= level["SortModelsByDate"] else default.get_bool("SortModelsByDate")
toggle.user_favorites = params.get("UserFavorites", encoding="utf-8") or ""
downloaded_models = [model for model in toggle.available_models.split(",") if any(MODELS_PATH.glob(f"{model}*"))]
toggle.model_randomizer = downloaded_models and (params.get_bool("ModelRandomizer") if tuning_level >= level["ModelRandomizer"] else default.get_bool("ModelRandomizer"))
if toggle.available_models and toggle.available_model_names and downloaded_models and toggle.model_versions:
if DEFAULT_MODEL not in toggle.available_models.split(","):
toggle.available_models += f",{DEFAULT_MODEL}"
toggle.available_model_names += f",{DEFAULT_MODEL_NAME}"
toggle.model_versions += f",{DEFAULT_MODEL_VERSION}"
downloaded_models += [DEFAULT_MODEL]
if toggle.model_randomizer:
if not started:
blacklisted_models = (params.get("BlacklistedModels", encoding="utf-8") or "").split(",")
selectable_models = [model for model in downloaded_models if model not in blacklisted_models]
toggle.model = random.choice(selectable_models) if selectable_models else default.get("Model", encoding="utf-8")
toggle.model_name = "Mystery Model 👻"
toggle.model_version = toggle.model_versions.split(",")[toggle.available_models.split(",").index(toggle.model)]
else:
toggle.model = params.get("Model", encoding="utf-8") if tuning_level >= level["Model"] else default.get("Model", encoding="utf-8")
if toggle.model in downloaded_models:
toggle.model_name = toggle.available_model_names.split(",")[toggle.available_models.split(",").index(toggle.model)]
toggle.model_version = toggle.model_versions.split(",")[toggle.available_models.split(",").index(toggle.model)]
else:
toggle.model = default.get("Model", encoding="utf-8")
toggle.model_name = toggle.available_model_names.split(",")[toggle.available_models.split(",").index(toggle.model)]
toggle.model_version = toggle.model_versions.split(",")[toggle.available_models.split(",").index(toggle.model)]
else:
toggle.model = DEFAULT_MODEL
toggle.model_name = DEFAULT_MODEL_NAME
toggle.model_version = DEFAULT_MODEL_VERSION
toggle.classic_longitudinal = toggle.model_version in {"v1", "v2", "v3", "v4"}
toggle.classic_model = toggle.model_version in {"v1", "v2", "v3", "v4"}
toggle.classic_longitudinal = toggle.model_version in {"v1", "v2", "v3", "v4", "v5", "v6"}
toggle.tinygrad_model = not toggle.classic_model and toggle.model_version not in {"v5", "v6"}
toggle.tinygrad_model = toggle.model_version in {"v8", "v9", "v10", "v11", "v12"}
toggle.tomb_raider = toggle.model == "space-lab"
toggle.model_ui = params.get_bool("ModelUI") if tuning_level >= level["ModelUI"] else default.get_bool("ModelUI")
toggle.dynamic_path_width = toggle.model_ui and (params.get_bool("DynamicPathWidth") if tuning_level >= level["DynamicPathWidth"] else default.get_bool("DynamicPathWidth"))
@@ -863,6 +955,7 @@ class FrogPilotVariables:
toggle.old_long_api |= toggle.openpilot_longitudinal and toggle.car_make == "hyundai" and not (params.get_bool("NewLongAPI") if tuning_level >= level["NewLongAPI"] else default.get_bool("NewLongAPI"))
personalize_openpilot = params.get_bool("PersonalizeOpenpilot") if tuning_level >= level["PersonalizeOpenpilot"] else default.get_bool("PersonalizeOpenpilot")
toggle.boot_logo = params.get("BootLogo", encoding="utf-8") or "starpilot"
toggle.color_scheme = toggle.current_holiday_theme if toggle.current_holiday_theme != "stock" else params.get("CustomColors", encoding="utf-8") if personalize_openpilot else "stock"
toggle.distance_icons = toggle.current_holiday_theme if toggle.current_holiday_theme != "stock" else params.get("CustomDistanceIcons", encoding="utf-8") if personalize_openpilot else "stock"
toggle.icon_pack = toggle.current_holiday_theme if toggle.current_holiday_theme != "stock" else params.get("CustomIcons", encoding="utf-8") if personalize_openpilot else "stock"
@@ -906,7 +999,6 @@ class FrogPilotVariables:
toggle.screen_timeout = params.get_int("ScreenTimeout") if screen_management and tuning_level >= level["ScreenTimeout"] else default.get_int("ScreenTimeout")
toggle.screen_timeout_onroad = params.get_int("ScreenTimeoutOnroad") if screen_management and tuning_level >= level["ScreenTimeoutOnroad"] else default.get_int("ScreenTimeoutOnroad")
toggle.standby_mode = screen_management and (params.get_bool("StandbyMode") if tuning_level >= level["StandbyMode"] else default.get_bool("StandbyMode"))
toggle.sng_hack = toggle.openpilot_longitudinal and toggle.car_make == "toyota" and not toggle.has_pedal and not has_sng and (params.get_bool("SNGHack") if tuning_level >= level["SNGHack"] else default.get_bool("SNGHack"))
toggle.speed_limit_controller = toggle.openpilot_longitudinal and (params.get_bool("SpeedLimitController") if tuning_level >= level["SpeedLimitController"] else default.get_bool("SpeedLimitController"))
@@ -953,7 +1045,16 @@ class FrogPilotVariables:
toggle.lock_doors = toyota_doors and (params.get_bool("LockDoors") if tuning_level >= level["LockDoors"] else default.get_bool("LockDoors"))
toggle.unlock_doors = toyota_doors and (params.get_bool("UnlockDoors") if tuning_level >= level["UnlockDoors"] else default.get_bool("UnlockDoors"))
toggle.volt_sng = toggle.car_model == "CHEVROLET_VOLT" and (params.get_bool("VoltSNG") if tuning_level >= level["VoltSNG"] else default.get_bool("VoltSNG"))
volt_models = {
"CHEVROLET_VOLT",
"CHEVROLET_VOLT_2019",
"CHEVROLET_VOLT_ASCM",
"CHEVROLET_VOLT_CAMERA",
}
toggle.volt_sng = toggle.car_model in volt_models and (params.get_bool("VoltSNG") if tuning_level >= level["VoltSNG"] else default.get_bool("VoltSNG"))
toggle.gm_pedal_longitudinal = params.get_bool("GMPedalLongitudinal") if tuning_level >= level["GMPedalLongitudinal"] else default.get_bool("GMPedalLongitudinal")
params_memory.put("FrogPilotToggles", json.dumps(toggle.__dict__))
params_memory.remove("FrogPilotTogglesUpdated")
+224
View File
@@ -0,0 +1,224 @@
import json
import threading
import time
from pathlib import Path
from openpilot.system.hardware import PC
TESTING_GROUNDS_SCHEMA_VERSION = 1
TESTING_GROUND_1 = "1"
TESTING_GROUND_2 = "2"
TESTING_GROUND_3 = "3"
TESTING_GROUND_4 = "4"
TESTING_GROUND_5 = "5"
TESTING_GROUND_6 = "6"
TESTING_GROUND_7 = "7"
TESTING_GROUND_IDS = (
TESTING_GROUND_1,
TESTING_GROUND_2,
TESTING_GROUND_3,
TESTING_GROUND_4,
TESTING_GROUND_5,
TESTING_GROUND_6,
TESTING_GROUND_7,
)
TESTING_GROUNDS_STATE_PATH = Path("/tmp/the_pond_testing_grounds_slots.json") if PC else Path("/data/testing_grounds/slots.json")
# Edit slot names/descriptions once here. The Pond and runtime checks share this table.
# Slots named "Unused" are hidden from the dropdown.
# Adding cLabel/dLabel/etc. automatically adds more mode buttons for that slot in Testing Ground.
TESTING_GROUNDS_SLOT_DEFINITIONS = (
{
"id": TESTING_GROUND_1,
"name": "GM Long Tune",
"description": "BoltLongTune A/B sandbox for GM ACC longitudinal testing.",
"aLabel": "A - Installed tune",
"bLabel": "B - BoltLongTune test",
},
{
"id": TESTING_GROUND_2,
"name": "Unused",
"description": "",
"aLabel": "A",
"bLabel": "B",
},
{
"id": TESTING_GROUND_3,
"name": "Unused",
"description": "",
"aLabel": "A",
"bLabel": "B",
},
{
"id": TESTING_GROUND_4,
"name": "Unused",
"description": "",
"aLabel": "A",
"bLabel": "B",
},
{
"id": TESTING_GROUND_5,
"name": "Unused",
"description": "",
"aLabel": "A",
"bLabel": "B",
},
{
"id": TESTING_GROUND_6,
"name": "Unused",
"description": "",
"aLabel": "A",
"bLabel": "B",
},
{
"id": TESTING_GROUND_7,
"name": "Unused",
"description": "",
"aLabel": "A",
"bLabel": "B",
},
)
_DEFAULT_ACTIVE_SLOT = TESTING_GROUND_1
DEFAULT_TESTING_GROUND_VARIANT = "A"
TESTING_GROUND_TEST_VARIANT = "B"
_CACHE_LOCK = threading.Lock()
_CACHE_LAST_REFRESH = 0.0
_CACHE_LAST_MTIME_NS = -1
_CACHE_ACTIVE_SLOT = _DEFAULT_ACTIVE_SLOT
_CACHE_ACTIVE_VARIANT = DEFAULT_TESTING_GROUND_VARIANT
def _extract_variant_labels(slot_definition):
labels = {}
for key, value in dict(slot_definition).items():
if not isinstance(key, str) or not key.endswith("Label"):
continue
variant = key[:-5].upper()
if len(variant) != 1 or not variant.isalpha():
continue
label = str(value or "").strip()
if label:
labels[variant] = label
if DEFAULT_TESTING_GROUND_VARIANT not in labels:
labels[DEFAULT_TESTING_GROUND_VARIANT] = DEFAULT_TESTING_GROUND_VARIANT
return dict(sorted(labels.items()))
TESTING_GROUND_VARIANT_LABELS = {
str(slot.get("id") or "").strip(): _extract_variant_labels(slot)
for slot in TESTING_GROUNDS_SLOT_DEFINITIONS
}
TESTING_GROUND_VARIANTS = tuple(
sorted({
variant
for labels in TESTING_GROUND_VARIANT_LABELS.values()
for variant in labels.keys()
})
)
def _normalize_variant(value, slot_id=None):
normalized_slot_id = str(slot_id or "").strip()
allowed_variants = set(TESTING_GROUND_VARIANT_LABELS.get(normalized_slot_id, {}).keys())
if not allowed_variants:
allowed_variants = set(TESTING_GROUND_VARIANTS) or {DEFAULT_TESTING_GROUND_VARIANT}
variant = str(value or "").strip().upper()
return variant if variant in allowed_variants else DEFAULT_TESTING_GROUND_VARIANT
def get_testing_ground_selection(refresh_interval_s=0.5):
global _CACHE_LAST_REFRESH, _CACHE_LAST_MTIME_NS, _CACHE_ACTIVE_SLOT, _CACHE_ACTIVE_VARIANT
now = time.monotonic()
with _CACHE_LOCK:
if (now - _CACHE_LAST_REFRESH) < refresh_interval_s:
return _CACHE_ACTIVE_SLOT, _CACHE_ACTIVE_VARIANT
_CACHE_LAST_REFRESH = now
try:
stat_result = TESTING_GROUNDS_STATE_PATH.stat()
except FileNotFoundError:
_CACHE_LAST_MTIME_NS = -1
_CACHE_ACTIVE_SLOT = _DEFAULT_ACTIVE_SLOT
_CACHE_ACTIVE_VARIANT = DEFAULT_TESTING_GROUND_VARIANT
return _CACHE_ACTIVE_SLOT, _CACHE_ACTIVE_VARIANT
except OSError:
return _CACHE_ACTIVE_SLOT, _CACHE_ACTIVE_VARIANT
if stat_result.st_mtime_ns == _CACHE_LAST_MTIME_NS:
return _CACHE_ACTIVE_SLOT, _CACHE_ACTIVE_VARIANT
try:
payload = json.loads(TESTING_GROUNDS_STATE_PATH.read_text(encoding="utf-8"))
except Exception:
return _CACHE_ACTIVE_SLOT, _CACHE_ACTIVE_VARIANT
if not isinstance(payload, dict):
return _CACHE_ACTIVE_SLOT, _CACHE_ACTIVE_VARIANT
slot_id = str(payload.get("activeSlot") or _DEFAULT_ACTIVE_SLOT).strip()
if slot_id not in TESTING_GROUND_IDS:
slot_id = _DEFAULT_ACTIVE_SLOT
_CACHE_ACTIVE_SLOT = slot_id
_CACHE_ACTIVE_VARIANT = _normalize_variant(payload.get("activeVariant"), slot_id)
_CACHE_LAST_MTIME_NS = stat_result.st_mtime_ns
return _CACHE_ACTIVE_SLOT, _CACHE_ACTIVE_VARIANT
def is_testing_ground_active(slot_id, variant=TESTING_GROUND_TEST_VARIANT, refresh_interval_s=0.5):
active_slot, active_variant = get_testing_ground_selection(refresh_interval_s=refresh_interval_s)
normalized_slot_id = str(slot_id or "").strip()
target_variant = _normalize_variant(variant, normalized_slot_id)
return normalized_slot_id == active_slot and active_variant == target_variant
class _TestingGroundFlag:
__slots__ = ("slot_id", "variant")
def __init__(self, slot_id, variant=TESTING_GROUND_TEST_VARIANT):
self.slot_id = str(slot_id or "").strip()
self.variant = _normalize_variant(variant, self.slot_id)
def __bool__(self):
return is_testing_ground_active(self.slot_id, self.variant)
def __repr__(self):
state = "active" if bool(self) else "inactive"
return f"<TestingGroundFlag slot={self.slot_id} variant={self.variant} {state}>"
use_testing_ground_1 = _TestingGroundFlag(TESTING_GROUND_1)
use_testing_ground_2 = _TestingGroundFlag(TESTING_GROUND_2)
use_testing_ground_3 = _TestingGroundFlag(TESTING_GROUND_3)
use_testing_ground_4 = _TestingGroundFlag(TESTING_GROUND_4)
use_testing_ground_5 = _TestingGroundFlag(TESTING_GROUND_5)
use_testing_ground_6 = _TestingGroundFlag(TESTING_GROUND_6)
use_testing_ground_7 = _TestingGroundFlag(TESTING_GROUND_7)
class _TestingGroundNamespace:
__slots__ = ()
id_1 = TESTING_GROUND_1
id_2 = TESTING_GROUND_2
id_3 = TESTING_GROUND_3
id_4 = TESTING_GROUND_4
id_5 = TESTING_GROUND_5
id_6 = TESTING_GROUND_6
id_7 = TESTING_GROUND_7
use_1 = use_testing_ground_1
use_2 = use_testing_ground_2
use_3 = use_testing_ground_3
use_4 = use_testing_ground_4
use_5 = use_testing_ground_5
use_6 = use_testing_ground_6
use_7 = use_testing_ground_7
def use(self, slot_id, variant=TESTING_GROUND_TEST_VARIANT):
return is_testing_ground_active(slot_id, variant)
def selection(self):
return get_testing_ground_selection()
testing_ground = _TestingGroundNamespace()
+9 -3
View File
@@ -9,7 +9,7 @@ from openpilot.common.conversions import Conversions as CV
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import A_CHANGE_COST, DANGER_ZONE_COST, J_EGO_COST, STOP_DISTANCE
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import A_CHANGE_COST, COMFORT_BRAKE, DANGER_ZONE_COST, J_EGO_COST
from openpilot.frogpilot.common.frogpilot_utilities import calculate_lane_width, calculate_road_curvature
from openpilot.frogpilot.common.frogpilot_variables import CRUISING_SPEED, MINIMUM_LATERAL_ACCELERATION, PLANNER_TIME, THRESHOLD, params, params_memory
@@ -65,6 +65,8 @@ class FrogPilotPlanner:
self.cem.curve_detected = False
self.cem.stop_sign_and_light(v_ego, sm, PLANNER_TIME - 2)
self.driving_in_curve = abs(self.lateral_acceleration) >= MINIMUM_LATERAL_ACCELERATION
self.frogpilot_events.update(v_cruise, sm, frogpilot_toggles)
@@ -85,7 +87,7 @@ class FrogPilotPlanner:
params_memory.remove("LastGPSPosition")
self.lateral_acceleration = v_ego**2 * (sm["carState"].steeringAngleDeg - sm["liveParameters"].angleOffsetDeg) * CV.DEG_TO_RAD / (self.CP.steerRatio * self.CP.wheelbase)
self.lateral_acceleration = v_ego**2 * sm["controlsState"].curvature
check_lane_width = frogpilot_toggles.adjacent_paths or frogpilot_toggles.adjacent_path_metrics or frogpilot_toggles.blind_spot_path or frogpilot_toggles.lane_detection
if check_lane_width and v_ego >= frogpilot_toggles.minimum_lane_change_speed:
@@ -115,7 +117,9 @@ class FrogPilotPlanner:
def update_lead_status(self):
following_lead = self.lead_one.status
following_lead &= self.lead_one.dRel < self.model_length + STOP_DISTANCE
from frogpilot.common.frogpilot_variables import get_frogpilot_toggles
fp_toggles = get_frogpilot_toggles()
following_lead &= self.lead_one.dRel < self.model_length + fp_toggles.stop_distance
self.tracking_lead_filter.update(following_lead)
return self.tracking_lead_filter.x >= THRESHOLD
@@ -138,6 +142,8 @@ class FrogPilotPlanner:
frogpilotPlan.desiredFollowDistance = self.frogpilot_following.desired_follow_distance
frogpilotPlan.disableThrottle = self.frogpilot_following.disable_throttle
frogpilotPlan.experimentalMode = self.cem.experimental_mode or self.frogpilot_vcruise.slc.experimental_mode
frogpilotPlan.forcingStop = self.frogpilot_vcruise.forcing_stop
@@ -1,22 +1,66 @@
#!/usr/bin/env python3
import time
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import DT_MDL
from openpilot.common.numpy_fast import interp
from openpilot.common.conversions import Conversions as CV
from openpilot.frogpilot.common.frogpilot_variables import CITY_SPEED_LIMIT, CRUISING_SPEED, THRESHOLD, params_memory
from openpilot.frogpilot.common.frogpilot_variables import CITY_SPEED_LIMIT, CRUISING_SPEED, THRESHOLD, params_memory, scale_threshold
class ConditionalExperimentalMode:
# ===== CONDITIONAL EXPERIMENTAL MODE SPEED-BASED TUNING =====
# Speed ranges: [0-35, 35-55, 55-70, 70+ mph]
# FILTER TIME CONSTANTS (Lower = More responsive, Higher = Smoother)
# [City, Urban Hwy, Rural Hwy, High Speed]
FILTER_TIME_CURVES = [0.9, 0.8, 0.6, 0.5] # Faster detection at highway speeds
FILTER_TIME_LEADS = [0.9, 0.8, 0.7, 0.5] # Less sensitive at 70+ mph for slow leads
FILTER_TIME_LIGHTS = [0.9, 0.8, 0.75, 0.55] # Less sensitive at 60+ mph for stoplights
# HIGHWAY LIGHT DETECTION MULTIPLIERS
# How much to increase model stop time at highway speeds
LIGHT_BOOSTS = [1.0, 1.2, 1.1, 1.0] # Keep conservative boost for highest speeds
LIGHT_SPEED_LOW = 50 * CV.MPH_TO_MS # 50 mph threshold
LIGHT_SPEED_HIGH = 60 * CV.MPH_TO_MS # 60 mph threshold
LIGHT_MAX_TIME = 9 # Balanced max time preserving city performance
# ===== END TUNING PARAMETERS =====
# Current active values
FILTER_TIME_CURVE = 0.8
FILTER_TIME_LEAD = 0.8
FILTER_TIME_LIGHT = 0.8
LIGHT_BOOST_LOW = 1.15
LIGHT_BOOST_HIGH = 1.2
# Small latch to avoid frame-to-frame mode chatter.
CEM_TRANSITION_GUARD_TIME = 0.50
CEM_TRANSITION_BUFFER_TIME = 0.25
@staticmethod
def get_speed_based_param(speed_mph, param_array):
"""Get parameter value based on current speed using smooth interpolation between breakpoints [0, 35, 55, 70]"""
return interp(speed_mph, [0, 35, 55, 70], param_array)
def __init__(self, FrogPilotPlanner):
self.frogpilot_planner = FrogPilotPlanner
self.curvature_filter = FirstOrderFilter(0, 1, DT_MDL)
self.slow_lead_filter = FirstOrderFilter(0, 1, DT_MDL)
self.stop_light_filter = FirstOrderFilter(0, 0.5, DT_MDL)
# Faster filters with hysteresis for better responsiveness
self.curvature_filter = FirstOrderFilter(0, self.FILTER_TIME_CURVE, DT_MDL)
self.slow_lead_filter = FirstOrderFilter(0, self.FILTER_TIME_LEAD, DT_MDL)
self.stop_light_filter = FirstOrderFilter(0, self.FILTER_TIME_LIGHT, DT_MDL)
self.curve_detected = False
self.experimental_mode = False
self.stop_light_detected = False
self.prev_experimental_mode = False # For hysteresis
self.mode_hold_until = 0.0
self.mode_false_since = 0.0
def update(self, v_ego, sm, frogpilot_toggles):
now = time.monotonic()
if frogpilot_toggles.experimental_mode_via_press:
self.status_value = params_memory.get_int("CEStatus")
else:
@@ -25,10 +69,22 @@ class ConditionalExperimentalMode:
if self.status_value not in {1, 2} and not sm["carState"].standstill:
self.update_conditions(v_ego, sm, frogpilot_toggles)
self.experimental_mode = self.check_conditions(v_ego, sm, frogpilot_toggles)
triggered = self.check_conditions(v_ego, sm, frogpilot_toggles)
if triggered:
self.mode_hold_until = now + self.CEM_TRANSITION_GUARD_TIME
self.mode_false_since = 0.0
elif self.mode_false_since == 0.0:
self.mode_false_since = now
hold_active = now < self.mode_hold_until
transition_buffer_active = self.mode_false_since != 0.0 and (now - self.mode_false_since) < self.CEM_TRANSITION_BUFFER_TIME
self.experimental_mode = triggered or hold_active or transition_buffer_active
self.prev_experimental_mode = self.experimental_mode
params_memory.put_int("CEStatus", self.status_value if self.experimental_mode else 0)
else:
self.mode_hold_until = 0.0
self.mode_false_since = 0.0
self.experimental_mode = self.status_value == 2 or sm["carState"].standstill and self.experimental_mode and self.frogpilot_planner.model_stopped
self.stop_light_detected &= self.status_value not in {1, 2}
self.stop_light_filter.x = 0
@@ -55,7 +111,7 @@ class ConditionalExperimentalMode:
self.status_value = 8
return True
if frogpilot_toggles.conditional_lead and self.slow_lead_detected:
if frogpilot_toggles.conditional_lead and self.slow_lead_detected and v_ego <= 35.31:
self.status_value = 9 if self.frogpilot_planner.lead_one.vLead < 1 else 10
return True
@@ -71,30 +127,70 @@ class ConditionalExperimentalMode:
def update_conditions(self, v_ego, sm, frogpilot_toggles):
self.curve_detection(v_ego, frogpilot_toggles)
self.slow_lead(frogpilot_toggles)
self.slow_lead(frogpilot_toggles, v_ego)
self.stop_sign_and_light(v_ego, sm, frogpilot_toggles.conditional_model_stop_time)
def curve_detection(self, v_ego, frogpilot_toggles):
self.curvature_filter.update(self.frogpilot_planner.road_curvature_detected or self.frogpilot_planner.driving_in_curve)
self.curve_detected = self.curvature_filter.x >= THRESHOLD and v_ego > CRUISING_SPEED
def slow_lead(self, frogpilot_toggles):
def slow_lead(self, frogpilot_toggles, v_ego):
if self.frogpilot_planner.tracking_lead:
slower_lead = frogpilot_toggles.conditional_slower_lead and self.frogpilot_planner.frogpilot_following.slower_lead
stopped_lead = frogpilot_toggles.conditional_stopped_lead and self.frogpilot_planner.lead_one.vLead < 1
lead_threshold = scale_threshold(v_ego)
# Adjust threshold based on lead probability for vision-only accuracy
lead_prob = getattr(self.frogpilot_planner.lead_one, 'modelProb', 1.0)
adjusted_threshold = lead_threshold * (1.0 + 0.2 * (1.0 - lead_prob)) # Higher threshold for lower confidence
self.slow_lead_filter.update(slower_lead or stopped_lead)
self.slow_lead_detected = self.slow_lead_filter.x >= THRESHOLD
self.slow_lead_detected = self.slow_lead_filter.x >= adjusted_threshold
else:
self.slow_lead_filter.x = 0
self.slow_lead_detected = False
def stop_sign_and_light(self, v_ego, sm, model_time):
if not sm["frogpilotCarState"].trafficModeEnabled:
model_stopping = self.frogpilot_planner.model_length < v_ego * model_time
speed_mph = v_ego * CV.MS_TO_MPH # Convert m/s to mph
# Interp for smooth scaling in 35-45 mph
bp = [0, 35, 45]
low_filter_time = 0.0 # No filtering under 35 mph
tuned_filter_time_curves = self.FILTER_TIME_CURVES[1] # At 35-55 mph
tuned_filter_time_leads = self.FILTER_TIME_LEADS[1]
tuned_filter_time_lights = self.FILTER_TIME_LIGHTS[1]
low_boost = 1.0
tuned_boost = self.LIGHT_BOOSTS[1]
low_cap_factor = 0.0 # No cap under 35 mph
tuned_cap_factor = 1.0
filter_time_curves = interp(speed_mph, bp, [low_filter_time, low_filter_time, tuned_filter_time_curves])
filter_time_leads = interp(speed_mph, bp, [low_filter_time, low_filter_time, tuned_filter_time_leads])
filter_time_lights = interp(speed_mph, bp, [low_filter_time, low_filter_time, tuned_filter_time_lights])
light_boost = interp(speed_mph, bp, [low_boost, low_boost, tuned_boost])
cap_factor = interp(speed_mph, bp, [low_cap_factor, low_cap_factor, tuned_cap_factor])
# Update filter times with interp
self.curvature_filter = FirstOrderFilter(self.curvature_filter.x, filter_time_curves, DT_MDL)
self.slow_lead_filter = FirstOrderFilter(self.slow_lead_filter.x, filter_time_leads, DT_MDL)
self.stop_light_filter = FirstOrderFilter(self.stop_light_filter.x, filter_time_lights, DT_MDL)
# Disable stoplight detection at very high speeds to prevent false positives
if speed_mph > 75: # Disable above 75 mph
self.stop_light_filter.x = 0
self.stop_light_detected = False
return
# Adjust model time with interp boost and gradual cap
adjusted_model_time = model_time * light_boost
if cap_factor > 0:
adjusted_model_time = min(adjusted_model_time, self.LIGHT_MAX_TIME * cap_factor + model_time * (1 - cap_factor)) # Gradual cap
model_stopping = self.frogpilot_planner.model_length < v_ego * adjusted_model_time
self.stop_light_filter.update(self.frogpilot_planner.model_stopped or model_stopping)
self.stop_light_detected = self.stop_light_filter.x >= THRESHOLD and not self.frogpilot_planner.tracking_lead
self.stop_light_detected = self.stop_light_filter.x >= THRESHOLD**2 and not self.frogpilot_planner.tracking_lead
else:
self.stop_light_filter.x = 0
self.stop_light_detected = False
@@ -1,33 +1,100 @@
#!/usr/bin/env python3
import numpy as np
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import CRUISE_MIN_ACCEL
from openpilot.selfdrive.controls.lib.longitudinal_planner import ACCEL_MIN, get_max_accel
def cubic_interp(x, xp, fp):
"""Cubic interpolation using NumPy's native operations for speed."""
# Boundary conditions
if x <= xp[0]:
return fp[0]
elif x >= xp[-1]:
return fp[-1]
# Find interval
i = np.searchsorted(xp, x) - 1
i = max(0, min(i, len(xp)-2)) # clamp the index
# Normalized position
t = (x - xp[i]) / float(xp[i+1] - xp[i])
# Hermite cubic formula
return fp[i]*(1 - 3*t**2 + 2*t**3) + fp[i+1]*(3*t**2 - 2*t**3)
def akima_interp(x, xp, fp):
"""Akima-inspired interpolation with reduced overshoot characteristics."""
if x <= xp[0]:
return fp[0]
elif x >= xp[-1]:
return fp[-1]
i = np.searchsorted(xp, x) - 1
i = max(0, min(i, len(xp)-2)) # clamp the index
t = (x - xp[i]) / float(xp[i+1] - xp[i])
# Quintic polynomial to reduce overshoot
t2 = t*t
t4 = t2*t2
t3 = t2*t
return (fp[i]*(1 - 10*t3 + 15*t4 - 6*t3*t2)
+ fp[i+1]*(10*t3 - 15*t4 + 6*t3*t2))
from openpilot.selfdrive.controls.lib.longitudinal_planner import A_CRUISE_MIN, get_max_accel
from openpilot.frogpilot.common.frogpilot_variables import CITY_SPEED_LIMIT
A_CRUISE_MIN_ECO = CRUISE_MIN_ACCEL / 2
A_CRUISE_MIN_SPORT = CRUISE_MIN_ACCEL * 2
A_CRUISE_MIN_ECO = A_CRUISE_MIN / 2
A_CRUISE_MIN_SPORT = A_CRUISE_MIN * 2
# MPH = [0.0, 11, 22, 34, 45, 56, 89]
A_CRUISE_MAX_BP_CUSTOM = [0.0, 5., 10., 15., 20., 25., 40.]
A_CRUISE_MAX_VALS_ECO = [2.0, 1.5, 1.0, 0.8, 0.6, 0.4, 0.2]
A_CRUISE_MAX_VALS_SPORT = [3.0, 2.5, 2.0, 1.5, 1.0, 0.8, 0.6]
# MPH = [0.0, 11, 22, 34, 45, 56, 89]
A_CRUISE_MAX_BP_CUSTOM = [0.0, 5., 10., 15., 20., 25., 40.]
A_CRUISE_MAX_VALS_ECO_EV = [1.15, 1.15, 1.15, 1.15, 1.30, 1.30, 1.72]
A_CRUISE_MAX_VALS_STANDARD_EV = [1.25, 1.25, 1.25, 1.25, 1.45, 1.50, 2.00]
A_CRUISE_MAX_VALS_SPORT_EV = [1.35, 1.35, 1.35, 1.35, 1.60, 1.60, 2.10]
A_CRUISE_MAX_VALS_SPORT_PLUS_EV = [1.55, 1.55, 1.55, 1.55, 1.84, 1.84, 2.42]
A_CRUISE_MAX_VALS_ECO_GAS = [2.0, 1.5, 1.0, 0.8, 0.6, 0.4, 0.2]
A_CRUISE_MAX_VALS_SPORT_GAS = [3.0, 2.5, 2.0, 1.5, 1.0, 0.8, 0.6]
A_CRUISE_MAX_VALS_ECO_TRUCK = [3.00, 1.05, 0.60, 0.50, 0.50, 0.45, 0.35]
A_CRUISE_MAX_VALS_STANDARD_TRUCK = [6.00, 1.10, 0.70, 0.60, 0.55, 0.45, 0.35]
A_CRUISE_MAX_VALS_SPORT_TRUCK = [6.00, 1.15, 0.75, 0.70, 0.60, 0.50, 0.40]
A_CRUISE_MAX_VALS_SPORT_PLUS_TRUCK = [6.00, 1.30, 0.90, 0.80, 0.70, 0.60, 0.45]
def get_max_accel_eco(v_ego):
return float(np.interp(v_ego, A_CRUISE_MAX_BP_CUSTOM, A_CRUISE_MAX_VALS_ECO))
def get_max_accel_eco(v_ego, ev_tuning=True, truck_tuning=False):
if ev_tuning:
cruise_vals = A_CRUISE_MAX_VALS_ECO_EV
elif truck_tuning:
cruise_vals = A_CRUISE_MAX_VALS_ECO_TRUCK
else:
cruise_vals = A_CRUISE_MAX_VALS_ECO_GAS
return float(akima_interp(v_ego, A_CRUISE_MAX_BP_CUSTOM, cruise_vals))
def get_max_accel_sport(v_ego):
return float(np.interp(v_ego, A_CRUISE_MAX_BP_CUSTOM, A_CRUISE_MAX_VALS_SPORT))
def get_max_accel_sport(v_ego, ev_tuning=True, truck_tuning=False):
if ev_tuning:
cruise_vals = A_CRUISE_MAX_VALS_SPORT_EV
elif truck_tuning:
cruise_vals = A_CRUISE_MAX_VALS_SPORT_TRUCK
else:
cruise_vals = A_CRUISE_MAX_VALS_SPORT_GAS
return float(akima_interp(v_ego, A_CRUISE_MAX_BP_CUSTOM, cruise_vals))
def get_max_accel_standard(v_ego, ev_tuning=True, truck_tuning=False):
if ev_tuning:
return float(akima_interp(v_ego, A_CRUISE_MAX_BP_CUSTOM, A_CRUISE_MAX_VALS_STANDARD_EV))
if truck_tuning:
return float(akima_interp(v_ego, A_CRUISE_MAX_BP_CUSTOM, A_CRUISE_MAX_VALS_STANDARD_TRUCK))
return get_max_accel(v_ego)
def get_max_accel_low_speeds(max_accel, v_cruise):
return float(np.interp(v_cruise, [0., CITY_SPEED_LIMIT / 2, CITY_SPEED_LIMIT], [max_accel / 4, max_accel / 2, max_accel]))
return float(akima_interp(v_cruise, [0., CITY_SPEED_LIMIT / 2, CITY_SPEED_LIMIT], [max_accel / 4, max_accel / 2, max_accel]))
def get_max_accel_ramp_off(max_accel, v_cruise, v_ego):
return float(np.interp(v_cruise - v_ego, [0., 1., 5.], [0., 0.5, max_accel]))
return float(akima_interp(v_cruise - v_ego, [0., 1., 5., 10.], [0., 0.5, 1.0, max_accel]))
def get_max_allowed_accel(v_ego):
return float(np.interp(v_ego, [0., 5., 20.], [4.0, 4.0, 2.0])) # ISO 15622:2018
def get_max_allowed_accel(v_ego, ev_tuning=True, truck_tuning=False):
if ev_tuning:
return float(akima_interp(v_ego, A_CRUISE_MAX_BP_CUSTOM, A_CRUISE_MAX_VALS_SPORT_PLUS_EV))
if truck_tuning:
return float(akima_interp(v_ego, A_CRUISE_MAX_BP_CUSTOM, A_CRUISE_MAX_VALS_SPORT_PLUS_TRUCK))
return float(akima_interp(v_ego, [0., 5., 20.], [4.0, 4.0, 2.0])) # ISO 15622:2018
class FrogPilotAcceleration:
def __init__(self, FrogPilotPlanner):
@@ -39,34 +106,34 @@ class FrogPilotAcceleration:
def update(self, v_ego, sm, frogpilot_toggles):
eco_gear = sm["frogpilotCarState"].ecoGear
sport_gear = sm["frogpilotCarState"].sportGear
ev_tuning = frogpilot_toggles.ev_tuning
truck_tuning = frogpilot_toggles.truck_tuning
if sm["frogpilotCarState"].trafficModeEnabled:
self.max_accel = get_max_accel(v_ego)
self.max_accel = get_max_accel_standard(v_ego, ev_tuning, truck_tuning)
elif frogpilot_toggles.map_acceleration and (eco_gear or sport_gear):
if eco_gear:
self.max_accel = get_max_accel_eco(v_ego)
self.max_accel = get_max_accel_eco(v_ego, ev_tuning, truck_tuning)
else:
if frogpilot_toggles.acceleration_profile == 2:
self.max_accel = get_max_accel_sport(v_ego)
self.max_accel = get_max_accel_sport(v_ego, ev_tuning, truck_tuning)
else:
self.max_accel = get_max_allowed_accel(v_ego)
self.max_accel = get_max_allowed_accel(v_ego, ev_tuning, truck_tuning)
else:
if frogpilot_toggles.acceleration_profile == 1:
self.max_accel = get_max_accel_eco(v_ego)
self.max_accel = get_max_accel_eco(v_ego, ev_tuning, truck_tuning)
elif frogpilot_toggles.acceleration_profile == 2:
self.max_accel = get_max_accel_sport(v_ego)
self.max_accel = get_max_accel_sport(v_ego, ev_tuning, truck_tuning)
elif frogpilot_toggles.acceleration_profile == 3:
self.max_accel = get_max_allowed_accel(v_ego)
self.max_accel = get_max_allowed_accel(v_ego, ev_tuning, truck_tuning)
else:
self.max_accel = get_max_accel(v_ego)
self.max_accel = get_max_accel_standard(v_ego, ev_tuning, truck_tuning)
if frogpilot_toggles.human_acceleration:
self.max_accel = min(get_max_accel_low_speeds(self.max_accel, self.frogpilot_planner.v_cruise), self.max_accel)
self.max_accel = min(get_max_accel_ramp_off(self.max_accel, self.frogpilot_planner.v_cruise, v_ego), self.max_accel)
if self.frogpilot_planner.tracking_lead:
self.min_accel = ACCEL_MIN
elif sm["frogpilotCarState"].forceCoast:
if sm["frogpilotCarState"].forceCoast:
self.min_accel = A_CRUISE_MIN_ECO
elif frogpilot_toggles.map_deceleration and (eco_gear or sport_gear):
if eco_gear:
@@ -79,4 +146,4 @@ class FrogPilotAcceleration:
elif frogpilot_toggles.deceleration_profile == 2:
self.min_accel = A_CRUISE_MIN_SPORT
else:
self.min_accel = CRUISE_MIN_ACCEL
self.min_accel = A_CRUISE_MIN
+40 -13
View File
@@ -1,16 +1,20 @@
#!/usr/bin/env python3
import numpy as np
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import COMFORT_BRAKE, STOP_DISTANCE, desired_follow_distance, get_jerk_factor, get_T_FOLLOW
from openpilot.common.conversions import Conversions as CV
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import COMFORT_BRAKE, desired_follow_distance, get_jerk_factor, get_T_FOLLOW
from openpilot.frogpilot.common.frogpilot_variables import CITY_SPEED_LIMIT
TRAFFIC_MODE_BP = [0., CITY_SPEED_LIMIT]
PERSONALITY_BP = [45. * CV.MPH_TO_MS, 70. * CV.MPH_TO_MS]
HIGHWAY_DISABLE_THROTTLE_MIN_SPEED = 45. * CV.MPH_TO_MS
class FrogPilotFollowing:
def __init__(self, FrogPilotPlanner):
self.frogpilot_planner = FrogPilotPlanner
self.disable_throttle = False
self.following_lead = False
self.slower_lead = False
@@ -47,12 +51,16 @@ class FrogPilotFollowing:
frogpilot_toggles.custom_personalities, sm["controlsState"].personality
)
self.t_follow = get_T_FOLLOW(
t_follow_param = get_T_FOLLOW(
frogpilot_toggles.aggressive_follow,
frogpilot_toggles.standard_follow,
frogpilot_toggles.relaxed_follow,
frogpilot_toggles.custom_personalities, sm["controlsState"].personality
)
if isinstance(t_follow_param, list):
self.t_follow = float(np.interp(v_ego, PERSONALITY_BP, t_follow_param))
else:
self.t_follow = float(t_follow_param)
else:
self.base_acceleration_jerk = 0
self.base_danger_jerk = 0
@@ -63,7 +71,27 @@ class FrogPilotFollowing:
self.danger_jerk = self.base_danger_jerk
self.speed_jerk = self.base_speed_jerk
self.following_lead = self.frogpilot_planner.tracking_lead and self.frogpilot_planner.lead_one.dRel < (self.t_follow + 1) * v_ego
self.following_lead = self.frogpilot_planner.tracking_lead and self.frogpilot_planner.lead_one.dRel < (self.t_follow * 2) * v_ego
self.disable_throttle = False
if self.frogpilot_planner.tracking_lead and self.frogpilot_planner.lead_one.status:
lead_distance = self.frogpilot_planner.lead_one.dRel
v_lead = self.frogpilot_planner.lead_one.vLead
closing_speed = max(0.0, v_ego - v_lead)
desired_gap = float(desired_follow_distance(v_ego, v_lead, self.t_follow))
ttc = lead_distance / max(closing_speed, 1e-3) if closing_speed > 0.1 else 1e6
# Keep a mild coasting behavior only for far/low-risk slower leads.
coast_window_open = lead_distance > desired_gap + max(4.0, 0.2 * v_ego)
coast_window_far = lead_distance < desired_gap + max(25.0, 1.2 * v_ego)
gentle_closing = closing_speed < max(2.0, 0.12 * v_ego)
# Highway-only coast suppression: keep low-speed lead departures responsive.
self.disable_throttle = (not self.following_lead and v_ego > HIGHWAY_DISABLE_THROTTLE_MIN_SPEED and coast_window_open and
coast_window_far and gentle_closing)
# Never coast when we are entering a potentially late-braking scenario.
self.disable_throttle &= ttc > 6.0 and lead_distance > desired_gap + 6.0
if sm["controlsState"].enabled and self.frogpilot_planner.tracking_lead:
self.update_follow_values(self.frogpilot_planner.lead_one.dRel, v_ego, self.frogpilot_planner.lead_one.vLead, frogpilot_toggles)
@@ -75,21 +103,20 @@ class FrogPilotFollowing:
# Offset by FrogAi for FrogPilot for a more natural approach to a faster lead
if frogpilot_toggles.human_following and v_lead > v_ego:
distance_factor = max(lead_distance - (v_ego * self.t_follow), 1)
accelerating_offset = float(np.clip(STOP_DISTANCE - v_ego, 1, distance_factor))
self.acceleration_jerk /= accelerating_offset
self.speed_jerk /= accelerating_offset
self.t_follow /= accelerating_offset
from frogpilot.common.frogpilot_variables import get_frogpilot_toggles
fp_toggles = get_frogpilot_toggles()
acceleration_offset = float(np.clip(fp_toggles.stop_distance - v_ego, 1, distance_factor))
self.acceleration_jerk /= acceleration_offset
self.speed_jerk /= acceleration_offset
self.t_follow /= acceleration_offset
# Offset by FrogAi for FrogPilot for a more natural approach to a slower lead
if (frogpilot_toggles.conditional_slower_lead or frogpilot_toggles.human_following) and v_lead < v_ego:
distance_factor = max(lead_distance - (v_lead * self.t_follow), 1)
braking_offset = float(np.clip(min(v_ego - v_lead, v_lead) - COMFORT_BRAKE, 1, distance_factor))
if frogpilot_toggles.human_following:
if not self.following_lead and v_lead > CITY_SPEED_LIMIT:
far_lead_offset = max(lead_distance - (v_ego * self.t_follow) - STOP_DISTANCE, 0)
else:
far_lead_offset = 0
from frogpilot.common.frogpilot_variables import get_frogpilot_toggles
fp_toggles = get_frogpilot_toggles()
far_lead_offset = max(lead_distance - (v_ego * self.t_follow) - fp_toggles.stop_distance, 0)
self.t_follow /= braking_offset + far_lead_offset
self.slower_lead = braking_offset > 1
+1 -1
View File
@@ -51,7 +51,7 @@ class FrogPilotTracking:
self.sound = FrogPilotAudibleAlert.none
self.state = State.disabled
self.model_name = clean_model_name(dict(zip(frogpilot_toggles.available_models.split(","), frogpilot_toggles.available_model_names.split(",")))[frogpilot_toggles.model])
self.model_name = clean_model_name(frogpilot_toggles.model_name)
def update(self, now, time_validated, sm, frogpilot_toggles):
v_cruise = min(sm["controlsState"].vCruiseCluster, V_CRUISE_MAX) * CV.KPH_TO_MS
+2 -11
View File
@@ -18,6 +18,7 @@ class FrogPilotVCruise:
self.override_force_stop = False
self.override_force_stop_timer = 0
self.force_stop_timer = 0.0
def update(self, gps_position, now, time_validated, v_cruise, v_ego, sm, frogpilot_toggles):
force_stop = self.frogpilot_planner.cem.stop_light_detected and sm["controlsState"].enabled and frogpilot_toggles.force_stops
@@ -58,16 +59,6 @@ class FrogPilotVCruise:
self.csc_target = v_cruise
# Mike's extended lead linear braking
if self.frogpilot_planner.lead_one.vLead < v_ego > CRUISING_SPEED and sm["controlsState"].enabled and self.frogpilot_planner.tracking_lead and frogpilot_toggles.human_following:
if not self.frogpilot_planner.frogpilot_following.following_lead:
decel_rate = (v_ego - self.frogpilot_planner.lead_one.vLead)**2 / self.frogpilot_planner.lead_one.dRel
self.braking_target = max(v_ego - (decel_rate * DT_MDL), self.frogpilot_planner.lead_one.vLead + CRUISING_SPEED)
else:
self.braking_target = v_cruise
else:
self.braking_target = v_cruise
# Pfeiferj's Speed Limit Controller
self.slc.frogpilot_toggles = frogpilot_toggles
@@ -97,7 +88,7 @@ class FrogPilotVCruise:
self.tracked_model_length = self.frogpilot_planner.model_length
targets = [self.braking_target, self.csc_target, v_cruise]
targets = [self.csc_target, v_cruise]
if frogpilot_toggles.speed_limit_controller:
targets.append(max(self.slc.overridden_speed, self.slc_target + self.slc_offset) - v_ego_diff)
@@ -1,17 +1,20 @@
#!/usr/bin/env python3
# Twilsonco's Lateral Neural Network Feedforward
from collections import deque
from difflib import SequenceMatcher
from typing import NamedTuple
# Twilsonco's Lateral Neural Network Feedforward Controller
import json
import math
import numpy as np
import os
from collections import deque
from difflib import SequenceMatcher
from cereal import log
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.numpy_fast import interp
from openpilot.common.params import Params
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
from openpilot.selfdrive.controls.lib.pid import PIDController
from openpilot.selfdrive.controls.lib.vehicle_model import ACCELERATION_DUE_TO_GRAVITY
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.frogpilot.common.frogpilot_variables import NNFF_MODELS_PATH, get_nnff_model_files, params
@@ -31,8 +34,7 @@ from openpilot.frogpilot.common.frogpilot_variables import NNFF_MODELS_PATH, get
ACTIVATION_FUNCTION_NAMES = {'σ': 'sigmoid'}
LOW_SPEED_X = [0, 10, 20, 30]
LOW_SPEED_Y = [15, 13, 10, 5]
LOW_SPEED_Y_NN = [12, 3, 1, 0]
LOW_SPEED_Y = [12, 3, 1, 0]
LAT_PLAN_MIN_IDX = 5
@@ -126,7 +128,7 @@ def get_nn_model_path(car, eps_firmware) -> str | None:
def find_valid_model(*queries):
for query in queries:
path, score = best_model_path(query)
if path and car in path and score >= 0.9:
if path and score >= 0.9:
return path
return None
@@ -152,21 +154,18 @@ def sign(x):
def similarity(s1: str, s2: str) -> float:
return SequenceMatcher(None, s1, s2).ratio()
class LatControlInputs(NamedTuple):
lateral_acceleration: float
roll_compensation: float
vego: float
aego: float
class NeuralNetworkFeedforward:
def __init__(self, CP, LatControlTorque):
self.lat_control_torque = LatControlTorque
class LatControlNNFF(LatControl):
def __init__(self, CP, CI, dt):
super().__init__(CP, CI, dt)
self.lat_torque_nn_model = get_nn_model(CP.carFingerprint, str(next((fw.fwVersion for fw in CP.carFw if fw.ecu == "eps"), "")).replace("\\", ""))
self.nnff_loaded = self.lat_torque_nn_model is not None
self.torque_from_lateral_accel = self.lat_control_torque.torque_from_lateral_accel
self.use_steering_angle = self.lat_control_torque.torque_params.useSteeringAngle
self.torque_params = CP.lateralTuning.torque
self.pid = PIDController(self.torque_params.kp, self.torque_params.ki,
pos_limit=self.steer_max, neg_limit=-self.steer_max)
self.torque_from_lateral_accel = CI.torque_from_lateral_accel()
self.use_steering_angle = self.torque_params.useSteeringAngle
self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg
# Instantaneous lateral jerk changes very rapidly, making it not useful on its own,
# however, we can "look ahead" to the future planned lateral jerk in order to gauge
@@ -184,7 +183,6 @@ class NeuralNetworkFeedforward:
self.lat_jerk_friction_factor = 0.4
# precompute time differences between ModelConstants.T_IDXS
self.lateral_delay = CP.steerActuatorDelay
self.t_diffs = np.diff(ModelConstants.T_IDXS)
self.pitch = FirstOrderFilter(0.0, 0.5, 0.01)
@@ -196,7 +194,7 @@ class NeuralNetworkFeedforward:
# setup future time offsets
self.future_times = [0.3, 0.6, 1.0, 1.5] # seconds in the future
self.nn_future_times = [time + self.lateral_delay for time in self.future_times]
self.nn_future_times = [time + CP.steerActuatorDelay for time in self.future_times]
# setup past time offsets
self.past_times = [-0.3, -0.2, -0.1]
@@ -207,97 +205,150 @@ class NeuralNetworkFeedforward:
self.roll_deque = deque(maxlen=history_check_frames[0])
def update_live_delay(self, lateral_delay):
self.lateral_delay = lateral_delay
self.nn_future_times = [time + self.lateral_delay for time in self.future_times]
self.nn_future_times = [time + lateral_delay for time in self.future_times]
self.past_future_len = len(self.past_times) + len(self.nn_future_times)
def compute_nnff(self, CS, VM, actual_lateral_accel, desired_lateral_accel, gravity_adjusted_lateral_accel, lateral_accel_deadzone, llk, measurement, model_data, params, pid_log, roll_compensation, setpoint, frogpilot_toggles):
if self.use_steering_angle:
actual_curvature_rate = -VM.calc_curvature(math.radians(CS.steeringRateDeg), CS.vEgo, 0.0)
actual_lateral_jerk = actual_curvature_rate * CS.vEgo ** 2
def update_live_torque_params(self, latAccelFactor, latAccelOffset, friction):
self.torque_params.latAccelFactor = latAccelFactor
self.torque_params.latAccelOffset = latAccelOffset
self.torque_params.friction = friction
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, curvature_limited, lat_delay, llk, model_data, frogpilot_toggles):
pid_log = log.ControlsState.LateralTorqueState.new_message()
if not active:
output_torque = 0.0
pid_log.active = False
else:
actual_lateral_jerk = 0.0
actual_curvature_vm = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll)
roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY
if self.use_steering_angle:
actual_curvature = actual_curvature_vm
curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0))
else:
actual_curvature_llk = llk.angularVelocityCalibrated.value[2] / CS.vEgo
actual_curvature = interp(CS.vEgo, [2.0, 5.0], [actual_curvature_vm, actual_curvature_llk])
curvature_deadzone = 0.0
desired_lateral_accel = desired_curvature * CS.vEgo ** 2
model_good = model_data is not None and len(model_data.orientation.x) >= CONTROL_N
# desired rate is the desired rate of change in the setpoint, not the absolute desired curvature
# desired_lateral_jerk = desired_curvature_rate * CS.vEgo ** 2
actual_lateral_accel = actual_curvature * CS.vEgo ** 2
lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2
if model_good:
# prepare "look-ahead" desired lateral jerk
lookahead = interp(CS.vEgo, self.friction_look_ahead_bp, self.friction_look_ahead_v)
friction_upper_idx = next((idxs for idxs, value in enumerate(ModelConstants.T_IDXS) if value > lookahead), 16)
low_speed_factor = interp(CS.vEgo, LOW_SPEED_X, LOW_SPEED_Y)**2
setpoint = desired_lateral_accel + low_speed_factor * desired_curvature
measurement = actual_lateral_accel + low_speed_factor * actual_curvature
gravity_adjusted_lateral_accel = desired_lateral_accel - roll_compensation
if self.nnff_loaded and frogpilot_toggles.nnff or frogpilot_toggles.nnff_lite:
if self.use_steering_angle:
actual_curvature_rate = -VM.calc_curvature(math.radians(CS.steeringRateDeg), CS.vEgo, 0.0)
actual_lateral_jerk = actual_curvature_rate * CS.vEgo ** 2
else:
actual_lateral_jerk = 0.0
predicted_lateral_jerk = get_predicted_lateral_jerk(model_data.acceleration.y, self.t_diffs)
desired_lateral_jerk = (interp(self.lateral_delay, ModelConstants.T_IDXS, model_data.acceleration.y) - desired_lateral_accel) / self.lateral_delay
model_good = model_data is not None and len(model_data.orientation.x) >= CONTROL_N
lookahead_lateral_jerk = get_lookahead_value(predicted_lateral_jerk[LAT_PLAN_MIN_IDX:friction_upper_idx], desired_lateral_jerk)
if model_good:
# prepare "look-ahead" desired lateral jerk
lookahead = interp(CS.vEgo, self.friction_look_ahead_bp, self.friction_look_ahead_v)
friction_upper_idx = next((idxs for idxs, value in enumerate(ModelConstants.T_IDXS) if value > lookahead), 16)
if not self.use_steering_angle or lookahead_lateral_jerk == 0.0:
lookahead_lateral_jerk = 0.0
actual_lateral_jerk = 0.0
self.lat_accel_friction_factor = 1.0
predicted_lateral_jerk = get_predicted_lateral_jerk(model_data.acceleration.y, self.t_diffs)
desired_lateral_jerk = (interp(lat_delay, ModelConstants.T_IDXS, model_data.acceleration.y) - desired_lateral_accel) / lat_delay
lateral_jerk_setpoint = self.lat_jerk_friction_factor * lookahead_lateral_jerk
lateral_jerk_measurement = self.lat_jerk_friction_factor * actual_lateral_jerk
else:
lateral_jerk_setpoint = 0
lateral_jerk_measurement = 0
lookahead_lateral_jerk = 0
lookahead_lateral_jerk = get_lookahead_value(predicted_lateral_jerk[LAT_PLAN_MIN_IDX:friction_upper_idx], desired_lateral_jerk)
if self.lat_control_torque.nnff_loaded and model_good and frogpilot_toggles.nnff:
# update past data
pitch = 0
roll = params.roll
if len(llk.calibratedOrientationNED.value) > 1:
pitch = self.pitch.update(llk.calibratedOrientationNED.value[1])
roll = roll_pitch_adjust(roll, pitch)
if not self.use_steering_angle or lookahead_lateral_jerk == 0.0:
lookahead_lateral_jerk = 0.0
actual_lateral_jerk = 0.0
self.lat_accel_friction_factor = 1.0
self.roll_deque.append(roll)
self.lateral_accel_desired_deque.append(desired_lateral_accel)
lateral_jerk_setpoint = self.lat_jerk_friction_factor * lookahead_lateral_jerk
lateral_jerk_measurement = self.lat_jerk_friction_factor * actual_lateral_jerk
else:
lateral_jerk_setpoint = 0
lateral_jerk_measurement = 0
lookahead_lateral_jerk = 0
# prepare past and future values
# adjust future times to account for longitudinal acceleration
adjusted_future_times = [time + 0.5 * CS.aEgo * (time / max(CS.vEgo, 1.0)) for time in self.nn_future_times]
past_rolls = [self.roll_deque[min(len(self.roll_deque)-1, offset)] for offset in self.history_frame_offsets]
future_rolls = [roll_pitch_adjust(interp(time, ModelConstants.T_IDXS, model_data.orientation.x) + roll, interp(time, ModelConstants.T_IDXS, model_data.orientation.y) + pitch) for time in adjusted_future_times]
if self.nnff_loaded and model_good and frogpilot_toggles.nnff:
# update past data
pitch = 0
roll = params.roll
if len(llk.calibratedOrientationNED.value) > 1:
pitch = self.pitch.update(llk.calibratedOrientationNED.value[1])
roll = roll_pitch_adjust(roll, pitch)
past_lateral_accels_desired = [self.lateral_accel_desired_deque[min(len(self.lateral_accel_desired_deque)-1, offset)] for offset in self.history_frame_offsets]
future_lateral_accels = [interp(time, ModelConstants.T_IDXS[:CONTROL_N], model_data.acceleration.y) for time in adjusted_future_times]
self.roll_deque.append(roll)
self.lateral_accel_desired_deque.append(desired_lateral_accel)
base_input = [CS.vEgo, roll]
nnff_common = past_rolls + future_rolls
# prepare past and future values
# adjust future times to account for longitudinal acceleration
adjusted_future_times = [time + 0.5 * CS.aEgo * (time / max(CS.vEgo, 1.0)) for time in self.nn_future_times]
past_rolls = [self.roll_deque[min(len(self.roll_deque)-1, offset)] for offset in self.history_frame_offsets]
future_rolls = [roll_pitch_adjust(interp(time, ModelConstants.T_IDXS, model_data.orientation.x) + roll, interp(time, ModelConstants.T_IDXS, model_data.orientation.y) + pitch) for time in adjusted_future_times]
# compute NNFF error response
nnff_setpoint_input = base_input[:1] + [setpoint, lateral_jerk_setpoint] + base_input[1:] + [setpoint] * self.past_future_len + nnff_common
nnff_measurement_input = base_input[:1] + [measurement, lateral_jerk_measurement] + base_input[1:] + [measurement] * self.past_future_len + nnff_common
past_lateral_accels_desired = [self.lateral_accel_desired_deque[min(len(self.lateral_accel_desired_deque)-1, offset)] for offset in self.history_frame_offsets]
future_lateral_accels = [interp(time, ModelConstants.T_IDXS[:CONTROL_N], model_data.acceleration.y) for time in adjusted_future_times]
torque_from_setpoint = self.lat_torque_nn_model.evaluate(nnff_setpoint_input)
torque_from_measurement = self.lat_torque_nn_model.evaluate(nnff_measurement_input)
base_input = [CS.vEgo, roll]
nnff_common = past_rolls + future_rolls
pid_log.error = torque_from_setpoint - torque_from_measurement
# compute NNFF error response
nnff_setpoint_input = base_input[:1] + [setpoint, lateral_jerk_setpoint] + base_input[1:] + [setpoint] * self.past_future_len + nnff_common
nnff_measurement_input = base_input[:1] + [measurement, lateral_jerk_measurement] + base_input[1:] + [measurement] * self.past_future_len + nnff_common
error_blend = interp(abs(desired_lateral_accel), [1.0, 2.0], [0.0, 1.0])
if error_blend > 0.0: # blend in stronger error response when in high lat accel
torque_from_error = self.lat_torque_nn_model.evaluate([CS.vEgo, setpoint - measurement, lateral_jerk_setpoint - lateral_jerk_measurement, 0.0])
if sign(pid_log.error) == sign(torque_from_error) and abs(pid_log.error) < abs(torque_from_error):
pid_log.error = pid_log.error * (1.0 - error_blend) + torque_from_error * error_blend
torque_from_setpoint = self.lat_torque_nn_model.evaluate(nnff_setpoint_input)
torque_from_measurement = self.lat_torque_nn_model.evaluate(nnff_measurement_input)
# compute feedforward (same as nn setpoint output)
error = setpoint - measurement
friction_input = self.lat_accel_friction_factor * error + self.lat_jerk_friction_factor * lookahead_lateral_jerk
nn_input = [CS.vEgo, desired_lateral_accel, friction_input, roll] + past_lateral_accels_desired + future_lateral_accels + nnff_common
ff = self.lat_torque_nn_model.evaluate(nn_input)
pid_log.error = torque_from_setpoint - torque_from_measurement
# apply friction override for cars with low NN friction response
if self.nn_friction_override:
pid_log.error += self.torque_from_lateral_accel(0.0, self.lat_control_torque.torque_params)
else:
torque_from_measurement = self.torque_from_lateral_accel(measurement, self.lat_control_torque.torque_params)
torque_from_setpoint = self.torque_from_lateral_accel(setpoint, self.lat_control_torque.torque_params)
error_blend = interp(abs(desired_lateral_accel), [1.0, 2.0], [0.0, 1.0])
if error_blend > 0.0: # blend in stronger error response when in high lat accel
torque_from_error = self.lat_torque_nn_model.evaluate([CS.vEgo, setpoint - measurement, lateral_jerk_setpoint - lateral_jerk_measurement, 0.0])
if sign(pid_log.error) == sign(torque_from_error) and abs(pid_log.error) < abs(torque_from_error):
pid_log.error = pid_log.error * (1.0 - error_blend) + torque_from_error * error_blend
pid_log.error = float(torque_from_setpoint - torque_from_measurement)
# compute feedforward (same as nn setpoint output)
error = setpoint - measurement
friction_input = self.lat_accel_friction_factor * error + self.lat_jerk_friction_factor * lookahead_lateral_jerk
nn_input = [CS.vEgo, desired_lateral_accel, friction_input, roll] + past_lateral_accels_desired + future_lateral_accels + nnff_common
ff = self.lat_torque_nn_model.evaluate(nn_input)
error = desired_lateral_accel - actual_lateral_accel
friction_input = self.lat_accel_friction_factor * error + self.lat_jerk_friction_factor * lookahead_lateral_jerk
ff = self.torque_from_lateral_accel(gravity_adjusted_lateral_accel, self.lat_control_torque.torque_params)
# apply friction override for cars with low NN friction response
if self.nn_friction_override:
pid_log.error += self.torque_from_lateral_accel(0.0, self.torque_params)
else:
torque_from_measurement = self.torque_from_lateral_accel(measurement, self.torque_params)
torque_from_setpoint = self.torque_from_lateral_accel(setpoint, self.torque_params)
return pid_log, ff
pid_log.error = float(torque_from_setpoint - torque_from_measurement)
error = desired_lateral_accel - actual_lateral_accel
friction_input = self.lat_accel_friction_factor * error + self.lat_jerk_friction_factor * lookahead_lateral_jerk
ff = self.torque_from_lateral_accel(gravity_adjusted_lateral_accel, self.torque_params)
else:
torque_from_measurement = self.torque_from_lateral_accel(measurement, self.torque_params)
torque_from_setpoint = self.torque_from_lateral_accel(setpoint, self.torque_params)
pid_log.error = float(torque_from_setpoint - torque_from_measurement)
ff = self.torque_from_lateral_accel(gravity_adjusted_lateral_accel, self.torque_params)
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
output_torque = self.pid.update(pid_log.error,
feedforward=ff,
speed=CS.vEgo,
freeze_integrator=freeze_integrator)
pid_log.active = True
pid_log.p = float(self.pid.p)
pid_log.i = float(self.pid.i)
pid_log.d = float(self.pid.d)
pid_log.f = float(self.pid.f)
pid_log.output = float(-output_torque)
pid_log.actualLateralAccel = float(actual_lateral_accel)
pid_log.desiredLateralAccel = float(desired_lateral_accel)
pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_safety, curvature_limited))
# TODO left is positive in this convention
return -output_torque, 0.0, pid_log
+1 -1
View File
@@ -17,7 +17,7 @@ from openpilot.frogpilot.common.frogpilot_variables import MAPD_PATH, RESOURCES_
VERSION = "v2"
GITHUB_VERSION_URL = f"https://github.com/{RESOURCES_REPO}/raw/Versions/mapd_version_{VERSION}.json"
GITLAB_VERSION_URL = f"https://gitlab.com/{RESOURCES_REPO}/-/raw/Versions/mapd_version_{VERSION}.json"
GITLAB_VERSION_URL = f"https://gitlab.com/firestar5683/FrogPilot-Resources/-/raw/Versions/mapd_version_{VERSION}.json"
VERSION_PATH = Path("/data/media/0/osm/mapd_version")
Binary file not shown.
Binary file not shown.
+66 -27
View File
@@ -20,6 +20,30 @@ from openpilot.frogpilot.common.frogpilot_variables import get_frogpilot_toggles
BASE_URL = "https://nominatim.openstreetmap.org"
MINIMUM_POPULATION = 100_000
SEARCH_RADIUS_DEGREES = 1.45
def get_population_value(population_str):
if population_str is None:
return None
try:
return int(str(population_str).replace(",", "").split(";")[0].strip())
except Exception:
return None
def search_nearby_major_cities(lat, lon, session, state_name, country_name):
viewbox = f"{lon - SEARCH_RADIUS_DEGREES},{lat + SEARCH_RADIUS_DEGREES},{lon + SEARCH_RADIUS_DEGREES},{lat - SEARCH_RADIUS_DEGREES}"
cities = (session.get(f"{BASE_URL}/search", params={
"addressdetails": 1, "bounded": 1, "extratags": 1, "format": "jsonv2", "limit": 20, "q": "city", "viewbox": viewbox
}, timeout=10).json() or [])
qualifying = [c for c in cities if (get_population_value((c.get("extratags") or {}).get("population")) or 0) >= MINIMUM_POPULATION]
if not qualifying:
return None
nearest = min(qualifying, key=lambda c: (float(c["lat"]) - lat) ** 2 + (float(c["lon"]) - lon) ** 2)
addr = nearest.get("address") or {}
return float(nearest["lat"]), float(nearest["lon"]), addr.get("city") or addr.get("town") or nearest.get("display_name", "").split(",")[0], state_name, country_name
def get_city_center(latitude, longitude):
try:
@@ -44,14 +68,7 @@ def get_city_center(latitude, longitude):
if data:
tags = data[0]
population = (tags.get("extratags") or {}).get("population")
population_value = None
if population is not None:
try:
population_value = int(str(population).replace(",", "").split(";")[0].strip())
except Exception:
population_value = None
population_value = get_population_value((tags.get("extratags") or {}).get("population"))
if population_value is not None and population_value >= MINIMUM_POPULATION:
latitude_value = float(tags["lat"])
@@ -62,6 +79,10 @@ def get_city_center(latitude, longitude):
return latitude_value, longitude_value, city_label, state_name, country_name
nearby_result = search_nearby_major_cities(latitude, longitude, session, state_name, country_name)
if nearby_result:
return nearby_result
query = f"{state_name} state capital" if country_code == "us" else f"capital of {state_name}, {country_name}"
response = session.get(f"{BASE_URL}/search", params={"addressdetails": 1, "extratags": 1, "format": "jsonv2", "limit": 5, "q": query}, timeout=10)
response.raise_for_status()
@@ -97,6 +118,19 @@ def get_city_center(latitude, longitude):
print(f"Falling back to (0, 0) for {latitude}, {longitude}")
return float(0.0), float(0.0), "N/A", "N/A", "N/A"
def update_branch_commits(now):
points = []
branch = get_build_metadata().channel # Current running branch
try:
response = requests.get(f"https://api.github.com/repos/firestar5683/StarPilot/commits/{branch}")
response.raise_for_status()
sha = response.json()["sha"]
points.append(Point("branch_commits").field("commit", sha).tag("branch", branch).time(now))
except Exception as e:
print(f"Failed to fetch commit for {branch}: {e}")
return points
def is_up_to_date(build_metadata):
remote_commit = run_cmd(["git", "ls-remote", "origin", build_metadata.channel], f"Fetched remote commit", "Failed to fetch remote commit", report=False)
@@ -116,11 +150,9 @@ def send_stats():
if frogpilot_toggles.car_make == "mock":
return
bucket = os.environ.get("STATS_BUCKET", "")
org_ID = os.environ.get("STATS_ORG_ID", "")
token = os.environ.get("STATS_TOKEN", "")
url = os.environ.get("STATS_URL", "")
bucket = "StarPilot"
org_ID = "StarPilot"
url = "https://stats.firestar.link"
frogpilot_stats = json.loads(params.get("FrogPilotStats") or "{}")
location = json.loads(params.get("LastGPSPosition") or "{}")
@@ -144,15 +176,23 @@ def send_stats():
selected_theme = random.choice([item for item, count in most_common if count == max_count]).replace("-user_created", "").replace("_", " ")
point = (Point("user_stats")
now = datetime.now(timezone.utc)
user_point = (
Point("user_stats")
.tag("car_make", "GM" if frogpilot_toggles.car_make == "gm" else frogpilot_toggles.car_make.title())
.tag("car_model", frogpilot_toggles.car_model)
.tag("city", city)
.tag("country", country)
.tag("device", HARDWARE.get_device_type())
.tag("driving_model", clean_model_name(frogpilot_toggles.model_name))
.tag("state", state)
.tag("theme", selected_theme.title())
.tag("branch", build_metadata.channel)
.tag("dongle_id", params.get("FrogPilotDongleId", encoding="utf-8"))
.field("blocked_user", frogpilot_toggles.block_user)
.field("car_make", "GM" if frogpilot_toggles.car_make == "gm" else frogpilot_toggles.car_make.title())
.field("car_model", frogpilot_toggles.car_model)
.field("city", city)
.field("country", country)
.field("current_months_kilometers", int(frogpilot_stats.get("CurrentMonthsKilometers", 0)))
.field("device", HARDWARE.get_device_type())
.field("driving_model", clean_model_name(frogpilot_toggles.model_name))
.field("event", 1)
.field("frogpilot_drives", int(frogpilot_stats.get("FrogPilotDrives", 0)))
.field("frogpilot_hours", float(frogpilot_stats.get("FrogPilotSeconds", 0)) / (60 * 60))
@@ -162,13 +202,12 @@ def send_stats():
.field("has_openpilot_longitudinal", frogpilot_toggles.openpilot_longitudinal)
.field("has_pedal", frogpilot_toggles.has_pedal)
.field("has_sdsu", frogpilot_toggles.has_sdsu)
.field("has_sascm", frogpilot_toggles.has_sascm)
.field("has_zss", frogpilot_toggles.has_zss)
.field("latitude", latitude)
.field("longitude", longitude)
.field("rainbow_path", frogpilot_toggles.rainbow_path)
.field("random_events", frogpilot_toggles.random_events)
.field("state", state)
.field("theme", selected_theme.title())
.field("total_aol_seconds", float(frogpilot_stats.get("AOLTime", 0)))
.field("total_lateral_seconds", float(frogpilot_stats.get("LateralTime", 0)))
.field("total_longitudinal_seconds", float(frogpilot_stats.get("LongitudinalTime", 0)))
@@ -177,13 +216,13 @@ def send_stats():
.field("up_to_date", is_up_to_date(build_metadata))
.field("using_stock_acc", not (frogpilot_toggles.has_cc_long or frogpilot_toggles.openpilot_longitudinal))
.tag("branch", build_metadata.channel)
.tag("dongle_id", params.get("FrogPilotDongleId", encoding="utf-8"))
.time(datetime.now(timezone.utc))
.time(now)
)
InfluxDBClient(org=org_ID, token=token, url=url).write_api(write_options=SYNCHRONOUS).write(bucket=bucket, org=org_ID, record=point)
all_points = [user_point] + update_branch_commits(now)
client = InfluxDBClient(org=org_ID, token=org_ID, url=url)
client.write_api(write_options=SYNCHRONOUS).write(bucket=bucket, org=org_ID, record=all_points)
print("Successfully sent FrogPilot stats!")
except Exception as exception:
print(f"Failed to send FrogPilot stats: {exception}")
+215
View File
@@ -0,0 +1,215 @@
#!/usr/bin/env python3
import hmac
import json
import platform
import shutil
import signal
import subprocess
import tarfile
import time
import threading
import urllib.request
from http.server import HTTPServer, BaseHTTPRequestHandler
from pathlib import Path
from openpilot.common.params import Params
GALAXY_DIR = Path("/data/galaxy")
FRPC_VERSION = "0.67.0"
FRPC_LOG = GALAXY_DIR / "frpc.log"
AUTH_PORT = 8083
process = None
auth_server = None
VALID_PATHS = {"/glxylogin": "glxyauth", "/glxyverify": "glxysession"}
class AuthHandler(BaseHTTPRequestHandler):
"""Validates login hashes and verifies session tokens."""
dongle_id = ""
def do_POST(self):
target = VALID_PATHS.get(self.path)
if not target:
self.send_response(404)
self.end_headers()
return
file_path = GALAXY_DIR / target
if not file_path.exists():
self.send_response(404)
self.end_headers()
return
try:
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length).decode("utf-8").strip()
if hmac.compare_digest(body, file_path.read_text().strip()):
self.send_response(200)
if self.path == "/glxylogin":
self.send_header("Content-Type", "application/json")
self.end_headers()
token = (GALAXY_DIR / "glxysession").read_text().strip()
self.wfile.write(json.dumps({"dongle_id": self.dongle_id, "token": token}).encode())
else:
self.end_headers()
return
except Exception:
pass
self.send_response(403)
self.end_headers()
def log_message(self, format, *args):
pass
def start_auth_server():
global auth_server
if auth_server is not None:
return
auth_server = HTTPServer(("127.0.0.1", AUTH_PORT), AuthHandler)
thread = threading.Thread(target=auth_server.serve_forever, daemon=True)
thread.start()
print(f"Galaxy: Auth server listening on 127.0.0.1:{AUTH_PORT}")
def cleanup_frpc(*_):
global process
if process is not None and process.poll() is None:
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process = None
def get_arch_url():
arch = platform.machine()
if arch in ("aarch64", "arm64"):
return f"https://github.com/fatedier/frp/releases/download/v{FRPC_VERSION}/frp_{FRPC_VERSION}_linux_arm64.tar.gz", f"frp_{FRPC_VERSION}_linux_arm64"
elif arch in ("x86_64", "amd64"):
return f"https://github.com/fatedier/frp/releases/download/v{FRPC_VERSION}/frp_{FRPC_VERSION}_linux_amd64.tar.gz", f"frp_{FRPC_VERSION}_linux_amd64"
return None, None
def setup_frpc():
GALAXY_DIR.mkdir(parents=True, exist_ok=True)
frpc_bin = GALAXY_DIR / "frpc"
if not frpc_bin.exists():
print("Galaxy: Downloading frpc...")
url, folder_name = get_arch_url()
if not url:
print("Galaxy: Unsupported architecture")
return False
tar_path = GALAXY_DIR / "frp.tar.gz"
try:
urllib.request.urlretrieve(url, tar_path)
with tarfile.open(tar_path, "r:gz") as tar:
tar.extractall(path=GALAXY_DIR, filter='data')
# Move binary
extracted_bin = GALAXY_DIR / folder_name / "frpc"
extracted_bin.rename(frpc_bin)
frpc_bin.chmod(0o755)
# Cleanup
tar_path.unlink()
shutil.rmtree(GALAXY_DIR / folder_name)
print("Galaxy: frpc downloaded and installed.")
except Exception as e:
print(f"Galaxy: Failed to install frpc: {e}")
return False
return True
def main():
global process
params = Params()
signal.signal(signal.SIGTERM, cleanup_frpc)
signal.signal(signal.SIGINT, cleanup_frpc)
dongle_id = params.get("DongleId", encoding='utf8')
while not dongle_id:
print("Galaxy: Waiting for DongleId...")
time.sleep(5)
dongle_id = params.get("DongleId", encoding='utf8')
print(f"Galaxy: DongleId: {dongle_id}")
AuthHandler.dongle_id = dongle_id
print("Galaxy: Starting manager loop...")
last_slug = None
while True:
glxyauth_file = GALAXY_DIR / "glxyauth"
galaxy_password_hash = glxyauth_file.read_text().strip() if glxyauth_file.exists() else None
slug_file = GALAXY_DIR / "glxyslug"
slug = slug_file.read_text().strip() if slug_file.exists() else None
is_paired = galaxy_password_hash and len(galaxy_password_hash) == 64 and slug
if is_paired:
if process is None or process.poll() is not None or slug != last_slug:
cleanup_frpc()
if process is not None and slug == last_slug:
print(f"Galaxy: frpc exited with code {process.returncode}. Restarting...")
print("Galaxy: Password set. Preparing frpc tunnel...")
if not setup_frpc():
print("Galaxy: FRPC setup failed. Retrying later...")
time.sleep(10)
continue
start_auth_server()
frpc_toml = GALAXY_DIR / "frpc.toml"
config = f"""\
serverAddr = "galaxy.firestar.link"
serverPort = 7000
[transport]
tls.enable = true
poolCount = 2
[[proxies]]
name = "{slug}_pond"
type = "http"
localIP = "127.0.0.1"
localPort = 8082
customDomains = ["{slug}.devices.local"]
transport.useCompression = true
[[proxies]]
name = "{slug}_auth"
type = "http"
localIP = "127.0.0.1"
localPort = {AUTH_PORT}
customDomains = ["auth-{slug}.devices.local"]
"""
frpc_toml.write_text(config)
print(f"Galaxy: Starting frpc tunnel (slug: {slug[:4]}...)...")
log_file = open(FRPC_LOG, 'a')
process = subprocess.Popen(
[str(GALAXY_DIR / "frpc"), "-c", str(frpc_toml)],
stdout=log_file,
stderr=log_file
)
last_slug = slug
else:
if process is not None and process.poll() is None:
print("Galaxy: Password cleared. Stopping frpc tunnel...")
cleanup_frpc()
last_slug = None
time.sleep(3)
if __name__ == "__main__":
main()
@@ -6,7 +6,7 @@
}
.disk .progress {
background: linear-gradient(to right, green 0%, yellow 80%, orange 90%, red 100%);
background: linear-gradient(to right, #5ec8c8 0%, #8b6cc5 60%, #e05577 85%, #c04466 100%);
border-radius: var(--border-radius-md);
height: var(--padding-base);
overflow: hidden;
@@ -15,7 +15,6 @@
.diskUsage,
.drivingStats,
.firehoseStats,
.softwareInfo {
background-color: var(--secondary-bg);
border-radius: var(--border-radius-md);
@@ -31,12 +30,17 @@
}
.diskUsage p,
.firehoseStats p,
.softwareGrid p {
font-size: var(--font-size-sm);
margin: 0;
}
.softwareGrid p {
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.drivingStat {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
@@ -65,7 +69,6 @@
.diskUsage:hover,
.drivingStats:hover,
.firehoseStats:hover,
.softwareInfo:hover {
box-shadow: var(--shadow-md);
transform: var(--hover-scale-sm);
@@ -1,148 +1,152 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core";
import { html } from "/assets/vendor/arrow-core.js";
function DiskUsage(disk) {
const used = parseFloat(disk.usedPercentage) || 0;
const rightRadius = used >= 100 ? "0" : "var(--border-radius-md)";
const HOME_STATE = {
status: "loading", // loading | ready | error
data: null,
unit: "miles",
error: "",
initialized: false,
};
return html`
<div class="disk">
<p>${disk.used} used of ${disk.size}</p>
<div class="progress">
<div
class="bar"
style="
border-bottom-right-radius: ${rightRadius};
border-top-right-radius: ${rightRadius};
width: ${100 - used}%;
"
></div>
function withTimeout(promise, timeoutMs, label) {
return new Promise((resolve, reject) => {
const timerId = setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs);
promise.then((value) => {
clearTimeout(timerId);
resolve(value);
}).catch((err) => {
clearTimeout(timerId);
reject(err);
});
});
}
function formatInt(value) {
const n = Number(value || 0);
return Number.isFinite(n) ? n.toLocaleString("en-US", { maximumFractionDigits: 0 }) : "0";
}
function renderDiskUsageSection(state) {
const { data } = state;
const shell = document.getElementById("home_shell");
if (!shell) return;
if (state.status === "error") {
shell.innerHTML = `<p class="error">Failed to load data: ${state.error}</p>`;
return;
}
if (state.status !== "ready" || !data) {
shell.innerHTML = "<p>Loading...</p>";
return;
}
const driveStats = data.driveStats || {};
const softwareInfo = data.softwareInfo || {};
const diskUsage = Array.isArray(data.diskUsage) ? data.diskUsage : [];
const diskError = Array.isArray(data.diskError) ? data.diskError : [];
const statBlock = (title, stats = {}) => `
<div class="drivingStat">
<h2>${title}</h2>
<div><p>${formatInt(stats.drives)}</p><p>drives</p></div>
<div><p>${formatInt(stats.distance)}</p><p>${stats.unit || state.unit}</p></div>
<div><p>${formatInt(stats.hours)}</p><p>hours</p></div>
</div>
`;
const diskBlock = (disk = {}) => {
const usedPct = Number.parseFloat(disk.usedPercentage) || 0;
const rightRadius = usedPct >= 100 ? "0" : "var(--border-radius-md)";
return `
<div class="disk">
<p>${disk.used || "0 GB"} used of ${disk.size || "0 GB"}</p>
<div class="progress">
<div
class="bar"
style="
border-bottom-right-radius: ${rightRadius};
border-top-right-radius: ${rightRadius};
width: ${Math.max(0, 100 - usedPct)}%;
"
></div>
</div>
</div>
`;
};
const softwareFields = [
["Branch Name", softwareInfo.branchName],
["Build", softwareInfo.buildEnvironment],
["Commit Hash", softwareInfo.commitHash],
["Fork Maintainer", softwareInfo.forkMaintainer],
["Update Available", softwareInfo.updateAvailable],
["Version Date", softwareInfo.versionDate],
];
const softwareMarkup = softwareFields
.map(([label, value]) => `<p><strong>${label}:</strong> ${value ?? "Unknown"}</p>`)
.join("");
const diskMarkup = diskError.length
? `<p>${diskError.join("<br>")}</p>`
: (diskUsage.length ? diskUsage.map(diskBlock).join("") : diskBlock({}));
shell.innerHTML = `
<div>
<h1>Galaxy</h1>
<div class="drivingStats">
${statBlock("All Time", driveStats.all)}
${statBlock("Past Week", driveStats.week)}
${statBlock("FrogPilot", driveStats.frogpilot)}
</div>
<h2>Disk Usage</h2>
<div class="diskUsage">
${diskMarkup}
</div>
<h2>Software Info</h2>
<div class="softwareInfo">
<div class="softwareGrid">${softwareMarkup}</div>
</div>
</div>
`;
}
function DriveStat(title, stats = {}, defaultUnit) {
const format = (n) =>
n?.toLocaleString("en-US", {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}) ?? "0";
async function initializeHome() {
try {
const [statsResponse, unitResponse] = await Promise.all([
withTimeout(fetch("/api/stats"), 5000, "stats request"),
withTimeout(fetch("/api/params?key=IsMetric"), 5000, "metric request"),
]);
return html`
<div class="drivingStat">
<h2>${title}</h2>
<div><p>${format(stats.drives)}</p><p>drives</p></div>
<div><p>${format(stats.distance)}</p><p>${stats.unit ?? defaultUnit}</p></div>
<div><p>${format(stats.hours)}</p><p>hours</p></div>
</div>
`;
}
if (!statsResponse.ok) throw new Error(`stats API error: ${statsResponse.status}`);
if (!unitResponse.ok) throw new Error(`params API error: ${unitResponse.status}`);
function renderSoftwareInfo(info = {}) {
const fields = [
["Branch Name", info.branchName],
["Build", info.buildEnvironment],
["Commit Hash", info.commitHash],
["Fork Maintainer", info.forkMaintainer],
["Update Available", info.updateAvailable],
["Version Date", info.versionDate],
];
const statsJson = await withTimeout(statsResponse.json(), 5000, "stats JSON parse");
const isMetricText = (await withTimeout(unitResponse.text(), 5000, "metric read")).trim();
return fields.map(
([label, value]) =>
html`<p><strong>${label}:</strong> ${value ?? "Unknown"}</p>`
);
}
function renderDiskUsageSection({ diskError, diskUsage }) {
if (diskError) {
return html`<p>${diskError.join("<br>")}</p>`;
HOME_STATE.data = statsJson;
HOME_STATE.unit = isMetricText === "1" ? "kilometers" : "miles";
HOME_STATE.status = "ready";
} catch (err) {
HOME_STATE.status = "error";
HOME_STATE.error = err?.message || String(err);
}
if (diskUsage?.length) {
return diskUsage.map(DiskUsage);
}
return DiskUsage({ size: "0 GB", used: "0 GB", usedPercentage: "0" });
renderDiskUsageSection(HOME_STATE);
}
export function Home() {
const state = reactive({
data: null,
unit: "miles",
isLoading: true,
error: null,
});
async function initialize() {
try {
const [statsResponse, unitResponse] = await Promise.all([
fetch("/api/stats"),
fetch("/api/params?key=IsMetric"),
]);
if (!statsResponse.ok) throw new Error(`API error: ${statsResponse.statusText}`);
if (!unitResponse.ok) throw new Error(`API error: ${unitResponse.statusText}`);
const statsJson = await statsResponse.json();
const isMetricText = (await unitResponse.text()).trim();
const isMetric = isMetricText === "1";
state.data = statsJson;
state.unit = isMetric ? "kilometers" : "miles";
localStorage.setItem("isMetric", isMetricText);
} catch (err) {
console.error("Failed to initialize component:", err);
state.error = err.message;
} finally {
state.isLoading = false;
setTimeout(() => {
renderDiskUsageSection(HOME_STATE);
if (!HOME_STATE.initialized) {
HOME_STATE.initialized = true;
initializeHome();
}
}
}, 0);
initialize();
return html`
<div>
${() => {
if (state.isLoading) {
return html`<p>Loading...</p>`;
}
if (state.error) {
return html`<p class="error">Failed to load data: ${state.error}</p>`;
}
if (state.data) {
const { driveStats, firehoseStats, softwareInfo } = state.data;
return html`
<h1>The Pond</h1>
<div class="drivingStats">
${DriveStat("All Time", driveStats?.all, state.unit)}
${DriveStat("Past Week", driveStats?.week, state.unit)}
${DriveStat("FrogPilot", driveStats?.frogpilot, state.unit)}
</div>
<h2>Disk Usage</h2>
<div class="diskUsage">
${renderDiskUsageSection(state.data)}
</div>
<h2>Firehose Segments</h2>
<div class="firehoseStats">
<p>
<strong>${(firehoseStats?.segments ?? 0).toLocaleString("en-US")}</strong>
segments in training data.
</p>
</div>
<h2>Software Info</h2>
<div class="softwareInfo">
<div class="softwareGrid">${renderSoftwareInfo(softwareInfo)}</div>
</div>
`;
}
return html`<p>No data available.</p>`;
}}
</div>
`;
return html`<div id="home_shell"><p>Loading...</p></div>`;
}
@@ -20,47 +20,49 @@
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
/* Colors */
--accent-bg: #673ab7;
--accent-hover-bg: #512da8;
--card-bg: #234423;
/* Colors — Galaxy palette (cosmic purple · teal · rose · amber) */
--accent-bg: #8b6cc5;
--accent-hover-bg: #7558b0;
--card-bg: #121224;
--color-black: #000000;
--color-confirm: #1a73e8;
--color-confirm-hover: #0758ad;
--color-gray-100: #f5f5f5;
--color-gray-200: #e0e0e0;
--color-gray-300: #c2c2c2;
--color-gray-400: #a4a4a4;
--color-gray-500: #8f8f8f;
--color-gray-600: #737373;
--color-gray-700: #595959;
--color-gray-800: #333333;
--color-gray-900: #1a1a1a;
--color-confirm: #8b6cc5;
--color-confirm-hover: #7558b0;
--color-gray-100: #f0f0f8;
--color-gray-200: #d8d8e4;
--color-gray-300: #b8b8cc;
--color-gray-400: #9898b0;
--color-gray-500: #7e7e98;
--color-gray-600: #636380;
--color-gray-700: #4a4a64;
--color-gray-800: #2e2e44;
--color-gray-900: #1a1a30;
--color-white: #ffffff;
--danger-bg: #b71c1c;
--danger-fg: #ef1313;
--danger-hover-bg: #d32f2f;
--input-bg: #2f5432;
--main-bg: #0b1b0b;
--main-fg: #178643;
--secondary-bg: #264026;
--selected-camera-bg: #1f2f1f;
--sidebar-active-bg: #64c87826;
--sidebar-bg: #264026;
--sidebar-border-color: #1e2e1e;
--danger-bg: #e05577;
--danger-fg: #e05577;
--danger-hover-bg: #c04466;
--glow-primary: 0 0 0 2px var(--main-fg), 0 0 10px rgba(139, 108, 197, 0.35);
--input-bg: #161630;
--main-bg: #06060f;
--main-fg: #8b6cc5;
--secondary-bg: #0e0e1a;
--selected-camera-bg: #0b0b18;
--sidebar-active-bg: rgba(139, 108, 197, 0.15);
--sidebar-bg: #0a0a16;
--sidebar-border-color: #1e1e3e;
--sidebar-fg: #ffffff;
--sidebar-title-fg: #193446;
--success-bg: #039226;
--success-fg: #00a100;
--success-hover-bg: #01be2d;
--text-color: #ffffff;
--text-muted: #a0a0a0;
--sidebar-title-fg: #8b6cc5;
--success-bg: #5ec8c8;
--success-fg: #5ec8c8;
--success-hover-bg: #4ab3b3;
--switch-inactive-bg: #1e1e3e;
--text-color: #e8e8f0;
--text-muted: #8080a8;
--text-on-primary: var(--sidebar-fg);
--text-on-surface: var(--text-color);
--thumb-color: #178643;
--track-color: #1a3a1a;
--warning-bg: #ff9800;
--warning-hover-bg: #f57c00;
--thumb-color: #8b6cc5;
--track-color: #14142e;
--warning-bg: #d4a060;
--warning-hover-bg: #b8884a;
/* Effects */
--disabled-opacity: 0.5;
@@ -71,7 +73,7 @@
--hover-scale-lg: scale(1.1);
/* Fonts */
--font-body: "Open Sans", sans-serif;
--font-body: "Inter", "Open Sans", sans-serif;
--font-mono: "Courier New", Courier, monospace;
--font-size-xs: 0.75rem;
--font-size-sm: 0.85rem;
@@ -96,9 +98,9 @@
--width-xxxxl: 1200px;
/* Shadows */
--shadow-xs: 0 1px 2px rgba(0,0,0,0.05);
--shadow-sm: 0 2px 4px rgba(0,0,0,0.1);
--shadow-md: 0 4px 12px rgba(0,0,0,0.2);
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.2);
/* Spacing */
--gap-xxs: 0.125rem;
@@ -172,12 +174,14 @@ input[type="checkbox"],
input[type="radio"],
.manage-keys-link,
.route_card,
.sidebar .menu_section > li > ul > li,
.sidebar .menu_section>li>ul>li,
.clickable {
cursor: pointer;
}
body { padding-bottom: 0 !important; }
body {
padding-bottom: 0 !important;
}
/* ――― Layout containers ――― */
.content {
@@ -199,9 +203,17 @@ h3 {
}
/* ――― Helpers & states ――― */
.hidden { display: none; }
html { cursor: default; }
.no_scroll { overflow: hidden; }
.hidden {
display: none;
}
html {
cursor: default;
}
.no_scroll {
overflow: hidden;
}
.not_implemented {
cursor: not-allowed;
@@ -251,7 +263,9 @@ textarea {
cursor: text;
}
label[for] { cursor: default; }
label[for] {
cursor: default;
}
/* ――― Snackbar component ――― */
.snackbar {
@@ -297,6 +311,7 @@ a {
bottom: 0;
opacity: 0;
}
to {
bottom: var(--snackbar-offset, 30px);
opacity: 1;
@@ -304,13 +319,20 @@ a {
}
@keyframes fadeout {
from { opacity: 1; }
to { opacity: 0; }
from {
opacity: 1;
}
to {
opacity: 0;
}
}
/* ――― Breakpoint overrides ――― */
@media only screen and (max-width: var(--breakpoint-md)) {
.content { margin-left: 0; }
.content {
margin-left: 0;
}
}
@media only screen and (max-width: 768px) and (orientation: portrait) {
@@ -320,6 +342,7 @@ a {
padding-left: 1rem;
padding-right: 1rem;
}
#snackbar_wrapper {
left: 50%;
transform: translateX(-50%);
@@ -333,6 +356,7 @@ a {
bottom: 0;
opacity: 0;
}
to {
bottom: var(--snackbar-offset, 30px);
opacity: 1;
@@ -340,18 +364,62 @@ a {
}
@-webkit-keyframes fadeout {
from { opacity: 1; }
to { opacity: 0; }
from {
opacity: 1;
}
to {
opacity: 0;
}
}
/* ——— Tunnel notice banner ——— */
.tunnel-notice {
align-items: center;
background: linear-gradient(135deg, var(--card-bg), var(--secondary-bg));
border: var(--border-width-thin) var(--border-style-base) var(--sidebar-border-color);
border-left: 3px solid var(--main-fg);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-md);
color: var(--text-color);
display: flex;
flex-direction: column;
gap: var(--gap-md);
margin: var(--margin-xl) auto;
max-width: var(--width-xl);
padding: var(--padding-xl) var(--padding-xxl);
text-align: center;
}
.tunnel-notice-icon {
font-size: 2.5rem;
opacity: 0.8;
}
.tunnel-notice-title {
color: var(--text-color);
font-size: var(--font-size-lg);
font-weight: var(--font-weight-bold);
margin: 0;
}
.tunnel-notice-body {
color: var(--text-muted);
font-size: var(--font-size-base);
line-height: var(--line-height-base);
margin: 0;
}
::-webkit-scrollbar {
height: 8px;
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--track-color);
}
::-webkit-scrollbar-thumb {
background-color: var(--thumb-color);
border-radius: var(--border-radius-sm);
}
}
@@ -1,4 +1,4 @@
import { html } from "https://esm.sh/@arrow-js/core";
import { html } from "/assets/vendor/arrow-core.js";
export function Modal({
title,
@@ -109,7 +109,7 @@
.favorites-toggle-button:hover {
background-color: var(--main-fg);
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
box-shadow: var(--glow-primary);
color: var(--text-color);
font-weight: var(--font-weight-bold);
transform: var(--hover-scale-sm);
@@ -352,7 +352,7 @@
.navigation-summary-widget button.directions:hover {
background-color: var(--success-hover-bg);
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
box-shadow: var(--glow-primary);
color: var(--text-color);
transform: var(--hover-scale-sm);
}
@@ -466,12 +466,12 @@
}
.search-provider-toggle button.active {
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
box-shadow: var(--glow-primary);
transform: var(--hover-scale-sm);
}
.search-provider-toggle button:hover {
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
box-shadow: var(--glow-primary);
transform: var(--hover-scale-sm);
}
@@ -533,4 +533,4 @@
min-width: 0;
width: calc(100% - var(--padding-xl));
}
}
}
@@ -1,4 +1,4 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core";
import { html, reactive } from "/assets/vendor/arrow-core.js";
import {
addRouteToMap,
formatMetersToHuman,
@@ -101,6 +101,25 @@ async function setSpecial(favorite, type, state, loadFavoritesAlphabetically) {
}
}
let mapboxLoadPromise = null;
function loadMapboxGL() {
if (mapboxLoadPromise) return mapboxLoadPromise;
mapboxLoadPromise = new Promise((resolve, reject) => {
const link = document.createElement("link");
link.href = "https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.css";
link.rel = "stylesheet";
document.head.appendChild(link);
const script = document.createElement("script");
script.src = "https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.js";
script.onload = resolve;
script.onerror = () => reject(new Error("Failed to load Mapbox GL"));
document.head.appendChild(script);
});
return mapboxLoadPromise;
}
export function NavDestination() {
let map;
let destinationMarker;
@@ -262,13 +281,18 @@ export function NavDestination() {
};
try {
state.destination = JSON.parse(data.destination);
} catch {}
} catch { }
try {
const prev = JSON.parse(data.previousDestinations);
state.previousDestinations = prev.map(d => ({ name: d.place_name }));
state.suggestions = JSON.stringify(state.previousDestinations);
} catch {}
setupMap();
} catch { }
try {
await setupMap();
} catch {
showSnackbar("Failed to load map resources…");
return;
}
loadFavoritesAlphabetically();
}
@@ -524,12 +548,18 @@ export function NavDestination() {
}
}
const setupMap = async () => {
const setupMap = async (retries = 0) => {
if (!state.mapboxPublic || state.initialized) return;
if (typeof mapboxgl === "undefined") {
await loadMapboxGL();
}
const container = document.getElementById("map");
if (!container) {
requestAnimationFrame(setupMap);
return;
if (retries >= 50) return;
await new Promise(r => requestAnimationFrame(r));
return setupMap(retries + 1);
}
state.initialized = true;
mapboxgl.accessToken = state.mapboxPublic;
@@ -585,9 +615,9 @@ export function NavDestination() {
return html`
<div class="navigation-container">
${() => {
if (state.missingKeys === null) return "";
return state.missingKeys
? html`
if (state.missingKeys === null) return "";
return state.missingKeys
? html`
<section class="keys-required-wrapper">
<div class="keys-required-widget">
<div class="keys-required-title">Mapbox Keys Required</div>
@@ -596,7 +626,7 @@ export function NavDestination() {
</div>
</section>
`
: html`
: html`
<div class="map-wrapper">
<div class="search-wrapper">
<div class="search-controls">
@@ -611,47 +641,47 @@ export function NavDestination() {
</div>
<div id="infobox">
${() => {
if (state.loadingRoute) {
return html`<div class="navigation-summary-widget loading-status"><span class="spinner"></span> Calculating route...</div>`;
} else if (state.selectedRoute) {
return NavigationDestination({
...state.selectedRoute,
isFavorited: isRouteFavorited(state.selectedRoute, state.favoriteRoutes),
isConfirmed: () => areRoutesEqual(state.selectedRoute, state.confirmedRoute),
map,
isMetric: state.isMetric,
cancelNavigationFn: () => {
state.selectedRoute = null;
state.confirmedRoute = null;
state.suggestions = state.previousDestinations;
if (destinationMarker) destinationMarker.remove();
},
onConfirm: () => {
state.confirmedRoute = JSON.parse(JSON.stringify(state.selectedRoute));
state.confirmedRouteRefresh = Math.random();
},
loadFavorites: loadFavoritesAlphabetically,
removeFavorite: confirmRemoveFavorite,
searchFieldState,
favoriteRoutes: state.favoriteRoutes
}, state.confirmedRouteRefresh);
} else if (JSON.parse(state.suggestions).length > 0) {
return SearchSuggestions({
suggestions: JSON.parse(state.suggestions),
selectSuggestion,
removeFavorite: confirmRemoveFavorite,
renameFavorite: confirmRenameFavorite,
setHome: setHome,
setWork: setWork
});
}
}}
if (state.loadingRoute) {
return html`<div class="navigation-summary-widget loading-status"><span class="spinner"></span> Calculating route...</div>`;
} else if (state.selectedRoute) {
return NavigationDestination({
...state.selectedRoute,
isFavorited: isRouteFavorited(state.selectedRoute, state.favoriteRoutes),
isConfirmed: () => areRoutesEqual(state.selectedRoute, state.confirmedRoute),
map,
isMetric: state.isMetric,
cancelNavigationFn: () => {
state.selectedRoute = null;
state.confirmedRoute = null;
state.suggestions = state.previousDestinations;
if (destinationMarker) destinationMarker.remove();
},
onConfirm: () => {
state.confirmedRoute = JSON.parse(JSON.stringify(state.selectedRoute));
state.confirmedRouteRefresh = Math.random();
},
loadFavorites: loadFavoritesAlphabetically,
removeFavorite: confirmRemoveFavorite,
searchFieldState,
favoriteRoutes: state.favoriteRoutes
}, state.confirmedRouteRefresh);
} else if (JSON.parse(state.suggestions).length > 0) {
return SearchSuggestions({
suggestions: JSON.parse(state.suggestions),
selectSuggestion,
removeFavorite: confirmRemoveFavorite,
renameFavorite: confirmRenameFavorite,
setHome: setHome,
setWork: setWork
});
}
}}
</div>
</div>
<div id="map"></div>
</div>
`;
}}
}}
</div>
${() => (state.showRemoveFavoriteModal ? Modal({
title: "Remove Favorite",
@@ -815,9 +845,9 @@ function NavigationDestination({
</div>
<div class="buttonCluster">
${() =>
isConfirmed()
? html`<button class="cancel" @click="${cancelNavigation}"><i class="bi bi-x-lg"></i> Cancel Navigation</button>`
: html`<button class="directions" @click="${confirmDestination}"><i class="bi bi-sign-turn-right"></i> Start Navigation</button>`}
isConfirmed()
? html`<button class="cancel" @click="${cancelNavigation}"><i class="bi bi-x-lg"></i> Cancel Navigation</button>`
: html`<button class="directions" @click="${confirmDestination}"><i class="bi bi-sign-turn-right"></i> Start Navigation</button>`}
<button class="favorite" @click="${toggleFavorite}">${isFavorited ? "💔 Unfavorite" : "❤️ Favorite"}</button>
</div>
</div>
@@ -47,7 +47,7 @@
transition: opacity 0.5s ease;
}
.navkeys-group > .navkeys-row:last-of-type {
.navkeys-group>.navkeys-row:last-of-type {
margin-bottom: 0;
}
@@ -79,7 +79,7 @@
.navkeys-input:hover,
.navkeys-input:focus {
border-color: var(--thumb-color);
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
box-shadow: var(--glow-primary);
transform: var(--hover-scale-sm);
}
@@ -153,4 +153,4 @@
.navkeys-input {
font-size: var(--font-size-sm);
}
}
}
@@ -1,4 +1,4 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { Modal } from "/assets/components/modal.js";
export function NavKeys() {
@@ -1,4 +1,5 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { isGalaxyTunnel } from "/assets/js/utils.js"
import { getOrdinalSuffix } from "/assets/components/navigation/navigation_utilities.js"
import { Modal } from "/assets/components/modal.js";
@@ -12,8 +13,18 @@ const state = reactive({
total: 0,
showDeleteAllModal: false,
isDeletingAll: false,
truncated: false,
})
const MAX_RENDERED_ROUTES = 250
const ROUTE_FLUSH_INTERVAL_MS = 120
let routesAbortController = null
let routesRequestToken = 0
let pendingRoutes = []
let flushTimerId = null
let seenRouteNames = new Set()
function formatRouteDate(dateString) {
const date = new Date(dateString)
if (isNaN(date.getTime())) {
@@ -31,10 +42,73 @@ function formatRouteDate(dateString) {
return `${month} ${day}${getOrdinalSuffix(day)}, ${year} - ${hour}:${minuteStr}${ampm}`
}
function resetRouteStreamState() {
pendingRoutes = []
if (flushTimerId !== null) {
clearTimeout(flushTimerId)
flushTimerId = null
}
seenRouteNames = new Set()
}
function flushPendingRoutes() {
if (pendingRoutes.length === 0) return
const availableSlots = Math.max(MAX_RENDERED_ROUTES - state.routes.length, 0)
if (availableSlots <= 0) {
pendingRoutes = []
state.truncated = true
return
}
const toAppend = pendingRoutes.slice(0, availableSlots)
pendingRoutes = []
if (toAppend.length > 0) {
state.routes = [...state.routes, ...toAppend]
}
if (state.routes.length >= MAX_RENDERED_ROUTES) {
state.truncated = true
}
}
function enqueueRoutes(rawRoutes) {
if (!Array.isArray(rawRoutes) || rawRoutes.length === 0) return
const nextRoutes = []
for (const route of rawRoutes) {
const name = String(route?.name || "")
if (!name || seenRouteNames.has(name)) continue
seenRouteNames.add(name)
nextRoutes.push({
...route,
timestamp: formatRouteDate(route.timestamp),
})
}
if (nextRoutes.length === 0) return
pendingRoutes.push(...nextRoutes)
if (flushTimerId === null) {
flushTimerId = setTimeout(() => {
flushTimerId = null
flushPendingRoutes()
}, ROUTE_FLUSH_INTERVAL_MS)
}
}
async function fetchRoutes() {
const requestToken = ++routesRequestToken
if (routesAbortController) {
routesAbortController.abort()
}
const controller = new AbortController()
routesAbortController = controller
try {
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const response = await fetch(`/api/routes?timezone=${encodeURIComponent(userTimezone)}`);
const response = await fetch(`/api/routes?timezone=${encodeURIComponent(userTimezone)}`, {
signal: controller.signal,
});
if (!response.ok) throw new Error();
const reader = response.body.getReader();
@@ -45,24 +119,24 @@ async function fetchRoutes() {
const { value, done } = await reader.read();
if (done) break;
if (requestToken !== routesRequestToken) return
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n\n");
const lines = buffer.split(/\r?\n\r?\n/);
buffer = lines.pop();
for (const line of lines) {
if (line.startsWith("data: ")) {
if (line.startsWith("data:")) {
try {
const data = JSON.parse(line.substring(6));
const payload = line.substring(5).trim()
if (!payload) continue
const data = JSON.parse(payload);
if (data.progress !== undefined && data.total !== undefined) {
state.progress = data.progress;
state.total = data.total;
}
if (data.routes) {
const routes = data.routes.map(route => ({
...route,
timestamp: formatRouteDate(route.timestamp),
}));
state.routes.push(...routes);
enqueueRoutes(data.routes)
}
} catch (e) {
console.error("Failed to parse JSON:", e);
@@ -70,21 +144,35 @@ async function fetchRoutes() {
}
}
}
} catch (_) {
state.error = "Couldn't load routes. Please try again later..."
flushPendingRoutes()
} catch (error) {
if (error?.name !== "AbortError") {
state.error = "Couldn't load routes. Please try again later..."
}
} finally {
state.loading = false
if (requestToken === routesRequestToken) {
flushPendingRoutes()
state.loading = false
if (routesAbortController === controller) {
routesAbortController = null
}
}
}
}
fetchRoutes()
function refresh() {
state.loading = true
state.error = null
state.routes = []
state.progress = 0
state.total = 0
state.truncated = false
resetRouteStreamState()
fetchRoutes()
}
refresh()
let overlay = null
function openDialog(htmlStr) {
@@ -124,27 +212,27 @@ async function deleteRoute(route) {
}
async function resetRouteName(route, dlg) {
const res = await fetch(`/api/routes/reset_name`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: route.name })
});
if (res.ok) {
const { timestamp } = await res.json();
closeDialog(dlg);
const routeInList = state.routes.find(r => r.name === route.name);
if (routeInList) {
routeInList.timestamp = formatRouteDate(timestamp);
}
route.timestamp = formatRouteDate(timestamp);
const overlayTitleSpan = overlay.querySelector(".media-player-title span");
if (overlayTitleSpan) {
overlayTitleSpan.textContent = formatRouteDate(timestamp);
}
showSnackbar("Route name reset!");
} else {
showSnackbar("Resetting name failed...", "error");
const res = await fetch(`/api/routes/reset_name`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: route.name })
});
if (res.ok) {
const { timestamp } = await res.json();
closeDialog(dlg);
const routeInList = state.routes.find(r => r.name === route.name);
if (routeInList) {
routeInList.timestamp = formatRouteDate(timestamp);
}
route.timestamp = formatRouteDate(timestamp);
const overlayTitleSpan = overlay.querySelector(".media-player-title span");
if (overlayTitleSpan) {
overlayTitleSpan.textContent = formatRouteDate(timestamp);
}
showSnackbar("Route name reset!");
} else {
showSnackbar("Resetting name failed...", "error");
}
}
async function renameRoute(route) {
@@ -317,6 +405,16 @@ async function deleteAllRoutes() {
}
export function RouteRecordings() {
if (isGalaxyTunnel()) {
return html`
<div class="tunnel-notice">
<div class="tunnel-notice-icon">🛰</div>
<h3 class="tunnel-notice-title">Dashcam Routes Unavailable via Galaxy</h3>
<p class="tunnel-notice-body">Loading dashcam routes requires a direct connection.<br>Connect to your device's local network to use this feature.</p>
</div>
`;
}
if (state.selectedRoute && !overlay) openOverlay(state.selectedRoute);
return html`
@@ -332,75 +430,36 @@ export function RouteRecordings() {
</button>
${() => {
const routesToShow = state.routes.filter(r => !state.showPreservedOnly || r.is_preserved);
const routesToShow = state.routes.filter(r => !state.showPreservedOnly || r.is_preserved);
if (routesToShow.length === 0) {
if (state.loading && state.total > 0) {
return html`<p class="screen-recordings-message">Processing Routes: ${state.progress} of ${state.total}</p>`;
}
if (state.loading && !state.isDeletingAll) {
return html`<p class="screen-recordings-message">Loading...</p>`;
}
if (state.isDeletingAll) {
return html`<p class="screen-recordings-message">Deleting routes...</p>`;
}
if (state.showPreservedOnly) {
return html`<p class="screen-recordings-message">No preserved routes...</p>`;
}
if (state.error) {
return html`<p class="screen-recordings-message">${state.error}</p>`;
}
return html`<p class="screen-recordings-message">No routes found...</p>`;
}
if (routesToShow.length === 0) {
if (state.loading && state.total > 0) {
return html`<p class="screen-recordings-message">Processing Routes: ${state.progress} of ${state.total}</p>`;
}
if (state.loading && !state.isDeletingAll) {
return html`<p class="screen-recordings-message">Loading...</p>`;
}
if (state.isDeletingAll) {
return html`<p class="screen-recordings-message">Deleting routes...</p>`;
}
if (state.showPreservedOnly) {
return html`<p class="screen-recordings-message">No preserved routes...</p>`;
}
if (state.error) {
return html`<p class="screen-recordings-message">${state.error}</p>`;
}
return html`<p class="screen-recordings-message">No routes found...</p>`;
}
return html`
return html`
<div class="screen-recordings-grid">
${routesToShow.map(
route => html`
route => html`
<div
class="recording-card"
@mouseenter="${e => {
if (state.selectedRoute) return;
const card = e.currentTarget;
const gif = card.querySelector(".recording-preview-gif");
const png = card.querySelector(".recording-preview-png");
if (card.dataset.gifLoaded) {
png.style.display = "none";
gif.style.display = "block";
return;
}
card.dataset.loadingGif = "true";
const preloader = new Image();
preloader.onload = () => {
if (card.dataset.loadingGif === "true") {
gif.src = preloader.src;
png.style.display = "none";
gif.style.display = "block";
card.dataset.gifLoaded = true;
}
delete card.dataset.loadingGif;
};
preloader.onerror = () => {
console.error("Failed to load preview GIF:", preloader.src);
delete card.dataset.loadingGif;
};
preloader.src = gif.dataset.src;
}}"
@mouseleave="${e => {
const card = e.currentTarget;
card.querySelector(".recording-preview-png").style.display = "block";
card.querySelector(".recording-preview-gif").style.display = "none";
if (card.dataset.loadingGif === "true") {
delete card.dataset.loadingGif;
}
}}"
@click="${() => {
state.selectedRoute = route;
}}"
state.selectedRoute = route;
}}"
>
<div class="preserved-icon" @click="${e => togglePreserved(route, e)}">
${() => html`<i class="bi ${route.is_preserved ? "bi-heart-fill" : "bi-heart"}"></i>`}
@@ -410,23 +469,22 @@ export function RouteRecordings() {
src="${route.png}"
class="recording-preview recording-preview-png"
style="display:block;"
>
<img
data-src="${route.gif}"
class="recording-preview recording-preview-gif"
style="display:none;"
loading="lazy"
>
</div>
<p class="recording-filename">${route.timestamp}</p>
</div>
`
)}
)}
</div>
`;
}}
}}
${() => state.truncated ? html`
<p class="screen-recordings-message">Showing first ${MAX_RENDERED_ROUTES} routes to keep the UI responsive.</p>
` : ""}
${() => {
if (state.routes.length > 0) {
return html`
if (state.routes.length > 0) {
return html`
<button
class="delete-all-button"
@click="${() => (state.showDeleteAllModal = true)}"
@@ -435,17 +493,17 @@ export function RouteRecordings() {
${() => (state.isDeletingAll ? "Deleting..." : "Delete All Routes")}
</button>
`;
}
return "";
}}
}
return "";
}}
</div>
${() => state.showDeleteAllModal ? Modal({
title: "Confirm Delete All",
message: "Are you sure you want to delete all routes? This action cannot be undone...",
onConfirm: deleteAllRoutes,
onCancel: () => { state.showDeleteAllModal = false; },
confirmText: "Delete All"
}) : ""}
title: "Confirm Delete All",
message: "Are you sure you want to delete all routes? This action cannot be undone...",
onConfirm: deleteAllRoutes,
onCancel: () => { state.showDeleteAllModal = false; },
confirmText: "Delete All"
}) : ""}
</div>
`;
}
@@ -1,4 +1,5 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { isGalaxyTunnel } from "/assets/js/utils.js"
import { Modal } from "/assets/components/modal.js";
const state = reactive({
@@ -126,7 +127,6 @@ async function renameFile(rec) {
if (recordingToUpdate) {
recordingToUpdate.filename = newFilename
recordingToUpdate.is_custom_name = true
recordingToUpdate.gif = `/screen_recordings/${val}.gif`
recordingToUpdate.png = `/screen_recordings/${val}.png`
}
@@ -142,8 +142,8 @@ async function renameFile(rec) {
}
function confirmDeleteFile(rec) {
state.recordingToDelete = rec;
state.showDeleteModal = true;
state.recordingToDelete = rec;
state.showDeleteModal = true;
}
async function deleteFile() {
@@ -152,11 +152,11 @@ async function deleteFile() {
const res = await fetch(`/api/screen_recordings/delete/${encodeURIComponent(rec.filename)}`, { method: "DELETE" })
if (res.ok) {
closeOverlay();
refresh();
showSnackbar("Recording deleted!");
closeOverlay();
refresh();
showSnackbar("Recording deleted!");
} else {
showSnackbar("Delete failed...", "error");
showSnackbar("Delete failed...", "error");
}
state.showDeleteModal = false;
@@ -221,6 +221,16 @@ async function deleteAllRecordings() {
}
export function ScreenRecordings() {
if (isGalaxyTunnel()) {
return html`
<div class="tunnel-notice">
<div class="tunnel-notice-icon">🛰</div>
<h3 class="tunnel-notice-title">Screen Recordings Unavailable via Galaxy</h3>
<p class="tunnel-notice-body">Loading screen recordings requires a direct connection.<br>Connect to your device's local network to use this feature.</p>
</div>
`;
}
if (state.selectedRecording && !overlay) openOverlay(state.selectedRecording)
return html`
@@ -229,77 +239,37 @@ export function ScreenRecordings() {
<div class="screen-recordings-title">Screen Recordings</div>
${() => {
if (state.loading && state.recordings.length === 0) return html`<p class="screen-recordings-message">Loading...</p>`
if (state.error) return html`<p class="screen-recordings-message">${state.error}</p>`
if (state.progress > 0 && state.progress < state.total) {
return html`<p class="screen-recordings-message">Processing Recordings: ${state.progress} of ${state.total}</p>`
}
if (state.recordings.length === 0 && !state.loading) {
return html`<p class="screen-recordings-message">No screen recordings found...</p>`
}
return ""
}}
if (state.loading && state.recordings.length === 0) return html`<p class="screen-recordings-message">Loading...</p>`
if (state.error) return html`<p class="screen-recordings-message">${state.error}</p>`
if (state.progress > 0 && state.progress < state.total) {
return html`<p class="screen-recordings-message">Processing Recordings: ${state.progress} of ${state.total}</p>`
}
if (state.recordings.length === 0 && !state.loading) {
return html`<p class="screen-recordings-message">No screen recordings found...</p>`
}
return ""
}}
<div class="screen-recordings-grid">
${() => state.recordings.map(rec => {
const displayName = rec.is_custom_name ? rec.filename.replace(/\.mp4$/i, "").replace(/_/g, " ") : formatScreenRecordingDate(rec.timestamp)
return html`
const displayName = rec.is_custom_name ? rec.filename.replace(/\.mp4$/i, "").replace(/_/g, " ") : formatScreenRecordingDate(rec.timestamp)
return html`
<div
class="recording-card"
@mouseenter="${e => {
if (state.selectedRecording) return;
const card = e.currentTarget;
const gif = card.querySelector(".recording-preview-gif");
const png = card.querySelector(".recording-preview-png");
if (card.dataset.gifLoaded) {
png.style.display = "none";
gif.style.display = "block";
return;
}
card.dataset.loadingGif = "true";
const preloader = new Image();
preloader.onload = () => {
if (card.dataset.loadingGif === "true") {
gif.src = preloader.src;
png.style.display = "none";
gif.style.display = "block";
card.dataset.gifLoaded = true;
}
delete card.dataset.loadingGif;
};
preloader.onerror = () => {
console.error("Failed to load preview GIF:", preloader.src);
delete card.dataset.loadingGif;
};
preloader.src = gif.dataset.src;
}}"
@mouseleave="${e => {
const card = e.currentTarget;
card.querySelector(".recording-preview-png").style.display = "block";
card.querySelector(".recording-preview-gif").style.display = "none";
if (card.dataset.loadingGif === "true") {
delete card.dataset.loadingGif;
}
}}"
@click="${() => { state.selectedRecording = rec }}"
>
<div class="recording-preview-container">
<img src="${rec.png}" class="recording-preview recording-preview-png" style="display:block;">
<img data-src="${rec.gif}" class="recording-preview recording-preview-gif" style="display:none;">
<img src="${rec.png}" class="recording-preview recording-preview-png" style="display:block;" loading="lazy">
</div>
<p class="recording-filename">${displayName}</p>
</div>
`
})}
})}
</div>
${() => {
if (state.recordings.length > 0) {
return html`
if (state.recordings.length > 0) {
return html`
<button
class="delete-all-button"
@click="${() => (state.showDeleteAllModal = true)}"
@@ -307,24 +277,24 @@ export function ScreenRecordings() {
Delete All Recordings
</button>
`
}
return ""
}}
}
return ""
}}
</div>
${() => state.showDeleteModal ? Modal({
title: "Confirm Delete",
message: `Are you sure you want to delete <strong>${state.recordingToDelete.filename}</strong>?`,
onConfirm: deleteFile,
onCancel: () => { state.showDeleteModal = false; state.recordingToDelete = null; },
confirmText: "Delete"
}) : ""}
title: "Confirm Delete",
message: `Are you sure you want to delete <strong>${state.recordingToDelete.filename}</strong>?`,
onConfirm: deleteFile,
onCancel: () => { state.showDeleteModal = false; state.recordingToDelete = null; },
confirmText: "Delete"
}) : ""}
${() => state.showDeleteAllModal ? Modal({
title: "Confirm Delete All",
message: "Are you sure you want to delete all screen recordings? This action cannot be undone...",
onConfirm: deleteAllRecordings,
onCancel: () => { state.showDeleteAllModal = false; },
confirmText: "Delete All"
}) : ""}
title: "Confirm Delete All",
message: "Are you sure you want to delete all screen recordings? This action cannot be undone...",
onConfirm: deleteAllRecordings,
onCancel: () => { state.showDeleteAllModal = false; },
confirmText: "Delete All"
}) : ""}
</div>
`
}
@@ -1,9 +1,12 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { createBrowserHistory, createRouter } from "https://esm.sh/@remix-run/router@1.3.1"
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { createBrowserHistory, createRouter } from "/assets/vendor/remix-router-1.3.1.js"
import { hideSidebar } from "/assets/js/utils.js"
import { DoorControl } from "/assets/components/tools/doors.js"
import { DeviceSettings } from "/assets/components/tools/device_settings.js"
import { ErrorLogs } from "/assets/components/tools/error_logs.js"
import { VehicleFeatures } from "/assets/components/tools/vehicle_features.js"
import { GalaxyPairing } from "/assets/components/tools/galaxy.js"
import { Home } from "/assets/components/home/home.js"
import { LongitudinalManeuvers } from "/assets/components/tools/longitudinal_maneuvers.js"
import { NavDestination } from "/assets/components/navigation/navigation_destination.js"
import { NavKeys } from "/assets/components/navigation/navigation_keys.js"
import { RouteRecordings } from "/assets/components/recordings/dashcam_routes.js"
@@ -11,27 +14,54 @@ import { SettingsView } from "/assets/components/settings.js"
import { ScreenRecordings } from "/assets/components/recordings/screen_recordings.js"
import { Sidebar } from "/assets/components/sidebar.js"
import { SpeedLimits } from "/assets/components/tools/speed_limits.js"
import { TailscaleControl } from "/assets/components/tailscale/tailscale.js"
import { ModelManager } from "/assets/components/tools/model_manager.js?v=20260303t"
import { LivePlots } from "/assets/components/tools/plots.js"
import { ThemeMaker } from "/assets/components/tools/theme_maker.js"
import { TestingGround } from "/assets/components/tools/testing_ground.js"
import { Troubleshoot } from "/assets/components/tools/troubleshoot.js"
import { TmuxLog } from "/assets/components/tools/tmux.js"
import { ToggleControl } from "/assets/components/tools/toggles.js"
import { TSKManager } from "/assets/components/tools/tsk_manager.js"
import { UpdateManager } from "/assets/components/tools/update_manager.js"
let router, routerState
function toRouterHref(href) {
try {
const url = new URL(href, window.location.origin)
return `${url.pathname}${url.search}${url.hash}`
} catch {
return href
}
}
function createRoute(id, path, component) {
return {
id,
path,
loader: () => {},
loader: () => { },
element: component,
}
}
function SafeHome() {
return html`
<div>
<h1>Galaxy</h1>
<p>Safe mode is active while UI rendering is being repaired.</p>
<p>
<a href="/galaxy">Galaxy Pairing</a> |
<a href="/device_settings">Toggles</a> |
<a href="/manage_updates">Software</a>
</p>
</div>
`
}
function Root() {
let routes = [
createRoute("doors", "/lock_or_unlock_doors", DoorControl),
createRoute("device_settings", "/device_settings/:section?", DeviceSettings),
createRoute("errorLogs", "/manage_error_logs", ErrorLogs),
createRoute("galaxy", "/galaxy", GalaxyPairing),
createRoute("navdestination", "/set_navigation_destination", NavDestination),
createRoute("navkeys", "/manage_navigation_keys", NavKeys),
createRoute("root", "/", Home),
@@ -39,11 +69,16 @@ function Root() {
createRoute("screen_recordings", "/screen_recordings", ScreenRecordings),
createRoute("settings", "/settings/:section/:subsection?", SettingsView),
createRoute("speed_limits", "/download_speed_limits", SpeedLimits),
createRoute("tailscale", "/manage_tailscale", TailscaleControl),
createRoute("model_manager", "/manage_models", ModelManager),
createRoute("longitudinal_maneuvers", "/longitudinal_maneuvers", LongitudinalManeuvers),
createRoute("plots", "/plots", LivePlots),
createRoute("thememaker", "/theme_maker", ThemeMaker),
createRoute("testing_ground", "/testing_ground", TestingGround),
createRoute("troubleshoot", "/troubleshoot", Troubleshoot),
createRoute("tmux", "/manage_tmux", TmuxLog),
createRoute("toggles", "/manage_toggles", ToggleControl),
createRoute("tsk_manager", "/tsk_manager", TSKManager),
createRoute("updates", "/manage_updates", UpdateManager),
createRoute("vehicle_features", "/vehicle_features", VehicleFeatures),
]
router = createRouter({
@@ -51,6 +86,11 @@ function Root() {
history: createBrowserHistory(),
}).initialize()
window.__thePondNavigate = (href) => {
router.navigate(toRouterHref(href))
window.scrollTo(0, 0)
}
routerState = reactive({
activePath: "/",
activePathFull: "/",
@@ -58,17 +98,21 @@ function Root() {
navigation: { state: "loading" },
errors: [],
params: {},
activeMatch: null,
})
router.subscribe(({ initialized, navigation, matches, errors }) => {
const [match] = matches
const match = Array.isArray(matches) && matches.length > 0
? matches[matches.length - 1]
: null
Object.assign(routerState, {
initialized,
activePath: match.route.path,
activePathFull: match.pathname,
activePath: match?.route?.path || "",
activePathFull: match?.pathname || "",
navigation,
params: match.params,
params: match?.params || {},
errors,
activeMatch: match,
})
})
@@ -76,17 +120,21 @@ function Root() {
${() => Sidebar(routerState.activePathFull)}
<div class="content">
${() => {
if (!routerState.initialized || routerState.navigation.state === "loading") {
return html`<div>Loading...</div>`
}
if (!routerState.initialized) {
return Home({ params: routerState.params })
}
if (routerState.errors?.root?.status === 404) {
return html`<h1>Not Found</h1>`
}
if (routerState.errors?.root?.status === 404) {
return html`<h1>Not Found</h1>`
}
const match = routes.find(r => r.path === routerState.activePath)
return match.element({ params: routerState.params })
}}
const routeElement = routerState.activeMatch?.route?.element
if (typeof routeElement !== "function") {
console.warn("[router] no route element for path:", routerState.activePathFull)
return Home({ params: routerState.params })
}
return routeElement({ params: routerState.params })
}}
</div>
`
}
@@ -97,7 +145,7 @@ export function Link(href, children, onClick, classes = "") {
href="${() => href}"
@click="${(e) => {
e.preventDefault()
router.navigate(e.currentTarget.href)
router.navigate(toRouterHref(e.currentTarget.href))
hideSidebar()
onClick?.()
}}"
@@ -105,8 +153,28 @@ export function Link(href, children, onClick, classes = "") {
}
export function Navigate(href) {
router.navigate(href)
router.navigate(toRouterHref(href))
window.scrollTo(0, 0)
}
Root()(document.getElementById("app"))
function mountRouterWhenReady() {
const mountNode = document.getElementById("app")
if (!mountNode) {
// Some embedded browsers execute module scripts before <body> is fully available.
setTimeout(mountRouterWhenReady, 0)
return
}
if (!window.__thePondRouterMounted) {
window.__thePondRouterMounted = true
Root()(mountNode)
} else {
console.warn("[router] duplicate mount prevented")
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", mountRouterWhenReady, { once: true })
} else {
mountRouterWhenReady()
}
@@ -30,7 +30,7 @@
position: relative;
}
.dropdown > select {
.dropdown>select {
appearance: none;
background-color: var(--sidebar-bg);
border: var(--border-width-thin) solid var(--sidebar-border-color);
@@ -74,19 +74,19 @@ input.searchfield:focus {
outline: none;
}
input:checked + .slider {
input:checked+.slider {
background-color: var(--success-bg);
}
input:checked + .slider:before {
input:checked+.slider:before {
transform: translateX(26px);
}
input:checked + .slider.loading:before {
input:checked+.slider.loading:before {
animation: rotationChecked 1s linear infinite;
}
input:focus + .slider {
input:focus+.slider {
box-shadow: 0 0 var(--border-width-thin) var(--success-bg);
}
@@ -133,11 +133,11 @@ input:focus + .slider {
z-index: 1;
}
.options > input {
.options>input {
display: none;
}
.options > input:checked + label {
.options>input:checked+label {
background-color: var(--success-bg);
color: var(--text-color);
font-weight: bold;
@@ -220,14 +220,13 @@ input:focus + .slider {
grid-column: span 2;
}
.setting.subsetting_link {
}
.setting.subsetting_link {}
.setting.subtoggle {
margin: 0 0 0 var(--padding-xl);
}
.setting.subtoggle + .setting {
.setting.subtoggle+.setting {
margin-top: var(--border-radius-xl);
}
@@ -300,7 +299,7 @@ input.searchfield {
animation: rotation 1s linear infinite;
background-color: none !important;
border: var(--border-width-thick) solid var(--sidebar-fg);
border-bottom-color: #46439b;
border-bottom-color: var(--main-fg);
box-sizing: border-box;
}
@@ -337,6 +336,7 @@ i.switch {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
@@ -346,7 +346,8 @@ i.switch {
0% {
transform: translateX(26px) rotate(0deg);
}
100% {
transform: translateX(26px) rotate(360deg);
}
}
}
@@ -1,4 +1,4 @@
import { html } from "https://esm.sh/@arrow-js/core"
import { html } from "/assets/vendor/arrow-core.js"
import { upperFirst } from "/assets/js/utils.js"
import { Navigate } from "/assets/components/router.js"
@@ -60,7 +60,10 @@
}
.sidebar {
background-color: var(--sidebar-bg);
background:
radial-gradient(ellipse at 30% 80%, rgba(139, 108, 197, 0.06) 0%, transparent 60%),
radial-gradient(ellipse at 70% 20%, rgba(94, 200, 200, 0.04) 0%, transparent 50%),
var(--sidebar-bg);
border-right: var(--border-width-thin) solid var(--sidebar-border-color);
display: flex;
flex-direction: column;
@@ -102,7 +105,7 @@
padding: 0;
}
.sidebar .menu_section > li > a span {
.sidebar .menu_section>li>a span {
color: var(--text-color);
display: inline-block;
font-size: var(--font-size-lg);
@@ -111,13 +114,13 @@
padding-left: var(--padding-sm);
}
.sidebar .menu_section > li > ul {
.sidebar .menu_section>li>ul {
list-style: none;
margin: 0;
padding: 0;
}
.sidebar .menu_section > li > ul > li {
.sidebar .menu_section>li>ul>li {
border-radius: var(--border-radius-sm);
margin-bottom: var(--padding-sm);
overflow: hidden;
@@ -125,23 +128,23 @@
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
}
.sidebar .menu_section > li > ul > li > a.menu-item-link {
.sidebar .menu_section>li>ul>li>a.menu-item-link {
border-radius: inherit;
}
.sidebar .menu_section > li > ul > li.active,
.sidebar .menu_section > li > ul > li:hover {
.sidebar .menu_section>li>ul>li.active,
.sidebar .menu_section>li>ul>li:hover {
background-color: var(--sidebar-active-bg);
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
box-shadow: var(--glow-primary);
transform: var(--hover-scale-sm);
}
.sidebar .menu_section > li > ul > li.active > a {
.sidebar .menu_section>li>ul>li.active>a {
color: var(--color-white);
font-weight: var(--font-weight-bold);
}
.sidebar .menu_section > li > ul > li:hover > a {
.sidebar .menu_section>li>ul>li:hover>a {
color: var(--color-white);
font-weight: var(--font-weight-demi-bold);
}
@@ -203,9 +206,13 @@
}
.sidebar_header p {
color: var(--success-hover-bg);
background: linear-gradient(135deg, #8b6cc5 0%, #5ec8c8 55%, #d4789c 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
letter-spacing: 0.05em;
}
.sidebar_widget {
@@ -234,7 +241,7 @@
transition: var(--transition-fast);
}
.sidebar .menu_section > li > a {
.sidebar .menu_section>li>a {
font-size: var(--padding-sm);
}
@@ -262,7 +269,7 @@
transition: var(--transition-fast);
}
.sidebar .menu_section > li > a {
.sidebar .menu_section>li>a {
font-size: var(--padding-sm);
}
@@ -277,4 +284,4 @@
#menu_button {
display: none;
}
}
}
@@ -1,8 +1,7 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core";
import { Link } from "/assets/components/router.js";
import { upperFirst, hideSidebar } from "/assets/js/utils.js";
import { html } from "/assets/vendor/arrow-core.js";
import { hideSidebar, upperFirst } from "/assets/js/utils.js";
const MenuItems = {
const MENU_ITEMS = {
home: [
{ name: "Home", link: "/", icon: "bi-house-fill" },
],
@@ -14,138 +13,126 @@ const MenuItems = {
{ name: "Dashcam Routes", link: "/dashcam_routes", icon: "bi-camera-reels" },
{ name: "Screen Recordings", link: "/screen_recordings", icon: "bi-record-circle" },
],
tailscale: [
{ name: "Tailscale", link: "/manage_tailscale", icon: "bi-wifi" },
],
tools: [
{ name: "Toggles", link: "/device_settings", icon: "bi-toggle-on" },
{ name: "Download Speed Limits", link: "/download_speed_limits", icon: "bi-download" },
{ name: "Error Logs", link: "/manage_error_logs", icon: "bi-exclamation-triangle" },
{ name: "Lock/Unlock Doors", link: "/lock_or_unlock_doors", icon: "bi-door-closed" },
{ name: "Galaxy", link: "/galaxy", icon: "bi-globe2" },
{ name: "Long Maneuvers", link: "/longitudinal_maneuvers", icon: "bi-signpost-split" },
{ name: "Model Manager", link: "/manage_models", icon: "bi-cpu" },
{ name: "Plots", link: "/plots", icon: "bi-graph-up-arrow" },
{ name: "Testing Ground", link: "/testing_ground", icon: "bi-bezier2" },
{ name: "Troubleshoot", link: "/troubleshoot", icon: "bi-tools" },
{ name: "Theme Maker", link: "/theme_maker", icon: "bi-palette-fill" },
{ name: "Tmux Log", link: "/manage_tmux", icon: "bi-terminal" },
{ name: "Toggles", link: "/manage_toggles", icon: "bi-toggle-on" },
{ name: "Toyota Security Keys", link: "/tsk_manager", icon: "bi-key-fill" },
{ name: "Backup and Restore", link: "/manage_toggles", icon: "bi-arrow-repeat" },
{ name: "Software", link: "/manage_updates", icon: "bi-arrow-up-circle" },
{ name: "Vehicle Features", link: "/vehicle_features", icon: "bi-car-front" },
],
};
const state = reactive({
doorsVisible: false,
isDoorsFetched: false,
isTSKFetched: false,
tskVisible: false,
function matchesPath(currentPath, link) {
if (link === "/") return currentPath === "/";
return currentPath === link || currentPath.startsWith(`${link}/`);
}
activeRoute: ""
});
function buildSectionMarkup(section, links, currentPath) {
const linksMarkup = links.map((link) => {
const active = matchesPath(currentPath, link.link) ? "active" : "";
return `
<li class="${active}">
<a class="menu-item-link" href="${link.link}">
<i class="bi ${link.icon}"></i>
<span>${upperFirst(link.name)}</span>
</a>
</li>
`;
}).join("");
export function Sidebar() {
const currentPath = window.location.pathname;
const activeItem = Object.values(MenuItems).flat().find(item => item.link === currentPath);
state.activeRoute = activeItem?.name ?? "";
return `
<div class="sidebar_widget">
<ul class="menu_section">
<li>
<span class="section-title">${upperFirst(section)}</span>
<ul id="${section}">
${linksMarkup}
</ul>
</li>
</ul>
</div>
`;
}
if (!state.isDoorsFetched) {
state.isDoorsFetched = true;
(async () => {
try {
const response = await fetch("/api/doors_available");
const data = await response.json();
state.doorsVisible = data.result;
} catch (e) {
console.error("Failed to fetch door availability:", e);
}
})();
}
function bindSidebarHandlers() {
const menuButton = document.getElementById("menu_button");
const underlay = document.getElementById("sidebarUnderlay");
if (!state.isTSKFetched) {
state.isTSKFetched = true;
(async () => {
try {
const response = await fetch("/api/tsk_available");
const data = await response.json();
state.tskVisible = data.result;
} catch (e) {
console.error("Failed to fetch TSK availability:", e);
}
})();
}
if (!menuButton || !underlay) return;
function navigate(link) {
state.activeRoute = link.name;
window.scrollTo(0, 0);
hideSidebar();
document.querySelectorAll('.sidebar li').forEach(el => {
el.classList.remove('active');
if (!window.__thePondSidebarMenuBound) {
window.__thePondSidebarMenuBound = true;
menuButton.addEventListener("click", () => {
const sidebar = document.getElementById("sidebar");
const currentUnderlay = document.getElementById("sidebarUnderlay");
if (!sidebar || !currentUnderlay) return;
sidebar.classList.toggle("visible");
currentUnderlay.classList.toggle("hidden");
});
const linkElement = document.querySelector(`.sidebar li a[href="${link.link}"]`);
if (linkElement) {
linkElement.parentElement.classList.add('active');
}
}
return html`
underlay.onclick = hideSidebar;
document.querySelectorAll("#sidebar a.menu-item-link").forEach((anchor) => {
if (anchor.dataset.boundClick === "1") return;
anchor.dataset.boundClick = "1";
anchor.addEventListener("click", (event) => {
if (event.defaultPrevented) return;
if (event.button !== 0) return;
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
event.preventDefault();
const href = anchor.getAttribute("href") || "/";
const navigate = window.__thePondNavigate;
if (typeof navigate === "function") {
navigate(href);
} else {
window.location.assign(href);
}
hideSidebar();
window.scrollTo(0, 0);
});
});
}
function renderSidebarIntoShell(currentPath) {
const shell = document.getElementById("sidebar_shell");
if (!shell) return;
const activePath = currentPath || window.location.pathname;
const sectionsMarkup = Object.entries(MENU_ITEMS)
.map(([section, links]) => buildSectionMarkup(section, links, activePath))
.join("");
shell.innerHTML = `
<div id="sidebarUnderlay" class="hidden"></div>
<div id="sidebar" class="sidebar">
<div>
<div class="title">
<img class="logo" src="/assets/images/main_logo.png" alt="FrogPilot logo" />
<img class="logo" src="/assets/images/main_logo.png" alt="Galaxy logo" />
<div class="title_text sidebar_header">
<p>The Pond</p>
<a href="https://github.com/Aidenir">by&nbsp;Aidenir</a>
<p>Galaxy</p>
</div>
</div>
<hr />
${() => Object.entries(MenuItems).map(([section, links]) => html`
<div class="sidebar_widget">
<ul class="menu_section">
<li>
<span class="section-title">${upperFirst(section)}</span>
<ul id="${section}">
${links.map(link => {
if (link.name === "Lock/Unlock Doors" && !state.doorsVisible) {
return "";
}
if (link.name === "Toyota Security Keys" && !state.tskVisible) {
return "";
}
const isActive = state.activeRoute === link.name;
const classList = [isActive && "active"].filter(Boolean).join(" ");
const content = html`
<div class="menu-item-link">
<i class="bi ${link.icon}"></i>
<span>${upperFirst(link.name)}</span>
</div>
`;
return html`
<li class="${classList}">
${Link(link.link, content, () => navigate(link))}
</li>
`;
})}
</ul>
</li>
</ul>
</div>
`)}
${sectionsMarkup}
</div>
</div>`;
</div>
`;
bindSidebarHandlers();
}
function setupMenuButton() {
const button = document.getElementById("menu_button");
const sidebar = document.getElementById("sidebar");
const underlay = document.getElementById("sidebarUnderlay");
button.addEventListener("click", () => {
sidebar.classList.toggle("visible");
underlay.classList.toggle("hidden");
});
underlay.addEventListener("click", hideSidebar);
export function Sidebar(currentPath) {
setTimeout(() => renderSidebarIntoShell(currentPath), 0);
return html`<div id="sidebar_shell"></div>`;
}
document.addEventListener("DOMContentLoaded", setupMenuButton, false);
@@ -1,4 +1,4 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { Modal } from "/assets/components/modal.js";
export function TailscaleControl() {
@@ -52,13 +52,14 @@ export function TailscaleControl() {
checkInstallStatus()
return html`
<div class="tailscale-wrapper">
<div class="toggle-control-widget" style="margin-top: 1.5rem">
<section class="tailscale-widget">
<div class="tailscale-title">
<div class="toggle-control-title">
${() => state.installed ? 'Uninstall Tailscale' : 'Install Tailscale'}
</div>
<p class="tailscale-text">
Tailscale creates a secure, private connection between your openpilot device and your phone or PC so you can access and control it from anywhere!
Tailscale creates a secure, private connection between your openpilot device and your phone or PC so you can access and control it from anywhere!<br><br>
<strong style="color: #ff9494;">Note: Not recommended. Using Galaxy Tunnel is the preferred remote connection method.</strong>
</p>
<div class="tailscale-button-wrapper">
<button
@@ -67,11 +68,11 @@ export function TailscaleControl() {
disabled="${() => state.status === 'installing' || state.status === 'uninstalling'}"
>
${() => {
if (state.status === 'installing') return 'Installing...'
if (state.status === 'uninstalling') return 'Uninstalling...'
if (state.installed) return 'Uninstall'
return 'Install'
}}
if (state.status === 'installing') return 'Installing...'
if (state.status === 'uninstalling') return 'Uninstalling...'
if (state.installed) return 'Uninstall'
return 'Install'
}}
</button>
<a class="tailscale-link" href="https://tailscale.com/download" target="_blank">
Download Tailscale on your other devices
@@ -79,12 +80,12 @@ export function TailscaleControl() {
</div>
</section>
${() => state.showUninstallModal ? Modal({
title: "Confirm Uninstall",
message: "Are you sure you want to uninstall Tailscale?",
onConfirm: handleAction,
onCancel: () => { state.showUninstallModal = false; },
confirmText: "Uninstall"
}) : ""}
title: "Confirm Uninstall",
message: "Are you sure you want to uninstall Tailscale?",
onConfirm: handleAction,
onCancel: () => { state.showUninstallModal = false; },
confirmText: "Uninstall"
}) : ""}
</div>
`
}
@@ -0,0 +1,498 @@
/* ――― Device Settings Page ――― */
.ds-wrapper {
max-width: var(--width-xxxl);
padding: var(--padding-base) var(--padding-lg) var(--padding-xxl);
}
/* ――― Section Tabs ――― */
.ds-tabs {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: var(--margin-base);
}
.ds-tab {
align-items: center;
background: var(--input-bg);
border: var(--border-style-main);
border-radius: 0.75rem;
color: var(--text-muted);
cursor: pointer;
display: inline-flex;
font-family: var(--font-body);
font-size: 0.88rem;
gap: 0.4rem;
padding: 0.45rem 0.7rem;
transition: background-color var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}
.ds-tab i {
font-size: 0.9rem;
}
.ds-tab:hover {
border-color: var(--main-fg);
color: var(--text-color);
}
.ds-tab.active {
background: var(--main-fg);
border-color: var(--main-fg);
color: var(--color-black);
}
/* ――― Search / Filter ――― */
.ds-search-row {
align-items: center;
display: flex;
gap: 0.5rem;
margin-bottom: var(--margin-lg);
}
.ds-search {
background-color: var(--input-bg);
border: var(--border-style-input);
border-radius: var(--border-radius-lg);
box-sizing: border-box;
color: var(--text-color);
font-family: var(--font-body);
font-size: var(--font-size-base);
margin-bottom: 0;
outline: none;
padding: var(--padding-sm) var(--padding-base);
transition: box-shadow var(--transition-fast);
width: 100%;
flex: 1;
}
.ds-search:focus {
box-shadow: 0 0 0 2px var(--main-fg);
}
.ds-search-clear {
background: var(--input-bg);
border: var(--border-style-main);
border-radius: var(--border-radius-base);
color: var(--text-color);
cursor: pointer;
font-family: var(--font-body);
font-size: var(--font-size-sm);
min-width: 4.75rem;
padding: 0.6rem 0.8rem;
transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}
.ds-search-clear:hover {
background: var(--main-fg);
border-color: var(--main-fg);
color: var(--color-black);
}
/* ――― Section Cards ――― */
.ds-section {
background-color: var(--secondary-bg);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-sm);
margin-bottom: var(--margin-lg);
overflow: hidden;
transition: box-shadow var(--transition-fast);
}
.ds-section:hover {
box-shadow: var(--shadow-md);
}
.ds-section-header {
align-items: center;
background-color: var(--input-bg);
display: flex;
justify-content: space-between;
padding: var(--padding-sm) var(--padding-base);
user-select: none;
}
.ds-static-header {
cursor: default;
}
.ds-section-header i {
color: var(--main-fg);
font-size: var(--font-size-lg);
margin-right: var(--margin-sm);
min-width: 1.5rem;
text-align: center;
}
.ds-section-title {
color: var(--text-color);
flex: 1;
font-size: var(--font-size-base);
font-weight: var(--font-weight-bold);
}
.ds-section-chevron {
color: var(--text-muted);
font-size: var(--font-size-sm);
transition: transform var(--transition-fast);
}
.ds-section.collapsed .ds-section-chevron {
transform: rotate(-90deg);
}
.ds-section-body {
max-height: 5000px;
overflow: hidden;
transition: max-height 0.3s ease-in-out;
}
.ds-section.collapsed .ds-section-body {
max-height: 0;
}
/* ――― Individual Toggle Rows ――― */
.ds-row {
align-items: center;
border-top: 1px solid var(--sidebar-border-color);
display: flex;
justify-content: space-between;
padding: var(--padding-sm) var(--padding-base);
transition: background-color var(--transition-fast);
}
.ds-row:hover {
background-color: var(--sidebar-active-bg);
}
.ds-row-label {
color: var(--text-color);
flex: 1;
font-size: var(--font-size-sm);
margin-right: var(--margin-base);
}
.ds-row-desc {
color: var(--text-muted);
font-size: var(--font-size-xs);
margin-top: 0.25rem;
line-height: 1.3;
white-space: pre-line;
}
/* ――― Child Row Modifier (Sub-menus) ――― */
.ds-child-modifier {
border-left: 2px solid var(--color-gray-200);
margin-left: 1rem;
padding-left: 1rem;
}
.ds-manage-btn {
display: inline-flex;
align-items: center;
background-color: var(--color-gray-200);
border: 1px solid var(--color-gray-300);
border-radius: 1rem;
color: var(--main-fg);
cursor: pointer;
font-size: 0.75rem;
font-weight: 500;
margin-top: 0.6rem;
padding: 0.25rem 0.75rem;
transition: all var(--transition-fast);
user-select: none;
width: fit-content;
}
.ds-manage-btn:hover {
background-color: var(--color-gray-300);
}
.ds-manage-btn i {
margin-left: 0.3rem;
}
/* ――― Toggle Switch ――― */
.ds-toggle {
appearance: none;
background-color: var(--track-color);
border: none;
border-radius: 1rem;
flex-shrink: 0;
height: 1.5rem;
outline: none;
position: relative;
transition: background-color var(--transition-fast);
width: 2.75rem;
}
.ds-toggle::after {
background-color: var(--color-gray-300);
border-radius: 50%;
content: "";
height: 1.1rem;
left: 0.2rem;
position: absolute;
top: 0.2rem;
transition: transform var(--transition-fast), background-color var(--transition-fast);
width: 1.1rem;
}
.ds-toggle:checked {
background-color: var(--main-fg);
}
.ds-select {
appearance: none;
background-color: var(--color-gray-950);
border: 1px solid var(--track-color);
border-radius: 0.5rem;
color: var(--color-gray-200);
cursor: pointer;
font-family: inherit;
font-size: 0.95rem;
padding: 0.5rem 2rem 0.5rem 1rem;
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
width: 100%;
max-width: 250px;
background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/200.svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.5rem center;
background-size: 1em;
}
.ds-select:hover {
border-color: var(--color-gray-500);
}
.ds-select:focus {
border-color: var(--main-fg);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
outline: none;
}
.ds-toggle:checked::after {
background-color: var(--color-white);
transform: translateX(1.25rem);
}
.ds-toggle:disabled {
opacity: var(--disabled-opacity);
}
/* ――― Loading State ――― */
.ds-loading {
color: var(--text-muted);
padding: var(--padding-xl);
text-align: center;
}
.ds-status-bar {
align-items: center;
color: var(--text-muted);
display: flex;
font-size: var(--font-size-xs);
gap: var(--gap-sm);
justify-content: space-between;
margin-bottom: var(--margin-sm);
}
/* ――― Empty Filter State ――― */
.ds-empty {
color: var(--text-muted);
font-style: italic;
padding: var(--padding-lg);
text-align: center;
}
/* ――― Custom Range Slider ――― */
.ds-row-numeric {
align-items: flex-start;
flex-direction: column;
}
.ds-row-info {
align-items: center;
display: flex;
justify-content: space-between;
margin-bottom: var(--margin-sm);
width: 100%;
}
.ds-row-value {
background-color: var(--sidebar-bg);
border: var(--border-style-main);
border-radius: var(--border-radius-base);
color: var(--text-color);
font-family: var(--font-body);
font-size: var(--font-size-sm);
padding: 0.2rem 0.6rem;
}
.ds-stepper-container {
width: 100%;
}
@media (min-width: 768px) {
.ds-stepper-container {
max-width: 350px;
}
}
.ds-stepper {
align-items: center;
background: var(--input-bg);
border: var(--border-style-main);
border-radius: var(--border-radius-base);
display: grid;
gap: var(--gap-sm);
grid-template-columns: 56px 1fr 56px;
padding: 0.45rem;
}
.ds-stepper-meta {
align-items: center;
color: var(--text-muted);
display: flex;
flex-direction: column;
font-size: var(--font-size-xs);
gap: 0.1rem;
justify-content: center;
min-height: 2rem;
text-align: center;
}
.ds-default-value {
color: var(--text-color);
font-weight: var(--font-weight-semibold);
}
.ds-step-value {
color: var(--text-muted);
}
.ds-manual-row {
align-items: center;
display: flex;
gap: 0.35rem;
margin-top: 0.15rem;
width: 100%;
}
.ds-manual-input {
background: var(--sidebar-bg);
border: var(--border-style-main);
border-radius: var(--border-radius-sm);
color: var(--text-color);
flex: 1;
font-family: var(--font-body);
font-size: var(--font-size-xs);
min-width: 0;
padding: 0.22rem 0.35rem;
}
.ds-manual-input:focus {
border-color: var(--main-fg);
outline: none;
}
.ds-apply-btn {
background: transparent;
border: var(--border-style-main);
border-radius: var(--border-radius-sm);
color: var(--text-color);
cursor: pointer;
font-size: var(--font-size-xs);
padding: 0.2rem 0.45rem;
transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
white-space: nowrap;
}
.ds-apply-btn:hover:not(:disabled) {
background: var(--main-fg);
border-color: var(--main-fg);
color: var(--color-black);
}
.ds-apply-btn:disabled {
cursor: not-allowed;
opacity: var(--disabled-opacity);
}
.ds-reset-btn {
background: transparent;
border: var(--border-style-main);
border-radius: var(--border-radius-sm);
color: var(--text-color);
cursor: pointer;
font-size: var(--font-size-xs);
margin-top: 0.15rem;
padding: 0.2rem 0.45rem;
transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}
.ds-reset-btn:hover:not(:disabled) {
background: var(--main-fg);
border-color: var(--main-fg);
color: var(--color-black);
}
.ds-reset-btn:disabled {
cursor: not-allowed;
opacity: var(--disabled-opacity);
}
.ds-stepper-btn {
align-items: center;
background: var(--sidebar-bg);
border: var(--border-style-main);
border-radius: var(--border-radius-base);
color: var(--text-color);
cursor: pointer;
display: inline-flex;
font-size: 1.25rem;
font-weight: var(--font-weight-bold);
height: 2.1rem;
justify-content: center;
line-height: 1;
transition: background-color var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast);
}
.ds-stepper-btn:hover:not(:disabled) {
background: var(--main-fg);
border-color: var(--main-fg);
color: var(--color-black);
transform: translateY(-1px);
}
.ds-stepper-btn:disabled {
cursor: not-allowed;
opacity: var(--disabled-opacity);
}
/* ――― Mobile ――― */
@media only screen and (max-width: 768px) and (orientation: portrait) {
.ds-wrapper {
padding: var(--padding-sm) var(--padding-base) var(--padding-xl);
}
.ds-search-row {
align-items: stretch;
flex-direction: column;
}
.ds-search-clear {
width: 100%;
}
.ds-tabs {
flex-wrap: nowrap;
overflow-x: auto;
padding-bottom: 0.25rem;
scrollbar-width: none;
}
.ds-tabs::-webkit-scrollbar {
display: none;
}
}
@@ -0,0 +1,825 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
const endpointOptionsCache = {}
const endpointOptionsInflight = {}
// Plain variables — scheduling/routing flags that must NOT be reactive
let syncScheduled = false
let lastParams = null
// Module-level state (persists across route changes)
const state = reactive({
layout: [],
allKeys: [],
paramMetaByKey: {},
values: {},
defaultValues: {},
loadingLayout: true,
loadingValues: true,
filter: "",
expanded: {},
fetched: false,
activeSectionSlug: "",
numericUpdating: {},
})
function slugifySectionName(name) {
return String(name || "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
}
function getSectionsWithSlug() {
return state.layout.map(section => ({
...section,
slug: slugifySectionName(section.name),
}))
}
function toSelectValue(value) {
return value === null || value === undefined ? "" : String(value)
}
function resolveEndpointTemplate(template) {
if (!template) return ""
return String(template).replace(/\{([A-Za-z0-9_]+)\}/g, (_, key) => {
return encodeURIComponent(toSelectValue(state.values[key]))
})
}
function scheduleSyncInputs() {
if (syncScheduled) return
syncScheduled = true
requestAnimationFrame(() => {
syncScheduled = false
syncInputs()
})
}
function applySelectOptions(el, options) {
el.innerHTML = ""
for (const opt of options || []) {
const o = document.createElement("option")
o.value = String(opt.value)
o.textContent = opt.label
el.appendChild(o)
}
}
async function hydrateEndpointOptions(el, key, endpoint) {
if (endpointOptionsCache[endpoint]) {
applySelectOptions(el, endpointOptionsCache[endpoint])
el.dataset.hydrated = "1"
el.value = toSelectValue(state.values[key])
return
}
if (!endpointOptionsInflight[endpoint]) {
endpointOptionsInflight[endpoint] = fetch(endpoint)
.then(r => r.json())
.then(options => {
endpointOptionsCache[endpoint] = options
return options
})
.catch(() => null)
.finally(() => {
delete endpointOptionsInflight[endpoint]
})
}
const options = await endpointOptionsInflight[endpoint]
if (!options || !el.isConnected) return
applySelectOptions(el, options)
el.dataset.hydrated = "1"
el.value = toSelectValue(state.values[key])
}
function syncInputs() {
// Sync checkboxes — set DOM property directly (attribute alone is unreliable)
for (const el of document.querySelectorAll("input[type='checkbox'].ds-toggle[id^='ds-']")) {
el.checked = !!state.values[el.id.slice(3)]
}
// Sync selects — hydrate options + set value
for (const el of document.querySelectorAll("select.ds-select[id^='ds-']")) {
const key = el.id.slice(3)
const endpointTemplate = el.getAttribute("data-endpoint")
const endpoint = resolveEndpointTemplate(endpointTemplate)
const inlineOptions = state.paramMetaByKey[key]?.options
if (endpoint) {
if (!el.dataset.hydrated || el.dataset.endpoint !== endpoint) {
el.dataset.endpoint = endpoint
hydrateEndpointOptions(el, key, endpoint)
} else {
el.value = toSelectValue(state.values[key])
}
continue
}
if (Array.isArray(inlineOptions) && inlineOptions.length > 0) {
if (!el.dataset.hydrated) {
applySelectOptions(el, inlineOptions)
el.dataset.hydrated = "1"
}
el.value = toSelectValue(state.values[key])
}
}
}
async function fetchDefaultValues() {
try {
const defaultsRes = await fetch("/api/params/defaults")
if (!defaultsRes.ok) return false
const defaultsData = await defaultsRes.json()
state.defaultValues = defaultsData || {}
return true
} catch (e) {
return false
}
}
async function fetchLayoutAndParams() {
state.loadingLayout = true
state.loadingValues = true
try {
const layoutRes = await fetch("/assets/components/tools/device_settings_layout.json")
const rawLayoutData = await layoutRes.json()
const layoutData = rawLayoutData
.map(section => ({
...section,
params: (section.params || []).filter(param => param.key !== "Model"),
}))
.filter(section => section.params.length > 0)
state.layout = layoutData
const keys = []
const paramMetaByKey = {}
for (const section of layoutData) {
for (const p of section.params) {
keys.push(p.key)
paramMetaByKey[p.key] = p
}
}
state.allKeys = keys
state.paramMetaByKey = paramMetaByKey
} catch (e) {
console.error("Failed to fetch UI layout:", e)
}
state.loadingLayout = false
// Pull params once at page load; local state handles subsequent edits.
try {
const valuesRes = await fetch("/api/params/all")
const data = await valuesRes.json()
state.values = data
if (!(await fetchDefaultValues())) {
state.defaultValues = {}
}
} catch (e) {
console.error("Failed to fetch param values:", e)
state.defaultValues = {}
}
state.loadingValues = false
// Resolve slug now that layout is available (uses stored route params)
resolveActiveSectionSlug(lastParams)
scheduleSyncInputs()
}
function formatSliderValue(val, stepStr, precisionInt, key) {
if (val === null || val === undefined) return "--"
const v = parseFloat(val)
if (Number.isNaN(v)) return val
const volumeKeys = [
"DisengageVolume", "EngageVolume", "PromptVolume",
"PromptDistractedVolume", "RefuseVolume",
"WarningImmediateVolume", "WarningSoftVolume",
]
if (key && volumeKeys.includes(key)) {
if (v === 0) return "Muted"
if (v === 101) return "Auto"
return `${v}%`
}
if (precisionInt !== undefined && precisionInt !== null) {
return Number(v.toFixed(precisionInt)).toString()
}
if (!stepStr || !stepStr.includes(".")) return Math.round(v).toString()
const dec = stepStr.split(".")[1].length
return Number(v.toFixed(dec)).toString()
}
function formatNumericForInput(value, precision) {
const n = Number(value)
if (!Number.isFinite(n)) return ""
return Number(n.toFixed(precision)).toString()
}
function formatStepValue(step, precision) {
const n = Number(step)
if (!Number.isFinite(n)) return "1"
return Number(n.toFixed(Math.max(0, precision))).toString()
}
function numericBounds(param) {
const defaultBounds = {
min: param.min !== undefined ? param.min : (param.data_type === "float" ? 0.0 : 0),
max: param.max !== undefined ? param.max : (param.data_type === "float" ? 100.0 : 100),
step: param.step !== undefined ? param.step : (param.data_type === "float" ? 0.01 : 1),
}
const toFinite = (value) => {
const n = Number(value)
return Number.isFinite(n) ? n : null
}
if (param.key === "ScreenBrightness") {
return { min: 1, max: 101, step: 1 }
}
if (param.key === "ScreenBrightnessOnroad") {
return { min: 0, max: 101, step: 1 }
}
// Personality jerk params are stored as percentage-style integers (25..200).
// Layout metadata currently uses normalized 0.5..3.0 ranges, which breaks
// the +/- stepper and clamps values like 50 down to 3.
if (/^(Traffic|Aggressive|Standard|Relaxed)Jerk(Acceleration|Deceleration|Danger|SpeedDecrease|Speed)$/.test(String(param.key || ""))) {
return { min: 25, max: 200, step: 1 }
}
if (param.key === "SteerKP") {
const base = toFinite(state.values.SteerKPStock) || toFinite(state.values.SteerKP) || 0.6
return { min: +(base * 0.5).toFixed(2), max: +(base * 1.5).toFixed(2), step: 0.01 }
}
if (param.key === "SteerLatAccel") {
const base = toFinite(state.values.SteerLatAccelStock) || toFinite(state.values.SteerLatAccel) || 2.0
return { min: +(base * 0.5).toFixed(2), max: +(base * 1.25).toFixed(2), step: 0.01 }
}
if (param.key === "SteerRatio") {
const base = toFinite(state.values.SteerRatioStock) || toFinite(state.values.SteerRatio) || 15.0
return { min: +(base * 0.25).toFixed(2), max: +(base * 1.5).toFixed(2), step: 0.01 }
}
return defaultBounds
}
function coerceValueByType(rawValue, dataType) {
if (dataType === "int") {
const n = Number.parseInt(rawValue, 10)
return Number.isFinite(n) ? n : rawValue
}
if (dataType === "float") {
const n = Number.parseFloat(rawValue)
return Number.isFinite(n) ? n : rawValue
}
return rawValue
}
function stepPrecision(step, explicitPrecision) {
if (explicitPrecision !== undefined && explicitPrecision !== null && explicitPrecision !== "") {
const parsed = Number.parseInt(explicitPrecision, 10)
if (Number.isFinite(parsed) && parsed >= 0) return parsed
}
const stepStr = String(step ?? "")
if (!stepStr.includes(".")) return 0
return stepStr.split(".")[1].length
}
function clampNumeric(value, min, max) {
return Math.min(max, Math.max(min, value))
}
function snapNumericToBoundsAndStep(rawValue, bounds, precision) {
const min = Number(bounds.min)
const max = Number(bounds.max)
const step = Number(bounds.step)
const value = Number(rawValue)
if (!Number.isFinite(min) || !Number.isFinite(max) || !Number.isFinite(value)) return null
const clamped = clampNumeric(value, min, max)
if (!Number.isFinite(step) || step <= 0) {
return clampNumeric(Number(clamped.toFixed(precision)), min, max)
}
const snapped = min + Math.round((clamped - min) / step) * step
return clampNumeric(Number(snapped.toFixed(precision)), min, max)
}
function resolveCurrentNumericValue(param, bounds) {
const raw = state.values[param.key]
const precision = stepPrecision(bounds.step, param.precision)
const snapped = snapNumericToBoundsAndStep(raw, bounds, precision)
if (snapped !== null) return snapped
const fallback = Number(bounds.min)
return Number.isFinite(fallback) ? fallback : 0
}
function resolveDefaultNumericValue(param, bounds) {
const precision = stepPrecision(bounds.step, param.precision)
const stockKey = `${param.key}Stock`
// Prefer live vehicle stock values when available.
const liveStock = snapNumericToBoundsAndStep(state.values?.[stockKey], bounds, precision)
if (liveStock !== null) return liveStock
// Fallback to default table stock value if present.
const defaultStock = snapNumericToBoundsAndStep(state.defaultValues?.[stockKey], bounds, precision)
if (defaultStock !== null) return defaultStock
// Final fallback: generic param default.
return snapNumericToBoundsAndStep(state.defaultValues?.[param.key], bounds, precision)
}
function isNumericUpdating(key) {
return !!state.numericUpdating[key]
}
function showParamSnackbar(message, level, timeout = 2200) {
showSnackbar(message, level, timeout, {
key: "device-settings-param-update",
replace: true,
})
}
function syncNumericDisplay(param, rawValue) {
const displayEl = document.getElementById(`ds-display-${param.key}`)
if (!displayEl) return
const bounds = numericBounds(param)
displayEl.textContent = formatSliderValue(
rawValue,
String(bounds.step),
param.precision,
param.key,
)
}
async function updateNumericParam(param, numericValue, options = {}) {
const key = param.key
const current = state.values[key]
const successMessage = options.successMessage
state.numericUpdating = { ...state.numericUpdating, [key]: true }
state.values = { ...state.values, [key]: numericValue }
syncNumericDisplay(param, numericValue)
try {
const res = await fetch("/api/params", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key, value: coerceValueByType(numericValue, param.data_type) }),
})
const data = await res.json()
if (res.ok) {
const updated = (data.updated && typeof data.updated === "object") ? data.updated : {}
const resolvedValue = Object.prototype.hasOwnProperty.call(updated, key) ? updated[key] : numericValue
state.values = { ...state.values, [key]: resolvedValue, ...updated }
state.numericUpdating = { ...state.numericUpdating, [key]: false }
syncNumericDisplay(param, resolvedValue)
showParamSnackbar(successMessage || data.message || `Parameter '${key}' updated.`)
scheduleSyncInputs()
} else {
state.values = { ...state.values, [key]: current }
state.numericUpdating = { ...state.numericUpdating, [key]: false }
syncNumericDisplay(param, current)
showParamSnackbar(data.error || "Failed to update parameter", "error")
}
} catch (e) {
state.values = { ...state.values, [key]: current }
state.numericUpdating = { ...state.numericUpdating, [key]: false }
syncNumericDisplay(param, current)
showParamSnackbar("Network error — is the device reachable?", "error")
}
}
function stepNumericParam(param, direction) {
const bounds = numericBounds(param)
const min = Number(bounds.min)
const max = Number(bounds.max)
const step = Number(bounds.step)
if (!Number.isFinite(min) || !Number.isFinite(max) || !Number.isFinite(step) || step <= 0) return
if (isNumericUpdating(param.key)) return
const current = resolveCurrentNumericValue(param, bounds)
const precision = stepPrecision(step, param.precision)
const epsilon = Math.pow(10, -(precision + 2))
const next = snapNumericToBoundsAndStep(current + (direction * step), bounds, precision)
if (next === null) return
if (Math.abs(next - current) <= epsilon) return
updateNumericParam(param, next)
}
function applyManualNumericParam(param) {
if (isNumericUpdating(param.key)) return
const inputEl = document.getElementById(`ds-manual-${param.key}`)
if (!inputEl) return
const raw = String(inputEl.value ?? "").trim()
if (!raw) {
showParamSnackbar("Enter a value first.", "error")
return
}
const parsed = Number.parseFloat(raw)
if (!Number.isFinite(parsed)) {
showParamSnackbar("Enter a valid number.", "error")
return
}
const bounds = numericBounds(param)
const precision = stepPrecision(bounds.step, param.precision)
const snapped = snapNumericToBoundsAndStep(parsed, bounds, precision)
if (snapped === null) {
showParamSnackbar("Value is out of range.", "error")
return
}
inputEl.value = formatNumericForInput(snapped, precision)
const current = resolveCurrentNumericValue(param, bounds)
const epsilon = Math.pow(10, -(precision + 2))
if (Math.abs(snapped - current) <= epsilon) return
updateNumericParam(param, snapped)
}
async function resetNumericParam(param) {
const bounds = numericBounds(param)
let defaultValue = resolveDefaultNumericValue(param, bounds)
if (defaultValue === null) {
const loaded = await fetchDefaultValues()
if (!loaded) {
showParamSnackbar("Couldn't load defaults. Try refreshing the page.", "error")
return
}
defaultValue = resolveDefaultNumericValue(param, bounds)
}
if (defaultValue === null) {
showParamSnackbar("No default value available for this setting.", "error")
return
}
if (isNumericUpdating(param.key)) return
const current = resolveCurrentNumericValue(param, bounds)
const precision = stepPrecision(bounds.step, param.precision)
const epsilon = Math.pow(10, -(precision + 2))
if (Math.abs(defaultValue - current) <= epsilon) return
updateNumericParam(param, defaultValue, {
successMessage: `Parameter '${param.key}' reset to default.`,
})
}
async function updateParam(key, elType) {
const current = state.values[key]
const el = document.getElementById(`ds-${key}`)
if (!el) return
const param = state.paramMetaByKey[key] || {}
let formattedVal
if (elType === "checkbox") {
formattedVal = !!el.checked
} else if (elType === "dropdown") {
formattedVal = coerceValueByType(el.value, param.data_type)
} else {
formattedVal = coerceValueByType(el.value, param.data_type)
}
try {
const res = await fetch("/api/params", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key, value: formattedVal }),
})
const data = await res.json()
if (res.ok) {
const updated = (data.updated && typeof data.updated === "object") ? data.updated : {}
state.values = { ...state.values, [key]: formattedVal, ...updated }
showParamSnackbar(data.message || `Parameter '${key}' updated.`)
scheduleSyncInputs()
} else {
revertInput(key, current, elType)
showParamSnackbar(data.error || "Failed to update parameter", "error")
}
} catch (e) {
revertInput(key, current, elType)
showParamSnackbar("Network error — is the device reachable?", "error")
}
}
function revertInput(key, current, elType) {
const el = document.getElementById(`ds-${key}`)
if (!el) return
if (elType === "checkbox") {
el.checked = !!current
return
}
if (elType === "dropdown") {
el.value = toSelectValue(current)
return
}
el.value = current
}
function toggleManage(key) {
state.expanded = { ...state.expanded, [key]: !state.expanded[key] }
scheduleSyncInputs()
}
function matchesFilter(p) {
if (!state.filter) return true
const q = state.filter.toLowerCase()
return p.label.toLowerCase().includes(q) || p.key.toLowerCase().includes(q)
}
function clearSearchFilter() {
if (!state.filter) return
state.filter = ""
scheduleSyncInputs()
}
function handleSectionTabClick(sectionSlug, event) {
if (!sectionSlug || sectionSlug === state.activeSectionSlug) return
// Preserve horizontal tab strip position on mobile when switching sections.
const tabsEl = event?.currentTarget?.closest(".ds-tabs")
const preservedScrollLeft = tabsEl ? tabsEl.scrollLeft : null
state.activeSectionSlug = sectionSlug
if (preservedScrollLeft !== null) {
requestAnimationFrame(() => {
const nextTabsEl = document.getElementById("ds-tabs")
if (nextTabsEl) nextTabsEl.scrollLeft = preservedScrollLeft
})
}
}
function renderSettingRow(p) {
if (p.parent_key && !state.filter) {
if (!state.values[p.parent_key]) return ""
if (!state.expanded[p.parent_key]) return ""
}
const isNumeric = p.ui_type === "numeric"
const isChild = p.parent_key ? "ds-child-modifier" : ""
return html`
<div class="ds-row ${isNumeric ? "ds-row-numeric" : ""} ${isChild}">
<div class="ds-row-info">
<div class="ds-row-text">
<span class="ds-row-label">${p.label}</span>
${p.description ? html`<div class="ds-row-desc">${p.description}</div>` : ""}
${() => p.is_parent_toggle && state.values[p.key] ? html`
<div class="ds-manage-btn" @click="${() => toggleManage(p.key)}">
${state.expanded[p.key] ? "Close" : "Manage"}
<i class="bi bi-chevron-${state.expanded[p.key] ? "up" : "down"}"></i>
</div>
` : ""}
</div>
${isNumeric ? html`<span class="ds-row-value" id="ds-display-${p.key}">${() => {
const currentValue = state.values[p.key]
const bounds = numericBounds(p)
return currentValue !== undefined ? formatSliderValue(currentValue, String(bounds.step), p.precision, p.key) : ".."
}}</span>` : ""}
</div>
${isNumeric ? html`
<div class="ds-stepper-container">
${(() => {
const bounds = numericBounds(p)
const currentNumeric = resolveCurrentNumericValue(p, bounds)
const precision = stepPrecision(bounds.step, p.precision)
const epsilon = Math.pow(10, -(precision + 2))
const updating = isNumericUpdating(p.key)
const canDecrease = !updating && currentNumeric > (Number(bounds.min) + epsilon)
const canIncrease = !updating && currentNumeric < (Number(bounds.max) - epsilon)
const defaultNumeric = resolveDefaultNumericValue(p, bounds)
const defaultLabel = defaultNumeric !== null
? formatSliderValue(defaultNumeric, String(bounds.step), p.precision, p.key)
: "N/A"
const canReset = !updating && defaultNumeric !== null && Math.abs(defaultNumeric - currentNumeric) > epsilon
const stepLabel = formatStepValue(bounds.step, precision)
return html`
<div class="ds-stepper">
<button
class="ds-stepper-btn"
?disabled="${!canDecrease}"
@click="${() => stepNumericParam(p, -1)}">-</button>
<div class="ds-stepper-meta">
<span>${formatSliderValue(bounds.min, String(bounds.step), p.precision, p.key)} to ${formatSliderValue(bounds.max, String(bounds.step), p.precision, p.key)}</span>
<span class="ds-step-value">Step: ${stepLabel} per click</span>
<span class="ds-default-value">Default: ${defaultLabel}</span>
<div class="ds-manual-row">
<input
type="number"
class="ds-manual-input"
id="ds-manual-${p.key}"
min="${bounds.min}"
max="${bounds.max}"
step="${bounds.step}"
?disabled="${updating}"
value="${() => formatNumericForInput(resolveCurrentNumericValue(p, numericBounds(p)), precision)}"
@keydown="${(e) => {
if (e.key !== "Enter") return
e.preventDefault()
applyManualNumericParam(p)
}}" />
<button
class="ds-apply-btn"
?disabled="${updating}"
@click="${() => applyManualNumericParam(p)}">Apply</button>
</div>
<button
class="ds-reset-btn"
?disabled="${!canReset}"
@click="${() => resetNumericParam(p)}">Reset to Default</button>
</div>
<button
class="ds-stepper-btn"
?disabled="${!canIncrease}"
@click="${() => stepNumericParam(p, 1)}">+</button>
</div>
`
})()}
</div>
` : p.ui_type === "dropdown" ? html`
<select
class="ds-select"
id="ds-${p.key}"
data-endpoint="${p.options_endpoint || ""}"
@change="${() => updateParam(p.key, "dropdown")}">
<option value="">Loading...</option>
</select>
` : html`
<input
type="checkbox"
class="ds-toggle"
id="ds-${p.key}"
@change="${() => updateParam(p.key, "checkbox")}" />
`}
</div>
`
}
// Resolve the active section slug imperatively — NEVER inside a reactive expression
function resolveActiveSectionSlug(params) {
if (state.layout.length === 0) return
const sections = getSectionsWithSlug()
const validSlugs = new Set(sections.map(s => s.slug))
const requestedSlug = String(params?.section || "").toLowerCase()
const fallbackSlug = sections[0].slug
const nextSlug = validSlugs.has(requestedSlug)
? requestedSlug
: (validSlugs.has(state.activeSectionSlug) ? state.activeSectionSlug : fallbackSlug)
if (state.activeSectionSlug !== nextSlug) {
state.activeSectionSlug = nextSlug
}
}
export function DeviceSettings({ params }) {
lastParams = params
if (!state.fetched) {
state.fetched = true
fetchLayoutAndParams()
}
// Resolve slug imperatively (safe: runs in function body, not reactive context)
resolveActiveSectionSlug(params)
return html`
<div class="ds-wrapper">
<h2>Toggles</h2>
<div class="ds-search-row">
<input
class="ds-search"
type="text"
placeholder="Search settings..."
value="${() => state.filter}"
@keydown="${(e) => {
if (e.key === "Escape") clearSearchFilter()
}}"
@input="${(e) => {
state.filter = e.target.value
scheduleSyncInputs()
}}" />
${() => state.filter ? html`
<button
class="ds-search-clear"
@click="${() => clearSearchFilter()}">
Clear
</button>
` : ""}
</div>
${() => {
if (state.loadingLayout || state.loadingValues) {
return html`<div class="ds-loading">Loading configuration...</div>`
}
const sections = getSectionsWithSlug()
if (sections.length === 0) {
return html`<div class="ds-empty">No settings available.</div>`
}
// Sync DOM inputs after ArrowJS renders (safe: syncScheduled is non-reactive)
scheduleSyncInputs()
// Search active → show matching results from ALL sections
if (state.filter) {
const MAX_PER_SECTION = 25
const searchResults = sections
.map(s => ({ ...s, matches: s.params.filter(p => matchesFilter(p)) }))
.filter(s => s.matches.length > 0)
const totalMatches = searchResults.reduce((n, s) => n + s.matches.length, 0)
return html`
<div class="ds-status-bar">
<span>${totalMatches} result${totalMatches !== 1 ? "s" : ""} across ${searchResults.length} section${searchResults.length !== 1 ? "s" : ""}</span>
<span>${state.allKeys.length} total mapped</span>
</div>
${searchResults.map(section => html`
<div class="ds-section">
<div class="ds-section-header ds-static-header">
<i class="bi ${section.icon}"></i>
<span class="ds-section-title">${section.name} (${section.matches.length})</span>
</div>
<div class="ds-section-body">
${section.matches.slice(0, MAX_PER_SECTION).map(p => renderSettingRow(p))}
${section.matches.length > MAX_PER_SECTION ? html`<div class="ds-row"><span class="ds-row-label" style="opacity:0.5">+${section.matches.length - MAX_PER_SECTION} more — refine your search</span></div>` : ""}
</div>
</div>
`)}
${totalMatches === 0 ? html`<div class="ds-empty">No settings match your search.</div>` : ""}
`
}
// No search → normal tab-based single-section view
const activeSection = sections.find(s => s.slug === state.activeSectionSlug) || sections[0]
const visibleParams = activeSection.params.filter(p => matchesFilter(p))
return html`
<div class="ds-tabs" id="ds-tabs">
${sections.map(section => html`
<button
class="ds-tab ${section.slug === state.activeSectionSlug ? "active" : ""}"
@click="${(e) => {
handleSectionTabClick(section.slug, e)
}}">
<i class="bi ${section.icon}"></i>
<span>${section.name}</span>
</button>
`)}
</div>
<div class="ds-status-bar">
<span>${activeSection.params.length} settings in ${activeSection.name}</span>
<span>${state.allKeys.length} total mapped</span>
</div>
<div class="ds-section">
<div class="ds-section-header ds-static-header">
<i class="bi ${activeSection.icon}"></i>
<span class="ds-section-title">${activeSection.name} (${visibleParams.length})</span>
</div>
<div class="ds-section-body">
${visibleParams.map(p => renderSettingRow(p))}
</div>
</div>
${visibleParams.length === 0 ? html`<div class="ds-empty">No settings match your search.</div>` : ""}
`
}}
</div>
`
}
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
import { html } from "https://esm.sh/@arrow-js/core"
import { html } from "/assets/vendor/arrow-core.js"
export function DoorControl () {
async function lockDoors () {
@@ -1,4 +1,4 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core";
import { html, reactive } from "/assets/vendor/arrow-core.js";
import { formatSecondsToHuman, parseErrorLogToDate } from "/assets/js/utils.js";
import { Modal } from "/assets/components/modal.js";
@@ -0,0 +1,137 @@
/* ――― Galaxy Pairing Widget ――― */
.galaxy-wrapper {
max-width: var(--width-xxxl);
padding: var(--padding-base) var(--padding-lg) var(--padding-xxl);
}
.galaxy-loading {
color: var(--text-muted);
padding: var(--padding-xl);
text-align: center;
}
.galaxy-widget {
align-items: center;
background-color: var(--secondary-bg);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
gap: var(--gap-lg);
max-width: var(--width-lg);
padding: var(--padding-xl);
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
}
.galaxy-widget:hover {
box-shadow: var(--shadow-md);
transform: var(--hover-scale-sm);
}
/* ――― Status Badge ――― */
.galaxy-status-badge {
align-items: center;
border-radius: 2rem;
display: inline-flex;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-bold);
gap: var(--gap-xs);
padding: 0.3rem 1rem;
}
.galaxy-paired {
background-color: rgba(94, 200, 200, 0.15);
color: var(--success-fg);
}
.galaxy-unpaired {
background-color: rgba(224, 85, 119, 0.15);
color: var(--danger-fg);
}
/* ――― Text ――― */
.galaxy-text {
color: var(--text-color);
line-height: var(--line-height-base);
text-align: center;
}
/* ――― URL Link ――― */
.galaxy-url {
color: var(--main-fg);
font-size: var(--font-size-base);
font-weight: var(--font-weight-demi-bold);
text-decoration: underline;
text-underline-offset: 3px;
transition: color var(--transition-fast);
word-break: break-all;
}
.galaxy-url:hover {
color: var(--accent-hover-bg);
}
/* ――― Input Group ――― */
.galaxy-input-group {
display: flex;
flex-direction: column;
gap: var(--gap-sm);
width: 100%;
}
.galaxy-input {
background-color: var(--input-bg);
border: var(--border-style-input);
border-radius: var(--border-radius-lg);
box-sizing: border-box;
color: var(--text-color);
font-family: var(--font-body);
font-size: var(--font-size-base);
outline: none;
padding: var(--padding-sm) var(--padding-base);
transition: box-shadow var(--transition-fast);
width: 100%;
}
.galaxy-input:focus {
box-shadow: 0 0 0 2px var(--main-fg);
}
/* ――― Buttons ――― */
.galaxy-button {
background-color: var(--input-bg);
border: none;
border-radius: var(--border-radius-lg);
color: var(--text-color);
font-size: var(--font-size-base);
font-weight: var(--font-weight-demi-bold);
padding: var(--padding-sm) var(--padding-base);
text-align: center;
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
width: 100%;
}
.galaxy-button:hover:not(:disabled) {
box-shadow: var(--shadow-md);
transform: var(--hover-scale-sm);
}
.galaxy-button:disabled {
opacity: var(--disabled-opacity);
}
.galaxy-button-danger {
background-color: rgba(224, 85, 119, 0.2);
color: var(--danger-fg);
}
.galaxy-button-danger:hover:not(:disabled) {
background-color: rgba(224, 85, 119, 0.35);
}
/* ――― Mobile ――― */
@media only screen and (max-width: 768px) and (orientation: portrait) {
.galaxy-wrapper {
padding: var(--padding-sm) var(--padding-base) var(--padding-xl);
}
}
@@ -0,0 +1,166 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { isGalaxyTunnel } from "/assets/js/utils.js"
import { Modal } from "/assets/components/modal.js"
const state = reactive({
paired: false,
url: "",
password: "",
loading: true,
submitting: false,
showUnpairModal: false,
fetched: false,
})
async function fetchStatus() {
state.loading = true
try {
const res = await fetch("/api/galaxy/status")
const data = await res.json()
state.paired = data.paired
state.url = data.url
} catch (e) {
console.error("Failed to fetch Galaxy status:", e)
}
state.loading = false
}
async function pair() {
if (state.submitting) return
const pw = state.password.trim()
if (pw.length < 6) {
showSnackbar("Password must be at least 6 characters.")
return
}
state.submitting = true
try {
const res = await fetch("/api/galaxy/pair", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password: pw }),
})
const data = await res.json()
if (res.ok) {
state.paired = true
state.url = data.url
state.password = ""
// Clear the DOM input value directly (Arrow.js doesn't two-way bind)
const input = document.querySelector(".galaxy-input")
if (input) input.value = ""
showSnackbar(data.message || "Paired!")
} else {
showSnackbar(data.error || "Pairing failed.")
}
} catch (e) {
showSnackbar("Network error — is the device reachable?")
}
state.submitting = false
}
async function unpair() {
state.showUnpairModal = false
state.submitting = true
try {
const res = await fetch("/api/galaxy/unpair", { method: "POST" })
const data = await res.json()
if (res.ok) {
state.paired = false
state.url = ""
showSnackbar(data.message || "Unpaired!")
} else {
showSnackbar(data.error || "Unpairing failed.")
}
} catch (e) {
showSnackbar("Network error — is the device reachable?")
}
state.submitting = false
}
export function GalaxyPairing() {
if (isGalaxyTunnel()) {
return html`
<div class="tunnel-notice">
<div class="tunnel-notice-icon">🛰</div>
<h3 class="tunnel-notice-title">Galaxy Pairing Unavailable via Galaxy</h3>
<p class="tunnel-notice-body">Galaxy pairing requires a direct connection.<br>Connect to your device's local network to use this feature.</p>
</div>
`;
}
if (!state.fetched) {
state.fetched = true
fetchStatus()
}
return html`
<div class="galaxy-wrapper">
<h2>Galaxy</h2>
${() => {
if (state.loading) {
return html`<div class="galaxy-loading">Checking pairing status…</div>`
}
if (state.paired) {
return html`
<section class="galaxy-widget">
<div class="galaxy-status-badge galaxy-paired">
<i class="bi bi-check-circle-fill"></i> Paired
</div>
<p class="galaxy-text">
Your device is paired with Galaxy. Access it remotely at:
</p>
<a class="galaxy-url" href="${state.url}" target="_blank" rel="noopener">
${state.url}
</a>
<button
class="galaxy-button galaxy-button-danger"
@click="${() => { state.showUnpairModal = true }}"
disabled="${() => state.submitting}"
>
${() => state.submitting ? "Unpairing…" : "Unpair"}
</button>
</section>
${() => state.showUnpairModal ? Modal({
title: "Confirm Unpair",
message: "Are you sure you want to unpair from Galaxy? You will lose remote access until you pair again.",
onConfirm: unpair,
onCancel: () => { state.showUnpairModal = false },
confirmText: "Unpair",
}) : ""}
`
}
return html`
<section class="galaxy-widget">
<div class="galaxy-status-badge galaxy-unpaired">
<i class="bi bi-x-circle-fill"></i> Not Paired
</div>
<p class="galaxy-text">
Pair your device with Galaxy to access The Pond remotely from anywhere.
Enter a password to secure your connection.
</p>
<div class="galaxy-input-group">
<input
class="galaxy-input"
type="password"
placeholder="Password (min 6 characters)"
@input="${(e) => { state.password = e.target.value }}"
@keydown="${(e) => { if (e.key === 'Enter') pair() }}"
/>
<button
class="galaxy-button"
@click="${pair}"
disabled="${() => state.submitting || state.password.trim().length < 6}"
>
${() => state.submitting ? "Pairing…" : "Pair"}
</button>
</div>
</section>
`
}}
</div>
`
}
@@ -0,0 +1,117 @@
.longManeuverPage {
max-width: var(--width-xxl);
}
.longManeuverCard {
background-color: var(--secondary-bg);
border-radius: var(--border-radius-md);
color: var(--text-color);
max-width: 90%;
padding: var(--border-radius-xl);
}
.longManeuverIntro {
margin-top: 0;
}
.longManeuverActions {
display: flex;
gap: var(--gap-sm);
margin-top: var(--margin-base);
}
.longManeuverButton {
background: var(--main-fg);
border: none;
border-radius: var(--border-radius-sm);
color: var(--text-color);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-demi-bold);
padding: var(--padding-sm) var(--padding-base);
}
.longManeuverButton:hover {
transform: var(--hover-scale-sm);
transition: transform var(--transition-fast), background-color var(--transition-fast);
}
.longManeuverButton:disabled {
cursor: not-allowed;
opacity: 0.55;
transform: none;
}
.longManeuverButton.danger {
background: var(--danger-bg);
}
.longManeuverButton.danger:hover {
background: var(--danger-hover-bg);
}
.longManeuverError {
color: var(--danger-fg);
margin-top: var(--margin-base);
}
.longManeuverMuted {
color: var(--text-muted);
}
.longManeuverStatusGrid {
display: grid;
gap: 0.5em 1em;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: var(--margin-base);
}
.longManeuverStatusGrid p {
font-size: var(--font-size-sm);
margin: 0;
}
.longManeuverCurrent {
margin-top: var(--margin-base);
}
.longManeuverCurrent p {
font-size: var(--font-size-sm);
margin: 0 0 var(--margin-xs);
}
.longManeuverInstructions,
.longManeuverHistory {
margin-top: var(--margin-base);
}
.longManeuverInstructions h3,
.longManeuverHistory h3 {
margin-bottom: var(--margin-xs);
}
.longManeuverInstructions ol,
.longManeuverHistory ol {
margin: var(--margin-xs) 0 0;
padding-left: 1.4rem;
}
.longManeuverInstructions li,
.longManeuverHistory li {
font-size: var(--font-size-sm);
margin-bottom: 0.25rem;
}
@media only screen and (max-width: 768px) and (orientation: portrait) {
.longManeuverCard {
max-width: 100%;
}
.longManeuverActions {
flex-direction: column;
}
.longManeuverStatusGrid {
grid-template-columns: 1fr;
}
}
@@ -0,0 +1,183 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
const state = reactive({
loading: true,
busy: false,
error: "",
data: null,
})
let initialized = false
let pollHandle = null
const POLL_INTERVAL_MS = 1000
function isLongitudinalManeuversRouteActive() {
return window.location.pathname === "/longitudinal_maneuvers"
}
function safeNumber(value, fallback = 0) {
const n = Number(value)
return Number.isFinite(n) ? n : fallback
}
function formatAgeSeconds(value) {
const sec = safeNumber(value, -1)
if (sec < 0) return "unknown"
if (sec < 1) return "just now"
if (sec < 60) return `${Math.round(sec)}s ago`
const min = sec / 60
if (min < 60) return `${Math.round(min)}m ago`
const hr = min / 60
return `${Math.round(hr)}h ago`
}
async function fetchStatus() {
try {
const response = await fetch("/api/longitudinal_maneuvers/status")
const payload = await response.json()
if (!response.ok) {
throw new Error(payload.error || response.statusText || "Failed to load maneuver status")
}
state.data = payload
state.error = ""
} catch (error) {
state.error = error?.message || "Failed to load maneuver status"
} finally {
state.loading = false
}
}
function stopPolling() {
if (!pollHandle) return
clearTimeout(pollHandle)
pollHandle = null
}
function ensurePolling() {
if (pollHandle) return
const poll = async () => {
if (!isLongitudinalManeuversRouteActive()) {
pollHandle = null
return
}
if (document.visibilityState !== "visible") {
pollHandle = setTimeout(poll, POLL_INTERVAL_MS)
return
}
await fetchStatus()
pollHandle = setTimeout(poll, POLL_INTERVAL_MS)
}
pollHandle = setTimeout(poll, POLL_INTERVAL_MS)
}
async function runAction(action) {
if (state.busy) return
state.busy = true
try {
const response = await fetch(`/api/longitudinal_maneuvers/${action}`, { method: "POST" })
const payload = await response.json()
if (!response.ok) {
throw new Error(payload.error || response.statusText || `Failed to ${action} maneuvers`)
}
state.data = payload
state.error = ""
showSnackbar(payload.message || "Action complete.")
} catch (error) {
const message = error?.message || `Failed to ${action} maneuvers`
state.error = message
showSnackbar(message, "error")
} finally {
state.busy = false
}
}
function initialize() {
if (initialized) return
initialized = true
fetchStatus()
ensurePolling()
}
function statusLine(label, value) {
return html`<p><strong>${label}:</strong> ${value}</p>`
}
export function LongitudinalManeuvers() {
initialize()
return html`
<div class="longManeuverPage">
<h2>Long Maneuvers</h2>
<div class="longManeuverCard">
<p class="longManeuverIntro">
Run the longitudinal maneuver suite from your phone and monitor progress live.
</p>
<div class="longManeuverActions">
<button
class="longManeuverButton"
?disabled="${state.busy}"
@click="${() => runAction("start")}">
Start / Arm
</button>
<button
class="longManeuverButton danger"
?disabled="${state.busy}"
@click="${() => runAction("stop")}">
Stop
</button>
</div>
${() => state.loading ? html`<p class="longManeuverMuted">Loading status...</p>` : ""}
${() => state.error ? html`<p class="longManeuverError">${state.error}</p>` : ""}
${() => state.data ? html`
<div class="longManeuverStatusGrid">
${statusLine("Mode Enabled", state.data.modeEnabled ? "Yes" : "No")}
${statusLine("State", state.data.state || "idle")}
${statusLine("Onroad", state.data.isOnroad ? "Yes" : "No")}
${statusLine("Engaged", state.data.isEngaged ? "Yes" : "No")}
${statusLine("Phase", state.data.phase || "n/a")}
${statusLine("Paddle Mode", state.data.paddleMode || "auto")}
${statusLine("Step", `${safeNumber(state.data.stepIndex, 0)}/${safeNumber(state.data.stepTotal, 0)}`)}
${statusLine("Run", `${safeNumber(state.data.runIndex, 0)}/${safeNumber(state.data.runTotal, 0)}`)}
${statusLine("Updated", formatAgeSeconds(state.data.updatedAgeSec))}
</div>
<div class="longManeuverCurrent">
<p><strong>Current Maneuver:</strong> ${state.data.maneuver || "n/a"}</p>
<p><strong>Popup Text:</strong> ${state.data.uiText1 || ""} ${state.data.uiText2 ? `| ${state.data.uiText2}` : ""}</p>
</div>
<div class="longManeuverInstructions">
<h3>Quick Guide</h3>
<ol>
<li>Find a large, empty, straight road or lot with no traffic.</li>
<li>Press <strong>Start / Arm</strong> here, then engage openpilot with SET.</li>
<li>Keep full supervision and be ready to disengage at all times.</li>
<li>For GM pedal-long cars, the suite runs both pedal-only and pedal+paddle phases automatically.</li>
<li>When the status says complete, collect logs and generate your HTML report.</li>
</ol>
</div>
<div class="longManeuverHistory">
<h3>Progress Chain</h3>
${() => (state.data.history || []).length ? html`
<ol>
${(state.data.history || []).slice().reverse().map((line) => html`<li>${line}</li>`)}
</ol>
` : html`<p class="longManeuverMuted">No steps logged yet.</p>`}
</div>
` : ""}
</div>
</div>
`
}
@@ -0,0 +1,253 @@
.mm-wrapper {
color: var(--text-color);
max-width: 1200px;
padding-right: var(--padding-base);
}
.mm-toolbar {
align-items: center;
background: var(--card-bg);
border: 1px solid var(--sidebar-border-color);
border-radius: var(--border-radius-lg);
display: flex;
flex-wrap: wrap;
gap: var(--gap-md);
justify-content: space-between;
margin: var(--margin-base) 0;
padding: var(--padding-base);
}
.mm-summary {
color: var(--text-muted);
display: flex;
flex-wrap: wrap;
gap: var(--gap-lg);
}
.mm-actions {
display: flex;
flex-wrap: wrap;
gap: var(--gap-sm);
}
.mm-status {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: var(--gap-sm);
margin-bottom: var(--margin-base);
}
.mm-progress {
background: var(--secondary-bg);
border: 1px solid var(--sidebar-border-color);
border-radius: var(--border-radius-base);
color: var(--text-color);
padding: 0.35rem 0.6rem;
}
.mm-filters {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: var(--gap-sm);
margin-bottom: var(--margin-base);
}
.mm-search,
.mm-select {
background: var(--input-bg);
border: 1px solid var(--sidebar-border-color);
border-radius: var(--border-radius-base);
color: var(--text-color);
font-size: var(--font-size-base);
min-height: 2.15rem;
padding: 0.4rem 0.55rem;
}
.mm-search {
min-width: min(420px, 90vw);
}
.mm-filter-label,
.mm-filter-checkbox {
align-items: center;
color: var(--text-muted);
display: flex;
gap: var(--gap-xs);
}
.mm-filter-break {
display: none;
}
.mm-series {
background: var(--card-bg);
border: 1px solid var(--sidebar-border-color);
border-radius: var(--border-radius-lg);
margin-bottom: var(--margin-base);
overflow: hidden;
}
.mm-series-header {
align-items: center;
background: rgba(255, 255, 255, 0.03);
border-bottom: 1px solid var(--sidebar-border-color);
display: flex;
justify-content: space-between;
padding: 0.55rem 0.8rem;
}
.mm-series-header h3 {
font-size: 1.02rem;
margin: 0;
padding: 0;
}
.mm-series-header span {
color: var(--text-muted);
}
.mm-row {
align-items: center;
border-bottom: 1px solid var(--sidebar-border-color);
display: flex;
gap: var(--gap-md);
justify-content: space-between;
padding: 0.75rem 0.8rem;
}
.mm-row:last-child {
border-bottom: none;
}
.mm-row-main {
min-width: 0;
}
.mm-row-title {
align-items: center;
display: flex;
font-size: 1rem;
gap: var(--gap-xs);
}
.mm-row-meta {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-top: 0.35rem;
}
.mm-row-actions {
align-items: center;
display: flex;
flex-shrink: 0;
gap: var(--gap-sm);
}
.mm-btn {
border: none;
border-radius: var(--border-radius-base);
color: var(--text-color);
min-height: 2.2rem;
min-width: 7.5rem;
padding: 0.45rem 0.8rem;
}
.mm-btn:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.mm-btn-primary {
background: var(--success-bg);
color: var(--color-black);
}
.mm-btn-primary:hover:not(:disabled) {
background: var(--success-hover-bg);
}
.mm-btn-secondary {
background: var(--sidebar-active-bg);
}
.mm-btn-secondary:hover:not(:disabled) {
background: var(--accent-hover-bg);
}
.mm-btn-danger {
background: var(--danger-bg);
}
.mm-btn-danger:hover:not(:disabled) {
background: var(--danger-hover-bg);
}
.mm-chip {
background: var(--secondary-bg);
border: 1px solid var(--sidebar-border-color);
border-radius: 999px;
color: var(--text-muted);
display: inline-flex;
font-size: 0.78rem;
padding: 0.1rem 0.5rem;
}
.mm-chip-active {
background: var(--success-bg);
border-color: transparent;
color: var(--color-black);
}
.mm-chip-favorite {
background: rgba(212, 160, 96, 0.18);
border-color: rgba(212, 160, 96, 0.5);
color: var(--warning-bg);
}
.mm-chip-warning {
background: rgba(224, 85, 119, 0.12);
border-color: rgba(224, 85, 119, 0.45);
color: var(--danger-fg);
}
.mm-star {
background: transparent;
border: none;
color: #e0b45a;
font-size: 1rem;
line-height: 1;
padding: 0;
}
.mm-empty {
color: var(--text-muted);
padding: 0.7rem 0;
}
@media (max-width: 850px) {
.mm-row {
align-items: flex-start;
flex-direction: column;
}
.mm-row-actions {
width: 100%;
}
.mm-btn {
width: 100%;
}
.mm-search {
min-width: 100%;
}
.mm-filter-break {
display: block;
flex-basis: 100%;
height: 0;
}
}
@@ -0,0 +1,586 @@
import { html, reactive } from "/assets/vendor/arrow-core.js";
const state = reactive({
loading: true,
refreshing: false,
error: "",
actionBusy: false,
sortMode: "alphabetical",
communityFavoriteFilter: "all",
models: [],
currentModel: "",
summary: { installed: 0, missing: 0, total: 0 },
status: {
modelToDownload: "",
downloadAll: false,
downloading: false,
cancelling: false,
progress: "",
isOnroad: false,
terminal: false,
},
});
let initialized = false;
let pollingHandle = null;
let statusInFlight = false;
let lastStatusSignature = "";
const REQUEST_TIMEOUT_MS = 20000;
const ACTIVE_POLL_INTERVAL_MS = 1000;
const IDLE_POLL_INTERVAL_MS = 4000;
function notify(message, variant = "success") {
if (typeof showSnackbar === "function") {
showSnackbar(message, variant);
} else if (variant === "error") {
console.error(message);
} else {
console.log(message);
}
}
function logDebug(message, details = null) {
if (details === null || details === undefined) {
console.log(`[ModelManager] ${message}`);
} else {
console.log(`[ModelManager] ${message}`, details);
}
}
function isModelRouteActive() {
return window.location.pathname === "/manage_models";
}
function safeText(value, fallback = "") {
if (value === null || value === undefined) return fallback;
return String(value);
}
function toBool(value) {
return !!value;
}
function toInt(value) {
const n = Number(value);
return Number.isFinite(n) ? n : 0;
}
function parseReleased(value) {
const ts = Date.parse(safeText(value, ""));
return Number.isNaN(ts) ? 0 : ts;
}
function normalizeSeries(model) {
return safeText(model?.series, "Custom Series") || "Custom Series";
}
function modelSortCompare(a, b) {
if (state.sortMode === "release_date") {
const dateDelta = parseReleased(b?.released) - parseReleased(a?.released);
if (dateDelta !== 0) return dateDelta;
}
return safeText(a?.label, a?.value).localeCompare(
safeText(b?.label, b?.value),
undefined,
{ sensitivity: "base", numeric: true },
);
}
function getFilteredModels() {
let rows = [...state.models].filter(model => model && typeof model === "object");
if (state.communityFavoriteFilter === "yes") {
rows = rows.filter(model => !!model.communityFavorite);
} else if (state.communityFavoriteFilter === "no") {
rows = rows.filter(model => !model.communityFavorite);
}
return rows;
}
function getSeriesGroups() {
const grouped = {};
for (const model of getFilteredModels()) {
const seriesName = normalizeSeries(model);
if (!grouped[seriesName]) grouped[seriesName] = [];
grouped[seriesName].push(model);
}
const seriesNames = Object.keys(grouped);
for (const seriesName of seriesNames) {
grouped[seriesName].sort(modelSortCompare);
}
if (state.sortMode === "release_date") {
seriesNames.sort((a, b) => {
const aNewest = Math.max(...grouped[a].map(model => parseReleased(model?.released)));
const bNewest = Math.max(...grouped[b].map(model => parseReleased(model?.released)));
const delta = bNewest - aNewest;
if (delta !== 0) return delta;
return a.localeCompare(b, undefined, { sensitivity: "base" });
});
} else {
seriesNames.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
}
return { grouped, seriesNames };
}
function getVisibleModels() {
const { grouped, seriesNames } = getSeriesGroups();
const rows = [];
for (const seriesName of seriesNames) {
rows.push(...grouped[seriesName]);
}
return rows;
}
function getReleaseOrderedModels() {
return getFilteredModels().sort(modelSortCompare);
}
function getInstalledModels() {
const rows = state.sortMode === "release_date" ? getReleaseOrderedModels() : getVisibleModels();
return rows.filter(model => !!model.installed);
}
function getCurrentModelName() {
const current = safeText(state.currentModel, "");
if (!current) return "none";
const match = state.models.find(model => safeText(model?.value, "") === current);
if (!match) return current;
return safeText(match.label, current);
}
async function fetchJson(url, options = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
try {
const response = await fetch(url, { ...options, signal: controller.signal });
let payload = {};
try {
payload = await response.json();
} catch {
payload = {};
}
if (!response.ok) {
const message = payload?.error || payload?.message || `Request failed (${response.status})`;
throw new Error(message);
}
return payload;
} finally {
clearTimeout(timer);
}
}
async function fetchStatus() {
if (statusInFlight) return;
statusInFlight = true;
try {
const payload = await fetchJson("/api/models/status");
const models = Array.isArray(payload.models)
? payload.models.filter(model => model && typeof model === "object")
: [];
state.models = models;
state.currentModel = safeText(payload.currentModel, "");
const summary = payload.summary && typeof payload.summary === "object" ? payload.summary : {};
state.summary = {
installed: toInt(summary.installed),
missing: toInt(summary.missing),
total: toInt(summary.total),
};
state.status = {
modelToDownload: safeText(payload.modelToDownload, ""),
downloadAll: toBool(payload.downloadAll),
downloading: toBool(payload.downloading),
cancelling: toBool(payload.cancelling),
progress: safeText(payload.progress, ""),
isOnroad: toBool(payload.isOnroad),
terminal: toBool(payload.terminal),
};
state.error = "";
const signature = [
state.models.length,
state.currentModel,
state.status.downloading,
state.status.downloadAll,
state.status.modelToDownload,
state.status.progress,
].join("|");
if (signature !== lastStatusSignature) {
lastStatusSignature = signature;
logDebug("Status updated", {
models: state.models.length,
currentModel: state.currentModel || "none",
downloading: state.status.downloading,
progress: state.status.progress || "Idle",
});
}
} catch (error) {
state.error = error?.message || String(error);
logDebug("Status fetch failed", state.error);
} finally {
statusInFlight = false;
state.loading = false;
state.refreshing = false;
}
}
async function refreshAll(showToast = false) {
state.refreshing = true;
if (state.models.length === 0) {
state.loading = true;
}
await fetchStatus();
if (showToast && !state.error) {
notify("Model list refreshed.");
}
}
function ensurePolling() {
if (pollingHandle) return;
const poll = async () => {
if (!isModelRouteActive()) {
pollingHandle = null;
return;
}
let nextDelay = IDLE_POLL_INTERVAL_MS;
try {
await fetchStatus();
nextDelay = state.status.downloading ? ACTIVE_POLL_INTERVAL_MS : IDLE_POLL_INTERVAL_MS;
} finally {
pollingHandle = setTimeout(poll, nextDelay);
}
};
pollingHandle = setTimeout(poll, ACTIVE_POLL_INTERVAL_MS);
}
async function setActiveModel(modelKey) {
const payload = await fetchJson("/api/params", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: "Model", value: modelKey }),
});
notify(payload.message || `Selected "${modelKey}".`);
}
async function startDownload(modelKey) {
const payload = await fetchJson("/api/models/download", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: modelKey }),
});
notify(payload.message || `Downloading "${modelKey}"...`);
}
async function startDownloadAll() {
const payload = await fetchJson("/api/models/download_all", { method: "POST" });
notify(payload.message || "Started downloading all models.");
}
async function cancelDownload() {
const payload = await fetchJson("/api/models/cancel", { method: "POST" });
notify(payload.message || "Cancellation requested.");
}
async function deleteModel(modelKey) {
const payload = await fetchJson("/api/models/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: modelKey }),
});
notify(payload.message || `Deleted files for "${modelKey}".`);
}
async function refreshManifest() {
const payload = await fetchJson("/api/models/refresh_manifest", { method: "POST" });
notify(payload.message || "Model manifest refreshed.");
}
async function runAction(action, modelKey = "") {
if (state.actionBusy) {
notify("Please wait for the current action to finish.", "error");
return;
}
state.actionBusy = true;
try {
if (action === "refresh") {
await refreshManifest();
await refreshAll(false);
return;
}
if (state.status.isOnroad && action !== "refresh") {
notify("Actions are blocked while onroad.", "error");
return;
}
if (action === "select") {
if (!modelKey) return;
await setActiveModel(modelKey);
} else if (action === "download") {
if (!modelKey) return;
await startDownload(modelKey);
} else if (action === "download-all") {
await startDownloadAll();
} else if (action === "cancel") {
await cancelDownload();
} else if (action === "delete") {
if (!modelKey) return;
const confirmed = window.confirm(`Delete local files for model \"${modelKey}\"?`);
if (!confirmed) return;
await deleteModel(modelKey);
}
await fetchStatus();
} catch (error) {
notify(error?.message || String(error), "error");
} finally {
state.actionBusy = false;
}
}
function bindDomHandlers() {
if (window.__modelManagerHandlersBound) return;
window.__modelManagerHandlersBound = true;
document.addEventListener("click", event => {
if (!isModelRouteActive()) return;
const target = event.target;
if (!(target instanceof Element)) return;
const button = target.closest("[data-mm-action]");
if (!button) return;
const action = safeText(button.getAttribute("data-mm-action"), "");
const modelKey = safeText(button.getAttribute("data-model"), "");
runAction(action, modelKey).catch(() => {});
});
document.addEventListener("change", event => {
if (!isModelRouteActive()) return;
const target = event.target;
if (!(target instanceof HTMLSelectElement)) return;
if (target.id === "mm-active-model-select") {
const modelKey = safeText(target.value, "");
if (!modelKey) return;
runAction("select", modelKey).catch(() => {});
return;
}
if (target.id === "mm-sort-mode-select") {
const value = safeText(target.value, "alphabetical");
state.sortMode = value === "release_date" ? "release_date" : "alphabetical";
return;
}
if (target.id === "mm-community-filter-select") {
const value = safeText(target.value, "all");
if (value === "yes" || value === "no" || value === "all") {
state.communityFavoriteFilter = value;
} else {
state.communityFavoriteFilter = "all";
}
}
});
}
function renderActions(model) {
const modelKey = safeText(model.value, "");
const modelIsDownloading = state.status.downloading && !state.status.downloadAll && state.status.modelToDownload === modelKey;
if (state.currentModel === modelKey) {
return html`<span class="mm-chip mm-chip-active">Active</span>`;
}
if (state.status.downloading) {
if (state.status.downloadAll || modelIsDownloading) {
return html`<button class="mm-btn mm-btn-danger" data-mm-action="cancel">Cancel</button>`;
}
return html`<span class="mm-chip">Busy</span>`;
}
if (model.installed) {
return html`
<button class="mm-btn mm-btn-secondary" data-mm-action="select" data-model="${modelKey}">Set Active</button>
<button class="mm-btn mm-btn-danger" data-mm-action="delete" data-model="${modelKey}">Delete</button>
`;
}
return html`<button class="mm-btn mm-btn-primary" data-mm-action="download" data-model="${modelKey}">Download</button>`;
}
function renderModelRow(model) {
const label = safeText(model.label, safeText(model.value, "Unnamed"));
const key = safeText(model.value, "");
return html`
<div class="mm-row">
<div class="mm-row-main">
<div class="mm-row-title">
<span>${label}</span>
</div>
<div class="mm-row-meta">
<span class="mm-chip">${key}</span>
${state.sortMode === "release_date" ? "" : model.series ? html`<span class="mm-chip">${safeText(model.series)}</span>` : ""}
${model.version ? html`<span class="mm-chip">Version ${safeText(model.version)}</span>` : ""}
${model.released ? html`<span class="mm-chip">Released ${safeText(model.released)}</span>` : ""}
${model.communityFavorite ? html`<span class="mm-chip mm-chip-favorite">Community Favorite</span>` : ""}
${model.partial ? html`<span class="mm-chip mm-chip-warning">Partial Files</span>` : ""}
</div>
</div>
<div class="mm-row-actions">
${renderActions(model)}
</div>
</div>
`;
}
function renderSeriesSection(seriesName, models) {
return html`
<section class="mm-series">
<header class="mm-series-header">
<h3>${seriesName}</h3>
<span>${models.length}</span>
</header>
<div class="mm-series-body">
${models.map(model => renderModelRow(model))}
</div>
</section>
`;
}
export function ModelManager() {
if (!initialized) {
initialized = true;
bindDomHandlers();
logDebug("Initializing component");
refreshAll().catch(error => {
state.error = error?.message || String(error);
state.loading = false;
state.refreshing = false;
logDebug("Initial refresh failed", state.error);
});
}
ensurePolling();
return html`
<div class="mm-wrapper">
<h2>Model Manager</h2>
${() => state.error ? html`<div class="mm-error">${state.error}</div>` : ""}
<div class="mm-debug">
Available Models=${() => state.models.length}
Current Model=${() => getCurrentModelName()}
</div>
<div class="mm-toolbar">
<div class="mm-summary">
<span><b>${state.summary.installed}</b> installed</span>
<span><b>${state.summary.missing}</b> missing</span>
<span><b>${state.summary.total}</b> total</span>
</div>
<div class="mm-actions">
${() => state.status.downloading
? html`<button class="mm-btn mm-btn-danger" data-mm-action="cancel">Cancel Download</button>`
: html`<button class="mm-btn mm-btn-primary" data-mm-action="download-all">Download All Missing</button>`}
<button class="mm-btn mm-btn-secondary" data-mm-action="refresh">Refresh</button>
</div>
</div>
<div class="mm-status">
<span class="mm-chip">Current: ${getCurrentModelName()}</span>
<span class="mm-chip">Progress: ${safeText(state.status.progress, "Idle")}</span>
${() => state.status.isOnroad ? html`<span class="mm-chip mm-chip-warning">Onroad: actions disabled</span>` : ""}
</div>
<div class="mm-filters">
<label class="mm-filter-label" for="mm-active-model-select">Active Model</label>
<select class="mm-select" id="mm-active-model-select">
${(() => {
const orderedInstalled = getInstalledModels().sort((a, b) => {
const aCurrent = safeText(a.value) === state.currentModel ? 0 : 1;
const bCurrent = safeText(b.value) === state.currentModel ? 0 : 1;
if (aCurrent !== bCurrent) return aCurrent - bCurrent;
return safeText(a.label, a.value).localeCompare(safeText(b.label, b.value), undefined, { sensitivity: "base" });
});
return orderedInstalled.length > 0
? orderedInstalled.map(model => html`
<option value="${safeText(model.value)}" ${safeText(model.value) === state.currentModel ? "selected" : ""}>
${safeText(model.label, model.value)}
</option>
`)
: html`<option value="">No installed models</option>`;
})()}
</select>
<label class="mm-filter-label" for="mm-sort-mode-select">Sort</label>
<select class="mm-select" id="mm-sort-mode-select">
<option value="alphabetical" ${state.sortMode === "alphabetical" ? "selected" : ""}>Alphabetical</option>
<option value="release_date" ${state.sortMode === "release_date" ? "selected" : ""}>Release Date</option>
</select>
<div class="mm-filter-break"></div>
<label class="mm-filter-label" for="mm-community-filter-select">Community Favorite</label>
<select class="mm-select" id="mm-community-filter-select">
<option value="all" ${state.communityFavoriteFilter === "all" ? "selected" : ""}>All</option>
<option value="yes" ${state.communityFavoriteFilter === "yes" ? "selected" : ""}>Yes</option>
<option value="no" ${state.communityFavoriteFilter === "no" ? "selected" : ""}>No</option>
</select>
</div>
${() => state.loading ? html`<div class="mm-empty">Loading models...</div>` : ""}
${() => !state.loading ? html`
<div class="mm-list">
${(() => {
if (state.sortMode === "release_date") {
const models = getReleaseOrderedModels();
return models.length === 0
? html`<div class="mm-empty">No models available.</div>`
: models.map(model => renderModelRow(model));
}
const { grouped, seriesNames } = getSeriesGroups();
return seriesNames.length === 0
? html`<div class="mm-empty">No models available.</div>`
: seriesNames.map(seriesName => renderSeriesSection(seriesName, grouped[seriesName]));
})()}
</div>
` : ""}
</div>
`;
}
@@ -0,0 +1,280 @@
.plotsPage {
max-width: var(--width-xxxxl);
}
.plotCard {
background-color: var(--secondary-bg);
border-radius: var(--border-radius-md);
color: var(--text-color);
max-width: 95%;
padding: var(--border-radius-xl);
}
.plotStatusCard {
margin-bottom: var(--margin-base);
}
.plotDescription {
margin: 0;
}
.plotStatusGrid {
display: grid;
gap: 0.5em 1em;
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: var(--margin-base);
}
.plotStatusGrid p,
.plotRangeRow span,
.plotLegendItem {
font-size: var(--font-size-sm);
margin: 0;
}
.plotActions {
display: flex;
gap: var(--gap-sm);
margin-top: var(--margin-base);
}
.plotButton {
background: var(--main-fg);
border: none;
border-radius: var(--border-radius-sm);
color: var(--text-color);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-demi-bold);
padding: var(--padding-sm) var(--padding-base);
}
.plotButton:hover {
transform: var(--hover-scale-sm);
transition: transform var(--transition-fast), background-color var(--transition-fast);
}
.plotCharts {
display: grid;
gap: var(--gap-sm);
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
max-width: 95%;
}
.plotAdvancedRow {
margin: var(--margin-base) 0;
}
.plotChartCard {
max-width: 100%;
}
.plotCardHeader {
align-items: baseline;
display: flex;
gap: var(--gap-sm);
justify-content: space-between;
}
.plotSource {
color: var(--text-muted);
font-size: var(--font-size-sm);
}
.plotLegend {
display: flex;
gap: var(--gap-sm);
margin-bottom: var(--margin-sm);
}
.plotLegendItem {
align-items: center;
display: inline-flex;
gap: var(--gap-xs);
}
.plotLegendLine {
border-radius: var(--border-radius-sm);
display: inline-block;
height: 3px;
width: 22px;
}
.plotLegendLine.desired,
.plotLine.desired {
background-color: var(--main-fg);
stroke: var(--main-fg);
}
.plotLegendLine.actual,
.plotLine.actual {
background-color: var(--success-fg);
stroke: var(--success-fg);
}
.plotLegendLine.p,
.plotLine.p {
background-color: #63b3ff;
stroke: #63b3ff;
}
.plotLegendLine.i,
.plotLine.i {
background-color: #63d79d;
stroke: #63d79d;
}
.plotLegendLine.d,
.plotLine.d {
background-color: #f0b35e;
stroke: #f0b35e;
}
.plotLegendLine.f,
.plotLine.f {
background-color: #d08bff;
stroke: #d08bff;
}
.plotSvgWrap {
background: #0b0b18;
border: 1px solid var(--track-color);
border-radius: var(--border-radius-sm);
min-height: 260px;
overflow: hidden;
}
.plotSvg {
display: block;
height: 260px;
width: 100%;
}
.plotLine {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 3;
}
.plotGridLine {
stroke: rgba(255, 255, 255, 0.08);
stroke-width: 1;
}
.plotZeroLine {
stroke: rgba(255, 255, 255, 0.35);
stroke-dasharray: 6 6;
stroke-width: 1;
}
.plotRangeRow {
display: flex;
justify-content: space-between;
margin-top: var(--margin-xs);
}
.plotEmpty {
align-items: center;
color: var(--text-muted);
display: flex;
font-size: var(--font-size-sm);
height: 260px;
justify-content: center;
}
.plotError {
color: var(--danger-fg);
margin-top: var(--margin-sm);
}
.qualityDetail {
color: var(--text-muted);
font-size: calc(var(--font-size-sm) * 0.95);
}
.qualitySummaryGrid {
display: grid;
gap: var(--gap-sm);
margin-top: var(--margin-base);
}
.qualitySummaryRow {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--track-color);
border-radius: var(--border-radius-sm);
padding: var(--padding-sm) var(--padding-base);
}
.qualitySentence {
align-items: baseline;
display: flex;
flex-wrap: wrap;
font-size: calc(var(--font-size-lg) * 1.05);
font-weight: var(--font-weight-demi-bold);
gap: var(--gap-xs);
line-height: 1.3;
margin: 0;
}
.qualityTone {
font-size: calc(var(--font-size-xl) * 1.05);
font-weight: var(--font-weight-extra-bold);
letter-spacing: 0.01em;
text-transform: uppercase;
}
.qualityTone-great {
color: #66e3ad;
}
.qualityTone-good {
color: #7fc2ff;
}
.qualityTone-fair {
color: #f8c36d;
}
.qualityTone-poor {
color: #ff8ca5;
}
.qualityTone-na {
color: #d7dcec;
}
.qualityMethodNote {
color: var(--text-muted);
font-size: calc(var(--font-size-sm) * 0.92);
margin-top: var(--margin-sm);
}
.qualityUserGuidance {
color: var(--text-muted);
font-size: calc(var(--font-size-sm) * 0.95);
margin: var(--margin-sm) 0 0 0;
}
@media only screen and (max-width: 768px) and (orientation: portrait) {
.plotCard,
.plotCharts {
max-width: 100%;
}
.plotCharts {
grid-template-columns: 1fr;
}
.plotStatusGrid {
grid-template-columns: 1fr;
}
.plotActions {
flex-direction: column;
}
.plotLegend {
flex-direction: column;
gap: var(--gap-xs);
}
}
@@ -0,0 +1,666 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
const state = reactive({
loading: true,
error: "",
paused: false,
showAdvancedTerms: false,
live: null,
samples: [],
})
let initialized = false
let pollHandle = null
let lastTimestamp = 0
let visibilityListenerAttached = false
const MAX_POINTS = 240
const POLL_INTERVAL_MS = 750
const SVG_WIDTH = 1000
const SVG_HEIGHT = 260
const ADVANCED_TERMS_KEY = "plotsShowAdvancedTerms"
const QUALITY_WINDOW_SECONDS = 30
const QUALITY_MIN_SAMPLES = 12
const LATERAL_QUALITY_CONFIG = {
desiredKey: "desiredLateralAccel",
actualKey: "actualLateralAccel",
minSpeedMps: 0.5,
minDemand: 0.015,
allowLowDemandFallback: true,
fallbackMinSpeedMps: 1.0,
fallbackMinPeakDemand: 0.03,
great: 0.15,
good: 0.30,
fair: 0.50,
}
const LONGITUDINAL_QUALITY_CONFIG = {
desiredKey: "desiredLongitudinalAccel",
actualKey: "actualLongitudinalAccel",
minSpeedMps: 0.0,
minDemand: 0.08,
allowLowDemandFallback: false,
applyPersistenceRules: true,
warnError: 0.50,
severeError: 0.90,
great: 0.32,
good: 0.52,
fair: 0.78,
}
function isPlotsRouteActive() {
return window.location.pathname === "/plots"
}
function toNumber(value, fallback = 0) {
const n = Number(value)
return Number.isFinite(n) ? n : fallback
}
function formatValue(value, digits = 2) {
return toNumber(value).toFixed(digits)
}
function formatAge(seconds) {
const value = Math.max(0, toNumber(seconds))
if (value < 1) return `${Math.round(value * 1000)} ms`
return `${value.toFixed(1)} s`
}
function formatSourceLabel(kind, source) {
const normalizedKind = String(kind || "").toLowerCase()
const normalizedSource = String(source || "").toLowerCase()
if (normalizedKind === "lateral") {
if (normalizedSource === "torquestate") return "Steering controller output"
if (normalizedSource === "curvature") return "Path model estimate"
}
if (normalizedKind === "longitudinal") {
if (normalizedSource.includes("atarget")) return "Planner target acceleration + measured acceleration"
if (normalizedSource.includes("pid sum")) return "PID term sum + measured acceleration"
if (normalizedSource.includes("caroutput")) return "Final accel command + measured acceleration"
if (normalizedSource.includes("livelocationkalman")) return "Control output + calibrated acceleration"
if (normalizedSource === "controlsstate") return "Planner target output"
}
if (normalizedKind === "lateralterms") {
if (normalizedSource === "torquestate") return "Steering torque controller"
if (normalizedSource === "pidstate") return "Steering angle PID controller"
}
if (normalizedKind === "longitudinalterms") {
if (normalizedSource === "controlsstate") return "Longitudinal controller terms"
}
return "Live control signal"
}
function percentile(sortedValues, percentileValue) {
const values = Array.isArray(sortedValues) ? sortedValues : []
if (!values.length) return 0
const p = Math.max(0, Math.min(1, Number(percentileValue)))
const index = (values.length - 1) * p
const lower = Math.floor(index)
const upper = Math.ceil(index)
if (lower === upper) return values[lower]
const weight = index - lower
return values[lower] * (1 - weight) + values[upper] * weight
}
function formatQualityLabel(label) {
const text = String(label || "").trim()
return text || "N/A"
}
function computeMatchQuality(samples, config) {
const safeSamples = Array.isArray(samples) ? samples : []
if (safeSamples.length < 2) {
return { label: "N/A", value: null, detail: "Waiting for data" }
}
const latestTs = toNumber(safeSamples[safeSamples.length - 1]?.timestamp, 0)
const cutoffTs = latestTs > 0 ? latestTs - QUALITY_WINDOW_SECONDS : 0
const recentSamples = safeSamples.filter((sample) => toNumber(sample?.timestamp, 0) >= cutoffTs)
const demandEligibleSamples = recentSamples.filter((sample) => {
const speed = Math.abs(toNumber(sample?.speed, 0))
const desired = Math.abs(toNumber(sample?.[config.desiredKey], 0))
const speedOk = config.minSpeedMps <= 0 ? true : speed >= config.minSpeedMps
const demandOk = config.minDemand <= 0 ? true : desired >= config.minDemand
return speedOk && demandOk
})
let eligibleSamples = demandEligibleSamples
let usedLowDemandFallback = false
const allowLowDemandFallback = config.allowLowDemandFallback !== false
if (allowLowDemandFallback && eligibleSamples.length < QUALITY_MIN_SAMPLES && recentSamples.length >= QUALITY_MIN_SAMPLES) {
const totalSpeed = recentSamples.reduce((sum, sample) => sum + Math.abs(toNumber(sample?.speed, 0)), 0)
const avgSpeed = recentSamples.length > 0 ? (totalSpeed / recentSamples.length) : 0
const peakDemand = recentSamples.reduce((peak, sample) => {
const desired = Math.abs(toNumber(sample?.[config.desiredKey], 0))
return Math.max(peak, desired)
}, 0)
const fallbackMinSpeed = Math.max(0, toNumber(config.fallbackMinSpeedMps, 0))
const fallbackMinPeakDemand = Math.max(0, toNumber(config.fallbackMinPeakDemand, 0))
if (avgSpeed >= fallbackMinSpeed && peakDemand >= fallbackMinPeakDemand) {
eligibleSamples = recentSamples
usedLowDemandFallback = true
}
}
if (eligibleSamples.length < QUALITY_MIN_SAMPLES) {
if (allowLowDemandFallback) {
return {
label: "N/A",
value: null,
detail: `Need ${QUALITY_MIN_SAMPLES} samples (${eligibleSamples.length} eligible / ${recentSamples.length} total)`,
}
}
return {
label: "N/A",
value: null,
detail: `Need ${QUALITY_MIN_SAMPLES} demand samples (${eligibleSamples.length} demand / ${recentSamples.length} total)`,
}
}
const errors = eligibleSamples
.map((sample) => Math.abs(toNumber(sample?.[config.desiredKey], 0) - toNumber(sample?.[config.actualKey], 0)))
.sort((a, b) => a - b)
if (!errors.length) {
return { label: "N/A", value: null, detail: "No eligible samples" }
}
const p50 = percentile(errors, 0.50)
const p90 = percentile(errors, 0.90)
const robustError = (0.7 * p50) + (0.3 * p90)
const sampleSummary = `${eligibleSamples.length} samples / ${QUALITY_WINDOW_SECONDS}s${usedLowDemandFallback ? ", low-demand fallback" : ""}`
if (config.applyPersistenceRules) {
const warnThreshold = Math.max(config.warnError || 0.35, config.good || 0.35)
const severeThreshold = Math.max(config.severeError || 0.70, config.fair || 0.55)
const warnFrac = errors.filter((value) => value > warnThreshold).length / errors.length
const severeFrac = errors.filter((value) => value > severeThreshold).length / errors.length
let label = "Poor"
if (robustError <= config.great && warnFrac <= 0.18 && severeFrac <= 0.05) label = "Great"
else if (robustError <= config.good && warnFrac <= 0.34 && severeFrac <= 0.12) label = "Good"
else if (robustError <= config.fair && warnFrac <= 0.55 && severeFrac <= 0.24) label = "Fair"
const warnPct = Math.round(warnFrac * 100)
const severePct = Math.round(severeFrac * 100)
return {
label,
value: robustError,
detail: `${sampleSummary}, ${warnPct}% > ${formatValue(warnThreshold)} and ${severePct}% > ${formatValue(severeThreshold)}`,
}
}
let label = "Poor"
if (robustError <= config.great) label = "Great"
else if (robustError <= config.good) label = "Good"
else if (robustError <= config.fair) label = "Fair"
return {
label,
value: robustError,
detail: sampleSummary,
}
}
function qualityToneClass(label) {
const normalized = String(label || "").toLowerCase()
if (normalized === "great") return "qualityTone-great"
if (normalized === "good") return "qualityTone-good"
if (normalized === "fair") return "qualityTone-fair"
if (normalized === "poor") return "qualityTone-poor"
return "qualityTone-na"
}
function stopPolling() {
if (!pollHandle) return
clearTimeout(pollHandle)
pollHandle = null
}
function pushSample(payload) {
const timestamp = toNumber(payload.timestamp, 0)
if (!timestamp || timestamp <= 0 || timestamp === lastTimestamp) return
lastTimestamp = timestamp
state.samples.push({
timestamp,
speed: toNumber(payload.speed),
controlsActive: !!payload.controlsActive,
longitudinalControlActive: !!payload.longitudinalControlActive,
desiredLateralAccel: toNumber(payload.desiredLateralAccel),
actualLateralAccel: toNumber(payload.actualLateralAccel),
desiredLongitudinalAccel: toNumber(payload.desiredLongitudinalAccel),
actualLongitudinalAccel: toNumber(payload.actualLongitudinalAccel),
lateralP: toNumber(payload.lateralP),
lateralI: toNumber(payload.lateralI),
lateralD: toNumber(payload.lateralD),
lateralF: toNumber(payload.lateralF),
longitudinalUpAccelCmd: toNumber(payload.longitudinalUpAccelCmd),
longitudinalUiAccelCmd: toNumber(payload.longitudinalUiAccelCmd),
longitudinalUfAccelCmd: toNumber(payload.longitudinalUfAccelCmd),
})
if (state.samples.length > MAX_POINTS) {
state.samples.splice(0, state.samples.length - MAX_POINTS)
}
}
async function fetchLiveData() {
const response = await fetch("/api/plots/live")
const payload = await response.json()
if (!response.ok) {
throw new Error(payload.error || response.statusText || "Failed to load live plot data")
}
state.live = payload
state.error = ""
state.loading = false
if (!payload.stale) {
pushSample(payload)
}
}
function ensurePolling() {
if (pollHandle) return
const poll = async () => {
if (!isPlotsRouteActive()) {
pollHandle = null
return
}
if (document.visibilityState !== "visible") {
pollHandle = setTimeout(poll, POLL_INTERVAL_MS)
return
}
if (!state.paused) {
try {
await fetchLiveData()
} catch (error) {
state.error = error?.message || String(error)
state.loading = false
}
}
pollHandle = setTimeout(poll, POLL_INTERVAL_MS)
}
pollHandle = setTimeout(poll, POLL_INTERVAL_MS)
}
function clearHistory() {
state.samples = []
lastTimestamp = 0
}
function togglePaused() {
state.paused = !state.paused
if (state.paused) {
stopPolling()
return
}
if (!isPlotsRouteActive()) return
fetchLiveData().catch((error) => {
state.error = error?.message || String(error)
})
ensurePolling()
}
function toggleAdvancedTerms() {
state.showAdvancedTerms = !state.showAdvancedTerms
try {
localStorage.setItem(ADVANCED_TERMS_KEY, state.showAdvancedTerms ? "1" : "0")
} catch (error) {
console.warn("Failed to persist plots advanced terms preference", error)
}
}
function yForValue(value, min, max) {
const safeMin = toNumber(min, -1)
const safeMax = toNumber(max, 1)
const clamped = Math.max(safeMin, Math.min(safeMax, toNumber(value)))
const span = Math.max(1e-6, safeMax - safeMin)
return ((safeMax - clamped) / span) * SVG_HEIGHT
}
function computeRange(samples, desiredKey, actualKey) {
let maxAbs = 0
for (const sample of samples) {
maxAbs = Math.max(
maxAbs,
Math.abs(toNumber(sample?.[desiredKey])),
Math.abs(toNumber(sample?.[actualKey])),
)
}
const halfSpan = Math.max(1.5, Math.ceil((maxAbs * 1.25) * 10) / 10)
return { min: -halfSpan, max: halfSpan }
}
function buildPolyline(samples, key, min, max) {
if (!samples.length) return ""
const lastIndex = Math.max(1, samples.length - 1)
return samples.map((sample, index) => {
const x = (index / lastIndex) * SVG_WIDTH
const y = yForValue(sample?.[key], min, max)
return `${x.toFixed(2)},${y.toFixed(2)}`
}).join(" ")
}
function computeRangeForKeys(samples, keys) {
let maxAbs = 0
for (const sample of samples) {
for (const key of keys) {
maxAbs = Math.max(maxAbs, Math.abs(toNumber(sample?.[key])))
}
}
const halfSpan = Math.max(0.15, Math.ceil((maxAbs * 1.35) * 1000) / 1000)
return { min: -halfSpan, max: halfSpan }
}
function latestSampleValue(key) {
const latest = state.samples[state.samples.length - 1]
if (latest) return latest[key]
return toNumber(state.live?.[key])
}
function PlotCard(title, desiredKey, actualKey, sourceKind, sourceLabel, desiredLabel = "Target", actualLabel = "Measured") {
const hasData = state.samples.length > 1
const range = computeRange(state.samples, desiredKey, actualKey)
const desiredPolyline = buildPolyline(state.samples, desiredKey, range.min, range.max)
const actualPolyline = buildPolyline(state.samples, actualKey, range.min, range.max)
const zeroY = yForValue(0, range.min, range.max)
const topQuarterY = yForValue(range.max * 0.5, range.min, range.max)
const bottomQuarterY = yForValue(range.min * 0.5, range.min, range.max)
return html`
<section class="plotCard plotChartCard">
<div class="plotCardHeader">
<h2>${title}</h2>
<span class="plotSource">Source: ${formatSourceLabel(sourceKind, sourceLabel)}</span>
</div>
<div class="plotLegend">
<span class="plotLegendItem"><i class="plotLegendLine desired"></i>${desiredLabel}: ${formatValue(latestSampleValue(desiredKey))}</span>
<span class="plotLegendItem"><i class="plotLegendLine actual"></i>${actualLabel}: ${formatValue(latestSampleValue(actualKey))}</span>
</div>
<div class="plotSvgWrap">
${hasData ? html`
<svg class="plotSvg" viewBox="0 0 ${SVG_WIDTH} ${SVG_HEIGHT}" preserveAspectRatio="none" role="img" aria-label="${title} live plot">
<line class="plotGridLine" x1="0" y1="${topQuarterY}" x2="${SVG_WIDTH}" y2="${topQuarterY}"></line>
<line class="plotZeroLine" x1="0" y1="${zeroY}" x2="${SVG_WIDTH}" y2="${zeroY}"></line>
<line class="plotGridLine" x1="0" y1="${bottomQuarterY}" x2="${SVG_WIDTH}" y2="${bottomQuarterY}"></line>
<polyline class="plotLine desired" points="${desiredPolyline}"></polyline>
<polyline class="plotLine actual" points="${actualPolyline}"></polyline>
</svg>
` : html`
<div class="plotEmpty">Waiting for enough live samples...</div>
`}
</div>
<div class="plotRangeRow">
<span>${formatValue(range.max, 1)} m/</span>
<span>0.0 m/</span>
<span>${formatValue(range.min, 1)} m/</span>
</div>
</section>
`
}
function LateralTermsPlotCard(sourceLabel) {
const hasData = state.samples.length > 1
const keys = ["lateralP", "lateralI", "lateralD", "lateralF"]
const range = computeRangeForKeys(state.samples, keys)
const zeroY = yForValue(0, range.min, range.max)
const topQuarterY = yForValue(range.max * 0.5, range.min, range.max)
const bottomQuarterY = yForValue(range.min * 0.5, range.min, range.max)
return html`
<section class="plotCard plotChartCard">
<div class="plotCardHeader">
<h2>Lateral Controller Terms</h2>
<span class="plotSource">Source: ${formatSourceLabel("lateralTerms", sourceLabel)}</span>
</div>
<div class="plotLegend">
<span class="plotLegendItem"><i class="plotLegendLine p"></i>P: ${formatValue(latestSampleValue("lateralP"), 3)}</span>
<span class="plotLegendItem"><i class="plotLegendLine i"></i>I: ${formatValue(latestSampleValue("lateralI"), 3)}</span>
<span class="plotLegendItem"><i class="plotLegendLine d"></i>D: ${formatValue(latestSampleValue("lateralD"), 3)}</span>
<span class="plotLegendItem"><i class="plotLegendLine f"></i>F: ${formatValue(latestSampleValue("lateralF"), 3)}</span>
</div>
<div class="plotSvgWrap">
${hasData ? html`
<svg class="plotSvg" viewBox="0 0 ${SVG_WIDTH} ${SVG_HEIGHT}" preserveAspectRatio="none" role="img" aria-label="Lateral controller terms live plot">
<line class="plotGridLine" x1="0" y1="${topQuarterY}" x2="${SVG_WIDTH}" y2="${topQuarterY}"></line>
<line class="plotZeroLine" x1="0" y1="${zeroY}" x2="${SVG_WIDTH}" y2="${zeroY}"></line>
<line class="plotGridLine" x1="0" y1="${bottomQuarterY}" x2="${SVG_WIDTH}" y2="${bottomQuarterY}"></line>
<polyline class="plotLine p" points="${buildPolyline(state.samples, "lateralP", range.min, range.max)}"></polyline>
<polyline class="plotLine i" points="${buildPolyline(state.samples, "lateralI", range.min, range.max)}"></polyline>
<polyline class="plotLine d" points="${buildPolyline(state.samples, "lateralD", range.min, range.max)}"></polyline>
<polyline class="plotLine f" points="${buildPolyline(state.samples, "lateralF", range.min, range.max)}"></polyline>
</svg>
` : html`
<div class="plotEmpty">Waiting for enough live samples...</div>
`}
</div>
<div class="plotRangeRow">
<span>${formatValue(range.max, 3)}</span>
<span>0.000</span>
<span>${formatValue(range.min, 3)}</span>
</div>
</section>
`
}
function LongitudinalTermsPlotCard(sourceLabel) {
const hasData = state.samples.length > 1
const keys = ["longitudinalUpAccelCmd", "longitudinalUiAccelCmd", "longitudinalUfAccelCmd"]
const range = computeRangeForKeys(state.samples, keys)
const zeroY = yForValue(0, range.min, range.max)
const topQuarterY = yForValue(range.max * 0.5, range.min, range.max)
const bottomQuarterY = yForValue(range.min * 0.5, range.min, range.max)
return html`
<section class="plotCard plotChartCard">
<div class="plotCardHeader">
<h2>Longitudinal Accel Cmd Terms</h2>
<span class="plotSource">Source: ${formatSourceLabel("longitudinalTerms", sourceLabel)}</span>
</div>
<div class="plotLegend">
<span class="plotLegendItem"><i class="plotLegendLine p"></i>Up: ${formatValue(latestSampleValue("longitudinalUpAccelCmd"), 3)}</span>
<span class="plotLegendItem"><i class="plotLegendLine i"></i>Ui: ${formatValue(latestSampleValue("longitudinalUiAccelCmd"), 3)}</span>
<span class="plotLegendItem"><i class="plotLegendLine f"></i>Uf: ${formatValue(latestSampleValue("longitudinalUfAccelCmd"), 3)}</span>
</div>
<div class="plotSvgWrap">
${hasData ? html`
<svg class="plotSvg" viewBox="0 0 ${SVG_WIDTH} ${SVG_HEIGHT}" preserveAspectRatio="none" role="img" aria-label="Longitudinal accel cmd terms live plot">
<line class="plotGridLine" x1="0" y1="${topQuarterY}" x2="${SVG_WIDTH}" y2="${topQuarterY}"></line>
<line class="plotZeroLine" x1="0" y1="${zeroY}" x2="${SVG_WIDTH}" y2="${zeroY}"></line>
<line class="plotGridLine" x1="0" y1="${bottomQuarterY}" x2="${SVG_WIDTH}" y2="${bottomQuarterY}"></line>
<polyline class="plotLine p" points="${buildPolyline(state.samples, "longitudinalUpAccelCmd", range.min, range.max)}"></polyline>
<polyline class="plotLine i" points="${buildPolyline(state.samples, "longitudinalUiAccelCmd", range.min, range.max)}"></polyline>
<polyline class="plotLine f" points="${buildPolyline(state.samples, "longitudinalUfAccelCmd", range.min, range.max)}"></polyline>
</svg>
` : html`
<div class="plotEmpty">Waiting for enough live samples...</div>
`}
</div>
<div class="plotRangeRow">
<span>${formatValue(range.max, 3)}</span>
<span>0.000</span>
<span>${formatValue(range.min, 3)}</span>
</div>
</section>
`
}
async function initialize() {
try {
state.showAdvancedTerms = localStorage.getItem(ADVANCED_TERMS_KEY) === "1"
} catch (error) {
state.showAdvancedTerms = false
}
try {
await fetchLiveData()
} catch (error) {
state.error = error?.message || String(error)
state.loading = false
} finally {
ensurePolling()
}
if (!visibilityListenerAttached) {
visibilityListenerAttached = true
document.addEventListener("visibilitychange", () => {
if (document.visibilityState !== "visible") {
stopPolling()
return
}
if (isPlotsRouteActive() && !state.paused) {
fetchLiveData().catch((error) => {
state.error = error?.message || String(error)
state.loading = false
})
ensurePolling()
}
})
}
}
export function LivePlots() {
if (!initialized) {
initialized = true
initialize()
}
if (!state.paused) {
ensurePolling()
}
return html`
<div class="plotsPage">
<h1>Plots</h1>
<section class="plotCard plotStatusCard">
<p class="plotDescription">
Live comparison view for tuning diagnostics. These scores are a quick health check, not a final verdict. Short spikes from bumps, lane changes, traffic transitions, and manual inputs can temporarily lower a score.
</p>
<p class="qualityUserGuidance">
Work in progress: use this as trend guidance over time. If you see a brief "Poor," keep driving and look for repeated behavior across multiple situations before changing tune values.
</p>
<div class="plotStatusGrid">
<p><strong>Onroad:</strong> ${state.live?.isOnroad ? "Yes" : "No"}</p>
<p><strong>Sample Age:</strong> ${formatAge(state.live?.sampleAgeSeconds)}</p>
<p><strong>Vehicle Speed:</strong> ${formatValue(state.live?.speed)} m/s</p>
<p><strong>Samples:</strong> ${state.samples.length}</p>
</div>
${() => {
const lateralQuality = computeMatchQuality(state.samples, LATERAL_QUALITY_CONFIG)
const longQuality = computeMatchQuality(state.samples, LONGITUDINAL_QUALITY_CONFIG)
return html`
<div class="qualitySummaryGrid">
<div class="qualitySummaryRow">
<p class="qualitySentence">
Your lateral tuning is
<span class="qualityTone ${qualityToneClass(lateralQuality.label)}">${formatQualityLabel(lateralQuality.label)}</span>
</p>
<p class="qualityDetail">
${lateralQuality.value === null ? lateralQuality.detail : `${formatValue(lateralQuality.value)} m/s² error (${lateralQuality.detail})`}
</p>
</div>
<div class="qualitySummaryRow">
<p class="qualitySentence">
Your longitudinal tuning is
<span class="qualityTone ${qualityToneClass(longQuality.label)}">${formatQualityLabel(longQuality.label)}</span>
</p>
<p class="qualityDetail">
${longQuality.value === null ? longQuality.detail : `${formatValue(longQuality.value)} m/s² error (${longQuality.detail})`}
</p>
</div>
</div>
`
}}
<div class="plotActions">
<button class="plotButton" @click="${togglePaused}">
${state.paused ? "Resume Live" : "Pause Live"}
</button>
<button class="plotButton" @click="${clearHistory}">
Clear History
</button>
</div>
${state.error ? html`<p class="plotError"><strong>Error:</strong> ${state.error}</p>` : ""}
${state.live?.lastError ? html`<p class="plotError"><strong>Source Error:</strong> ${state.live.lastError}</p>` : ""}
<p class="qualityMethodNote">
Match rating uses a 30-second rolling window. Lateral prefers true steering-demand moments, and longitudinal also checks how much of the window stays above error limits so brief spikes are less likely to mark "Poor."
</p>
</section>
<div class="plotCharts">
${PlotCard(
"Lateral Response (m/s²)",
"desiredLateralAccel",
"actualLateralAccel",
"lateral",
state.live?.lateralSource,
"Target",
"Measured",
)}
${PlotCard(
"Longitudinal Response (m/s²)",
"desiredLongitudinalAccel",
"actualLongitudinalAccel",
"longitudinal",
state.live?.longitudinalSource,
"Target",
"Measured",
)}
</div>
<div class="plotAdvancedRow">
<button class="plotButton" @click="${toggleAdvancedTerms}">
${state.showAdvancedTerms ? "Hide Advanced Controller Terms" : "Show Advanced Controller Terms"}
</button>
</div>
${state.showAdvancedTerms ? html`
<div class="plotCharts">
${LateralTermsPlotCard(state.live?.lateralTermsSource)}
${LongitudinalTermsPlotCard(state.live?.longitudinalTermsSource)}
</div>
` : ""}
${state.loading ? html`<p>Loading live data...</p>` : ""}
</div>
`
}
@@ -1,4 +1,4 @@
import { html } from "https://esm.sh/@arrow-js/core"
import { html } from "/assets/vendor/arrow-core.js"
export function SpeedLimits() {
function handleDownload() {
@@ -0,0 +1,152 @@
.testingGroundPage {
max-width: var(--width-xxl);
}
.testingGroundCard {
background-color: var(--secondary-bg);
border-radius: var(--border-radius-md);
color: var(--text-color);
max-width: 90%;
padding: var(--border-radius-xl);
}
.testingGroundIntro {
margin-top: 0;
}
.testingGroundTopList {
margin-top: var(--margin-base);
}
.testingGroundTopList ol {
margin: var(--margin-xs) 0 0;
padding-left: 1.4rem;
}
.testingGroundTopList li {
font-size: var(--font-size-sm);
margin-bottom: 0.25rem;
}
.testingGroundStatusGrid {
display: grid;
gap: 0.5em 1em;
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: var(--margin-base);
}
.testingGroundStatusGrid p {
font-size: var(--font-size-sm);
margin: 0;
}
.testingGroundSelectionRow {
align-items: center;
display: flex;
gap: var(--gap-sm);
margin-top: var(--margin-base);
}
.testingGroundSelect {
background-color: var(--main-bg);
border: 1px solid var(--track-color);
border-radius: var(--border-radius-sm);
color: var(--text-color);
flex: 1;
font-size: var(--font-size-sm);
min-width: 0;
padding: var(--padding-sm);
}
.testingGroundModeButton:hover {
transform: var(--hover-scale-sm);
transition: transform var(--transition-fast), background-color var(--transition-fast);
}
.testingGroundDetails {
margin-top: var(--margin-base);
}
.testingGroundDetails h3 {
margin-bottom: var(--margin-xs);
}
.testingGroundDetails p {
font-size: var(--font-size-sm);
margin: 0 0 var(--margin-xs);
}
.testingGroundMuted {
color: var(--text-muted);
}
.testingGroundActionGrid {
display: grid;
gap: var(--gap-sm);
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
margin-top: var(--margin-base);
}
.testingGroundModeButton {
align-items: center;
background: #2a2752;
border: none;
border-radius: var(--border-radius-sm);
color: var(--text-color);
display: flex;
flex-direction: column;
gap: var(--gap-xs);
min-height: 120px;
padding: var(--padding-base);
}
.testingGroundModeButtonB {
background: #27434b;
}
.testingGroundModeButtonC {
background: #413029;
}
.testingGroundModeButton.active {
box-shadow: 0 0 0 2px var(--main-fg) inset;
}
.testingGroundModeLetter {
font-size: clamp(2rem, 4vw, 2.75rem);
font-weight: var(--font-weight-bold);
line-height: 1;
}
.testingGroundModeLabel {
font-size: var(--font-size-sm);
text-align: center;
}
.testingGroundError {
color: var(--danger-fg);
margin-top: var(--margin-base);
}
.testingGroundActiveSummary {
margin-top: var(--margin-base);
}
@media only screen and (max-width: 768px) and (orientation: portrait) {
.testingGroundCard {
max-width: 100%;
}
.testingGroundStatusGrid {
grid-template-columns: 1fr;
}
.testingGroundSelectionRow {
align-items: stretch;
flex-direction: column;
}
.testingGroundActionGrid {
grid-template-columns: 1fr;
}
}
@@ -0,0 +1,288 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
const state = reactive({
loading: true,
error: "",
busy: false,
data: null,
selectedSlot: "",
})
let initialized = false
function slotId(slot) {
return String(slot?.id || "").trim()
}
function isUnusedSlot(slot) {
const name = String(slot?.name || "").trim().toLowerCase()
return name === "unused" || name.startsWith("unused ")
}
function getSelectableSlots() {
const selectable = Array.isArray(state.data?.selectableSlots) ? state.data.selectableSlots : []
if (selectable.length) return selectable
const slots = Array.isArray(state.data?.slots) ? state.data.slots : []
return slots.filter((slot) => !isUnusedSlot(slot))
}
function getVisibleSlotLines() {
return getSelectableSlots().map((slot) => `${slot.id}. ${slot.name}`)
}
function getSelectedSlot() {
const slots = Array.isArray(state.data?.slots) ? state.data.slots : []
const selectedId = String(state.selectedSlot || "").trim()
if (!selectedId) return null
return slots.find((slot) => slotId(slot) === selectedId) || null
}
function getActiveSlot() {
const slots = Array.isArray(state.data?.slots) ? state.data.slots : []
const activeId = String(state.data?.activeSlot || "").trim()
if (!activeId) return null
return slots.find((slot) => slotId(slot) === activeId) || null
}
function getVariantLabels(slot) {
if (!slot || typeof slot !== "object") {
return { A: "A" }
}
const labels = {}
const rawVariantLabels = slot.variantLabels
if (rawVariantLabels && typeof rawVariantLabels === "object") {
Object.entries(rawVariantLabels).forEach(([rawMode, rawLabel]) => {
const mode = String(rawMode || "").trim().toUpperCase()
const label = String(rawLabel || "").trim()
if (mode.length === 1 && /^[A-Z]$/.test(mode) && label) {
labels[mode] = label
}
})
}
const aLabel = String(slot.aLabel || "").trim()
const bLabel = String(slot.bLabel || "").trim()
if (aLabel) labels.A = labels.A || aLabel
if (bLabel) labels.B = labels.B || bLabel
if (!labels.A) labels.A = "A"
return Object.keys(labels).sort().reduce((acc, key) => {
acc[key] = labels[key]
return acc
}, {})
}
function getVariantModes(slot) {
return Object.keys(getVariantLabels(slot))
}
function getDefaultMode(slot) {
const modes = getVariantModes(slot)
if (modes.includes("A")) return "A"
return modes[0] || "A"
}
function toModeLabel(slot, mode) {
const labels = getVariantLabels(slot)
return labels[String(mode || "").trim().toUpperCase()] || String(mode || "").trim().toUpperCase() || "A"
}
function isModeActive(mode) {
const normalizedMode = String(mode || "").trim().toUpperCase()
return String(state.data?.activeSlot || "").trim() === String(state.selectedSlot || "").trim() && String(state.data?.activeVariant || "").trim().toUpperCase() === normalizedMode
}
function getSelectedMode() {
const selectedSlot = getSelectedSlot()
if (!selectedSlot) return "A"
if (String(state.data?.activeSlot || "").trim() === String(state.selectedSlot || "").trim()) {
return String(state.data?.activeVariant || "").trim().toUpperCase() || getDefaultMode(selectedSlot)
}
return getDefaultMode(selectedSlot)
}
function modeButtonClass(mode) {
const normalizedMode = String(mode || "").trim().toUpperCase()
return [
"testingGroundModeButton",
`testingGroundModeButton${normalizedMode}`,
isModeActive(normalizedMode) ? "active" : "",
].filter(Boolean).join(" ")
}
async function fetchTestingGrounds() {
try {
const response = await fetch("/api/testing_grounds")
const payload = await response.json()
if (!response.ok) {
throw new Error(payload.error || response.statusText || "Failed to load testing grounds")
}
state.data = payload
state.error = ""
const selectable = Array.isArray(payload.selectableSlots) ? payload.selectableSlots : []
const hasCurrentSelection = selectable.some((slot) => slotId(slot) === String(state.selectedSlot || "").trim())
if (!hasCurrentSelection) {
const activeSlot = String(payload.activeSlot || "").trim()
const activeSelectable = selectable.some((slot) => slotId(slot) === activeSlot)
state.selectedSlot = activeSelectable ? activeSlot : slotId(selectable[0] || {})
}
} catch (error) {
const message = error?.message || "Failed to load testing grounds"
state.error = message
}
}
async function applySelection(slotValue, mode, showToast = true) {
const normalizedSlot = String(slotValue || "").trim()
const normalizedMode = String(mode || "").trim().toUpperCase()
if (!normalizedSlot || !normalizedMode) return false
state.busy = true
try {
const response = await fetch("/api/testing_grounds/select", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slotId: normalizedSlot, variant: normalizedMode }),
})
const payload = await response.json()
if (!response.ok) {
throw new Error(payload.error || response.statusText || "Failed to update testing ground mode")
}
state.data = payload
state.error = ""
state.selectedSlot = normalizedSlot
if (showToast) {
showSnackbar(payload.message || `Testing Ground ${normalizedSlot} set to ${normalizedMode}.`)
}
return true
} catch (error) {
const message = error?.message || "Failed to update testing ground mode"
state.error = message
if (showToast) {
showSnackbar(message, "error")
}
return false
} finally {
state.busy = false
}
}
async function selectMode(mode) {
if (state.busy) return
const slot = getSelectedSlot()
const slotValue = slotId(slot)
if (!slotValue) {
showSnackbar("Select a testing ground first.", "error")
return
}
await applySelection(slotValue, mode, true)
}
async function selectSlot(slotValue) {
const normalizedSlot = String(slotValue || "").trim()
state.selectedSlot = normalizedSlot
if (!normalizedSlot || state.busy) return
const activeSlot = String(state.data?.activeSlot || "").trim()
if (normalizedSlot === activeSlot) return
const success = await applySelection(normalizedSlot, "A", false)
if (!success) {
state.selectedSlot = activeSlot
if (state.error) {
showSnackbar(state.error, "error")
}
}
}
function initialize() {
if (initialized) return
initialized = true
fetchTestingGrounds().finally(() => {
state.loading = false
})
}
export function TestingGround() {
initialize()
return html`
<div class="testingGroundPage">
<h2>Testing Ground</h2>
${() => state.loading ? html`<div class="testingGroundCard">Loading testing ground state...</div>` : ""}
${() => !state.loading ? html`
<div class="testingGroundCard">
<p class="testingGroundIntro">
A/B Tuning Sandbox. If you don't know what this is, you probably shouldn't be here ;)
</p>
<div class="testingGroundTopList">
<strong>Current Test Slots</strong>
<ol>
${getVisibleSlotLines().map((line) => html`<li>${line}</li>`)}
</ol>
</div>
<div class="testingGroundStatusGrid">
<p><strong>Selected Slot:</strong> ${getSelectedSlot()?.name || "Unknown"}</p>
<p><strong>Selected Mode:</strong> ${getSelectedMode()}</p>
<p><strong>Onroad:</strong> ${state.data?.isOnroad ? "Yes" : "No"}</p>
<p><strong>Selectable Slots:</strong> ${getSelectableSlots().length}</p>
</div>
<div class="testingGroundSelectionRow">
<select
class="testingGroundSelect"
?disabled="${state.busy || getSelectableSlots().length === 0}"
@change="${(event) => {
selectSlot(String(event.target.value || ""))
}}">
${() => getSelectableSlots().length
? getSelectableSlots().map((slot) => html`<option value="${slotId(slot)}" ${slotId(slot) === state.selectedSlot ? "selected" : ""}>${slot.id}. ${slot.name}</option>`)
: html`<option value="">No active test slots</option>`
}
</select>
</div>
${() => getSelectedSlot() ? html`
<div class="testingGroundDetails">
<h3>${getSelectedSlot().name}</h3>
${() => getSelectedSlot().description ? html`<p>${getSelectedSlot().description}</p>` : ""}
</div>
` : ""}
${() => getSelectedSlot() ? html`
<div class="testingGroundActionGrid">
${() => getVariantModes(getSelectedSlot()).map((mode) => html`
<button
class="${modeButtonClass(mode)}"
?disabled="${state.busy}"
@click="${() => selectMode(mode)}">
<span class="testingGroundModeLetter">${mode}</span>
<span class="testingGroundModeLabel">${toModeLabel(getSelectedSlot(), mode)}</span>
</button>
`)}
</div>
` : ""}
${() => state.error ? html`<p class="testingGroundError"><strong>Error:</strong> ${state.error}</p>` : ""}
${() => getActiveSlot() ? html`
<p class="testingGroundActiveSummary">
Currently active: <strong>${getActiveSlot().id}. ${getActiveSlot().name}</strong> in mode <strong>${state.data?.activeVariant || "A"}</strong>.
</p>
` : ""}
</div>
` : ""}
</div>
`
}
@@ -77,11 +77,11 @@
display: none;
}
.checklist-item input[type="checkbox"]:checked ~ .custom-checkbox {
.checklist-item input[type="checkbox"]:checked~.custom-checkbox {
background-color: var(--success-bg);
}
.checklist-item input[type="checkbox"]:checked ~ .custom-checkbox::before {
.checklist-item input[type="checkbox"]:checked~.custom-checkbox::before {
transform: translateX(24px);
}
@@ -170,6 +170,7 @@
flex-shrink: 0;
transition: color var(--transition-fast), transform 0.2s;
}
.file-clear-button:hover {
color: var(--danger-hover-bg);
transform: scale(1.1);
@@ -613,7 +614,7 @@ label.file-upload-button {
.turn-signal-input:focus,
.turn-signal-input:hover {
border-color: var(--thumb-color);
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
box-shadow: var(--glow-primary);
outline: none;
transform: var(--hover-scale-sm);
}
@@ -690,8 +691,15 @@ label.file-upload-button {
}
@keyframes modalFadeIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@media only screen and (max-width: 768px) and (orientation: portrait) {
@@ -713,8 +721,8 @@ label.file-upload-button {
flex-wrap: wrap;
}
.save-button-wrapper > button {
.save-button-wrapper>button {
flex-basis: calc(50% - 1rem);
margin-bottom: var(--gap-sm);
}
}
}
@@ -1,4 +1,4 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core";
import { html, reactive } from "/assets/vendor/arrow-core.js";
import { Modal } from "/assets/components/modal.js";
const defaultColors = {
@@ -97,7 +97,8 @@ const state = reactive({
showDeleteConfirmModal: false,
themeToDelete: null,
themes: [],
activeTab: "colors"
activeTab: "colors",
fetched: false,
});
let draggedIndex = -1;
@@ -405,17 +406,19 @@ const fetchDownloadables = async () => {
};
(async () => {
try {
const response = await fetch("/api/params?key=DiscordUsername");
state.discordUsername = await response.text();
await loadDefaultTheme();
await fetchDownloadables();
} catch {}
})();
export function ThemeMaker() {
if (!state.fetched) {
state.fetched = true;
(async () => {
try {
const response = await fetch("/api/params?key=DiscordUsername");
state.discordUsername = await response.text();
await loadDefaultTheme();
await fetchDownloadables();
} catch {}
})();
}
const normalize = (str) => (str || "")
.toString()
.trim()
@@ -1,5 +1,5 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { formatSecondsToHuman } from "/assets/js/utils.js"
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { formatSecondsToHuman, isGalaxyTunnel } from "/assets/js/utils.js"
import { Modal } from "/assets/components/modal.js";
const logSelectorState = reactive({
@@ -12,6 +12,16 @@ const logSelectorState = reactive({
newName: ""
});
let liveEventSource = null;
let livePollTimer = null;
let tmuxLifecycleVersion = 0;
const REMOTE_POLL_INTERVAL_MS = 2000;
const TRANSPORT_LIFECYCLE_INTERVAL_MS = 500;
function isTmuxRouteActive() {
return window.location.pathname === "/manage_tmux";
}
async function loadTmuxLogs() {
if (logSelectorState.loading || logSelectorState.logsLoadedOnce) return;
@@ -122,34 +132,34 @@ function TmuxLogSelector({ action, closeFn }) {
</div>
${() => {
if (logSelectorState.loading && !logSelectorState.logsLoadedOnce) {
return html`<div class="fileEntry"><p>Loading...</p></div>`;
}
if (logSelectorState.files.length === 0) {
return html`<div class="fileEntry"><p>No tmux logs found!</p></div>`;
}
return logSelectorState.files.map(file => html`
if (logSelectorState.loading && !logSelectorState.logsLoadedOnce) {
return html`<div class="fileEntry"><p>Loading...</p></div>`;
}
if (logSelectorState.files.length === 0) {
return html`<div class="fileEntry"><p>No tmux logs found!</p></div>`;
}
return logSelectorState.files.map(file => html`
<div class="fileEntry" @click="${() => handleFileClick(file)}">
<p><span class="label">Filename:</span> <span class="value">${file.filename}</span></p>
<p><span class="label">Date:</span> <span class="value">${file.date}</span></p>
<p><span class="label">Age:</span> <span class="value">${file.timeSince < 60 ? "just now" : `${formatSecondsToHuman(file.timeSince, "minutes")} ago`}</span></p>
</div>
`);
}}
}}
<button @click="${closeFn}" class="cancel-button">Close</button>
${() => logSelectorState.logToDelete ? Modal({
title: "Confirm Delete",
message: `Are you sure you want to delete <strong>${logSelectorState.logToDelete.filename}</strong>?`,
onConfirm: confirmDeleteFile,
onCancel: () => { logSelectorState.logToDelete = null },
confirmText: "Yes, Delete"
}) : ""}
title: "Confirm Delete",
message: `Are you sure you want to delete <strong>${logSelectorState.logToDelete.filename}</strong>?`,
onConfirm: confirmDeleteFile,
onCancel: () => { logSelectorState.logToDelete = null },
confirmText: "Yes, Delete"
}) : ""}
${() => logSelectorState.logToRename ? Modal({
title: "Rename Log",
message: html`
title: "Rename Log",
message: html`
<div>
<p>Rename <strong>${logSelectorState.logToRename.filename}</strong> to:</p>
<div style="margin-top: 10px;">
@@ -163,14 +173,14 @@ function TmuxLogSelector({ action, closeFn }) {
</div>
</div>
`,
onConfirm: confirmRenameFile,
onCancel: () => {
logSelectorState.logToRename = null;
logSelectorState.newName = "";
},
confirmText: "Rename",
confirmClass: "btn-primary"
}) : ""}
onConfirm: confirmRenameFile,
onCancel: () => {
logSelectorState.logToRename = null;
logSelectorState.newName = "";
},
confirmText: "Rename",
confirmClass: "btn-primary"
}) : ""}
</div>
</div>
`
@@ -182,23 +192,143 @@ export function TmuxLog() {
latest: '',
log: '',
selectorAction: null,
transport: "connecting",
});
const lifecycleVersion = ++tmuxLifecycleVersion;
let lifecycleTimer = null;
const event_source = new EventSource("/api/tmux_log/live");
event_source.onmessage = e => {
state.latest = e.data;
function applyIncomingLog(data) {
state.latest = data || "";
if (!state.paused) {
state.log = state.latest;
}
}
event_source.onerror = err => {
console.error("Error receiving tmux log:", err);
event_source.close();
function stopLiveTransport() {
if (liveEventSource) {
liveEventSource.close();
liveEventSource = null;
}
if (livePollTimer) {
clearInterval(livePollTimer);
livePollTimer = null;
}
}
function togglePause () {
async function fetchSnapshot() {
try {
const response = await fetch("/api/tmux_log/snapshot");
if (!response.ok) throw new Error(await response.text());
const payload = await response.json();
applyIncomingLog(payload?.data || "");
} catch (err) {
console.error("Error fetching tmux log snapshot:", err);
}
}
function startPolling() {
if (livePollTimer) return;
if (liveEventSource) {
liveEventSource.close();
liveEventSource = null;
}
state.transport = "polling";
fetchSnapshot();
livePollTimer = setInterval(() => {
if (!isTmuxRouteActive()) {
stopLiveTransport();
return;
}
if (document.visibilityState !== "visible") {
return;
}
fetchSnapshot();
}, REMOTE_POLL_INTERVAL_MS);
}
function startEventStream() {
if (liveEventSource) return;
if (livePollTimer) {
clearInterval(livePollTimer);
livePollTimer = null;
}
state.transport = "streaming";
liveEventSource = new EventSource("/api/tmux_log/live");
let receivedAnyMessage = false;
liveEventSource.onmessage = e => {
if (!isTmuxRouteActive() || document.visibilityState !== "visible") return;
receivedAnyMessage = true;
applyIncomingLog(e.data);
};
liveEventSource.onerror = err => {
console.error("Error receiving tmux log stream:", err);
if (!isTmuxRouteActive()) {
stopLiveTransport();
return;
}
startPolling();
};
// If SSE connects but no payload arrives quickly (common through tunnels), fallback.
setTimeout(() => {
if (!receivedAnyMessage && state.transport === "streaming" && isTmuxRouteActive()) {
startPolling();
}
}, 3000);
}
function updateTransportForLifecycle() {
if (lifecycleVersion !== tmuxLifecycleVersion) {
stopLiveTransport();
if (lifecycleTimer) {
clearInterval(lifecycleTimer);
lifecycleTimer = null;
}
return;
}
if (!isTmuxRouteActive()) {
stopLiveTransport();
state.transport = "inactive";
if (lifecycleTimer) {
clearInterval(lifecycleTimer);
lifecycleTimer = null;
}
return;
}
if (document.visibilityState !== "visible") {
stopLiveTransport();
state.transport = "paused";
return;
}
if (isGalaxyTunnel()) {
startPolling();
return;
}
if (!liveEventSource && !livePollTimer) {
startEventStream();
}
}
updateTransportForLifecycle();
lifecycleTimer = setInterval(updateTransportForLifecycle, TRANSPORT_LIFECYCLE_INTERVAL_MS);
function getStatusLabel() {
if (state.transport === "polling") {
return "Tmux Live Log (Remote Mode)";
}
if (state.transport === "streaming") {
return "Tmux Live Log";
}
return "Tmux Live Log";
}
function togglePause() {
state.paused = !state.paused;
if (!state.paused) {
state.log = state.latest;
@@ -248,7 +378,7 @@ export function TmuxLog() {
<div class="tmux-block">
<div class="tmux-wrapper">
<div class="tmuxContainer">
<div class="tmuxHeader">Tmux Live Log</div>
<div class="tmuxHeader">${() => getStatusLabel()}</div>
<pre class="tmuxLog">${() => state.log}</pre>
</div>
</div>
@@ -263,20 +393,20 @@ export function TmuxLog() {
</div>
${() => state.selectorAction
? TmuxLogSelector({
action: state.selectorAction,
closeFn: () => (state.selectorAction = null)
})
: ""
}
? TmuxLogSelector({
action: state.selectorAction,
closeFn: () => (state.selectorAction = null)
})
: ""
}
${() => logSelectorState.showDeleteAllModal ? Modal({
title: "Delete All Logs",
message: "Are you sure you want to delete all of your session logs?",
onConfirm: deleteAllSessions,
onCancel: () => { logSelectorState.showDeleteAllModal = false },
confirmText: "Delete All"
}) : ""}
title: "Delete All Logs",
message: "Are you sure you want to delete all of your session logs?",
onConfirm: deleteAllSessions,
onCancel: () => { logSelectorState.showDeleteAllModal = false },
confirmText: "Delete All"
}) : ""}
</div>
`;
}
@@ -1,7 +1,8 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { Modal } from "/assets/components/modal.js"
import { TailscaleControl } from "/assets/components/tailscale/tailscale.js"
export function ToggleControl () {
export function ToggleControl() {
const state = reactive({
showResetDefaultModal: false,
showResetStockModal: false,
@@ -14,7 +15,7 @@ export function ToggleControl () {
fileInput.addEventListener("change", restoreToggles)
document.body.appendChild(fileInput)
async function backupToggles () {
async function backupToggles() {
const response = await fetch("/api/toggles/backup", { method: "POST" })
const blob = await response.blob()
@@ -26,7 +27,7 @@ export function ToggleControl () {
URL.revokeObjectURL(downloadUrl)
}
async function restoreToggles (event) {
async function restoreToggles(event) {
const uploadedFile = event.target.files[0]
if (uploadedFile) {
const fileContents = await uploadedFile.text()
@@ -45,11 +46,11 @@ export function ToggleControl () {
}
}
function confirmResetDefault () {
function confirmResetDefault() {
state.showResetDefaultModal = true;
}
async function resetTogglesToDefault () {
async function resetTogglesToDefault() {
state.showResetDefaultModal = false;
showSnackbar("Resetting toggles to their default values...");
await new Promise(resolve => setTimeout(resolve, 3000));
@@ -58,11 +59,11 @@ export function ToggleControl () {
await fetch("/api/toggles/reset_default", { method: "POST" });
}
function confirmResetStock () {
function confirmResetStock() {
state.showResetStockModal = true;
}
async function resetTogglesToStock () {
async function resetTogglesToStock() {
state.showResetStockModal = false;
showSnackbar("Resetting toggles to stock openpilot values...");
await new Promise(resolve => setTimeout(resolve, 3000));
@@ -71,7 +72,7 @@ export function ToggleControl () {
await fetch("/api/toggles/reset_stock", { method: "POST" });
}
function triggerRestorePrompt () {
function triggerRestorePrompt() {
fileInput.click()
}
@@ -87,9 +88,9 @@ export function ToggleControl () {
</section>
<section class="toggle-control-widget" style="margin-left: 1.5rem">
<div class="toggle-control-title">Reset Toggles to Default FrogPilot/Stock openpilot</div>
<div class="toggle-control-title">Reset Toggles to Default StarPilot</div>
<p class="toggle-control-text">
Reset all toggles to default FrogPilot/stock openpilot settings.
Reset all toggles to default StarPilot settings.
</p>
<button class="toggle-control-button" @click="${confirmResetDefault}">
Reset Toggles to Default
@@ -98,20 +99,22 @@ export function ToggleControl () {
Reset Toggles to Stock
</button>
</section>
${TailscaleControl()}
</div>
${() => state.showResetDefaultModal ? Modal({
title: "Reset Toggles",
message: "Are you sure you want to reset all toggles to their default FrogPilot values?",
onConfirm: resetTogglesToDefault,
onCancel: () => { state.showResetDefaultModal = false; },
confirmText: "Reset to Default"
}) : ""}
title: "Reset Toggles",
message: "Are you sure you want to reset all toggles to their default FrogPilot values?",
onConfirm: resetTogglesToDefault,
onCancel: () => { state.showResetDefaultModal = false; },
confirmText: "Reset to Default"
}) : ""}
${() => state.showResetStockModal ? Modal({
title: "Reset Toggles",
message: "Are you sure you want to reset all toggles to stock openpilot values?",
onConfirm: resetTogglesToStock,
onCancel: () => { state.showResetStockModal = false; },
confirmText: "Reset to Stock"
}) : ""}
title: "Reset Toggles",
message: "Are you sure you want to reset all toggles to stock openpilot values?",
onConfirm: resetTogglesToStock,
onCancel: () => { state.showResetStockModal = false; },
confirmText: "Reset to Stock"
}) : ""}
`
}
@@ -0,0 +1,124 @@
.troubleshootPage {
display: flex;
flex-direction: column;
gap: 16px;
}
.troubleshootCard {
background: var(--sidebar-bg);
border: 1px solid var(--sidebar-border-color);
border-radius: var(--border-radius-lg);
padding: 16px;
}
.troubleshootIntro {
margin: 0 0 12px;
opacity: 0.9;
}
.troubleshootActionRow {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.troubleshootButton {
background: linear-gradient(135deg, #7a62b8, #8b6cc5);
color: #fff;
border: none;
border-radius: var(--border-radius-md);
padding: 10px 14px;
font-size: 0.95rem;
font-weight: 700;
cursor: pointer;
}
.troubleshootButton:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.troubleshootDanger {
background: linear-gradient(135deg, #b14a6b, #d95a7b);
}
.troubleshootError {
color: #ff6b6b;
}
.troubleshootStatusLine {
margin: 6px 0 0;
}
.troubleshootSectionHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
}
.troubleshootSectionHeader h3,
.troubleshootCard h3 {
margin: 0;
}
.troubleshootHeaderRow,
.troubleshootItemRow,
.troubleshootSnapshotRow {
display: grid;
gap: 10px;
align-items: center;
border-top: 1px solid var(--sidebar-border-color);
padding: 8px 0;
}
.troubleshootHeaderRow {
font-weight: 700;
opacity: 0.9;
}
.troubleshootHeaderRow {
grid-template-columns: 2fr 1fr 1fr;
}
.troubleshootHeaderRowSnapshot {
grid-template-columns: 2fr 2fr;
}
.troubleshootSnapshotRow {
grid-template-columns: 2fr 2fr;
}
.troubleshootItemRow {
grid-template-columns: 2fr 1fr 1fr;
}
.troubleshootItemLabel {
font-weight: 600;
}
.troubleshootItemValue,
.troubleshootItemDefault {
font-family: "Open Sans", sans-serif;
word-break: break-word;
}
@media (max-width: 720px) {
.troubleshootHeaderRow,
.troubleshootItemRow {
grid-template-columns: 1.3fr 1fr 1fr;
font-size: 0.9rem;
}
.troubleshootSnapshotRow {
grid-template-columns: 1fr;
gap: 4px;
}
.troubleshootSectionHeader {
align-items: flex-start;
flex-direction: column;
}
}
@@ -0,0 +1,208 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
const state = reactive({
loading: true,
error: "",
snapshot: [],
sections: [],
isOnroad: false,
refreshing: false,
busySection: "",
})
let initialized = false
function formatValue(value) {
if (typeof value === "boolean") return value ? "On" : "Off"
if (typeof value === "number") {
if (Number.isInteger(value)) return String(value)
return String(Number(value.toFixed(4)))
}
if (value === null || value === undefined) return "n/a"
const text = String(value).trim()
return text || "(empty)"
}
function buildReportText() {
const lines = []
lines.push("StarPilot Troubleshoot Report")
lines.push(`Generated: ${new Date().toISOString()}`)
lines.push(`Onroad: ${state.isOnroad ? "Yes" : "No"}`)
lines.push("")
lines.push("Snapshot")
for (const item of state.snapshot) {
lines.push(`- ${item.label}: ${formatValue(item.value)}`)
}
for (const section of state.sections) {
lines.push("")
lines.push(section.title)
for (const item of section.items || []) {
lines.push(`- ${item.label}: ${formatValue(item.value)} (default: ${formatValue(item.defaultValue)})`)
}
}
return lines.join("\n")
}
async function copyToClipboard() {
const text = buildReportText()
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(text)
} else {
const textArea = document.createElement("textarea")
textArea.value = text
textArea.style.position = "fixed"
textArea.style.left = "-9999px"
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
document.execCommand("copy")
document.body.removeChild(textArea)
}
showSnackbar("Troubleshoot report copied to clipboard.", "", 2500, { key: "troubleshoot-copy" })
} catch (error) {
showSnackbar(error?.message || "Failed to copy report.", "error", 3000, { key: "troubleshoot-copy" })
}
}
async function fetchTroubleshoot(showToast = false) {
state.refreshing = true
try {
const response = await fetch("/api/troubleshoot")
const payload = await response.json()
if (!response.ok) {
throw new Error(payload.error || response.statusText || "Failed to load troubleshoot data")
}
state.snapshot = Array.isArray(payload.snapshot) ? payload.snapshot : []
state.sections = Array.isArray(payload.sections) ? payload.sections : []
state.isOnroad = !!payload.isOnroad
state.error = ""
if (showToast) {
showSnackbar("Troubleshoot data refreshed.", "", 1800, { key: "troubleshoot-refresh" })
}
} catch (error) {
const message = error?.message || "Failed to load troubleshoot data"
state.error = message
if (showToast) {
showSnackbar(message, "error", 3000, { key: "troubleshoot-refresh" })
}
} finally {
state.loading = false
state.refreshing = false
}
}
async function resetSection(section) {
const sectionId = String(section?.id || "")
if (!sectionId || state.busySection) return
const sectionTitle = String(section?.title || "this section")
if (!confirm(`Reset ${sectionTitle} to defaults?`)) return
state.busySection = sectionId
try {
const response = await fetch("/api/troubleshoot/reset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sectionId }),
})
const payload = await response.json()
if (!response.ok) {
throw new Error(payload.error || response.statusText || "Failed to reset section")
}
showSnackbar(payload.message || `${sectionTitle} reset.`, "", 2500, { key: "troubleshoot-reset" })
await fetchTroubleshoot(false)
} catch (error) {
showSnackbar(error?.message || "Failed to reset section.", "error", 3000, { key: "troubleshoot-reset" })
} finally {
state.busySection = ""
}
}
function initialize() {
if (initialized) return
initialized = true
fetchTroubleshoot(false)
}
function itemRows(section) {
const items = Array.isArray(section?.items) ? section.items : []
return items.map((item) => html`
<div class="troubleshootItemRow">
<div class="troubleshootItemLabel">${item.label}</div>
<div class="troubleshootItemValue">${formatValue(item.value)}</div>
<div class="troubleshootItemDefault">${formatValue(item.defaultValue)}</div>
</div>
`)
}
export function Troubleshoot() {
initialize()
return html`
<div class="troubleshootPage">
<h2>Troubleshoot</h2>
${() => state.loading ? html`<div class="troubleshootCard">Loading troubleshoot data...</div>` : ""}
${() => !state.loading ? html`
<div class="troubleshootCard">
<p class="troubleshootIntro">
Quick diagnostics snapshot for weird behavior reports and copy-ready debug logs.
</p>
<div class="troubleshootActionRow">
<button class="troubleshootButton" ?disabled="${state.refreshing}" @click="${() => fetchTroubleshoot(true)}">
${state.refreshing ? "Refreshing..." : "Refresh"}
</button>
<button class="troubleshootButton" @click="${copyToClipboard}">
Copy to Clipboard
</button>
</div>
${() => state.error ? html`<p class="troubleshootError"><strong>Error:</strong> ${state.error}</p>` : ""}
<p class="troubleshootStatusLine"><strong>Onroad:</strong> ${state.isOnroad ? "Yes" : "No"}</p>
</div>
<div class="troubleshootCard">
<h3>Snapshot</h3>
<div class="troubleshootHeaderRow troubleshootHeaderRowSnapshot">
<span>Field</span>
<span>Value</span>
</div>
${() => state.snapshot.map((item) => html`
<div class="troubleshootSnapshotRow">
<div class="troubleshootItemLabel">${item.label}</div>
<div class="troubleshootItemValue">${formatValue(item.value)}</div>
</div>
`)}
</div>
${() => state.sections.map((section) => html`
<div class="troubleshootCard">
<div class="troubleshootSectionHeader">
<h3>${section.title}</h3>
${section.resettable ? html`
<button
class="troubleshootButton troubleshootDanger"
?disabled="${state.busySection === section.id}"
@click="${() => resetSection(section)}">
${state.busySection === section.id ? "Resetting..." : "Reset to Default"}
</button>
` : ""}
</div>
<div class="troubleshootHeaderRow">
<span>Setting</span>
<span>Current</span>
<span>Default</span>
</div>
${itemRows(section)}
</div>
`)}
` : ""}
</div>
`
}
@@ -73,7 +73,7 @@
transition: opacity var(--transition-slow);
}
.tskkeys-group > .tskkeys-row:last-of-type {
.tskkeys-group>.tskkeys-row:last-of-type {
margin-bottom: 0;
}
@@ -105,7 +105,7 @@
.tskkeys-input:hover,
.tskkeys-input:focus {
border-color: var(--thumb-color);
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
box-shadow: var(--glow-primary);
transform: var(--hover-scale-sm);
}
@@ -211,4 +211,4 @@
.tskkeys-input {
font-size: var(--font-size-sm);
}
}
}
@@ -1,4 +1,4 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { Modal } from "/assets/components/modal.js"
const state = reactive({
@@ -165,15 +165,16 @@ const api = {
}
}
let hasLoaded = false
if (!hasLoaded) {
hasLoaded = true
api.load()
}
export function TSKManager() {
return html`
<div class="tskkeys-wrapper tskkeys-offset-top">
let hasLoaded = false
export function TSKManager() {
if (!hasLoaded) {
hasLoaded = true
api.load()
}
return html`
<div class="tskkeys-wrapper tskkeys-offset-top">
<div class="tskkeys-container">
<div class="tskkeys-title">Toyota Security Key Manager</div>
@@ -0,0 +1,206 @@
.updateManager {
max-width: var(--width-xxl);
}
.updateActions {
display: flex;
gap: var(--gap-sm);
margin-top: var(--margin-base);
}
.updateButton {
background: var(--main-fg);
border: none;
border-radius: var(--border-radius-sm);
color: var(--text-color);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-demi-bold);
padding: var(--padding-sm) var(--padding-base);
}
.updateButton.danger {
background: var(--danger-bg);
}
.updateButton.danger:hover {
background: var(--danger-hover-bg);
}
.updateButton:hover {
transform: var(--hover-scale-sm);
transition: transform var(--transition-fast), background-color var(--transition-fast);
}
.updateButton:disabled {
cursor: not-allowed;
opacity: 0.55;
transform: none;
}
.updateCard {
background-color: var(--secondary-bg);
border-radius: var(--border-radius-md);
color: var(--text-color);
max-width: 80%;
padding: var(--border-radius-xl);
}
.updateError {
color: var(--danger-fg);
margin-top: var(--margin-sm);
}
.updateGrid {
display: grid;
gap: 0.5em 1em;
grid-template-columns: repeat(3, 1fr);
}
.updateGrid p {
font-size: var(--font-size-sm);
margin: 0;
}
.updateLink {
color: var(--main-fg);
display: inline-block;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-bold);
}
.updateMessage {
margin-top: var(--margin-sm);
}
.updateProgressCard {
margin-top: var(--margin-base);
}
.updateProgressDetail {
font-size: var(--font-size-sm);
margin-top: var(--margin-sm);
}
.updateProgressDetail.error {
color: var(--danger-fg);
}
.updateProgressFill {
background: linear-gradient(90deg, #5ec8c8 0%, #8b6cc5 100%);
border-radius: var(--border-radius-sm);
height: 100%;
transition: width var(--transition-base);
}
.updateProgressFill.error {
background: linear-gradient(90deg, #b43a3a 0%, #de5656 100%);
}
.updateProgressHeader {
display: flex;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-demi-bold);
justify-content: space-between;
margin-bottom: var(--margin-xs);
}
.updateProgressTrack {
background: var(--track-color);
border-radius: var(--border-radius-sm);
height: 12px;
overflow: hidden;
}
.updateProgressTrack.error {
border: 1px solid var(--danger-bg);
}
.updateToggleRow {
align-items: center;
display: flex;
gap: var(--gap-sm);
justify-content: space-between;
margin-top: var(--margin-base);
}
.updateWarning {
margin-top: var(--margin-base);
}
.updateHint {
font-size: var(--font-size-sm);
margin-top: var(--margin-sm);
}
.updateRebootNotice {
font-weight: var(--font-weight-bold);
}
.updateFooter {
border-top: 1px solid var(--track-color);
margin-top: var(--margin-base);
padding-top: var(--margin-sm);
}
.updateBranchSection {
margin-top: var(--margin-base);
}
.updateAdvancedNote {
font-size: var(--font-size-sm);
margin-top: var(--margin-sm);
}
.updateBranchHeader {
align-items: center;
display: flex;
font-size: var(--font-size-sm);
justify-content: space-between;
}
.updateBranchRow {
align-items: center;
display: flex;
gap: var(--gap-sm);
margin-top: var(--margin-sm);
}
.updateSelect {
background-color: var(--main-bg);
border: 1px solid var(--track-color);
border-radius: var(--border-radius-sm);
color: var(--text-color);
flex: 1;
font-size: var(--font-size-sm);
min-width: 0;
padding: var(--padding-sm);
}
.updateSelect:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.updateToggleRow input:disabled {
cursor: not-allowed;
opacity: 0.6;
}
@media only screen and (max-width: 768px) and (orientation: portrait) {
.updateActions {
flex-direction: column;
}
.updateBranchRow {
flex-direction: column;
align-items: stretch;
}
.updateCard {
max-width: 100%;
}
.updateGrid {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
}
@@ -0,0 +1,617 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
const state = reactive({
loading: true,
error: "",
status: null,
checkedForUpdates: false,
checkBusy: false,
updateBusy: false,
toggleBusy: false,
showAdvancedOptions: false,
branches: [],
selectedBranch: "",
hasManualBranchSelection: false,
branchesError: "",
branchesBusy: false,
switchBusy: false,
})
let initialized = false
let pollHandle = null
let reconnectPending = false
let rebootNoticeShown = false
const POLL_INTERVAL_MS = 1000
const ADVANCED_OPTIONS_KEY = "softwareShowAdvancedOptions"
function isUpdateRouteActive() {
return window.location.pathname === "/manage_updates"
}
function shortHash(value) {
const text = String(value || "").trim()
return text ? text.slice(0, 10) : "Unknown"
}
function toPercent(value) {
const n = Number(value)
if (!Number.isFinite(n)) return 0
return Math.max(0, Math.min(100, n))
}
function findBranchInList(branches, target) {
const wanted = String(target || "").trim()
if (!wanted || !Array.isArray(branches) || branches.length === 0) return ""
if (branches.includes(wanted)) return wanted
const wantedLower = wanted.toLowerCase()
const caseInsensitive = branches.find((branch) => String(branch || "").toLowerCase() === wantedLower)
return caseInsensitive || ""
}
function isSelectedBranchDifferent() {
const currentBranch = String(state.status?.branch || "").trim()
const selectedBranch = String(state.selectedBranch || "").trim()
return !!currentBranch && !!selectedBranch && currentBranch !== selectedBranch
}
function normalizeGithubRemote(remoteValue, commitsUrlValue = "") {
let remote = String(remoteValue || "").trim()
if (!remote) {
const commitsUrl = String(commitsUrlValue || "").trim()
const marker = "/commits/"
const markerIndex = commitsUrl.indexOf(marker)
if (commitsUrl.startsWith("https://github.com/") && markerIndex > 0) {
remote = commitsUrl.slice(0, markerIndex)
} else {
return ""
}
}
if (remote.startsWith("git@github.com:")) {
remote = `https://github.com/${remote.split(":", 2)[1] || ""}`
} else if (remote.startsWith("ssh://git@github.com/")) {
remote = `https://github.com/${remote.split("ssh://git@github.com/", 2)[1] || ""}`
} else if (remote.startsWith("http://github.com/")) {
remote = `https://github.com/${remote.split("http://github.com/", 2)[1] || ""}`
}
if (!remote.startsWith("https://github.com/")) return ""
remote = remote.replace(/\/+$/, "")
if (remote.endsWith(".git")) remote = remote.slice(0, -4)
return remote
}
function currentOrSelectedBranchForCommits() {
const selected = String(state.selectedBranch || "").trim()
if (selected) return selected
return String(state.status?.branch || "").trim()
}
function activeCommitsUrl() {
const branch = currentOrSelectedBranchForCommits()
if (!branch) return ""
const remote = normalizeGithubRemote(state.status?.originRemote, state.status?.commitsUrl)
if (!remote) return ""
return `${remote}/commits/${encodeURIComponent(branch)}/`
}
function activeCommitsLabel() {
const branch = currentOrSelectedBranchForCommits()
return branch
? `View latest commit for the "${branch}" branch`
: "View latest commit for this branch"
}
function shouldShowPrimaryUpdateAction() {
if (state.status?.running) return true
if (isSelectedBranchDifferent()) return true
return !!state.checkedForUpdates && !!state.status?.updateAvailable
}
function shouldContinuePolling() {
return !!state.status?.running || state.status?.stage === "rebooting" || reconnectPending
}
function shouldShowRebootNotice() {
if (state.status?.stage === "rebooting") return true
if (reconnectPending) return true
const message = String(state.status?.message || "").toLowerCase()
return message.includes("reboot")
}
function isHtmlLike(text) {
const value = String(text || "").trim()
return value.startsWith("<!DOCTYPE") || value.startsWith("<html") || value.startsWith("<")
}
async function readJsonPayload(response) {
const bodyText = await response.text()
if (!bodyText) return {}
try {
return JSON.parse(bodyText)
} catch (parseError) {
const error = new Error("Invalid JSON response")
error.cause = parseError
error.bodyText = bodyText
throw error
}
}
function setRebootingUiState(detail = "Reboot in progress...") {
state.status = {
...(state.status || {}),
running: false,
stage: "rebooting",
progressStep: 5,
progressTotalSteps: state.status?.progressTotalSteps || 5,
progressStepPercent: 100,
progressPercent: 100,
progressLabel: "Rebooting device",
progressDetail: detail,
message: "Update complete. Device is rebooting. Please wait...",
}
state.error = ""
}
function isRebootRelatedConnectionError(error) {
const message = String(error?.message || "")
return /Invalid JSON response|Unexpected token '<'|Failed to fetch|NetworkError|Load failed/i.test(message)
}
function stopPolling() {
if (!pollHandle) return
clearTimeout(pollHandle)
pollHandle = null
}
function ensurePolling() {
if (pollHandle) return
const poll = async () => {
if (!isUpdateRouteActive()) {
pollHandle = null
return
}
if (document.visibilityState !== "visible") {
pollHandle = setTimeout(poll, POLL_INTERVAL_MS)
return
}
await fetchStatus(false)
if (shouldContinuePolling()) {
pollHandle = setTimeout(poll, POLL_INTERVAL_MS)
} else {
pollHandle = null
}
}
pollHandle = setTimeout(poll, POLL_INTERVAL_MS)
}
async function fetchStatus(showToast) {
if (showToast) state.checkBusy = true
try {
const response = await fetch("/api/update/fast/status")
const payload = await readJsonPayload(response)
if (!response.ok) {
throw new Error(payload.error || response.statusText || "Failed to load update status")
}
state.status = payload
state.error = ""
if (reconnectPending && !payload.running && payload.stage !== "rebooting") {
reconnectPending = false
rebootNoticeShown = false
showSnackbar("Connection reestablished.")
}
if (payload.stage === "rebooting" && !rebootNoticeShown) {
rebootNoticeShown = true
showSnackbar("Device rebooting, please wait for reconnection...")
}
if (!state.selectedBranch && payload.branch) {
state.selectedBranch = payload.branch
}
if (shouldContinuePolling()) {
ensurePolling()
} else {
stopPolling()
}
if (showToast) {
state.checkedForUpdates = true
showSnackbar(payload.updateAvailable ? "Update available." : "No update available.")
}
} catch (error) {
const isRebootTransitionError = !showToast
&& (state.status?.running || state.status?.stage === "rebooting" || reconnectPending)
&& (isHtmlLike(error?.bodyText) || isRebootRelatedConnectionError(error))
if (isRebootTransitionError) {
reconnectPending = true
setRebootingUiState("Device rebooting, please wait for reconnection...")
if (!rebootNoticeShown) {
rebootNoticeShown = true
showSnackbar("Device rebooting, please wait for reconnection...")
}
ensurePolling()
return
}
const message = error?.message || String(error)
state.error = message
if (showToast) {
showSnackbar(message, "error")
}
} finally {
if (showToast) state.checkBusy = false
}
}
async function fetchBranches(showToast = false) {
state.branchesBusy = true
try {
const response = await fetch("/api/update/branches")
const payload = await readJsonPayload(response)
if (!response.ok) {
throw new Error(payload.error || response.statusText || "Failed to load branches")
}
const branches = Array.isArray(payload.branches) ? payload.branches : []
const currentBranchRaw = String(payload.currentBranch || state.status?.branch || "").trim()
const matchedCurrentBranch = findBranchInList(branches, currentBranchRaw)
state.branches = branches
state.branchesError = payload.remoteError || ""
if (branches.length === 0) {
state.selectedBranch = currentBranchRaw
state.hasManualBranchSelection = false
} else if (matchedCurrentBranch && !state.hasManualBranchSelection) {
// Default to the branch currently installed unless the user picked one explicitly.
state.selectedBranch = matchedCurrentBranch
} else if (!state.selectedBranch || !branches.includes(state.selectedBranch)) {
state.selectedBranch = matchedCurrentBranch || branches[0]
state.hasManualBranchSelection = false
}
if (showToast) {
showSnackbar(branches.length ? `Loaded ${branches.length} branches.` : "No branches found.")
}
} catch (error) {
const message = error?.message || "Failed to load branches"
state.branchesError = message
if (showToast) {
showSnackbar(message, "error")
}
} finally {
state.branchesBusy = false
}
}
function setAdvancedOptions(enabled) {
const next = !!enabled
state.showAdvancedOptions = next
if (!next) {
const currentBranch = String(state.status?.branch || "").trim()
if (currentBranch) {
state.selectedBranch = currentBranch
}
state.hasManualBranchSelection = false
}
try {
localStorage.setItem(ADVANCED_OPTIONS_KEY, next ? "1" : "0")
} catch (error) {
console.warn("Failed to persist advanced options preference", error)
}
if (next && state.branches.length === 0 && !state.branchesBusy) {
fetchBranches(false)
}
}
async function setAutomaticUpdates(enabled) {
if (state.toggleBusy) return
if (state.status?.isOnroad) {
showSnackbar("Actions are blocked while onroad.", "error")
return
}
state.toggleBusy = true
try {
const response = await fetch("/api/params", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: "AutomaticUpdates", value: !!enabled }),
})
const payload = await response.json()
if (!response.ok) {
throw new Error(payload.error || response.statusText || "Failed to update Automatic Updates")
}
state.status = { ...(state.status || {}), automaticUpdates: !!enabled }
showSnackbar(payload.message || "Automatic updates updated.")
} catch (error) {
showSnackbar(error?.message || "Failed to update Automatic Updates", "error")
await fetchStatus(false)
} finally {
state.toggleBusy = false
}
}
async function runFastUpdate() {
if (state.updateBusy) return
if (state.status?.running) {
showSnackbar("Fast update is already running.")
return
}
if (state.status?.isOnroad) {
showSnackbar("Actions are blocked while onroad.", "error")
return
}
const confirmed = window.confirm(
"Fast update warning:\n\n" +
"- This update method skips backup creation.\n" +
"- Your device will reboot when the update is done.\n\n" +
"Continue with fast update?"
)
if (!confirmed) return
state.updateBusy = true
try {
reconnectPending = false
rebootNoticeShown = false
const response = await fetch("/api/update/fast", { method: "POST" })
const payload = await readJsonPayload(response)
if (!response.ok) {
throw new Error(payload.error || response.statusText || "Failed to start fast update")
}
state.status = {
...(state.status || {}),
running: true,
stage: "starting",
progressStep: 1,
progressTotalSteps: state.status?.progressTotalSteps || 5,
progressStepPercent: 0,
progressPercent: 0,
progressLabel: "Preparing update",
progressDetail: "Initializing update process...",
message: payload.message || "Fast update started. Device will reboot when complete.",
lastError: "",
}
state.error = ""
showSnackbar(payload.message || "Fast update started.")
await fetchStatus(false)
ensurePolling()
} catch (error) {
showSnackbar(error?.message || "Failed to start fast update", "error")
} finally {
state.updateBusy = false
}
}
async function runBranchSwitch() {
if (state.switchBusy) return
if (state.status?.running) {
showSnackbar("An update action is already running.")
return
}
if (state.status?.isOnroad) {
showSnackbar("Actions are blocked while onroad.", "error")
return
}
const targetBranch = String(state.selectedBranch || "").trim()
if (!targetBranch) {
showSnackbar("Select a target branch first.", "error")
return
}
const currentBranch = String(state.status?.branch || "").trim()
const actionLabel = currentBranch && currentBranch === targetBranch ? "update" : "switch and update"
const confirmed = window.confirm(
`This will ${actionLabel} to the '${targetBranch}' branch.\n\n` +
"- This update method skips backup creation.\n" +
"- Your device will reboot when the update is done.\n\n" +
"Continue?"
)
if (!confirmed) return
state.switchBusy = true
try {
reconnectPending = false
rebootNoticeShown = false
const response = await fetch("/api/update/branch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ branch: targetBranch }),
})
const payload = await readJsonPayload(response)
if (!response.ok) {
throw new Error(payload.error || response.statusText || "Failed to start branch switch")
}
state.status = {
...(state.status || {}),
running: true,
stage: "starting",
progressStep: 1,
progressTotalSteps: state.status?.progressTotalSteps || 5,
progressStepPercent: 0,
progressPercent: 0,
progressLabel: "Preparing branch switch",
progressDetail: "Initializing branch switch...",
message: payload.message || `Branch switch started for '${targetBranch}'. Device will reboot when complete.`,
lastError: "",
}
state.error = ""
showSnackbar(payload.message || "Branch switch started.")
await fetchStatus(false)
ensurePolling()
} catch (error) {
showSnackbar(error?.message || "Failed to start branch switch", "error")
} finally {
state.switchBusy = false
}
}
function initialize() {
if (initialized) return
initialized = true
try {
state.showAdvancedOptions = localStorage.getItem(ADVANCED_OPTIONS_KEY) === "1"
} catch (error) {
state.showAdvancedOptions = false
}
const initTasks = [fetchStatus(false)]
if (state.showAdvancedOptions) {
initTasks.push(fetchBranches(false))
}
Promise.all(initTasks).finally(() => {
state.loading = false
})
}
export function UpdateManager() {
initialize()
return html`
<div class="updateManager">
<h2>Software</h2>
${() => state.loading ? html`<div class="updateCard">Loading update status...</div>` : ""}
${() => !state.loading ? html`
<div class="updateCard">
<div class="updateGrid">
<p><strong>Current Branch:</strong> ${state.status?.branch || "Unknown"}</p>
<p><strong>Installed Commit:</strong> ${shortHash(state.status?.localCommit)}</p>
<p><strong>Latest Commit:</strong> ${shortHash(state.status?.remoteCommit)}</p>
<p><strong>Update Available:</strong> ${state.status?.updateAvailable ? "Yes" : "No"}</p>
<p><strong>Status:</strong> ${state.status?.stage || "idle"}</p>
<p><strong>Onroad:</strong> ${state.status?.isOnroad ? "Yes" : "No"}</p>
</div>
<div class="updateProgressCard">
<div class="updateProgressHeader">
<span>
Step ${state.status?.progressStep || 0}/${state.status?.progressTotalSteps || 5}:
${state.status?.progressLabel || "Idle"}
</span>
<span>${Math.round(toPercent(state.status?.progressPercent))}%</span>
</div>
<div class="updateProgressTrack ${state.status?.stage === "error" ? "error" : ""}">
<div class="updateProgressFill ${state.status?.stage === "error" ? "error" : ""}" style="width: ${toPercent(state.status?.progressPercent)}%;"></div>
</div>
${() => state.status?.progressDetail ? html`<p class="updateProgressDetail ${state.status?.stage === "error" ? "error" : ""}">${state.status.progressDetail}</p>` : ""}
</div>
${() => state.status?.isOnroad ? html`<p class="updateWarning"><strong>Onroad: actions disabled</strong></p>` : ""}
<label class="updateToggleRow">
<span>Automatically install updates</span>
<input
type="checkbox"
class="ds-toggle"
?checked="${!!state.status?.automaticUpdates}"
?disabled="${!!state.status?.isOnroad || state.toggleBusy || !!state.status?.running}"
@change="${(event) => setAutomaticUpdates(!!event.target.checked)}"
/>
</label>
<label class="updateToggleRow updateAdvancedToggle">
<span>Show advanced options</span>
<input
type="checkbox"
class="ds-toggle"
?checked="${() => state.showAdvancedOptions}"
@change="${(event) => setAdvancedOptions(!!event.target.checked)}"
/>
</label>
${() => state.showAdvancedOptions ? html`
<div class="updateBranchSection">
<div class="updateBranchHeader">
<strong>Branch switching</strong>
${state.branchesBusy ? html`<span>Loading...</span>` : ""}
</div>
<p class="updateAdvancedNote">
For advanced users only. Switching to test/dev branches can introduce instability.
</p>
<div class="updateBranchRow">
<select
class="updateSelect"
?disabled="${!!state.status?.isOnroad || !!state.status?.running || state.switchBusy || state.branchesBusy || state.branches.length === 0}"
@change="${(event) => {
state.selectedBranch = String(event.target.value || "")
state.hasManualBranchSelection = true
}}">
${() => state.branches.length
? state.branches.map((branch) => html`<option value="${branch}" ${branch === state.selectedBranch ? "selected" : ""}>${branch}${branch === state.status?.branch ? " (current)" : ""}</option>`)
: html`<option value="">No branches found</option>`
}
</select>
<button
class="updateButton"
?disabled="${state.branchesBusy || !!state.status?.running}"
@click="${() => fetchBranches(true)}">
${state.branchesBusy ? "Refreshing..." : "Refresh Branches"}
</button>
</div>
${() => state.branchesError ? html`<p class="updateError"><strong>Branch List:</strong> ${state.branchesError}</p>` : ""}
</div>
` : ""}
${() => state.status?.message && state.status?.stage !== "rebooting" ? html`<p class="updateMessage">${state.status.message}</p>` : ""}
${() => state.status?.remoteError ? html`<p class="updateError"><strong>Remote Check:</strong> ${state.status.remoteError}</p>` : ""}
${() => state.status?.lastError ? html`<p class="updateError"><strong>Last Error:</strong> ${state.status.lastError}</p>` : ""}
${() => state.error ? html`<p class="updateError"><strong>Error:</strong> ${state.error}</p>` : ""}
<div class="updateActions">
${() => !isSelectedBranchDifferent() ? html`
<button class="updateButton" @click="${() => fetchStatus(true)}">
${state.checkBusy ? "Checking..." : "Check for Updates"}
</button>
` : ""}
${() => shouldShowPrimaryUpdateAction() ? html`
<button
class="updateButton danger"
?disabled="${!!state.status?.isOnroad || !!state.status?.running || (isSelectedBranchDifferent() ? state.switchBusy : state.updateBusy)}"
@click="${() => isSelectedBranchDifferent() ? runBranchSwitch() : runFastUpdate()}">
${state.status?.running
? "Update Running..."
: (isSelectedBranchDifferent()
? (state.switchBusy ? "Starting..." : "Switch + Update")
: (state.updateBusy ? "Starting..." : "Fast Update (No Backup)"))}
</button>
` : ""}
</div>
${() => !state.status?.running && !shouldShowPrimaryUpdateAction()
? html`<p class="updateHint">Run <strong>Check for Updates</strong> first, or select a different branch in advanced options.</p>`
: ""}
${() => shouldShowRebootNotice()
? html`<p class="updateHint updateRebootNotice">Device rebooting, please wait for reconnection...</p>`
: ""}
${() => activeCommitsUrl() ? html`
<div class="updateFooter">
<a class="updateLink" href="${activeCommitsUrl()}" target="_blank" rel="noopener noreferrer">
${activeCommitsLabel()}
</a>
</div>
` : ""}
</div>
` : ""}
</div>
`
}
@@ -0,0 +1,162 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { DoorControl } from "/assets/components/tools/doors.js"
import { TSKManager } from "/assets/components/tools/tsk_manager.js"
const state = reactive({
activeTool: null,
toolStatus: {
doors: "untested", // untested, allowed, denied
tsk: "untested"
},
loading: false
})
async function checkToolAvailability(toolName) {
if (state.toolStatus[toolName] !== "untested") {
state.activeTool = toolName;
return;
}
state.loading = true;
state.activeTool = toolName;
try {
const response = await fetch(`/api/car_features_check?tool=${toolName}`);
const data = await response.json();
state.toolStatus[toolName] = data.result ? "allowed" : "denied";
} catch (error) {
console.error("Failed to check feature availability:", error);
state.toolStatus[toolName] = "denied";
} finally {
state.loading = false;
}
}
export function VehicleFeatures() {
return html`
<style>
.vf-container {
padding: var(--padding-lg);
max-width: var(--max-width-content);
margin: 0 auto;
}
.vf-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--gap-lg);
margin-top: var(--margin-lg);
}
.vf-card {
background: var(--card-bg);
border: var(--border-width-thin) var(--border-style-base) var(--sidebar-border-color);
border-radius: var(--border-radius-lg);
padding: var(--padding-lg);
cursor: pointer;
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.vf-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
border-color: var(--main-fg);
}
.vf-card i {
font-size: 2.5rem;
margin-bottom: var(--margin-sm);
color: var(--main-fg);
}
.vf-card h3 {
margin: 0 0 var(--margin-xs) 0;
font-size: var(--font-size-lg);
}
.vf-card p {
margin: 0;
font-size: var(--font-size-sm);
color: var(--text-muted);
}
.vf-back {
background: none;
border: none;
color: var(--main-fg);
cursor: pointer;
font-size: var(--font-size-base);
display: flex;
align-items: center;
gap: var(--gap-sm);
padding: 0;
margin-bottom: var(--margin-lg);
}
.vf-back:hover {
text-decoration: underline;
}
.vf-error {
margin-top: var(--margin-lg);
padding: var(--padding-base);
background: rgba(224, 85, 119, 0.1);
border: var(--border-width-thin) var(--border-style-base) var(--danger-fg);
border-radius: var(--border-radius-md);
color: var(--danger-fg);
text-align: center;
}
.vf-loader {
text-align: center;
padding: var(--padding-xxl);
color: var(--text-muted);
}
</style>
<div class="vf-container">
${() => {
if (!state.activeTool) {
return html`
<h1>Vehicle Specific Features</h1>
<p style="color: var(--text-muted); margin-bottom: var(--margin-xl);">
Select a feature below to access it. These features verify vehicle compatibility when launched.
</p>
<div class="vf-grid">
<div class="vf-card" @click="${() => checkToolAvailability('doors')}">
<i class="bi bi-door-closed"></i>
<h3>Lock/Unlock Doors</h3>
<p>Send lock or unlock commands remotely to your vehicle.</p>
</div>
<div class="vf-card" @click="${() => checkToolAvailability('tsk')}">
<i class="bi bi-key-fill"></i>
<h3>Toyota Security Keys</h3>
<p>Manage and apply security keys for secOC protected devices.</p>
</div>
</div>
`;
}
return html`
<button class="vf-back" @click="${() => { state.activeTool = null; }}">
<i class="bi bi-arrow-left"></i> Back to Features
</button>
${() => {
if (state.loading) {
return html`<div class="vf-loader"><i class="bi bi-hourglass-split"></i> Verifying vehicle compatibility...</div>`;
}
if (state.toolStatus[state.activeTool] === "denied") {
const toolNames = { doors: "Lock/Unlock Doors", tsk: "Toyota Security Keys" };
return html`
<div class="vf-error">
<i class="bi bi-exclamation-triangle-fill" style="font-size: 2rem; display: block; margin-bottom: var(--margin-sm);"></i>
<strong>${toolNames[state.activeTool]}</strong> is not supported for your current vehicle.
</div>
`;
}
if (state.activeTool === "doors") return DoorControl();
if (state.activeTool === "tsk") return TSKManager();
return "";
}}
`;
}}
</div>
`;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 680 B

After

Width:  |  Height:  |  Size: 975 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1008 KiB

After

Width:  |  Height:  |  Size: 332 KiB

+39 -15
View File
@@ -1,26 +1,50 @@
function showSnackbar(msg, level, timeout = 3500) {
function showSnackbar(msg, level, timeout = 3500, options = {}) {
const wrapper = document.getElementById("snackbar_wrapper")
if (!wrapper) return
const key = String(options?.key || "").trim()
const replace = options?.replace !== false
const dismissSnackbar = (snackbar) => {
if (!snackbar) return
if (snackbar.__hideTimer) clearTimeout(snackbar.__hideTimer)
if (snackbar.__removeTimer) clearTimeout(snackbar.__removeTimer)
snackbar.style.opacity = 0
snackbar.__removeTimer = setTimeout(() => snackbar.remove(), 1000)
}
const setSnackbarContent = (snackbar) => {
snackbar.innerHTML = msg
snackbar.className = "snackbar show"
if (level === "error") {
snackbar.style.backgroundColor = "#f44336"
} else {
snackbar.style.backgroundColor = ""
}
}
if (key && replace) {
const existing = Array.from(wrapper.children).find((el) => el.dataset?.snackbarKey === key)
if (existing) {
setSnackbarContent(existing)
existing.style.opacity = 1
if (existing.__hideTimer) clearTimeout(existing.__hideTimer)
if (existing.__removeTimer) clearTimeout(existing.__removeTimer)
existing.__hideTimer = setTimeout(() => dismissSnackbar(existing), timeout)
return
}
}
// Ensure max 2 snackbars are visible
if (wrapper.children.length >= 2) {
const first = wrapper.children[0]
first.style.opacity = 0
setTimeout(() => first.remove(), 1000)
dismissSnackbar(wrapper.children[0])
}
const snackbar = document.createElement("div")
snackbar.innerHTML = msg
snackbar.className = "snackbar show"
snackbar.id = `snackbar_${Math.random().toString(36).slice(5)}`
if (level === "error") {
snackbar.style.backgroundColor = "#f44336"
}
if (key) snackbar.dataset.snackbarKey = key
setSnackbarContent(snackbar)
wrapper.appendChild(snackbar)
setTimeout(() => {
snackbar.style.opacity = 0
setTimeout(() => snackbar.remove(), 1000)
}, timeout)
snackbar.__hideTimer = setTimeout(() => dismissSnackbar(snackbar), timeout)
}
@@ -65,3 +65,11 @@ export function hideSidebar() {
document.getElementById("sidebarUnderlay")?.classList.add("hidden")
html.classList.remove("no_scroll")
}
/**
* Returns true when the page is being accessed through the Galaxy tunnel
* (the public-facing domain is galaxy.firestar.link)
*/
export function isGalaxyTunnel() {
return window.location.hostname === 'galaxy.firestar.link';
}
@@ -1,5 +1,5 @@
{
"name": "The Pond",
"name": "Galaxy",
"short_name": "",
"icons": [
{
@@ -17,4 +17,4 @@
"background_color": "#151414",
"theme_color": "#151414",
"display": "standalone"
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+67 -47
View File
@@ -1,59 +1,79 @@
<!doctype html>
<html lang="en" id="htmlElement" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/images/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/images/favicon-16x16.png">
<link rel="manifest" href="/assets/manifest.json">
<link rel="shortcut icon" href="/assets/images/favicon.ico">
<meta name="theme-color" content="#4285f4" />
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/images/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/images/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<link rel="shortcut icon" href="/assets/images/favicon.ico">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link href="https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.css" rel="stylesheet">
<meta name="theme-color" content="#8b6cc5" />
<link rel="stylesheet" href="/assets/components/home/home.css">
<link rel="stylesheet" href="/assets/components/main.css">
<link rel="stylesheet" href="/assets/components/modal.css">
<link rel="stylesheet" href="/assets/components/navigation/navigation_destination.css">
<link rel="stylesheet" href="/assets/components/navigation/navigation_keys.css">
<link rel="stylesheet" href="/assets/components/recordings/dashcam_routes.css">
<link rel="stylesheet" href="/assets/components/recordings/screen_recordings.css">
<link rel="stylesheet" href="/assets/components/settings.css">
<link rel="stylesheet" href="/assets/components/sidebar.css">
<link rel="stylesheet" href="/assets/components/tailscale/tailscale.css">
<link rel="stylesheet" href="/assets/components/tools/doors.css">
<link rel="stylesheet" href="/assets/components/tools/error_logs.css">
<link rel="stylesheet" href="/assets/components/tools/speed_limits.css">
<link rel="stylesheet" href="/assets/components/tools/theme_maker.css">
<link rel="stylesheet" href="/assets/components/tools/tmux.css">
<link rel="stylesheet" href="/assets/components/tools/toggles.css">
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script type="module" src="/assets/components/router.js"></script>
<script src="/assets/js/snackbar.js"></script>
<link rel="stylesheet" href="/assets/components/home/home.css">
<link rel="stylesheet" href="/assets/components/main.css">
<link rel="stylesheet" href="/assets/components/modal.css">
<link rel="stylesheet" href="/assets/components/navigation/navigation_destination.css">
<link rel="stylesheet" href="/assets/components/navigation/navigation_keys.css">
<link rel="stylesheet" href="/assets/components/recordings/dashcam_routes.css">
<link rel="stylesheet" href="/assets/components/recordings/screen_recordings.css">
<link rel="stylesheet" href="/assets/components/settings.css">
<link rel="stylesheet" href="/assets/components/sidebar.css">
<link rel="stylesheet" href="/assets/components/tailscale/tailscale.css">
<link rel="stylesheet" href="/assets/components/tools/doors.css">
<link rel="stylesheet" href="/assets/components/tools/error_logs.css">
<link rel="stylesheet" href="/assets/components/tools/model_manager.css">
<link rel="stylesheet" href="/assets/components/tools/plots.css">
<link rel="stylesheet" href="/assets/components/tools/speed_limits.css">
<link rel="stylesheet" href="/assets/components/tools/theme_maker.css">
<link rel="stylesheet" href="/assets/components/tools/testing_ground.css">
<link rel="stylesheet" href="/assets/components/tools/troubleshoot.css">
<link rel="stylesheet" href="/assets/components/tools/tmux.css">
<link rel="stylesheet" href="/assets/components/tools/toggles.css">
<link rel="stylesheet" href="/assets/components/tools/update_manager.css">
<link rel="stylesheet" href="/assets/components/tools/device_settings.css">
<link rel="stylesheet" href="/assets/components/tools/galaxy.css">
<link rel="stylesheet" href="/assets/components/tools/longitudinal_maneuvers.css">
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.js"></script>
<script type="module">
import("/assets/components/router.js").catch((err) => {
console.error("[the_pond] bootstrap failed", err);
const target = document.getElementById("app") || document.body;
const pre = document.createElement("pre");
pre.style.whiteSpace = "pre-wrap";
pre.style.color = "#ff8080";
pre.style.background = "#200";
pre.style.padding = "12px";
pre.style.margin = "12px";
pre.textContent = `[the_pond] failed to load router:\n${err?.stack || err}`;
target.appendChild(pre);
});
</script>
<script src="/assets/js/snackbar.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap"
rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300..800&family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap"
rel="stylesheet">
<title>The Pond</title>
</head>
<title>Galaxy</title>
</head>
<body>
<div id="app"></div>
<!-- Menu button for phone -->
<div id="menu_button">
<i class="bi bi-list"></i>
</div>
<!-- Snackbar for messages -->
<div id="snackbar_wrapper"></div>
</body>
<body>
<div id="app"></div>
<!-- Menu button for phone -->
<div id="menu_button">
<i class="bi bi-list"></i>
</div>
<!-- Snackbar for messages -->
<div id="snackbar_wrapper"></div>
</body>
</html>
File diff suppressed because it is too large Load Diff
+13 -38
View File
@@ -8,7 +8,6 @@ import secrets
import shutil
import subprocess
import time
import uuid
from datetime import datetime
from pathlib import Path
@@ -563,20 +562,11 @@ def process_route(footage_path, route_name):
return {
"name": route_name,
"gif": f"/thumbnails/{route_name}--0/preview.gif",
"png": f"/thumbnails/{route_name}--0/preview.png",
"timestamp": route_timestamp_str,
"is_preserved": has_preserve_attr(segment_path)
}
def process_route_gif(footage_path, route_name):
segment_path = f"{footage_path}{route_name}--0"
qcamera_path = f"{segment_path}/qcamera.ts"
gif_output_path = os.path.join(segment_path, "preview.gif")
if not os.path.exists(gif_output_path):
video_to_gif(qcamera_path, gif_output_path)
def process_screen_recording(mp4):
stem = mp4.with_suffix("")
png_path = stem.with_suffix(".png")
@@ -592,44 +582,29 @@ def process_screen_recording(mp4):
return {
"filename": mp4.name,
"gif": f"/screen_recordings/{stem.with_suffix('.gif').name}",
"png": f"/screen_recordings/{png_path.name}",
"timestamp": datetime.fromtimestamp(mp4.stat().st_mtime).isoformat(),
"is_custom_name": is_custom_name
}
def process_screen_recording_gif(mp4):
stem = mp4.with_suffix("")
gif_path = stem.with_suffix(".gif")
if not gif_path.exists():
video_to_gif(mp4, gif_path)
def run_ffmpeg(args):
process = subprocess.Popen(["ffmpeg", "-hide_banner", "-loglevel", "error"] + args, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
return stdout
def segment_to_segment_name(data_dir, segment):
full_path = os.path.join(data_dir, f"FakeDongleID1337|{segment}")
return SegmentName(full_path)
def video_to_gif(input_path, output_path):
output_path = Path(output_path)
sped_up_path = output_path.with_suffix(f".{uuid.uuid4()}.spedup.mp4")
run_ffmpeg(["-i", str(input_path), "-an", "-vf", "setpts=PTS/35", str(sped_up_path)])
run_ffmpeg(["-i", str(sped_up_path), "-loop", "0", str(output_path)])
if os.path.exists(sped_up_path):
os.remove(sped_up_path)
def video_to_png(input_path, output_path):
run_ffmpeg([
"-ss", str(get_video_duration(input_path) / 2),
"-i", str(input_path),
"-frames:v", "1",
str(output_path)
])
try:
subprocess.run([
"ffmpeg", "-hide_banner", "-loglevel", "error",
"-ss", "1",
"-i", str(input_path),
"-frames:v", "1",
"-y",
str(output_path)
], capture_output=True, check=True, text=True)
except subprocess.CalledProcessError as e:
print(f"Failed to generate PNG for {input_path}")
if e.stderr:
print(e.stderr)
def xor_encrypt_decrypt(data, key):
return "".join(chr(ord(c) ^ ord(key[i % len(key)])) for i, c in enumerate(data))
+6 -2
View File
@@ -13,11 +13,15 @@ class ModelConstants:
META_T_IDXS = [2., 4., 6., 8., 10.]
# model inputs constants
MODEL_FREQ = 20
HISTORY_FREQ = 5
HISTORY_LEN_SECONDS = 5
TEMPORAL_SKIP = MODEL_FREQ // HISTORY_FREQ
FULL_HISTORY_BUFFER_LEN = MODEL_FREQ * HISTORY_LEN_SECONDS
INPUT_HISTORY_BUFFER_LEN = HISTORY_FREQ * HISTORY_LEN_SECONDS
N_FRAMES = 2
MODEL_RUN_FREQ = 20
MODEL_CONTEXT_FREQ = 5 # "model_trained_fps"
FULL_HISTORY_BUFFER_LEN = MODEL_RUN_FREQ * MODEL_CONTEXT_FREQ
TEMPORAL_SKIP = MODEL_RUN_FREQ // MODEL_CONTEXT_FREQ
FEATURE_LEN = 512
+37 -6
View File
@@ -3,11 +3,25 @@ import capnp
import numpy as np
from cereal import log
from openpilot.frogpilot.tinygrad_modeld.constants import ModelConstants, Plan, Meta
from openpilot.selfdrive.controls.lib.drive_helpers import get_curvature_from_plan
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
ConfidenceClass = log.ModelDataV2.ConfidenceClass
# Return curvature for lateral action. If the model outputs desired_curvature and we're not in mlsim mode,
# use it directly; otherwise derive from the plan using yaw and yaw-rate.
def get_curvature_from_output(output: dict, plan: np.ndarray, v_ego: float, lat_action_t: float, mlsim: bool) -> float:
if not mlsim:
desired = output.get('desired_curvature')
if desired is not None:
return float(desired[0, 0])
# Use yaw (index 2) and yaw_rate (index 2)
theta = plan[:, Plan.T_FROM_CURRENT_EULER][:, 2]
theta_dot = plan[:, Plan.ORIENTATION_RATE][:, 2]
return float(get_curvature_from_plan(theta, theta_dot, ModelConstants.T_IDXS, v_ego, lat_action_t))
class PublishState:
def __init__(self):
@@ -82,15 +96,32 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D
modelV2.timestampEof = timestamp_eof
modelV2.modelExecutionTime = model_execution_time
# normalize plan tensors to (IDX_N, WIDTH)
plan_arr = net_output_data['plan'][0]
plan_stds_arr = net_output_data['plan_stds'][0]
# plan
fill_xyzt(modelV2.position, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.POSITION].T, *net_output_data['plan_stds'][0,:,Plan.POSITION].T)
fill_xyzt(modelV2.velocity, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.VELOCITY].T)
fill_xyzt(modelV2.acceleration, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ACCELERATION].T)
fill_xyzt(modelV2.orientation, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.T_FROM_CURRENT_EULER].T)
fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T)
fill_xyzt(modelV2.position, ModelConstants.T_IDXS, *plan_arr[:,Plan.POSITION].T, *plan_stds_arr[:,Plan.POSITION].T)
fill_xyzt(modelV2.velocity, ModelConstants.T_IDXS, *plan_arr[:,Plan.VELOCITY].T)
fill_xyzt(modelV2.acceleration, ModelConstants.T_IDXS, *plan_arr[:,Plan.ACCELERATION].T)
fill_xyzt(modelV2.orientation, ModelConstants.T_IDXS, *plan_arr[:,Plan.T_FROM_CURRENT_EULER].T)
fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *plan_arr[:,Plan.ORIENTATION_RATE].T)
# temporal pose
temporal_pose = modelV2.temporalPose
if 'sim_pose' in net_output_data:
temporal_pose.trans = net_output_data['sim_pose'][0,:ModelConstants.POSE_WIDTH//2].tolist()
temporal_pose.transStd = net_output_data['sim_pose_stds'][0,:ModelConstants.POSE_WIDTH//2].tolist()
temporal_pose.rot = net_output_data['sim_pose'][0,ModelConstants.POSE_WIDTH//2:].tolist()
temporal_pose.rotStd = net_output_data['sim_pose_stds'][0,ModelConstants.POSE_WIDTH//2:].tolist()
else:
temporal_pose.trans = plan_arr[0,Plan.VELOCITY].tolist()
temporal_pose.transStd = plan_stds_arr[0,Plan.VELOCITY].tolist()
temporal_pose.rot = plan_arr[0,Plan.ORIENTATION_RATE].tolist()
temporal_pose.rotStd = plan_stds_arr[0,Plan.ORIENTATION_RATE].tolist()
# poly path
fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T)
fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, *plan_arr[:,Plan.POSITION].T)
# action
modelV2.action = action

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