diff --git a/SConstruct b/SConstruct index 2437f99d9..dbeff51da 100644 --- a/SConstruct +++ b/SConstruct @@ -20,7 +20,7 @@ SetOption('num_jobs', max(1, int(os.cpu_count()/(1 if "CI" in os.environ else 2) AddOption('--ccflags', action='store', type='string', default='', help='pass arbitrary flags over the command line') AddOption('--verbose', action='store_true', default=False, help='show full build commands') -release = not os.path.exists(File('#.gitattributes').abspath) # file absent on release branch, see release_files.py +release = not os.path.exists(File('#.gitmodules').abspath) # file absent on release branch, see release_files.py AddOption('--minimal', action='store_false', dest='extras', @@ -316,8 +316,6 @@ def count_scons_nodes(nodes): if node in seen: continue seen.add(node) - if hasattr(node, 'has_builder') and node.has_builder(): - build_product_nodes.add(node) executor = node.get_executor() if executor is not None: stack += executor.get_all_prerequisites() + executor.get_all_children() @@ -326,7 +324,6 @@ def count_scons_nodes(nodes): progress_interval = 5 progress_count = 0 -build_product_nodes = set() progress_total = max(1, count_scons_nodes(env.arg2nodes(BUILD_TARGETS or [Dir('.')], env.fs.Entry))) def progress_function(node): @@ -342,13 +339,3 @@ def progress_function(node): Progress(progress_function, interval=progress_interval) AddPostAction(BUILD_TARGETS or [Dir('.')], prune_cache_dir) - -def check_build_product_size(target, source, env): - limit = 50 * 1024 * 1024 # GitHub max size - for t in target: - if str(t).endswith('.pkl'): # chunked during release packaging - continue - if hasattr(t, 'isfile') and t.isfile() and (size := os.path.getsize(t.abspath)) > limit: - raise SCons.Errors.UserError(f"{t} is {size / (1024 * 1024):.1f} MiB, exceeding the {limit / (1024 * 1024):.1f} MiB limit") -if not GetOption('extras'): - AddPostAction(list(build_product_nodes), Action(check_build_product_size, None)) diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 240bc3b64..806e41d4c 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -56,6 +56,8 @@ function launch { mv "$DIR" /data/safe_staging/old_openpilot mv "${STAGING_ROOT}/finalized" "$DIR" cd "$DIR" + # releases before the openpilot/ move can't swap AGNOS on newer AGNOS, use ours + [ ! -L system/hardware/tici/agnos.json ] && /data/safe_staging/old_openpilot/openpilot/common/hardware/comma/agnos.py --verify system/hardware/tici/agnos.json && sudo reboot echo "Restarting launch script ${LAUNCHER_LOCATION}" unset AGNOS_VERSION diff --git a/openpilot/common/hardware/comma/hardware.py b/openpilot/common/hardware/comma/hardware.py index 1b3680338..a0ec71b6a 100644 --- a/openpilot/common/hardware/comma/hardware.py +++ b/openpilot/common/hardware/comma/hardware.py @@ -8,6 +8,7 @@ from functools import cached_property, lru_cache from pathlib import Path from openpilot.cereal import log +from openpilot.common.esim.base import WEBBING_ICCID_PREFIX from openpilot.common.utils import sudo_read, sudo_write from openpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action from openpilot.common.esim.base import LPABase @@ -207,6 +208,8 @@ class HardwareComma(HardwareBase): def get_network_metered(self, network_type) -> bool: if network_type in (NetworkType.cell2G, NetworkType.cell3G, NetworkType.cell4G, NetworkType.cell5G): + if self.get_modem_state().get("iccid", "").startswith(WEBBING_ICCID_PREFIX): + return True from openpilot.common.params import Params return Params().get_bool("GsmMetered") try: diff --git a/openpilot/common/hardware/comma/modem.py b/openpilot/common/hardware/comma/modem.py index 901efae2b..f1b79862b 100755 --- a/openpilot/common/hardware/comma/modem.py +++ b/openpilot/common/hardware/comma/modem.py @@ -17,6 +17,8 @@ from ipaddress import IPv4Address, AddressValueError from enum import Enum +from openpilot.common.esim.base import WEBBING_ICCID_PREFIX + logging.basicConfig( level=logging.INFO, format="%(asctime)s.%(msecs)03d %(levelname)-7s modem: %(message)s", @@ -44,7 +46,6 @@ NETWORK_TYPE = {0: "gsm", 1: "gsm", 3: "gsm", 8: "gsm", 11: "nr", 12: "nr", 13: "nr"} DIAL_CID = 1 -WEBBING_ICCID_PREFIX = "8985235" PPPD_CMD = [ "sudo", "pppd", PPP_PORT, "460800", "noauth", "nodetach", "noipdefault", "usepeerdns", diff --git a/openpilot/common/hardware/comma/power_monitor.py b/openpilot/common/hardware/comma/power_monitor.py index 9ae2c9e13..296290dae 100755 --- a/openpilot/common/hardware/comma/power_monitor.py +++ b/openpilot/common/hardware/comma/power_monitor.py @@ -9,25 +9,22 @@ from openpilot.common.realtime import Ratekeeper from openpilot.common.filter_simple import FirstOrderFilter -def read_power(panda=None): - if panda is not None and panda.get_type() == panda.HW_TYPE_CUATRO: - health = panda.health() - return health['voltage'] * health['current'] / 1e6 +def read_power(): with open("/sys/bus/i2c/devices/0-0040/hwmon/hwmon1/power1_input") as f: return int(f.read()) / 1e6 -def sample_power(seconds=5, panda=None) -> list[float]: +def sample_power(seconds=5) -> list[float]: rate = 123 rk = Ratekeeper(rate, print_delay_threshold=None) pwrs = [] for _ in range(rate*seconds): - pwrs.append(read_power(panda)) + pwrs.append(read_power()) rk.keep_time() return pwrs -def get_power(seconds=5, panda=None): - pwrs = sample_power(seconds, panda) +def get_power(seconds=5): + pwrs = sample_power(seconds) return np.mean(pwrs) def wait_for_power(min_pwr, max_pwr, min_secs_in_range, timeout): diff --git a/openpilot/selfdrive/ui/layouts/settings/firehose.py b/openpilot/selfdrive/ui/layouts/settings/firehose.py index 8ac6fe3d2..b06840e4d 100644 --- a/openpilot/selfdrive/ui/layouts/settings/firehose.py +++ b/openpilot/selfdrive/ui/layouts/settings/firehose.py @@ -11,7 +11,7 @@ TITLE = tr_noop("Firehose Mode") DESCRIPTION = tr_noop( "sunnypilot learns to drive by watching humans, like you, drive.\n\n" + "Firehose Mode allows you to maximize your training data uploads to improve " - + "openpilot's driving models. More data means bigger models, which means better Experimental Mode." + + "openpilot's driving models. More data means bigger models, which means better driving." ) INSTRUCTIONS = tr_noop( "For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n" diff --git a/openpilot/selfdrive/ui/lib/prime_state.py b/openpilot/selfdrive/ui/lib/prime_state.py index 1fa87e31d..88de7c21f 100644 --- a/openpilot/selfdrive/ui/lib/prime_state.py +++ b/openpilot/selfdrive/ui/lib/prime_state.py @@ -10,6 +10,7 @@ from openpilot.common.realtime import drop_realtime from openpilot.common.swaglog import cloudlog from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID from openpilot.selfdrive.ui.lib.api_helpers import get_token +from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert class PrimeType(IntEnum): @@ -22,6 +23,10 @@ class PrimeType(IntEnum): MAGENTA_NEW = 4 PURPLE = 5 +class Provider(str): + GOOGLE = "google" + GITHUB = "github" + APPLE = "apple" class PrimeState: FETCH_INTERVAL = 5.0 # seconds between API calls @@ -33,6 +38,13 @@ class PrimeState: self._lock = threading.Lock() self._session = requests.Session() # reuse session to reduce SSL handshake overhead self.prime_type: PrimeType = self._load_initial_state() + self._prime_trial_available = False + pairing_provider = os.getenv("PAIRING_PROVIDER") or self._params.get("PairingProvider") + self._pairing_provider: Provider | None = Provider(pairing_provider) if pairing_provider is not None else None + self._pairing_email: str | None = self._params.get("PairingEmail") + self._commacare = False + + set_offroad_alert("Offroad_Pairing", self.prime_type <= PrimeType.UNPAIRED) self._running = False self._thread = None @@ -56,18 +68,81 @@ class PrimeState: response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token, session=self._session) if response.status_code == 200: data = response.json() + set_offroad_alert("Offroad_UploadsIgnored", bool(data.get("ignore_uploads"))) + is_paired = data.get("is_paired", False) prime_type = data.get("prime_type", 0) self.set_type(PrimeType(prime_type) if is_paired else PrimeType.UNPAIRED) + if not is_paired: + self.set_provider(None, None) + + prime_trial_available = data.get("trial_claimed") is False and data.get("eligible_features", {}).get("prime", False) + self.set_prime_trial_available(prime_trial_available) + + commacare = data.get("commacare", False) + self.set_commacare(commacare) except Exception as e: cloudlog.error(f"Failed to fetch prime status: {e}") + def _fetch_pairing_provider(self) -> None: + dongle_id = self._params.get("DongleId") + if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: + return + + try: + identity_token = get_token(dongle_id) + response = api_get(f"v1/devices/{dongle_id}/owner", timeout=self.API_TIMEOUT, access_token=identity_token, session=self._session) + if response.status_code == 200: + data = response.json() + user_id = data.get("user_id", "") + provider = Provider(user_id.partition("_")[0]) + email = data.get("email") + self.set_provider(provider, email) + except Exception as e: + cloudlog.error(f"Failed to fetch pairing provider: {e}") + def set_type(self, prime_type: PrimeType) -> None: with self._lock: if prime_type != self.prime_type: self.prime_type = prime_type self._params.put("PrimeType", int(prime_type)) cloudlog.info(f"Prime type updated to {prime_type}") + set_offroad_alert("Offroad_Pairing", self.prime_type <= PrimeType.UNPAIRED) + + def set_provider(self, provider: Provider | None, email: str | None): + with self._lock: + if self.prime_type <= PrimeType.UNPAIRED: + provider = None + email = None + email = email or None # if data.get(email) returns "" instead of None + + if self._pairing_provider != provider: + self._pairing_provider = provider + if provider is None: + self._params.remove("PairingProvider") + else: + self._params.put("PairingProvider", str(provider)) + + if self._pairing_email != email: + self._pairing_email = email + if email is None: + self._params.remove("PairingEmail") + else: + self._params.put("PairingEmail", email) + + def set_commacare(self, has_commacare: bool): + with self._lock: + if self.prime_type <= PrimeType.UNPAIRED: + self._commacare = False + else: + self._commacare = has_commacare + + def set_prime_trial_available(self, prime_trail_available: bool): + with self._lock: + if self.prime_type <= PrimeType.UNPAIRED: + self._prime_trial_available = False + else: + self._prime_trial_available = prime_trail_available def _worker_thread(self) -> None: drop_realtime() @@ -75,6 +150,7 @@ class PrimeState: while self._running: if not ui_state.started and device._awake: self._fetch_prime_status() + self._fetch_pairing_provider() for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)): if not self._running: @@ -109,5 +185,25 @@ class PrimeState: with self._lock: return self.prime_type > PrimeType.UNPAIRED + def can_claim_prime_trial(self) -> bool: + with self._lock: + return self._prime_trial_available + + def has_commacare(self) -> bool: + with self._lock: + return self._commacare + + def get_pairing_provider(self) -> str | None: + with self._lock: + return self._pairing_provider + + def get_pairing_account(self) -> str: + with self._lock: + if not self._pairing_provider: + return "unknown" + elif self._pairing_provider == Provider.GITHUB or not self._pairing_email: + return f"{self._pairing_provider} account" + return self._pairing_email + def __del__(self): self.stop() diff --git a/openpilot/selfdrive/ui/widgets/offroad_alerts.py b/openpilot/selfdrive/ui/widgets/offroad_alerts.py index 88ccc5df7..e2b62834c 100644 --- a/openpilot/selfdrive/ui/widgets/offroad_alerts.py +++ b/openpilot/selfdrive/ui/widgets/offroad_alerts.py @@ -263,6 +263,8 @@ class OffroadAlert(AbstractAlert): def _build_alerts(self): self.sorted_alerts = [] for key, config in sorted(OFFROAD_ALERTS.items(), key=lambda x: x[1].get("severity", 0), reverse=True): + if key == "Offroad_Pairing": + continue severity = config.get("severity", 0) alert_data = AlertData(key=key, text="", severity=severity) self.sorted_alerts.append(alert_data) diff --git a/openpilot/system/athena/athenad.py b/openpilot/system/athena/athenad.py index 4d4735b46..9ae121ec6 100755 --- a/openpilot/system/athena/athenad.py +++ b/openpilot/system/athena/athenad.py @@ -385,19 +385,6 @@ def getVersion() -> dict[str, str]: } -@dispatcher.add_method -def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: str | None = None, place_details: str | None = None) -> dict[str, int]: - destination = { - "latitude": latitude, - "longitude": longitude, - "place_name": place_name, - "place_details": place_details, - } - Params().put("NavDestination", json.dumps(destination)) - - return {"success": 1} - - def scan_dir(path: str, prefix: str, base: str | None = None) -> list[str]: if base is None: base = path @@ -720,6 +707,20 @@ def cancelUpload(upload_id: str | list[str]) -> dict[str, int | str]: cancelled_uploads.update(cancelled_ids) return {"success": 1} +@dispatcher.add_method +def setNavDestination(latitude: float, longitude: float) -> dict[str, int | str]: + if not (-90 <= latitude <= 90 and -180 <= longitude <= 180): + return {"success": 0, "error": "Invalid destination coordinates"} + Params().put("NavDestination", {"latitude": latitude, "longitude": longitude}, block=True) + return {"success": 1} + + +@dispatcher.add_method +def clearNavDestination() -> dict[str, int]: + Params().remove("NavDestination") + return {"success": 1} + + @dispatcher.add_method def setRouteViewed(route: str) -> dict[str, int | str]: # maintain a list of the last 10 routes viewed in connect diff --git a/openpilot/system/hardware/chestnut/firmware_wrapped.bin b/openpilot/system/hardware/chestnut/firmware_wrapped.bin index 1d738e4a7..ff0381a6e 100644 Binary files a/openpilot/system/hardware/chestnut/firmware_wrapped.bin and b/openpilot/system/hardware/chestnut/firmware_wrapped.bin differ diff --git a/openpilot/system/hardware/chestnut/status.py b/openpilot/system/hardware/chestnut/status.py index f108c3d62..270576406 100644 --- a/openpilot/system/hardware/chestnut/status.py +++ b/openpilot/system/hardware/chestnut/status.py @@ -5,7 +5,7 @@ from openpilot.common.version import get_build_metadata, CHESTNUT_BRANCHES from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_compiled -CHESTNUT_RELEASE_BRANCHES = ("release-chestnut", "release-chestnut-staging", "nightly-chestnut") +CHESTNUT_RELEASE_BRANCHES = ("release-chestnut", "release-chestnut-staging", "nightly-chestnut", "nightly-chestnut-dev") CHESTNUT_POWERED_VOLTAGE = 5000 GPU_TEMP_LIMIT = 100. MEMORY_TEMP_LIMIT = 95. @@ -24,6 +24,7 @@ class ChestnutStatus: self.link_failures = 0 self.model_loading_seen = False self.model_attempted = False + self.model_failed = False self.overheated = False self.usb_seen = False self.usb_failed = False @@ -43,14 +44,18 @@ class ChestnutStatus: self.link_failures = 0 self.model_loading_seen = False self.model_attempted = False + self.model_failed = False self.usb_seen = firmware_ok self.usb_failed = False self.model_loading_seen |= model_loading self.model_attempted |= self.model_loading_seen and not model_loading and model_active is not None + self.model_failed |= not offroad and self.model_attempted and model_active is False and len(detected) > 0 - if not offroad and self.usb_seen and not firmware_ok: + if not offroad and self.usb_seen and len(devices) != 1: self.usb_failed = True + if offroad and len(devices) == 1: + self.usb_failed = False if not offroad and state is not None: powered = state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE @@ -72,6 +77,7 @@ class ChestnutStatus: self.power_unavailable = False self.power_lost = False self.power_restored = False + self.model_failed = False if state is not None: gpu_limit = GPU_TEMP_LIMIT - (TEMP_HYSTERESIS if self.overheated else 0.) @@ -87,15 +93,15 @@ class ChestnutStatus: chestnut_needs_switch = len(devices) == 1 and not big_model_available and chestnut_target is not None set_alert("Offroad_ChestnutBranch", chestnut_needs_switch, chestnut_target if chestnut_needs_switch else None) set_alert("Offroad_ChestnutNotDetected", missing) - set_alert("Offroad_ChestnutOverheated", self.overheated, f"{state.tempC:.0f} °C" if state is not None else None) + set_alert("Offroad_ChestnutOverheated", self.overheated) set_alert("Offroad_ChestnutUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None) if self.power_lost: - pcie_alert = ("Chestnut power restored. 12V is stable again, cycle ignition." if self.power_restored else - "Chestnut power disconnected. Check 12V connection, then cycle ignition." if self.power_unavailable else - "Chestnut power lost. Possibly caused by an engine-crank voltage drop. Check 12V connection, then cycle ignition.") + pcie_alert = ("Chestnut power restored. Restart the car to retry." if self.power_restored else + "Chestnut power disconnected. Check the 12V connection, then restart the car." if self.power_unavailable else + "Chestnut power lost. Check the 12V connection, then restart the car.") else: - pcie_alert = "Chestnut GPU unavailable. PCIe link is not up. Check the GPU is securely seated." + pcie_alert = "Chestnut GPU unavailable. Check the GPU is securely seated, then restart the car." set_alert("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert) - set_alert("Offroad_ChestnutUncompiled", offroad and firmware_ok and not chestnut_compiled()) + set_alert("Offroad_ChestnutModelError", self.model_failed and not (missing or self.pcie_failed)) set_alert("Offroad_ChestnutUpdateFailed", offroad and firmware_failed) self.offroad = offroad diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index 1d6662458..c54f8062f 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -41,9 +41,10 @@ PANDA_STATES_TIMEOUT = round(1000 / SERVICE_LIST['pandaStates'].frequency * 1.5) ONROAD_CYCLE_TIME = 1 # seconds to wait offroad after requesting an onroad cycle class Chestnut: - # flash offroad, modeld ignores chestnut until the product string matches + # flash before going onroad, modeld ignores chestnut until the product string matches MAX_ATTEMPTS = 3 RETRY_INTERVAL = 20. + STARTUP_TIMEOUT = 30. def __init__(self): self.thread: threading.Thread | None = None @@ -52,6 +53,10 @@ class Chestnut: self.flashed = False self.mismatch = False + @property + def flashing(self) -> bool: + return self.thread is not None and self.thread.is_alive() and time.monotonic() - self.last_attempt < self.STARTUP_TIMEOUT + @property def failed(self) -> bool: return self.mismatch and self.attempts >= self.MAX_ATTEMPTS and self.thread is not None and not self.thread.is_alive() and not self.flashed @@ -73,7 +78,7 @@ class Chestnut: return if self.thread is not None and self.thread.is_alive(): return - if time.monotonic() - self.last_attempt < self.RETRY_INTERVAL: + if self.attempts > 0 and time.monotonic() - self.last_attempt < self.RETRY_INTERVAL: return self.attempts += 1 @@ -223,7 +228,7 @@ def hardware_thread(end_event, hw_queue) -> None: network_strength=NetworkStrength.unknown, network_stats={'wwanTx': -1, 'wwanRx': -1}, modem_temps=[], - usb_state=[], + usb_state=get_usb_state(), ) all_temp_filter = FirstOrderFilter(0., TEMP_TAU, DT_HW, initialized=False) @@ -315,7 +320,7 @@ def hardware_thread(end_event, hw_queue) -> None: chestnut.update(started_ts is None, last_hw_state.usb_state) chestnut_state = sm["chestnutState"] chestnut_valid = sm.alive["chestnutState"] and sm.valid["chestnutState"] - chestnut_status.update(started_ts is None, branch, last_hw_state.usb_state, chestnut.failed, + chestnut_status.update(started_ts is None or not onroad_conditions["ignition"], branch, last_hw_state.usb_state, chestnut.failed, params.get_bool("ChestnutLoading"), params.get("ChestnutActive"), chestnut_state if chestnut_valid else None, set_offroad_alert_if_changed) @@ -375,6 +380,8 @@ def hardware_thread(end_event, hw_queue) -> None: # must be at an engageable thermal band to go onroad startup_conditions["device_temp_engageable"] = thermal_status < ThermalStatus.overheated + startup_conditions["not_flashing_chestnut"] = not chestnut.flashing + # ensure device is fully booted startup_conditions["device_booted"] = startup_conditions.get("device_booted", False) or HARDWARE.booted() diff --git a/openpilot/system/ubloxd/pigeond.py b/openpilot/system/ubloxd/pigeond.py index aa171a9a2..52a5a5d78 100755 --- a/openpilot/system/ubloxd/pigeond.py +++ b/openpilot/system/ubloxd/pigeond.py @@ -67,7 +67,9 @@ def get_assistnow_messages() -> list[bytes]: while len(dat) > 0: assert dat[:2] == b"\xB5\x62" msg_len = 6 + (dat[5] << 8 | dat[4]) + 2 - msgs.append(dat[:msg_len]) + # Skip MGA-INI messages; time from proxyserver is a stale cache + if dat[2:4] != b"\x13\x40": + msgs.append(dat[:msg_len]) dat = dat[msg_len:] return msgs @@ -146,6 +148,31 @@ class TTYPigeon: return True return False +def send_time(pigeon: TTYPigeon) -> None: + if not system_time_valid(): + return + + cloudlog.warning("Sending current time to ublox") + + # UBX-MGA-INI-TIME_UTC + t_now = datetime.now(UTC).replace(tzinfo=None) + msg = add_ubx_checksum(b"\xB5\x62\x13\x40\x18\x00" + struct.pack(" None: # store almanac in flash pigeon.send(b"\xB5\x62\x09\x14\x04\x00\x00\x00\x00\x00\x21\xEC") @@ -167,6 +194,27 @@ def init_baudrate(pigeon: TTYPigeon): pigeon.set_baud(460800) +def send_position(pigeon: TTYPigeon) -> None: + if not (position := Params().get("LastGPSPosition")): + return + + with log.Event.from_bytes(position) as evt: + gps = evt.gpsLocationExternal + if datetime.now(UTC).timestamp() - gps.unixTimestampMillis / 1000 > 7 * 24 * 3600: + return + cloudlog.warning("Sending last position to ublox") + + # UBX-MGA-INI-POS_LLH + msg = add_ubx_checksum(b"\xB5\x62\x13\x40\x14\x00" + struct.pack(" bool: # try initializing a few times for _ in range(10): @@ -185,7 +233,7 @@ def init_pigeon(pigeon: TTYPigeon) -> bool: pigeon.send_with_ack(b"\xB5\x62\x06\x08\x06\x00\x64\x00\x01\x00\x00\x00\x79\x10") # UBX-CFG-NAV5 (0x06 0x24) - pigeon.send_with_ack(b"\xB5\x62\x06\x24\x24\x00\x05\x00\x04\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5A\x63") + pigeon.send_with_ack(b"\xB5\x62\x06\x24\x24\x00\x07\x00\x04\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5C\xAB") # UBX-CFG-ODO (0x06 0x1E) pigeon.send_with_ack(b"\xB5\x62\x06\x1E\x14\x00\x00\x00\x00\x00\x01\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3C\x37") @@ -217,29 +265,6 @@ def init_pigeon(pigeon: TTYPigeon) -> bool: else: cloudlog.error(f"failed to restore almanac backup, status: {restore_status}") - # sending time to ublox - if system_time_valid(): - t_now = datetime.now(UTC).replace(tzinfo=None) - cloudlog.warning("Sending current time to ublox") - - # UBX-MGA-INI-TIME_UTC - msg = add_ubx_checksum(b"\xB5\x62\x13\x40\x18\x00" + struct.pack(" None: init_baudrate(pigeon) init_pigeon(pigeon) + if system_time_valid(): + try: + send_time(pigeon) + send_position(pigeon) + except Exception: + cloudlog.exception("failed to send time and position to ublox") + def run_receiving(duration: int = 0): pm = messaging.PubMaster(['ubloxRaw']) @@ -301,6 +333,7 @@ def run_receiving(duration: int = 0): try: for msg in assist_messages: pigeon.send_with_ack(msg, ack=UBLOX_ASSIST_ACK) + send_time(pigeon) cloudlog.warning("AssistNow messages sent") except Exception: cloudlog.warning("failed to send AssistNow messages") diff --git a/openpilot/system/ubloxd/ubloxd.py b/openpilot/system/ubloxd/ubloxd.py index 3b32f7065..3365e9c69 100755 --- a/openpilot/system/ubloxd/ubloxd.py +++ b/openpilot/system/ubloxd/ubloxd.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import math +import time import capnp import calendar import numpy as np @@ -8,6 +9,7 @@ from dataclasses import dataclass from openpilot.cereal import log from openpilot.cereal import messaging +from openpilot.common.params import Params from openpilot.system.ubloxd.ubx import Ubx from openpilot.system.ubloxd.gps import Gps from openpilot.system.ubloxd.glonass import Glonass @@ -510,6 +512,8 @@ def main(): parser = UbloxMsgParser() pm = messaging.PubMaster(['ubloxGnss', 'gpsLocationExternal']) sock = messaging.sub_sock('ubloxRaw', timeout=100, conflate=False) + params = Params() + last_position_save = float('-inf') while True: msg = messaging.recv_one(sock) @@ -529,6 +533,10 @@ def main(): service, dat = res pm.send(service, dat) + if service == 'gpsLocationExternal' and dat.gpsLocationExternal.hasFix and time.monotonic() - last_position_save > 60: + params.put("LastGPSPosition", dat.to_bytes()) + last_position_save = time.monotonic() + if __name__ == '__main__': main() diff --git a/openpilot/system/ui/lib/scroll_panel2.py b/openpilot/system/ui/lib/scroll_panel2.py index 19faaa542..b53e66379 100644 --- a/openpilot/system/ui/lib/scroll_panel2.py +++ b/openpilot/system/ui/lib/scroll_panel2.py @@ -263,6 +263,10 @@ class GuiScrollPanel2: def state(self) -> ScrollState: return self._state + @property + def velocity(self) -> float: + return self._velocity + def is_touch_valid(self) -> bool: # MIN_VELOCITY_FOR_CLICKING is checked in auto-scroll state return bool(self._state != ScrollState.MANUAL_SCROLL) diff --git a/openpilot/system/ui/widgets/button.py b/openpilot/system/ui/widgets/button.py index c031b2f14..cca48cb19 100644 --- a/openpilot/system/ui/widgets/button.py +++ b/openpilot/system/ui/widgets/button.py @@ -21,6 +21,7 @@ class ButtonStyle(IntEnum): NO_EFFECT = 6 KEYBOARD = 7 FORGET_WIFI = 8 + PILL = 11 # For pill buttons in list items, for example the multiple button selector ICON_PADDING = 15 @@ -39,10 +40,12 @@ BUTTON_TEXT_COLOR = { ButtonStyle.NO_EFFECT: rl.Color(228, 228, 228, 255), ButtonStyle.KEYBOARD: rl.Color(221, 221, 221, 255), ButtonStyle.FORGET_WIFI: rl.Color(51, 51, 51, 255), + ButtonStyle.PILL: rl.Color(228, 228, 228, 255), } BUTTON_DISABLED_TEXT_COLORS = { ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.WHITE, + ButtonStyle.PILL: rl.Color(150, 150, 150, 255), } BUTTON_BACKGROUND_COLORS = { @@ -57,6 +60,7 @@ BUTTON_BACKGROUND_COLORS = { ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255), ButtonStyle.KEYBOARD: rl.Color(68, 68, 68, 255), ButtonStyle.FORGET_WIFI: rl.Color(189, 189, 189, 255), + ButtonStyle.PILL: rl.Color(57, 57, 57, 255), } BUTTON_PRESSED_BACKGROUND_COLORS = { @@ -71,10 +75,12 @@ BUTTON_PRESSED_BACKGROUND_COLORS = { ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255), ButtonStyle.KEYBOARD: rl.Color(51, 51, 51, 255), ButtonStyle.FORGET_WIFI: rl.Color(130, 130, 130, 255), + ButtonStyle.PILL: rl.Color(74, 74, 74, 255), } BUTTON_DISABLED_BACKGROUND_COLORS = { ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.BLANK, + ButtonStyle.PILL: rl.Color(57, 57, 57, 150), } diff --git a/openpilot/system/ui/widgets/list_view.py b/openpilot/system/ui/widgets/list_view.py index b376928c5..e993009fb 100644 --- a/openpilot/system/ui/widgets/list_view.py +++ b/openpilot/system/ui/widgets/list_view.py @@ -7,7 +7,8 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, T from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.button import Button, ButtonStyle +from openpilot.system.ui.widgets.button import (Button, ButtonStyle, BUTTON_TEXT_COLOR, BUTTON_DISABLED_TEXT_COLORS, BUTTON_BACKGROUND_COLORS, + BUTTON_PRESSED_BACKGROUND_COLORS, BUTTON_DISABLED_BACKGROUND_COLORS) from openpilot.system.ui.widgets.toggle import Toggle, WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets.html_render import HtmlRenderer, ElementType @@ -242,12 +243,12 @@ class MultipleButtonAction(ItemAction): if is_selected: bg_color = rl.Color(51, 171, 76, 255) # Green elif is_pressed: - bg_color = rl.Color(74, 74, 74, 255) # Dark gray + bg_color = BUTTON_PRESSED_BACKGROUND_COLORS[ButtonStyle.PILL] else: - bg_color = rl.Color(57, 57, 57, 255) # Gray + bg_color = BUTTON_BACKGROUND_COLORS[ButtonStyle.PILL] if not self.enabled: - bg_color = rl.Color(bg_color.r, bg_color.g, bg_color.b, 150) # Dim + bg_color = rl.Color(bg_color.r, bg_color.g, bg_color.b, BUTTON_DISABLED_BACKGROUND_COLORS[ButtonStyle.PILL].a) # Draw button rl.draw_rectangle_rounded(button_rect, 1.0, 20, bg_color) @@ -257,7 +258,7 @@ class MultipleButtonAction(ItemAction): text_size = measure_text_cached(self._font, text, 40) text_x = button_x + (self.button_width - text_size.x) / 2 text_y = button_y + (BUTTON_HEIGHT - text_size.y) / 2 - text_color = rl.Color(228, 228, 228, 255) if self.enabled else rl.Color(150, 150, 150, 255) + text_color = BUTTON_TEXT_COLOR[ButtonStyle.PILL] if self.enabled else BUTTON_DISABLED_TEXT_COLORS[ButtonStyle.PILL] rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, text_color) def _handle_mouse_release(self, mouse_pos: MousePos): diff --git a/openpilot/system/ui/widgets/mici_keyboard.py b/openpilot/system/ui/widgets/mici_keyboard.py index bc3041aa3..4ebb940ee 100644 --- a/openpilot/system/ui/widgets/mici_keyboard.py +++ b/openpilot/system/ui/widgets/mici_keyboard.py @@ -353,8 +353,12 @@ class MiciKeyboard(Widget): # draw black circle behind selected key circle_alpha = int(self._selected_key_filter.x * 225) - rl.draw_circle_gradient(rl.Vector2(key_x + key.rect.width / 2, key_y + key.rect.height / 2), - SELECTED_CHAR_FONT_SIZE, rl.Color(0, 0, 0, circle_alpha), rl.BLANK) + if rl.RAYLIB_VERSION_MAJOR >= 6: + rl.draw_circle_gradient(rl.Vector2(key_x + key.rect.width / 2, key_y + key.rect.height / 2), + SELECTED_CHAR_FONT_SIZE, rl.Color(0, 0, 0, circle_alpha), rl.BLANK) + else: # AGNOS updater compatibility for raylib 5 + rl.draw_circle_gradient(int(key_x + key.rect.width / 2), int(key_y + key.rect.height / 2), + SELECTED_CHAR_FONT_SIZE, rl.Color(0, 0, 0, circle_alpha), rl.BLANK) else: # move other keys away from selected key a bit dx = key.original_position.x - self._closest_key[0].original_position.x diff --git a/openpilot/system/ui/widgets/scroller.py b/openpilot/system/ui/widgets/scroller.py index e79c4592f..ebde01c42 100644 --- a/openpilot/system/ui/widgets/scroller.py +++ b/openpilot/system/ui/widgets/scroller.py @@ -97,6 +97,7 @@ class _Scroller(Widget): self._scroll_enabled: bool | Callable[[], bool] = True self._show_scroll_indicator = scroll_indicator and self._horizontal + self.scroll_indicator_start_after: Widget | None = None self._scroll_indicator = ScrollIndicator() self._edge_shadows = edge_shadows and self._horizontal @@ -364,7 +365,14 @@ class _Scroller(Widget): # Draw scroll indicator on top of edge shadows if self._show_scroll_indicator and len(self._visible_items) > 0: - self._scroll_indicator.update(self._scroll_offset, self._content_size, self._rect) + viewport = self._rect + if self.scroll_indicator_start_after is not None: + offset = min(self._rect.width, max(0.0, self.scroll_indicator_start_after.rect.x + + self.scroll_indicator_start_after.rect.width + + self._spacing - self._rect.x)) + viewport = rl.Rectangle(self._rect.x + offset, self._rect.y, self._rect.width - offset, self._rect.height) + + self._scroll_indicator.update(self._scroll_offset, self._content_size, viewport) self._scroll_indicator.render() def show_event(self): diff --git a/openpilot/system/updated/updated.py b/openpilot/system/updated/updated.py index 8363808dd..96954ea29 100755 --- a/openpilot/system/updated/updated.py +++ b/openpilot/system/updated/updated.py @@ -277,6 +277,8 @@ def handle_agnos_update() -> None: cloudlog.info(f"Beginning background installation for AGNOS {updated_version}") manifest_path = os.path.join(OVERLAY_MERGED, "openpilot/system/hardware/comma/agnos.json") + if not os.path.isfile(manifest_path): # releases before the openpilot/ move + manifest_path = os.path.join(OVERLAY_MERGED, "system/hardware/tici/agnos.json") target_slot_number = get_target_slot_number() flash_agnos_update(manifest_path, target_slot_number, cloudlog, progress_callback=on_progress) diff --git a/panda/board/jungle/__init__.py b/panda/board/jungle/__init__.py index 3fe8afd7f..b4d870b54 100644 --- a/panda/board/jungle/__init__.py +++ b/panda/board/jungle/__init__.py @@ -47,7 +47,7 @@ class PandaJungle(Panda): HARNESS_ORIENTATION_2 = 2 @classmethod - def spi_connect(cls, serial, ignore_version=False): + def spi_connect(cls, serial): return None, None, None, None def flash(self, fn=None, code=None, reconnect=True):