Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0495453e3a | |||
| 5340cc449c | |||
| 89f6f7a42b | |||
| 073d9ba098 | |||
| 3ad700a964 | |||
| c69a5080f4 | |||
| 6ab4400614 | |||
| 8222e303a2 | |||
| 43da13c8b0 | |||
| 1b1d60d088 | |||
| 9421030e2c | |||
| 930fa680cf | |||
| f4fb138009 | |||
| 35cee6a7f9 | |||
| 66fbf8b21f | |||
| f3306bec23 | |||
| 5338f9a5d5 | |||
| 6a702911ab | |||
| 4da1ddc500 | |||
| cb0f964e60 | |||
| a868fc6650 | |||
| 2f44ed860d | |||
| 9bb133a188 | |||
| 251b755efd | |||
| d56a8f6c23 | |||
| 894d792dbb | |||
| 91cb407979 | |||
| 2361ad82c9 | |||
| cea54bf498 | |||
| bc012595ca | |||
| c1df1eaf2a | |||
| 323e269a6b | |||
| 4c9430caf1 | |||
| 0b193e90f0 | |||
| c3d0c9c7c3 | |||
| 89d871ea40 | |||
| 77956f33c2 | |||
| 3e47e95934 | |||
| 3086285c72 | |||
| 5fc40a8936 | |||
| 19565e7aca | |||
| 22789bc95f | |||
| 22a949893d | |||
| 7e749a73a4 | |||
| 36c2cb5fb0 | |||
| fa9234212b | |||
| fd7b50a1a6 | |||
| 64087ac7ea | |||
| 961fc23845 | |||
| 6d98e4a784 | |||
| cfd8c78c4c | |||
| 1542e69a20 | |||
| ffdea13de8 | |||
| 60b000f7b5 | |||
| 1a27190a67 | |||
| 69703fd2ac | |||
| 365bf72ca2 | |||
| 7af2d11cab | |||
| 319b8c03f6 | |||
| 42cf37d175 | |||
| e767f8a3d2 | |||
| c159747e76 | |||
| 5e97a89432 | |||
| d056ca7a3f | |||
| a3df9b39f4 | |||
| 8818b7a1e2 | |||
| 7baa741d4f | |||
| becda14f28 | |||
| e9081aa1b7 | |||
| f86622bc96 | |||
| a74fc22d71 | |||
| ce0323e4c0 | |||
| ec63eca6ab | |||
| d79d4e9efd | |||
| 00a594f106 | |||
| e7ec4df222 | |||
| abac2983b5 | |||
| 50514c1e0e | |||
| d4b1f9612b | |||
| 6492e43ba3 | |||
| 9d96d2678e | |||
| 1d7934d112 | |||
| 9fe567174d | |||
| daf2e2e139 | |||
| 028eb11d88 | |||
| 54d02f9990 | |||
| 6cad16a714 | |||
| be6b3ea4ff | |||
| 47516dc865 | |||
| 84da3740ee | |||
| c41dc651c2 | |||
| 454c11b083 | |||
| aa51eee24e | |||
| 89d49e4d8d | |||
| f266e7de9d | |||
| dbef810865 | |||
| fd5033b16d | |||
| e5d59e6c45 | |||
| d7231a1a74 | |||
| 0bb8f383bf | |||
| f558bc6b61 | |||
| e79f98816d | |||
| 52f6ec0001 | |||
| f67af0f222 | |||
| 76d2b1eb56 | |||
| 8d9f88009d | |||
| ab036f7451 | |||
| 2792c69652 | |||
| d6ca567d5d | |||
| 01be6c4d81 | |||
| f23b77180e | |||
| 4df8dda8ba | |||
| 23bc2b80d8 | |||
| 5b823fee98 | |||
| a67d46b953 | |||
| 13c3070fa1 | |||
| 72dc8dd753 | |||
| b499c34970 | |||
| 0325685719 | |||
| 0d4ec3f1d8 | |||
| 8f010a4c3f | |||
| 2cafeee8c1 | |||
| 8bd757ca63 | |||
| 8cd5ee4f80 | |||
| 40bcc00261 | |||
| 785eee7fdb | |||
| 9e52017553 | |||
| 8907ac78c8 | |||
| ba5eea4253 | |||
| 7421c67a17 |
@@ -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},
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
Before Width: | Height: | Size: 503 KiB After Width: | Height: | Size: 875 KiB |
@@ -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,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}")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 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>
|
||||
`
|
||||
}
|
||||
@@ -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/s²</span>
|
||||
<span>0.0 m/s²</span>
|
||||
<span>${formatValue(range.min, 1)} m/s²</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>
|
||||
`;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 975 B After Width: | Height: | Size: 680 B |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 332 KiB After Width: | Height: | Size: 1008 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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!")), ""},
|
||||
|
||||
@@ -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."), ""},
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -66,11 +66,6 @@ void deleteThemeAsset(QDir &directory, const QString &subFolder, const QString &
|
||||
}
|
||||
|
||||
void downloadThemeAsset(const QString &input, const std::string ¶mKey, const QString &assetParam, Params ¶ms, Params ¶ms_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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
const uint8_t gitversion[] = "DEV-45631cf2-DEBUG";
|
||||
const uint8_t gitversion[] = "DEV-87d2f6ab-DEBUG";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 60–0 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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||