From a8122c8f057201c0236399df033ce6398d10a1a7 Mon Sep 17 00:00:00 2001 From: James <91348155+FrogAi@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:00:00 -0700 Subject: [PATCH] Offline maps Co-Authored-By: Jacob Pfeifer --- frogpilot/common/frogpilot_functions.py | 34 +++++- frogpilot/frogpilot_process.py | 4 +- frogpilot/navigation/mapd.py | 153 ++++++++++++++++++++++++ system/manager/process_config.py | 1 + 4 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 frogpilot/navigation/mapd.py diff --git a/frogpilot/common/frogpilot_functions.py b/frogpilot/common/frogpilot_functions.py index b3015380..65251f8b 100644 --- a/frogpilot/common/frogpilot_functions.py +++ b/frogpilot/common/frogpilot_functions.py @@ -16,7 +16,7 @@ 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, use_konik_server from openpilot.frogpilot.common.frogpilot_variables import ( - ERROR_LOGS_PATH, FROGS_GO_MOO_PATH, HD_LOGS_PATH, KONIK_LOGS_PATH, THEME_SAVE_PATH, + ERROR_LOGS_PATH, FROGS_GO_MOO_PATH, HD_LOGS_PATH, KONIK_LOGS_PATH, MAPD_PATH, MAPS_PATH, THEME_SAVE_PATH, FrogPilotVariables, get_frogpilot_toggles ) @@ -96,6 +96,38 @@ def update_boot_logo(frogpilot=False, stock=False): run_cmd(["sudo", "mount", "-o", f"remount,{mount_options}", "/"], "Successfully restored / mount options", "Failed to restore / mount options") +def update_maps(now, params, params_memory): + while not MAPD_PATH.exists(): + time.sleep(60) + + maps_selected = params.get("MapsSelected") + if not maps_selected or not (maps_selected.get("nations") or maps_selected.get("states")): + return + + day = now.day + is_first = day == 1 + is_sunday = now.weekday() == 6 + schedule = params.get("PreferredSchedule") + + maps_downloaded = MAPS_PATH.exists() + if maps_downloaded and (schedule == 0 or (schedule == 1 and not is_sunday) or (schedule == 2 and not is_first)): + return + + suffix = "th" if 11 <= day <= 13 else {1: "st", 2: "nd", 3: "rd"}.get(day % 10, "th") + todays_date = now.strftime(f"%B {day}{suffix}, %Y") + + if maps_downloaded and params.get("LastMapsUpdate") == todays_date: + return + + if params.get("OSMDownloadProgress") is None: + params_memory.put("OSMDownloadLocations", maps_selected) + + while params.get("OSMDownloadProgress") is not None: + time.sleep(60) + + params.put("LastMapsUpdate", todays_date) + + def update_openpilot(thread_manager, params): def update_available(): run_cmd(["pkill", "-SIGUSR1", "-f", "system.updated.updated"], "Checking for updates...", "Failed to check for update...", report=False) diff --git a/frogpilot/frogpilot_process.py b/frogpilot/frogpilot_process.py index ac9b74aa..b1dc4eb3 100644 --- a/frogpilot/frogpilot_process.py +++ b/frogpilot/frogpilot_process.py @@ -10,7 +10,7 @@ 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_functions import update_maps, update_openpilot from openpilot.frogpilot.common.frogpilot_utilities import ThreadManager, flash_panda, is_url_pingable, lock_doors from openpilot.frogpilot.common.frogpilot_variables import ERROR_LOGS_PATH, FrogPilotVariables from openpilot.frogpilot.controls.frogpilot_planner import FrogPilotPlanner @@ -45,6 +45,8 @@ def update_checks(now, theme_manager, thread_manager, params, params_memory, fro while not (is_url_pingable("https://github.com") or is_url_pingable("https://gitlab.com")): time.sleep(60) + thread_manager.run_with_lock(update_maps, (now, params, params_memory)) + theme_manager.update_themes(frogpilot_toggles, boot_run) if frogpilot_toggles.automatic_updates: diff --git a/frogpilot/navigation/mapd.py b/frogpilot/navigation/mapd.py new file mode 100644 index 00000000..c7c2181f --- /dev/null +++ b/frogpilot/navigation/mapd.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# PFEIFER - MAPD - Modified by FrogAi for FrogPilot +import json +import os +import shutil +import stat +import subprocess +import tempfile +import time +import urllib.request + +from pathlib import Path + +from openpilot.common.params import Params + +from openpilot.frogpilot.common.frogpilot_utilities import is_url_pingable +from openpilot.frogpilot.common.frogpilot_variables import MAPD_PATH, RESOURCES_REPO + +VERSION = "v2" + +GITHUB_VERSION_URL = f"https://github.com/{RESOURCES_REPO}/raw/Versions/mapd_version_{VERSION}.json" +GITLAB_VERSION_URL = f"https://gitlab.com/{RESOURCES_REPO}/-/raw/Versions/mapd_version_{VERSION}.json" + +VERSION_PATH = Path("/data/media/0/osm/mapd_version") + +def cleanup_temp_files(): + parent = MAPD_PATH.parent + try: + if not parent.exists() or not parent.is_dir(): + return + except OSError as e: + print(f"Skipping cleanup; cannot access {parent}: {e}") + return + try: + for file in parent.glob("mapd*"): + if file == MAPD_PATH or file == VERSION_PATH: + continue + if file.is_file(): + try: + file.unlink() + except Exception as exception: + print(f"Failed to delete leftover file {file}: {exception}") + except OSError as exception: + print(f"Skipping cleanup in {parent} due to I/O error: {exception}") + +def download(): + Path(MAPD_PATH).parent.mkdir(parents=True, exist_ok=True) + + while not (is_url_pingable("https://github.com") or is_url_pingable("https://gitlab.com")): + time.sleep(60) + + latest_version = get_latest_version() + urls = [ + f"https://github.com/pfeiferj/openpilot-mapd/releases/download/{latest_version}/mapd", + f"https://gitlab.com/{RESOURCES_REPO}/-/raw/Mapd/{latest_version}" + ] + for url in urls: + try: + with urllib.request.urlopen(url) as response: + with tempfile.NamedTemporaryFile("wb", delete=False, dir=MAPD_PATH.parent) as temp_file: + shutil.copyfileobj(response, temp_file) + temp_file_path = Path(temp_file.name) + os.fsync(temp_file.fileno()) + + os.chmod(temp_file_path, os.stat(temp_file_path).st_mode | stat.S_IEXEC) + os.rename(temp_file_path, MAPD_PATH) + + with open(VERSION_PATH, "w") as version_file: + version_file.write(latest_version) + os.fsync(version_file.fileno()) + + return + except Exception as exception: + print(f"Failed to download mapd from {url}: {exception}") + if "temp_file_path" in locals() and temp_file_path.exists(): + temp_file_path.unlink(missing_ok=True) + +def get_latest_version(): + while not (is_url_pingable("https://github.com") or is_url_pingable("https://gitlab.com")): + time.sleep(60) + + for url in [GITHUB_VERSION_URL, GITLAB_VERSION_URL]: + try: + with urllib.request.urlopen(url, timeout=10) as response: + return json.loads(response.read().decode("utf-8"))["version"] + except Exception as exception: + print(f"Error fetching mapd version from {url}: {exception}") + return "v0" + +def update_mapd(): + if not MAPD_PATH.exists(): + print(f"{MAPD_PATH} not found. Downloading...") + download() + return False + + if not VERSION_PATH.exists(): + print(f"{VERSION_PATH} not found. Downloading mapd...") + download() + return False + + if is_url_pingable("https://github.com") or is_url_pingable("https://gitlab.com"): + try: + with open(VERSION_PATH) as version_file: + local_version = version_file.read().strip() + if local_version != get_latest_version(): + print("New mapd version available. Updating...") + download() + return False + else: + return True + except Exception as exception: + print(f"Error checking version: {exception}") + return False + + if not os.access(MAPD_PATH, os.X_OK): + print(f"{MAPD_PATH} is not executable. Fixing permissions...") + try: + os.chmod(MAPD_PATH, os.stat(MAPD_PATH).st_mode | stat.S_IEXEC) + except Exception as exception: + print(f"Failed to set executable permissions on {MAPD_PATH}: {exception}") + return False + + return True + +def mapd_thread(): + params_memory = Params(memory=True) + + params_memory.put("MapdLogLevel", "disabled") + + while True: + try: + cleanup_temp_files() + except OSError as exception: + print(f"Cleanup errored: {exception}") + time.sleep(5) + continue + + while not update_mapd(): + time.sleep(60) + continue + + try: + process = subprocess.Popen(str(MAPD_PATH)) + process.wait() + except FileNotFoundError as error: + print(f"Subprocess failed: {error}") + download() + +def main(): + mapd_thread() + +if __name__ == "__main__": + main() diff --git a/system/manager/process_config.py b/system/manager/process_config.py index ff28b3d8..d2c2e687 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -130,6 +130,7 @@ elif TICI: procs.append(NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=5)), procs += [ PythonProcess("frogpilot_process", "frogpilot.frogpilot_process", always_run), + PythonProcess("mapd", "frogpilot.navigation.mapd", always_run), ] managed_processes = {p.name: p for p in procs}