Compare commits

..

130 Commits

Author SHA1 Message Date
firestar5683 0495453e3a Malibu 2026-02-10 20:53:35 -06:00
firestar5683 5340cc449c Blazer Pedal 2026-02-09 16:20:54 -06:00
firestar5683 89f6f7a42b User adjustable offsets 2026-02-07 23:32:46 -06:00
firestar5683 073d9ba098 Integrator Smooth On Handoff 2026-02-06 15:48:06 -06:00
firestar5683 3ad700a964 Malibu ASCM Fingerprint 2026-02-06 12:29:33 -06:00
firestar5683 c69a5080f4 Revert "merry christmas"
This reverts commit fa9234212b.
2026-02-05 22:49:40 -06:00
firestar5683 6ab4400614 Lights 2026-02-05 22:40:20 -06:00
firestar5683 8222e303a2 New Models 2026-02-05 15:06:24 -06:00
firestar5683 43da13c8b0 Limp? 2026-02-05 13:57:49 -06:00
firestar5683 1b1d60d088 Increase Fault Resilience 2026-02-05 13:35:38 -06:00
firestarsdog 9421030e2c Stats
Stats
2026-02-02 01:03:32 -05:00
firestar5683 930fa680cf Update carcontroller.py 2026-02-01 22:01:53 -06:00
firestar5683 f4fb138009 Update gmcan.py 2026-01-30 00:17:30 -06:00
firestar5683 35cee6a7f9 malibu pedal tuning 2026-01-29 00:43:38 -06:00
firestar5683 66fbf8b21f phase sync 2026-01-27 22:26:51 -06:00
firestar5683 f3306bec23 update 2026-01-27 00:12:01 -06:00
firestar5683 5338f9a5d5 malibu buttons 2026-01-26 00:08:27 -06:00
firestar5683 6a702911ab Reapply "More buttons?"
This reverts commit 4da1ddc500.
2026-01-25 23:53:44 -06:00
firestar5683 4da1ddc500 Revert "More buttons?"
This reverts commit cb0f964e60.
2026-01-22 12:02:27 -06:00
firestar5683 cb0f964e60 More buttons? 2026-01-22 01:19:59 -06:00
firestar5683 a868fc6650 Buttons 2026-01-22 00:29:40 -06:00
firestar5683 2f44ed860d Malibu Buttons 2026-01-20 23:16:35 -06:00
firestar5683 9bb133a188 Revert "Mac Update"
This reverts commit d56a8f6c23.
2026-01-19 11:41:36 -06:00
firestarsdog 251b755efd Add SASCM to vehicle settings detection/stats 2026-01-19 10:33:15 -06:00
firestar5683 d56a8f6c23 Mac Update 2026-01-18 22:22:24 -06:00
firestarsdog 894d792dbb Stats 2026-01-18 22:17:16 -06:00
firestar5683 91cb407979 Update carcontroller.py 2026-01-16 16:31:30 -06:00
firestar5683 2361ad82c9 update redneck 2026-01-15 22:49:34 -06:00
firestar5683 cea54bf498 Malibu GuessTune 2026-01-15 22:40:08 -06:00
firestar5683 bc012595ca More Malibu 2026-01-14 23:35:45 -06:00
firestar5683 c1df1eaf2a fix redneck v2 2026-01-14 13:59:17 -06:00
firestar5683 323e269a6b Remove lat smooth seconds 2026-01-14 13:50:00 -06:00
firestar5683 4c9430caf1 Malibu Phase Sync 2026-01-12 23:31:50 -06:00
firestar5683 0b193e90f0 frogpilot migration 2026-01-12 22:11:24 -06:00
firestar5683 c3d0c9c7c3 More defaults 2026-01-12 22:04:57 -06:00
firestar5683 89d871ea40 Update defaults 2026-01-12 22:00:11 -06:00
firestar5683 77956f33c2 Big Mac 2026-01-11 23:19:24 -06:00
firestar5683 3e47e95934 Malibu Checksum 2026-01-11 23:19:24 -06:00
firestar5683 3086285c72 Revert "Malibu Buttons?"
This reverts commit 22789bc95f.
2026-01-10 14:23:59 -06:00
firestar5683 5fc40a8936 Try Higher Friction 2026-01-10 14:04:39 -06:00
firestar5683 19565e7aca Malibu Pedal Tuning 2026-01-10 00:13:58 -06:00
firestar5683 22789bc95f Malibu Buttons? 2026-01-09 23:33:23 -06:00
firestar5683 22a949893d Autotune Off 2026-01-09 22:41:24 -06:00
firestar5683 7e749a73a4 pedal long flag 2026-01-09 18:05:44 -06:00
firestar5683 36c2cb5fb0 Update interface.py 2026-01-09 17:00:03 -06:00
firestar5683 fa9234212b merry christmas 2025-12-24 22:23:43 -06:00
firestar5683 fd7b50a1a6 ds2 2025-12-24 21:29:13 -06:00
firestar5683 64087ac7ea No sub? 2025-12-18 12:29:24 -06:00
firestar5683 961fc23845 Use new torque Controller 2025-12-16 22:12:50 -06:00
firestar5683 6d98e4a784 Fix Volt 2019? 2025-12-16 17:18:02 -06:00
firestar5683 cfd8c78c4c Update interface.py 2025-12-16 17:12:01 -06:00
firestar5683 1542e69a20 Try friction adjustment 2025-12-16 17:12:01 -06:00
firestar5683 ffdea13de8 Update frogpilot_tracking.py 2025-12-16 17:12:00 -06:00
firestar5683 60b000f7b5 minsteer speed 2025-12-16 17:12:00 -06:00
firestar5683 1a27190a67 Update Percentages 2025-12-16 17:12:00 -06:00
firestar5683 69703fd2ac Torque Rest of Fleet 2025-12-16 17:12:00 -06:00
firestar5683 365bf72ca2 Update ui 2025-12-12 12:01:29 -06:00
firestar5683 7af2d11cab Update frogpilot_variables.py 2025-12-12 11:38:21 -06:00
firestar5683 319b8c03f6 Trailer Load Gas Tuning 2025-12-12 10:50:45 -06:00
firestar5683 42cf37d175 With Clamping 2025-12-12 02:09:20 -06:00
firestar5683 e767f8a3d2 Volt TorqueTune 2025-12-12 02:04:01 -06:00
firestar5683 c159747e76 Live Friction 2025-12-11 16:55:36 -06:00
firestar5683 5e97a89432 Zero error 2025-12-11 15:13:01 -06:00
firestar5683 d056ca7a3f Updates 2025-12-09 19:59:54 -06:00
firestar5683 a3df9b39f4 Patch 2025-12-08 11:37:40 -06:00
firestar5683 8818b7a1e2 UI 2025-12-05 20:13:07 -06:00
firestar5683 7baa741d4f LattyBoi2.0 2025-12-03 23:31:13 -06:00
firestar5683 becda14f28 Latty Boi
Revert "Latty Boi"

This reverts commit af687e501cc4bdcda7840453d06595d7ea674148.

Reapply "Latty Boi"

This reverts commit ff5566d4439f5e4997fe83f4f21d0be62b75d75d.
2025-12-02 20:30:36 -06:00
firestar5683 e9081aa1b7 Recovery Power 2025-12-02 17:19:30 -06:00
firestar5683 f86622bc96 New planplus 2025-12-02 12:09:16 -06:00
firestar5683 a74fc22d71 stopngo 2025-12-01 16:06:31 -06:00
firestar5683 ce0323e4c0 Update interface.py 2025-11-28 19:21:04 -06:00
Woohyun Rho ec63eca6ab Update 2025-11-28 17:11:04 -06:00
firestar5683 d79d4e9efd BlazeIt 2025-11-28 17:06:33 -06:00
firestar5683 00a594f106 Create CHEVROLET_MALIBU_CC.json 2025-11-21 18:30:58 -06:00
firestar5683 e7ec4df222 New Lateral Changes 2025-11-18 20:49:13 -06:00
firestar5683 abac2983b5 Update 2025-11-15 14:45:36 -06:00
firestar5683 50514c1e0e Revert "Upstream Lateral"
This reverts commit d4b1f9612b.
2025-11-10 23:07:10 -06:00
firestar5683 d4b1f9612b 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:57 -06:00
firestarsdog 6492e43ba3 chevrolet_volt_camera manual selection only 2025-11-05 14:57:19 -05:00
firestar5683 9d96d2678e Volt Camera? 2025-11-05 11:44:54 -06:00
firestar5683 1d7934d112 nnff 2025-10-28 20:58:44 -05:00
firestar5683 9fe567174d NNFF on 2025-10-27 15:10:10 -05:00
firestar5683 daf2e2e139 Update 2025-10-25 15:45:41 -05:00
firestar5683 028eb11d88 Medium Fanta 2025-10-22 22:26:45 -05:00
firestar5683 54d02f9990 Update interfaces.py 2025-10-20 13:50:05 -05:00
firestar5683 6cad16a714 no nnff 2025-10-19 17:59:54 -05:00
firestar5683 be6b3ea4ff Scene Complexity 2025-10-18 14:25:15 -05:00
firestar5683 47516dc865 Update carcontroller.py 2025-10-17 19:10:24 -05:00
firestar5683 84da3740ee Update interface.py 2025-10-17 18:12:43 -05:00
firestar5683 c41dc651c2 CEM 2025-10-17 17:40:03 -05:00
firestar5683 454c11b083 lite 2025-10-17 17:14:13 -05:00
firestar5683 aa51eee24e Redneck 2.0 2025-10-17 16:58:39 -05:00
firestar5683 89d49e4d8d Modify torque tuning parameters in interfaces.py
Adjusted torque tuning parameters for improved performance.
2025-10-17 16:55:36 -05:00
firestar5683 f266e7de9d Hurts Donut 2025-10-16 23:57:47 -05:00
firestar5683 dbef810865 Smoothy Boi 2025-10-16 23:39:45 -05:00
firestar5683 fd5033b16d lat3 2025-10-15 22:21:03 -05:00
firestar5683 e5d59e6c45 oopsie doopsie 2025-10-12 00:12:12 -05:00
firestar5683 d7231a1a74 Revert "Humanlanechanges fix"
This reverts commit e79f98816d.
2025-10-12 00:11:24 -05:00
firestar5683 0bb8f383bf Revert "Duh"
This reverts commit f558bc6b61.
2025-10-12 00:11:21 -05:00
firestarsdog f558bc6b61 Duh 2025-10-11 20:58:51 -04:00
niknak6 e79f98816d Humanlanechanges fix 2025-10-11 19:45:18 -04:00
firestar5683 52f6ec0001 New Lateral Changes 2025-10-10 20:59:53 -05:00
firestar5683 f67af0f222 Fix Standard 2025-10-10 19:25:02 -05:00
firestar5683 76d2b1eb56 Update interface.py 2025-10-08 23:28:31 -05:00
firestar5683 8d9f88009d error? 2025-10-08 23:14:33 -05:00
firestar5683 ab036f7451 Fix New Devices 2025-10-08 22:20:38 -05:00
firestar5683 2792c69652 No positive P-response for long control if user-selected parameter set 2025-10-08 07:43:54 -05:00
firestar5683 d6ca567d5d Update frogpilot_acceleration.py 2025-10-04 23:55:37 -05:00
firestar5683 01be6c4d81 Automatic updates 2025-10-04 16:03:20 -05:00
firestar5683 f23b77180e Steer Alerts 2025-10-04 01:48:51 -05:00
firestar5683 4df8dda8ba Update frogpilot_acceleration.py 2025-10-03 22:41:48 -05:00
firestar5683 23bc2b80d8 Revert "Update latcontrol_torque.py"
This reverts commit 13c3070fa1.
2025-10-03 22:05:10 -05:00
firestar5683 5b823fee98 SteerAlerts
Revert "SteerAlerts"

This reverts commit cbaba399c5b9caac5a8faf3917f0929d747a0acc.

Update controlsd.py
2025-10-03 22:05:01 -05:00
firestar5683 a67d46b953 Donut DM 2025-10-03 20:47:20 -05:00
firestar5683 13c3070fa1 Update latcontrol_torque.py 2025-10-03 20:11:45 -05:00
firestar5683 72dc8dd753 sp 2025-10-03 18:40:23 -05:00
firestar5683 b499c34970 Update gm_global_a_powertrain_generated.dbc 2025-10-03 16:15:40 -05:00
firestar5683 0325685719 Update 2025-10-03 00:43:14 -05:00
firestar5683 0d4ec3f1d8 Panda 2025-10-03 00:22:24 -05:00
firestar5683 8f010a4c3f Kao Panda 2025-10-03 00:16:05 -05:00
firestar5683 2cafeee8c1 Cruise Fault?
Revert "Cruise Fault?"

This reverts commit 4a160adff3de7428808476056ba1489484dd09cd.
2025-10-02 23:45:11 -05:00
firestar5683 8bd757ca63 Cleanup: Nuke of the Bad Stuff Cont 2025-10-01 18:51:05 -04:00
firestar5683 8cd5ee4f80 Cleanup: Nuke of the Bad Stuff 2025-10-01 18:48:05 -04:00
firestar5683 40bcc00261 Revert "Camera Volts"
This reverts commit 9e52017553.
2025-10-01 14:02:11 -04:00
firestar5683 785eee7fdb firehose 2025-10-01 14:00:18 -04:00
firestarsdog 9e52017553 Camera Volts 2025-10-01 09:54:15 -04:00
firestar5683 8907ac78c8 Update 2025-09-30 14:31:12 -05:00
firestar5683 ba5eea4253 Kaofui 2025-09-30 10:13:43 -05:00
firestar5683 7421c67a17 Dom 2025-09-30 09:15:54 -05:00
161 changed files with 1391 additions and 19246 deletions
+1 -12
View File
@@ -164,9 +164,6 @@ 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},
@@ -238,7 +235,6 @@ 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},
@@ -251,7 +247,6 @@ std::unordered_map<std::string, uint32_t> keys = {
{"BlacklistedModels", PERSISTENT},
{"BlindSpotMetrics", PERSISTENT},
{"BlindSpotPath", PERSISTENT},
{"BootLogo", PERSISTENT},
{"BorderMetrics", PERSISTENT},
{"CalibratedLateralAcceleration", PERSISTENT},
{"CalibrationProgress", PERSISTENT},
@@ -277,7 +272,6 @@ 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},
@@ -316,7 +310,6 @@ std::unordered_map<std::string, uint32_t> keys = {
{"DisengageVolume", PERSISTENT},
{"DoToggleReset", PERSISTENT},
{"DoToggleResetStock", PERSISTENT},
{"DownloadableBootLogos", PERSISTENT},
{"DownloadableColors", PERSISTENT},
{"DownloadableDistanceIcons", PERSISTENT},
{"DownloadableIcons", PERSISTENT},
@@ -328,8 +321,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},
@@ -357,7 +350,6 @@ std::unordered_map<std::string, uint32_t> keys = {
{"GasRegenCmd", PERSISTENT},
{"GMPedalLongitudinal", PERSISTENT},
{"GoatScream", PERSISTENT},
{"GoatScreamCriticalAlerts", PERSISTENT},
{"GreenLightAlert", PERSISTENT},
{"HideAlerts", PERSISTENT},
{"HideLeadMarker", PERSISTENT},
@@ -477,9 +469,6 @@ 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},
Binary file not shown.
+11 -28
View File
@@ -29,15 +29,13 @@ def check_github_rate_limit():
print(f"Error checking GitHub rate limit: {error}")
return False
def download_file(cancel_param, destination, progress_param, url, download_param, params_memory, allow_unknown_size=False, suppress_errors=False):
def download_file(cancel_param, destination, progress_param, url, download_param, params_memory):
try:
destination.parent.mkdir(parents=True, exist_ok=True)
total_size = get_remote_file_size(url, suppress_errors=suppress_errors or allow_unknown_size)
if total_size == 0 and not allow_unknown_size:
total_size = get_remote_file_size(url)
if total_size == 0:
if not url.endswith(".gif"):
if suppress_errors:
return
handle_error(None, "Download invalid...", "Download invalid...", download_param, progress_param, params_memory)
return
@@ -58,30 +56,24 @@ def download_file(cancel_param, destination, progress_param, url, download_param
temp_file.write(chunk)
downloaded_size += len(chunk)
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:
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...")
temp_file_path.rename(destination)
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, suppress_errors=False):
def get_remote_file_size(url):
try:
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 error:
if not suppress_errors:
handle_request_error(error, None, None, None, None)
handle_request_error(error, None, None, None, None)
return 0
def get_repository_url():
@@ -112,17 +104,8 @@ def handle_request_error(error, destination, download_param, progress_param, par
error_message = error_map.get(type(error), "Unexpected error")
handle_error(destination, f"Failed: {error_message}", error, download_param, progress_param, params_memory)
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
def verify_download(file_path, url):
remote_file_size = get_remote_file_size(url)
if remote_file_size == 0:
print(f"Error fetching remote size for {file_path}")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 503 KiB

After

Width:  |  Height:  |  Size: 875 KiB

+50 -140
View File
@@ -37,7 +37,6 @@ HOLIDAY_SLUGS = {
}
THEME_COMPONENT_PARAMS = {
"boot_logos": "BootLogoToDownload",
"colors": "ColorToDownload",
"distance_icons": "DistanceIconToDownload",
"icons": "IconToDownload",
@@ -94,16 +93,8 @@ 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()
if not repo_url:
@@ -111,12 +102,7 @@ class ThemeManager:
self.downloading_theme = False
return
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":
if 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"]
@@ -131,40 +117,37 @@ class ThemeManager:
for extension in extensions:
theme_path = download_path.with_suffix(extension)
theme_urls = [f"{download_link}/{candidate}{extension}" for candidate in name_candidates]
if theme_component != "boot_logos":
theme_urls = [download_link + extension]
theme_url = download_link + extension
for theme_url in theme_urls:
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, params_memory)
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)
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)
self.downloading_theme = False
return
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 verify_download(theme_path, theme_url):
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
return
if extension == ".zip":
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
extract_zip(theme_path, download_path)
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)
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
params_memory.remove(asset_param)
if extension == ".zip":
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
extract_zip(theme_path, download_path)
self.downloading_theme = False
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
params_memory.remove(asset_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
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
@@ -175,7 +158,7 @@ class ThemeManager:
repo_encoded = quote_plus(RESOURCES_REPO)
assets = {"boot_logos": [], "themes": {}, "wheels": []}
assets = {"themes": {}, "wheels": []}
try:
def list_files(branch):
if is_github:
@@ -251,20 +234,6 @@ 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)
@@ -277,7 +246,6 @@ 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
@@ -297,7 +265,7 @@ class ThemeManager:
parts = base.replace("_", "-").split("-")
capitalized_parts = [part.capitalize() for part in parts if part]
if len(capitalized_parts) > 1 and component not in {"boot_logos", "steering_wheels"}:
if len(capitalized_parts) > 1 and component != "steering_wheels":
display = f"{capitalized_parts[0]} ({' '.join(capitalized_parts[1:])})"
else:
display = " ".join(capitalized_parts)
@@ -353,44 +321,37 @@ class ThemeManager:
}
def handle_verification_failure(self, extension, theme_component, theme_name, asset_param, theme_path, download_path, frogpilot_toggles):
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":
if 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]
for candidate in name_candidates:
delete_file(theme_path)
delete_file(theme_path)
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)
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, params_memory)
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 verify_download(theme_path, theme_url):
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
# Let the caller continue trying alternate extensions before surfacing failure.
handle_error(None, "Download failed...", "Download failed...", asset_param, DOWNLOAD_PROGRESS_PARAM, params_memory)
self.downloading_theme = False
return False
@staticmethod
@@ -454,8 +415,6 @@ 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:
@@ -463,7 +422,6 @@ 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),
@@ -476,7 +434,6 @@ 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),
@@ -487,7 +444,6 @@ 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),
@@ -502,9 +458,7 @@ 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 == "boot_logo":
self.update_boot_logo(current_value)
elif asset_type == "wheel_image":
if 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)
@@ -545,16 +499,9 @@ 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_boot_logos, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels):
def update_theme_params(self, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels):
def update_param(key, assets, subfolder):
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":
if 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:
@@ -564,7 +511,6 @@ 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")
@@ -583,20 +529,12 @@ 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)
}))
@@ -604,9 +542,7 @@ class ThemeManager:
print("ThemesDownloaded updated successfully")
def update_theme_size(self, theme_component, theme_name, file_size):
if theme_component == "boot_logos":
key = "boot_logos"
elif theme_component == "steering_wheels":
if theme_component == "steering_wheels":
key = "wheels"
else:
key = "themes"
@@ -614,7 +550,7 @@ class ThemeManager:
if key not in self.theme_sizes:
self.theme_sizes[key] = {}
if key in {"boot_logos", "wheels"}:
if key == "wheels":
self.theme_sizes[key][theme_name] = file_size
else:
if theme_name not in self.theme_sizes[key]:
@@ -636,7 +572,6 @@ class ThemeManager:
if not assets:
return
downloadable_boot_logos = []
downloadable_colors = []
downloadable_distance_icons = []
downloadable_icons = []
@@ -658,10 +593,8 @@ 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}")
@@ -672,21 +605,7 @@ 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_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}")
self.update_theme_params(downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels)
def update_wheel_image(self, image, boot_run=False, random_event=False):
wheel_save_location = ACTIVE_THEME_PATH / "steering_wheel"
@@ -722,15 +641,6 @@ 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")
+2 -13
View File
@@ -2,7 +2,6 @@
import json
import math
import numpy as np
import os
import requests
import shutil
import subprocess
@@ -23,7 +22,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, FW_PATH
from panda import Panda
from openpilot.frogpilot.common.frogpilot_variables import EARTH_RADIUS, KONIK_PATH, MAPD_PATH, MAPS_PATH, params, params_cache, params_memory
@@ -152,21 +151,11 @@ 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(fn=flash_fn)
panda.flash()
panda.close()
except Exception as exception:
print(f"Error flashing Panda {serial}: {exception}")
+15 -54
View File
@@ -62,7 +62,6 @@ 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")
@@ -76,8 +75,8 @@ 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 = "sc"
DEFAULT_MODEL_NAME = "South Carolina (Default) 👀📡"
DEFAULT_MODEL = "bd2"
DEFAULT_MODEL_NAME = "Firehose (Default) 👀📡"
DEFAULT_MODEL_VERSION = "v11"
BUTTON_FUNCTIONS = {
@@ -134,7 +133,6 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("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"),
@@ -148,7 +146,6 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("AlwaysOnLateral", "1", 0, "0"),
("AlwaysOnLateralLKAS", "1", 0, "0"),
("AlwaysOnLateralMain", "1", 0, "0"),
("AlwaysAllowUploads", "0", 2, "0"),
("AMapKey1", "", 0, ""),
("AMapKey2", "", 0, ""),
("AutomaticallyDownloadModels", "1", 1, "0"),
@@ -162,7 +159,6 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("BlacklistedModels", "", 2, ""),
("BlindSpotMetrics", "1", 3, "0"),
("BlindSpotPath", "1", 1, "0"),
("BootLogo", "starpilot", 0, "stock"),
("BorderMetrics", "1", 3, "0"),
("CalibratedLateralAcceleration", str(DEFAULT_LATERAL_ACCELERATION), 2, str(DEFAULT_LATERAL_ACCELERATION)),
("CalibrationProgress", "0", 3, "0"),
@@ -174,7 +170,7 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("CECurves", "0", 1, "0"),
("CECurvesLead", "0", 1, "0"),
("CELead", "0", 1, "0"),
("CEModelStopTime", "7", 2, "0"),
("CEModelStopTime", str(PLANNER_TIME - 2), 2, "0"),
("CENavigation", "0", 2, "0"),
("CENavigationIntersections", "1", 2, "0"),
("CENavigationLead", "1", 2, "0"),
@@ -223,6 +219,7 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("DynamicPathWidth", "0", 2, "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"),
@@ -240,13 +237,9 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("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"),
@@ -307,7 +300,7 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("NavSettingLeftSide", "0", 0, "0"),
("NavSettingTime24h", "0", 0, "0"),
("NewLongAPI", "0", 2, "1"),
("NNFF", "0", 2, "0"),
("NNFF", "1", 2, "0"),
("NNFFLite", "0", 2, "0"),
("NoLogging", "0", 2, "0"),
("NoUploads", "0", 2, "0"),
@@ -459,7 +452,7 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("WarningSoftVolume", "101", 2, "101"),
("WheelIcon", "frog", 0, "stock"),
("WheelSpeed", "0", 2, "0"),
("StopDistance", "6.0", 3, "6.0"),
("StopDistance", "6", 3, "6"),
("RecoveryPower", "1.0", 2, "1.0")
]
@@ -537,18 +530,6 @@ 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
@@ -590,7 +571,6 @@ class FrogPilotVariables:
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)
@@ -640,34 +620,19 @@ class FrogPilotVariables:
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.5, latAccelFactor * 1.25) if advanced_lateral_tuning and tuning_level >= level["SteerLatAccel"] else latAccelFactor
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.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.25, steerRatio * 1.5) if advanced_lateral_tuning and tuning_level >= level["SteerRatio"] else steerRatio
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.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 = toggle.car_make == "gm" and not (toggle.car_model.startswith("CHEVROLET_VOLT") and not toggle.car_model.endswith("_CC")) and CP.carFingerprint in GM_EV_CAR 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.ev_tuning = params.get_bool("EVTuning") if advanced_longitudinal_tuning and tuning_level >= level["EVTuning"] else ev_vehicle
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
@@ -698,7 +663,7 @@ class FrogPilotVariables:
toggle.car_model = params.get("CarModel", encoding="utf-8") or toggle.car_model
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.cluster_offset = params.get_float("ClusterOffset") if toggle.car_make == "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"))
@@ -722,7 +687,6 @@ 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"))
@@ -808,7 +772,6 @@ 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
@@ -840,11 +803,10 @@ 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.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.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.force_fingerprint = (params.get_bool("ForceFingerprint") if tuning_level >= level["ForceFingerprint"] else default.get_bool("ForceFingerprint")) and toggle.car_model is not None
@@ -955,7 +917,6 @@ 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"
-224
View File
@@ -1,224 +0,0 @@
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()
@@ -1,6 +1,4 @@
#!/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
@@ -34,10 +32,6 @@ class ConditionalExperimentalMode:
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]"""
@@ -55,12 +49,8 @@ class ConditionalExperimentalMode:
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:
@@ -68,23 +58,28 @@ class ConditionalExperimentalMode:
if self.status_value not in {1, 2} and not sm["carState"].standstill:
self.update_conditions(v_ego, sm, frogpilot_toggles)
new_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
# Add hysteresis to prevent rapid toggling
if new_experimental_mode and not self.prev_experimental_mode:
# Require weaker conditions to turn on
hysteresis_factor = 0.9
elif not new_experimental_mode and self.prev_experimental_mode:
# Require stronger conditions to turn off
hysteresis_factor = 1.2
else:
hysteresis_factor = 1.0
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
# Apply hysteresis to key conditions
if hasattr(self, 'slow_lead_detected'):
self.slow_lead_detected = self.slow_lead_detected if hysteresis_factor == 1.0 else (self.slow_lead_filter.x >= scale_threshold(v_ego) * hysteresis_factor)
if hasattr(self, 'curve_detected'):
self.curve_detected = self.curve_detected if hysteresis_factor == 1.0 else (self.curvature_filter.x >= THRESHOLD * hysteresis_factor)
self.experimental_mode = triggered or hold_active or transition_buffer_active
self.experimental_mode = self.check_conditions(v_ego, sm, frogpilot_toggles)
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
@@ -47,53 +47,26 @@ 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_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_EV = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
A_CRUISE_MAX_VALS_SPORT_EV = [1.25, 1.25, 1.25, 1.25, 1.5, 1.5, 2.0]
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, 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
def get_max_accel_eco(v_ego, ev_tuning=True):
cruise_vals = A_CRUISE_MAX_VALS_ECO_EV if ev_tuning else 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, 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
def get_max_accel_sport(v_ego, ev_tuning=True):
cruise_vals = A_CRUISE_MAX_VALS_SPORT_EV if ev_tuning else 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(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(akima_interp(v_cruise - v_ego, [0., 1., 5., 10.], [0., 0.5, 1.0, max_accel]))
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))
def get_max_allowed_accel(v_ego):
return float(akima_interp(v_ego, [0., 5., 20.], [4.0, 4.0, 2.0])) # ISO 15622:2018
class FrogPilotAcceleration:
@@ -107,27 +80,26 @@ class FrogPilotAcceleration:
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_standard(v_ego, ev_tuning, truck_tuning)
self.max_accel = get_max_accel(v_ego)
elif frogpilot_toggles.map_acceleration and (eco_gear or sport_gear):
if eco_gear:
self.max_accel = get_max_accel_eco(v_ego, ev_tuning, truck_tuning)
self.max_accel = get_max_accel_eco(v_ego, ev_tuning)
else:
if frogpilot_toggles.acceleration_profile == 2:
self.max_accel = get_max_accel_sport(v_ego, ev_tuning, truck_tuning)
self.max_accel = get_max_accel_sport(v_ego, ev_tuning)
else:
self.max_accel = get_max_allowed_accel(v_ego, ev_tuning, truck_tuning)
self.max_accel = get_max_allowed_accel(v_ego)
else:
if frogpilot_toggles.acceleration_profile == 1:
self.max_accel = get_max_accel_eco(v_ego, ev_tuning, truck_tuning)
self.max_accel = get_max_accel_eco(v_ego, ev_tuning)
elif frogpilot_toggles.acceleration_profile == 2:
self.max_accel = get_max_accel_sport(v_ego, ev_tuning, truck_tuning)
self.max_accel = get_max_accel_sport(v_ego, ev_tuning)
elif frogpilot_toggles.acceleration_profile == 3:
self.max_accel = get_max_allowed_accel(v_ego, ev_tuning, truck_tuning)
self.max_accel = get_max_allowed_accel(v_ego)
else:
self.max_accel = get_max_accel_standard(v_ego, ev_tuning, truck_tuning)
self.max_accel = get_max_accel(v_ego)
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)
+4 -18
View File
@@ -7,7 +7,7 @@ from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import COMFO
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]
PERSONALITY_BP = [20. * CV.KPH_TO_MS, 90. * CV.KPH_TO_MS]
class FrogPilotFollowing:
def __init__(self, FrogPilotPlanner):
@@ -73,23 +73,9 @@ class FrogPilotFollowing:
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)
self.disable_throttle = (not self.following_lead and v_ego > 5.0 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
self.disable_throttle = self.frogpilot_planner.tracking_lead and not self.following_lead
self.disable_throttle &= self.frogpilot_planner.lead_one.dRel + 6.0 < (self.t_follow * 2 * 2) * v_ego
self.disable_throttle &= self.frogpilot_planner.lead_one.vLead < v_ego * 0.75
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)
@@ -215,6 +215,7 @@ class LatControlNNFF(LatControl):
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()
pid_log.error = 0.0
if not active:
output_torque = 0.0
pid_log.active = False
@@ -300,7 +301,7 @@ class LatControlNNFF(LatControl):
torque_from_setpoint = self.lat_torque_nn_model.evaluate(nnff_setpoint_input)
torque_from_measurement = self.lat_torque_nn_model.evaluate(nnff_measurement_input)
pid_log.error = torque_from_setpoint - torque_from_measurement
pid_log.error = float(torque_from_setpoint - torque_from_measurement)
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
@@ -316,7 +317,7 @@ class LatControlNNFF(LatControl):
# 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)
pid_log.error += float(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)
-215
View File
@@ -1,215 +0,0 @@
#!/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, #5ec8c8 0%, #8b6cc5 60%, #e05577 85%, #c04466 100%);
background: linear-gradient(to right, green 0%, yellow 80%, orange 90%, red 100%);
border-radius: var(--border-radius-md);
height: var(--padding-base);
overflow: hidden;
@@ -15,6 +15,7 @@
.diskUsage,
.drivingStats,
.firehoseStats,
.softwareInfo {
background-color: var(--secondary-bg);
border-radius: var(--border-radius-md);
@@ -30,6 +31,7 @@
}
.diskUsage p,
.firehoseStats p,
.softwareGrid p {
font-size: var(--font-size-sm);
margin: 0;
@@ -63,6 +65,7 @@
.diskUsage:hover,
.drivingStats:hover,
.firehoseStats:hover,
.softwareInfo:hover {
box-shadow: var(--shadow-md);
transform: var(--hover-scale-sm);
@@ -64,61 +64,56 @@ function renderDiskUsageSection({ diskError, diskUsage }) {
return DiskUsage({ size: "0 GB", used: "0 GB", usedPercentage: "0" });
}
const state = reactive({
data: null,
unit: "miles",
isLoading: true,
error: null,
});
let initialized = false;
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;
}
}
export function Home() {
if (!initialized) {
initialized = true;
initialize();
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;
}
}
initialize();
return html`
<div>
${() => {
if (state.isLoading) {
return html`<p>Loading...</p>`;
}
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.error) {
return html`<p class="error">Failed to load data: ${state.error}</p>`;
}
if (state.data) {
const { driveStats, softwareInfo } = state.data;
return html`
<h1>Galaxy</h1>
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)}
@@ -131,15 +126,23 @@ export function Home() {
${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>`;
}}
return html`<p>No data available.</p>`;
}}
</div>
`;
}
@@ -20,49 +20,47 @@
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
/* Colors — Galaxy palette (cosmic purple · teal · rose · amber) */
--accent-bg: #8b6cc5;
--accent-hover-bg: #7558b0;
--card-bg: #121224;
/* Colors */
--accent-bg: #673ab7;
--accent-hover-bg: #512da8;
--card-bg: #234423;
--color-black: #000000;
--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-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-white: #ffffff;
--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;
--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;
--sidebar-fg: #ffffff;
--sidebar-title-fg: #8b6cc5;
--success-bg: #5ec8c8;
--success-fg: #5ec8c8;
--success-hover-bg: #4ab3b3;
--switch-inactive-bg: #1e1e3e;
--text-color: #e8e8f0;
--text-muted: #8080a8;
--sidebar-title-fg: #193446;
--success-bg: #039226;
--success-fg: #00a100;
--success-hover-bg: #01be2d;
--text-color: #ffffff;
--text-muted: #a0a0a0;
--text-on-primary: var(--sidebar-fg);
--text-on-surface: var(--text-color);
--thumb-color: #8b6cc5;
--track-color: #14142e;
--warning-bg: #d4a060;
--warning-hover-bg: #b8884a;
--thumb-color: #178643;
--track-color: #1a3a1a;
--warning-bg: #ff9800;
--warning-hover-bg: #f57c00;
/* Effects */
--disabled-opacity: 0.5;
@@ -73,7 +71,7 @@
--hover-scale-lg: scale(1.1);
/* Fonts */
--font-body: "Inter", "Open Sans", sans-serif;
--font-body: "Open Sans", sans-serif;
--font-mono: "Courier New", Courier, monospace;
--font-size-xs: 0.75rem;
--font-size-sm: 0.85rem;
@@ -98,9 +96,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;
@@ -174,14 +172,12 @@ 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 {
@@ -203,17 +199,9 @@ 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;
@@ -263,9 +251,7 @@ textarea {
cursor: text;
}
label[for] {
cursor: default;
}
label[for] { cursor: default; }
/* ――― Snackbar component ――― */
.snackbar {
@@ -311,7 +297,6 @@ a {
bottom: 0;
opacity: 0;
}
to {
bottom: var(--snackbar-offset, 30px);
opacity: 1;
@@ -319,20 +304,13 @@ 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) {
@@ -342,7 +320,6 @@ a {
padding-left: 1rem;
padding-right: 1rem;
}
#snackbar_wrapper {
left: 50%;
transform: translateX(-50%);
@@ -356,7 +333,6 @@ a {
bottom: 0;
opacity: 0;
}
to {
bottom: var(--snackbar-offset, 30px);
opacity: 1;
@@ -364,62 +340,18 @@ a {
}
@-webkit-keyframes fadeout {
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;
from { opacity: 1; }
to { opacity: 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);
}
}
@@ -109,7 +109,7 @@
.favorites-toggle-button:hover {
background-color: var(--main-fg);
box-shadow: var(--glow-primary);
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
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: var(--glow-primary);
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
color: var(--text-color);
transform: var(--hover-scale-sm);
}
@@ -466,12 +466,12 @@
}
.search-provider-toggle button.active {
box-shadow: var(--glow-primary);
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
transform: var(--hover-scale-sm);
}
.search-provider-toggle button:hover {
box-shadow: var(--glow-primary);
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
transform: var(--hover-scale-sm);
}
@@ -533,4 +533,4 @@
min-width: 0;
width: calc(100% - var(--padding-xl));
}
}
}
@@ -101,25 +101,6 @@ 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;
@@ -281,18 +262,13 @@ 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 { }
try {
await setupMap();
} catch {
showSnackbar("Failed to load map resources…");
return;
}
} catch {}
setupMap();
loadFavoritesAlphabetically();
}
@@ -548,18 +524,12 @@ export function NavDestination() {
}
}
const setupMap = async (retries = 0) => {
const setupMap = async () => {
if (!state.mapboxPublic || state.initialized) return;
if (typeof mapboxgl === "undefined") {
await loadMapboxGL();
}
const container = document.getElementById("map");
if (!container) {
if (retries >= 50) return;
await new Promise(r => requestAnimationFrame(r));
return setupMap(retries + 1);
requestAnimationFrame(setupMap);
return;
}
state.initialized = true;
mapboxgl.accessToken = state.mapboxPublic;
@@ -615,9 +585,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>
@@ -626,7 +596,7 @@ export function NavDestination() {
</div>
</section>
`
: html`
: html`
<div class="map-wrapper">
<div class="search-wrapper">
<div class="search-controls">
@@ -641,47 +611,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",
@@ -845,9 +815,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: var(--glow-primary);
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
transform: var(--hover-scale-sm);
}
@@ -153,4 +153,4 @@
.navkeys-input {
font-size: var(--font-size-sm);
}
}
}
@@ -1,5 +1,4 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { isGalaxyTunnel } from "/assets/js/utils.js"
import { getOrdinalSuffix } from "/assets/components/navigation/navigation_utilities.js"
import { Modal } from "/assets/components/modal.js";
@@ -125,27 +124,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);
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");
}
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) {
@@ -318,16 +317,6 @@ 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`
@@ -343,36 +332,75 @@ 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>`}
@@ -382,19 +410,23 @@ export function RouteRecordings() {
src="${route.png}"
class="recording-preview recording-preview-png"
style="display:block;"
loading="lazy"
>
<img
data-src="${route.gif}"
class="recording-preview recording-preview-gif"
style="display:none;"
>
</div>
<p class="recording-filename">${route.timestamp}</p>
</div>
`
)}
)}
</div>
`;
}}
}}
${() => {
if (state.routes.length > 0) {
return html`
if (state.routes.length > 0) {
return html`
<button
class="delete-all-button"
@click="${() => (state.showDeleteAllModal = true)}"
@@ -403,17 +435,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,5 +1,4 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { isGalaxyTunnel } from "/assets/js/utils.js"
import { Modal } from "/assets/components/modal.js";
const state = reactive({
@@ -127,6 +126,7 @@ 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,16 +221,6 @@ 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`
@@ -239,37 +229,77 @@ 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;" loading="lazy">
<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;">
</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)}"
@@ -277,24 +307,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,12 +1,9 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { createBrowserHistory, createRouter } from "https://esm.sh/@remix-run/router@1.3.1"
import { hideSidebar } from "/assets/js/utils.js"
import { DeviceSettings } from "/assets/components/tools/device_settings.js"
import { DoorControl } from "/assets/components/tools/doors.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"
@@ -14,14 +11,11 @@ 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 { ModelManager } from "/assets/components/tools/model_manager.js?v=20260303t"
import { LivePlots } from "/assets/components/tools/plots.js"
import { TailscaleControl } from "/assets/components/tailscale/tailscale.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 { UpdateManager } from "/assets/components/tools/update_manager.js"
import { TSKManager } from "/assets/components/tools/tsk_manager.js"
let router, routerState
@@ -29,16 +23,15 @@ function createRoute(id, path, component) {
return {
id,
path,
loader: () => { },
loader: () => {},
element: component,
}
}
function Root() {
let routes = [
createRoute("device_settings", "/device_settings/:section?", DeviceSettings),
createRoute("doors", "/lock_or_unlock_doors", DoorControl),
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),
@@ -46,16 +39,11 @@ function Root() {
createRoute("screen_recordings", "/screen_recordings", ScreenRecordings),
createRoute("settings", "/settings/:section/:subsection?", SettingsView),
createRoute("speed_limits", "/download_speed_limits", SpeedLimits),
createRoute("model_manager", "/manage_models", ModelManager),
createRoute("longitudinal_maneuvers", "/longitudinal_maneuvers", LongitudinalManeuvers),
createRoute("plots", "/plots", LivePlots),
createRoute("tailscale", "/manage_tailscale", TailscaleControl),
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("updates", "/manage_updates", UpdateManager),
createRoute("vehicle_features", "/vehicle_features", VehicleFeatures),
createRoute("tsk_manager", "/tsk_manager", TSKManager),
]
router = createRouter({
@@ -73,13 +61,13 @@ function Root() {
})
router.subscribe(({ initialized, navigation, matches, errors }) => {
const [match] = matches || []
const [match] = matches
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,
})
})
@@ -88,21 +76,17 @@ function Root() {
${() => Sidebar(routerState.activePathFull)}
<div class="content">
${() => {
if (!routerState.initialized) {
return Home({ params: routerState.params })
}
if (!routerState.initialized || routerState.navigation.state === "loading") {
return html`<div>Loading...</div>`
}
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)
if (!match) {
console.warn("[router] no route match for path:", routerState.activePathFull)
return Home({ params: routerState.params })
}
return match.element({ params: routerState.params })
}}
const match = routes.find(r => r.path === routerState.activePath)
return match.element({ params: routerState.params })
}}
</div>
`
}
@@ -125,9 +109,4 @@ export function Navigate(href) {
window.scrollTo(0, 0)
}
if (!window.__thePondRouterMounted) {
window.__thePondRouterMounted = true
Root()(document.getElementById("app"))
} else {
console.warn("[router] duplicate mount prevented")
}
Root()(document.getElementById("app"))
@@ -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,13 +220,14 @@ 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);
}
@@ -299,7 +300,7 @@ input.searchfield {
animation: rotation 1s linear infinite;
background-color: none !important;
border: var(--border-width-thick) solid var(--sidebar-fg);
border-bottom-color: var(--main-fg);
border-bottom-color: #46439b;
box-sizing: border-box;
}
@@ -336,7 +337,6 @@ i.switch {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
@@ -346,8 +346,7 @@ i.switch {
0% {
transform: translateX(26px) rotate(0deg);
}
100% {
transform: translateX(26px) rotate(360deg);
}
}
}
@@ -60,10 +60,7 @@
}
.sidebar {
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);
background-color: var(--sidebar-bg);
border-right: var(--border-width-thin) solid var(--sidebar-border-color);
display: flex;
flex-direction: column;
@@ -105,7 +102,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);
@@ -114,13 +111,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;
@@ -128,23 +125,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: var(--glow-primary);
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
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);
}
@@ -206,13 +203,9 @@
}
.sidebar_header p {
background: linear-gradient(135deg, #8b6cc5 0%, #5ec8c8 55%, #d4789c 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: var(--success-hover-bg);
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
letter-spacing: 0.05em;
}
.sidebar_widget {
@@ -241,7 +234,7 @@
transition: var(--transition-fast);
}
.sidebar .menu_section>li>a {
.sidebar .menu_section > li > a {
font-size: var(--padding-sm);
}
@@ -269,7 +262,7 @@
transition: var(--transition-fast);
}
.sidebar .menu_section>li>a {
.sidebar .menu_section > li > a {
font-size: var(--padding-sm);
}
@@ -284,4 +277,4 @@
#menu_button {
display: none;
}
}
}
@@ -14,37 +14,59 @@ 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: "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: "Lock/Unlock Doors", link: "/lock_or_unlock_doors", icon: "bi-door-closed" },
{ name: "Theme Maker", link: "/theme_maker", icon: "bi-palette-fill" },
{ name: "Tmux Log", link: "/manage_tmux", icon: "bi-terminal" },
{ 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" },
{ name: "Toggles", link: "/manage_toggles", icon: "bi-toggle-on" },
{ name: "Toyota Security Keys", link: "/tsk_manager", icon: "bi-key-fill" },
],
};
const state = reactive({
doorsVisible: false,
isDoorsFetched: false,
isTSKFetched: false,
tskVisible: false,
activeRoute: ""
});
export function Sidebar() {
const currentPath = window.location.pathname;
const matchesPath = (link) => {
if (link === "/") return currentPath === "/";
return currentPath === link || currentPath.startsWith(`${link}/`);
};
const activeItem = Object.values(MenuItems).flat().find(item => matchesPath(item.link));
const activeItem = Object.values(MenuItems).flat().find(item => item.link === currentPath);
state.activeRoute = activeItem?.name ?? "";
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);
}
})();
}
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);
}
})();
}
function navigate(link) {
state.activeRoute = link.name;
@@ -66,9 +88,10 @@ export function Sidebar() {
<div id="sidebar" class="sidebar">
<div>
<div class="title">
<img class="logo" src="/assets/images/main_logo.png" alt="Galaxy logo" />
<img class="logo" src="/assets/images/main_logo.png" alt="FrogPilot logo" />
<div class="title_text sidebar_header">
<p>Galaxy</p>
<p>The Pond</p>
<a href="https://github.com/Aidenir">by&nbsp;Aidenir</a>
</div>
</div>
<hr />
@@ -79,22 +102,30 @@ export function Sidebar() {
<span class="section-title">${upperFirst(section)}</span>
<ul id="${section}">
${links.map(link => {
const isActive = state.activeRoute === link.name;
const classList = [isActive && "active"].filter(Boolean).join(" ");
if (link.name === "Lock/Unlock Doors" && !state.doorsVisible) {
return "";
}
const content = html`
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`
return html`
<li class="${classList}">
${Link(link.link, content, () => navigate(link))}
</li>
`;
})}
})}
</ul>
</li>
</ul>
@@ -52,14 +52,13 @@ export function TailscaleControl() {
checkInstallStatus()
return html`
<div class="toggle-control-widget" style="margin-top: 1.5rem">
<div class="tailscale-wrapper">
<section class="tailscale-widget">
<div class="toggle-control-title">
<div class="tailscale-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!<br><br>
<strong style="color: #ff9494;">Note: Not recommended. Using Galaxy Tunnel is the preferred remote connection method.</strong>
Tailscale creates a secure, private connection between your openpilot device and your phone or PC so you can access and control it from anywhere!
</p>
<div class="tailscale-button-wrapper">
<button
@@ -68,11 +67,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
@@ -80,12 +79,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>
`
}
@@ -1,446 +0,0 @@
/* ――― 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-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;
}
}
@@ -1,756 +0,0 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
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 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)
}
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
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-default-value">Default: ${defaultLabel}</span>
<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,137 +0,0 @@
/* ――― 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);
}
}
@@ -1,166 +0,0 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
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>
`
}
@@ -1,117 +0,0 @@
.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;
}
}
@@ -1,183 +0,0 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
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>
`
}
@@ -1,253 +0,0 @@
.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;
}
}
@@ -1,586 +0,0 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core";
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>
`;
}
@@ -1,280 +0,0 @@
.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);
}
}
@@ -1,666 +0,0 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
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,152 +0,0 @@
.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;
}
}
@@ -1,288 +0,0 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
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,7 +170,6 @@
flex-shrink: 0;
transition: color var(--transition-fast), transform 0.2s;
}
.file-clear-button:hover {
color: var(--danger-hover-bg);
transform: scale(1.1);
@@ -614,7 +613,7 @@ label.file-upload-button {
.turn-signal-input:focus,
.turn-signal-input:hover {
border-color: var(--thumb-color);
box-shadow: var(--glow-primary);
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
outline: none;
transform: var(--hover-scale-sm);
}
@@ -691,15 +690,8 @@ 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) {
@@ -721,8 +713,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);
}
}
}
@@ -97,8 +97,7 @@ const state = reactive({
showDeleteConfirmModal: false,
themeToDelete: null,
themes: [],
activeTab: "colors",
fetched: false,
activeTab: "colors"
});
let draggedIndex = -1;
@@ -406,19 +405,17 @@ const fetchDownloadables = async () => {
};
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 {}
})();
}
(async () => {
try {
const response = await fetch("/api/params?key=DiscordUsername");
state.discordUsername = await response.text();
await loadDefaultTheme();
await fetchDownloadables();
} catch {}
})();
export function ThemeMaker() {
const normalize = (str) => (str || "")
.toString()
.trim()
@@ -1,5 +1,5 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { formatSecondsToHuman, isGalaxyTunnel } from "/assets/js/utils.js"
import { formatSecondsToHuman } from "/assets/js/utils.js"
import { Modal } from "/assets/components/modal.js";
const logSelectorState = reactive({
@@ -12,16 +12,6 @@ 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;
@@ -132,34 +122,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;">
@@ -173,14 +163,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>
`
@@ -192,143 +182,23 @@ export function TmuxLog() {
latest: '',
log: '',
selectorAction: null,
transport: "connecting",
});
const lifecycleVersion = ++tmuxLifecycleVersion;
let lifecycleTimer = null;
function applyIncomingLog(data) {
state.latest = data || "";
const event_source = new EventSource("/api/tmux_log/live");
event_source.onmessage = e => {
state.latest = e.data;
if (!state.paused) {
state.log = state.latest;
}
}
function stopLiveTransport() {
if (liveEventSource) {
liveEventSource.close();
liveEventSource = null;
}
if (livePollTimer) {
clearInterval(livePollTimer);
livePollTimer = null;
}
event_source.onerror = err => {
console.error("Error receiving tmux log:", err);
event_source.close();
}
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() {
function togglePause () {
state.paused = !state.paused;
if (!state.paused) {
state.log = state.latest;
@@ -378,7 +248,7 @@ export function TmuxLog() {
<div class="tmux-block">
<div class="tmux-wrapper">
<div class="tmuxContainer">
<div class="tmuxHeader">${() => getStatusLabel()}</div>
<div class="tmuxHeader">Tmux Live Log</div>
<pre class="tmuxLog">${() => state.log}</pre>
</div>
</div>
@@ -393,20 +263,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,8 +1,7 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
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,
@@ -15,7 +14,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()
@@ -27,7 +26,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()
@@ -46,11 +45,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));
@@ -59,11 +58,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));
@@ -72,7 +71,7 @@ export function ToggleControl() {
await fetch("/api/toggles/reset_stock", { method: "POST" });
}
function triggerRestorePrompt() {
function triggerRestorePrompt () {
fileInput.click()
}
@@ -88,9 +87,9 @@ export function ToggleControl() {
</section>
<section class="toggle-control-widget" style="margin-left: 1.5rem">
<div class="toggle-control-title">Reset Toggles to Default StarPilot</div>
<div class="toggle-control-title">Reset Toggles to Default FrogPilot/Stock openpilot</div>
<p class="toggle-control-text">
Reset all toggles to default StarPilot settings.
Reset all toggles to default FrogPilot/stock openpilot settings.
</p>
<button class="toggle-control-button" @click="${confirmResetDefault}">
Reset Toggles to Default
@@ -99,22 +98,20 @@ 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"
}) : ""}
`
}
@@ -1,124 +0,0 @@
.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;
}
}
@@ -1,208 +0,0 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
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: var(--glow-primary);
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
transform: var(--hover-scale-sm);
}
@@ -211,4 +211,4 @@
.tskkeys-input {
font-size: var(--font-size-sm);
}
}
}
@@ -165,16 +165,15 @@ const api = {
}
}
let hasLoaded = false
export function TSKManager() {
if (!hasLoaded) {
hasLoaded = true
api.load()
}
return html`
<div class="tskkeys-wrapper tskkeys-offset-top">
let hasLoaded = false
if (!hasLoaded) {
hasLoaded = true
api.load()
}
export function TSKManager() {
return html`
<div class="tskkeys-wrapper tskkeys-offset-top">
<div class="tskkeys-container">
<div class="tskkeys-title">Toyota Security Key Manager</div>
@@ -1,206 +0,0 @@
.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));
}
}
@@ -1,617 +0,0 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
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>
`
}
@@ -1,162 +0,0 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
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: 21 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 B

After

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

After

Width:  |  Height:  |  Size: 1008 KiB

+15 -39
View File
@@ -1,50 +1,26 @@
function showSnackbar(msg, level, timeout = 3500, options = {}) {
function showSnackbar(msg, level, timeout = 3500) {
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) {
dismissSnackbar(wrapper.children[0])
const first = wrapper.children[0]
first.style.opacity = 0
setTimeout(() => first.remove(), 1000)
}
const snackbar = document.createElement("div")
snackbar.innerHTML = msg
snackbar.className = "snackbar show"
snackbar.id = `snackbar_${Math.random().toString(36).slice(5)}`
if (key) snackbar.dataset.snackbarKey = key
setSnackbarContent(snackbar)
if (level === "error") {
snackbar.style.backgroundColor = "#f44336"
}
wrapper.appendChild(snackbar)
snackbar.__hideTimer = setTimeout(() => dismissSnackbar(snackbar), timeout)
setTimeout(() => {
snackbar.style.opacity = 0
setTimeout(() => snackbar.remove(), 1000)
}, timeout)
}
@@ -65,11 +65,3 @@ 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": "Galaxy",
"name": "The Pond",
"short_name": "",
"icons": [
{
@@ -17,4 +17,4 @@
"background_color": "#151414",
"theme_color": "#151414",
"display": "standalone"
}
}
+47 -54
View File
@@ -1,66 +1,59 @@
<!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">
<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">
<meta name="theme-color" content="#4285f4" />
<meta name="theme-color" content="#8b6cc5" />
<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">
<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 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="/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 type="module" src="/assets/components/router.js"></script>
<script src="/assets/js/snackbar.js"></script>
<script type="module" src="/assets/components/router.js"></script>
<script src="/assets/js/snackbar.js"></script>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.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=Inter:wght@300..800&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=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap"
rel="stylesheet">
<title>Galaxy</title>
</head>
<title>The Pond</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
+38 -13
View File
@@ -8,6 +8,7 @@ import secrets
import shutil
import subprocess
import time
import uuid
from datetime import datetime
from pathlib import Path
@@ -562,11 +563,20 @@ 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")
@@ -582,29 +592,44 @@ 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):
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)
run_ffmpeg([
"-ss", str(get_video_duration(input_path) / 2),
"-i", str(input_path),
"-frames:v", "1",
str(output_path)
])
def xor_encrypt_decrypt(data, key):
return "".join(chr(ord(c) ^ ord(key[i % len(key)])) for i, c in enumerate(data))
@@ -103,10 +103,6 @@ class Parser:
out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
if 'planplus' in outs:
self.parse_mdn('planplus', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
elif 'planplus' in outs:
planplus_mhp = outs['planplus'].shape[1] != 2 * ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH
planplus_in_N, planplus_out_N = (ModelConstants.PLAN_MHP_N, ModelConstants.PLAN_MHP_SELECTION) if planplus_mhp else (0, 0)
self.parse_mdn('planplus', outs, in_N=planplus_in_N, out_N=planplus_out_N, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
if 'lane_lines' in outs:
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0,
out_shape=(ModelConstants.NUM_LANE_LINES, ModelConstants.IDX_N, ModelConstants.LANE_LINES_WIDTH))
+5 -7
View File
@@ -125,8 +125,8 @@ class ModelState:
cloudlog.warning(f"ModelVersion parameter not available: {e}")
model_dir = MODELS_PATH
# For the default "sc" model, use built-in files from the models directory
if model_id == "sc":
# For the default "bd2" model, use built-in files from the models directory
if model_id == "bd2":
models_dir = Path(__file__).parent / "models"
VISION_PKL_PATH = models_dir / "driving_vision_tinygrad.pkl"
POLICY_PKL_PATH = models_dir / "driving_policy_tinygrad.pkl"
@@ -191,8 +191,6 @@ class ModelState:
# Add policy_generation attribute after loading policy_metadata
self.policy_generation = model_version or "v8"
self.is_v11 = (self.policy_generation == "v11")
self.is_v10 = (self.policy_generation == "v10")
self.is_v12 = (self.policy_generation == "v12")
self.is_v9 = (self.policy_generation == "v9")
self.mlsim = (self.policy_generation in ("v8", "v10", "v11", "v12"))
@@ -331,14 +329,14 @@ class ModelState:
self.full_prev_desired_curv[0,-1,:] = policy_outputs_dict['desired_curvature'][0, :]
if self.prev_desired_curv_key is not None:
# v9/v10/v11/v12 models expect zeros for prev_desired_curv(s); others use history
if self.is_v9 or self.is_v10 or self.is_v11 or self.is_v12:
# v9 models expect zeros for prev_desired_curv(s); others use history
if self.is_v9:
self.numpy_inputs[self.prev_desired_curv_key][:] = 0 * self.full_prev_desired_curv[0, self.temporal_idxs]
else:
self.numpy_inputs[self.prev_desired_curv_key][:] = self.full_prev_desired_curv[0, self.temporal_idxs]
if self.off_policy_enabled and self.off_policy_prev_desired_curv_key is not None:
if self.is_v9 or self.is_v12:
if self.is_v9:
self.off_policy_numpy_inputs[self.off_policy_prev_desired_curv_key][:] = 0 * self.full_prev_desired_curv[0, self.temporal_idxs]
else:
self.off_policy_numpy_inputs[self.off_policy_prev_desired_curv_key][:] = self.full_prev_desired_curv[0, self.temporal_idxs]
@@ -37,7 +37,6 @@ FrogPilotDevicePanel::FrogPilotDevicePanel(FrogPilotSettingsWindow *parent) : Fr
{"DeviceShutdown", tr("Device Shutdown Timer"), tr("<b>Keep the device on for the set amount of time after a drive</b> before it shuts down automatically."), ""},
{"NoLogging", tr("Disable Logging"), QString("<b>%1</b><br><br>%2").arg(tr("WARNING: This will prevent your drives from being recorded and all data will be unobtainable!")).arg(tr("<b>Prevent the device from saving driving data.</b>")), ""},
{"NoUploads", tr("Disable Uploads"), QString("<b>%1</b><br><br>%2").arg(tr("WARNING: This will prevent your drives from being uploaded to <b>comma connect</b> which will impact debugging and official support from comma!")).arg(tr("<b>Prevent the device from uploading driving data.</b>")), ""},
{"AlwaysAllowUploads", tr("Always Allow Uploads"), tr("<b>Force driving data uploads regardless of network type, metered status, and upload restrictions.</b> This overrides normal upload blocking checks."), ""},
{"HigherBitrate", tr("High-Quality Recording"), tr("<b>Save drive footage in higher video quality.</b>"), ""},
{"LowVoltageShutdown", tr("Low-Voltage Cutoff"), tr("<b>While parked, if the battery voltage falls below the set level, the device shuts down</b> to prevent excessive battery drain."), ""},
{"IncreaseThermalLimits", tr("Raise Temperature Limits"), QString("<b>%1</b><br><br>%2").arg(tr("WARNING: Running at higher temperatures may damage your device!")).arg(tr("<b>Allow the device to run at higher temperatures</b> before throttling or shutting down. Use only if you understand the risks!")), ""},
+1 -1
View File
@@ -25,7 +25,7 @@ private:
std::map<QString, AbstractControl*> toggles;
QSet<QString> deviceManagementKeys = {"AlwaysAllowUploads", "DeviceShutdown", "HigherBitrate", "IncreaseThermalLimits", "LowVoltageShutdown", "NoLogging", "NoUploads", "UseKonikServer"};
QSet<QString> deviceManagementKeys = {"DeviceShutdown", "HigherBitrate", "IncreaseThermalLimits", "LowVoltageShutdown", "NoLogging", "NoUploads", "UseKonikServer"};
QSet<QString> screenKeys = {"ScreenBrightness", "ScreenBrightnessOnroad", "ScreenRecorder", "ScreenTimeout", "ScreenTimeoutOnroad", "StandbyMode"};
QSet<QString> parentKeys;
@@ -273,10 +273,7 @@ void FrogPilotSettingsWindow::updateVariables() {
hasSNG = hasOpenpilotLongitudinal && CP.getAutoResumeSng();
hasZSS = frogpilot_toggles.value("has_zss").toBool();
isAngleCar = CP.getSteerControlType() == cereal::CarParams::SteerControlType::ANGLE;
isBolt = carFingerprint == "CHEVROLET_BOLT_ACC_2022_2023" ||
carFingerprint == "CHEVROLET_BOLT_CC_2022_2023" ||
carFingerprint == "CHEVROLET_BOLT_CC_2019_2021" ||
carFingerprint == "CHEVROLET_BOLT_CC_2017";
isBolt = carFingerprint == "CHEVROLET_BOLT_CC" || carFingerprint == "CHEVROLET_BOLT_EUV";
isGM = carMake == "gm";
isHKG = carMake == "hyundai";
isHKGCanFd = isHKG && safetyModel == cereal::CarParams::SafetyModel::HYUNDAI_CANFD;
@@ -284,7 +281,8 @@ void FrogPilotSettingsWindow::updateVariables() {
isTorqueCar = CP.getLateralTuning().which() == cereal::CarParams::LateralTuning::TORQUE;
isToyota = carMake == "toyota";
isTSK = CP.getSecOcRequired();
isVolt = carFingerprint == "CHEVROLET_VOLT";
isVolt = carFingerprint.find("CHEVROLET_VOLT") == 0;
if (isVolt) hasSNG = false;
longitudinalActuatorDelay = CP.getLongitudinalActuatorDelay();
startAccel = CP.getStartAccel();
steerActuatorDelay = CP.getSteerActuatorDelay();
@@ -103,10 +103,6 @@ FrogPilotLongitudinalPanel::FrogPilotLongitudinalPanel(
"vehicle's detected powertrain type but can be overridden if the "
"automatic choice doesn't match."),
""},
{"TruckTuning", tr("Truck Tuning"),
tr("<b>Use aggressive acceleration profiles tuned for trucks.</b> "
"Intended for heavy vehicles that need stronger throttle."),
""},
{"LongitudinalActuatorDelay",
longitudinalActuatorDelay != 0
? QString(tr("Actuator Delay (Default: %1)"))
@@ -1129,21 +1125,6 @@ FrogPilotLongitudinalPanel::FrogPilotLongitudinalPanel(
&FrogPilotLongitudinalPanel::updateToggles);
}
QObject::connect(static_cast<ToggleControl *>(toggles["EVTuning"]),
&ToggleControl::toggleFlipped, this, [this]() {
if (params.getBool("EVTuning")) {
params.putBool("TruckTuning", false);
}
updateToggles();
});
QObject::connect(static_cast<ToggleControl *>(toggles["TruckTuning"]),
&ToggleControl::toggleFlipped, this, [this]() {
if (params.getBool("TruckTuning")) {
params.putBool("EVTuning", false);
}
updateToggles();
});
FrogPilotParamValueControl *trafficFollowToggle =
static_cast<FrogPilotParamValueControl *>(toggles["TrafficFollow"]);
FrogPilotParamValueControl *trafficAccelerationToggle =
@@ -1651,15 +1632,10 @@ void FrogPilotLongitudinalPanel::updateToggles() {
else if (key == "StoppingDecelRate" || key == "VEgoStarting" ||
key == "VEgoStopping") {
setVisible &= !isGM || !params.getBool("ExperimentalGMTune");
setVisible &= !isToyota || !params.getBool("FrogsGoMoosTweak");
}
if (key == "EVTuning") {
toggle->setEnabled(!params.getBool("TruckTuning"));
} else if (key == "TruckTuning") {
toggle->setEnabled(!params.getBool("EVTuning"));
}
toggle->setVisible(setVisible);
if (setVisible) {
@@ -40,7 +40,7 @@ private:
std::map<QString, AbstractControl*> toggles;
QSet<QString> advancedLongitudinalTuneKeys = {"EVTuning", "TruckTuning", "LongitudinalActuatorDelay", "StartAccel", "StopAccel", "StoppingDecelRate", "VEgoStarting", "VEgoStopping"};
QSet<QString> advancedLongitudinalTuneKeys = {"EVTuning", "LongitudinalActuatorDelay", "StartAccel", "StopAccel", "StoppingDecelRate", "VEgoStarting", "VEgoStopping"};
QSet<QString> aggressivePersonalityKeys = {"AggressiveFollow", "AggressiveFollowHigh", "AggressiveJerkAcceleration", "AggressiveJerkDeceleration", "AggressiveJerkDanger", "AggressiveJerkSpeed", "AggressiveJerkSpeedDecrease", "ResetAggressivePersonality"};
QSet<QString> conditionalExperimentalKeys = {"CESpeed", "CESpeedLead", "CECurves", "CELead", "CEModelStopTime", "CENavigation", "CESignalSpeed", "ShowCEMStatus"};
QSet<QString> curveSpeedKeys = {"CalibratedLateralAcceleration", "CalibrationProgress", "ResetCurveData", "ShowCSCStatus"};
@@ -53,7 +53,6 @@ FrogPilotSoundsPanel::FrogPilotSoundsPanel(FrogPilotSettingsWindow *parent) : Fr
{"CustomAlerts", tr("FrogPilot Alerts"), tr("<b>Optional FrogPilot alerts</b> that highlight driving events in a more noticeable way."), "../../frogpilot/assets/toggle_icons/icon_green_light.png"},
{"GoatScream", tr("Goat Scream"), tr("<b>Play the infamous \"Goat Scream\" when the steering controller reaches its limit.</b> Based on the \"Turn Exceeds Steering Limit\" event."), ""},
{"GoatScreamCriticalAlerts", tr("Goat Scream Critical Alerts"), tr("<b>Play the infamous \"Goat Scream\" for full-screen critical alerts that require immediate takeover.</b><br><br>Examples include: \"TAKE CONTROL IMMEDIATELY\" and \"Stock AEB: Risk of Collision\"."), ""},
{"GreenLightAlert", tr("Green Light Alert"), tr("<b>Play an alert when the model predicts a red light has turned green.</b><br><br><i><b>Disclaimer</b>: openpilot does not explicitly detect traffic lights. This alert is based on end-to-end model predictions from camera input and may trigger even when the light has not changed.</i>"), ""},
{"LeadDepartingAlert", tr("Lead Departing Alert"), tr("<b>Play an alert when the lead vehicle departs from a stop.</b>"), ""},
{"LoudBlindspotAlert", tr("Loud \"Car Detected in Blindspot\" Alert"), tr("<b>Play a louder alert if a vehicle is in the blind spot when attempting to change lanes.</b> Based on the \"Car Detected in Blindspot\" event."), ""},
+1 -1
View File
@@ -28,7 +28,7 @@ private:
std::map<QString, AbstractControl*> toggles;
QSet<QString> alertVolumeControlKeys {"DisengageVolume", "EngageVolume", "PromptDistractedVolume", "PromptVolume", "RefuseVolume", "WarningImmediateVolume", "WarningSoftVolume"};
QSet<QString> customAlertsKeys {"GoatScream", "GoatScreamCriticalAlerts", "GreenLightAlert", "LeadDepartingAlert", "LoudBlindspotAlert", "SpeedLimitChangedAlert"};
QSet<QString> customAlertsKeys {"GoatScream", "GreenLightAlert", "LeadDepartingAlert", "LoudBlindspotAlert", "SpeedLimitChangedAlert"};
QSet<QString> parentKeys;
-65
View File
@@ -66,11 +66,6 @@ void deleteThemeAsset(QDir &directory, const QString &subFolder, const QString &
}
void downloadThemeAsset(const QString &input, const std::string &paramKey, const QString &assetParam, Params &params, Params &params_memory) {
if (paramKey == "BootLogoToDownload") {
params_memory.put(paramKey, input.trimmed().toStdString());
return;
}
QString output = input;
int tilde = output.indexOf("~");
if (tilde >= 0) {
@@ -233,7 +228,6 @@ FrogPilotThemesPanel::FrogPilotThemesPanel(FrogPilotSettingsWindow *parent) : Fr
const std::vector<std::tuple<QString, QString, QString, QString>> themeToggles {
{"PersonalizeOpenpilot", tr("Custom Themes"), tr("<b>The overall look and feel of openpilot.</b> Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!"), "../../frogpilot/assets/toggle_icons/icon_frog.png"},
{"BootLogo", tr("Boot Logo"), tr("<b>The boot logo shown while the device starts.</b>"), ""},
{"CustomColors", tr("Color Scheme"), tr("<b>The color scheme used throughout openpilot.</b> Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!"), ""},
{"CustomDistanceIcons", tr("Distance Button"), tr("<b>The distance button icons shown on the driving screen.</b> Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!"), ""},
{"CustomIcons", tr("Icon Pack"), tr("<b>The icon style used across openpilot.</b> Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!"), ""},
@@ -258,57 +252,6 @@ FrogPilotThemesPanel::FrogPilotThemesPanel(FrogPilotSettingsWindow *parent) : Fr
themesLayout->setCurrentWidget(customThemesPanel);
});
themeToggle = personalizeOpenpilotToggle;
} else if (param == "BootLogo") {
manageBootLogosButton = new FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")});
QObject::connect(manageBootLogosButton, &FrogPilotButtonsControl::buttonClicked, [this](int id) {
// Show all downloaded boot logos, including the currently selected one.
QStringList bootLogos = getThemeList(true, QDir(bootLogosDirectory.path()), "", "BootLogo", params);
if (id == 0) {
QString bootLogoToDelete = MultiOptionDialog::getSelection(tr("Select a boot logo to delete"), bootLogos, "", this);
if (!bootLogoToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Delete the \"%1\" boot logo?").arg(bootLogoToDelete), tr("Delete"), this)) {
bootLogosDownloaded = false;
deleteThemeAsset(bootLogosDirectory, "", "DownloadableBootLogos", bootLogoToDelete, params);
}
} else if (id == 1) {
if (bootLogoDownloading) {
cancellingDownload = true;
params_memory.putBool("CancelThemeDownload", true);
QTimer::singleShot(2500, [this]() {
bootLogoDownloading = false;
cancellingDownload = false;
themeDownloading = false;
params_memory.putBool("CancelThemeDownload", false);
});
} else {
QStringList downloadableBootLogos = QString::fromStdString(params.get("DownloadableBootLogos")).split(",");
bootLogoToDownload = MultiOptionDialog::getSelection(tr("Select a boot logo to download"), downloadableBootLogos, "", this);
if (!bootLogoToDownload.isEmpty()) {
manageBootLogosButton->setValue(storeThemeName(bootLogoToDownload, "BootLogo", params));
bootLogoDownloading = true;
themeDownloading = true;
params_memory.put("ThemeDownloadProgress", "Downloading...");
downloadThemeAsset(bootLogoToDownload, "BootLogoToDownload", "DownloadableBootLogos", params, params_memory);
downloadStatusLabel->setText("Downloading...");
}
}
} else if (id == 2) {
QString bootLogoToSelect = MultiOptionDialog::getSelection(tr("Select a boot logo"), bootLogos, getThemeName("BootLogo", params), this);
if (!bootLogoToSelect.isEmpty()) {
manageBootLogosButton->setValue(storeThemeName(bootLogoToSelect, "BootLogo", params));
}
}
});
manageBootLogosButton->setValue(getThemeName(param.toStdString(), params));
themeToggle = manageBootLogosButton;
} else if (param == "CustomColors") {
manageCustomColorsButton = new FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")});
QObject::connect(manageCustomColorsButton, &FrogPilotButtonsControl::buttonClicked, [this](int id) {
@@ -761,7 +704,6 @@ FrogPilotThemesPanel::FrogPilotThemesPanel(FrogPilotSettingsWindow *parent) : Fr
}
void FrogPilotThemesPanel::showEvent(QShowEvent *event) {
bootLogosDownloaded = params.get("DownloadableBootLogos").empty();
colorsDownloaded = params.get("DownloadableColors").empty();
distanceIconsDownloaded = params.get("DownloadableDistanceIcons").empty();
iconsDownloaded = params.get("DownloadableIcons").empty();
@@ -814,7 +756,6 @@ void FrogPilotThemesPanel::updateState(const UIState &s, const FrogPilotUIState
finalizingDownload = true;
QTimer::singleShot(2500, [this]() {
bootLogoDownloading = false;
colorDownloading = false;
distanceIconDownloading = false;
finalizingDownload = false;
@@ -824,7 +765,6 @@ void FrogPilotThemesPanel::updateState(const UIState &s, const FrogPilotUIState
themeDownloading = false;
wheelDownloading = false;
bootLogosDownloaded = params.get("DownloadableBootLogos").empty();
colorsDownloaded = params.get("DownloadableColors").empty();
distanceIconsDownloaded = params.get("DownloadableDistanceIcons").empty();
iconsDownloaded = params.get("DownloadableIcons").empty();
@@ -842,11 +782,6 @@ void FrogPilotThemesPanel::updateState(const UIState &s, const FrogPilotUIState
bool parked = !s.scene.started || fs.frogpilot_scene.parked || fs.frogpilot_toggles.value("frogs_go_moo").toBool();
manageBootLogosButton->setText(1, bootLogoDownloading ? tr("CANCEL") : tr("DOWNLOAD"));
manageBootLogosButton->setEnabledButtons(0, !themeDownloading);
manageBootLogosButton->setEnabledButtons(1, fs.frogpilot_scene.online && (!themeDownloading || bootLogoDownloading) && !cancellingDownload && !finalizingDownload && !bootLogosDownloaded && parked);
manageBootLogosButton->setEnabledButtons(2, !themeDownloading);
manageCustomColorsButton->setText(1, colorDownloading ? tr("CANCEL") : tr("DOWNLOAD"));
manageCustomColorsButton->setEnabledButtons(0, !themeDownloading);
manageCustomColorsButton->setEnabledButtons(1, fs.frogpilot_scene.online && (!themeDownloading || colorDownloading) && !cancellingDownload && !finalizingDownload && !colorsDownloaded && parked);
+1 -6
View File
@@ -19,8 +19,6 @@ private:
void updateToggles();
bool cancellingDownload;
bool bootLogoDownloading = false;
bool bootLogosDownloaded = false;
bool colorDownloading;
bool colorsDownloaded;
bool distanceIconDownloading;
@@ -42,11 +40,10 @@ private:
std::map<QString, AbstractControl*> toggles;
QSet<QString> customThemeKeys = {"BootLogo", "CustomColors", "CustomDistanceIcons", "CustomIcons", "CustomSignals", "CustomSounds", "DownloadStatusLabel", "WheelIcon"};
QSet<QString> customThemeKeys = {"CustomColors", "CustomDistanceIcons", "CustomIcons", "CustomSignals", "CustomSounds", "DownloadStatusLabel", "WheelIcon"};
QSet<QString> parentKeys;
FrogPilotButtonsControl *manageBootLogosButton;
FrogPilotButtonsControl *manageCustomColorsButton;
FrogPilotButtonsControl *manageCustomIconsButton;
FrogPilotButtonsControl *manageCustomSignalsButton;
@@ -58,13 +55,11 @@ private:
LabelControl *downloadStatusLabel;
QDir bootLogosDirectory{"/data/themes/bootlogos/"};
QDir themePacksDirectory{"/data/themes/theme_packs/"};
QDir wheelsDirectory{"/data/themes/steering_wheels/"};
QJsonObject frogpilotToggleLevels;
QString bootLogoToDownload;
QString colorSchemeToDownload;
QString distanceIconPackToDownload;
QString iconPackToDownload;
+4 -30
View File
@@ -1,9 +1,6 @@
#include <QRegularExpression>
#include <QTextStream>
#include <chrono>
#include <thread>
#include "frogpilot/ui/qt/offroad/vehicle_settings.h"
QStringList getCarNames(const QString &carMake, QMap<QString, QString> &carModels) {
@@ -170,11 +167,9 @@ FrogPilotVehiclesPanel::FrogPilotVehiclesPanel(FrogPilotSettingsWindow *parent)
std::vector<std::tuple<QString, QString, QString, QString>> vehicleToggles {
{"GMToggles", tr("General Motors Settings"), tr("<b>FrogPilot features for General Motors vehicles.</b>"), ""},
{"ExperimentalGMTune", tr("FrogsGoMoo's Experimental Tune"), tr("<b>Experimental GM tune by FrogsGoMoo</b> that attempts to smoothen stopping and takeoff control. Use at your own risk!"), ""},
{"GMPedalLongitudinal", tr("Use Pedal for Longitudinal Control"), tr("<b>Use the pedal interceptor for longitudinal control</b> instead of camera ACC/Redneck when available."), ""},
{"RemapCancelToDistance", tr("Remap cancel to distance"), tr("<b>Treat CANCEL as distance-button input</b> on supported pedal-long GM platforms. Toggle requires a reboot to take effect."), ""},
{"LongPitch", tr("Smooth Pedal Response on Hills"), tr("<b>Smoothen acceleration and braking</b> when driving downhill/uphill."), ""},
{"RedPanda", tr("Red Panda"), tr("<b>Enable Red Panda behavior</b> for GM (alternate safety config and bus numbering). Requires a reboot to take effect."), ""},
{"RemoteStartBootsComma", tr("Remote Start Boots Comma"), tr("<b>Use GM C9 SystemPowerMode</b> for ignition detection. Toggle requires a panda firmware update and a reboot to take effect."), ""},
{"VoltSNG", tr("Stop-and-Go Hack"), tr("<b>Force stop-and-go</b> on the 2017 Chevy Volt."), ""},
{"HKGToggles", tr("Hyundai/Kia/Genesis Settings"), tr("<b>FrogPilot features for Genesis, Hyundai, and Kia vehicles.</b>"), ""},
@@ -288,7 +283,7 @@ FrogPilotVehiclesPanel::FrogPilotVehiclesPanel(FrogPilotSettingsWindow *parent)
static_cast<FrogPilotParamValueControl*>(toggles["LockDoorsTimer"])->setWarning("<b>Warning:</b> openpilot can't detect if keys are still inside the car, so ensure you have a spare key to prevent accidental lockouts!");
QSet<QString> rebootKeys = {"NewLongAPI", "RemapCancelToDistance", "TacoTuneHacks"};
QSet<QString> rebootKeys = {"NewLongAPI", "TacoTuneHacks"};
for (const QString &key : rebootKeys) {
QObject::connect(static_cast<ToggleControl*>(toggles[key]), &ToggleControl::toggleFlipped, [key, this](bool state) {
if (started) {
@@ -305,25 +300,6 @@ FrogPilotVehiclesPanel::FrogPilotVehiclesPanel(FrogPilotSettingsWindow *parent)
});
}
ParamControl *remoteStartToggle = static_cast<ParamControl*>(toggles["RemoteStartBootsComma"]);
QObject::connect(remoteStartToggle, &ToggleControl::toggleFlipped, [parent, remoteStartToggle, this](bool state) {
const QString prompt = tr("Remote Start requires a Panda firmware update. Flash the Panda now?");
if (!FrogPilotConfirmationDialog::yesorno(prompt, this)) {
params.putBool("RemoteStartBootsComma", !state);
remoteStartToggle->refresh();
return;
}
std::thread([parent, this]() {
parent->keepScreenOn = true;
params_memory.putBool("FlashPanda", true);
while (params_memory.getBool("FlashPanda")) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
Hardware::reboot();
}).detach();
});
openDescriptions(forceOpenDescriptions, toggles);
QObject::connect(uiState(), &UIState::offroadTransition, [selectMakeButton, selectModelButton, this]() {
@@ -405,9 +381,7 @@ void FrogPilotVehiclesPanel::updateToggles() {
bool setVisible = tuningLevel >= frogpilotToggleLevels[key].toDouble();
if (key == "ClusterOffset") {
setVisible &= isGM || isToyota;
} else if (gmKeys.contains(key)) {
if (gmKeys.contains(key)) {
setVisible &= isGM;
} else if (hkgKeys.contains(key)) {
setVisible &= isHKG;
@@ -433,7 +407,7 @@ void FrogPilotVehiclesPanel::updateToggles() {
setVisible &= isHKGCanFd;
}
else if (key == "GMPedalLongitudinal" || key == "RemapCancelToDistance") {
else if (key == "GMPedalLongitudinal") {
setVisible &= hasPedal;
}
+3 -4
View File
@@ -36,10 +36,10 @@ private:
std::map<QString, AbstractControl*> toggles;
QSet<QString> gmKeys = {"GMPedalLongitudinal", "LongPitch", "RedPanda", "RemapCancelToDistance", "RemoteStartBootsComma", "VoltSNG"};
QSet<QString> gmKeys = {"ExperimentalGMTune", "GMPedalLongitudinal", "LongPitch", "VoltSNG"};
QSet<QString> hkgKeys = {"NewLongAPI", "TacoTuneHacks"};
QSet<QString> longitudinalKeys = {"FrogsGoMoosTweak", "LongPitch", "NewLongAPI", "RemapCancelToDistance", "SNGHack", "VoltSNG"};
QSet<QString> toyotaKeys = {"FrogsGoMoosTweak", "LockDoorsTimer", "SNGHack", "ToyotaDoors"};
QSet<QString> longitudinalKeys = {"ExperimentalGMTune", "FrogsGoMoosTweak", "LongPitch", "NewLongAPI", "SNGHack", "VoltSNG"};
QSet<QString> toyotaKeys = {"ClusterOffset", "FrogsGoMoosTweak", "LockDoorsTimer", "SNGHack", "ToyotaDoors"};
QSet<QString> vehicleInfoKeys = {"BlindSpotSupport", "HardwareDetected", "OpenpilotLongitudinal", "PedalSupport", "RadarSupport", "SASCMSupport", "SDSUSupport", "SNGSupport"};
QSet<QString> parentKeys;
@@ -50,7 +50,6 @@ private:
ParamControl *forceFingerprint;
Params params;
Params params_memory{"/dev/shm/params"};
Params params_default{"/dev/shm/params_default"};
QJsonObject frogpilotToggleLevels;
+1
View File
@@ -165,6 +165,7 @@ BO_ 481 ASCMSteeringButton: 7 K124_ASCM
SG_ DistanceButton : 22|1@0+ (1,0) [0|0] "" NEO
SG_ LKAButton : 23|1@0+ (1,0) [0|0] "" NEO
SG_ ACCAlwaysOne : 24|1@0+ (1,0) [0|1] "" XXX
SG_ ACCHiddenBit : 30|1@0+ (1,0) [0|1] "" XXX
SG_ ACCButtons : 46|3@0+ (1,0) [0|0] "" NEO
SG_ DriveModeButton : 39|1@0+ (1,0) [0|1] "" XXX
SG_ RollingCounter : 33|2@0+ (1,0) [0|3] "" NEO
-5
View File
@@ -13,11 +13,6 @@ a.out
dist/
pandacan.egg-info/
obj/
!board/obj/
!board/obj/bootstub.panda_remote.bin
!board/obj/bootstub.panda_h7_remote.bin
!board/obj/panda_remote.bin.signed
!board/obj/panda_h7_remote.bin.signed
examples/output.csv
.DS_Store
.vscode*
+5 -28
View File
@@ -33,7 +33,6 @@ extern bool can_loopback;
// Ignition detected from CAN meessages
bool ignition_can = false;
uint32_t ignition_can_cnt = 0U;
extern bool gm_remote_start_boots_comma;
#define ALL_CAN_SILENT 0xFF
#define ALL_CAN_LIVE 0
@@ -201,24 +200,12 @@ void ignition_can_hook(CANPacket_t *to_push) {
if (bus == 0) {
int addr = GET_ADDR(to_push);
int len = GET_LEN(to_push);
// GM exception
#ifdef PANDA_GM_REMOTE_START_C9
if (true) {
#else
if (gm_remote_start_boots_comma) {
#endif
if ((addr == 0xC9) && (len == 8)) {
// Matches SystemPowerMode (1=Run, 0=Off)
ignition_can = (GET_BYTE(to_push, 6) & 0x10U) != 0U;
ignition_can_cnt = 0U;
}
} else {
if ((addr == 0x1F1) && (len == 8)) {
// SystemPowerMode (2=Run, 3=Crank Request)
ignition_can = (GET_BYTE(to_push, 0) & 0x2U) != 0U;
ignition_can_cnt = 0U;
}
if ((addr == 0xC9) && (len == 8)) {
// Matches SystemPowerMode (1=Run, 0=Off)
ignition_can = (GET_BYTE(to_push, 6) & 0x10U) != 0U;
ignition_can_cnt = 0U;
}
// Tesla exception
@@ -233,16 +220,6 @@ void ignition_can_hook(CANPacket_t *to_push) {
ignition_can = (GET_BYTE(to_push, 0) >> 5) == 0x6U;
ignition_can_cnt = 0U;
}
} else if (bus == 2) {
int addr = GET_ADDR(to_push);
int len = GET_LEN(to_push);
// GM exception, SDGM cars have this message on bus 2
if ((addr == 0x1F1) && (len == 8)) {
// SystemPowerMode (2=Run, 3=Crank Request)
ignition_can = (GET_BYTE(to_push, 0) & 0x2U) != 0U;
ignition_can_cnt = 0U;
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
const uint8_t gitversion[] = "DEV-45631cf2-DEBUG";
const uint8_t gitversion[] = "DEV-87d2f6ab-DEBUG";
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+20 -346
View File
@@ -9,17 +9,6 @@ const SteeringLimits GM_STEERING_LIMITS = {
.type = TorqueDriverLimited,
};
const SteeringLimits GM_BOLT_2017_STEERING_LIMITS = {
.max_steer = 450,
.max_rate_up = 15,
.max_rate_down = 34,
.driver_torque_allowance = 78,
.driver_torque_factor = 6,
.max_rt_delta = 345,
.max_rt_interval = 200000,
.type = TorqueDriverLimited,
};
const LongitudinalLimits GM_ASCM_LONG_LIMITS = {
.max_gas = 8191,
.min_gas = 5500,
@@ -34,7 +23,6 @@ const LongitudinalLimits GM_CAM_LONG_LIMITS = {
.max_brake = 400,
};
const SteeringLimits *gm_steer_limits;
const LongitudinalLimits *gm_long_limits;
const int GM_STANDSTILL_THRSLD = 10; // 0.311kph
@@ -48,33 +36,17 @@ const CanMsg GM_ASCM_TX_MSGS[] = {{0x180, 0, 4}, {0x409, 0, 7}, {0x40A, 0, 7}, {
{0xA1, 1, 7}, {0x306, 1, 8}, {0x308, 1, 7}, {0x310, 1, 2}, // obs bus
{0x315, 2, 5}}; // ch bus
const CanMsg GM_CAM_TX_MSGS[] = {{0x180, 0, 4}, {0x370, 0, 6}, {0x200, 0, 6}, {0x1E1, 0, 7}, {0x3D1, 0, 8}, {0xBD, 0, 7}, {0x1F5, 0, 8}, // pt bus
const CanMsg GM_CAM_TX_MSGS[] = {{0x180, 0, 4}, {0x200, 0, 6}, {0x1E1, 0, 7}, {0xBD, 0, 7}, {0x1F5, 0, 8}, // pt bus
{0x1E1, 2, 7}, {0x184, 2, 8}}; // camera bus
const CanMsg GM_CAM_LONG_TX_MSGS[] = {{0x180, 0, 4}, {0x315, 0, 5}, {0x2CB, 0, 8}, {0x370, 0, 6}, {0x200, 0, 6}, {0x3D1, 0, 8}, {0xBD, 0, 7}, {0x1F5, 0, 8}, // pt bus
const CanMsg GM_CAM_LONG_TX_MSGS[] = {{0x180, 0, 4}, {0x315, 0, 5}, {0x2CB, 0, 8}, {0x370, 0, 6}, {0x200, 0, 6}, {0xBD, 0, 7}, {0x1F5, 0, 8}, // pt bus
{0x315, 2, 5}, {0x1E1, 2, 7}, {0x184, 2, 8}}; // camera bus
const CanMsg GM_SDGM_TX_MSGS[] = {{0x180, 0, 4}, {0x1E1, 0, 7}, {0xBD, 0, 7}, {0x1F5, 0, 8}, // pt bus
{0x184, 2, 8}}; // camera bus
const CanMsg GM_CC_LONG_TX_MSGS[] = {{0x180, 0, 4}, {0x370, 0, 6}, {0x1E1, 0, 7}, {0x3D1, 0, 8}, {0xBD, 0, 7}, {0x1F5, 0, 8}, // pt bus
const CanMsg GM_CC_LONG_TX_MSGS[] = {{0x180, 0, 4}, {0x1E1, 0, 7}, {0xBD, 0, 7}, {0x1F5, 0, 8}, // pt bus
{0x184, 2, 8}, {0x1E1, 2, 7}}; // camera bus
// TODO: do checksum and counter checks. Add correct timestep, 0.1s for now.
RxCheck gm_rx_checks[] = {
{.msg = {{0x184, 0, 8, .frequency = 10U}, { 0 }, { 0 }}},
{.msg = {{0x34A, 0, 5, .frequency = 10U}, { 0 }, { 0 }}},
{.msg = {{0x1E1, 0, 7, .frequency = 10U}, // Non-SDGM Car
{0x1E1, 2, 7, .frequency = 100000U}}}, // SDGM Car
{.msg = {{0xF1, 0, 6, .frequency = 10U}, // Non-SDGM Car
{0xF1, 2, 6, .frequency = 100000U}}}, // SDGM Car
{.msg = {{0x1C4, 0, 8, .frequency = 10U}, { 0 }, { 0 }}},
{.msg = {{0xC9, 0, 8, .frequency = 10U}, { 0 }, { 0 }}},
};
// Volt ASCM / ASCM_INT stock-camera path should pin 0x1E1/0xF1 to bus 0 to avoid
// latching bus 2 when mirrored startup traffic is present.
RxCheck gm_ascm_int_stock_cam_rx_checks[] = {
{.msg = {{0x184, 0, 8, .frequency = 10U}, { 0 }, { 0 }}},
{.msg = {{0x34A, 0, 5, .frequency = 10U}, { 0 }, { 0 }}},
{.msg = {{0x1E1, 0, 7, .frequency = 10U}, { 0 }, { 0 }}},
@@ -94,23 +66,6 @@ const uint16_t GM_PARAM_PEDAL_INTERCEPTOR = 128;
const uint16_t GM_PARAM_ASCM_INT = 256;
const uint16_t GM_PARAM_FORCE_BRAKE_C9 = 512;
const uint16_t GM_PARAM_HW_SDGM = 1024;
const uint16_t GM_PARAM_BOLT_2017 = 2048;
const uint16_t GM_PARAM_BOLT_2022_PEDAL = 4096;
const uint16_t GM_PARAM_REMOTE_START_BOOTS_COMMA = 8192;
const uint16_t GM_PARAM_PANDA_3D1_SCHED = 16384;
const uint16_t GM_PARAM_PANDA_PADDLE_SCHED = 32768;
const uint32_t GM_3D1_PERIOD_US = 100000U;
const uint32_t GM_3D1_TX_OFFSET_US = 0U;
const uint32_t GM_3D1_LOCK_TOLERANCE_US = 20000U;
const uint32_t GM_PADDLE_PERIOD_US = 25000U;
const uint32_t GM_PADDLE_TX_OFFSET_US = 0U;
const uint32_t GM_PADDLE_LOCK_TOLERANCE_US = 5000U;
const uint32_t GM_PADDLE_STALE_US = 100000U;
const uint32_t GM_PADDLE_FEED_STALE_US = 100000U;
void can_send(CANPacket_t *to_push, uint8_t bus_number, bool skip_tx_hook);
void can_set_checksum(CANPacket_t *packet);
enum {
GM_BTN_UNPRESS = 1,
@@ -132,115 +87,7 @@ bool gm_pedal_long = false;
bool gm_cc_long = false;
bool gm_skip_relay_check = false;
bool gm_force_ascm = false;
bool gm_bolt_2022_pedal = false;
bool gm_ascm_int = false;
bool gm_force_brake_c9 = false;
bool gm_remote_start_boots_comma = false;
bool gm_panda_3d1_sched = false;
bool gm_panda_paddle_sched = false;
bool gm_3d1_spoof_valid = false;
bool gm_3d1_internal_tx = false;
uint8_t gm_3d1_spoof_data[8] = {0U};
uint32_t gm_3d1_next_tx_us = 0U;
uint32_t gm_3d1_expected_stock_us = 0U;
uint32_t gm_3d1_last_stock_us = 0U;
bool gm_3d1_phase_locked = false;
bool gm_paddle_internal_tx = false;
typedef struct {
bool spoof_valid;
uint8_t spoof_data[8];
uint32_t next_tx_us;
uint32_t expected_stock_us;
uint32_t last_stock_us;
uint32_t last_feed_us;
bool phase_locked;
} GmPeriodicSpoofState;
GmPeriodicSpoofState gm_bd_state = {0};
GmPeriodicSpoofState gm_prndl2_state = {0};
static void gm_update_periodic_phase(GmPeriodicSpoofState *state, uint32_t now_us, uint32_t period_us, uint32_t lock_tolerance_us) {
state->last_stock_us = now_us;
if (!state->phase_locked) {
state->phase_locked = true;
state->expected_stock_us = now_us + period_us;
} else {
int32_t phase_err_us = (int32_t)(now_us - state->expected_stock_us);
if (phase_err_us < 0) {
phase_err_us = -phase_err_us;
}
if ((uint32_t)phase_err_us <= lock_tolerance_us) {
state->expected_stock_us += period_us;
} else {
state->expected_stock_us = now_us + period_us;
}
}
}
static bool gm_periodic_scheduler_ready(const GmPeriodicSpoofState *state, uint32_t now_us, uint32_t stale_us, uint32_t feed_stale_us) {
bool stock_stale = (state->last_stock_us == 0U) || (get_ts_elapsed(now_us, state->last_stock_us) > stale_us);
bool feed_stale = (state->last_feed_us == 0U) || (get_ts_elapsed(now_us, state->last_feed_us) > feed_stale_us);
return state->phase_locked && !stock_stale && !feed_stale;
}
static void gm_try_send_periodic_spoof(uint32_t now_us, uint32_t addr, uint8_t dlc, GmPeriodicSpoofState *state, uint32_t period_us) {
if (!(gm_panda_paddle_sched && state->spoof_valid && (state->next_tx_us != 0U))) {
return;
}
if (!gm_periodic_scheduler_ready(state, now_us, GM_PADDLE_STALE_US, GM_PADDLE_FEED_STALE_US)) {
return;
}
if ((int32_t)(now_us - state->next_tx_us) < 0) {
return;
}
CANPacket_t to_send = {0};
to_send.returned = 0U;
to_send.rejected = 0U;
to_send.extended = 0U;
to_send.addr = addr;
to_send.bus = 0U;
to_send.data_len_code = dlc;
(void)memcpy(to_send.data, state->spoof_data, 8U);
can_set_checksum(&to_send);
gm_paddle_internal_tx = true;
can_send(&to_send, 0U, false);
gm_paddle_internal_tx = false;
state->next_tx_us += period_us;
}
static void gm_try_send_3d1_spoof(uint32_t now_us) {
if (!(gm_panda_3d1_sched && gm_3d1_spoof_valid && (gm_3d1_next_tx_us != 0U))) {
return;
}
if ((int32_t)(now_us - gm_3d1_next_tx_us) < 0) {
return;
}
CANPacket_t to_send = {0};
to_send.returned = 0U;
to_send.rejected = 0U;
to_send.extended = 0U;
to_send.addr = 0x3D1U;
to_send.bus = 0U;
to_send.data_len_code = 8U;
(void)memcpy(to_send.data, gm_3d1_spoof_data, 8U);
can_set_checksum(&to_send);
gm_3d1_internal_tx = true;
can_send(&to_send, 0U, false);
gm_3d1_internal_tx = false;
gm_3d1_next_tx_us += GM_3D1_PERIOD_US;
}
static void gm_rx_hook(const CANPacket_t *to_push) {
if (GET_BUS(to_push) == 0U) {
@@ -263,13 +110,6 @@ static void gm_rx_hook(const CANPacket_t *to_push) {
// ACC steering wheel buttons (GM_CAM is tied to the PCM)
if ((addr == 0x1E1) && (!gm_pcm_cruise || gm_cc_long)) {
int button = (GET_BYTE(to_push, 5) & 0x70U) >> 4;
bool remap_cancel_to_distance = (alternative_experience & ALT_EXP_GM_REMAP_CANCEL_TO_DISTANCE) != 0;
// Malibu Hybrid pedal-long observed format uses byte3 bit0 set (typically 0x01/0x41) on ASCMSteeringButton.
// Don't treat that physical CANCEL frame as OP disengage.
bool malibu_cancel_passthrough = remap_cancel_to_distance && gm_bolt_2022_pedal && gm_pedal_long && ((GET_BYTE(to_push, 3) & 0x1U) != 0U);
// Gen1 Bolt pedal-long behavior: let wheel CANCEL act as an in-drive aux button (personality cycling),
// so safety should not force a controls drop on this RX cancel edge.
bool bolt_cancel_passthrough = remap_cancel_to_distance && gm_pedal_long && !gm_has_acc && !gm_bolt_2022_pedal;
// enter controls on falling edge of set or rising edge of resume (avoids fault)
bool set = (button != GM_BTN_SET) && (cruise_button_prev == GM_BTN_SET);
@@ -279,7 +119,7 @@ static void gm_rx_hook(const CANPacket_t *to_push) {
}
// exit controls on cancel press
if ((button == GM_BTN_CANCEL) && !(malibu_cancel_passthrough || bolt_cancel_passthrough)) {
if (button == GM_BTN_CANCEL) {
controls_allowed = false;
}
@@ -289,6 +129,7 @@ static void gm_rx_hook(const CANPacket_t *to_push) {
// Reference for brake pressed signals:
// https://github.com/commaai/openpilot/blob/master/selfdrive/car/gm/carstate.py
// Prefer 0xC9 (ECMEngineStatus) when gm_force_brake_c9 is set, otherwise keep legacy behavior.
// This allows SDGM/Traverse variants without 0xBE (ECMAcceleratorPos) to report brake correctly.
if ((addr == 0xC9) && gm_force_brake_c9) {
brake_pressed = GET_BIT(to_push, 40U) != 0U;
} else if ((addr == 0xBE) && ((gm_hw == GM_ASCM) || (gm_hw == GM_SDGM))) {
@@ -313,60 +154,18 @@ static void gm_rx_hook(const CANPacket_t *to_push) {
}
}
// Cruise check for ACC models with pedal interceptor - block stock ACC
if ((addr == 0x1C4) && gm_has_acc && enable_gas_interceptor && gm_bolt_2022_pedal) {
cruise_engaged_prev = false;
}
// Cruise check for CC only cars
if ((addr == 0x3D1) && !gm_has_acc) {
uint32_t now_us = microsecond_timer_get();
gm_3d1_last_stock_us = now_us;
bool cruise_engaged = (GET_BYTE(to_push, 4) >> 7) != 0U;
if (gm_cc_long) {
pcm_cruise_check(cruise_engaged);
} else {
cruise_engaged_prev = cruise_engaged;
}
if (gm_panda_3d1_sched) {
if (!gm_3d1_phase_locked) {
gm_3d1_phase_locked = true;
gm_3d1_expected_stock_us = now_us + GM_3D1_PERIOD_US;
} else {
int32_t phase_err_us = (int32_t)(now_us - gm_3d1_expected_stock_us);
if (phase_err_us < 0) {
phase_err_us = -phase_err_us;
}
if ((uint32_t)phase_err_us <= GM_3D1_LOCK_TOLERANCE_US) {
gm_3d1_expected_stock_us += GM_3D1_PERIOD_US;
} else {
gm_3d1_expected_stock_us = now_us + GM_3D1_PERIOD_US;
}
}
gm_3d1_next_tx_us = now_us + GM_3D1_TX_OFFSET_US;
gm_try_send_3d1_spoof(now_us);
}
}
if (addr == 0xBD) {
regen_braking = (GET_BYTE(to_push, 0) >> 4) != 0U;
if (gm_panda_paddle_sched) {
uint32_t now_us = microsecond_timer_get();
gm_update_periodic_phase(&gm_bd_state, now_us, GM_PADDLE_PERIOD_US, GM_PADDLE_LOCK_TOLERANCE_US);
gm_bd_state.next_tx_us = now_us + GM_PADDLE_TX_OFFSET_US;
gm_try_send_periodic_spoof(now_us, 0xBDU, 7U, &gm_bd_state, GM_PADDLE_PERIOD_US);
}
}
if ((addr == 0x1F5) && gm_panda_paddle_sched) {
uint32_t now_us = microsecond_timer_get();
gm_update_periodic_phase(&gm_prndl2_state, now_us, GM_PADDLE_PERIOD_US, GM_PADDLE_LOCK_TOLERANCE_US);
gm_prndl2_state.next_tx_us = now_us + GM_PADDLE_TX_OFFSET_US;
gm_try_send_periodic_spoof(now_us, 0x1F5U, 8U, &gm_prndl2_state, GM_PADDLE_PERIOD_US);
}
// Pedal Interceptor
@@ -385,14 +184,13 @@ static void gm_rx_hook(const CANPacket_t *to_push) {
}
generic_rx_checks(stock_ecu_detected);
}
// Cruise check for ASCMActiveCruiseControlStatus on bus 2.
// Keep kaofui behavior for non-Bolt paths; Bolt pedal path keeps local tracking.
if ((GET_ADDR(to_push) == 0x370) && (GET_BUS(to_push) == 2U)) {
// Cruise check for Gen2 Bolt (ASCMActiveCruiseControlStatus on bus 2)
int addr = GET_ADDR(to_push);
if ((addr == 0x370) && (GET_BUS(to_push) == 2U)) {
bool cruise_engaged = (GET_BYTE(to_push, 2) >> 7) != 0U; // ACCCmdActive
if (gm_bolt_2022_pedal) {
cruise_engaged_prev = cruise_engaged;
} else if (gm_pcm_cruise && gm_has_acc) {
// Align SDGM/camera PCM cruise behavior with ASCM path: when using stock PCM cruise,
// drive controls_allowed via pcm_cruise_check on ACC engaged edges.
if (gm_pcm_cruise && gm_has_acc) {
pcm_cruise_check(cruise_engaged);
} else {
cruise_engaged_prev = cruise_engaged;
@@ -420,7 +218,7 @@ static bool gm_tx_hook(const CANPacket_t *to_send) {
bool steer_req = GET_BIT(to_send, 3U);
if (steer_torque_cmd_checks(desired_torque, steer_req, *gm_steer_limits)) {
if (steer_torque_cmd_checks(desired_torque, steer_req, GM_STEERING_LIMITS)) {
tx = false;
}
}
@@ -452,9 +250,6 @@ static bool gm_tx_hook(const CANPacket_t *to_send) {
int button = (GET_BYTE(to_send, 5) >> 4) & 0x7U;
bool allowed_btn = (button == GM_BTN_CANCEL) && cruise_engaged_prev;
if (gm_hw == GM_CAM && enable_gas_interceptor && gm_bolt_2022_pedal && button == GM_BTN_CANCEL) {
allowed_btn = true;
}
// For CC_LONG or PCM cruise vehicles, allow SET/RESUME when cruise is engaged
if (gm_cc_long || gm_pcm_cruise) {
allowed_btn |= cruise_engaged_prev && (button == GM_BTN_SET || button == GM_BTN_RESUME || button == GM_BTN_UNPRESS);
@@ -464,78 +259,6 @@ static bool gm_tx_hook(const CANPacket_t *to_send) {
tx = false;
}
}
// Cruise status spoofing only for non-ACC (CC-only) paths
if (addr == 0x3D1) {
bool allowed_cruise_status = !gm_has_acc;
if (!allowed_cruise_status) {
tx = false;
} else if (gm_panda_3d1_sched) {
if (gm_3d1_internal_tx) {
tx = true;
} else {
uint32_t now_us = microsecond_timer_get();
(void)memcpy(gm_3d1_spoof_data, to_send->data, 8U);
gm_3d1_spoof_valid = true;
if (gm_3d1_next_tx_us == 0U) {
gm_3d1_next_tx_us = now_us + GM_3D1_TX_OFFSET_US;
}
bool stock_stale = (gm_3d1_last_stock_us == 0U) || (get_ts_elapsed(now_us, gm_3d1_last_stock_us) > 300000U);
bool scheduler_ready = gm_3d1_phase_locked && !stock_stale;
tx = !scheduler_ready;
}
}
}
// REGEN PADDLE
if (addr == 0xBD) {
bool regen_apply = GET_BIT(to_send, 7) || GET_BIT(to_send, 6) || GET_BIT(to_send, 5) || GET_BIT(to_send, 4);
if (!controls_allowed && regen_apply) {
tx = false;
}
if (gm_panda_paddle_sched && !gm_paddle_internal_tx) {
uint32_t now_us = microsecond_timer_get();
if (tx) {
(void)memcpy(gm_bd_state.spoof_data, to_send->data, 7U);
gm_bd_state.spoof_data[7] = 0U;
gm_bd_state.spoof_valid = true;
gm_bd_state.last_feed_us = now_us;
if (gm_bd_state.next_tx_us == 0U) {
gm_bd_state.next_tx_us = now_us + GM_PADDLE_TX_OFFSET_US;
}
}
bool scheduler_ready = gm_periodic_scheduler_ready(&gm_bd_state, now_us, GM_PADDLE_STALE_US, GM_PADDLE_FEED_STALE_US);
if (scheduler_ready) {
tx = false;
}
}
}
// PRNDL2 regen check (7 for Gen0, Gen1. 5 For Gen2)
if (addr == 0x1F5) {
uint8_t prndl2 = GET_BYTE(to_send, 3) & 0xF;
bool prndl_apply = (prndl2 == 7) || (prndl2 == 5);
if (!controls_allowed && prndl_apply) {
tx = false;
}
if (gm_panda_paddle_sched && !gm_paddle_internal_tx) {
uint32_t now_us = microsecond_timer_get();
if (tx) {
(void)memcpy(gm_prndl2_state.spoof_data, to_send->data, 8U);
gm_prndl2_state.spoof_valid = true;
gm_prndl2_state.last_feed_us = now_us;
if (gm_prndl2_state.next_tx_us == 0U) {
gm_prndl2_state.next_tx_us = now_us + GM_PADDLE_TX_OFFSET_US;
}
}
bool scheduler_ready = gm_periodic_scheduler_ready(&gm_prndl2_state, now_us, GM_PADDLE_STALE_US, GM_PADDLE_FEED_STALE_US);
if (scheduler_ready) {
tx = false;
}
}
}
return tx;
}
@@ -546,32 +269,16 @@ static int gm_fwd_hook(int bus_num, int addr) {
if (bus_num == 0) {
// block PSCMStatus; forwarded through openpilot to hide an alert from the camera
bool is_pscm_msg = (addr == 0x184);
// For non-ACC camera/SDGM paths, keep stock ECMCruiseControl off camera side
// so openpilot's spoofed 0x3D1 is the only cruise-status source there.
bool is_ecm_cruise_status_msg = (addr == 0x3D1) && !gm_has_acc;
if (!is_pscm_msg && !is_ecm_cruise_status_msg) {
if (!is_pscm_msg) {
bus_fwd = 2;
}
}
if (bus_num == 2) {
// block lkas message and acc messages if gm_cam_long, forward all others
bool is_lkas_msg = (addr == 0x180);
bool is_acc_status_msg = (addr == 0x370);
bool is_acc_actuation_msg = (addr == 0x315) || (addr == 0x2CB);
// Block steering if we are controlling LKA
bool block_msg = is_lkas_msg;
// Block Dashboard Status if we are in Native Long OR Pedal Long
if (gm_cam_long || gm_pedal_long) {
block_msg |= is_acc_status_msg;
}
// Block Native Actuation ONLY if we are in Native Long (not Pedal)
if (gm_cam_long) {
block_msg |= is_acc_actuation_msg;
}
bool is_acc_msg = (addr == 0x315) || (addr == 0x2CB) || (addr == 0x370);
bool block_msg = is_lkas_msg || (is_acc_msg && gm_cam_long);
if (!block_msg) {
bus_fwd = 0;
}
@@ -582,7 +289,7 @@ static int gm_fwd_hook(int bus_num, int addr) {
}
static safety_config gm_init(uint16_t param) {
gm_ascm_int = GET_FLAG(param, GM_PARAM_ASCM_INT);
const bool gm_ascm_int = GET_FLAG(param, GM_PARAM_ASCM_INT);
if (GET_FLAG(param, GM_PARAM_HW_CAM)) {
gm_hw = GM_CAM;
} else if (GET_FLAG(param, GM_PARAM_HW_SDGM)) {
@@ -592,52 +299,22 @@ static safety_config gm_init(uint16_t param) {
}
gm_force_ascm = GET_FLAG(param, GM_PARAM_HW_ASCM_LONG);
gm_steer_limits = GET_FLAG(param, GM_PARAM_BOLT_2017) ? &GM_BOLT_2017_STEERING_LIMITS : &GM_STEERING_LIMITS;
if (gm_hw == GM_ASCM || gm_force_ascm || gm_ascm_int) {
gm_long_limits = &GM_ASCM_LONG_LIMITS;
gm_long_limits = &GM_ASCM_LONG_LIMITS;
} else if ((gm_hw == GM_CAM) || (gm_hw == GM_SDGM)) {
gm_long_limits = &GM_CAM_LONG_LIMITS;
gm_long_limits = &GM_CAM_LONG_LIMITS;
} else {
}
gm_pedal_long = GET_FLAG(param, GM_PARAM_PEDAL_LONG);
gm_cc_long = GET_FLAG(param, GM_PARAM_CC_LONG);
enable_gas_interceptor = GET_FLAG(param, GM_PARAM_PEDAL_INTERCEPTOR);
gm_cam_long = GET_FLAG(param, GM_PARAM_HW_CAM_LONG) && !gm_cc_long;
gm_bolt_2022_pedal = GET_FLAG(param, GM_PARAM_BOLT_2022_PEDAL);
gm_pcm_cruise = (((gm_hw == GM_CAM) || (gm_hw == GM_SDGM)) && (!gm_cam_long || gm_cc_long) && !gm_force_ascm && !gm_pedal_long);
gm_skip_relay_check = GET_FLAG(param, GM_PARAM_NO_CAMERA);
gm_has_acc = !GET_FLAG(param, GM_PARAM_NO_ACC);
enable_gas_interceptor = GET_FLAG(param, GM_PARAM_PEDAL_INTERCEPTOR);
gm_force_brake_c9 = GET_FLAG(param, GM_PARAM_FORCE_BRAKE_C9);
gm_remote_start_boots_comma = GET_FLAG(param, GM_PARAM_REMOTE_START_BOOTS_COMMA);
gm_panda_3d1_sched = GET_FLAG(param, GM_PARAM_PANDA_3D1_SCHED) && gm_pedal_long && !gm_has_acc && !gm_bolt_2022_pedal;
gm_panda_paddle_sched = GET_FLAG(param, GM_PARAM_PANDA_PADDLE_SCHED) && gm_pedal_long && enable_gas_interceptor;
bool gm_ascm_int_stock_cam = gm_ascm_int && (gm_hw == GM_CAM) && gm_pcm_cruise && !gm_cam_long && !gm_pedal_long && !gm_cc_long;
gm_3d1_spoof_valid = false;
gm_3d1_internal_tx = false;
gm_3d1_next_tx_us = 0U;
gm_3d1_expected_stock_us = 0U;
gm_3d1_last_stock_us = 0U;
gm_3d1_phase_locked = false;
gm_paddle_internal_tx = false;
gm_bd_state.spoof_valid = false;
gm_bd_state.next_tx_us = 0U;
gm_bd_state.expected_stock_us = 0U;
gm_bd_state.last_stock_us = 0U;
gm_bd_state.last_feed_us = 0U;
gm_bd_state.phase_locked = false;
(void)memset(gm_bd_state.spoof_data, 0, sizeof(gm_bd_state.spoof_data));
gm_prndl2_state.spoof_valid = false;
gm_prndl2_state.next_tx_us = 0U;
gm_prndl2_state.expected_stock_us = 0U;
gm_prndl2_state.last_stock_us = 0U;
gm_prndl2_state.last_feed_us = 0U;
gm_prndl2_state.phase_locked = false;
(void)memset(gm_prndl2_state.spoof_data, 0, sizeof(gm_prndl2_state.spoof_data));
safety_config ret = BUILD_SAFETY_CFG(gm_rx_checks, GM_ASCM_TX_MSGS);
if (gm_hw == GM_CAM) {
@@ -649,9 +326,6 @@ static safety_config gm_init(uint16_t param) {
ret = BUILD_SAFETY_CFG(gm_rx_checks, GM_CAM_TX_MSGS);
}
}
if (gm_ascm_int_stock_cam) {
SET_RX_CHECKS(gm_ascm_int_stock_cam_rx_checks, ret);
}
return ret;
}
-3
View File
@@ -275,6 +275,3 @@ const uint32_t RELAY_TRNS_TIMEOUT = 1U;
// Always on Lateral
#define ALT_EXP_ALWAYS_ON_LATERAL 32
// GM pedal-long option: interpret wheel CANCEL as an auxiliary distance-style button.
#define ALT_EXP_GM_REMAP_CANCEL_TO_DISTANCE 64
-7
View File
@@ -112,7 +112,6 @@ class ALTERNATIVE_EXPERIENCE:
RAISE_LONGITUDINAL_LIMITS_TO_ISO_MAX = 8
ALLOW_AEB = 16
ALWAYS_ON_LATERAL = 32
GM_REMAP_CANCEL_TO_DISTANCE = 64
class Panda:
@@ -212,7 +211,6 @@ class Panda:
FLAG_HYUNDAI_ALT_LIMITS = 64
FLAG_HYUNDAI_CANFD_HDA2_ALT_STEERING = 128
FLAG_HYUNDAI_LFA_BTN = 256
FLAG_HYUNDAI_TACO_TUNE_HACK = 512
FLAG_TESLA_POWERTRAIN = 1
FLAG_TESLA_LONG_CONTROL = 2
@@ -241,11 +239,6 @@ class Panda:
FLAG_GM_ASCM_INT = 256
FLAG_GM_FORCE_BRAKE_C9 = 512
FLAG_GM_HW_SDGM = 1024
FLAG_GM_BOLT_2017 = 2048
FLAG_GM_BOLT_2022_PEDAL = 4096
FLAG_GM_REMOTE_START_BOOTS_COMMA = 8192
FLAG_GM_PANDA_3D1_SCHED = 16384
FLAG_GM_PANDA_PADDLE_SCHED = 32768
FLAG_FORD_LONG_CONTROL = 1
FLAG_FORD_CANFD = 2
+5 -139
View File
@@ -11,14 +11,12 @@ from openpilot.selfdrive.car.fingerprints import eliminate_incompatible_cars, al
from openpilot.selfdrive.car.vin import get_vin, is_valid_vin, VIN_UNKNOWN
from openpilot.selfdrive.car.fw_versions import get_fw_versions_ordered, get_present_ecus, match_fw_to_car, set_obd_multiplexing
from openpilot.selfdrive.car.mock.values import CAR as MOCK
from openpilot.selfdrive.car.gm.values import CAR as GM_CAR, CanBus as GMCanBus
from openpilot.common.swaglog import cloudlog
import cereal.messaging as messaging
from openpilot.selfdrive.car import gen_empty_fingerprint
from openpilot.system.version import get_build_metadata
FRAME_FINGERPRINT = 100 # 1s
SOURCE_BRANCH_FILE = "/data/media/0/starpilot_source_branch"
EventName = car.CarEvent.EventName
FrogPilotEventName = custom.FrogPilotCarEvent.EventName
@@ -70,9 +68,8 @@ interface_names = _get_interface_names()
interfaces = load_interfaces(interface_names)
def can_fingerprint(next_can: Callable) -> tuple[str | None, dict[int, dict], dict[int, set[int]]]:
def can_fingerprint(next_can: Callable) -> tuple[str | None, dict[int, dict]]:
finger = gen_empty_fingerprint()
nonzero_addrs = {bus: set() for bus in finger}
candidate_cars = {i: all_legacy_fingerprint_cars() for i in [0, 1]} # attempt fingerprint on both bus 0 and 1
frame = 0
car_fingerprint = None
@@ -87,10 +84,7 @@ def can_fingerprint(next_can: Callable) -> tuple[str | None, dict[int, dict], di
if can.src < 128:
if can.src not in finger:
finger[can.src] = {}
nonzero_addrs[can.src] = set()
finger[can.src][can.address] = len(can.dat)
if any(can.dat):
nonzero_addrs[can.src].add(can.address)
for b in candidate_cars:
# Ignore extended messages and VIN query response.
@@ -111,7 +105,7 @@ def can_fingerprint(next_can: Callable) -> tuple[str | None, dict[int, dict], di
frame += 1
return car_fingerprint, finger, nonzero_addrs
return car_fingerprint, finger
# **** for use live only ****
@@ -168,7 +162,7 @@ def fingerprint(logcan, sendcan, num_pandas):
# CAN fingerprint
# drain CAN socket so we get the latest messages
messaging.drain_sock_raw(logcan)
car_fingerprint, finger, nonzero_addrs = can_fingerprint(lambda: get_one_can(logcan))
car_fingerprint, finger = can_fingerprint(lambda: get_one_can(logcan))
exact_match = True
source = car.CarParams.FingerprintSource.can
@@ -187,74 +181,16 @@ def fingerprint(logcan, sendcan, num_pandas):
fw_count=len(car_fw), ecu_responses=list(ecu_rx_addrs), vin_rx_addr=vin_rx_addr, vin_rx_bus=vin_rx_bus,
fingerprints=repr(finger), fw_query_time=fw_query_time, error=True)
return car_fingerprint, finger, nonzero_addrs, vin, car_fw, source, exact_match
return car_fingerprint, finger, vin, car_fw, source, exact_match
def get_car_interface(CP, FPCP):
CarInterface, CarController, CarState = interfaces[CP.carFingerprint]
return CarInterface(CP, FPCP, CarController, CarState)
def get_cached_car_fingerprint(params: Params) -> str | None:
for key in ("CarParamsPersistent", "CarParamsCache", "CarParams"):
cp_bytes = params.get(key)
if cp_bytes is None:
continue
try:
with car.CarParams.from_bytes(cp_bytes) as cached_cp:
if cached_cp.carFingerprint:
return cached_cp.carFingerprint
except Exception:
continue
return None
def clear_stale_car_params(params: Params, candidate: str) -> None:
cached_fingerprint = get_cached_car_fingerprint(params)
if cached_fingerprint is None or cached_fingerprint == candidate:
return
stale_keys = (
"CarParams",
"CarParamsCache",
"CarParamsPersistent",
"FrogPilotCarParams",
"FrogPilotCarParamsPersistent",
"CarModelName",
)
for key in stale_keys:
params.remove(key)
cloudlog.warning("cleared stale car params after fingerprint change: %s -> %s", cached_fingerprint, candidate)
def migrate_legacy_bolt_candidate(candidate: str) -> str:
source_branch = ""
try:
with open(SOURCE_BRANCH_FILE, encoding="utf-8") as f:
source_branch = f.read().strip()
except OSError:
pass
migration_branch = source_branch or get_build_metadata().channel
replacements = {}
if migration_branch in {"TorqueTune", "TorquePedal"}:
replacements = {
"CHEVROLET_BOLT_EUV": GM_CAR.CHEVROLET_BOLT_ACC_2022_2023,
"CHEVROLET_BOLT_CC": GM_CAR.CHEVROLET_BOLT_CC_2022_2023,
}
elif migration_branch in {"TotallyTune", "StarPilot-2017", "StarPilot 2017"}:
replacements = {
"CHEVROLET_BOLT_CC": GM_CAR.CHEVROLET_BOLT_CC_2017,
}
elif migration_branch in {"StarPilot"}:
replacements = {
"CHEVROLET_BOLT_CC": GM_CAR.CHEVROLET_BOLT_CC_2019_2021,
}
normalized_candidate = candidate[4:] if candidate.startswith("CAR.") else candidate
return replacements.get(normalized_candidate, normalized_candidate)
def get_car(logcan, sendcan, experimental_long_allowed, params, num_pandas=1, frogpilot_toggles=None):
candidate, fingerprints, nonzero_addrs, vin, car_fw, source, exact_match = fingerprint(logcan, sendcan, num_pandas)
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(logcan, sendcan, num_pandas)
if candidate is None or frogpilot_toggles.force_fingerprint:
if frogpilot_toggles.car_model is not None:
@@ -266,80 +202,10 @@ def get_car(logcan, sendcan, experimental_long_allowed, params, num_pandas=1, fr
params.put_nonblocking("CarMake", candidate.split('_')[0].title())
params.put_nonblocking("CarModel", candidate)
# Branch migration can leave legacy Bolt candidate names active in params/cache.
# Remap the selected candidate itself so fingerprint selection and params stay in sync.
migrated_candidate = migrate_legacy_bolt_candidate(candidate)
if candidate != migrated_candidate:
cloudlog.warning("legacy Bolt candidate migration: %s -> %s", candidate, migrated_candidate)
candidate = migrated_candidate
params.put_nonblocking("CarMake", candidate.split('_')[0].title())
params.put_nonblocking("CarModel", candidate)
params.remove("CarModelName")
# VIN-based Bolt year mapping (selfdrive-only, bolt variants only)
if not frogpilot_toggles.force_fingerprint and is_valid_vin(vin):
bolt_variants = {
"CHEVROLET_BOLT_EUV",
"CHEVROLET_BOLT_CC",
"CAR.CHEVROLET_BOLT_EUV",
"CAR.CHEVROLET_BOLT_CC",
GM_CAR.CHEVROLET_BOLT_ACC_2022_2023,
GM_CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL,
GM_CAR.CHEVROLET_BOLT_CC_2022_2023,
GM_CAR.CHEVROLET_BOLT_CC_2019_2021,
GM_CAR.CHEVROLET_BOLT_CC_2017,
}
if candidate in bolt_variants:
year_code = vin[9:10]
year_map = {
"H": GM_CAR.CHEVROLET_BOLT_CC_2017, # 2017
"J": GM_CAR.CHEVROLET_BOLT_CC_2019_2021, # 2018
"K": GM_CAR.CHEVROLET_BOLT_CC_2019_2021, # 2019
"L": GM_CAR.CHEVROLET_BOLT_CC_2019_2021, # 2020
"M": GM_CAR.CHEVROLET_BOLT_CC_2019_2021, # 2021
"N": GM_CAR.CHEVROLET_BOLT_ACC_2022_2023, # 2022
"P": GM_CAR.CHEVROLET_BOLT_ACC_2022_2023, # 2023
}
if year_code in year_map:
vin_candidate = year_map[year_code]
if vin_candidate == GM_CAR.CHEVROLET_BOLT_ACC_2022_2023:
has_acc_data = (
0x370 in nonzero_addrs.get(GMCanBus.CAMERA, set()) or
0x370 in nonzero_addrs.get(GMCanBus.POWERTRAIN, set())
)
has_pedal_msg = 0x201 in fingerprints.get(GMCanBus.POWERTRAIN, {})
if has_acc_data:
vin_candidate = GM_CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL if has_pedal_msg else GM_CAR.CHEVROLET_BOLT_ACC_2022_2023
else:
vin_candidate = GM_CAR.CHEVROLET_BOLT_CC_2022_2023
if candidate != vin_candidate:
prev_candidate = candidate
candidate = vin_candidate
params.put_nonblocking("CarMake", candidate.split('_')[0].title())
params.put_nonblocking("CarModel", candidate)
params.remove("CarModelName")
cloudlog.warning("VIN Bolt override: %s -> %s", prev_candidate, candidate)
# Always prefer live fingerprint naming for Bolt variants to avoid stale manual labels.
if candidate in {
GM_CAR.CHEVROLET_BOLT_ACC_2022_2023,
GM_CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL,
GM_CAR.CHEVROLET_BOLT_CC_2022_2023,
GM_CAR.CHEVROLET_BOLT_CC_2019_2021,
GM_CAR.CHEVROLET_BOLT_CC_2017,
"CHEVROLET_BOLT_EUV",
"CHEVROLET_BOLT_CC",
"CAR.CHEVROLET_BOLT_EUV",
"CAR.CHEVROLET_BOLT_CC",
}:
params.remove("CarModelName")
if frogpilot_toggles.block_user:
candidate = MOCK.MOCK
sentry.capture_block()
clear_stale_car_params(params, candidate)
CarInterface, _, _ = interfaces[candidate]
CP = CarInterface.get_params(candidate, fingerprints, car_fw, experimental_long_allowed, frogpilot_toggles, docs=False)
FPCP = CarInterface.get_frogpilot_params(candidate, fingerprints, car_fw, CP, frogpilot_toggles)
+2 -3
View File
@@ -56,9 +56,8 @@ class Car:
# set alternative experiences from parameters
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
if self.disengage_on_accelerator:
self.CP.alternativeExperience &= ~ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS
else:
self.CP.alternativeExperience = 0
if not self.disengage_on_accelerator:
self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS
openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle")
+1 -1
View File
@@ -158,7 +158,7 @@ MIGRATION = {
"CADILLAC ESCALADE 2017": GM.CADILLAC_ESCALADE,
"CADILLAC ESCALADE ESV 2016": GM.CADILLAC_ESCALADE_ESV,
"CADILLAC ESCALADE ESV 2019": GM.CADILLAC_ESCALADE_ESV_2019,
"CHEVROLET BOLT EUV 2022": GM.CHEVROLET_BOLT_ACC_2022_2023,
"CHEVROLET BOLT EUV 2022": GM.CHEVROLET_BOLT_EUV,
"CHEVROLET SILVERADO 1500 2020": GM.CHEVROLET_SILVERADO,
"CHEVROLET EQUINOX 2019": GM.CHEVROLET_EQUINOX,
"CHEVROLET TRAILBLAZER 2021": GM.CHEVROLET_TRAILBLAZER,
+79 -340
View File
@@ -1,7 +1,5 @@
from typing import Tuple
import time
import math
from openpilot.common.swaglog import cloudlog
from cereal import car
from openpilot.common.conversions import Conversions as CV
from openpilot.common.filter_simple import FirstOrderFilter
@@ -11,11 +9,10 @@ from openpilot.common.params_pyx import Params
from opendbc.can.packer import CANPacker
from openpilot.selfdrive.car import apply_driver_steer_torque_limits, create_gas_interceptor_command
from openpilot.selfdrive.car.gm import gmcan
from openpilot.selfdrive.car.gm.values import CAR, DBC, AccState, CanBus, CarControllerParams, CruiseButtons, GMFlags, CC_ONLY_CAR, SDGM_CAR, ASCM_INT, EV_CAR, CC_REGEN_PADDLE_CAR
from openpilot.selfdrive.car.gm.values import DBC, AccState, CanBus, CarControllerParams, CruiseButtons, GMFlags, CC_ONLY_CAR, SDGM_CAR, EV_CAR, CAR
from openpilot.selfdrive.car.interfaces import CarControllerBase
from openpilot.selfdrive.controls.lib.drive_helpers import apply_deadzone
from openpilot.selfdrive.controls.lib.vehicle_model import ACCELERATION_DUE_TO_GRAVITY
from openpilot.frogpilot.common.testing_grounds import testing_ground
VisualAlert = car.CarControl.HUDControl.VisualAlert
NetworkLocation = car.CarParams.NetworkLocation
@@ -27,6 +24,7 @@ TransmissionType = car.CarParams.TransmissionType
CAMERA_CANCEL_DELAY_FRAMES = 10
# Enforce a minimum interval between steering messages to avoid a fault
MIN_STEER_MSG_INTERVAL_MS = 15
# Constants for pitch compensation
BRAKE_PITCH_FACTOR_BP = [5., 10.] # [m/s] smoothly revert to planned accel at low speeds
BRAKE_PITCH_FACTOR_V = [0., 1.] # [unitless in [0,1]]; don't touch
@@ -42,7 +40,6 @@ class CarController(CarControllerBase):
self.apply_speed = 0
self.frame = 0
self.last_steer_frame = 0
self.last_steer_ts_ns = 0
self.last_button_frame = 0
self.cancel_counter = 0
self.pedal_steady = 0.
@@ -53,22 +50,16 @@ class CarController(CarControllerBase):
self.params = CarControllerParams(self.CP)
self.is_volt = self.CP.carFingerprint in (CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_2019, CAR.CHEVROLET_VOLT_ASCM, CAR.CHEVROLET_VOLT_CAMERA, CAR.CHEVROLET_VOLT_CC)
self.pedal_scale = 1.0
self.params_ = Params()
self.mass = CP.mass
self.tireRadius = 0.075 * CP.wheelbase + 0.1453
self.frontalArea = 1.05 * CP.wheelbase + 0.0679
self.coeffDrag = 0.30
self.airDensity = 1.225
self.params_ = Params()
self.malibu_cancel_phase = 0
self.malibu_cancel_last_ts = 0.0
self.malibu_cancel_frame = 0
self.malibu_cancel_frame_offset = 0
self.malibu_button_phase = 0
self.malibu_last_button_ts_nanos = 0
self.packer_pt = CANPacker(DBC[self.CP.carFingerprint]['pt'])
self.packer_obj = CANPacker(DBC[self.CP.carFingerprint]['radar'])
@@ -78,254 +69,41 @@ class CarController(CarControllerBase):
self.accel_g = 0.0
self.pitch = FirstOrderFilter(0., 0.09 * 4, DT_CTRL * 4) # runs at 25 Hz
self.accel_g = 0.0
self.regen_paddle_pressed = False
self.aego = 0.0
self.regen_paddle_timer = 0
self.regen_press_counter = 0
self.regen_release_counter = 0
self.regen_min_on_frames = 0
self.regen_min_off_frames = 0
self.planner_regen_hold = False
self.paddle_handoff_frames = 0
self.pedal_active_last = False
self.maneuver_paddle_mode = "auto"
def calc_pedal_command(self, accel: float, long_active: bool, car_velocity) -> Tuple[float, bool]:
if not long_active:
self.planner_regen_hold = False
self.regen_paddle_pressed = False
self.regen_paddle_timer = 0
self.regen_press_counter = 0
self.regen_release_counter = 0
self.regen_min_on_frames = 0
self.regen_min_off_frames = 0
self.pedal_active_last = False
self.pedal_steady = 0.0
return 0., False
@staticmethod
def calc_pedal_command(accel: float, long_active: bool) -> float:
if not long_active: return 0.
supports_regen_paddle = self.CP.carFingerprint in CC_REGEN_PADDLE_CAR
switched_state = False
press_regen_paddle = False
if supports_regen_paddle:
# Regen paddle state machine: speed-aware thresholds + min on/off duration for robust paddle transitions.
press_cmd_threshold = interp(car_velocity, [0.0, 4.0, 12.0, 25.0], [-0.90, -0.82, -0.72, -0.65])
release_cmd_threshold = interp(car_velocity, [0.0, 4.0, 12.0, 25.0], [-0.10, -0.17, -0.24, -0.30])
press_aego_threshold = interp(car_velocity, [0.0, 4.0, 12.0, 25.0], [-0.95, -0.86, -0.76, -0.70])
release_aego_threshold = interp(car_velocity, [0.0, 4.0, 12.0, 25.0], [-0.16, -0.23, -0.30, -0.36])
press_confirm_frames = int(round(interp(car_velocity, [0.0, 4.0, 12.0, 25.0], [8.0, 6.0, 5.0, 4.0])))
release_confirm_frames = int(round(interp(car_velocity, [0.0, 4.0, 12.0, 25.0], [18.0, 15.0, 12.0, 10.0])))
min_on_frames = int(round(interp(car_velocity, [0.0, 4.0, 12.0, 25.0], [34.0, 27.0, 20.0, 16.0])))
min_off_frames = int(round(interp(car_velocity, [0.0, 4.0, 12.0, 25.0], [16.0, 14.0, 12.0, 10.0])))
# Extra hysteresis in the 8-20 mph band to reduce paddle edge pulsing.
confirm_boost = int(round(interp(car_velocity, [0.0, 6.0, 8.0, 20.0, 25.0], [0.0, 0.0, 3.0, 3.0, 0.0])))
press_confirm_frames += confirm_boost
release_confirm_frames += confirm_boost
want_press = self.planner_regen_hold or accel <= press_cmd_threshold or self.aego <= press_aego_threshold
want_release = (not self.planner_regen_hold) and accel >= release_cmd_threshold and self.aego >= release_aego_threshold
if want_press:
self.regen_press_counter += 1
else:
self.regen_press_counter = max(self.regen_press_counter - 1, 0)
if want_release:
self.regen_release_counter += 1
else:
self.regen_release_counter = max(self.regen_release_counter - 1, 0)
# Strong planner request can skip most of debounce delay.
if self.planner_regen_hold and accel <= (press_cmd_threshold - 0.30):
self.regen_press_counter = max(self.regen_press_counter, press_confirm_frames)
if self.regen_min_on_frames > 0:
self.regen_min_on_frames -= 1
if self.regen_min_off_frames > 0:
self.regen_min_off_frames -= 1
if self.regen_paddle_pressed:
if self.regen_min_on_frames == 0 and self.regen_release_counter >= release_confirm_frames:
self.regen_paddle_pressed = False
self.regen_min_off_frames = min_off_frames
self.regen_release_counter = 0
switched_state = True
else:
if self.regen_min_off_frames == 0 and self.regen_press_counter >= press_confirm_frames:
self.regen_paddle_pressed = True
self.regen_min_on_frames = min_on_frames
self.regen_press_counter = 0
switched_state = True
self.regen_paddle_timer = self.regen_press_counter
press_regen_paddle = self.regen_paddle_pressed
if self.maneuver_paddle_mode == "off":
self.regen_paddle_pressed = False
self.regen_press_counter = 0
self.regen_release_counter = 0
self.regen_min_on_frames = 0
press_regen_paddle = False
elif self.maneuver_paddle_mode == "force":
forced_press = accel < -0.02
self.regen_paddle_pressed = forced_press
press_regen_paddle = forced_press
zero = 0.15625 # 40/256
if accel > 0.:
# Scales the accel from 0-1 to 0.156-1
pedal_gas = clip(((1 - zero) * accel + zero), 0., 1.)
else:
self.planner_regen_hold = False
self.regen_paddle_pressed = False
self.regen_paddle_timer = 0
self.regen_press_counter = 0
self.regen_release_counter = 0
self.regen_min_on_frames = 0
self.regen_min_off_frames = 0
# if accel is negative, -0.1 -> 0.015625
pedal_gas = clip(zero + accel, 0., zero) # Make brake the same size as gas, but clip to regen
# Regen gain ratios from bin-averaged 600 deceleration sweep; Calculates stronger decel from paddle
speed_mps = [0.559, 1.678, 2.797, 3.916, 5.035, 6.154, 7.273, 8.392, 9.511, 10.63,
11.749, 12.868, 13.987, 15.106, 16.225, 17.344, 18.463, 19.582, 20.701, 21.820,
22.939, 24.058, 25.177, 26.296]
regen_gain_ratio = [1.01, 1.01, 1.02, 1.05, 1.08, 1.31, 1.33,
1.34, 1.35, 1.36, 1.37, 1.38, 1.39, 1.39,
1.40, 1.40, 1.41, 1.42, 1.43, 1.43, 1.44,
1.44, 1.45, 1.45]
return pedal_gas
gain = interp(car_velocity, speed_mps, regen_gain_ratio)
# Terminal-stop authority boost for paddle+pedal mode: lower gain at low speed => stronger net decel.
gain *= interp(car_velocity, [0.0, 2.0, 4.0, 5.5, 8.0, 12.0], [0.92, 0.92, 0.93, 0.94, 0.96, 1.0])
accel_gain = interp(car_velocity, [0.0, 3.0, 8.0, 20.0], [0.47, 0.52, 0.57, 0.61])
pedaloffset = interp(car_velocity, [0.0, 1.0, 3.0, 6.0, 15.0, 30.0], [0.085, 0.11, 0.17, 0.23, 0.235, 0.23])
accel_term_scale = (1.0 / max(gain, 1e-3)) if press_regen_paddle else 1.0
# De-sensitize small commands asymmetrically: keep decel smoother while preserving accel pickup.
if accel >= 0.0:
small_cmd_scale = interp(abs(accel), [0.0, 0.35, 0.8, 1.5, 2.5], [0.58, 0.68, 0.82, 0.93, 1.0])
else:
small_cmd_scale = interp(abs(accel), [0.0, 0.35, 0.8, 1.5, 2.5], [0.44, 0.54, 0.70, 0.89, 1.0])
accel_cmd = accel * small_cmd_scale
if (not press_regen_paddle) and accel < -2.0:
accel_cmd *= interp(abs(accel), [2.0, 2.5, 3.0], [1.0, 1.03, 1.06])
raw_pedal_gas = clip(pedaloffset + accel_cmd * accel_gain * accel_term_scale, 0.0, 1.0)
# Safety cap with continuous low-speed transition (removes the near-step around ~4.5 m/s).
pedal_gas_max = interp(car_velocity, [0.0, 1.0, 2.5, 4.5, 6.0, 8.0, 12.0], [0.20, 0.235, 0.29, 0.365, 0.52, 0.78, 1.0])
target_pedal_gas = clip(raw_pedal_gas, 0.0, pedal_gas_max)
# Blend and rate-limit command changes for smoother transients while preserving clip bounds.
if not self.pedal_active_last or switched_state:
pedal_gas = target_pedal_gas
self.pedal_active_last = True
else:
urgency = clip(abs(accel) / 2.0, 0.0, 1.0)
rate_up = interp(car_velocity, [0.0, 3.0, 8.0, 20.0], [0.007, 0.012, 0.022, 0.036]) + 0.011 * urgency
if accel > 1.2:
# Recover high-command launch response without changing low-command smoothness.
rate_up += interp(car_velocity, [0.0, 4.0, 12.0, 25.0], [0.006, 0.005, 0.003, 0.002])
rate_down = interp(car_velocity, [0.0, 3.0, 8.0, 20.0], [0.008, 0.014, 0.026, 0.045]) + 0.015 * urgency
pedal_gas = clip(target_pedal_gas, self.pedal_steady - rate_down, self.pedal_steady + rate_up)
self.pedal_steady = pedal_gas
return pedal_gas, press_regen_paddle
def _update_malibu_button_slot(self, cs):
button_ts = cs.steering_button_ts_nanos
if button_ts != 0 and button_ts != self.malibu_last_button_ts_nanos:
self.malibu_last_button_ts_nanos = button_ts
return True
return False
def _sync_malibu_phase_from_oem(self, cs):
phase_map = gmcan.malibu_phase_map_for_acc(cs.cruise_buttons)
if phase_map and cs.steering_button_checksum in phase_map:
phase = phase_map[cs.steering_button_checksum]
self.malibu_cancel_phase = phase
self.malibu_button_phase = phase
def update(self, CC, CS, now_nanos, frogpilot_toggles):
self.CS = CS
self.aego = CS.out.aEgo
actuators = CC.actuators
accel = brake_accel = actuators.accel
press_regen_paddle = False
# Longitudinal maneuvers can force paddle behavior for A/B testing.
if self.frame % 25 == 0:
mode = self.params_.get("LongitudinalManeuverPaddleMode", encoding="utf-8")
mode = (mode or "auto").strip().lower()
self.maneuver_paddle_mode = mode if mode in ("auto", "off", "force") else "auto"
kaofui_cars = SDGM_CAR | ASCM_INT | {
CAR.CHEVROLET_VOLT,
CAR.CHEVROLET_VOLT_2019,
CAR.CHEVROLET_VOLT_ASCM,
CAR.CHEVROLET_VOLT_CAMERA,
CAR.CHEVROLET_VOLT_CC,
CAR.CHEVROLET_MALIBU_CC,
CAR.CHEVROLET_MALIBU_HYBRID_CC,
}
volt_like = {
CAR.CHEVROLET_VOLT,
CAR.CHEVROLET_VOLT_2019,
CAR.CHEVROLET_VOLT_ASCM,
CAR.CHEVROLET_VOLT_CAMERA,
CAR.CHEVROLET_VOLT_CC,
}
# Planner-driven regen hold: gate by car support and OP long active, use commanded accel thresholds
if (self.CP.enableGasInterceptor and self.CP.carFingerprint in CC_REGEN_PADDLE_CAR
and self.CP.openpilotLongitudinalControl and CC.longActive):
if self.maneuver_paddle_mode == "off":
self.planner_regen_hold = False
elif self.maneuver_paddle_mode == "force":
self.planner_regen_hold = accel < -0.02
else:
# Pre-arm paddle on strong commanded decel with speed-aware hysteresis.
planner_press_threshold = interp(CS.out.vEgo, [0.0, 4.0, 12.0, 25.0], [-0.95, -0.82, -0.70, -0.62])
planner_release_threshold = interp(CS.out.vEgo, [0.0, 4.0, 12.0, 25.0], [-0.14, -0.22, -0.30, -0.36])
if accel <= planner_press_threshold:
self.planner_regen_hold = True
elif accel >= planner_release_threshold:
self.planner_regen_hold = False
else:
self.planner_regen_hold = False
hud_control = CC.hudControl
hud_alert = hud_control.visualAlert
hud_v_cruise = hud_control.setSpeed
if hud_v_cruise > 70:
hud_v_cruise = 0
now_sec = now_nanos * 1e-9
# Send CAN commands.
can_sends = []
malibu_oem_button_slot = False
if self.CP.carFingerprint == CAR.CHEVROLET_MALIBU_HYBRID_CC:
malibu_oem_button_slot = self._update_malibu_button_slot(CS)
if malibu_oem_button_slot:
self._sync_malibu_phase_from_oem(CS)
raw_regen_active = (
self.CP.carFingerprint in CC_REGEN_PADDLE_CAR and
self.CP.openpilotLongitudinalControl and
CC.longActive and
self.CP.enableGasInterceptor and
self.regen_paddle_pressed
)
use_panda_paddle_sched = (
self.CP.enableGasInterceptor and
self.CP.carFingerprint in CC_REGEN_PADDLE_CAR and
self.CP.openpilotLongitudinalControl and
bool(self.CP.flags & GMFlags.PEDAL_LONG.value)
)
if use_panda_paddle_sched:
if CC.enabled:
self.paddle_handoff_frames = 2
paddle_sched_feed_active = CC.enabled or (self.paddle_handoff_frames > 0)
if not CC.enabled and self.paddle_handoff_frames > 0:
self.paddle_handoff_frames -= 1
else:
self.paddle_handoff_frames = 0
paddle_sched_feed_active = False
# Preserve existing on-send gate: only send "pressed" while above low-speed threshold.
paddle_spoof_pressed = raw_regen_active and (CS.out.vEgo > 2.68)
phase_map = gmcan.malibu_phase_map_for_acc(CS.cruise_buttons)
if phase_map and CS.steering_button_checksum in phase_map:
phase = (phase_map[CS.steering_button_checksum] + 1) % 4
self.malibu_cancel_phase = phase
self.malibu_button_phase = phase
# Steering (Active: 50Hz, inactive: 10Hz)
steer_step = self.params.STEER_STEP if CC.latActive else self.params.INACTIVE_STEER_STEP
@@ -358,33 +136,24 @@ class CarController(CarControllerBase):
if (self.CP.flags & GMFlags.CC_LONG.value) and CC.enabled and not CS.out.cruiseState.enabled: # Send 0 so Panda doesn't error
apply_steer = 0
self.last_steer_ts_ns = now_nanos
self.last_steer_frame = self.frame
self.apply_steer_last = apply_steer
idx = self.lka_steering_cmd_counter % 4
can_sends.append(gmcan.create_steering_control(self.packer_pt, CanBus.POWERTRAIN, apply_steer, idx, CC.latActive))
spoof_ecm_cruise_cars = {
CAR.CHEVROLET_BOLT_CC_2017,
CAR.CHEVROLET_BOLT_CC_2019_2021,
CAR.CHEVROLET_BOLT_CC_2022_2023,
CAR.CHEVROLET_MALIBU_HYBRID_CC,
}
non_acc_pedal_long = (self.CP.flags & GMFlags.PEDAL_LONG.value) and self.CP.carFingerprint in spoof_ecm_cruise_cars and self.CP.enableGasInterceptor
if non_acc_pedal_long and self.frame % 4 == 0:
spoof_enabled = True
spoof_set_speed_kph = hud_v_cruise * CV.MS_TO_KPH
can_sends.append(gmcan.create_ecm_cruise_control_command(
self.packer_pt, CanBus.POWERTRAIN, spoof_enabled, spoof_set_speed_kph))
malibu_cancel_requested = False
if self.CP.openpilotLongitudinalControl:
# Gas/regen, brakes, and UI commands - all at 25Hz
if self.frame % 4 == 0:
stopping = actuators.longControlState == LongCtrlState.stopping
# Pitch compensated acceleration;
# TODO: include future pitch (sm['modelDataV2'].orientation.y) to account for long actuator delay
if frogpilot_toggles.long_pitch and len(CC.orientationNED) > 1 and not self.is_volt:
self.pitch.update(CC.orientationNED[1])
self.accel_g = ACCELERATION_DUE_TO_GRAVITY * apply_deadzone(self.pitch.x, PITCH_DEADZONE) # driving uphill is positive pitch
accel += self.accel_g
brake_accel = actuators.accel + self.accel_g * interp(CS.out.vEgo, BRAKE_PITCH_FACTOR_BP, BRAKE_PITCH_FACTOR_V)
at_full_stop = CC.longActive and CS.out.standstill
near_stop = CC.longActive and (CS.out.vEgo < self.params.NEAR_STOP_BRAKE_PHASE)
interceptor_gas_cmd = 0
@@ -396,6 +165,7 @@ class CarController(CarControllerBase):
self.apply_gas = self.params.INACTIVE_REGEN
self.apply_brake = int(min(-100 * frogpilot_toggles.stopAccel, self.params.MAX_BRAKE))
else:
# Normal operation
if self.is_volt:
if len(CC.orientationNED) == 3 and CS.out.vEgo > self.CP.vEgoStopping:
volt_pitch_accel = math.sin(CC.orientationNED[1]) * ACCELERATION_DUE_TO_GRAVITY
@@ -431,31 +201,31 @@ class CarController(CarControllerBase):
gas_max = self.params.MAX_GAS
accel_max = self.params.ACCEL_MAX
if testing_ground.use_1:
accel_max = min(accel_max, interp(CS.out.vEgo, [0.0, 4.0, 12.0], [1.25, 1.6, self.params.ACCEL_MAX]))
accel = clip(actuators.accel + accel_due_to_pitch, self.params.ACCEL_MIN, accel_max)
torque = self.tireRadius * ((self.mass*accel) + (0.5*self.coeffDrag*self.frontalArea*self.airDensity*CS.out.vEgo**2))
torque = self.tireRadius * ((self.mass * accel) + (0.5 * self.coeffDrag * self.frontalArea * self.airDensity * CS.out.vEgo ** 2))
scaled_torque = torque + self.params.ZERO_GAS
apply_gas_torque = clip(scaled_torque, self.params.MAX_ACC_REGEN, gas_max)
BRAKE_SWITCH = int(round(interp(CS.out.vEgo, self.params.BRAKE_SWITCH_LOOKUP_BP, self.params.BRAKE_SWITCH_LOOKUP_V)))
if testing_ground.use_1:
brake_switch_bias = int(round(interp(CS.out.vEgo, [0.0, 6.0, 15.0, 30.0], [60.0, 120.0, 180.0, 220.0])))
BRAKE_SWITCH = min(self.params.ZERO_GAS, BRAKE_SWITCH + brake_switch_bias)
brake_accel = min((scaled_torque - BRAKE_SWITCH)/(self.tireRadius*self.mass), 0)
brake_accel = min((scaled_torque - BRAKE_SWITCH) / (self.tireRadius * self.mass), 0)
self.apply_gas = int(round(apply_gas_torque))
self.apply_brake = int(round(interp(brake_accel, self.params.BRAKE_LOOKUP_BP, self.params.BRAKE_LOOKUP_V)))
if self.apply_brake > 0:
self.apply_gas = self.params.INACTIVE_REGEN
# Clamp within message-valid ranges to avoid ASCM faults from overshoot or rounding
self.apply_gas = int(round(clip(self.apply_gas, self.params.MAX_ACC_REGEN, self.params.MAX_GAS)))
self.apply_brake = int(round(clip(self.apply_brake, 0, self.params.MAX_BRAKE)))
if self.apply_brake > 0 and self.apply_gas > self.params.INACTIVE_REGEN:
self.apply_gas = self.params.INACTIVE_REGEN
# Don't allow any gas above inactive regen while stopping
# FIXME: brakes aren't applied immediately when enabling at a stop
if stopping:
self.apply_gas = self.params.INACTIVE_REGEN
if self.CP.carFingerprint in CC_ONLY_CAR:
# gas interceptor only used for full long control on cars without ACC
interceptor_gas_cmd, press_regen_paddle = self.calc_pedal_command(actuators.accel, CC.longActive, CS.out.vEgo)
interceptor_gas_cmd = self.calc_pedal_command(actuators.accel, CC.longActive)
interceptor_gas_cmd = clip(interceptor_gas_cmd * self.pedal_scale, 0., 1.)
if self.CP.enableGasInterceptor and self.apply_gas > self.params.INACTIVE_REGEN and CS.out.cruiseState.standstill:
# "Tap" the accelerator pedal to re-engage ACC
@@ -465,12 +235,8 @@ class CarController(CarControllerBase):
idx = (self.frame // 4) % 4
if paddle_sched_feed_active:
can_sends.append(gmcan.create_prndl2_command(self.packer_pt, CanBus.POWERTRAIN, paddle_spoof_pressed, self.CP))
can_sends.append(gmcan.create_regen_paddle_command(self.packer_pt, CanBus.POWERTRAIN, paddle_spoof_pressed))
if self.CP.flags & GMFlags.CC_LONG.value:
if CC.longActive and CS.out.cruiseState.enabled and CS.out.vEgo > self.CP.minEnableSpeed:
if CC.longActive and CS.out.vEgo > self.CP.minEnableSpeed:
# Using extend instead of append since the message is only sent intermittently
can_sends.extend(gmcan.create_gm_cc_spam_command(self.packer_pt, self, CS, actuators, frogpilot_toggles))
elif (CS.out.cruiseState.enabled and CC.enabled and self.frame % 52 == 0 and
@@ -488,7 +254,7 @@ class CarController(CarControllerBase):
friction_brake_bus = CanBus.CHASSIS
# GM Camera exceptions
# TODO: can we always check the longControlState?
if self.CP.networkLocation == NetworkLocation.fwdCamera:
if self.CP.networkLocation == NetworkLocation.fwdCamera and self.CP.carFingerprint not in CC_ONLY_CAR:
at_full_stop = at_full_stop and stopping
friction_brake_bus = CanBus.POWERTRAIN
if self.CP.carFingerprint in SDGM_CAR:
@@ -504,74 +270,53 @@ class CarController(CarControllerBase):
acc_engaged = CC.enabled
# GasRegenCmdActive needs to be 1 to avoid cruise faults. It describes the ACC state, not actuation
can_sends.append(gmcan.create_gas_regen_command(self.packer_pt, CanBus.POWERTRAIN, self.apply_gas, idx, acc_engaged, at_full_stop,
include_always_one3=self.CP.carFingerprint in kaofui_cars))
can_sends.append(gmcan.create_gas_regen_command(self.packer_pt, CanBus.POWERTRAIN, self.apply_gas, idx, acc_engaged, at_full_stop))
can_sends.append(gmcan.create_friction_brake_command(self.packer_ch, friction_brake_bus, self.apply_brake,
idx, CC.enabled, near_stop, at_full_stop, self.CP))
idx, CC.enabled, near_stop, at_full_stop, self.CP))
is_bolt_acc_pedal = self.CP.carFingerprint == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL
if self.CP.carFingerprint not in CC_ONLY_CAR or is_bolt_acc_pedal:
# Send dashboard UI commands (ACC status)
send_fcw = hud_alert == VisualAlert.fcw
can_sends.append(gmcan.create_acc_dashboard_command(
self.packer_pt, CanBus.POWERTRAIN, CC.enabled, hud_v_cruise * CV.MS_TO_KPH, hud_control, send_fcw))
can_sends.append(gmcan.create_acc_dashboard_command(self.packer_pt, CanBus.POWERTRAIN, CC.enabled,
hud_v_cruise * CV.MS_TO_KPH, hud_control, send_fcw))
else:
# to keep accel steady for logs when not sending gas
accel += self.accel_g
# Radar needs to know current speed and yaw rate (50hz),
# and that ADAS is alive (5hz, previously 10hz)
if not self.CP.radarUnavailable:
send_adas = True
if self.CP.carFingerprint in kaofui_cars:
if self.CP.carFingerprint in ASCM_INT:
send_adas = True
else:
send_adas = (self.CP.networkLocation != NetworkLocation.fwdCamera) and (self.CP.carFingerprint not in SDGM_CAR)
# and that ADAS is alive (10hz)
if not self.CP.radarUnavailable and self.CP.networkLocation != NetworkLocation.fwdCamera and self.CP.carFingerprint not in SDGM_CAR:
tt = self.frame * DT_CTRL
time_and_headlights_step = 10
if self.frame % time_and_headlights_step == 0:
idx = (self.frame // time_and_headlights_step) % 4
can_sends.append(gmcan.create_adas_time_status(CanBus.OBSTACLE, int((tt - self.start_time) * 60), idx))
can_sends.append(gmcan.create_adas_headlights_status(self.packer_obj, CanBus.OBSTACLE))
if send_adas:
tt = self.frame * DT_CTRL
if self.CP.carFingerprint in kaofui_cars:
time_and_headlights_step = 10
speed_and_accelerometer_step = 2
if self.frame % time_and_headlights_step == 0:
idx = (self.frame // time_and_headlights_step) % 4
can_sends.append(gmcan.create_adas_time_status(CanBus.OBSTACLE, int((tt - self.start_time) * 60), idx))
can_sends.append(gmcan.create_adas_headlights_status(self.packer_obj, CanBus.OBSTACLE))
if self.frame % speed_and_accelerometer_step == 0:
idx = (self.frame // speed_and_accelerometer_step) % 4
can_sends.append(gmcan.create_adas_steering_status(CanBus.OBSTACLE, idx))
can_sends.append(gmcan.create_adas_accelerometer_speed_status(CanBus.OBSTACLE, CS.out.vEgo, idx))
else:
time_and_headlights_step = 20
if self.frame % time_and_headlights_step == 0:
idx = (self.frame // time_and_headlights_step) % 4
can_sends.append(gmcan.create_adas_time_status(CanBus.OBSTACLE, int((tt - self.start_time) * 60), idx))
can_sends.append(gmcan.create_adas_headlights_status(self.packer_obj, CanBus.OBSTACLE))
can_sends.append(gmcan.create_adas_steering_status(CanBus.OBSTACLE, idx))
can_sends.append(gmcan.create_adas_accelerometer_speed_status(CanBus.OBSTACLE, CS.out.vEgo, idx))
speed_and_accelerometer_step = 2
if self.frame % speed_and_accelerometer_step == 0:
idx = (self.frame // speed_and_accelerometer_step) % 4
can_sends.append(gmcan.create_adas_steering_status(CanBus.OBSTACLE, idx))
can_sends.append(gmcan.create_adas_accelerometer_speed_status(CanBus.OBSTACLE, CS.out.vEgo, idx))
if self.CP.networkLocation == NetworkLocation.gateway and (self.frame % (self.params.ADAS_KEEPALIVE_STEP if self.CP.carFingerprint in kaofui_cars else self.params.ADAS_KEEPALIVE_STEP * 2)) == 0:
if self.CP.networkLocation == NetworkLocation.gateway and self.frame % self.params.ADAS_KEEPALIVE_STEP == 0:
can_sends += gmcan.create_adas_keepalive(CanBus.POWERTRAIN)
# TODO: integrate this with the code block below?
stock_cc_active = CS.out.cruiseState.enabled or CS.pcm_acc_status != AccState.OFF
if self.CP.carFingerprint == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL:
stock_cc_active = CS.out.cruiseState.enabled
pedal_cancel = self.CP.flags & GMFlags.PEDAL_LONG.value
if self.CP.carFingerprint == CAR.CHEVROLET_MALIBU_HYBRID_CC:
pedal_cancel = pedal_cancel and CC.longActive
if (
pedal_cancel
(self.CP.flags & GMFlags.PEDAL_LONG.value) # Always cancel stock CC when using pedal interceptor
or (self.CP.flags & GMFlags.CC_LONG.value and not CC.enabled) # Cancel stock CC if OP is not active
) and stock_cc_active:
) and CS.out.cruiseState.enabled:
if self.CP.carFingerprint == CAR.CHEVROLET_MALIBU_HYBRID_CC:
malibu_cancel_requested = True
# Match 33 Hz cadence (every 3 frames) and align phase to the last seen checksum.
if self.malibu_cancel_frame % 3 == 0:
can_sends.append(gmcan.create_buttons_malibu_cancel(
CanBus.POWERTRAIN, self.malibu_cancel_phase, CS.steering_button_prefix))
self.malibu_cancel_phase = (self.malibu_cancel_phase + 1) % 4
self.malibu_cancel_frame += 1
else:
if (self.frame - self.last_button_frame) * DT_CTRL > 0.04:
self.last_button_frame = self.frame
cancel_bus = CanBus.CAMERA if self.CP.carFingerprint == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL else CanBus.POWERTRAIN
can_sends.append(gmcan.create_buttons(self.packer_pt, cancel_bus, (CS.buttons_counter + 1) % 4, CruiseButtons.CANCEL))
can_sends.append(gmcan.create_buttons(self.packer_pt, CanBus.POWERTRAIN, (CS.buttons_counter + 1) % 4, CruiseButtons.CANCEL))
else:
# While car is braking, cancel button causes ECM to enter a soft disable state with a fault status.
@@ -579,24 +324,18 @@ class CarController(CarControllerBase):
self.cancel_counter = self.cancel_counter + 1 if CC.cruiseControl.cancel else 0
# Stock longitudinal, integrated at camera
if self.CP.carFingerprint == CAR.CHEVROLET_MALIBU_HYBRID_CC:
if self.cancel_counter > CAMERA_CANCEL_DELAY_FRAMES:
malibu_cancel_requested = True
elif (self.frame - self.last_button_frame) * DT_CTRL > 0.04:
if self.cancel_counter > CAMERA_CANCEL_DELAY_FRAMES:
self.last_button_frame = self.frame
if self.CP.carFingerprint in SDGM_CAR and self.CP.carFingerprint not in (volt_like | {CAR.CHEVROLET_BLAZER, CAR.CHEVROLET_MALIBU_SDGM, CAR.CHEVROLET_TRAVERSE}):
can_sends.append(gmcan.create_buttons(self.packer_pt, CanBus.POWERTRAIN, CS.buttons_counter, CruiseButtons.CANCEL))
else:
if self.cancel_counter > CAMERA_CANCEL_DELAY_FRAMES:
if self.CP.carFingerprint == CAR.CHEVROLET_MALIBU_HYBRID_CC:
if self.malibu_cancel_frame % 3 == 0:
can_sends.append(gmcan.create_buttons_malibu_cancel(
CanBus.POWERTRAIN, self.malibu_cancel_phase, CS.steering_button_prefix))
self.malibu_cancel_phase = (self.malibu_cancel_phase + 1) % 4
self.malibu_cancel_frame += 1
else:
if (self.frame - self.last_button_frame) * DT_CTRL > 0.04:
self.last_button_frame = self.frame
can_sends.append(gmcan.create_buttons(self.packer_pt, CanBus.CAMERA, CS.buttons_counter, CruiseButtons.CANCEL))
if self.CP.carFingerprint == CAR.CHEVROLET_MALIBU_HYBRID_CC:
# Malibu has no reliable rolling counter in 0x1E1; lock cancel send to each observed OEM 0x1E1 slot.
if malibu_cancel_requested and malibu_oem_button_slot:
can_sends.append(gmcan.create_buttons_malibu_cancel(
CanBus.POWERTRAIN, (self.malibu_cancel_phase + 1) % 4, CS.steering_button_prefix))
self.malibu_cancel_phase = (self.malibu_cancel_phase + 1) % 4
if self.CP.networkLocation == NetworkLocation.fwdCamera:
# Silence "Take Steering" alert sent by camera, forward PSCMStatus with HandsOffSWlDetectionStatus=1
if self.frame % 10 == 0:
+44 -155
View File
@@ -5,7 +5,7 @@ from openpilot.common.numpy_fast import mean
from opendbc.can.can_define import CANDefine
from opendbc.can.parser import CANParser
from openpilot.selfdrive.car.interfaces import CarStateBase
from openpilot.selfdrive.car.gm.values import DBC, AccState, CanBus, STEER_THRESHOLD, GMFlags, CC_ONLY_CAR, CAMERA_ACC_CAR, SDGM_CAR, CC_REGEN_PADDLE_CAR, ASCM_INT, CAR
from openpilot.selfdrive.car.gm.values import DBC, AccState, CanBus, STEER_THRESHOLD, GMFlags, CC_ONLY_CAR, CAMERA_ACC_CAR, SDGM_CAR, ASCM_INT, CAR
TransmissionType = car.CarParams.TransmissionType
NetworkLocation = car.CarParams.NetworkLocation
@@ -28,44 +28,26 @@ class CarState(CarStateBase):
self.buttons_counter = 0
self.steering_button_checksum = 0
self.steering_button_prefix = 0x01
self.steering_button_ts_nanos = 0
self.prev_distance_button = 0
self.distance_button = 0
self.single_pedal_mode = False
self.pedal_steady = 0.
self.ecm_cruise_control_ts_nanos = 0
self.accelerator_pedal2_ts_nanos = 0
def update(self, pt_cp, cam_cp, loopback_cp, frogpilot_toggles):
ret = car.CarState.new_message()
fp_ret = custom.FrogPilotCarState.new_message()
volt_like = {CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_2019, CAR.CHEVROLET_VOLT_ASCM, CAR.CHEVROLET_VOLT_CAMERA, CAR.CHEVROLET_VOLT_CC}
kaofui_state_cars = volt_like | SDGM_CAR | ASCM_INT | {
CAR.CHEVROLET_BLAZER,
CAR.CHEVROLET_MALIBU_SDGM,
CAR.CHEVROLET_MALIBU_HYBRID_CC,
}
sdgm_non_volt = self.CP.carFingerprint in SDGM_CAR and \
self.CP.carFingerprint not in kaofui_state_cars
self.prev_cruise_buttons = self.cruise_buttons
self.prev_distance_button = self.distance_button
if not sdgm_non_volt:
self.cruise_buttons = pt_cp.vl["ASCMSteeringButton"]["ACCButtons"]
self.distance_button = pt_cp.vl["ASCMSteeringButton"]["DistanceButton"]
self.buttons_counter = pt_cp.vl["ASCMSteeringButton"]["RollingCounter"]
self.steering_button_checksum = pt_cp.vl["ASCMSteeringButton"]["SteeringButtonChecksum"]
self.steering_button_ts_nanos = pt_cp.ts_nanos["ASCMSteeringButton"]["ACCButtons"]
acc_always_one = pt_cp.vl["ASCMSteeringButton"]["ACCAlwaysOne"]
acc_hidden_bit = pt_cp.vl["ASCMSteeringButton"].get("ACCHiddenBit", 0)
self.steering_button_prefix = (int(acc_always_one) & 1) | ((int(acc_hidden_bit) & 1) << 6)
else:
self.cruise_buttons = cam_cp.vl["ASCMSteeringButton"]["ACCButtons"]
self.distance_button = cam_cp.vl["ASCMSteeringButton"]["DistanceButton"]
self.buttons_counter = cam_cp.vl["ASCMSteeringButton"]["RollingCounter"]
self.steering_button_ts_nanos = cam_cp.ts_nanos["ASCMSteeringButton"]["ACCButtons"]
self.cruise_buttons = pt_cp.vl["ASCMSteeringButton"]["ACCButtons"]
self.distance_button = pt_cp.vl["ASCMSteeringButton"]["DistanceButton"]
self.buttons_counter = pt_cp.vl["ASCMSteeringButton"]["RollingCounter"]
self.steering_button_checksum = pt_cp.vl["ASCMSteeringButton"]["SteeringButtonChecksum"]
acc_always_one = pt_cp.vl["ASCMSteeringButton"]["ACCAlwaysOne"]
acc_hidden_bit = pt_cp.vl["ASCMSteeringButton"].get("ACCHiddenBit", 0)
self.steering_button_prefix = (int(acc_always_one) & 1) | ((int(acc_hidden_bit) & 1) << 6)
self.pscm_status = copy.copy(pt_cp.vl["PSCMStatus"])
# This is to avoid a fault where you engage while still moving backwards after shifting to D.
# An Equinox has been seen with an unsupported status (3), so only check if either wheel is in reverse (2)
@@ -75,13 +57,6 @@ class CarState(CarStateBase):
self.loopback_lka_steering_cmd_updated = len(loopback_cp.vl_all["ASCMLKASteeringCmd"]["RollingCounter"]) > 0
if self.loopback_lka_steering_cmd_updated:
self.loopback_lka_steering_cmd_ts_nanos = loopback_cp.ts_nanos["ASCMLKASteeringCmd"]["RollingCounter"]
# Track timestamps for OEM PRNDL2 and Regen Paddle messages (used to sync spoofing timing)
self.prndl2_ts_nanos = pt_cp.ts_nanos["ECMPRDNL2"]["PRNDL2"]
if self.CP.carFingerprint in CC_REGEN_PADDLE_CAR:
self.regen_paddle_ts_nanos = pt_cp.ts_nanos["EBCMRegenPaddle"]["RegenPaddle"]
else:
self.regen_paddle_ts_nanos = 0
if self.CP.networkLocation == NetworkLocation.fwdCamera and not self.CP.flags & GMFlags.NO_CAMERA.value:
self.pt_lka_steering_cmd_counter = pt_cp.vl["ASCMLKASteeringCmd"]["RollingCounter"]
self.cam_lka_steering_cmd_counter = cam_cp.vl["ASCMLKASteeringCmd"]["RollingCounter"]
@@ -94,7 +69,6 @@ class CarState(CarStateBase):
)
ret.vEgoRaw = mean([ret.wheelSpeeds.fl, ret.wheelSpeeds.fr, ret.wheelSpeeds.rl, ret.wheelSpeeds.rr])
ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
ret.vEgoCluster = ret.vEgo * frogpilot_toggles.cluster_offset
# sample rear wheel speeds, standstill=True if ECM allows engagement with brake
ret.standstill = ret.wheelSpeeds.rl <= STANDSTILL_THRESHOLD and ret.wheelSpeeds.rr <= STANDSTILL_THRESHOLD
@@ -103,42 +77,29 @@ class CarState(CarStateBase):
else:
ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(pt_cp.vl["ECMPRDNL2"]["PRNDL2"], None))
no_accel_pos = bool(self.CP.flags & GMFlags.NO_ACCELERATOR_POS_MSG.value)
if no_accel_pos:
if self.CP.carFingerprint in kaofui_state_cars:
ret.brake = pt_cp.vl.get("EBCMBrakePedalPosition", {}).get("BrakePedalPosition", 0) / 0xd0
else:
ret.brake = pt_cp.vl["EBCMBrakePedalPosition"]["BrakePedalPosition"] / 0xd0
if self.CP.flags & GMFlags.NO_ACCELERATOR_POS_MSG.value:
ret.brake = pt_cp.vl.get("EBCMBrakePedalPosition", {}).get("BrakePedalPosition", 0) / 0xd0
else:
if self.CP.carFingerprint in kaofui_state_cars:
ret.brake = pt_cp.vl.get("ECMAcceleratorPos", {}).get("BrakePedalPos", 0)
else:
ret.brake = pt_cp.vl["ECMAcceleratorPos"]["BrakePedalPos"]
ret.brake = pt_cp.vl.get("ECMAcceleratorPos", {}).get("BrakePedalPos", 0)
if self.CP.carFingerprint in {CAR.CHEVROLET_MALIBU_CC} or (self.CP.carFingerprint == CAR.CHEVROLET_BLAZER and not no_accel_pos):
ret.brakePressed = ret.brake >= 8
elif (self.CP.flags & GMFlags.FORCE_BRAKE_C9.value) or ((self.CP.networkLocation == NetworkLocation.fwdCamera) and (self.CP.carFingerprint != CAR.CHEVROLET_BLAZER)):
if (self.CP.flags & GMFlags.FORCE_BRAKE_C9.value) or ((self.CP.networkLocation == NetworkLocation.fwdCamera) and (self.CP.carFingerprint != CAR.CHEVROLET_BLAZER)):
ret.brakePressed = pt_cp.vl["ECMEngineStatus"]["BrakePressed"] != 0
else:
# Some Volt 2016-17 have loose brake pedal push rod retainers which causes the ECM to believe
# that the brake is being intermittently pressed without user interaction.
# To avoid a cruise fault we need to use a conservative brake position threshold
# https://static.nhtsa.gov/odi/tsbs/2017/MC-10137629-9999.pdf
analog_thresh = 0.10 if no_accel_pos else 8
analog_thresh = 0.10 if (self.CP.flags & GMFlags.NO_ACCELERATOR_POS_MSG.value) else 8
ret.brakePressed = ret.brake >= analog_thresh
# Regen braking is braking
if self.CP.transmissionType == TransmissionType.direct:
ret.regenBraking = pt_cp.vl["EBCMRegenPaddle"]["RegenPaddle"] != 0
self.single_pedal_mode = (ret.gearShifter == GearShifter.low or
pt_cp.vl["EVDriveMode"]["SinglePedalModeActive"] == 1 or
(ret.regenBraking and GearShifter.manumatic) or
(self.CP.carFingerprint in (CAR.CHEVROLET_BOLT_ACC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL, CAR.CHEVROLET_BOLT_CC_2022_2023) and self.CP.enableGasInterceptor))
self.single_pedal_mode = ret.gearShifter == GearShifter.low or pt_cp.vl["EVDriveMode"]["SinglePedalModeActive"] == 1 or (ret.regenBraking and GearShifter.manumatic) or (self.CP.carFingerprint in [CAR.CHEVROLET_BOLT_EUV, CAR.CHEVROLET_BOLT_CC] and self.CP.enableGasInterceptor)
if self.CP.enableGasInterceptor:
ret.gas = (pt_cp.vl["GAS_SENSOR"]["INTERCEPTOR_GAS"] + pt_cp.vl["GAS_SENSOR"]["INTERCEPTOR_GAS2"]) / 2.
threshold = 23 if self.CP.carFingerprint in CAMERA_ACC_CAR else 4 # Panda 595 threshold = 23.65. Set lower to avoid panda blocking messages and GasInterceptor faulting.
threshold = 21 if self.CP.carFingerprint in CAMERA_ACC_CAR else 4 # Panda 595 threshold = 23.65. Set lower to avoid panda blocking messages and GasInterceptor faulting.
ret.gasPressed = ret.gas > threshold
else:
ret.gas = pt_cp.vl["AcceleratorPedal2"]["AcceleratorPedal2"] / 254.
@@ -155,32 +116,19 @@ class CarState(CarStateBase):
ret.steerFaultTemporary = self.lkas_status == 2
ret.steerFaultPermanent = self.lkas_status == 3
if not sdgm_non_volt:
# 1 - open, 0 - closed
ret.doorOpen = (pt_cp.vl["BCMDoorBeltStatus"]["FrontLeftDoor"] == 1 or
pt_cp.vl["BCMDoorBeltStatus"]["FrontRightDoor"] == 1 or
pt_cp.vl["BCMDoorBeltStatus"]["RearLeftDoor"] == 1 or
pt_cp.vl["BCMDoorBeltStatus"]["RearRightDoor"] == 1)
# 1 - latched
ret.seatbeltUnlatched = pt_cp.vl["BCMDoorBeltStatus"]["LeftSeatBelt"] == 0
ret.leftBlinker = pt_cp.vl["BCMTurnSignals"]["TurnSignals"] == 1
ret.rightBlinker = pt_cp.vl["BCMTurnSignals"]["TurnSignals"] == 2
# 1 - open, 0 - closed
ret.doorOpen = (pt_cp.vl["BCMDoorBeltStatus"]["FrontLeftDoor"] == 1 or
pt_cp.vl["BCMDoorBeltStatus"]["FrontRightDoor"] == 1 or
pt_cp.vl["BCMDoorBeltStatus"]["RearLeftDoor"] == 1 or
pt_cp.vl["BCMDoorBeltStatus"]["RearRightDoor"] == 1)
ret.parkingBrake = pt_cp.vl["BCMGeneralPlatformStatus"]["ParkBrakeSwActive"] == 1
else:
# 1 - open, 0 - closed
ret.doorOpen = (cam_cp.vl["BCMDoorBeltStatus"]["FrontLeftDoor"] == 1 or
cam_cp.vl["BCMDoorBeltStatus"]["FrontRightDoor"] == 1 or
cam_cp.vl["BCMDoorBeltStatus"]["RearLeftDoor"] == 1 or
cam_cp.vl["BCMDoorBeltStatus"]["RearRightDoor"] == 1)
# 1 - latched
ret.seatbeltUnlatched = pt_cp.vl["BCMDoorBeltStatus"]["LeftSeatBelt"] == 0
ret.leftBlinker = pt_cp.vl["BCMTurnSignals"]["TurnSignals"] == 1
ret.rightBlinker = pt_cp.vl["BCMTurnSignals"]["TurnSignals"] == 2
# 1 - latched
ret.seatbeltUnlatched = cam_cp.vl["BCMDoorBeltStatus"]["LeftSeatBelt"] == 0
ret.leftBlinker = cam_cp.vl["BCMTurnSignals"]["TurnSignals"] == 1
ret.rightBlinker = cam_cp.vl["BCMTurnSignals"]["TurnSignals"] == 2
ret.parkingBrake = cam_cp.vl["BCMGeneralPlatformStatus"]["ParkBrakeSwActive"] == 1
ret.parkingBrake = pt_cp.vl["BCMGeneralPlatformStatus"]["ParkBrakeSwActive"] == 1
ret.cruiseState.available = pt_cp.vl["ECMEngineStatus"]["CruiseMainOn"] != 0
ret.espDisabled = pt_cp.vl["ESPStatus"]["TractionControlOn"] != 1
ret.accFaulted = (pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.FAULTED or
@@ -191,47 +139,23 @@ class CarState(CarStateBase):
if self.CP.networkLocation == NetworkLocation.fwdCamera and not self.CP.flags & GMFlags.NO_CAMERA.value:
if self.CP.carFingerprint not in CC_ONLY_CAR:
ret.cruiseState.speed = cam_cp.vl["ASCMActiveCruiseControlStatus"]["ACCSpeedSetpoint"] * CV.KPH_TO_MS
if self.CP.carFingerprint not in (SDGM_CAR | ASCM_INT):
if self.CP.carFingerprint not in (SDGM_CAR|ASCM_INT):
ret.stockAeb = cam_cp.vl["AEBCmd"]["AEBCmdActive"] != 0
else:
ret.stockAeb = False
# openpilot controls nonAdaptive when not pcmCruise
# 2016-2018 Volt won't identify non-adaptive cruise state since switchable cruise state was not introduced till 2019 model year / SDGM Global AAdd commentMore actions
if self.CP.pcmCruise and self.CP.carFingerprint not in ASCM_INT:
ret.cruiseState.nonAdaptive = cam_cp.vl["ASCMActiveCruiseControlStatus"]["ACCCruiseState"] not in (2, 3)
if self.CP.carFingerprint in CC_ONLY_CAR:
self.ecm_cruise_control_ts_nanos = pt_cp.ts_nanos["ECMCruiseControl"]["CruiseActive"]
self.accelerator_pedal2_ts_nanos = pt_cp.ts_nanos["AcceleratorPedal2"]["CruiseState"]
ret.accFaulted = False
ret.cruiseState.speed = pt_cp.vl["ECMCruiseControl"]["CruiseSetSpeed"] * CV.KPH_TO_MS
if self.CP.carFingerprint == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL:
try:
ret.cruiseState.enabled = cam_cp.vl["ASCMActiveCruiseControlStatus"]["ACCCmdActive"] != 0
except:
ret.cruiseState.enabled = pt_cp.vl["ECMCruiseControl"]["CruiseActive"] != 0
else:
# Most CC paths use ECM first.
try:
ret.cruiseState.enabled = pt_cp.vl["ECMCruiseControl"]["CruiseActive"] != 0
except:
ret.cruiseState.enabled = cam_cp.vl["ASCMActiveCruiseControlStatus"]["ACCCmdActive"] != 0
else:
self.ecm_cruise_control_ts_nanos = 0
self.accelerator_pedal2_ts_nanos = 0
ret.cruiseState.enabled = pt_cp.vl["ECMCruiseControl"]["CruiseActive"] != 0
if self.CP.enableBsm:
if not sdgm_non_volt:
ret.leftBlindspot = pt_cp.vl["BCMBlindSpotMonitor"]["LeftBSM"] == 1
ret.rightBlindspot = pt_cp.vl["BCMBlindSpotMonitor"]["RightBSM"] == 1
else:
ret.leftBlindspot = cam_cp.vl["BCMBlindSpotMonitor"]["LeftBSM"] == 1
ret.rightBlindspot = cam_cp.vl["BCMBlindSpotMonitor"]["RightBSM"] == 1
ret.leftBlindspot = pt_cp.vl["BCMBlindSpotMonitor"]["LeftBSM"] == 1
ret.rightBlindspot = pt_cp.vl["BCMBlindSpotMonitor"]["RightBSM"] == 1
# FrogPilot CarState functions
self.lkas_previously_enabled = self.lkas_enabled
if sdgm_non_volt:
self.lkas_enabled = cam_cp.vl["ASCMSteeringButton"]["LKAButton"]
else:
self.lkas_enabled = pt_cp.vl["ASCMSteeringButton"]["LKAButton"]
self.lkas_enabled = pt_cp.vl["ASCMSteeringButton"]["LKAButton"]
self.pcm_acc_status = pt_cp.vl["AcceleratorPedal2"]["CruiseState"]
@@ -243,31 +167,14 @@ class CarState(CarStateBase):
def get_cam_can_parser(CP, FPCP):
messages = []
if CP.networkLocation == NetworkLocation.fwdCamera and not CP.flags & GMFlags.NO_CAMERA.value:
volt_like = {CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_2019, CAR.CHEVROLET_VOLT_ASCM, CAR.CHEVROLET_VOLT_CAMERA, CAR.CHEVROLET_VOLT_CC}
kaofui_state_cars = volt_like | SDGM_CAR | ASCM_INT | {
CAR.CHEVROLET_BLAZER,
CAR.CHEVROLET_MALIBU_SDGM,
CAR.CHEVROLET_MALIBU_HYBRID_CC,
}
sdgm_non_volt = CP.carFingerprint in SDGM_CAR and \
CP.carFingerprint not in kaofui_state_cars
messages += [
("ASCMLKASteeringCmd", 10),
]
if sdgm_non_volt:
messages += [
("BCMTurnSignals", 1),
("BCMDoorBeltStatus", 10),
("BCMGeneralPlatformStatus", 10),
("ASCMSteeringButton", 33),
]
if CP.enableBsm:
messages.append(("BCMBlindSpotMonitor", 10))
elif CP.carFingerprint not in (SDGM_CAR | ASCM_INT):
if CP.carFingerprint not in (SDGM_CAR|ASCM_INT):
messages += [
("AEBCmd", 10),
]
if CP.carFingerprint not in CC_ONLY_CAR or CP.carFingerprint == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL:
if CP.carFingerprint not in CC_ONLY_CAR:
messages += [
("ASCMActiveCruiseControlStatus", 25),
]
@@ -277,43 +184,25 @@ class CarState(CarStateBase):
@staticmethod
def get_can_parser(CP, FPCP):
messages = [
("BCMTurnSignals", 1),
("ECMPRDNL2", 10),
("PSCMStatus", 10),
("ESPStatus", 10),
("BCMDoorBeltStatus", 10),
("BCMGeneralPlatformStatus", 10),
("EBCMWheelSpdFront", 20),
("EBCMWheelSpdRear", 20),
("EBCMFrictionBrakeStatus", 20),
("AcceleratorPedal2", 33),
("ASCMSteeringButton", 33),
("ECMEngineStatus", 100),
("PSCMSteeringAngle", 100),
("ECMAcceleratorPos", 80),
("SportMode", 0),
]
volt_like = {CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_2019, CAR.CHEVROLET_VOLT_ASCM, CAR.CHEVROLET_VOLT_CAMERA, CAR.CHEVROLET_VOLT_CC}
kaofui_state_cars = volt_like | SDGM_CAR | ASCM_INT | {
CAR.CHEVROLET_BLAZER,
CAR.CHEVROLET_MALIBU_SDGM,
CAR.CHEVROLET_MALIBU_HYBRID_CC,
}
prndl2_rate = 10 if CP.carFingerprint in kaofui_state_cars else 40
sdgm_non_volt = CP.carFingerprint in SDGM_CAR and \
CP.carFingerprint not in kaofui_state_cars
if sdgm_non_volt:
messages += [
("ECMPRDNL2", prndl2_rate),
("AcceleratorPedal2", 40),
("ECMEngineStatus", 80),
]
else:
messages += [
("ECMPRDNL2", prndl2_rate),
("AcceleratorPedal2", 33),
("ECMEngineStatus", 100),
("BCMTurnSignals", 1),
("BCMDoorBeltStatus", 10),
("BCMGeneralPlatformStatus", 10),
("ASCMSteeringButton", 33),
]
if CP.enableBsm:
messages.append(("BCMBlindSpotMonitor", 10))
if CP.enableBsm:
messages.append(("BCMBlindSpotMonitor", 10))
# Used to read back last counter sent to PT by camera
if CP.networkLocation == NetworkLocation.fwdCamera:
@@ -325,9 +214,8 @@ class CarState(CarStateBase):
messages.append(("EBCMBrakePedalPosition", 100))
if CP.transmissionType == TransmissionType.direct:
regen_paddle_rate = 50 if CP.carFingerprint in kaofui_state_cars else 40
messages += [
("EBCMRegenPaddle", regen_paddle_rate),
("EBCMRegenPaddle", 50),
("EVDriveMode", 0),
]
@@ -341,6 +229,7 @@ class CarState(CarStateBase):
("GAS_SENSOR", 50),
]
return CANParser(DBC[CP.carFingerprint]["pt"], messages, CanBus.POWERTRAIN)
@staticmethod
+16 -25
View File
@@ -84,34 +84,14 @@ FINGERPRINTS = {
CAR.CADILLAC_ESCALADE_ESV_2019: [{
715: 8, 840: 5, 717: 5, 869: 4, 880: 6, 289: 8, 454: 8, 842: 5, 460: 5, 463: 3, 801: 8, 170: 8, 190: 6, 241: 6, 201: 8, 417: 7, 211: 2, 419: 1, 398: 8, 426: 7, 487: 8, 442: 8, 451: 8, 452: 8, 453: 6, 479: 3, 311: 8, 500: 6, 647: 6, 193: 8, 707: 8, 197: 8, 209: 7, 199: 4, 455: 7, 313: 8, 481: 7, 485: 8, 489: 8, 249: 8, 393: 7, 407: 7, 413: 8, 422: 4, 431: 8, 501: 8, 499: 3, 810: 8, 508: 8, 381: 8, 462: 4, 532: 6, 562: 8, 386: 8, 761: 7, 573: 1, 554: 3, 719: 5, 560: 8, 1279: 4, 388: 8, 288: 5, 1005: 6, 497: 8, 844: 8, 961: 8, 967: 4, 977: 8, 979: 8, 985: 5, 1001: 8, 1017: 8, 1019: 2, 1020: 8, 1217: 8, 510: 8, 866: 4, 304: 1, 969: 8, 384: 4, 1033: 7, 1009: 8, 1034: 7, 1296: 4, 1930: 7, 1105: 5, 1013: 5, 1225: 7, 1919: 7, 320: 3, 534: 2, 352: 5, 298: 8, 1223: 2, 1233: 8, 608: 8, 1265: 8, 609: 6, 1267: 1, 1417: 8, 610: 6, 1906: 7, 611: 6, 612: 8, 613: 8, 208: 8, 564: 5, 309: 8, 1221: 5, 1280: 4, 1249: 8, 1907: 7, 1257: 6, 1300: 8, 1920: 7, 563: 5, 1322: 6, 1323: 4, 1328: 4, 1917: 7, 328: 1, 1912: 7, 1914: 7, 804: 3, 1918: 7
}],
CAR.CHEVROLET_BOLT_ACC_2022_2023: [{
CAR.CHEVROLET_BOLT_EUV: [{
189: 7, 190: 7, 193: 8, 197: 8, 201: 8, 209: 7, 211: 3, 241: 6, 257: 8, 288: 5, 289: 8, 298: 8, 304: 3, 309: 8, 311: 8, 313: 8, 320: 4, 322: 7, 328: 1, 352: 5, 381: 8, 384: 4, 386: 8, 388: 8, 451: 8, 452: 8, 453: 6, 458: 5, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 497: 8, 500: 6, 501: 8, 528: 5, 532: 6, 560: 8, 562: 8, 563: 5, 565: 5, 566: 8, 587: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 707: 8, 715: 8, 717: 5, 753: 5, 761: 7, 789: 5, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 848: 4, 869: 4, 880: 6, 977: 8, 1001: 8, 1017: 8, 1020: 8, 1217: 8, 1221: 5, 1233: 8, 1249: 8, 1265: 8, 1280: 4, 1296: 4, 1300: 8, 1611: 8, 1930: 7
}],
CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL: [{
189: 7, 190: 7, 193: 8, 197: 8, 201: 8, 209: 7, 211: 3, 241: 6, 257: 8, 288: 5, 289: 8, 298: 8, 304: 3, 309: 8, 311: 8, 313: 8, 320: 4, 322: 7, 328: 1, 352: 5, 381: 8, 384: 4, 386: 8, 388: 8, 451: 8, 452: 8, 453: 6, 458: 5, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 497: 8, 500: 6, 501: 8, 528: 5, 532: 6, 560: 8, 562: 8, 563: 5, 565: 5, 566: 8, 587: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 707: 8, 715: 8, 717: 5, 753: 5, 761: 7, 789: 5, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 848: 4, 869: 4, 880: 6, 977: 8, 1001: 8, 1017: 8, 1020: 8, 1217: 8, 1221: 5, 1233: 8, 1249: 8, 1265: 8, 1280: 4, 1296: 4, 1300: 8, 1611: 8, 1930: 7
}],
CAR.CHEVROLET_BOLT_CC_2022_2023: [{
189: 7, 190: 7, 193: 8, 197: 8, 201: 8, 209: 7, 211: 3, 241: 6, 257: 8, 288: 5, 289: 8, 298: 8, 304: 3, 309: 8, 311: 8, 313: 8, 320: 4, 322: 7, 328: 1, 352: 5, 381: 8, 384: 4, 386: 8, 388: 8, 451: 8, 452: 8, 453: 6, 458: 5, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 497: 8, 500: 6, 501: 8, 528: 5, 532: 6, 560: 8, 562: 8, 563: 5, 565: 5, 566: 8, 587: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 707: 8, 715: 8, 717: 5, 753: 5, 761: 7, 789: 5, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 848: 4, 869: 4, 880: 6, 977: 8, 1001: 8, 1017: 8, 1020: 8, 1217: 8, 1221: 5, 1233: 8, 1249: 8, 1265: 8, 1280: 4, 1296: 4, 1300: 8, 1611: 8, 1930: 7
}],
CAR.CHEVROLET_BOLT_CC_2017: [
CAR.CHEVROLET_BOLT_CC: [
# Bolt Premier w/o ACC 2017
{
170: 8, 188: 8, 189: 7, 190: 6, 192: 5, 193: 8, 197: 8, 201: 6, 209: 7, 211: 2, 241: 6, 289: 1, 290: 1, 298: 8, 304: 8, 309: 8, 311: 8, 313: 8, 320: 8, 322: 7, 328: 1, 352: 5, 353: 3, 368: 8, 381: 6, 384: 8, 386: 8, 388: 8, 390: 7, 407: 7, 417: 7, 419: 1, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 458: 8, 463: 3, 479: 3, 481: 7, 485: 8, 489: 5, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 503: 1, 508: 8, 512: 3, 514: 2, 516: 4, 519: 2, 521: 3, 528: 5, 530: 8, 532: 7, 537: 5, 539: 8, 542: 7, 546: 7, 550: 8, 554: 3, 558: 8, 560: 6, 562: 4, 563: 5, 564: 5, 565: 8, 566: 6, 567: 5, 568: 1, 569: 3, 573: 1, 577: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 3, 707: 8, 711: 6, 717: 5, 753: 5, 761: 7, 800: 6, 810: 8, 832: 8, 840: 6, 842: 6, 844: 8, 866: 4, 869: 4, 872: 1, 961: 8, 967: 4, 969: 8, 977: 8, 979: 7, 985: 5, 988: 6, 989: 8, 995: 7, 1001: 5, 1003: 5, 1005: 6, 1009: 8, 1013: 3, 1017: 8, 1019: 2, 1020: 8, 1022: 1, 1105: 6, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1243: 3, 1249: 8, 1257: 6, 1265: 8, 1275: 3, 1280: 4, 1300: 8, 1322: 6, 1328: 4, 1601: 8, 1904: 7, 1905: 7, 1906: 7, 1907: 7, 1912: 7, 1913: 7, 1927: 7, 2016: 8, 2020: 8, 2024: 8, 2028: 8
},
# Bolt EV Premier 2017
{
170: 8, 188: 8, 189: 7, 190: 6, 192: 5, 193: 8, 197: 8, 201: 6, 209: 7, 211: 2, 241: 6, 289: 1, 290: 1, 298: 8, 304: 8, 309: 8, 311: 8, 313: 8, 320: 8, 322: 7, 328: 1, 352: 5, 353: 3, 368: 8, 381: 6, 384: 8, 386: 8, 388: 8, 390: 7, 407: 7, 417: 7, 419: 1, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 458: 8, 463: 3, 479: 3, 481: 7, 485: 8, 489: 5, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 503: 1, 508: 8, 512: 3, 514: 2, 516: 4, 519: 2, 521: 3, 528: 5, 530: 8, 532: 7, 537: 5, 539: 8, 542: 7, 546: 7, 550: 8, 554: 3, 558: 8, 560: 6, 562: 4, 563: 5, 564: 5, 565: 8, 566: 6, 567: 5, 568: 1, 569: 3, 573: 1, 577: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 3, 707: 8, 711: 6, 717: 5, 753: 5, 761: 7, 800: 6, 810: 8, 832: 8, 840: 6, 842: 6, 844: 8, 866: 4, 869: 4, 872: 1, 961: 8, 967: 4, 969: 8, 977: 8, 979: 7, 985: 5, 988: 6, 989: 8, 995: 7, 1001: 5, 1003: 5, 1005: 6, 1009: 8, 1013: 3, 1017: 8, 1019: 2, 1020: 8, 1022: 1, 1105: 6, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1243: 3, 1249: 8, 1257: 6, 1265: 8, 1275: 3, 1280: 4, 1300: 8, 1322: 6, 1328: 4, 1601: 8, 1904: 7, 1905: 7, 1906: 7, 1907: 7, 1912: 7, 1913: 7, 1927: 7, 2016: 8, 2020: 8, 2024: 8, 2028: 8
},
# Bolt EV Premier 2017 w Pedal
{ # pylint: disable=duplicate-key
170: 8, 188: 8, 189: 7, 190: 6, 192: 5, 193: 8, 197: 8, 201: 6, 209: 7, 211: 2, 241: 6, 289: 1, 290: 1, 298: 8, 304: 8, 309: 8, 311: 8, 313: 8, 320: 8, 322: 7, 328: 1, 352: 5, 353: 3, 368: 8, 381: 6, 384: 8, 386: 8, 388: 8, 390: 7, 407: 7, 417: 7, 419: 1, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 458: 8, 463: 3, 479: 3, 481: 7, 485: 8, 489: 5, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 503: 1, 508: 8, 512: 3, 512: 6, 513: 6, 514: 2, 516: 4, 519: 2, 521: 3, 528: 5, 530: 8, 532: 7, 537: 5, 539: 8, 542: 7, 546: 7, 550: 8, 554: 3, 558: 8, 560: 6, 562: 4, 563: 5, 564: 5, 565: 8, 566: 6, 567: 5, 568: 1, 569: 3, 573: 1, 577: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 3, 707: 8, 711: 6, 717: 5, 753: 5, 761: 7, 800: 6, 810: 8, 832: 8, 840: 6, 842: 6, 844: 8, 866: 4, 869: 4, 872: 1, 961: 8, 967: 4, 969: 8, 977: 8, 979: 7, 985: 5, 988: 6, 989: 8, 995: 7, 1001: 5, 1003: 5, 1005: 6, 1009: 8, 1013: 3, 1017: 8, 1019: 2, 1020: 8, 1022: 1, 1105: 6, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1243: 3, 1249: 8, 1257: 6, 1265: 8, 1275: 3, 1280: 4, 1300: 8, 1322: 6, 1328: 4, 1601: 8, 1904: 7, 1905: 7, 1906: 7, 1907: 7, 1912: 7, 1913: 7, 1927: 7, 2016: 8, 2020: 8, 2024: 8, 2028: 8 # pylint: disable=duplicate-key # noqa: F601
},
# Bolt EV Premier 2017 2 w Pedal
{
170: 8, 188: 8, 189: 7, 190: 6, 193: 8, 197: 8, 201: 8, 209: 7, 211: 2, 241: 6, 298: 8, 304: 1, 308: 4, 309: 8, 311: 8, 313: 8, 320: 3, 322: 7, 328: 1, 352: 5, 353: 3, 381: 6, 384: 4, 386: 8, 388: 8, 390: 7, 407: 7, 417: 7, 419: 1, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 503: 1, 508: 8, 513: 6, 528: 5, 532: 6, 546: 7, 550: 8, 554: 3, 558: 8, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 566: 6, 567: 5, 568: 1, 573: 1, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 3, 707: 8, 711: 6, 717: 5, 753: 5, 761: 7, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 869: 4, 872: 1, 961: 8, 967: 4, 969: 8, 977: 8, 979: 7, 985: 5, 988: 6, 989: 8, 995: 7, 1001: 8, 1005: 6, 1009: 8, 1013: 3, 1017: 8, 1019: 2, 1020: 8, 1022: 1, 1105: 6, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1243: 3, 1249: 8, 1257: 6, 1265: 8, 1275: 3, 1280: 4, 1300: 8, 1322: 6, 1328: 4, 1904: 7, 1905: 7, 1906: 7, 1907: 7, 1912: 7, 1913: 7, 1922: 7, 1927: 7
}],
CAR.CHEVROLET_BOLT_CC_2019_2021: [
# Chevy Bolt EV 2018-2021
# Bolt Premier no ACC 2018 + Pedal
{
170: 8, 188: 8, 189: 7, 190: 6, 193: 8, 197: 8, 201: 8, 209: 7, 211: 2, 241: 6, 298: 8, 304: 1, 308: 4, 309: 8, 311: 8, 313: 8, 320: 3, 322: 7, 328: 1, 352: 5, 353: 3, 381: 6, 384: 4, 386: 8, 388: 8, 390: 7, 407: 7, 417: 7, 419: 1, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 503: 2, 508: 8, 513: 6, 528: 5, 532: 6, 546: 7, 550: 8, 554: 3, 558: 8, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 566: 6, 567: 5, 568: 1, 573: 1, 577: 8, 592: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 3, 707: 8, 711: 6, 717: 5, 753: 5, 761: 7, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 869: 4, 872: 1, 961: 8, 967: 4, 969: 8, 977: 8, 979: 7, 985: 5, 988: 6, 989: 8, 995: 7, 1001: 8, 1005: 6, 1009: 8, 1013: 3, 1017: 8, 1019: 2, 1020: 8, 1022: 1, 1105: 6, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1243: 3, 1249: 8, 1257: 6, 1265: 8, 1275: 3, 1280: 4, 1300: 8, 1322: 6, 1328: 4, 1601: 8, 1616: 8, 1904: 7, 1905: 7, 1906: 7, 1907: 7, 1912: 7, 1913: 7, 1922: 7, 1927: 7, 2020: 8, 2023: 8, 2028: 8, 2031: 8
@@ -132,6 +112,18 @@ FINGERPRINTS = {
{
170: 8, 188: 8, 189: 7, 190: 6, 193: 8, 197: 8, 201: 8, 209: 7, 211: 2, 241: 6, 288: 5, 298: 8, 304: 1, 308: 4, 309: 8, 311: 8, 313: 8, 320: 3, 322: 7, 328: 1, 352: 5, 353: 3, 368: 3, 381: 8, 386: 8, 388: 8, 390: 7, 407: 7, 417: 7, 419: 1, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 503: 2, 508: 8, 512: 6, 513: 6, 528: 5, 532: 6, 546: 7, 550: 8, 554: 3, 558: 8, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 566: 7, 567: 5, 568: 2, 569: 3, 573: 1, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 3, 707: 8, 711: 6, 753: 5, 761: 7, 810: 8, 840: 5, 842: 5, 844: 8, 848: 4, 866: 4, 872: 1, 961: 8, 967: 4, 969: 8, 975: 2, 977: 8, 979: 7, 985: 5, 988: 6, 989: 8, 995: 7, 1001: 8, 1005: 6, 1009: 8, 1013: 3, 1017: 8, 1019: 2, 1020: 8, 1022: 1, 1037: 5, 1105: 5, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1236: 8, 1243: 3, 1249: 8, 1257: 6, 1265: 8, 1275: 3, 1279: 4, 1280: 4, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 1904: 7, 1905: 7, 1906: 7, 1907: 7, 1912: 7, 1913: 7, 1922: 7, 1927: 7
},
# Bolt EV Premier 2017
{
170: 8, 188: 8, 189: 7, 190: 6, 192: 5, 193: 8, 197: 8, 201: 6, 209: 7, 211: 2, 241: 6, 289: 1, 290: 1, 298: 8, 304: 8, 309: 8, 311: 8, 313: 8, 320: 8, 322: 7, 328: 1, 352: 5, 353: 3, 368: 8, 381: 6, 384: 8, 386: 8, 388: 8, 390: 7, 407: 7, 417: 7, 419: 1, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 458: 8, 463: 3, 479: 3, 481: 7, 485: 8, 489: 5, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 503: 1, 508: 8, 512: 3, 514: 2, 516: 4, 519: 2, 521: 3, 528: 5, 530: 8, 532: 7, 537: 5, 539: 8, 542: 7, 546: 7, 550: 8, 554: 3, 558: 8, 560: 6, 562: 4, 563: 5, 564: 5, 565: 8, 566: 6, 567: 5, 568: 1, 569: 3, 573: 1, 577: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 3, 707: 8, 711: 6, 717: 5, 753: 5, 761: 7, 800: 6, 810: 8, 832: 8, 840: 6, 842: 6, 844: 8, 866: 4, 869: 4, 872: 1, 961: 8, 967: 4, 969: 8, 977: 8, 979: 7, 985: 5, 988: 6, 989: 8, 995: 7, 1001: 5, 1003: 5, 1005: 6, 1009: 8, 1013: 3, 1017: 8, 1019: 2, 1020: 8, 1022: 1, 1105: 6, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1243: 3, 1249: 8, 1257: 6, 1265: 8, 1275: 3, 1280: 4, 1300: 8, 1322: 6, 1328: 4, 1601: 8, 1904: 7, 1905: 7, 1906: 7, 1907: 7, 1912: 7, 1913: 7, 1927: 7, 2016: 8, 2020: 8, 2024: 8, 2028: 8
},
# Bolt EV Premier 2017 w Pedal
{ # pylint: disable=duplicate-key
170: 8, 188: 8, 189: 7, 190: 6, 192: 5, 193: 8, 197: 8, 201: 6, 209: 7, 211: 2, 241: 6, 289: 1, 290: 1, 298: 8, 304: 8, 309: 8, 311: 8, 313: 8, 320: 8, 322: 7, 328: 1, 352: 5, 353: 3, 368: 8, 381: 6, 384: 8, 386: 8, 388: 8, 390: 7, 407: 7, 417: 7, 419: 1, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 458: 8, 463: 3, 479: 3, 481: 7, 485: 8, 489: 5, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 503: 1, 508: 8, 512: 3, 512: 6, 513: 6, 514: 2, 516: 4, 519: 2, 521: 3, 528: 5, 530: 8, 532: 7, 537: 5, 539: 8, 542: 7, 546: 7, 550: 8, 554: 3, 558: 8, 560: 6, 562: 4, 563: 5, 564: 5, 565: 8, 566: 6, 567: 5, 568: 1, 569: 3, 573: 1, 577: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 3, 707: 8, 711: 6, 717: 5, 753: 5, 761: 7, 800: 6, 810: 8, 832: 8, 840: 6, 842: 6, 844: 8, 866: 4, 869: 4, 872: 1, 961: 8, 967: 4, 969: 8, 977: 8, 979: 7, 985: 5, 988: 6, 989: 8, 995: 7, 1001: 5, 1003: 5, 1005: 6, 1009: 8, 1013: 3, 1017: 8, 1019: 2, 1020: 8, 1022: 1, 1105: 6, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1243: 3, 1249: 8, 1257: 6, 1265: 8, 1275: 3, 1280: 4, 1300: 8, 1322: 6, 1328: 4, 1601: 8, 1904: 7, 1905: 7, 1906: 7, 1907: 7, 1912: 7, 1913: 7, 1927: 7, 2016: 8, 2020: 8, 2024: 8, 2028: 8 # pylint: disable=duplicate-key # noqa: F601
},
# Bolt EV Premier 2017 2 w Pedal
{
170: 8, 188: 8, 189: 7, 190: 6, 193: 8, 197: 8, 201: 8, 209: 7, 211: 2, 241: 6, 298: 8, 304: 1, 308: 4, 309: 8, 311: 8, 313: 8, 320: 3, 322: 7, 328: 1, 352: 5, 353: 3, 381: 6, 384: 4, 386: 8, 388: 8, 390: 7, 407: 7, 417: 7, 419: 1, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 503: 1, 508: 8, 513: 6, 528: 5, 532: 6, 546: 7, 550: 8, 554: 3, 558: 8, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 566: 6, 567: 5, 568: 1, 573: 1, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 3, 707: 8, 711: 6, 717: 5, 753: 5, 761: 7, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 869: 4, 872: 1, 961: 8, 967: 4, 969: 8, 977: 8, 979: 7, 985: 5, 988: 6, 989: 8, 995: 7, 1001: 8, 1005: 6, 1009: 8, 1013: 3, 1017: 8, 1019: 2, 1020: 8, 1022: 1, 1105: 6, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1243: 3, 1249: 8, 1257: 6, 1265: 8, 1275: 3, 1280: 4, 1300: 8, 1322: 6, 1328: 4, 1904: 7, 1905: 7, 1906: 7, 1907: 7, 1912: 7, 1913: 7, 1922: 7, 1927: 7
},
# Bolt EV Premier no ACC 2023
{
170: 8, 188: 8, 189: 7, 190: 7, 193: 8, 197: 8, 201: 8, 209: 7, 211: 3, 241: 6, 257: 8, 288: 5, 289: 8, 292: 2, 298: 8, 304: 3, 308: 4, 309: 8, 311: 8, 313: 8, 320: 4, 322: 7, 328: 1, 331: 3, 352: 5, 353: 3, 368: 3, 381: 8, 384: 4, 386: 8, 388: 8, 390: 7, 398: 8, 407: 7, 417: 8, 419: 1, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 458: 5, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 503: 2, 508: 8, 528: 5, 532: 6, 546: 7, 550: 8, 554: 3, 558: 8, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 566: 8, 567: 5, 568: 2, 569: 3, 573: 1, 577: 8, 592: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 6, 707: 8, 711: 6, 715: 8, 717: 5, 753: 5, 761: 7, 789: 5, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 848: 4, 866: 4, 869: 4, 872: 1, 880: 6, 961: 8, 967: 4, 969: 8, 975: 2, 977: 8, 979: 8, 985: 5, 988: 6, 989: 8, 995: 7, 1001: 8, 1005: 6, 1009: 8, 1010: 8, 1013: 6, 1015: 1, 1017: 8, 1019: 2, 1020: 8, 1037: 5, 1105: 5, 1187: 5, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1236: 8, 1249: 8, 1257: 6, 1265: 8, 1275: 3, 1279: 4, 1280: 4, 1296: 4, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 1601: 8, 1616: 8, 1618: 8, 1905: 7, 1906: 7, 1907: 7, 1910: 7, 1912: 7, 1913: 7, 1922: 7, 1927: 7, 1930: 7, 2016: 8, 2020: 8, 2023: 8, 2024: 8, 2028: 8, 2031: 8
@@ -203,8 +195,7 @@ FINGERPRINTS = {
CAR.CADILLAC_XT4: [
# Cadillac XT4 w/ ACC 2023
{
190: 6, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 2, 241: 6, 249: 8, 257: 8, 288: 5, 289: 8, 292: 2, 298: 8, 304: 3, 309: 8, 313: 8, 320: 4, 322: 7, 328: 1, 331: 3, 352: 5, 353: 3, 368: 3, 381: 8, 384: 4, 386: 8, 388: 8, 393: 7, 398: 8, 401: 8, 407: 7, 413: 8, 417: 7, 419: 1, 422: 4, 426: 7, 431: 8, 442: 8, 451: 8, 452: 8, 453: 6, 455: 7, 479: 3, 481: 7, 485: 8, 489: 8, 497: 8, 499: 3, 500: 6, 501: 8, 503: 2, 508: 8, 532: 6, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 567: 5, 573: 1, 577: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 6, 707: 8, 715: 8, 717: 5, 719: 5, 761: 7, 767: 4, 806: 1, 840: 5, 842: 5, 844: 8, 866: 4, 869: 4, 872: 1, 880: 6, 961: 8, 969: 8, 975: 2, 977: 8, 979: 8, 985: 5, 1001: 8, 1005: 6, 1009: 8, 1011: 6, 1013: 5, 1017: 8, 1020: 8, 1033: 7, 1034: 7, 1037: 5, 1105: 5, 1187: 5, 1195: 3, 1217: 8, 1221: 5, 1223: 2, 1225: 7, 1233: 8, 1236: 8, 1249: 8, 1257: 6, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 1, 1268: 2, 1271: 8, 1273: 3, 1276: 2, 1277: 7, 1278: 4, 1279: 4, 1280: 4, 1296: 4, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 1345: 8, 1417: 8, 1512: 8, 1517: 8, 1601: 8, 1609: 8, 1613: 8, 1649: 8, 1792: 8, 1793: 8, 1798: 8, 1824: 8, 1825: 8, 1840: 8, 1842: 8, 1858: 8, 1860: 8, 1863: 8, 1872: 8, 1875: 8, 1882: 8, 1888: 8, 1889: 8, 1892: 8, 1906: 7, 1907: 7, 1912: 7, 1919: 7, 1920: 8, 1924: 8, 1930: 7, 1937: 8, 1953: 8, 1968: 8, 1969: 8, 1971: 8, 1975: 8, 1984: 8, 1988: 8, 2000: 8, 2001: 8, 2002: 8, 2016: 8, 2017: 8, 2018: 8, 2020: 8, 2021: 8, 2024: 8, 2026: 8
}],
190: 6, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 2, 241: 6, 249: 8, 257: 8, 288: 5, 289: 8, 292: 2, 298: 8, 304: 3, 309: 8, 313: 8, 320: 4, 322: 7, 328: 1, 331: 3, 352: 5, 353: 3, 368: 3, 381: 8, 384: 4, 386: 8, 388: 8, 393: 7, 398: 8, 401: 8, 407: 7, 413: 8, 417: 7, 419: 1, 422: 4, 426: 7, 431: 8, 442: 8, 451: 8, 452: 8, 453: 6, 455: 7, 479: 3, 481: 7, 485: 8, 489: 8, 497: 8, 499: 3, 500: 6, 501: 8, 503: 2, 508: 8, 532: 6, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 567: 5, 573: 1, 577: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 6, 707: 8, 715: 8, 717: 5, 719: 5, 761: 7, 767: 4, 806: 1, 840: 5, 842: 5, 844: 8, 866: 4, 869: 4, 872: 1, 880: 6, 961: 8, 969: 8, 975: 2, 977: 8, 979: 8, 985: 5, 1001: 8, 1005: 6, 1009: 8, 1011: 6, 1013: 5, 1017: 8, 1020: 8, 1033: 7, 1034: 7, 1037: 5, 1105: 5, 1187: 5, 1195: 3, 1217: 8, 1221: 5, 1223: 2, 1225: 7, 1233: 8, 1236: 8, 1249: 8, 1257: 6, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 1, 1268: 2, 1271: 8, 1273: 3, 1276: 2, 1277: 7, 1278: 4, 1279: 4, 1280: 4, 1296: 4, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 1345: 8, 1417: 8, 1512: 8, 1517: 8, 1601: 8, 1609: 8, 1613: 8, 1649: 8, 1792: 8, 1793: 8, 1798: 8, 1824: 8, 1825: 8, 1840: 8, 1842: 8, 1858: 8, 1860: 8, 1863: 8, 1872: 8, 1875: 8, 1882: 8, 1888: 8, 1889: 8, 1892: 8, 1906: 7, 1907: 7, 1912: 7, 1919: 7, 1920: 8, 1924: 8, 1930: 7, 1937: 8, 1953: 8, 1968: 8, 1969: 8, 1971: 8, 1975: 8, 1984: 8, 1988: 8, 2000: 8, 2001: 8, 2002: 8, 2016: 8, 2017: 8, 2018: 8, 2020: 8, 2021: 8, 2024: 8, 2026: 8 }],
CAR.CADILLAC_XT5_CC: [
# TRain's 2017 XT5
{
@@ -237,7 +228,7 @@ FINGERPRINTS = {
}],
CAR.CHEVROLET_MALIBU_HYBRID_CC: [
{
193: 8, 197: 8, 201: 8, 209: 7, 211: 2, 241: 6, 249: 8, 352: 5, 386: 8, 451: 8, 452: 8, 453: 6, 481: 7, 485: 8, 489: 8, 493: 8, 500: 6, 560: 8, 562: 8, 566: 6, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 707: 8, 717: 5, 761: 7, 810: 8, 840: 5, 842: 5, 844: 8, 869: 4
193:8, 197:8, 201:8, 209:7, 211:2, 241:6, 249:8, 352:5, 386:8, 451:8, 452:8, 453:6, 481:7, 485:8, 489:8, 493:8, 500:6, 560:8, 562:8, 566:6, 609:6, 610:6, 611:6, 612:8, 613:8, 707:8, 717:5, 761:7, 810:8, 840:5, 842:5, 844:8, 869:4
}],
CAR.CHEVROLET_TRAX: [
{

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