From 47ccab65d90cd93049733898c0d78c85dadf2ee8 Mon Sep 17 00:00:00 2001 From: inauner Date: Wed, 26 Aug 2026 13:52:27 -0700 Subject: [PATCH] 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