diff --git a/frogpilot/assets/stock_theme/colors/colors.json b/frogpilot/assets/stock_theme/colors/colors.json new file mode 100644 index 000000000..177c002a1 --- /dev/null +++ b/frogpilot/assets/stock_theme/colors/colors.json @@ -0,0 +1,44 @@ +{ + "LaneLines": { + "red": 255, + "green": 255, + "blue": 255, + "alpha": 178 + }, + "LeadMarker": { + "red": 201, + "green": 34, + "blue": 49, + "alpha": 255 + }, + "Path": { + "red": 48, + "green": 255, + "blue": 156, + "alpha": 255 + }, + "PathEdge": { + "red": 38, + "green": 209, + "blue": 125, + "alpha": 255 + }, + "Sidebar1": { + "red": 255, + "green": 255, + "blue": 255, + "alpha": 178 + }, + "Sidebar2": { + "red": 255, + "green": 255, + "blue": 255, + "alpha": 255 + }, + "Sidebar3": { + "red": 255, + "green": 255, + "blue": 255, + "alpha": 255 + } +} diff --git a/frogpilot/assets/stock_theme/distance_icons/aggressive.png b/frogpilot/assets/stock_theme/distance_icons/aggressive.png new file mode 100644 index 000000000..9aa3ea30f Binary files /dev/null and b/frogpilot/assets/stock_theme/distance_icons/aggressive.png differ diff --git a/frogpilot/assets/stock_theme/distance_icons/relaxed.png b/frogpilot/assets/stock_theme/distance_icons/relaxed.png new file mode 100644 index 000000000..d5c79a627 Binary files /dev/null and b/frogpilot/assets/stock_theme/distance_icons/relaxed.png differ diff --git a/frogpilot/assets/stock_theme/distance_icons/standard.png b/frogpilot/assets/stock_theme/distance_icons/standard.png new file mode 100644 index 000000000..9e1205739 Binary files /dev/null and b/frogpilot/assets/stock_theme/distance_icons/standard.png differ diff --git a/frogpilot/assets/stock_theme/distance_icons/traffic.png b/frogpilot/assets/stock_theme/distance_icons/traffic.png new file mode 100644 index 000000000..e028dc365 Binary files /dev/null and b/frogpilot/assets/stock_theme/distance_icons/traffic.png differ diff --git a/frogpilot/assets/stock_theme/icons b/frogpilot/assets/stock_theme/icons new file mode 120000 index 000000000..dad20fe21 --- /dev/null +++ b/frogpilot/assets/stock_theme/icons @@ -0,0 +1 @@ +../../../selfdrive/assets/images \ No newline at end of file diff --git a/frogpilot/assets/stock_theme/sounds b/frogpilot/assets/stock_theme/sounds new file mode 120000 index 000000000..30f6f70dd --- /dev/null +++ b/frogpilot/assets/stock_theme/sounds @@ -0,0 +1 @@ +../../../selfdrive/assets/sounds \ No newline at end of file diff --git a/frogpilot/assets/stock_theme/steering_wheel/wheel.png b/frogpilot/assets/stock_theme/steering_wheel/wheel.png new file mode 120000 index 000000000..7b82fc83d --- /dev/null +++ b/frogpilot/assets/stock_theme/steering_wheel/wheel.png @@ -0,0 +1 @@ +../../../selfdrive/assets/icons/chffr_wheel.png \ No newline at end of file diff --git a/frogpilot/assets/theme_manager.py b/frogpilot/assets/theme_manager.py new file mode 100644 index 000000000..750e2669a --- /dev/null +++ b/frogpilot/assets/theme_manager.py @@ -0,0 +1,522 @@ +#!/usr/bin/env python3 +import glob +import requests +import shutil + +from pathlib import Path +from urllib.parse import quote_plus + +from openpilot.frogpilot.common.frogpilot_download_utilities import GITLAB_URL, download_file, get_repository_url, handle_error, verify_download +from openpilot.frogpilot.common.frogpilot_utilities import delete_file, extract_zip, load_json_file, update_json_file +from openpilot.frogpilot.common.frogpilot_variables import ACTIVE_THEME_PATH, RESOURCES_REPO, THEME_SAVE_PATH + +CANCEL_DOWNLOAD_PARAM = "CancelThemeDownload" +DOWNLOAD_PROGRESS_PARAM = "ThemeDownloadProgress" + +STOCKOP_THEME_PATH = Path(__file__).parent / "stock_theme" + +THEME_COMPONENT_PARAMS = { + "colors": "ColorToDownload", + "distance_icons": "DistanceIconToDownload", + "icons": "IconToDownload", + "signals": "SignalToDownload", + "sounds": "SoundToDownload", + "steering_wheels": "WheelToDownload" +} + +class ThemeManager: + def __init__(self, params, params_memory, boot_run=False): + self.params = params + self.params_memory = params_memory + + self.downloading_theme = False + self.theme_updated = False + + self.previous_asset_mappings = {} + + self.theme_sizes_path = THEME_SAVE_PATH / "theme_sizes.json" + + self.theme_sizes = load_json_file(self.theme_sizes_path) + + self.session = requests.Session() + self.session.headers.update({ + "Accept": "application/vnd.github.v3+json", + "Accept-Language": "en", + "User-Agent": "frogpilot-theme-downloader/1.0 (https://github.com/FrogAi/FrogPilot)" + }) + + if boot_run: + self.copy_default_theme() + + @staticmethod + def copy_default_theme(): + world_frog_day_theme_path = HOLIDAY_THEME_PATH / "world_frog_day" + + for theme_subfolder_name, save_subfolder_path in [ + ("colors", "theme_packs/frog/colors"), + ("distance_icons", "theme_packs/frog-animated/distance_icons"), + ("icons", "theme_packs/frog-animated/icons"), + ("signals", "theme_packs/frog/signals"), + ("sounds", "theme_packs/frog/sounds"), + ]: + source_folder_path = world_frog_day_theme_path / theme_subfolder_name + destination_folder_path = THEME_SAVE_PATH / save_subfolder_path + destination_folder_path.mkdir(parents=True, exist_ok=True) + shutil.copytree(source_folder_path, destination_folder_path, dirs_exist_ok=True) + + steering_wheel_image_path = world_frog_day_theme_path / "steering_wheel/wheel.png" + steering_wheel_save_path = THEME_SAVE_PATH / "steering_wheels/frog.png" + steering_wheel_save_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(steering_wheel_image_path, steering_wheel_save_path) + + def download_theme(self, theme_component, theme_name, asset_param, frogpilot_toggles): + self.downloading_theme = True + + repo_url = get_repository_url(self.session) + if not repo_url: + handle_error(None, asset_param, "Repository unavailable", "GitHub and GitLab are offline...", self.params_memory, DOWNLOAD_PROGRESS_PARAM) + self.downloading_theme = False + return + + 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 + extension = ".zip" + elif theme_component == "steering_wheels": + download_link = f"{repo_url}/Steering-Wheels/{theme_name}" + download_path = THEME_SAVE_PATH / theme_component / theme_name + extension = ".gif" + else: + download_link = f"{repo_url}/Themes/{theme_name}/{theme_component}" + download_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component + extension = ".zip" + + theme_path = download_path.with_suffix(extension) + theme_url = download_link + extension + + delete_file(theme_path) + + print(f"Downloading theme from GitHub: {theme_name}") + download_file(CANCEL_DOWNLOAD_PARAM, theme_path, asset_param, self.params_memory, DOWNLOAD_PROGRESS_PARAM, self.session, theme_url) + + if theme_component == "steering_wheels" and not theme_path.exists() and theme_path.with_suffix(".png").exists(): + theme_path = theme_path.with_suffix(".png") + extension = ".png" + theme_url = theme_url.replace(".gif", ".png") + + if self.params_memory.get_bool(CANCEL_DOWNLOAD_PARAM): + delete_file(theme_path) + handle_error(None, asset_param, "Download cancelled...", "Download cancelled...", self.params_memory, DOWNLOAD_PROGRESS_PARAM) + + self.downloading_theme = False + return + + if verify_download(theme_path, self.params_memory, self.session, 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) + + if extension == ".zip": + self.params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...") + extract_zip(theme_path, download_path) + + self.params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!") + self.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 + + handle_error(download_path, asset_param, "Download failed...", "Download failed...", self.params_memory, DOWNLOAD_PROGRESS_PARAM) + self.downloading_theme = False + + def fetch_assets(self, repo_url, frogpilot_toggles): + is_github = "github" in repo_url + is_gitlab = "gitlab" in repo_url + + repo_encoded = quote_plus(RESOURCES_REPO) + + assets = {"themes": {}, "wheels": []} + try: + def list_files(branch): + if is_github: + response = self.session.get(f"https://api.github.com/repos/{RESOURCES_REPO}/git/trees/{branch}?recursive=1", timeout=10) + response.raise_for_status() + return [ + { + "path": item.get("path", ""), + "name": Path(item.get("path", "")).name, + "type": item.get("type"), + "size": item.get("size", 0), + } + for item in response.json().get("tree", []) + if item.get("type") == "blob" + ] + if is_gitlab: + response = self.session.get(f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/tree?ref={branch}&recursive=true", timeout=10) + response.raise_for_status() + return [ + { + "path": item.get("path", ""), + "name": item.get("name", ""), + "type": item.get("type"), + "size": 0, + } + for item in response.json() + if item.get("type") in ("blob", "file") + ] + print(f"Unsupported repository URL: {repo_url}") + return [] + + def file_size(branch, path, fallback): + if is_github: + return int(fallback or 0) + response = self.session.head(f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/files/{quote_plus(path)}/raw?ref={branch}", timeout=10) + return int(response.headers.get("content-length", 0)) if response.ok else 0 + + for branch in ["Distance-Icons", "Steering-Wheels"]: + for item in list_files(branch): + if item.get("type") not in ("file", "blob"): + continue + + path = item["path"] + size = file_size(branch, path, item.get("size", 0)) + + if branch == "Steering-Wheels": + assets["wheels"].append(path) + theme_name = Path(path).stem + local_files = list((THEME_SAVE_PATH / "steering_wheels").glob(f"{theme_name}.*")) + if local_files and size > 0: + local_size = self.theme_sizes.get("wheels", {}).get(theme_name) + if local_size != size: + self.download_theme("steering_wheels", theme_name, THEME_COMPONENT_PARAMS["steering_wheels"], frogpilot_toggles) + + elif branch == "Distance-Icons": + component_name = "distance_icons" + theme_name = Path(path).stem + assets["themes"].setdefault(theme_name, set()).add(component_name) + + local_path = THEME_SAVE_PATH / "theme_packs" / theme_name / component_name + if local_path.exists() and size > 0: + local_size = self.theme_sizes.get("themes", {}).get(theme_name, {}).get(component_name) + if local_size != size: + self.download_theme(component_name, theme_name, THEME_COMPONENT_PARAMS[component_name], frogpilot_toggles) + + branch = "Themes" + for item in list_files(branch): + if item.get("type") not in ("file", "blob") or "/" not in item["path"]: + continue + + expected_size = file_size(branch, item["path"], item.get("size", 0)) + + theme_name, sub_path = item["path"].split("/", 1) + theme_path = sub_path.lower() + + for key in ("colors", "icons", "signals", "sounds"): + if key in theme_path: + assets["themes"].setdefault(theme_name, set()).add(key) + + local_path = THEME_SAVE_PATH / "theme_packs" / theme_name / key + if local_path.exists(): + local_size = self.theme_sizes.get("themes", {}).get(theme_name, {}).get(key) + if local_size != expected_size: + print(f"{key} {theme_name} is outdated, redownloading...") + self.download_theme(key, theme_name, THEME_COMPONENT_PARAMS[key], frogpilot_toggles) + break + + assets["themes"] = {key: sorted(list(value)) for key, value in assets["themes"].items()} + assets["wheels"].sort() + return assets + + except requests.exceptions.RequestException as error: + print(f"Failed to fetch theme sizes from {'GitHub' if is_github else 'GitLab'}: {error}") + return {} + + @staticmethod + def format_name(name, component): + base = Path(name).stem + creator = "" + if "~" in base: + base, creator = base.split("~", 1) + + parts = base.replace("_", "-").split("-") + capitalized_parts = [part.capitalize() for part in parts if part] + + if len(capitalized_parts) > 1 and component != "steering_wheels": + display = f"{capitalized_parts[0]} ({' '.join(capitalized_parts[1:])})" + else: + display = " ".join(capitalized_parts) + + if creator: + return f"{display} - by: {creator}" + return display + + @staticmethod + def get_full_themes(): + theme_packs_path = THEME_SAVE_PATH / "theme_packs" + if not theme_packs_path.exists(): + return [] + + valid_themes = set() + for theme_directory in theme_packs_path.iterdir(): + if not theme_directory.is_dir(): + continue + + base_name = theme_directory.name.replace("-animated", "") + + animated_path = theme_packs_path / f"{base_name}-animated" + base_path = theme_packs_path / base_name + + base_valid = all((base_path / asset).is_dir() for asset in {"colors", "sounds"}) + animated_icons_exist = (animated_path / "icons").is_dir() + base_icons_exist = (base_path / "icons").is_dir() + + if base_valid and (animated_icons_exist or base_icons_exist): + if animated_icons_exist: + valid_themes.add(f"{base_name}-animated") + else: + valid_themes.add(base_name) + + return sorted(valid_themes) + + def handle_verification_failure(self, extension, theme_component, theme_name, asset_param, theme_path, download_path, frogpilot_toggles): + if theme_component == "distance_icons": + download_link = f"{GITLAB_URL}/Distance-Icons/{theme_name}" + elif theme_component == "steering_wheels": + download_link = f"{GITLAB_URL}/Steering-Wheels/{theme_name}" + else: + download_link = f"{GITLAB_URL}/Themes/{theme_name}/{theme_component}" + + delete_file(theme_path) + + theme_url = download_link + extension + print(f"Downloading theme from GitLab: {theme_name}") + download_file(CANCEL_DOWNLOAD_PARAM, theme_path, asset_param, self.params_memory, DOWNLOAD_PROGRESS_PARAM, self.session, theme_url) + + if theme_component == "steering_wheels" and not theme_path.exists() and theme_path.with_suffix(".png").exists(): + theme_path = theme_path.with_suffix(".png") + extension = ".png" + theme_url = theme_url.replace(".gif", ".png") + + if verify_download(theme_path, self.params_memory, self.session, 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": + self.params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...") + extract_zip(theme_path, download_path) + + self.params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!") + self.params_memory.remove(asset_param) + + self.downloading_theme = False + + self.update_themes(frogpilot_toggles) + return True + + handle_error(None, asset_param, "Download failed...", "Download failed...", self.params_memory, DOWNLOAD_PROGRESS_PARAM) + self.downloading_theme = False + return False + + def update_active_theme(self, frogpilot_toggles, boot_run=False): + asset_mappings = { + "color_scheme": ("colors", frogpilot_toggles.color_scheme), + "distance_icons": ("distance_icons", frogpilot_toggles.distance_icons), + "icon_pack": ("icons", frogpilot_toggles.icon_pack), + "sound_pack": ("sounds", frogpilot_toggles.sound_pack), + "turn_signal_pack": ("signals", frogpilot_toggles.signal_icons), + "wheel_image": ("wheel_image", frogpilot_toggles.wheel_image) + } + + if asset_mappings != self.previous_asset_mappings: + for asset, (asset_type, current_value) in asset_mappings.items(): + print(f"Updating {asset}: {asset_type} with value {current_value}") + + if asset_type == "wheel_image": + self.update_wheel_image(current_value, boot_run=boot_run) + else: + self.update_theme_asset(asset_type, current_value, boot_run=boot_run) + + self.previous_asset_mappings = asset_mappings + + self.theme_updated = True + + def update_theme_asset(self, asset_type, theme, boot_run=False): + save_location = ACTIVE_THEME_PATH / asset_type + asset_location = THEME_SAVE_PATH / "theme_packs" / theme / asset_type + + if not asset_location.exists() or theme == "stock": + asset_location = STOCKOP_THEME_PATH / asset_type + print(f"Using the stock {asset_type[:-1]} instead") + + delete_file(save_location, print_error=not boot_run) + + save_location.parent.mkdir(parents=True, exist_ok=True) + save_location.symlink_to(asset_location, target_is_directory=True) + print(f"Linked {save_location} to {asset_location}") + + def update_theme_params(self, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels): + def update_param(key, assets, subfolder): + 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: + themes_path = THEME_SAVE_PATH / "theme_packs" + existing_assets = {self.format_name(item.parent.name, subfolder) for item in themes_path.glob(f"*/{subfolder}") if item.is_dir()} + + self.params.put(key, ",".join(sorted(set(assets) - existing_assets))) + print(f"{key} updated successfully") + + update_param("DownloadableColors", downloadable_colors, "colors") + update_param("DownloadableDistanceIcons", downloadable_distance_icons, "distance_icons") + update_param("DownloadableIcons", downloadable_icons, "icons") + update_param("DownloadableSignals", downloadable_signals, "signals") + update_param("DownloadableSounds", downloadable_sounds, "sounds") + update_param("DownloadableWheels", downloadable_wheels, "steering_wheels") + + downloaded_themes = {} + for theme_dir in (THEME_SAVE_PATH / "theme_packs").iterdir(): + components = [] + for component in ["colors", "distance_icons", "icons", "signals", "sounds"]: + if (theme_dir / component).is_dir(): + components.append(component) + + if components: + theme_name = self.format_name(theme_dir.name, "theme_packs") + downloaded_themes[theme_name] = sorted(components) + + 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")) + + self.params.put("ThemesDownloaded", { + "themes": {key: downloaded_themes[key] for key in sorted(downloaded_themes)}, + "steering_wheels": sorted(downloaded_wheels) + }) + + print("ThemesDownloaded updated successfully") + + def update_theme_size(self, theme_component, theme_name, file_size): + if theme_component == "steering_wheels": + key = "wheels" + else: + key = "themes" + + if key not in self.theme_sizes: + self.theme_sizes[key] = {} + + if key == "wheels": + self.theme_sizes[key][theme_name] = file_size + else: + if theme_name not in self.theme_sizes[key]: + self.theme_sizes[key][theme_name] = {} + self.theme_sizes[key][theme_name][theme_component] = file_size + + update_json_file(self.theme_sizes_path, self.theme_sizes) + + def update_themes(self, frogpilot_toggles, boot_run=False): + if self.downloading_theme: + return + + repo_url = get_repository_url(self.session) + if repo_url is None: + print("GitHub and GitLab are offline...") + return + + assets = self.fetch_assets(repo_url, frogpilot_toggles) + if not assets: + return + + downloadable_colors = [] + downloadable_distance_icons = [] + downloadable_icons = [] + downloadable_signals = [] + downloadable_sounds = [] + + for theme, available_assets in assets["themes"].items(): + theme_name = self.format_name(theme, "theme_packs") + print(f"Theme found: {theme_name}") + + if "colors" in available_assets: + downloadable_colors.append(theme_name) + if "distance_icons" in available_assets: + downloadable_distance_icons.append(theme_name) + if "icons" in available_assets: + downloadable_icons.append(theme_name) + if "signals" in available_assets: + downloadable_signals.append(theme_name) + if "sounds" in available_assets: + downloadable_sounds.append(theme_name) + + downloadable_wheels = [self.format_name(wheel, "steering_wheels") for wheel in assets["wheels"]] + + print(f"Downloadable Colors: {downloadable_colors}") + print(f"Downloadable Icons: {downloadable_icons}") + print(f"Downloadable Signals: {downloadable_signals}") + print(f"Downloadable Sounds: {downloadable_sounds}") + print(f"Downloadable Distance Icons: {downloadable_distance_icons}") + print(f"Downloadable Wheels: {downloadable_wheels}") + + if boot_run: + self.validate_themes(downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels, frogpilot_toggles) + + self.update_theme_params(downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels) + + def update_wheel_image(self, image, boot_run=False): + wheel_save_location = ACTIVE_THEME_PATH / "steering_wheel" + + if image == "stock": + wheel_location = STOCKOP_THEME_PATH / "steering_wheel" + else: + wheel_location = THEME_SAVE_PATH / "steering_wheels" + + if not wheel_location.exists(): + wheel_location = STOCKOP_THEME_PATH / "steering_wheel" + print("Using the stock steering wheel instead") + + delete_file(wheel_save_location, print_error=not boot_run) + wheel_save_location.mkdir(parents=True, exist_ok=True) + + image_name = image.replace(" ", "_").lower() + matching_files = [images for images in wheel_location.iterdir() if images.stem.lower() in {image_name, "wheel"}] + if matching_files: + source_file = matching_files[0] + destination_file = wheel_save_location / f"wheel{source_file.suffix}" + destination_file.symlink_to(source_file) + print(f"Linked {destination_file} to {source_file}") + + def validate_themes(self, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels, frogpilot_toggles): + downloaded_data = self.params.get("ThemesDownloaded") + + 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") + + for component in components: + component_path = THEME_SAVE_PATH / "theme_packs" / theme_folder_name / component + if not component_path.is_dir() or not any(component_path.iterdir()): + print(f"Missing or empty component '{component}' for theme '{theme_folder_name}'. Downloading...") + self.download_theme(component, theme_folder_name, THEME_COMPONENT_PARAMS.get(component), frogpilot_toggles) + self.update_active_theme(frogpilot_toggles) + + wheels_path = THEME_SAVE_PATH / "steering_wheels" + for display_name in downloaded_data.get("steering_wheels", []): + file_stem = display_name.replace(" ", "_").lower() + matching_files = list(wheels_path.glob(f"{file_stem}.*")) + if not matching_files: + print(f"Missing steering wheel '{display_name}'. Downloading...") + self.download_theme("steering_wheels", file_stem, THEME_COMPONENT_PARAMS["steering_wheels"], frogpilot_toggles) + self.update_active_theme(frogpilot_toggles) + + for dir_path in THEME_SAVE_PATH.glob("**/*"): + if dir_path.is_dir() and not any(dir_path.iterdir()): + print(f"Deleting empty folder: {dir_path}") + delete_file(dir_path) + elif dir_path.is_file() and dir_path.name.startswith("tmp"): + print(f"Deleting temp file: {dir_path}") + delete_file(dir_path) + + print("Theme validation complete.") diff --git a/frogpilot/common/frogpilot_functions.py b/frogpilot/common/frogpilot_functions.py index f5192d84a..b5bf3a5ce 100644 --- a/frogpilot/common/frogpilot_functions.py +++ b/frogpilot/common/frogpilot_functions.py @@ -11,11 +11,12 @@ from openpilot.common.params import Params from openpilot.common.time_helpers import system_time_valid from openpilot.system.hardware import HARDWARE +from openpilot.frogpilot.assets.theme_manager import ThemeManager from openpilot.frogpilot.common.frogpilot_backups import backup_frogpilot from openpilot.frogpilot.common.frogpilot_utilities import is_FrogsGoMoo, run_cmd from openpilot.frogpilot.common.frogpilot_variables import ( - FROGS_GO_MOO_PATH, - FrogPilotVariables + FROGS_GO_MOO_PATH, THEME_SAVE_PATH, + FrogPilotVariables, get_frogpilot_toggles ) @@ -23,6 +24,7 @@ def frogpilot_boot_functions(build_metadata, params): params_memory = Params(memory=True) FrogPilotVariables() + ThemeManager(params, params_memory, boot_run=True).update_active_theme(frogpilot_toggles=get_frogpilot_toggles(), boot_run=True) def boot_thread(): while not system_time_valid(): @@ -36,6 +38,7 @@ def frogpilot_boot_functions(build_metadata, params): def install_frogpilot(build_metadata, params): paths = [ + THEME_SAVE_PATH ] for path in paths: path.mkdir(parents=True, exist_ok=True) diff --git a/frogpilot/common/frogpilot_variables.py b/frogpilot/common/frogpilot_variables.py index ab9f19d64..aa5415b96 100644 --- a/frogpilot/common/frogpilot_variables.py +++ b/frogpilot/common/frogpilot_variables.py @@ -148,8 +148,18 @@ class FrogPilotVariables: self.frogs_go_moo = FROGS_GO_MOO_PATH.is_file() toggle.block_user = (self.development_branch or branch == "MAKE-PRS-HERE" or self.vetting_branch) and not self.frogs_go_moo + stock_colors_json = (STOCK_THEME_PATH / "colors/colors.json") + self.stock_colors = json.loads(stock_colors_json.read_text()) if stock_colors_json.is_file() else {} + self.update() + def get_color(self, key, theme_colors): + for source in (theme_colors, self.stock_colors): + color = source.get(key) + if isinstance(color, dict): + return f"#{color.get("alpha", 255):02X}{color.get("red", 255):02X}{color.get("green", 255):02X}{color.get("blue", 255):02X}" + return "#FFFFFFFF" + def get_value(self, key, cast=bool, condition=True, conversion=None, default=None, min=None, max=None): if not condition or (self.tuning_level < self.tuning_levels.get(key, 0)): if default is not None: @@ -336,6 +346,22 @@ class FrogPilotVariables: toggle.traffic_mode_jerk_speed_decrease = [self.get_value("TrafficJerkSpeedDecrease", cast=float, condition=toggle.custom_personalities, conversion=0.01, min=0.25, max=2.0), toggle.aggressive_jerk_speed_decrease] toggle.traffic_mode_follow = [self.get_value("TrafficFollow", cast=float, condition=toggle.custom_personalities, min=0.5, max=MAX_T_FOLLOW), toggle.aggressive_follow] + custom_themes = self.get_value("CustomThemes") + toggle.color_scheme = self.get_value("ColorScheme", cast=None, condition=custom_themes, default="stock") + theme_colors = json.loads(THEME_COLORS_PATH.read_text()) if THEME_COLORS_PATH.is_file() else {} + toggle.lane_lines_color = self.get_color("LaneLines", theme_colors) + toggle.lead_marker_color = self.get_color("LeadMarker", theme_colors) + toggle.path_color = self.get_color("Path", theme_colors) + toggle.path_edges_color = self.get_color("PathEdge", theme_colors) + toggle.sidebar_color1 = self.get_color("Sidebar1", theme_colors) + toggle.sidebar_color2 = self.get_color("Sidebar2", theme_colors) + toggle.sidebar_color3 = self.get_color("Sidebar3", theme_colors) + toggle.distance_icons = self.get_value("DistanceIconPack", cast=None, condition=custom_themes, default="stock") + toggle.icon_pack = self.get_value("IconPack", cast=None, condition=custom_themes, default="stock") + toggle.signal_icons = self.get_value("SignalAnimation", cast=None, condition=custom_themes, default="stock") + toggle.sound_pack = self.get_value("SoundPack", cast=None, condition=custom_themes, default="stock") + toggle.wheel_image = self.get_value("WheelIcon", cast=None, condition=custom_themes, default="stock") + custom_ui = self.get_value("CustomUI") toggle.acceleration_path = toggle.openpilot_longitudinal and (self.get_value("AccelerationPath", condition=custom_ui)) toggle.adjacent_paths = self.get_value("AdjacentPath", condition=custom_ui) diff --git a/frogpilot/controls/frogpilot_planner.py b/frogpilot/controls/frogpilot_planner.py index a9b4a0cc3..350e63a06 100644 --- a/frogpilot/controls/frogpilot_planner.py +++ b/frogpilot/controls/frogpilot_planner.py @@ -106,7 +106,7 @@ class FrogPilotPlanner: self.tracking_lead_filter.update(following_lead) return self.tracking_lead_filter.x >= THRESHOLD - def publish(self, sm, pm, frogpilot_toggles): + def publish(self, theme_updated, sm, pm, frogpilot_toggles): frogpilot_plan_send = messaging.new_message("frogpilotPlan") frogpilot_plan_send.valid = sm.all_checks(service_list=["carState", "controlsState", "selfdriveState", "radarState"]) frogpilotPlan = frogpilot_plan_send.frogpilotPlan @@ -136,6 +136,8 @@ class FrogPilotPlanner: frogpilotPlan.roadCurvature = self.road_curvature + frogpilotPlan.themeUpdated = theme_updated + frogpilotPlan.vCruise = float(self.v_cruise) pm.send("frogpilotPlan", frogpilot_plan_send) diff --git a/frogpilot/frogpilot_process.py b/frogpilot/frogpilot_process.py index f8d21b0a7..eeb2b5c35 100644 --- a/frogpilot/frogpilot_process.py +++ b/frogpilot/frogpilot_process.py @@ -8,6 +8,7 @@ from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL, Priority, Ratekeeper, config_realtime_process from openpilot.common.time_helpers import system_time_valid +from openpilot.frogpilot.assets.theme_manager import THEME_COMPONENT_PARAMS, ThemeManager from openpilot.frogpilot.common.frogpilot_backups import backup_toggles from openpilot.frogpilot.common.frogpilot_functions import update_openpilot from openpilot.frogpilot.common.frogpilot_utilities import ThreadManager, is_url_pingable @@ -18,7 +19,11 @@ from openpilot.frogpilot.system.frogpilot_tracking import FrogPilotTracking ASSET_CHECK_RATE = (1 / DT_MDL) -def check_assets(thread_manager, params_memory, frogpilot_toggles): +def check_assets(theme_manager, thread_manager, params_memory, frogpilot_toggles): + for asset_type, asset_param in THEME_COMPONENT_PARAMS.items(): + asset_to_download = params_memory.get(asset_param) + if asset_to_download: + thread_manager.run_with_lock(theme_manager.download_theme, (asset_type, asset_to_download, asset_param, frogpilot_toggles)) def transition_offroad(frogpilot_planner, thread_manager, time_validated, sm, params, frogpilot_toggles): params.put("LastGPSPosition", json.dumps(frogpilot_planner.gps_position)) @@ -28,19 +33,23 @@ def transition_offroad(frogpilot_planner, thread_manager, time_validated, sm, pa def transition_onroad(): -def update_checks(now, thread_manager, params, params_memory, frogpilot_toggles, boot_run=False): +def update_checks(now, theme_manager, thread_manager, params, params_memory, frogpilot_toggles, boot_run=False): while not (is_url_pingable("https://github.com") or is_url_pingable("https://gitlab.com")): time.sleep(60) + theme_manager.update_themes(frogpilot_toggles, boot_run) + if frogpilot_toggles.automatic_updates: thread_manager.run_with_lock(update_openpilot, (thread_manager, params)) time.sleep(1) -def update_toggles(frogpilot_variables, started, thread_manager, time_validated, params): +def update_toggles(frogpilot_variables, started, theme_manager, thread_manager, time_validated, params): frogpilot_variables.update(started) frogpilot_toggles = frogpilot_variables.frogpilot_toggles + theme_manager.update_active_theme(frogpilot_toggles) + if time_validated: thread_manager.run_with_lock(backup_toggles, (params)) @@ -62,6 +71,7 @@ def frogpilot_thread(): params_memory = Params(memory=True) frogpilot_variables = FrogPilotVariables() + theme_manager = ThemeManager(params, params_memory) thread_manager = ThreadManager() frogpilot_toggles = frogpilot_variables.frogpilot_toggles @@ -78,7 +88,7 @@ def frogpilot_thread(): started = sm["deviceState"].started if not started and started_previously: - frogpilot_toggles = update_toggles(frogpilot_variables, started, thread_manager, time_validated, params) + frogpilot_toggles = update_toggles(frogpilot_variables, started, theme_manager, thread_manager, time_validated, params) transition_offroad(frogpilot_planner, thread_manager, time_validated, sm, params, frogpilot_toggles) run_update_checks = True @@ -90,28 +100,29 @@ def frogpilot_thread(): if started and sm.updated["modelV2"]: frogpilot_planner.update(now, time_validated, sm, frogpilot_toggles) - frogpilot_planner.publish(sm, pm, frogpilot_toggles) + frogpilot_planner.publish(theme_manager.theme_updated, sm, pm, frogpilot_toggles) frogpilot_tracking.update(now, time_validated, sm, frogpilot_toggles) elif not started: frogpilot_plan_send = messaging.new_message("frogpilotPlan") frogpilot_plan_send.frogpilotPlan.frogpilotToggles = json.dumps(vars(frogpilot_toggles)) + frogpilot_plan_send.frogpilotPlan.themeUpdated = theme_manager.theme_updated pm.send("frogpilotPlan", frogpilot_plan_send) started_previously = started if rate_keeper.frame % ASSET_CHECK_RATE == 0: - check_assets(thread_manager, params_memory, frogpilot_toggles) + check_assets(theme_manager, thread_manager, params_memory, frogpilot_toggles) if params_memory.get_bool("FrogPilotTogglesUpdated"): - frogpilot_toggles = update_toggles(frogpilot_variables, started, thread_manager, time_validated, params) + frogpilot_toggles = update_toggles(frogpilot_variables, started, theme_manager, thread_manager, time_validated, params) run_update_checks |= params_memory.get_bool("ManualUpdateInitiated") run_update_checks |= now.second == 0 and (now.minute % 60 == 0 or (now.minute % 5 == 0 and frogpilot_variables.frogs_go_moo)) run_update_checks &= time_validated if run_update_checks: - thread_manager.run_with_lock(update_checks, (now, thread_manager, params, params_memory, frogpilot_toggles)) + thread_manager.run_with_lock(update_checks, (now, theme_manager, thread_manager, params, params_memory, frogpilot_toggles)) run_update_checks = False elif not time_validated: @@ -121,7 +132,7 @@ def frogpilot_thread(): thread_manager.run_with_lock(backup_toggles, (params, True)) thread_manager.run_with_lock(send_stats, (params, frogpilot_toggles)) - thread_manager.run_with_lock(update_checks, (now, thread_manager, params, params_memory, frogpilot_toggles, True)) + thread_manager.run_with_lock(update_checks, (now, theme_manager, thread_manager, params, params_memory, frogpilot_toggles, True)) rate_keeper.keep_time() diff --git a/frogpilot/ui/frogpilot_ui.cc b/frogpilot/ui/frogpilot_ui.cc index 05ab0b513..dc63586e8 100644 --- a/frogpilot/ui/frogpilot_ui.cc +++ b/frogpilot/ui/frogpilot_ui.cc @@ -16,6 +16,9 @@ static void update_state(FrogPilotUIState *fs) { } if (fpsm.updated("frogpilotPlan")) { const cereal::FrogPilotPlan::Reader &frogpilotPlan = fpsm["frogpilotPlan"].getFrogpilotPlan(); + if (frogpilotPlan.getThemeUpdated()) { + emit fs->themeUpdated(); + } capnp::Text::Reader toggles = frogpilotPlan.getFrogpilotToggles(); QByteArray current_toggles(toggles.cStr(), toggles.size()); static QByteArray previous_toggles; diff --git a/frogpilot/ui/frogpilot_ui.h b/frogpilot/ui/frogpilot_ui.h index 8eba66a1e..09ef20528 100644 --- a/frogpilot/ui/frogpilot_ui.h +++ b/frogpilot/ui/frogpilot_ui.h @@ -39,6 +39,7 @@ public: WifiManager *wifi; signals: + void themeUpdated(); }; FrogPilotUIState *frogpilotUIState(); diff --git a/frogpilot/ui/qt/onroad/frogpilot_annotated_camera.cc b/frogpilot/ui/qt/onroad/frogpilot_annotated_camera.cc index 4f11a07b3..17454989d 100644 --- a/frogpilot/ui/qt/onroad/frogpilot_annotated_camera.cc +++ b/frogpilot/ui/qt/onroad/frogpilot_annotated_camera.cc @@ -1,6 +1,8 @@ #include "frogpilot/ui/qt/onroad/frogpilot_annotated_camera.h" FrogPilotAnnotatedCameraWidget::FrogPilotAnnotatedCameraWidget(QWidget *parent) : QWidget(parent) { + animationTimer = new QTimer(this); + QSize iconSize(img_size / 4, img_size / 4); curveSpeedIcon = loadPixmap("../../frogpilot/assets/other_images/curve_speed.png", {btn_size, btn_size}); @@ -13,9 +15,96 @@ FrogPilotAnnotatedCameraWidget::FrogPilotAnnotatedCameraWidget(QWidget *parent) loadGif("../../frogpilot/assets/other_images/turn_icon.gif", cemTurnIcon, QSize(widget_size, widget_size), this); loadGif("../../frogpilot/assets/other_images/chill_mode_icon.gif", chillModeIcon, QSize(widget_size, widget_size), this); loadGif("../../frogpilot/assets/other_images/experimental_mode_icon.gif", experimentalModeIcon, QSize(widget_size, widget_size), this); + + QObject::connect(animationTimer, &QTimer::timeout, [this] { + animationFrameIndex = (animationFrameIndex + 1) % totalFrames; + }); + QObject::connect(frogpilotUIState(), &FrogPilotUIState::themeUpdated, this, &FrogPilotAnnotatedCameraWidget::updateSignals); + QObject::connect(uiState(), &UIState::offroadTransition, [this] { + QJsonObject stats = QJsonDocument::fromJson(QString::fromStdString(params.get("FrogPilotStats")).toUtf8()).object(); + stats["FrogHops"] = stats.value("FrogHops").toInt(0) + frogHopCount; + params.putNonBlocking("FrogPilotStats", QJsonDocument(stats).toJson(QJsonDocument::Compact).toStdString()); + + frogHopCount = 0; + }); } void FrogPilotAnnotatedCameraWidget::showEvent(QShowEvent *event) { + updateSignals(); +} + +void FrogPilotAnnotatedCameraWidget::updateSignals() { + QVector().swap(blindspotImages); + QVector().swap(blindspotImagesRight); + QVector().swap(signalImages); + QVector().swap(signalImagesRight); + + bool isGif = false; + + QFileInfoList files = QDir("../../frogpilot/assets/active_theme/signals/").entryInfoList(QDir::Files | QDir::NoDotAndDotDot, QDir::Name); + for (const QFileInfo &fileInfo : files) { + QString fileName = fileInfo.fileName(); + QString filePath = fileInfo.absoluteFilePath(); + + if (fileName.endsWith(".gif", Qt::CaseInsensitive)) { + isGif = true; + + QMovie movie(filePath); + movie.setCacheMode(QMovie::CacheNone); + movie.start(); + + int frameCount = movie.frameCount(); + signalImages.reserve(frameCount); + signalImagesRight.reserve(frameCount); + + for (int i = 0; i < frameCount; ++i) { + movie.jumpToFrame(i); + + QPixmap frame = movie.currentPixmap(); + signalImages.append(frame); + signalImagesRight.append(frame.transformed(QTransform().scale(-1, 1))); + } + + movie.stop(); + } else if (fileName.endsWith(".png", Qt::CaseInsensitive)) { + QPixmap img(filePath); + if (fileName.contains("blindspot", Qt::CaseInsensitive)) { + blindspotImages.append(img); + blindspotImagesRight.append(img.transformed(QTransform().scale(-1, 1))); + } else { + signalImages.append(img); + signalImagesRight.append(img.transformed(QTransform().scale(-1, 1))); + } + } else { + QStringList parts = fileName.split('_'); + if (parts.size() == 2) { + signalStyle = parts[0]; + signalAnimationLength = parts[1].toInt(); + } + } + } + + if (!signalImages.isEmpty()) { + QPixmap &firstImage = signalImages.front(); + signalHeight = firstImage.height(); + signalWidth = firstImage.width(); + totalFrames = signalImages.size(); + + if (isGif && signalStyle == "traditional") { + signalMovement = (width() + signalWidth * 2) / totalFrames; + signalStyle = "traditional_gif"; + } else { + signalMovement = 0; + } + } else { + signalAnimationLength = 0; + signalHeight = 0; + signalMovement = 0; + signalWidth = 0; + totalFrames = 0; + + signalStyle = "None"; + } } void FrogPilotAnnotatedCameraWidget::updateState(const UIState &s, const FrogPilotUIState &fs) { @@ -51,6 +140,8 @@ void FrogPilotAnnotatedCameraWidget::updateState(const UIState &s, const FrogPil blindspotLeft = carState.getLeftBlindspot(); blindspotRight = carState.getRightBlindspot(); + blinkerLeft = carState.getLeftBlinker(); + blinkerRight = carState.getRightBlinker(); cscControllingSpeed = frogpilotPlan.getCscControllingSpeed(); cscSpeed = frogpilotPlan.getCscSpeed(); cscTraining = frogpilotPlan.getCscTraining(); @@ -59,6 +150,22 @@ void FrogPilotAnnotatedCameraWidget::updateState(const UIState &s, const FrogPil hideBottomIcons = selfdriveState.getAlertSize() != cereal::SelfdriveState::AlertSize::NONE; hideBottomIcons |= frogpilotSelfdriveState.getAlertSize() != cereal::FrogPilotSelfdriveState::AlertSize::NONE; + hideBottomIcons |= signalStyle.startsWith("traditional") && (blinkerLeft || blinkerRight); + + static int lastFrameIndex; + if (lastFrameIndex > animationFrameIndex && frogpilot_toggles.value("signal_icons").toString() == "frog") { + frogHopCount++; + } + lastFrameIndex = animationFrameIndex; + + if ((blinkerLeft || blinkerRight) && signalStyle != "None") { + if (!animationTimer->isActive()) { + animationTimer->start(signalAnimationLength); + } + } else if (animationTimer->isActive()) { + animationFrameIndex = 0; + animationTimer->stop(); + } if (cscTraining) { if (!glowTimer.isValid()) { @@ -95,6 +202,10 @@ void FrogPilotAnnotatedCameraWidget::paintFrogPilotWidgets(QPainter &p, UIState paintCurveSpeedControl(p); } } + + if ((blinkerLeft || blinkerRight) && signalStyle != "None") { + paintTurnSignals(p); + } } void FrogPilotAnnotatedCameraWidget::paintBlindSpotPath(QPainter &p) { @@ -306,3 +417,32 @@ void FrogPilotAnnotatedCameraWidget::paintCurveSpeedControlTraining(QPainter &p) p.restore(); } + +void FrogPilotAnnotatedCameraWidget::paintTurnSignals(QPainter &p) { + int frameIndex = qBound(0, animationFrameIndex, totalFrames - 1); + + bool blindspotActive = blinkerLeft ? blindspotLeft : blindspotRight; + + int signalXPosition = 0; + int signalYPosition = 0; + + if (signalStyle == "static") { + signalXPosition = blinkerLeft ? (rect().center().x() * 0.75) - signalWidth : rect().center().x() * 1.25; + signalYPosition = signalHeight / 2; + } else { + if (signalStyle == "traditional_gif") { + signalXPosition = blinkerLeft ? width() - (frameIndex * signalMovement) + signalWidth : (frameIndex * signalMovement) - signalWidth; + } else { + signalXPosition = blinkerLeft ? width() - ((frameIndex + 1) * signalWidth) : frameIndex * signalWidth; + } + signalYPosition = height() - signalHeight - alertHeight; + } + + if (blinkerLeft) { + QPixmap &imgToDraw = (blindspotActive && !blindspotImages.empty()) ? blindspotImages[0] : signalImages[frameIndex]; + p.drawPixmap(signalXPosition, signalYPosition, signalWidth, signalHeight, imgToDraw); + } else { + QPixmap &imgToDraw = (blindspotActive && !blindspotImagesRight.empty()) ? blindspotImagesRight[0] : signalImagesRight[frameIndex]; + p.drawPixmap(signalXPosition, signalYPosition, signalWidth, signalHeight, imgToDraw); + } +} diff --git a/frogpilot/ui/qt/onroad/frogpilot_annotated_camera.h b/frogpilot/ui/qt/onroad/frogpilot_annotated_camera.h index f82e4efa7..f0f2eb3cc 100644 --- a/frogpilot/ui/qt/onroad/frogpilot_annotated_camera.h +++ b/frogpilot/ui/qt/onroad/frogpilot_annotated_camera.h @@ -20,6 +20,8 @@ public: bool isCruiseSet; bool rightHandDM; + int alertHeight; + float speed; FrogPilotUIScene frogpilot_scene; @@ -37,6 +39,8 @@ public: QSize defaultSize; + QString signalStyle; + protected: void showEvent(QShowEvent *event) override; @@ -45,13 +49,25 @@ private: void paintCompass(QPainter &p); void paintCurveSpeedControl(QPainter &p); void paintCurveSpeedControlTraining(QPainter &p); + void paintTurnSignals(QPainter &p); + void updateSignals(); bool blindspotLeft; bool blindspotRight; + bool blinkerLeft; + bool blinkerRight; bool cscControllingSpeed; bool cscTraining; bool experimentalMode; + int animationFrameIndex; + int frogHopCount; + int signalAnimationLength; + int signalHeight; + int signalMovement; + int signalWidth; + int totalFrames; + float cscSpeed; float distanceConversion; float roadCurvature; @@ -84,4 +100,11 @@ private: QString leadDistanceUnit; QString leadSpeedUnit; QString speedUnit; + + QTimer *animationTimer; + + QVector blindspotImages; + QVector blindspotImagesRight; + QVector signalImages; + QVector signalImagesRight; }; diff --git a/selfdrive/ui/qt/onroad/buttons.cc b/selfdrive/ui/qt/onroad/buttons.cc index 722cad7ee..046a0af87 100644 --- a/selfdrive/ui/qt/onroad/buttons.cc +++ b/selfdrive/ui/qt/onroad/buttons.cc @@ -24,6 +24,7 @@ ExperimentalButton::ExperimentalButton(QWidget *parent) : experimental_mode(fals QObject::connect(this, &QPushButton::clicked, this, &ExperimentalButton::changeMode); // FrogPilot variables + QObject::connect(frogpilotUIState(), &FrogPilotUIState::themeUpdated, this, &ExperimentalButton::updateTheme); } void ExperimentalButton::changeMode() { @@ -57,12 +58,22 @@ void ExperimentalButton::updateState(const UIState &s, const FrogPilotUIState &f void ExperimentalButton::paintEvent(QPaintEvent *event) { QPainter p(this); - QPixmap img = experimental_mode ? experimental_img : engage_img; - drawIcon(p, QPoint(btn_size / 2, btn_size / 2), img, background_color, (isDown() || !engageable) ? 0.6 : 1.0); + p.setClipRegion(QRegion(QRect(0, 0, btn_size, btn_size), QRegion::Ellipse)); + p.setRenderHint(QPainter::Antialiasing); + + if (frogpilot_toggles.value("wheel_image").toString() == "stock") { + QPixmap img = experimental_mode ? experimental_img : engage_img; + drawIcon(p, QPoint(btn_size / 2, btn_size / 2), img, background_color, (isDown() || !engageable) ? 0.6 : 1.0); + } else if (wheel_gif) { + drawIcon(p, QPoint(btn_size / 2, btn_size / 2), wheel_gif->currentPixmap(), background_color, (isDown() || !engageable) ? 0.6 : 1.0); + } else if (!wheel_img.isNull()) { + drawIcon(p, QPoint(btn_size / 2, btn_size / 2), wheel_img, background_color, (isDown() || !engageable) ? 0.6 : 1.0); + } } // FrogPilot variables void ExperimentalButton::showEvent(QShowEvent *event) { + updateTheme(); } void ExperimentalButton::updateBackgroundColor() { @@ -78,3 +89,7 @@ void ExperimentalButton::updateBackgroundColor() { background_color = QColor(0, 0, 0, 166); } } + +void ExperimentalButton::updateTheme() { + loadImage("../../frogpilot/assets/active_theme/steering_wheel/wheel", wheel_img, wheel_gif, QSize(img_size, img_size), this); +} diff --git a/selfdrive/ui/qt/onroad/buttons.h b/selfdrive/ui/qt/onroad/buttons.h index d006891db..889419503 100644 --- a/selfdrive/ui/qt/onroad/buttons.h +++ b/selfdrive/ui/qt/onroad/buttons.h @@ -32,10 +32,15 @@ private: // FrogPilot variables void showEvent(QShowEvent *event) override; void updateBackgroundColor(); + void updateTheme(); Params params_memory{"", true}; QColor background_color; + + QPixmap wheel_img; + + QSharedPointer wheel_gif; }; void drawIcon(QPainter &p, const QPoint ¢er, const QPixmap &img, const QBrush &bg, float opacity); diff --git a/selfdrive/ui/qt/onroad/model.cc b/selfdrive/ui/qt/onroad/model.cc index db2d8d087..3bacd090f 100644 --- a/selfdrive/ui/qt/onroad/model.cc +++ b/selfdrive/ui/qt/onroad/model.cc @@ -41,11 +41,11 @@ void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) { update_leads(radar_state, model.getPosition()); const auto &lead_two = radar_state.getLeadTwo(); if (lead_one.getStatus()) { - drawLead(painter, lead_one, lead_vertices[0], surface_rect); + drawLead(painter, lead_one, lead_vertices[0], surface_rect, QColor(frogpilot_toggles.value("lead_marker_color").toString())); } else { // FrogPilot variables if (lead_two.getStatus() && (std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0)) { - drawLead(painter, lead_two, lead_vertices[1], surface_rect); + drawLead(painter, lead_two, lead_vertices[1], surface_rect, QColor(frogpilot_toggles.value("lead_marker_color").toString())); } // FrogPilot variables @@ -120,7 +120,13 @@ void ModelRenderer::update_model(const cereal::ModelDataV2::Reader &model, const void ModelRenderer::drawLaneLines(QPainter &painter) { // lanelines for (int i = 0; i < std::size(lane_line_vertices); ++i) { - painter.setBrush(QColor::fromRgbF(1.0, 1.0, 1.0, std::clamp(lane_line_probs[i], 0.0, 0.7))); + if (frogpilot_toggles.value("color_scheme").toString() != "stock") { + painter.setBrush(QColor::fromRgbF(1.0, 1.0, 1.0, std::clamp(lane_line_probs[i], 0.0, 0.7))); + } else { + QColor lane_color = QColor(frogpilot_toggles.value("lane_lines_color").toString()); + lane_color.setAlphaF(lane_color.alphaF() * std::clamp(lane_line_probs[i], 0.0, 0.7)); + painter.setBrush(lane_color); + } painter.drawPolygon(lane_line_vertices[i]); } @@ -146,18 +152,24 @@ void ModelRenderer::drawPath(QPainter &painter, const cereal::ModelDataV2::Reade // Flip so 0 is bottom of frame float lin_grad_point = (height - track_vertices[track_idx].y()) / height; - // speed up: 120, slow down: 0 - float path_hue = fmax(fmin(60 + acceleration[i] * 35, 120), 0); - // FIXME: painter.drawPolygon can be slow if hue is not rounded - path_hue = int(path_hue * 100 + 0.5) / 100; + if (fabs(acceleration[i]) < 0.25 && frogpilot_toggles.value("color_scheme").toString() != "stock") { + QColor color = QColor(frogpilot_toggles.value("path_color").toString()); + color.setAlphaF(util::map_val(lin_grad_point, 0.0f, 1.0f, 1.0f, 0.1f)); + bg.setColorAt(lin_grad_point, color); + } else { + // speed up: 120, slow down: 0 + float path_hue = fmax(fmin(60 + acceleration[i] * 35, 120), 0); + // FIXME: painter.drawPolygon can be slow if hue is not rounded + path_hue = int(path_hue * 100 + 0.5) / 100; - float saturation = fmin(fabs(acceleration[i] * 1.5), 1); - float lightness = util::map_val(saturation, 0.0f, 1.0f, 0.95f, 0.62f); // lighter when grey - float alpha = util::map_val(lin_grad_point, 0.75f / 2.f, 0.75f, 0.4f, 0.0f); // matches previous alpha fade - bg.setColorAt(lin_grad_point, QColor::fromHslF(path_hue / 360., saturation, lightness, alpha)); + float saturation = fmin(fabs(acceleration[i] * 1.5), 1); + float lightness = util::map_val(saturation, 0.0f, 1.0f, 0.95f, 0.62f); // lighter when grey + float alpha = util::map_val(lin_grad_point, 0.75f / 2.f, 0.75f, 0.4f, 0.0f); // matches previous alpha fade + bg.setColorAt(lin_grad_point, QColor::fromHslF(path_hue / 360., saturation, lightness, alpha)); - // Skip a point, unless next is last - i += (i + 2) < max_len ? 1 : 0; + // Skip a point, unless next is last + i += (i + 2) < max_len ? 1 : 0; + } } } else { diff --git a/selfdrive/ui/qt/sidebar.cc b/selfdrive/ui/qt/sidebar.cc index e7d346dc2..1ab2fbb5f 100644 --- a/selfdrive/ui/qt/sidebar.cc +++ b/selfdrive/ui/qt/sidebar.cc @@ -42,6 +42,7 @@ Sidebar::Sidebar(QWidget *parent) : QFrame(parent), onroad(false), flag_pressed( pm = std::make_unique(std::vector{"bookmarkButton"}); // FrogPilot variables + QObject::connect(frogpilotUIState(), &FrogPilotUIState::themeUpdated, this, &Sidebar::updateTheme); } void Sidebar::mousePressEvent(QMouseEvent *event) { @@ -109,7 +110,7 @@ void Sidebar::updateState(const UIState &s, const FrogPilotUIState &fs) { connectStatus = ItemStatus{{tr("CONNECT"), tr("OFFLINE")}, warning_color}; } else { connectStatus = nanos_since_boot() - last_ping < 80e9 - ? ItemStatus{{tr("CONNECT"), tr("ONLINE")}, good_color} + ? ItemStatus{{tr("CONNECT"), tr("ONLINE")}, QColor(frogpilot_toggles.value("sidebar_color3").toString())} : ItemStatus{{tr("CONNECT"), tr("ERROR")}, danger_color}; } setProperty("connectStatus", QVariant::fromValue(connectStatus)); @@ -117,13 +118,13 @@ void Sidebar::updateState(const UIState &s, const FrogPilotUIState &fs) { ItemStatus tempStatus = {{tr("TEMP"), tr("HIGH")}, danger_color}; auto ts = deviceState.getThermalStatus(); if (ts == cereal::DeviceState::ThermalStatus::GREEN) { - tempStatus = {{tr("TEMP"), tr("GOOD")}, good_color}; + tempStatus = {{tr("TEMP"), tr("GOOD")}, QColor(frogpilot_toggles.value("sidebar_color1").toString())}; } else if (ts == cereal::DeviceState::ThermalStatus::YELLOW) { tempStatus = {{tr("TEMP"), tr("OK")}, warning_color}; } setProperty("tempStatus", QVariant::fromValue(tempStatus)); - ItemStatus pandaStatus = {{tr("VEHICLE"), tr("ONLINE")}, good_color}; + ItemStatus pandaStatus = {{tr("VEHICLE"), tr("ONLINE")}, QColor(frogpilot_toggles.value("sidebar_color2").toString())}; if (s.scene.pandaType == cereal::PandaState::PandaType::UNKNOWN) { pandaStatus = {{tr("NO"), tr("PANDA")}, danger_color}; } @@ -143,9 +144,9 @@ void Sidebar::paintEvent(QPaintEvent *event) { // buttons p.setOpacity(settings_pressed ? 0.65 : 1.0); - p.drawPixmap(settings_btn.x(), settings_btn.y(), settings_img); + p.drawPixmap(settings_btn.x(), settings_btn.y(), settings_gif ? settings_gif->currentPixmap() : settings_img); p.setOpacity(onroad && flag_pressed ? 0.65 : 1.0); - p.drawPixmap(home_btn.x(), home_btn.y(), onroad ? flag_img : home_img); + p.drawPixmap(home_btn.x(), home_btn.y(), onroad ? flag_gif ? flag_gif->currentPixmap() : flag_img : home_gif ? home_gif->currentPixmap() : home_img); if (recording_audio) { p.setBrush(danger_color); p.setOpacity(mic_indicator_pressed ? 0.65 : 1.0); @@ -188,4 +189,11 @@ void Sidebar::paintEvent(QPaintEvent *event) { // FrogPilot variables void Sidebar::showEvent(QShowEvent *event) { + updateTheme(); +} + +void Sidebar::updateTheme() { + loadImage("../../frogpilot/assets/active_theme/icons/button_home", home_img, home_gif, home_btn.size(), this); + loadImage("../../frogpilot/assets/active_theme/icons/button_flag", flag_img, flag_gif, home_btn.size(), this); + loadImage("../../frogpilot/assets/active_theme/icons/button_settings", settings_img, settings_gif, settings_btn.size(), this); } diff --git a/selfdrive/ui/qt/sidebar.h b/selfdrive/ui/qt/sidebar.h index 246117864..ae4427822 100644 --- a/selfdrive/ui/qt/sidebar.h +++ b/selfdrive/ui/qt/sidebar.h @@ -70,6 +70,11 @@ private: // FrogPilot variables void showEvent(QShowEvent *event); + void updateTheme(); Params params; + + QSharedPointer flag_gif; + QSharedPointer home_gif; + QSharedPointer settings_gif; }; diff --git a/selfdrive/ui/soundd.py b/selfdrive/ui/soundd.py index 779adc280..09fde8397 100644 --- a/selfdrive/ui/soundd.py +++ b/selfdrive/ui/soundd.py @@ -3,6 +3,7 @@ import numpy as np import time import wave +from pathlib import Path from cereal import car, custom, messaging from openpilot.common.basedir import BASEDIR @@ -15,7 +16,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.system import micd from openpilot.system.hardware import HARDWARE -from openpilot.frogpilot.common.frogpilot_variables import get_frogpilot_toggles +from openpilot.frogpilot.common.frogpilot_variables import ACTIVE_THEME_PATH, get_frogpilot_toggles SAMPLE_RATE = 48000 SAMPLE_BUFFER = 4096 # (approx 100ms) @@ -70,8 +71,6 @@ def check_selfdrive_timeout_alert(sm): class Soundd: def __init__(self): - self.load_sounds() - self.current_alert = AudibleAlert.none self.current_volume = MIN_VOLUME self.current_sound_frame = 0 @@ -87,6 +86,8 @@ class Soundd: self.auto_volume = 0 + self.previous_sound_pack = None + self.update_frogpilot_sounds() def load_sounds(self): @@ -96,13 +97,26 @@ class Soundd: for sound in sound_list: filename, play_count, volume = sound_list[sound] - with wave.open(BASEDIR + "/selfdrive/assets/sounds/" + filename, 'r') as wavefile: - assert wavefile.getnchannels() == 1 - assert wavefile.getsampwidth() == 2 - assert wavefile.getframerate() == SAMPLE_RATE + sounds_path = self.sound_directory / filename - length = wavefile.getnframes() - self.loaded_sounds[sound] = np.frombuffer(wavefile.readframes(length), dtype=np.int16).astype(np.float32) / (2**16/2) + if not sounds_path.exists() and "_tizi" in filename: + standard_path = self.sound_directory / filename.replace("_tizi", "") + if standard_path.exists(): + sounds_path = standard_path + + if sounds_path.exists(): + wavefile = wave.open(str(sounds_path), 'r') + else: + if filename == "startup.wav": + filename = "engage.wav" + wavefile = wave.open(BASEDIR + "/selfdrive/assets/sounds/" + filename, 'r') + + assert wavefile.getnchannels() == 1 + assert wavefile.getsampwidth() == 2 + assert wavefile.getframerate() == SAMPLE_RATE + + length = wavefile.getnframes() + self.loaded_sounds[sound] = np.frombuffer(wavefile.readframes(length), dtype=np.int16).astype(np.float32) / (2**16/2) def get_sound_data(self, frames): # get "frames" worth of data from the current alert sound, looping when required @@ -207,9 +221,9 @@ class Soundd: if frogpilot_toggles != self.frogpilot_toggles: self.frogpilot_toggles = frogpilot_toggles - self.update_frogpilot_sounds() + stream = self.update_frogpilot_sounds(sd, stream) - def update_frogpilot_sounds(self): + def update_frogpilot_sounds(self, sd=None, stream=None): self.volume_map = { AudibleAlert.engage: self.frogpilot_toggles.engage_volume / 100.0, AudibleAlert.disengage: self.frogpilot_toggles.disengage_volume / 100.0, @@ -230,6 +244,23 @@ class Soundd: if sound not in self.volume_map: self.volume_map[sound] = 1.01 + if self.frogpilot_toggles.sound_pack != "stock": + self.sound_directory = ACTIVE_THEME_PATH / "sounds" + else: + self.sound_directory = Path(BASEDIR) / "selfdrive" / "assets" / "sounds" + + if self.frogpilot_toggles.sound_pack != self.previous_sound_pack: + self.load_sounds() + + self.previous_sound_pack = self.frogpilot_toggles.sound_pack + + if stream is not None: + stream.close() + stream = self.get_stream(sd) + stream.start() + + return stream + def main(): s = Soundd()