Files
StarPilot/openpilot/starpilot/spot_monitor/preferences.py
T
firestar5683 9d298304c3 Vision
Implement vision annotation observations, inference and preferences through a dedicated runtime owner.
2026-10-14 20:02:39 -05:00

102 lines
3.9 KiB
Python

"""Strict default-off saved V-ASM observation settings; no legacy adoption."""
from dataclasses import dataclass
import hashlib
import json
import math
from typing import Any
from openpilot.starpilot.saved_source import read_saved
from openpilot.starpilot.spot_monitor.policy import Annotation, decode_annotation, encode_annotation
KEY = "VASMPreferences"
MAX_BYTES = 8192
@dataclass(frozen=True)
class Preferences:
enabled: bool = False
annotation: Annotation | None = None
confidence: float = 0.94
smooth_seconds: float = 0.2
@dataclass(frozen=True)
class SavedPreferences:
raw: bytes | None
readable: bool
valid: bool
preferences: Preferences = Preferences()
@property
def fingerprint(self) -> str:
return hashlib.sha256(self.raw or b"").hexdigest() if self.valid else ""
def _unique(pairs: list[tuple[str, object]]) -> dict[str, object]:
result: dict[str, object] = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate V-ASM preference")
result[key] = value
return result
def decode(raw: bytes) -> Preferences | None:
if type(raw) is not bytes or len(raw) > MAX_BYTES:
return None
try:
data = json.loads(raw, object_pairs_hook=_unique,
parse_constant=lambda _: (_ for _ in ()).throw(ValueError("nonfinite")))
if type(data) is not dict or set(data) != {"version", "enabled", "annotation", "confidence", "smoothSeconds"}:
return None
if type(data["version"]) is not int or data["version"] != 1 or type(data["enabled"]) is not bool:
return None
confidence, smooth = data["confidence"], data["smoothSeconds"]
if type(confidence) not in (int, float) or type(smooth) not in (int, float) or not math.isfinite(confidence) or \
not math.isfinite(smooth) or not 0.8 <= confidence <= 1.0 or not 0.01 <= smooth <= 0.5:
return None
annotation = data["annotation"]
if annotation is None:
return Preferences(False, None, float(confidence), float(smooth)) if not data["enabled"] else None
if type(annotation) is not dict:
return None
annotation_raw = json.dumps(annotation, separators=(",", ":"), allow_nan=False).encode()
return Preferences(data["enabled"], decode_annotation(annotation_raw), float(confidence), float(smooth))
except (UnicodeDecodeError, ValueError, TypeError, OverflowError, RecursionError):
return None
def encode(preferences: Preferences) -> bytes:
if type(preferences) is not Preferences or type(preferences.enabled) is not bool or \
(preferences.enabled and preferences.annotation is None) or type(preferences.confidence) not in (int, float) or \
type(preferences.smooth_seconds) not in (int, float) or not math.isfinite(preferences.confidence) or \
not math.isfinite(preferences.smooth_seconds) or not 0.8 <= preferences.confidence <= 1.0 or \
not 0.01 <= preferences.smooth_seconds <= 0.5:
raise ValueError("Invalid V-ASM preferences")
annotation = json.loads(encode_annotation(preferences.annotation)) if preferences.annotation is not None else None
raw = json.dumps({"version": 1, "enabled": preferences.enabled, "annotation": annotation,
"confidence": preferences.confidence, "smoothSeconds": preferences.smooth_seconds},
sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
if len(raw) > MAX_BYTES:
raise ValueError("Oversized V-ASM preferences")
return raw
def read_preferences(params: Any) -> SavedPreferences:
raw, readable = read_saved(params, KEY, MAX_BYTES)
if not readable:
return SavedPreferences(raw, False, False)
if raw is None:
return SavedPreferences(None, True, True)
preferences = decode(raw)
return SavedPreferences(raw, True, preferences is not None, preferences or Preferences())
def enabled(params: Any, *, development: bool) -> bool:
if not development:
return False
saved = read_preferences(params)
return saved.readable and saved.valid and saved.preferences.enabled