From 979b29765275b46f4127304ecbc1d9351b4356d8 Mon Sep 17 00:00:00 2001 From: inauner Date: Wed, 26 Aug 2026 13:00:10 -0700 Subject: [PATCH 1/2] galaxy: fix Toyota and Lexus door lock and unlock vehicle check and execution --- starpilot/system/the_galaxy/the_galaxy.py | 63 ++++++++++++++--------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/starpilot/system/the_galaxy/the_galaxy.py b/starpilot/system/the_galaxy/the_galaxy.py index 5891ada7c..29303d685 100644 --- a/starpilot/system/the_galaxy/the_galaxy.py +++ b/starpilot/system/the_galaxy/the_galaxy.py @@ -4682,48 +4682,63 @@ def setup(app): try: with car.CarParams.from_bytes(params.get("CarParamsPersistent")) as cp: if tool == "doors": - return jsonify({"result": HARDWARE.get_device_type() != "tici" and cp.carName == "toyota"}) + car_brand = getattr(cp, "brand", getattr(cp, "carName", "")) + return jsonify({"result": car_brand == "toyota"}) elif tool == "tsk": - return jsonify({"result": cp.secOcRequired}) + return jsonify({"result": getattr(cp, "secOcRequired", False)}) except Exception: pass return jsonify({"result": False}) @app.route("/api/doors/lock", methods=["POST"]) def lock_doors(): - can_parser = CANParser("toyota_nodsu_pt_generated", [("DOOR_LOCKS", 3)], bus=0) - can_sock = messaging.sub_sock("can", timeout=100) + try: + can_parser = CANParser("toyota_nodsu_pt_generated", [("DOOR_LOCKS", 3)], bus=0) + can_sock = messaging.sub_sock("can", timeout=100) - while True: - with Panda(disable_checks=True) as panda: - if not params.get_bool("IsOnroad"): - panda.set_safety_mode(panda.SAFETY_TOYOTA) - panda.can_send(0x750, LOCK_CMD, 0) + for _ in range(6): + try: + with Panda(disable_checks=True) as panda: + if not params.get_bool("IsOnroad"): + panda.set_safety_mode(car.CarParams.SafetyModel.toyota) + panda.can_send(0x750, LOCK_CMD, 0) + panda.can_send(0x750, LOCK_CMD, 1) + except Exception: + pass - time.sleep(1) + time.sleep(1) - lock_status = get_lock_status(can_parser, can_sock) - if lock_status == 0: - break + lock_status = get_lock_status(can_parser, can_sock) + if lock_status == 0: + break + except Exception as e: + return {"message": f"Lock failed: {e}"}, 500 return {"message": "Doors locked!"} @app.route("/api/doors/unlock", methods=["POST"]) def unlock_doors(): - can_parser = CANParser("toyota_nodsu_pt_generated", [("DOOR_LOCKS", 3)], bus=0) - can_sock = messaging.sub_sock("can", timeout=100) + try: + can_parser = CANParser("toyota_nodsu_pt_generated", [("DOOR_LOCKS", 3)], bus=0) + can_sock = messaging.sub_sock("can", timeout=100) - while True: - with Panda(disable_checks=True) as panda: - if not params.get_bool("IsOnroad"): - panda.set_safety_mode(panda.SAFETY_TOYOTA) - panda.can_send(0x750, UNLOCK_CMD, 0) + for _ in range(6): + try: + with Panda(disable_checks=True) as panda: + if not params.get_bool("IsOnroad"): + panda.set_safety_mode(car.CarParams.SafetyModel.toyota) + panda.can_send(0x750, UNLOCK_CMD, 0) + panda.can_send(0x750, UNLOCK_CMD, 1) + except Exception: + pass - time.sleep(1) + time.sleep(1) - lock_status = get_lock_status(can_parser, can_sock) - if lock_status != 0: - break + lock_status = get_lock_status(can_parser, can_sock) + if lock_status != 0: + break + except Exception as e: + return {"message": f"Unlock failed: {e}"}, 500 return {"message": "Doors unlocked!"} From 47ccab65d90cd93049733898c0d78c85dadf2ee8 Mon Sep 17 00:00:00 2001 From: inauner Date: Wed, 26 Aug 2026 13:52:27 -0700 Subject: [PATCH 2/2] hardware: add 30s sustained low voltage debounce and sentry power off notifications --- system/hardware/hardwared.py | 47 +++++++++++++++++++++++++++++ system/hardware/power_monitoring.py | 26 +++++++++++----- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/system/hardware/hardwared.py b/system/hardware/hardwared.py index ec7c52a59..9fb9be0f6 100644 --- a/system/hardware/hardwared.py +++ b/system/hardware/hardwared.py @@ -82,6 +82,32 @@ def notify_sentry_power_off(reason: str, power_monitor: PowerMonitoring) -> bool return False +def notify_sentry_low_voltage(power_monitor: PowerMonitoring) -> bool: + port = os.environ.get("SP_GALAXY_PORT", "8083" if PC else "8082") + v = round(power_monitor.car_voltage_mV / 1000, 2) + event = { + "eventId": f"low-voltage-{time.time_ns()}", + "kind": "warning", + "detectedAt": datetime.now(timezone.utc).isoformat(), + "reason": "low_voltage", + "message": f"Low vehicle battery warning: {v:.2f}V (at or below 11.8V).", + "voltage": v, + "instantVoltage": round(power_monitor.car_voltage_instant_mV / 1000, 2), + "batteryCapacityUwh": power_monitor.get_car_battery_capacity(), + } + try: + response = requests.post( + f"http://127.0.0.1:{port}/api/sentry/events", + json=event, + timeout=4, + ) + response.raise_for_status() + return True + except requests.RequestException as error: + cloudlog.warning(f"Sentry low-voltage notification unavailable: {error}") + return False + + class Chestnut: """Keep the ASM2464PD dock on the firmware expected by the GPU runtime.""" MAX_ATTEMPTS = 3 @@ -305,6 +331,8 @@ def hardware_thread(end_event, hw_queue) -> None: pwrsave = False offroad_cycle_count = 0 sentry_power_off_notified = False + sentry_low_voltage_notified = False + last_low_voltage_notify_ts = 0.0 params = Params() power_monitor = PowerMonitoring() @@ -523,6 +551,10 @@ def hardware_thread(end_event, hw_queue) -> None: statlog.sample("som_power_draw", som_power_draw) msg.deviceState.somPowerDrawW = som_power_draw + if not onroad_conditions["ignition"] and (count % int(30. / DT_HW) == 0): + low_v_str = f" [LOW VOLTAGE SUSTAINED: {time.monotonic() - power_monitor.low_voltage_start_time:.1f}s / 30.0s]" if power_monitor.low_voltage_start_time else "" + print(f"[hardwared] Offroad Power: {power_monitor.car_voltage_mV / 1000.0:.2f}V (instant: {power_monitor.car_voltage_instant_mV / 1000.0:.2f}V), draw: {current_power_draw:.1f}W{low_v_str}", flush=True) + # Check if we need to shut down shutdown_reason = power_monitor.shutdown_reason( onroad_conditions["ignition"], in_car, off_ts, started_seen, starpilot_toggles, @@ -536,6 +568,21 @@ def hardware_thread(end_event, hw_queue) -> None: else: sentry_power_off_notified = False + # Low voltage warning notification (without device shutdown) + if in_car and not onroad_conditions["ignition"] and off_ts is not None: + voltage_v = power_monitor.car_voltage_mV / 1000.0 + if voltage_v <= 11.8: + now_mono = time.monotonic() + if not sentry_low_voltage_notified or (now_mono - last_low_voltage_notify_ts > 1800): + sentry_low_voltage_notified = True + last_low_voltage_notify_ts = now_mono + if params.get_bool("SentryModeEnabled"): + notify_sentry_low_voltage(power_monitor) + elif voltage_v > 12.2: + sentry_low_voltage_notified = False + else: + sentry_low_voltage_notified = False + msg.deviceState.started = started_ts is not None msg.deviceState.startedMonoTime = int(1e9*(started_ts or 0)) diff --git a/system/hardware/power_monitoring.py b/system/hardware/power_monitoring.py index baf22fbd0..0e3224c88 100644 --- a/system/hardware/power_monitoring.py +++ b/system/hardware/power_monitoring.py @@ -19,6 +19,7 @@ MAX_TIME_OFFROAD_S = 30*3600 MIN_ON_TIME_S = 3600 DELAY_SHUTDOWN_TIME_S = 300 # Wait at least DELAY_SHUTDOWN_TIME_S seconds after offroad_time to shutdown. VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S = 60 +VOLTAGE_SHUTDOWN_SUSTAINED_TIME_S = 30.0 class PowerMonitoring: def __init__(self): @@ -29,12 +30,13 @@ class PowerMonitoring: self.next_pulsed_measurement_time = None self.car_voltage_mV = 12e3 # Low-passed version of peripheralState voltage self.car_voltage_instant_mV = 12e3 # Last value of peripheralState voltage + self.low_voltage_start_time = None # Monotonic timestamp when low voltage was first observed self.integration_lock = threading.Lock() - car_battery_capacity_uWh = self.params.get("CarBatteryCapacity") or 0 + car_battery_capacity_uWh = self.params.get("CarBatteryCapacity") or CAR_BATTERY_CAPACITY_uWh # Reset capacity if it's low - self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 10), car_battery_capacity_uWh) + self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 2), car_battery_capacity_uWh) # Calculation tick def calculate(self, voltage: int | None, ignition: bool): @@ -110,19 +112,29 @@ class PowerMonitoring: def shutdown_reason(self, ignition: bool, in_car: bool, offroad_timestamp: float | None, started_seen: bool, starpilot_toggles: SimpleNamespace) -> str | None: if offroad_timestamp is None: + self.low_voltage_start_time = None return None now = time.monotonic() offroad_time = (now - offroad_timestamp) - low_voltage_shutdown = (self.car_voltage_mV < (starpilot_toggles.low_voltage_shutdown * 1e3) and - offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S) + + cutoff_voltage = starpilot_toggles.low_voltage_shutdown if getattr(starpilot_toggles, "low_voltage_shutdown", 0) > 0 else 11.8 + is_below_voltage = self.car_voltage_mV < (cutoff_voltage * 1e3) + + if is_below_voltage and offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S: + if self.low_voltage_start_time is None: + self.low_voltage_start_time = now + low_voltage_sustained_time = now - self.low_voltage_start_time + low_voltage_shutdown = low_voltage_sustained_time >= VOLTAGE_SHUTDOWN_SUSTAINED_TIME_S + else: + self.low_voltage_start_time = None + low_voltage_shutdown = False + reason = None - if offroad_time > starpilot_toggles.device_shutdown_time: + if starpilot_toggles.device_shutdown_time > 0 and offroad_time > starpilot_toggles.device_shutdown_time: reason = "offroad_timeout" elif low_voltage_shutdown: reason = "low_voltage" - elif self.car_battery_capacity_uWh <= 0: - reason = "battery_capacity_exhausted" should_shutdown = reason is not None should_shutdown &= not ignition