This commit is contained in:
firestar5683
2026-08-17 09:59:08 -05:00
parent 161ecbc1ab
commit 3719f97866
17 changed files with 362 additions and 35 deletions
Binary file not shown.
+2
View File
@@ -132,7 +132,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"SentryModeCapture", {CLEAR_ON_MANAGER_START, BOOL}},
{"SentryModeLastEvent", {PERSISTENT, JSON, "{}", "{}"}},
{"SentryModeNtfyUrl", {PERSISTENT, STRING}},
{"SentryModeSensitivity", {PERSISTENT, FLOAT, "0.04", "0.04", 0, SETTINGS_SIMPLE}},
{"SentryModeStatus", {CLEAR_ON_MANAGER_START | DONT_LOG, JSON}},
{"SentryModeWarningTime", {PERSISTENT, FLOAT, "1.0", "1.0", 0, SETTINGS_SIMPLE}},
{"SentryModeWebhook", {PERSISTENT, STRING}},
{"SecOCKey", {PERSISTENT | DONT_LOG, STRING}},
{"ShowDebugInfo", {PERSISTENT, BOOL}},
Binary file not shown.
+22 -7
View File
@@ -27,6 +27,7 @@ from openpilot.selfdrive.controls.lib.longitudinal_vehicle_tunes import (
is_gm_silverado_early_follow_lead,
is_toyota_rav4_tss2_post_departure_tune,
get_toyota_rav4_tss2_early_lead_cap,
is_toyota_rav4_tss2_radar_follow_lead,
get_toyota_sienna_post_departure_restop_cap,
get_untracked_slow_lead_decel_scale,
)
@@ -128,6 +129,7 @@ VISION_LEAD_APPROACH_DEFICIT_BUFFER_GAIN = 0.20
VISION_LEAD_APPROACH_BRAKING_DEFICIT_MIN = 0.75
VISION_LEAD_APPROACH_BRAKING_MIN_LEAD_BRAKE = 0.45
VISION_LEAD_APPROACH_BRAKING_FULL_LEAD_BRAKE = 1.20
PLANNER_SAFETY_WARNING_INTERVAL = 5.0
VISION_LEAD_APPROACH_BRAKING_FLOOR_MIN_DECEL = 1.30
VISION_LEAD_APPROACH_BRAKING_FLOOR_MAX_DECEL = 1.75
VISION_LEAD_APPROACH_CONFIRM_TIME = 0.25
@@ -458,7 +460,9 @@ VISION_CLOSE_RELEASE_HOLD_MIN_LEAD_DELTA = -0.1
VISION_CLOSE_RELEASE_HOLD_MAX_LEAD_DELTA = 1.5
VISION_CLOSE_RELEASE_HOLD_MIN_BRAKE = 0.18
VISION_CLOSE_RELEASE_HOLD_MAX_BRAKE = 0.40
MANUAL_STOP_RESUME_OVERRIDE_TIME = 3.0
# Give a driver-initiated launch enough time to clear a stale model stop/light
# prediction before the stop request is allowed to reassert.
MANUAL_STOP_RESUME_OVERRIDE_TIME = 6.0
MANUAL_STOP_RESUME_OVERRIDE_MAX_SPEED = 2.0
def get_planner_v_ego(CP, car_state):
@@ -573,6 +577,7 @@ class LongitudinalPlanner:
self._uncert_last = 0.0
self._uncert_last_t = None
self._panic_bypass_log_t = 0.0
self._safety_warning_log_t = 0.0
self.effective_t_follow = None
self.vision_low_speed_stop_hold_until = 0.0
self.vision_lead_approach_confirm_t = 0.0
@@ -1234,7 +1239,10 @@ class LongitudinalPlanner:
starpilot_car_state = sm["starpilotCarState"]
except KeyError:
starpilot_car_state = None
accel_pressed = bool(getattr(starpilot_car_state, "accelPressed", False))
accel_pressed = bool(
getattr(starpilot_car_state, "accelPressed", False) or
getattr(sm["carState"], "gasPressed", False)
)
model_should_stop = bool(getattr(sm["modelV2"].action, "shouldStop", False))
standstill = bool(getattr(sm["carState"], "standstill", False))
forcing_stop = bool(getattr(sm["starpilotPlan"], "forcingStop", False))
@@ -1980,9 +1988,14 @@ class LongitudinalPlanner:
not experimental_mode and
any(is_gm_silverado_early_follow_lead(self.CP, lead, scene_v_ego) for lead in (self.lead_one, self.lead_two))
)
rav4_radar_follow = (
not experimental_mode and
any(is_toyota_rav4_tss2_radar_follow_lead(self.CP, lead, scene_v_ego)
for lead in (self.lead_one, self.lead_two))
)
# StarPilot trackingLead is debounce/model-length based. Keep a raw close-lead
# safety path so ACC/chill does not ignore a visible lead during that debounce.
lead_control_active = tracking_lead or raw_close_lead_control or early_truck_follow
lead_control_active = tracking_lead or raw_close_lead_control or early_truck_follow or rav4_radar_follow
lead_one_active = bool(self.lead_one.status and lead_control_active)
effective_t_follow = self.get_dynamic_t_follow(sm['starpilotPlan'].tFollow, self.lead_one if lead_one_active else None, v_ego)
@@ -2209,10 +2222,12 @@ class LongitudinalPlanner:
# Safety checks for rubber-banding mitigation
max_jerk = np.max(np.abs(self.mpc.j_solution))
max_accel_change = np.max(np.abs(np.diff(self.mpc.a_solution)))
if max_jerk > 5.0: # m/s^3
cloudlog.warning(f"High jerk detected: {max_jerk:.2f} m/s^3")
if max_accel_change > 2.0: # m/s^2
cloudlog.warning(f"High acceleration change: {max_accel_change:.2f} m/s^2")
if (max_jerk > 5.0 or max_accel_change > 2.0) and now_t - self._safety_warning_log_t >= PLANNER_SAFETY_WARNING_INTERVAL:
cloudlog.warning(
f"Longitudinal planner output discontinuity: jerk={max_jerk:.2f} m/s^3, "
f"accel_change={max_accel_change:.2f} m/s^2"
)
self._safety_warning_log_t = now_t
# Interpolate 0.05 seconds and save as starting point for next iteration
a_prev = self.a_desired
@@ -27,8 +27,18 @@ TOYOTA_RAV4_TSS2_EARLY_LEAD_MIN_CLOSING_SPEED = 4.0
TOYOTA_RAV4_TSS2_EARLY_LEAD_MIN_BRAKE = 0.8
TOYOTA_RAV4_TSS2_EARLY_LEAD_MAX_BRAKE = 2.0
TOYOTA_RAV4_TSS2_EARLY_LEAD_MAX_DECEL = 0.5
TOYOTA_RAV4_TSS2_RADAR_FOLLOW_MIN_SPEED = 5.0
TOYOTA_RAV4_TSS2_RADAR_FOLLOW_MIN_CLOSING_SPEED = 0.75
TOYOTA_RAV4_TSS2_RADAR_FOLLOW_MIN_DISTANCE = 70.0
TOYOTA_RAV4_TSS2_RADAR_FOLLOW_MAX_DISTANCE = 100.0
TOYOTA_RAV4_TSS2_RADAR_FOLLOW_DISTANCE_TIME = 4.5
TOYOTA_RAV4_TSS2_RADAR_FOLLOW_DISTANCE_OFFSET = 32.0
TOYOTA_RAV4_TSS2_RADAR_FOLLOW_MAX_LATERAL_OFFSET = 1.75
TOYOTA_CAMRY_TSS2_FORCE_STOP_HANDOFF_M = 4.5
TOYOTA_CAMRY_TSS2_FORCE_STOP_DISTANCE_BIAS_M = 2.0
# The Camry's force-stop path otherwise consumes the model endpoint before the
# normal MPC stop-distance margin can be applied. Keep it within the forward
# offset range exposed by the Force Stop setting.
TOYOTA_CAMRY_TSS2_FORCE_STOP_DISTANCE_BIAS_M = 6.0
DEFAULT_FORCE_STOP_HANDOFF_M = 6.0
@@ -83,6 +93,31 @@ def get_toyota_rav4_tss2_early_lead_cap(CP, lead, v_ego, accel_min):
return max(float(accel_min), -min(TOYOTA_RAV4_TSS2_EARLY_LEAD_MAX_DECEL, decel))
def is_toyota_rav4_tss2_radar_follow_lead(CP, lead, v_ego):
"""Keep a credible RAV4 radar lead active through model-horizon dropouts."""
if (
not is_toyota_rav4_tss2_post_departure_tune(CP) or
lead is None or not bool(getattr(lead, "status", False)) or
not bool(getattr(lead, "radar", False)) or
float(v_ego) < TOYOTA_RAV4_TSS2_RADAR_FOLLOW_MIN_SPEED or
abs(float(getattr(lead, "yRel", 0.0))) > TOYOTA_RAV4_TSS2_RADAR_FOLLOW_MAX_LATERAL_OFFSET
):
return False
lead_speed = max(float(getattr(lead, "vLead", 0.0)), 0.0)
closing_speed = float(v_ego) - lead_speed
distance_limit = float(np.clip(
TOYOTA_RAV4_TSS2_RADAR_FOLLOW_DISTANCE_OFFSET +
TOYOTA_RAV4_TSS2_RADAR_FOLLOW_DISTANCE_TIME * float(v_ego),
TOYOTA_RAV4_TSS2_RADAR_FOLLOW_MIN_DISTANCE,
TOYOTA_RAV4_TSS2_RADAR_FOLLOW_MAX_DISTANCE,
))
return (
float(getattr(lead, "dRel", float("inf"))) <= distance_limit and
closing_speed >= TOYOTA_RAV4_TSS2_RADAR_FOLLOW_MIN_CLOSING_SPEED
)
def allow_radar_standstill_gap_settle(CP):
"""Keep the generic stopped-lead gap nudge out of the early RAV4 TSS2 path."""
return not (
@@ -23,6 +23,7 @@ from openpilot.selfdrive.controls.lib.longitudinal_vehicle_tunes import (
get_follow_prebrake_min_headway,
get_toyota_rav4_tss2_early_lead_cap,
get_toyota_sienna_post_departure_restop_cap,
is_toyota_rav4_tss2_radar_follow_lead,
is_gm_silverado_early_follow_lead,
is_toyota_rav4_tss2_post_departure_tune,
)
@@ -1764,6 +1765,23 @@ def test_manual_resume_override_clears_no_lead_model_stop_at_standstill(model_ve
assert planner.output_a_target >= 0.2
@pytest.mark.parametrize("model_version", ["v11", "v12", "v13", "v14", "v15"])
def test_manual_resume_override_accepts_accelerator_pedal(model_version):
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
planner = LongitudinalPlanner(CP, init_v=0.0)
sm = make_sm(0.0, desired_accel=0.0, min_accel=-0.5)
sm["carState"].standstill = True
sm["carState"].gasPressed = True
sm["controlsState"].longControlState = LongCtrlState.stopping
sm["modelV2"].action.shouldStop = True
sm["starpilotPlan"].forcingStop = True
planner.update(sm, make_toggles(model_version))
assert not planner.output_should_stop
assert planner.output_a_target >= 0.2
@pytest.mark.parametrize("model_version", ["v11", "v12", "v13", "v14", "v15"])
def test_manual_resume_override_does_not_clear_stopped_lead_stop(model_version):
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
@@ -2827,6 +2845,25 @@ def test_rav4_tss2_early_lead_cap_does_not_change_other_paths():
assert get_toyota_rav4_tss2_early_lead_cap(rav4, radar_lead, 21.0, -3.5) is None
def test_rav4_tss2_radar_follow_admits_closing_lead_before_model_tracking():
rav4 = ToyotaCarInterface.get_non_essential_params(TOYOTA_CAR.TOYOTA_RAV4_TSS2)
lead = make_lead(status=True, d_rel=74.0, v_lead=1.0, radar=True)
assert is_toyota_rav4_tss2_radar_follow_lead(rav4, lead, 9.7)
def test_rav4_tss2_radar_follow_admission_is_vehicle_and_safety_scoped():
rav4 = ToyotaCarInterface.get_non_essential_params(TOYOTA_CAR.TOYOTA_RAV4_TSS2)
other = ToyotaCarInterface.get_non_essential_params(TOYOTA_CAR.TOYOTA_RAV4_TSS2_2022)
lead = make_lead(status=True, d_rel=74.0, v_lead=1.0, radar=True)
far_lead = make_lead(status=True, d_rel=110.0, v_lead=1.0, radar=True)
vision_lead = make_lead(status=True, d_rel=74.0, v_lead=1.0, radar=False, model_prob=1.0)
assert not is_toyota_rav4_tss2_radar_follow_lead(other, lead, 9.7)
assert not is_toyota_rav4_tss2_radar_follow_lead(rav4, far_lead, 9.7)
assert not is_toyota_rav4_tss2_radar_follow_lead(rav4, vision_lead, 9.7)
@pytest.mark.parametrize("model_version", ["v11", "v12", "v13", "v14", "v15"])
def test_force_stop_handoff_sets_output_should_stop_before_zero_vcruise(model_version):
v_ego = 1.25
@@ -126,7 +126,7 @@ def test_camry_tss2_uses_closer_force_stop_handoff():
def test_camry_tss2_gets_forward_force_stop_bias_only():
assert get_force_stop_distance_bias("TOYOTA_CAMRY_TSS2") == pytest.approx(2.0)
assert get_force_stop_distance_bias("TOYOTA_CAMRY_TSS2") == pytest.approx(6.0)
assert get_force_stop_distance_bias("TOYOTA_RAV4_TSS2") == pytest.approx(0.0)
@@ -510,6 +510,23 @@ def test_force_stop_releases_after_cem_light_clears_while_moving():
assert not vcruise.force_stop_from_light
def test_force_stop_light_release_ignores_coarse_stopped_model_horizon():
planner, vcruise = make_vcruise(red_light=True, raw_model_stopped=True, forcing_stop=True)
sm = make_sm(standstill=False)
toggles = make_toggles()
update_vcruise(vcruise, sm, toggles, now=0.0, v_ego=3.0)
planner.starpilot_cem.stop_light_detected = False
update_vcruise(vcruise, sm, toggles, now=0.25, v_ego=3.0)
assert vcruise.forcing_stop
result = update_vcruise(vcruise, sm, toggles, now=0.75, v_ego=3.0)
assert result == pytest.approx(20.0)
assert not vcruise.forcing_stop
assert not vcruise.force_stop_from_light
def test_force_stop_turn_scene_veto_blocks_new_activation():
_, vcruise = make_vcruise(red_light=False, raw_model_stopped=False, forcing_stop=False)
sm = make_sm(standstill=False)
+2 -2
View File
@@ -408,7 +408,7 @@ class HudRenderer(Widget):
# draw drop shadow
circle_radius = 162 // 2
rl.draw_circle_gradient(int(x + circle_radius), int(y + circle_radius), circle_radius,
rl.draw_circle_gradient(rl.Vector2(x + circle_radius, y + circle_radius), circle_radius,
rl.Color(0, 0, 0, int(255 / 2 * alpha)), rl.BLANK)
set_speed_color = rl.Color(255, 255, 255, int(255 * 0.9 * alpha))
@@ -630,7 +630,7 @@ class HudRenderer(Widget):
center = rl.Vector2(button_rect.x + button_rect.width / 2, button_rect.y + button_rect.height / 2)
radius = min(button_rect.width, button_rect.height) / 2
rl.draw_circle_gradient(int(center.x), int(center.y), radius, rl.Color(0, 0, 0, 90), rl.BLANK)
rl.draw_circle_gradient(center, radius, rl.Color(0, 0, 0, 90), rl.BLANK)
rl.draw_circle(int(center.x), int(center.y), radius, fill)
rl.draw_ring(center, radius - 6, radius, 0, 360, 48, outline)
@@ -487,7 +487,6 @@ class StarPilotVCruise:
self.force_stop_from_light and
not sm["carState"].standstill and
not stop_light_detected and
not raw_model_stopped and
not dash_active
)
if light_stop_cleared:
@@ -86,6 +86,31 @@
outline: none;
}
.sentry-range-control {
align-items: center;
display: flex;
gap: 0.75rem;
min-width: min(100%, 28rem);
}
.sentry-range {
accent-color: var(--main-fg);
cursor: pointer;
flex: 1;
min-width: 12rem;
}
.sentry-range:disabled {
cursor: not-allowed;
}
.sentry-range-value {
color: var(--text-color);
font-variant-numeric: tabular-nums;
min-width: 3.5rem;
text-align: right;
}
.sentry-toggle {
accent-color: var(--main-fg);
cursor: pointer;
@@ -190,4 +215,13 @@
min-width: 0;
width: 100%;
}
.sentry-range-control {
min-width: 0;
width: 100%;
}
.sentry-range {
min-width: 0;
}
}
@@ -50,6 +50,11 @@ function startPolling() {
pollTimer = window.setInterval(fetchStatus, 5000)
}
function numericParam(key, fallback) {
const value = Number(state.params[key])
return Number.isFinite(value) ? value : fallback
}
async function saveParam(key, value) {
state.savingKey = key
try {
@@ -231,7 +236,7 @@ export function SentryMode() {
<section class="sentry-card">
<h3>Configuration</h3>
<p class="sentry-muted">Galaxy is the built-in notification and image viewer. Webhook and ntfy delivery are optional.</p>
<p class="sentry-muted">Galaxy is the built-in notification and image viewer. Sentry detects accelerometer movement. Webhook and ntfy delivery are optional.</p>
${() => state.loading ? html`<div class="sentry-loading">Loading Sentry settings…</div>` : html`
<label class="sentry-setting-row">
@@ -269,6 +274,40 @@ export function SentryMode() {
@change="${(event) => saveParam("SentryModeNtfyUrl", event.currentTarget.value.trim())}" />
</label>
<label class="sentry-field sentry-range-field">
<span><strong>Motion sensitivity</strong><small>Lower values detect smaller acceleration changes. Default: 0.04.</small></span>
<div class="sentry-range-control">
<input
class="sentry-range"
type="range"
min="0.005"
max="1"
step="0.005"
value="${() => numericParam("SentryModeSensitivity", 0.04)}"
disabled="${() => state.savingKey === "SentryModeSensitivity"}"
aria-label="Motion sensitivity"
@change="${(event) => saveParam("SentryModeSensitivity", Number(event.currentTarget.value))}" />
<output class="sentry-range-value">${() => numericParam("SentryModeSensitivity", 0.04).toFixed(3)}</output>
</div>
</label>
<label class="sentry-field sentry-range-field">
<span><strong>Warning persistence</strong><small>How long movement must continue before the first alert. Default: 1 second.</small></span>
<div class="sentry-range-control">
<input
class="sentry-range"
type="range"
min="0.1"
max="10"
step="0.1"
value="${() => numericParam("SentryModeWarningTime", 1)}"
disabled="${() => state.savingKey === "SentryModeWarningTime"}"
aria-label="Warning persistence"
@change="${(event) => saveParam("SentryModeWarningTime", Number(event.currentTarget.value))}" />
<output class="sentry-range-value">${() => numericParam("SentryModeWarningTime", 1).toFixed(1)}s</output>
</div>
</label>
<div class="sentry-action-row">
<button class="sentry-button" @click="${enablePush}" disabled="${() => state.pushBusy}">
${() => state.pushBusy ? "Enabling…" : "Enable browser notifications"}
@@ -28,6 +28,7 @@ self.addEventListener("push", (event) => {
badge: scopedUrl("/assets/images/favicon-32x32.png"),
requireInteraction: true,
}
if (data.image) options.image = scopedUrl(data.image)
event.waitUntil(self.registration.showNotification(title, options))
})
@@ -584,21 +584,65 @@ def test_route_listing_uses_all_segment_times_when_segment_zero_was_touched(tmp_
assert end == "2026-07-18T07:22:00"
def test_route_listing_prefers_logged_time_after_offline_clock_reset(tmp_path, monkeypatch):
def test_route_listing_does_not_parse_logs_when_filesystem_time_is_valid(tmp_path, monkeypatch):
route_start = utilities.datetime(2026, 7, 18, 7, 19, 0)
route_name = "000011e3--6e01289631"
segment = tmp_path / f"{route_name}--0"
segment.mkdir()
(segment / "qlog.zst").write_bytes(b"placeholder")
segment_end = route_start.timestamp() + 60
os.utime(segment, (segment_end, segment_end))
def fail_if_read(_path):
raise AssertionError("valid filesystem timestamps must not decompress route logs")
monkeypatch.setattr(utilities, "_route_logged_start_time", fail_if_read)
routes = utilities._list_dashboard_routes([tmp_path])
assert routes[0]["startedAt"] == route_start
assert routes[0]["timeSource"] == utilities.DASHBOARD_TIME_SOURCE_FILESYSTEM
def test_route_listing_applies_limit_before_parsing_old_logs(tmp_path, monkeypatch):
old_segment = tmp_path / "00000001--abcdef1234--0"
old_segment.mkdir()
(old_segment / "qlog.zst").write_bytes(b"placeholder")
stale_time = utilities.datetime(2025, 7, 18, 7, 20, 0).timestamp()
os.utime(old_segment, (stale_time, stale_time))
current_segment = tmp_path / "00000002--abcdef1234--0"
current_segment.mkdir()
current_time = utilities.datetime(2026, 7, 18, 7, 20, 0).timestamp()
os.utime(current_segment, (current_time, current_time))
def fail_if_read(_path):
raise AssertionError("routes outside the scan limit must not be parsed")
monkeypatch.setattr(utilities, "_route_logged_start_time", fail_if_read)
routes = utilities._list_dashboard_routes([tmp_path], limit=1)
assert [route["name"] for route in routes] == ["00000002--abcdef1234"]
def test_route_listing_defers_offline_clock_repair_to_background_analysis(tmp_path, monkeypatch):
route_name = "000011e3--6e01289631"
segment = tmp_path / f"{route_name}--0"
segment.mkdir()
(segment / "qlog.zst").write_bytes(b"placeholder")
stale_time = utilities.datetime(2025, 7, 18, 7, 20, 0).timestamp()
os.utime(segment, (stale_time, stale_time))
monkeypatch.setattr(utilities, "_route_logged_start_time", lambda _path: route_start)
def fail_if_read(_path):
raise AssertionError("dashboard route listing must not decompress logs")
monkeypatch.setattr(utilities, "_route_logged_start_time", fail_if_read)
routes = utilities._list_dashboard_routes([tmp_path])
assert routes[0]["startedAt"] == route_start
assert routes[0]["timeSource"] == utilities.DASHBOARD_TIME_SOURCE_LOG
assert routes[0]["startedAt"] is None
assert routes[0]["timeSource"] == ""
def test_top_models_are_ranked_from_persisted_usage_not_favorites():
+55 -3
View File
@@ -109,6 +109,10 @@ LEGACY_LATERAL_METHOD_API_PREFIX = "/api/" + "".join(("f", "t", "m"))
VASM_CONFIGURATION_KEYS = {"VASMEnabled", "VASMConfidenceThreshold", "VASMSmoothSeconds", "VASMAnnotationConfig"}
PIP_PREVIEW_CONFIGURATION_KEYS = {"PIPPreviewEnabled", "PIPPreviewMask", "PIPPreviewShowOnBlinker", "PIPPreviewShowOnBSM"}
MODEL_SMOOTHING_KEYS = {"LatSmoothSeconds", "LongSmoothSeconds"}
SENTRY_NUMERIC_PARAM_BOUNDS = {
"SentryModeSensitivity": (0.005, 1.0, 0.005),
"SentryModeWarningTime": (0.1, 10.0, 0.1),
}
GALAXY_DEPS_PATH = "/data/galaxy_deps"
LEGACY_GALAXY_DEPS_PATH = "/data/" + "".join(chr(code) for code in (112, 111, 110, 100)) + "_deps"
@@ -747,6 +751,24 @@ def _sentry_push_subscription_count() -> int:
return len(_load_sentry_push_subscriptions())
def _sentry_public_base_url() -> str:
configured_url = os.getenv("STARPILOT_GALAXY_PUBLIC_URL", "").strip().rstrip("/")
if configured_url:
return configured_url
slug = _read_galaxy_text(_get_galaxy_dir() / "glxyslug")
return f"https://galaxy.firestar.link/{slug}" if slug else ""
def _sentry_external_image_urls(event: dict) -> list[str]:
base_url = _sentry_public_base_url()
if not base_url:
return []
public_event = _public_sentry_event(event)
return [f"{base_url}{image_url}" for image_url in public_event["imageUrls"]]
def _sentry_notification_channels() -> dict[str, bool]:
return {
"webPush": _sentry_push_subscription_count() > 0,
@@ -781,6 +803,9 @@ def _dispatch_sentry_push(event: dict) -> None:
"eventId": event_id,
"url": f"/sentry?event={quote(event_id, safe='')}",
}
image_urls = _sentry_external_image_urls(event)
if image_urls:
payload["image"] = image_urls[0]
with _SENTRY_PUSH_LOCK:
subscriptions = _load_sentry_push_subscriptions()
@@ -840,10 +865,14 @@ def _dispatch_sentry_event(event: dict) -> None:
ntfy_url = (params.get("SentryModeNtfyUrl", encoding="utf-8") or "").strip()
if ntfy_url:
try:
image_urls = _sentry_external_image_urls(event)
headers = {"Title": "StarPilot Sentry Mode", "Priority": "urgent", "Tags": "warning,car"}
if image_urls:
headers["Attach"] = image_urls[0]
response = requests.post(
ntfy_url,
data=message.encode("utf-8"),
headers={"Title": "StarPilot Sentry Mode", "Priority": "urgent", "Tags": "warning,car"},
headers=headers,
timeout=10,
)
response.raise_for_status()
@@ -951,6 +980,7 @@ _STATS_RESPONSE_CACHE = {
"updated_at": 0.0,
"payload": None,
}
_STATS_RESPONSE_LOCK = threading.Lock()
try:
FOOTAGE_PATHS = [
@@ -4859,6 +4889,17 @@ def setup(app):
if key not in allowed_keys:
return jsonify({"error": f"Parameter '{key}' is not editable."}), 403
if key in SENTRY_NUMERIC_PARAM_BOUNDS:
minimum, maximum, step = SENTRY_NUMERIC_PARAM_BOUNDS[key]
try:
numeric = float(data["value"])
except (TypeError, ValueError):
return jsonify({"error": f"{key} must be numeric."}), 400
if not math.isfinite(numeric) or numeric < minimum or numeric > maximum:
return jsonify({"error": f"{key} must be between {minimum} and {maximum}."}), 400
numeric = round(round(numeric / step) * step, 3)
str_val = str(numeric)
if key == "AlphaLongitudinalEnabled":
if not _get_alpha_longitudinal_available():
return jsonify({"error": "Alpha Longitudinal is not available for the detected vehicle."}), 403
@@ -6295,8 +6336,7 @@ def setup(app):
return jsonify({"message": "Speed limit processing started.", "status": "Calculating..."}), 202
@app.route("/api/stats", methods=["GET"])
def get_stats():
def _get_stats_locked():
cache_now = time.monotonic()
cached_payload = _STATS_RESPONSE_CACHE.get("payload")
if cached_payload is not None and cache_now - _STATS_RESPONSE_CACHE.get("updated_at", 0.0) < STATS_RESPONSE_CACHE_SECONDS:
@@ -6340,6 +6380,18 @@ def setup(app):
})
return payload
@app.route("/api/stats", methods=["GET"])
def get_stats():
cache_now = time.monotonic()
cached_payload = _STATS_RESPONSE_CACHE.get("payload")
if cached_payload is not None and cache_now - _STATS_RESPONSE_CACHE.get("updated_at", 0.0) < STATS_RESPONSE_CACHE_SECONDS:
return cached_payload
# Flask serves requests concurrently. Serialize cache misses so a slow
# storage scan cannot be multiplied by repeated homepage polling.
with _STATS_RESPONSE_LOCK:
return _get_stats_locked()
@app.route("/api/stats/ignore_drive", methods=["POST"])
def ignore_drive_stats():
request_data = request.get_json() or {}
+35 -14
View File
@@ -995,25 +995,23 @@ def _select_dashboard_segment_candidate(candidates):
def _estimate_route_start_details(segments):
estimates = []
time_source = DASHBOARD_TIME_SOURCE_FILESYSTEM
# Reading a compressed qlog materializes the complete log in memory. Route
# log analysis happens in the bounded background worker; the synchronous
# dashboard path must only use filesystem metadata.
filesystem_estimates = []
for segment in segments:
log_path = get_route_log_path(segment.get("path"))
if log_path is not None:
logged_time = _route_logged_start_time(log_path)
if logged_time is not None and _dashboard_time_is_valid(logged_time, require_recent=True):
estimates.append(logged_time.timestamp())
time_source = DASHBOARD_TIME_SOURCE_LOG
continue
segment_num = max(0, _safe_int(segment.get("num", 0), 0))
# Segment directory mtimes normally land at the end of their one-minute segment.
estimate = _segment_mtime(segment.get("path")) - (segment_num + 1) * 60
parsed = _timestamp_to_dashboard_time(estimate, require_recent=True)
parsed = _timestamp_to_dashboard_time(estimate)
if parsed is not None:
estimates.append(parsed.timestamp())
filesystem_estimates.append(parsed.timestamp())
# Dashboard analysis can touch a segment directory later, but cannot make it older.
return (datetime.fromtimestamp(min(estimates)), time_source) if estimates else (None, "")
if filesystem_estimates:
return datetime.fromtimestamp(min(filesystem_estimates)), DASHBOARD_TIME_SOURCE_FILESYSTEM
return None, ""
def _estimate_route_started_at(segments):
@@ -1051,8 +1049,25 @@ def _list_dashboard_routes(footage_paths, limit=DASHBOARD_ROUTE_SCAN_LIMIT):
route["segments_by_num"].setdefault(segment_num, []).append(entry)
route["modified_at"] = max(route["modified_at"], _segment_mtime(entry))
# Route IDs are monotonically increasing on-device and remain reliable when
# the wall clock is wrong. Bound the candidate set before inspecting segment
# contents so old route history cannot make every homepage request unbounded.
def route_sequence(route):
try:
return int(str(route["name"]).split("--", 1)[0], 16)
except (KeyError, TypeError, ValueError):
return -1
candidates = sorted(
routes.values(),
key=lambda route: (route_sequence(route), route["modified_at"], route["name"]),
reverse=True,
)
if limit is not None:
candidates = candidates[:max(0, limit)]
route_infos = []
for route in routes.values():
for route in candidates:
segments = []
for segment_num, candidates in sorted(route["segments_by_num"].items()):
selected = _select_dashboard_segment_candidate(candidates)
@@ -2961,6 +2976,12 @@ def get_route_start_time(path):
if modified_time <= 0:
return None
filesystem_time = _timestamp_to_dashboard_time(modified_time)
if filesystem_time is not None:
return filesystem_time
# Recover dates from logs only for files created while the system clock was
# genuinely invalid. Normal route browsing stays metadata-only.
logged_time = _route_logged_start_time(log_path)
if logged_time is not None:
return logged_time
+31
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
from datetime import datetime, timezone
import json
import math
import os
import threading
import time
@@ -23,7 +24,12 @@ from openpilot.system.sentryd.detector import MotionDetector
ARM_DELAY_SECONDS = 90.0
LOOP_INTERVAL_SECONDS = 0.1
SENSITIVITY = 0.04
MIN_SENSITIVITY = 0.005
MAX_SENSITIVITY = 1.0
WARNING_TRIGGER_COUNT = 10
WARNING_TIME_SECONDS = 1.0
MIN_WARNING_TIME_SECONDS = 0.1
MAX_WARNING_TIME_SECONDS = 10.0
ALARM_TRIGGER_COUNT = 25
ALARM_TIME_SECONDS = 30.0
RESET_TIME_SECONDS = 60.0
@@ -62,6 +68,30 @@ class SentryMode:
self.started_at = clock()
self.armed = False
self._last_status = None
self._sync_detector_settings()
def _read_float_param(self, key: str, default: float, minimum: float, maximum: float) -> float:
try:
value = self.params.get_float(key, return_default=True, default=default)
except (AttributeError, TypeError, ValueError):
value = default
try:
value = float(value)
except (TypeError, ValueError):
value = default
if not math.isfinite(value):
value = default
return min(maximum, max(minimum, value))
def _sync_detector_settings(self) -> None:
self.detector.sensitivity = self._read_float_param(
"SentryModeSensitivity", SENSITIVITY, MIN_SENSITIVITY, MAX_SENSITIVITY,
)
warning_time = self._read_float_param(
"SentryModeWarningTime", WARNING_TIME_SECONDS, MIN_WARNING_TIME_SECONDS, MAX_WARNING_TIME_SECONDS,
)
self.detector.warning_trigger_count = max(1, math.ceil(warning_time / LOOP_INTERVAL_SECONDS))
def _is_onroad(self) -> bool:
try:
@@ -170,6 +200,7 @@ class SentryMode:
self.armed = True
self._write_status("armed")
self._sync_detector_settings()
message = self.sm["accelerometer"]
if message is None or message.acceleration is None:
self._write_status("sensor_unavailable")
+1 -1
View File
@@ -353,7 +353,7 @@ class MiciKeyboard(Widget):
# draw black circle behind selected key
circle_alpha = int(self._selected_key_filter.x * 225)
rl.draw_circle_gradient(int(key_x + key.rect.width / 2), int(key_y + key.rect.height / 2),
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:
# move other keys away from selected key a bit