ui: port mici visual polish and Wi-Fi status updates

volt dbc

Don't go onroad til controls are allowed

Button Mici

Start of Day 3

./models

ui: keep mici toggle labels at a stable size

compile
This commit is contained in:
firestar5683
2026-03-23 01:55:54 -05:00
parent 77764bd345
commit d2819e5a04
66 changed files with 1132 additions and 196 deletions
+1
View File
@@ -15,6 +15,7 @@ a.out
.cache/
.comma_sysroot/
.venv-linux-arm64/
compiledmodels/
/docs_site/
+8 -1
View File
@@ -249,11 +249,18 @@ def get_frogpilot_toggles(sm=messaging.SubMaster(["frogpilotPlan"])):
return toggles
@cache
def _process_frogpilot_plan_toggles(toggles):
return SimpleNamespace(**json.loads(toggles))
def process_frogpilot_toggles(toggles):
if toggles:
return SimpleNamespace(**json.loads(toggles))
return _process_frogpilot_plan_toggles(toggles)
return FrogPilotVariables().frogpilot_toggles
process_frogpilot_toggles.cache_clear = _process_frogpilot_plan_toggles.cache_clear
def update_frogpilot_toggles():
if not hasattr(update_frogpilot_toggles, "_params_memory"):
update_frogpilot_toggles._params_memory = Params(memory=True)
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
import json
import os
import signal
import subprocess
import sys
import time
from collections import defaultdict, deque
from pathlib import Path
from openpilot.common.basedir import BASEDIR
from openpilot.common.swaglog import cloudlog
MAPD_DIR = Path(BASEDIR) / "frogpilot/navigation"
MAPD_BIN = MAPD_DIR / "mapd"
OFFLINE_ROOT = Path("/data/media/0/osm/offline")
RESTART_DELAY_S = 0.25
FAILURE_WINDOW_S = 3.0
FAILURE_THRESHOLD = 3
def extract_bounds_filename(line: str) -> str | None:
try:
payload = json.loads(line)
except json.JSONDecodeError:
return None
if payload.get("msg") != "Loading bounds file":
return None
filename = payload.get("filename")
return filename if isinstance(filename, str) else None
def is_offline_read_error(line: str) -> bool:
try:
payload = json.loads(line)
except json.JSONDecodeError:
return False
return payload.get("msg") == "could not unmarshal offline data"
class CorruptTileMonitor:
def __init__(self, threshold: int = FAILURE_THRESHOLD, window_s: float = FAILURE_WINDOW_S):
self.threshold = threshold
self.window_s = window_s
self.current_filename: str | None = None
self.failures: dict[str, deque[float]] = defaultdict(deque)
def observe(self, line: str, now: float | None = None) -> str | None:
filename = extract_bounds_filename(line)
if filename is not None:
self.current_filename = filename
return None
if not is_offline_read_error(line) or self.current_filename is None:
return None
ts = time.monotonic() if now is None else now
failures = self.failures[self.current_filename]
failures.append(ts)
cutoff = ts - self.window_s
while failures and failures[0] < cutoff:
failures.popleft()
if len(failures) >= self.threshold:
return self.current_filename
return None
def quarantine_offline_tile(filename: str) -> Path | None:
tile_path = Path(filename)
try:
tile_path.relative_to(OFFLINE_ROOT)
except ValueError:
cloudlog.warning(f"mapd_wrapper refusing to quarantine unexpected path: {filename}")
return None
if not tile_path.exists():
return None
quarantined = tile_path.with_name(f"{tile_path.name}.corrupt.{int(time.time())}")
tile_path.rename(quarantined)
return quarantined
def terminate_child(proc: subprocess.Popen[str]) -> None:
if proc.poll() is not None:
return
proc.terminate()
try:
proc.wait(timeout=2)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait(timeout=2)
def run_mapd_once() -> int:
proc = subprocess.Popen(
[MAPD_BIN.as_posix()],
cwd=MAPD_DIR,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
assert proc.stdout is not None
def _handle_signal(signum, _frame):
terminate_child(proc)
raise SystemExit(128 + signum)
signal.signal(signal.SIGTERM, _handle_signal)
signal.signal(signal.SIGINT, _handle_signal)
monitor = CorruptTileMonitor()
for line in proc.stdout:
print(line, end="")
bad_tile = monitor.observe(line)
if bad_tile is None:
continue
quarantined = quarantine_offline_tile(bad_tile)
if quarantined is None:
cloudlog.warning(f"mapd_wrapper detected repeated offline read failures for {bad_tile}, but could not quarantine it")
else:
message = f"mapd_wrapper quarantined corrupt offline tile: {bad_tile} -> {quarantined}"
print(message, flush=True)
cloudlog.warning(message)
terminate_child(proc)
return 1
return proc.wait()
def main() -> None:
while True:
exit_code = run_mapd_once()
if exit_code == 1:
time.sleep(RESTART_DELAY_S)
continue
raise SystemExit(exit_code)
if __name__ == "__main__":
main()
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
import json
from pathlib import Path
from openpilot.frogpilot.navigation.mapd_wrapper import CorruptTileMonitor, quarantine_offline_tile
def _loading_line(filename: str) -> str:
return json.dumps({"msg": "Loading bounds file", "filename": filename})
def _error_line() -> str:
return json.dumps({"msg": "could not unmarshal offline data", "error": "EOF"})
def test_corrupt_tile_monitor_triggers_after_repeated_failures():
filename = "/data/media/0/osm/offline/36/-98/37.500000_-98.000000_37.750000_-97.750000"
monitor = CorruptTileMonitor(threshold=3, window_s=3.0)
assert monitor.observe(_loading_line(filename), now=0.0) is None
assert monitor.observe(_error_line(), now=0.1) is None
assert monitor.observe(_loading_line(filename), now=0.2) is None
assert monitor.observe(_error_line(), now=0.3) is None
assert monitor.observe(_loading_line(filename), now=0.4) is None
assert monitor.observe(_error_line(), now=0.5) == filename
def test_quarantine_offline_tile_renames_file(tmp_path, monkeypatch):
offline_root = tmp_path / "offline"
tile = offline_root / "36/-98/37.500000_-98.000000_37.750000_-97.750000"
tile.parent.mkdir(parents=True)
tile.write_text("bad")
monkeypatch.setattr("openpilot.frogpilot.navigation.mapd_wrapper.OFFLINE_ROOT", offline_root)
quarantined = quarantine_offline_tile(tile.as_posix())
assert quarantined is not None
assert not tile.exists()
assert Path(quarantined).exists()
assert Path(quarantined).name.startswith(f"{tile.name}.corrupt.")
Executable
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -eo pipefail
DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
set +u
source "$DIR/launch_env.sh"
exec python3 "$DIR/scripts/model_compiler.py" "$@"
-1
View File
@@ -485,7 +485,6 @@ class CarInterface(CarInterfaceBase):
elif candidate == CAR.GMC_YUKON:
ret.steerActuatorDelay = 0.5
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
ret.dashcamOnly = True # Needs steerRatio, tireStiffness, and lat accel factor tuning
# OPGM variables
elif candidate in (CAR.CHEVROLET_MALIBU, CAR.CHEVROLET_MALIBU_CC, CAR.CHEVROLET_MALIBU_HYBRID_CC):
@@ -0,0 +1,375 @@
CM_ "AUTOGENERATED FILE, DO NOT EDIT";
CM_ "Imported file _comma.dbc starts here";
BO_ 512 GAS_COMMAND: 6 NEO
SG_ GAS_COMMAND : 7|16@0+ (0.2777778,-96.111115) [0|1] "" INTERCEPTOR
SG_ GAS_COMMAND2 : 23|16@0+ (0.120853074,-79.15877) [0|1] "" INTERCEPTOR
SG_ ENABLE : 39|1@0+ (1,0) [0|1] "" INTERCEPTOR
SG_ COUNTER_PEDAL : 35|4@0+ (1,0) [0|15] "" INTERCEPTOR
SG_ CHECKSUM_PEDAL : 47|8@0+ (1,0) [0|255] "" INTERCEPTOR
BO_ 513 GAS_SENSOR: 6 INTERCEPTOR
SG_ INTERCEPTOR_GAS : 7|16@0+ (0.2777778,-96.111115) [0|1] "" NEO
SG_ INTERCEPTOR_GAS2 : 23|16@0+ (0.120853074,-79.15877) [0|1] "" NEO
SG_ STATE : 39|4@0+ (1,0) [0|15] "" NEO
SG_ COUNTER_PEDAL : 35|4@0+ (1,0) [0|15] "" NEO
SG_ CHECKSUM_PEDAL : 47|8@0+ (1,0) [0|255] "" NEO
VAL_ 513 STATE 5 "FAULT_TIMEOUT" 4 "FAULT_STARTUP" 3 "FAULT_SCE" 2 "FAULT_SEND" 1 "FAULT_BAD_CHECKSUM" 0 "NO_FAULT" ;
CM_ "gm_global_a_powertrain.dbc starts here";
VERSION ""
NS_ :
NS_DESC_
CM_
BA_DEF_
BA_
VAL_
CAT_DEF_
CAT_
FILTER
BA_DEF_DEF_
EV_DATA_
ENVVAR_DATA_
SGTYPE_
SGTYPE_VAL_
BA_DEF_SGTYPE_
BA_SGTYPE_
SIG_TYPE_REF_
VAL_TABLE_
SIG_GROUP_
SIG_VALTYPE_
SIGTYPE_VALTYPE_
BO_TX_BU_
BA_DEF_REL_
BA_REL_
BA_DEF_DEF_REL_
BU_SG_REL_
BU_EV_REL_
BU_BO_REL_
SG_MUL_VAL_
BS_:
BU_: K16_BECM K73_TCIC K9_BCM K43_PSCM K17_EBCM K20_ECM K114B_HPCM NEO K124_ASCM EPB
VAL_TABLE_ TurnSignals 2 "Right Turn" 1 "Left Turn" 0 "None" ;
VAL_TABLE_ Intellibeam 1 "Active" 0 "Inactive" ;
VAL_TABLE_ HighBeamsActive 1 "Active" 0 "Inactive" ;
VAL_TABLE_ HighBeamsTemporary 1 "Active" 0 "Inactive" ;
VAL_TABLE_ ACCLeadCar 1 "Present" 0 "Not Present" ;
VAL_TABLE_ ACCCmdActive 1 "Active" 0 "Inactive" ;
VAL_TABLE_ BrakePedalPressed 1 "Pressed" 0 "Depressed" ;
VAL_TABLE_ DistanceButton 1 "Active" 0 "Inactive" ;
VAL_TABLE_ LKAButton 1 "Active" 0 "Inactive" ;
VAL_TABLE_ ACCButtons 6 "Cancel" 5 "Main" 3 "Set" 2 "Resume" 1 "None" ;
VAL_TABLE_ DriveModeButton 1 "Active" 0 "Inactive" ;
VAL_TABLE_ PRNDL 3 "Reverse" 2 "Drive" 1 "Neutral" 0 "Park" ;
VAL_TABLE_ ESPButton 1 "Active" 0 "Inactive" ;
VAL_TABLE_ DoorStatus 1 "Opened" 0 "Closed" ;
VAL_TABLE_ SeatBeltStatus 1 "Latched" 0 "Unlatched" ;
VAL_TABLE_ LKASteeringCmdActive 1 "Active" 0 "Inactive" ;
VAL_TABLE_ ACCGapLevel 3 "Far" 2 "Med" 1 "Near" 0 "Inactive" ;
VAL_TABLE_ GasRegenCmdActiveInv 1 "Inactive" 0 "Active" ;
VAL_TABLE_ GasRegenCmdActive 1 "Active" 0 "Inactive" ;
VAL_TABLE_ LKATorqueDeliveredStatus 3 "Failed" 2 "Temp. Limited" 1 "Active" 0 "Inactive" ;
VAL_TABLE_ HandsOffSWDetectionStatus 1 "Hands On" 0 "Hands Off" ;
VAL_TABLE_ HandsOffSWDetectionMode 2 "Failed" 1 "Enabled" 0 "Disabled" ;
BO_ 189 EBCMRegenPaddle: 7 K17_EBCM
SG_ RegenPaddle : 7|4@0+ (1,0) [0|0] "" NEO
BO_ 190 ECMAcceleratorPos: 6 K20_ECM
SG_ BrakePedalPos : 15|8@0+ (1,0) [0|0] "sticky" NEO
SG_ GasPedalAndAcc : 23|8@0+ (1,0) [0|0] "" NEO
BO_ 201 ECMEngineStatus: 8 K20_ECM
SG_ EngineTPS : 39|8@0+ (0.392156863,0) [0|100.000000065] "%" NEO
SG_ EngineRPM : 15|16@0+ (0.25,0) [0|0] "RPM" NEO
SG_ CruiseMainOn : 29|1@0+ (1,0) [0|1] "" NEO
SG_ BrakePressed : 40|1@0+ (1,0) [0|1] "" NEO
SG_ Standstill : 2|1@0+ (1,0) [0|1] "" NEO
SG_ CruiseActive : 31|2@0+ (1,0) [0|3] "" NEO
BO_ 209 EBCMBrakePedalSensors: 7 K17_EBCM
SG_ Counter1 : 7|2@0+ (1,0) [0|3] "" XXX
SG_ Counter2 : 23|2@0+ (1,0) [0|3] "" XXX
SG_ BrakePedalPosition1 : 5|14@0+ (1,0) [0|16383] "" XXX
SG_ BrakePedalPosition2 : 21|14@0- (-1,0) [0|16383] "" XXX
SG_ BrakeNormalized1 : 39|8@0+ (1,0) [0|255] "" XXX
SG_ BrakeNormalized2 : 47|8@0- (-1,0) [0|255] "" XXX
BO_ 241 EBCMBrakePedalPosition: 6 K17_EBCM
SG_ BrakePressed : 1|1@0+ (1,0) [0|1] "" XXX
SG_ BrakePedalPosition : 15|8@0+ (1,0) [0|255] "" NEO
BO_ 298 BCMDoorBeltStatus: 8 K9_BCM
SG_ RearLeftDoor : 8|1@0+ (1,0) [0|0] "" NEO
SG_ FrontLeftDoor : 9|1@0+ (1,0) [0|0] "" NEO
SG_ FrontRightDoor : 10|1@0+ (1,0) [0|0] "" NEO
SG_ RearRightDoor : 23|1@0+ (1,0) [0|0] "" NEO
SG_ LeftSeatBelt : 12|1@0+ (1,0) [0|0] "" NEO
SG_ RightSeatBelt : 53|1@0+ (1,0) [0|0] "" NEO
BO_ 309 ECMPRDNL: 8 K20_ECM
SG_ PRNDL : 2|3@0+ (1,0) [0|0] "" NEO
SG_ ESPButton : 4|1@0+ (1,0) [0|1] "" XXX
BO_ 320 BCMTurnSignals: 3 K9_BCM
SG_ TurnSignals : 19|2@0+ (1,0) [0|0] "" NEO
SG_ Intellibeam : 13|1@0+ (1,0) [0|1] "" XXX
SG_ HighBeamsActive : 7|1@0+ (1,0) [0|1] "" XXX
SG_ HighBeamsTemporary : 5|1@0+ (1,0) [0|1] "" XXX
BO_ 322 BCMBlindSpotMonitor: 7 K9_BCM
SG_ LeftBSM : 6|1@0+ (1,0) [0|1] "" XXX
SG_ RightBSM : 7|1@0+ (1,0) [0|1] "" XXX
BO_ 328 PSCM_148: 1 K43_PSCM
BO_ 381 ESPStatus: 6 K20_ECM
SG_ TractionControlOn : 5|1@0+ (1,0) [0|0] "" NEO
SG_ MSG17D_AccPower : 35|12@0- (1,0) [0|0] "" NEO
BO_ 384 ASCMLKASteeringCmd: 4 NEO
SG_ RollingCounter : 5|2@0+ (1,0) [0|0] "" NEO
SG_ LKASteeringCmdChecksum : 19|12@0+ (1,0) [0|0] "" NEO
SG_ LKASteeringCmdActive : 3|1@0+ (1,0) [0|0] "" NEO
SG_ LKASteeringCmd : 2|11@0- (1,0) [0|0] "" NEO
BO_ 388 PSCMStatus: 8 K43_PSCM
SG_ HandsOffSWDetectionMode : 20|2@0+ (1,0) [0|3] "" NEO
SG_ HandsOffSWlDetectionStatus : 21|1@0+ (1,0) [0|1] "" NEO
SG_ LKATorqueDeliveredStatus : 5|3@0+ (1,0) [0|7] "" NEO
SG_ LKADriverAppldTrq : 50|11@0- (0.01,0) [-10.24|10.23] "Nm" NEO
SG_ LKATorqueDelivered : 18|11@0- (0.01,0) [0|1] "" NEO
SG_ LKATotalTorqueDelivered : 2|11@0- (0.01,0) [-10.24|10.23] "Nm" NEO
SG_ RollingCounter : 38|4@0+ (1,0) [0|15] "" XXX
SG_ PSCMStatusChecksum : 33|10@0+ (1,0) [0|1023] "" XXX
BO_ 417 AcceleratorPedal: 7 XXX
SG_ AcceleratorPedal : 55|8@0+ (1,0) [0|0] "" NEO
BO_ 451 GasAndAcc: 8 XXX
SG_ GasPedalAndAcc2 : 55|8@0+ (1,0) [0|0] "" NEO
BO_ 452 AcceleratorPedal2: 8 XXX
SG_ CruiseState : 15|3@0+ (1,0) [0|7] "" NEO
SG_ AcceleratorPedal2 : 47|8@0+ (1,0) [0|0] "" NEO
BO_ 481 ASCMSteeringButton: 7 K124_ASCM
SG_ DistanceButton : 22|1@0+ (1,0) [0|0] "" NEO
SG_ LKAButton : 23|1@0+ (1,0) [0|0] "" NEO
SG_ ACCAlwaysOne : 24|1@0+ (1,0) [0|1] "" XXX
SG_ ACCButtons : 46|3@0+ (1,0) [0|0] "" NEO
SG_ DriveModeButton : 39|1@0+ (1,0) [0|1] "" XXX
SG_ RollingCounter : 33|2@0+ (1,0) [0|3] "" NEO
SG_ SteeringButtonChecksum : 43|12@0+ (1,0) [0|255] "" NEO
BO_ 485 PSCMSteeringAngle: 8 K43_PSCM
SG_ SteeringWheelAngle : 15|16@0- (0.0625,0) [-2047|2047] "deg" NEO
SG_ SteeringWheelRate : 27|12@0- (1,0) [-2047|2047] "deg/s" NEO
BO_ 489 EBCMVehicleDynamic: 8 K17_EBCM
SG_ BrakePedalPressed : 6|1@0+ (1,0) [0|0] "" NEO
SG_ LateralAcceleration : 3|10@0- (0.161,0) [-2047|2047] "m/s2" NEO
SG_ YawRate : 35|12@0- (0.625,0) [0|1] "" NEO
SG_ YawRate2 : 51|12@0- (0.0625,0) [-2047|2047] "grad/s" NEO
BO_ 352 BCMImmobilizer: 5 K9_BCM
SG_ ImmobilizerInfo : 7|32@0+ (1,0) [0|4294967295] "" XXX
BO_ 497 BCMGeneralPlatformStatus: 8 K9_BCM
SG_ SystemPowerMode : 1|2@0+ (1,0) [0|3] "" XXX
SG_ SystemBackUpPowerMode : 5|2@0+ (1,0) [0|3] "" XXX
SG_ ParkBrakeSwActive : 36|1@0+ (1,0) [0|3] "" XXX
BO_ 500 SportMode: 6 XXX
SG_ SportMode : 15|1@0+ (1,0) [0|1] "" XXX
BO_ 501 ECMPRDNL2: 8 K20_ECM
SG_ TransmissionState : 48|4@1+ (1,0) [0|7] "" NEO
SG_ PRNDL2 : 27|4@0+ (1,0) [0|255] "" NEO
SG_ ManualMode : 41|1@0+ (1,0) [0|1] "" NEO
BO_ 532 BRAKE_RELATED: 6 XXX
SG_ UserBrakePressure : 0|9@0+ (1,0) [0|511] "" XXX
BO_ 560 EPBStatus: 8 EPB
SG_ EPBClosed : 12|1@0+ (1,0) [0|1] "" NEO
BO_ 562 EBCMFrictionBrakeStatus: 8 K17_EBCM
SG_ FrictionBrakeUnavailable : 46|1@0+ (1,0) [0|1] "" XXX
BO_ 608 SPEED_RELATED: 8 XXX
SG_ RollingCounter : 5|2@0+ (1,0) [0|0] "" XXX
SG_ ClusterSpeed : 31|8@0+ (1,0) [0|0] "" XXX
BO_ 711 BECMBatteryVoltageCurrent: 6 K17_EBCM
SG_ HVBatteryVoltage : 31|12@0+ (0.125,0) [0|511.875] "V" NEO
SG_ HVBatteryCurrent : 12|13@0- (0.15,0) [-614.4|614.25] "A" NEO
BO_ 715 ASCMGasRegenCmd: 8 K124_ASCM
SG_ GasRegenAlwaysOne2 : 9|1@0+ (1,0) [0|1] "" NEO
SG_ GasRegenAlwaysOne : 14|1@0+ (1,0) [0|1] "" NEO
SG_ GasRegenChecksum : 47|24@0+ (1,0) [0|0] "" NEO
SG_ GasRegenCmdActiveInv : 32|1@0+ (1,0) [0|0] "" NEO
SG_ GasRegenFullStopActive : 13|1@0+ (1,0) [0|0] "" NEO
SG_ GasRegenCmdActive : 0|1@0+ (1,0) [0|0] "" NEO
SG_ RollingCounter : 7|2@0+ (1,0) [0|0] "" NEO
SG_ GasRegenAlwaysOne3 : 23|1@0+ (1,0) [0|1] "" NEO
SG_ GasRegenCmd : 22|12@0+ (1,0) [0|0] "" NEO
BO_ 717 ASCM_2CD: 5 K124_ASCM
BO_ 761 BRAKE_RELATED_2: 7 XXX
SG_ UserBrakePressure2 : 47|9@0+ (1,0) [0|511] "" XXX
BO_ 789 EBCMFrictionBrakeCmd: 5 K124_ASCM
SG_ RollingCounter : 33|2@0+ (1,0) [0|0] "" NEO
SG_ FrictionBrakeMode : 7|4@0+ (1,0) [0|0] "" NEO
SG_ FrictionBrakeChecksum : 23|16@0+ (1,0) [0|0] "" NEO
SG_ FrictionBrakeCmd : 3|12@0- (1,0) [0|0] "" NEO
BO_ 800 AEBCmd: 6 K124_ASCM
SG_ RollingCounter : 5|2@0+ (1,0) [0|3] "" NEO
SG_ AEBChecksum : 27|20@0+ (1,0) [0|0] "" NEO
SG_ AEBCmdActive : 3|1@1+ (1,0) [0|1] "" NEO
SG_ AEBCmd : 2|11@0+ (1,0) [0|0] "" NEO
SG_ AEBCmd2 : 23|8@0+ (1,0) [0|0] "" NEO
BO_ 810 TCICOnStarGPSPosition: 8 K73_TCIC
SG_ GPSLongitude : 39|32@0+ (1,-2147483648) [0|0] "milliarcsecond" NEO
SG_ GPSLatitude : 7|32@0+ (1,0) [0|0] "milliarcsecond" NEO
BO_ 840 EBCMWheelSpdFront: 5 K17_EBCM
SG_ FLWheelSpd : 7|16@0+ (0.0311,0) [0|255] "km/h" NEO
SG_ FRWheelSpd : 23|16@0+ (0.0311,0) [0|255] "km/h" NEO
BO_ 842 EBCMWheelSpdRear: 5 K17_EBCM
SG_ RLWheelSpd : 7|16@0+ (0.0311,0) [0|255] "km/h" NEO
SG_ RRWheelSpd : 23|16@0+ (0.0311,0) [0|255] "km/h" NEO
SG_ RRWheelDir : 34|3@0+ (1,0) [0|7] "" NEO
SG_ RLWheelDir : 37|3@0+ (1,0) [0|7] "" NEO
BO_ 869 ASCM_365: 4 K124_ASCM
BO_ 880 ASCMActiveCruiseControlStatus: 6 K124_ASCM
SG_ ACCCruiseState : 8|3@1+ (1,0) [0|7] "" XXX
SG_ ACCLeadCar : 44|1@0+ (1,0) [0|0] "" Vector__XXX
SG_ ACCAlwaysOne2 : 32|1@0+ (1,0) [0|0] "" Vector__XXX
SG_ ACCAlwaysOne : 0|1@0+ (1,0) [0|0] "" Vector__XXX
SG_ ACCSpeedSetpoint : 19|12@0+ (0.0625,0) [0|255.9375] "km/h" NEO
SG_ ACCGapLevel : 21|2@0+ (1,0) [0|0] "" NEO
SG_ ACCResumeButton : 1|1@0+ (1,0) [0|0] "" NEO
SG_ ACCCmdActive : 23|1@0+ (1,0) [0|0] "" NEO
SG_ FCWAlert : 41|2@0+ (1,0) [0|3] "" XXX
BO_ 967 EVDriveMode: 4 XXX
SG_ SinglePedalModeActive : 7|1@0+ (1,0) [0|1] "" XXX
SG_ SinglePedalModeRisingEdge : 21|1@0+ (1,0) [0|1] "" XXX
SG_ SinglePedalModeFallingEdge : 22|1@0+ (1,0) [0|1] "" XXX
BO_ 977 ECMCruiseControl: 8 K20_ECM
SG_ CruiseActive : 39|1@0+ (1,0) [0|3] "" NEO
SG_ CruiseSetSpeed : 19|12@0+ (0.0625,0) [0|0] "km/h" NEO
BO_ 1001 ECMVehicleSpeed: 8 K20_ECM
SG_ VehicleSpeed : 7|16@0+ (0.01,0) [0|0] "mph" NEO
SG_ VehicleSpeedLeft : 39|16@0+ (0.01,0) [0|0] "mph" NEO
BO_ 1033 ASCMKeepAlive: 7 NEO
SG_ ASCMKeepAliveAllZero : 7|56@0+ (1,0) [0|0] "" NEO
BO_ 1034 ASCM_40A: 7 K124_ASCM
BO_ 1217 ECMEngineCoolantTemp: 8 K20_ECM
SG_ EngineCoolantTemp : 23|8@0+ (1,-40) [0|0] "C" NEO
BO_ 1249 VIN_Part2: 8 K20_ECM
SG_ VINPart2 : 7|64@0+ (1,0) [0|0] "" NEO
BO_ 1296 ASCM_510: 4 K124_ASCM
BO_ 1300 VIN_Part1: 8 K20_ECM
SG_ VINPart1 : 7|64@0+ (1,0) [0|0] "" NEO
BO_ 1912 PSCM_778: 8 K43_PSCM
BO_ 1930 ASCM_78A: 7 K124_ASCM
BO_TX_BU_ 384 : K124_ASCM,NEO;
BO_TX_BU_ 880 : NEO,K124_ASCM;
BO_TX_BU_ 1033 : K124_ASCM,NEO;
BO_TX_BU_ 715 : NEO,K124_ASCM;
BO_TX_BU_ 789 : NEO,K124_ASCM;
BO_TX_BU_ 800 : NEO,K124_ASCM;
CM_ BU_ K16_BECM "Battery Energy Control Module";
CM_ BU_ K73_TCIC "Telematics Communication Control Module";
CM_ BU_ K9_BCM "Body Control Module";
CM_ BU_ K43_PSCM "Power Steering Control Module";
CM_ BU_ K17_EBCM "Electronic Brake Control Module";
CM_ BU_ K20_ECM "Engine Control Module";
CM_ BU_ K114B_HPCM "Hybrid Powertrain Control Module";
CM_ BU_ NEO "Comma NEO";
CM_ BU_ K124_ASCM "Active Safety Control Module";
CM_ SG_ 381 MSG17D_AccPower "Need to investigate";
CM_ BO_ 190 "Length varies from 6 to 8 bytes by car";
CM_ SG_ 190 GasPedalAndAcc "ACC baseline is 62";
CM_ SG_ 322 LeftBSM "For some cars, this can only be when the blinker is also active";
CM_ SG_ 322 RightBSM "For some cars, this can only be when the blinker is also active";
CM_ SG_ 352 ImmobilizerInfo "Non-zero when ignition or accessory mode";
CM_ SG_ 451 GasPedalAndAcc2 "ACC baseline is 62";
CM_ SG_ 481 ACCAlwaysOne "Usually 1 if the car is equipped with ACC";
CM_ SG_ 562 FrictionBrakeUnavailable "1 when ACC brake control is unavailable. Stays high if brake command messages are blocked for a period of time";
CM_ SG_ 497 SystemPowerMode "Describes ignition";
CM_ SG_ 497 SystemBackUpPowerMode "Describes ignition + preconditioning mode, noisy";
CM_ SG_ 501 PRNDL2 "When ManualMode is Active, Value is 13=L1 12=L2 11=L3 ... 4=L10";
CM_ SG_ 532 UserBrakePressure "can be lower than other brake position signals when the brakes are pre-filled from ACC braking and the user presses on the brakes. user-only pressure?";
CM_ SG_ 608 ClusterSpeed "Cluster speed signal seems to match dash on newer cars, but is a lower rate and can be noisier.";
CM_ SG_ 761 UserBrakePressure2 "Similar to BRAKE_RELATED->UserBrakePressure";
CM_ SG_ 1001 VehicleSpeed "Spinouts show here on 2wd. Speed derived from right front wheel (drive tire)";
BA_DEF_ "UseGMParameterIDs" INT 0 0;
BA_DEF_ "ProtocolType" STRING ;
BA_DEF_ "BusType" STRING ;
BA_DEF_DEF_ "UseGMParameterIDs" 1;
BA_DEF_DEF_ "ProtocolType" "GMLAN";
BA_DEF_DEF_ "BusType" "";
BA_ "BusType" "CAN";
BA_ "ProtocolType" "GMLAN";
BA_ "UseGMParameterIDs" 0;
VAL_ 497 SystemPowerMode 3 "Crank Request" 2 "Run" 1 "Accessory" 0 "Off";
VAL_ 497 SystemBackUpPowerMode 3 "Crank Request" 2 "Run" 1 "Accessory" 0 "Off";
VAL_ 481 DistanceButton 1 "Active" 0 "Inactive" ;
VAL_ 481 LKAButton 1 "Active" 0 "Inactive" ;
VAL_ 481 ACCButtons 6 "Cancel" 5 "Main" 3 "Set" 2 "Resume" 1 "None" ;
VAL_ 481 DriveModeButton 1 "Active" 0 "Inactive" ;
VAL_ 452 CruiseState 4 "Standstill" 3 "Faulted" 1 "Active" 0 "Off" ;
VAL_ 309 PRNDL 3 "R" 2 "D" 1 "N" 0 "P" ;
VAL_ 309 ESPButton 1 "Active" 0 "Inactive" ;
VAL_ 384 LKASteeringCmdActive 1 "Active" 0 "Inactive" ;
VAL_ 842 RRWheelDir 0 "Stationary" 1 "Forward" 2 "Reverse" 3 "Unsupported" 4 "Fault";
VAL_ 842 RLWheelDir 0 "Stationary" 1 "Forward" 2 "Reverse" 3 "Unsupported" 4 "Fault";
VAL_ 880 ACCCruiseState 2 "Adaptive" 3 "Adaptive" 4 "Non-adaptive" 5 "Non-adaptive" ;
VAL_ 880 ACCLeadCar 1 "Present" 0 "Not Present" ;
VAL_ 880 ACCGapLevel 3 "Far" 2 "Med" 1 "Near" 0 "Inactive" ;
VAL_ 880 ACCResumeButton 1 "Pressed" 0 "Depressed" ;
VAL_ 880 ACCCmdActive 1 "Active" 0 "Inactive" ;
VAL_ 388 HandsOffSWDetectionMode 2 "Failed" 1 "Enabled" 0 "Disabled" ;
VAL_ 388 HandsOffSWlDetectionStatus 1 "Hands On" 0 "Hands Off" ;
VAL_ 388 LKATorqueDeliveredStatus 3 "Failed" 2 "Temp. Limited" 1 "Active" 0 "Inactive" ;
VAL_ 489 BrakePedalPressed 1 "Pressed" 0 "Depressed" ;
VAL_ 715 GasRegenCmdActiveInv 1 "Inactive" 0 "Active" ;
VAL_ 715 GasRegenCmdActive 1 "Active" 0 "Inactive" ;
VAL_ 320 Intellibeam 1 "Active" 0 "Inactive" ;
VAL_ 320 HighBeamsActive 1 "Active" 0 "Inactive" ;
VAL_ 320 HighBeamsTemporary 1 "Active" 0 "Inactive" ;
VAL_ 501 PRNDL2 6 "L" 4 "D" 3 "N" 2 "R" 1 "P" 0 "Shifting";
VAL_ 501 TransmissionState 11 "Shifting" 10 "Reverse" 9 "Forward" 8 "Disengaged";
VAL_ 501 ManualMode 1 "Active" 0 "Inactive"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,2 +1,2 @@
extern const uint8_t gitversion[19];
const uint8_t gitversion[19] = "DEV-60a7fb11-DEBUG";
const uint8_t gitversion[19] = "DEV-d17d41fb-DEBUG";
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
DEV-60a7fb11-DEBUG
DEV-d17d41fb-DEBUG
+233
View File
@@ -0,0 +1,233 @@
#!/usr/bin/env python3
import argparse
import codecs
import os
import pickle
import re
import shutil
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_INPUT_ROOT = Path("/data/openpilot/uncompiledmodels")
DEFAULT_OUTPUT_ROOT = Path("/data/openpilot/compiledmodels")
COMPILE_SCRIPT = REPO_ROOT / "tinygrad_repo/examples/openpilot/compile3.py"
COMPONENT_ALIASES = {
"driving_off_policy": ("driving_off_policy", "off_policy", "offpolicy"),
"driving_policy": ("driving_policy", "policy"),
"driving_vision": ("driving_vision", "vision"),
}
REQUIRED_COMPONENTS = {"driving_policy", "driving_vision"}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Compile staged ONNX driving models into tinygrad pkls without touching selfdrive/modeld/models.",
)
parser.add_argument("--model", help="Output model key, for example sc2.")
parser.add_argument("--input-dir", type=Path, default=DEFAULT_INPUT_ROOT, help="Directory containing staged ONNX files. Flat root files like driving_policy.onnx are preferred.")
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_ROOT, help="Directory for compiled tinygrad pkls and metadata.")
parser.add_argument("--list", action="store_true", help="List detected staged models and exit.")
parser.add_argument("--force", action="store_true", help="Legacy no-op. Compiled outputs are always cleared before a build.")
args, unknown = parser.parse_known_args()
dynamic_model_flags = [arg[2:] for arg in unknown if arg.startswith("--")]
invalid = [arg for arg in unknown if not arg.startswith("--")]
if invalid:
parser.error(f"Unexpected arguments: {' '.join(invalid)}")
if len(dynamic_model_flags) > 1:
parser.error("Pass only one dynamic model flag, for example ./models --sc2")
if args.model and dynamic_model_flags and args.model != dynamic_model_flags[0]:
parser.error("Use either --model sc2 or --sc2, not both with different values.")
args.model = args.model or (dynamic_model_flags[0] if dynamic_model_flags else None)
return args
def detect_component(path: Path) -> str | None:
stem = path.stem.lower()
for component, aliases in COMPONENT_ALIASES.items():
if any(alias in stem for alias in aliases):
return component
return None
def find_staged_models(input_root: Path) -> dict[str, dict[str, Path]]:
found: dict[str, dict[str, Path]] = {}
if not input_root.is_dir():
return found
for child in sorted(input_root.iterdir()):
if not child.is_dir():
continue
model_files = {}
for onnx_file in sorted(child.glob("*.onnx")):
component = detect_component(onnx_file)
if component:
model_files[component] = onnx_file
if model_files:
found[child.name] = model_files
flat_root_files = {}
for onnx_file in sorted(input_root.glob("*.onnx")):
component = detect_component(onnx_file)
if component is None:
continue
model_key = None
lowered = onnx_file.stem.lower()
for alias in COMPONENT_ALIASES[component]:
if lowered == alias:
model_key = None
break
suffix = f"_{alias}"
if lowered.endswith(suffix):
model_key = onnx_file.stem[:-len(suffix)]
break
if model_key in ("", "driving"):
model_key = None
if model_key:
found.setdefault(model_key, {})[component] = onnx_file
else:
flat_root_files[component] = onnx_file
if flat_root_files:
found["_root"] = flat_root_files
return found
def resolve_model_files(input_root: Path, model_key: str) -> dict[str, Path]:
staged = find_staged_models(input_root)
if model_key in staged:
return staged[model_key]
root_files = staged.get("_root")
if root_files and len(staged) == 1:
return root_files
prefixed_files = {}
for onnx_file in sorted(input_root.glob(f"{model_key}_*.onnx")):
component = detect_component(onnx_file)
if component:
prefixed_files[component] = onnx_file
return prefixed_files
def get_metadata_value_by_name(model, name: str):
for prop in model.metadata_props:
if prop.key == name:
return prop.value
return None
def write_metadata(onnx_path: Path, output_path: Path) -> None:
import onnx
model = onnx.load(str(onnx_path))
output_slices = get_metadata_value_by_name(model, "output_slices")
if output_slices is None:
raise ValueError(f"output_slices not found in metadata for {onnx_path.name}")
def get_name_and_shape(value_info) -> tuple[str, tuple[int, ...]]:
shape = tuple(int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim)
return value_info.name, shape
metadata = {
"model_checkpoint": get_metadata_value_by_name(model, "model_checkpoint"),
"output_slices": pickle.loads(codecs.decode(output_slices.encode(), "base64")),
"input_shapes": dict(get_name_and_shape(x) for x in model.graph.input),
"output_shapes": dict(get_name_and_shape(x) for x in model.graph.output),
}
with open(output_path, "wb") as f:
pickle.dump(metadata, f)
def compile_component(onnx_path: Path, output_path: Path) -> None:
env = os.environ.copy()
existing_pythonpath = env.get("PYTHONPATH", "")
env["PYTHONPATH"] = f"{REPO_ROOT}:{existing_pythonpath}" if existing_pythonpath else str(REPO_ROOT)
subprocess.run(
[sys.executable, str(COMPILE_SCRIPT), str(onnx_path), str(output_path)],
cwd=REPO_ROOT,
env=env,
check=True,
)
def clear_existing_outputs(output_dir: Path) -> list[Path]:
removed = []
for existing in sorted(output_dir.iterdir()):
if existing.is_file() or existing.is_symlink():
existing.unlink()
elif existing.is_dir():
shutil.rmtree(existing)
removed.append(existing)
return removed
def list_models(staged: dict[str, dict[str, Path]], input_root: Path) -> int:
if not staged:
print(f"No staged models found in {input_root}")
return 0
for model_key, files in sorted(staged.items()):
print(model_key)
for component, path in sorted(files.items()):
print(f" {component}: {path}")
return 0
def main() -> int:
args = parse_args()
staged = find_staged_models(args.input_dir)
if args.list:
return list_models(staged, args.input_dir)
if not args.model:
available = ", ".join(sorted(k for k in staged if k != "_root"))
raise SystemExit(f"Choose a model key, for example ./models --sc2. Available staged models: {available or 'none'}")
model_key = args.model.strip()
files = resolve_model_files(args.input_dir, model_key)
if not files:
raise SystemExit(
f"No staged ONNX files found for {model_key} in {args.input_dir}. "
f"Use {args.input_dir}/driving_policy.onnx and {args.input_dir}/driving_vision.onnx, "
f"or optionally {args.input_dir / model_key}/*.onnx"
)
missing = sorted(REQUIRED_COMPONENTS - set(files))
if missing:
raise SystemExit(f"Missing required ONNX files for {model_key}: {', '.join(missing)}")
args.output_dir.mkdir(parents=True, exist_ok=True)
print(f"Compiling {model_key} from {args.input_dir} -> {args.output_dir}")
removed = clear_existing_outputs(args.output_dir)
if removed:
print(f" cleared {len(removed)} existing output entries")
for component, onnx_path in sorted(files.items()):
output_pkl = args.output_dir / f"{model_key}_{component}_tinygrad.pkl"
output_metadata = args.output_dir / f"{model_key}_{component}_metadata.pkl"
print(f" compiling {component}: {onnx_path.name}")
compile_component(onnx_path, output_pkl)
write_metadata(onnx_path, output_metadata)
print(f" saved {output_pkl.name}")
print(f" saved {output_metadata.name}")
print("Done.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

+4 -56
View File
@@ -25,7 +25,6 @@ MODEL_PKL_PATH = Path(__file__).parent / 'models/dmonitoring_model_tinygrad.pkl'
METADATA_PATH = Path(__file__).parent / 'models/dmonitoring_model_metadata.pkl'
MODELS_DIR = Path(__file__).parent / 'models'
class ModelState:
inputs: dict[str, np.ndarray]
output: np.ndarray
@@ -46,42 +45,8 @@ class ModelState:
self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
self._blob_cache : dict[int, Tensor] = {}
self.image_warp = None
self._warp_rebuild_attempted: set[tuple[int, int]] = set()
self._warp_backend_rebuild_attempted: set[tuple[int, int]] = set()
self.model_run = pickle.loads(read_file_chunked(str(MODEL_PKL_PATH)))
def _load_or_rebuild_dm_warp(self, width: int, height: int):
warp_path = MODELS_DIR / f'dm_warp_{width}x{height}_tinygrad.pkl'
resolution_key = (width, height)
def load_warp():
with open(warp_path, "rb") as f:
return pickle.load(f)
try:
return load_warp()
except Exception as error:
if resolution_key in self._warp_rebuild_attempted:
raise
self._warp_rebuild_attempted.add(resolution_key)
cloudlog.exception(f"Failed to load DM warp artifact {warp_path}: {error}")
cloudlog.warning(f"Rebuilding DM warp artifact for {width}x{height}")
try:
warp_path.unlink(missing_ok=True)
except Exception:
pass
from openpilot.selfdrive.modeld.compile_warp import compile_dm_warp
compile_dm_warp(width, height)
try:
return load_warp()
except Exception as retry_error:
cloudlog.exception(f"Reload failed after rebuilding {warp_path}: {retry_error}")
raise
def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]:
self.numpy_inputs['calib'][0,:] = calib
@@ -89,33 +54,16 @@ class ModelState:
if self.image_warp is None:
self.frame_buf_params = get_nv12_info(buf.width, buf.height)
self.image_warp = self._load_or_rebuild_dm_warp(buf.width, buf.height)
warp_path = MODELS_DIR / f'dm_warp_{buf.width}x{buf.height}_tinygrad.pkl'
with open(warp_path, "rb") as f:
self.image_warp = pickle.load(f)
ptr = buf.data.ctypes.data
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
if ptr not in self._blob_cache:
self._blob_cache[ptr] = Tensor.from_blob(ptr, (self.frame_buf_params[3],), dtype='uint8')
self.warp_inputs_np['transform'][:] = transform[:]
resolution_key = (buf.width, buf.height)
try:
self.tensor_inputs['input_img'] = self.image_warp(self._blob_cache[ptr], self.warp_inputs['transform']).realize()
except AssertionError as error:
# Handle runtime backend mismatch (e.g. CPU-captured warp artifact on QCOM device).
if "args mismatch in JIT" not in str(error) or resolution_key in self._warp_backend_rebuild_attempted:
raise
self._warp_backend_rebuild_attempted.add(resolution_key)
cloudlog.warning(f"DM warp JIT backend mismatch for {buf.width}x{buf.height}; rebuilding artifact for active backend")
warp_path = MODELS_DIR / f'dm_warp_{buf.width}x{buf.height}_tinygrad.pkl'
try:
warp_path.unlink(missing_ok=True)
except Exception:
pass
from openpilot.selfdrive.modeld.compile_warp import compile_dm_warp
compile_dm_warp(buf.width, buf.height)
self.image_warp = self._load_or_rebuild_dm_warp(buf.width, buf.height)
self.tensor_inputs['input_img'] = self.image_warp(self._blob_cache[ptr], self.warp_inputs['transform']).realize()
self.tensor_inputs['input_img'] = self.image_warp(self._blob_cache[ptr], self.warp_inputs['transform']).realize()
output = self.model_run(**self.tensor_inputs).contiguous().realize().uop.base.buffer.numpy().flatten()
Binary file not shown.
+4 -9
View File
@@ -48,16 +48,11 @@ def dmonitoringd_thread():
DM.always_on = params.get_bool("AlwaysOnDM")
demo_mode = params.get_bool("IsDriverViewEnabled") and sm["carState"].gearShifter != GearShifter.reverse
# save rhd virtual toggle every 5 mins, but only with clear confidence.
# save rhd virtual toggle every 5 mins
if (sm['driverStateV2'].frameId % 6000 == 0 and not demo_mode and
DM.wheelpos.prob_offseter.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT):
wheelpos_mean = DM.wheelpos.prob_offseter.filtered_stat.M
save_rhd = DM.settings._WHEELPOS_THRESHOLD_ENTER_RHD + DM.settings._WHEELPOS_SAVE_MARGIN
save_lhd = DM.settings._WHEELPOS_THRESHOLD_ENTER_LHD - DM.settings._WHEELPOS_SAVE_MARGIN
if wheelpos_mean >= save_rhd:
params.put_bool_nonblocking("IsRhdDetected", True)
elif wheelpos_mean <= save_lhd:
params.put_bool_nonblocking("IsRhdDetected", False)
DM.wheelpos.prob_offseter.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT and
DM.wheel_on_right == (DM.wheelpos.prob_offseter.filtered_stat.M > DM.settings._WHEELPOS_THRESHOLD)):
params.put_bool_nonblocking("IsRhdDetected", DM.wheel_on_right)
def main():
dmonitoringd_thread()
+22 -63
View File
@@ -35,11 +35,7 @@ class DRIVER_MONITOR_SETTINGS:
self._EYE_THRESHOLD = 0.65
self._SG_THRESHOLD = 0.9
self._BLINK_THRESHOLD = 0.865
self._PHONE_THRESH = 0.75 if device_type == 'mici' else 0.4
self._PHONE_THRESH2 = 15.0
self._PHONE_MAX_OFFSET = 0.06
self._PHONE_MIN_OFFSET = 0.025
self._PHONE_THRESH = 0.5
self._POSE_PITCH_THRESHOLD = 0.3133
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
@@ -50,6 +46,8 @@ class DRIVER_MONITOR_SETTINGS:
self._PITCH_NATURAL_OFFSET = 0.011 # initial value before offset is learned
self._PITCH_NATURAL_THRESHOLD = 0.449
self._YAW_NATURAL_OFFSET = 0.075 # initial value before offset is learned
self._PITCH_NATURAL_VAR = 3*0.01
self._YAW_NATURAL_VAR = 3*0.05
self._PITCH_MAX_OFFSET = 0.124
self._PITCH_MIN_OFFSET = -0.0881
self._YAW_MAX_OFFSET = 0.289
@@ -70,18 +68,9 @@ class DRIVER_MONITOR_SETTINGS:
self._WHEELPOS_CALIB_MIN_SPEED = 11
self._WHEELPOS_THRESHOLD = 0.5
self._WHEELPOS_FILTER_MIN_COUNT = int(15 / self._DT_DMON) # allow 15 seconds to converge wheel side
self._WHEELPOS_THRESHOLD_ENTER_RHD = self._WHEELPOS_THRESHOLD
self._WHEELPOS_THRESHOLD_ENTER_LHD = self._WHEELPOS_THRESHOLD
self._WHEELPOS_SAVE_MARGIN = 0.0
self._WHEELPOS_STARTUP_OVERRIDE_RHD = 0.55
self._WHEELPOS_STARTUP_OVERRIDE_LHD = 0.45
# C4 (mici) has shown borderline wheel-side probabilities around 0.5x.
# Use hysteresis and stricter persistence thresholds to avoid false RHD latching.
if device_type == 'mici':
self._WHEELPOS_THRESHOLD_ENTER_RHD = 0.65
self._WHEELPOS_THRESHOLD_ENTER_LHD = 0.35
self._WHEELPOS_SAVE_MARGIN = 0.05
self._WHEELPOS_DATA_AVG = 0.03
self._WHEELPOS_DATA_VAR = 3*5.5e-5
self._WHEELPOS_MAX_COUNT = -1
self._RECOVERY_FACTOR_MAX = 5. # relative to minus step change
self._RECOVERY_FACTOR_MIN = 1.25 # relative to minus step change
@@ -96,24 +85,26 @@ class DistractedType:
DISTRACTED_PHONE = 1 << 2
class DriverPose:
def __init__(self, max_trackable):
def __init__(self, settings):
pitch_filter_raw_priors = (settings._PITCH_NATURAL_OFFSET, settings._PITCH_NATURAL_VAR, 2)
yaw_filter_raw_priors = (settings._YAW_NATURAL_OFFSET, settings._YAW_NATURAL_VAR, 2)
self.yaw = 0.
self.pitch = 0.
self.roll = 0.
self.yaw_std = 0.
self.pitch_std = 0.
self.roll_std = 0.
self.pitch_offseter = RunningStatFilter(max_trackable=max_trackable)
self.yaw_offseter = RunningStatFilter(max_trackable=max_trackable)
self.pitch_offseter = RunningStatFilter(raw_priors=pitch_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
self.yaw_offseter = RunningStatFilter(raw_priors=yaw_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
self.calibrated = False
self.low_std = True
self.cfactor_pitch = 1.
self.cfactor_yaw = 1.
class DriverProb:
def __init__(self, max_trackable):
def __init__(self, raw_priors, max_trackable):
self.prob = 0.
self.prob_offseter = RunningStatFilter(max_trackable=max_trackable)
self.prob_offseter = RunningStatFilter(raw_priors=raw_priors, max_trackable=max_trackable)
self.prob_calibrated = False
class DriverBlink:
@@ -152,10 +143,11 @@ class DriverMonitoring:
self.settings = settings if settings is not None else DRIVER_MONITOR_SETTINGS(device_type=HARDWARE.get_device_type())
# init driver status
self.wheelpos = DriverProb(-1)
self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT)
self.phone = DriverProb(self.settings._POSE_OFFSET_MAX_COUNT)
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
self.pose = DriverPose(settings=self.settings)
self.blink = DriverBlink()
self.phone_prob = 0.
self.always_on = always_on
self.distracted_types = []
@@ -256,12 +248,7 @@ class DriverMonitoring:
if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
distracted_types.append(DistractedType.DISTRACTED_BLINK)
if self.phone.prob_calibrated:
using_phone = self.phone.prob > max(min(self.phone.prob_offseter.filtered_stat.M, self.settings._PHONE_MAX_OFFSET), self.settings._PHONE_MIN_OFFSET) \
* self.settings._PHONE_THRESH2
else:
using_phone = self.phone.prob > self.settings._PHONE_THRESH
if using_phone:
if self.phone_prob > self.settings._PHONE_THRESH:
distracted_types.append(DistractedType.DISTRACTED_PHONE)
return distracted_types
@@ -274,34 +261,8 @@ class DriverMonitoring:
self.wheelpos.prob_offseter.push_and_update(rhd_pred)
self.wheelpos.prob_calibrated = self.wheelpos.prob_offseter.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT
startup_override = None
if not self.wheelpos.prob_calibrated and not demo_mode and not op_engaged:
left_face_detected = driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD
right_face_detected = driver_state.rightDriverData.faceProb > self.settings._FACE_THRESHOLD
if rhd_pred <= self.settings._WHEELPOS_STARTUP_OVERRIDE_LHD and left_face_detected and not right_face_detected:
startup_override = False
elif rhd_pred >= self.settings._WHEELPOS_STARTUP_OVERRIDE_RHD and right_face_detected and not left_face_detected:
startup_override = True
if self.wheelpos.prob_calibrated or demo_mode:
wheelpos_mean = self.wheelpos.prob_offseter.filtered_stat.M
enter_rhd = self.settings._WHEELPOS_THRESHOLD_ENTER_RHD
enter_lhd = self.settings._WHEELPOS_THRESHOLD_ENTER_LHD
# Hysteresis: avoid side flapping near 0.5 and preserve last stable side.
if self.wheel_on_right_last is None:
if wheelpos_mean >= enter_rhd:
self.wheel_on_right = True
elif wheelpos_mean <= enter_lhd:
self.wheel_on_right = False
else:
self.wheel_on_right = self.wheel_on_right_default
elif self.wheel_on_right_last:
self.wheel_on_right = wheelpos_mean > enter_lhd
else:
self.wheel_on_right = wheelpos_mean >= enter_rhd
elif startup_override is not None:
self.wheel_on_right = startup_override
self.wheel_on_right = self.wheelpos.prob_offseter.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD
else:
self.wheel_on_right = self.wheel_on_right_default # use default/saved if calibration is unfinished
# make sure no switching when engaged
@@ -325,7 +286,7 @@ class DriverMonitoring:
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
self.phone.prob = driver_data.phoneProb
self.phone_prob = driver_data.phoneProb
self.distracted_types = self._get_distracted_types()
self.driver_distracted = (DistractedType.DISTRACTED_PHONE in self.distracted_types
@@ -339,11 +300,9 @@ class DriverMonitoring:
if self.face_detected and car_speed > self.settings._POSE_CALIB_MIN_SPEED and self.pose.low_std and (not op_engaged or not self.driver_distracted):
self.pose.pitch_offseter.push_and_update(self.pose.pitch)
self.pose.yaw_offseter.push_and_update(self.pose.yaw)
self.phone.prob_offseter.push_and_update(self.phone.prob)
self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \
self.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
self.phone.prob_calibrated = self.phone.prob_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
if self.face_detected and not self.driver_distracted:
if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD:
@@ -449,8 +408,8 @@ class DriverMonitoring:
"posePitchValidCount": self.pose.pitch_offseter.filtered_stat.n,
"poseYawOffset": self.pose.yaw_offseter.filtered_stat.mean(),
"poseYawValidCount": self.pose.yaw_offseter.filtered_stat.n,
"phoneProbOffset": self.phone.prob_offseter.filtered_stat.mean(),
"phoneProbValidCount": self.phone.prob_offseter.filtered_stat.n,
"phoneProbOffset": 0.,
"phoneProbValidCount": 0,
"stepChange": self.step_change,
"awarenessActive": self.awareness_active,
"awarenessPassive": self.awareness_passive,
+1 -27
View File
@@ -30,23 +30,6 @@ def make_msg(face_detected, distracted=False, model_uncertain=False):
return ds
def make_dual_msg(left_face_prob, right_face_prob, wheel_on_right_prob=0., model_uncertain=False):
ds = log.DriverStateV2.new_message()
ds.wheelOnRightProb = wheel_on_right_prob
for side, face_prob in ((ds.leftDriverData, left_face_prob), (ds.rightDriverData, right_face_prob)):
side.faceOrientation = [0., 0., 0.]
side.facePosition = [0., 0.]
side.faceProb = face_prob
side.leftEyeProb = 1.
side.rightEyeProb = 1.
side.leftBlinkProb = 0.
side.rightBlinkProb = 0.
side.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain]
side.facePositionStd = [1.*model_uncertain, 1.*model_uncertain]
side.phoneProb = 0.
return ds
# driver state from neural net, 10Hz
msg_NO_FACE_DETECTED = make_msg(False)
msg_ATTENTIVE = make_msg(True)
@@ -88,16 +71,6 @@ class TestMonitoring:
events, _ = self._run_seq(always_attentive, always_false, always_true, always_false)
self._assert_no_events(events)
def test_saved_rhd_recovers_to_lhd_with_strong_left_face(self):
settings = DRIVER_MONITOR_SETTINGS(device_type='mici')
DM = DriverMonitoring(rhd_saved=True, settings=settings)
msg = make_dual_msg(left_face_prob=0.95, right_face_prob=0.2, wheel_on_right_prob=0.35)
DM._update_states(msg, [0, 0, 0], 0, False, False)
assert not DM.wheel_on_right
assert DM.face_detected
# engaged, driver is distracted and does nothing
def test_fully_distracted_driver(self):
events, d_status = self._run_seq(always_distracted, always_false, always_true, always_false)
@@ -230,3 +203,4 @@ class TestMonitoring:
events[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names
assert EventName.driverUnresponsive in \
events[int((INVISIBLE_SECONDS_TO_RED-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names
@@ -91,7 +91,7 @@ class DeveloperLayoutMici(NavWidget):
self._long_maneuver_toggle,
self._alpha_long_toggle,
self._debug_mode_toggle,
], snap_items=False)
], snap_items=False, scroll_indicator=True, edge_shadows=True)
# Toggle lists
self._refresh_toggles = (
+13 -2
View File
@@ -76,6 +76,8 @@ def _engaged_confirmation_callback(callback: Callable, action_text: str):
icon = "icons_mici/settings/device/reboot.png"
elif action_text == "reset":
icon = "icons_mici/settings/device/lkas.png"
elif action_text == "reset driver monitoring":
icon = "icons_mici/settings/device/cameras.png"
elif action_text == "uninstall":
icon = "icons_mici/settings/device/uninstall.png"
else:
@@ -83,7 +85,7 @@ def _engaged_confirmation_callback(callback: Callable, action_text: str):
icon = "icons_mici/settings/comma_icon.png"
dlg: BigConfirmationDialogV2 | BigDialog = BigConfirmationDialogV2(f"slide to\n{action_text.lower()}", icon, red=red,
exit_on_confirm=action_text == "reset",
exit_on_confirm=action_text in {"reset", "reset driver monitoring"},
confirm_callback=confirm_callback)
gui_app.set_modal_overlay(dlg)
else:
@@ -459,9 +461,17 @@ class DeviceLayoutMici(NavWidget):
params.remove("LiveDelay")
params.put_bool("OnroadCycleRequested", True)
def reset_driver_monitoring_callback():
params = ui_state.params
params.remove("IsRhdDetected")
params.put_bool("OnroadCycleRequested", True)
def uninstall_openpilot_callback():
ui_state.params.put_bool("DoUninstall", True)
reset_driver_monitoring_btn = BigButton("reset driver monitoring calibration", "", "icons_mici/settings/device/cameras.png")
reset_driver_monitoring_btn.set_click_callback(lambda: _engaged_confirmation_callback(reset_driver_monitoring_callback, "reset driver monitoring"))
reset_calibration_btn = BigButton("reset calibration", "", "icons_mici/settings/device/lkas.png")
reset_calibration_btn.set_click_callback(lambda: _engaged_confirmation_callback(reset_calibration_callback, "reset"))
@@ -508,13 +518,14 @@ class DeviceLayoutMici(NavWidget):
PairBigButton(),
review_training_guide_btn,
driver_cam_btn,
reset_driver_monitoring_btn,
# lang_button,
reset_calibration_btn,
uninstall_openpilot_btn,
regulatory_btn,
reboot_btn,
self._power_off_btn,
], snap_items=False)
], snap_items=False, scroll_indicator=True, edge_shadows=True)
# Set up back navigation
self.set_back_callback(back_callback)
@@ -3,7 +3,7 @@ from enum import IntEnum
from collections.abc import Callable
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon, normalize_ssid
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle, BigParamControl
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
from openpilot.selfdrive.ui.ui_state import ui_state
@@ -75,8 +75,14 @@ class NetworkLayoutMici(NavWidget):
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
self._network_metered_btn.set_enabled(False)
wifi_button = BigButton("wi-fi")
self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 64, 56)
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 64, 47)
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 64, 47)
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47)
wifi_button = BigButton("wi-fi", "not connected", self._wifi_slash_txt)
wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
self._wifi_button = wifi_button
# ******** Advanced settings ********
# ******** Roaming toggle ********
@@ -101,7 +107,7 @@ class NetworkLayoutMici(NavWidget):
self._cellular_metered_btn,
# */
self._ip_address_btn,
], snap_items=False)
], snap_items=False, scroll_indicator=True, edge_shadows=True)
# Set initial config
roaming_enabled = ui_state.params.get_bool("GsmRoaming")
@@ -90,7 +90,7 @@ class SettingsLayout(NavWidget):
#BigDialogButton("manual", "", "icons_mici/settings/manual_icon.png", "Check out the mici user\nmanual at comma.ai/setup"),
firehose_btn,
developer_btn,
], snap_items=False)
], snap_items=False, scroll_indicator=True, edge_shadows=True)
# Set up back navigation
self.set_back_callback(self.close_settings)
@@ -35,7 +35,7 @@ class TogglesLayoutMici(NavWidget):
record_front,
record_mic,
enable_openpilot,
], snap_items=False)
], snap_items=False, scroll_indicator=True, edge_shadows=True)
# Toggle lists
self._refresh_toggles = (
@@ -214,6 +214,10 @@ class AugmentedRoadView(CameraView):
# debug
self._pm = messaging.PubMaster(['uiDebug'])
@staticmethod
def _controls_ready() -> bool:
return ui_state.sm.recv_frame["selfdriveState"] >= ui_state.started_frame
def is_swiping_left(self) -> bool:
"""Check if currently swiping left (for scroller to disable)."""
return self._bookmark_icon.is_swiping_left()
@@ -224,6 +228,8 @@ class AugmentedRoadView(CameraView):
# update offroad label
if ui_state.panda_type == log.PandaState.PandaType.unknown:
self._offroad_label.set_text("system booting")
elif ui_state.started and not self._controls_ready():
self._offroad_label.set_text("waiting for\ncontrols to start")
else:
self._offroad_label.set_text("start the car to\nuse openpilot")
@@ -259,6 +265,21 @@ class AugmentedRoadView(CameraView):
# Render the base camera view
super()._render(self._content_rect)
waiting_for_controls = ui_state.started and not self._controls_ready()
if waiting_for_controls:
rl.draw_rectangle(int(self._content_rect.x), int(self._content_rect.y),
int(self._content_rect.width), int(self._content_rect.height),
rl.Color(0, 0, 0, 145))
self._offroad_label.render(self._content_rect)
rl.end_scissor_mode()
self._draw_border()
self._bookmark_icon.render(self.rect)
msg = messaging.new_message('uiDebug')
msg.uiDebug.drawTimeMillis = (time.monotonic() - start_draw) * 1000
self._pm.send('uiDebug', msg)
return
in_reverse = self._is_in_reverse()
# Draw all UI overlays
+18 -3
View File
@@ -110,6 +110,7 @@ class BigButton(Widget):
self.text = text
self.value = value
self.set_icon(icon)
self._label_font_size_override: int | None = None
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
@@ -136,6 +137,18 @@ class BigButton(Widget):
def set_icon(self, icon: Union[str, rl.Texture]):
self._txt_icon = gui_app.texture(icon, 64, 64) if isinstance(icon, str) and len(icon) else icon
def _refresh_label_metrics(self):
font_size = self._label_font_size_override if self._label_font_size_override is not None else self._get_label_font_size()
self._label.set_font_size(font_size)
self._needs_scroll = measure_text_cached(self._label_font, self.text, font_size).x + 25 > self._rect.width
self._scroll_offset = 0
self._scroll_timer = 0
self._scroll_state = ScrollState.PRE_SCROLL
def _set_label_font_size_override(self, font_size: int | None):
self._label_font_size_override = font_size
self._refresh_label_metrics()
def set_rotate_icon(self, rotate: bool):
if rotate and self._rotate_icon_t is not None:
return
@@ -165,10 +178,12 @@ class BigButton(Widget):
def set_text(self, text: str):
self.text = text
self._label.set_text(text)
self._refresh_label_metrics()
def set_value(self, value: str):
self.value = value
self._sub_label.set_text(value)
self._refresh_label_metrics()
def get_value(self) -> str:
return self.value
@@ -256,7 +271,7 @@ class BigToggle(BigButton):
self._checked = initial_state
self._toggle_callback = toggle_callback
self._label.set_font_size(48)
self._set_label_font_size_override(48)
def _load_images(self):
super()._load_images()
@@ -296,8 +311,8 @@ class BigMultiToggle(BigToggle):
self._select_callback = select_callback
self._label.set_width(int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width))
# TODO: why isn't this automatic?
self._label.set_font_size(self._get_label_font_size())
# Keep the title size stable when the selected option changes.
self._set_label_font_size_override(self._get_label_font_size())
self.set_value(self._options[0])
+9 -1
View File
@@ -113,6 +113,14 @@ class BigConfirmationDialogV2(BigDialogBase):
self._slider = BigSlider(title, icon_txt, confirm_callback=self._on_confirm)
self._slider.set_enabled(lambda: not self._swiping_away)
def show_event(self):
super().show_event()
self._slider.show_event()
def hide_event(self):
super().hide_event()
self._slider.hide_event()
def _on_confirm(self):
if self._confirm_callback:
self._confirm_callback()
@@ -122,7 +130,7 @@ class BigConfirmationDialogV2(BigDialogBase):
def _update_state(self):
super()._update_state()
if self._swiping_away and not self._slider.confirmed:
self._slider.reset()
self._slider.reset(reset_shimmer=False)
def _render(self, _) -> DialogResult:
self._slider.render(self._rect)
+21 -1
View File
@@ -363,6 +363,26 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
connect(dcamBtn, &ButtonControl::clicked, [=]() { emit showDriverView(); });
addItem(dcamBtn);
resetDmCalibBtn = new ButtonControl(
tr("Reset Driver Monitoring"),
tr("RESET"),
tr("Clears the saved driver monitoring wheel-side calibration if the device thinks you're seated on the wrong side. "
"Resetting will restart openpilot if the car is powered on.")
);
connect(resetDmCalibBtn, &ButtonControl::clicked, [&]() {
if (!uiState()->engaged()) {
if (ConfirmationDialog::confirm(tr("Are you sure you want to reset driver monitoring calibration?"), tr("Reset"), this)) {
if (!uiState()->engaged()) {
params.remove("IsRhdDetected");
params.putBool("OnroadCycleRequested", true);
}
}
} else {
ConfirmationDialog::alert(tr("Disengage to Reset Driver Monitoring"), this);
}
});
addItem(resetDmCalibBtn);
resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), "");
connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription);
connect(resetCalibBtn, &ButtonControl::clicked, [&]() {
@@ -420,7 +440,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
});
QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) {
for (auto btn : findChildren<ButtonControl *>()) {
if (btn != pair_device && btn != resetCalibBtn) {
if (btn != pair_device && btn != resetCalibBtn && btn != resetDmCalibBtn) {
btn->setEnabled(offroad);
}
}
+1
View File
@@ -94,6 +94,7 @@ private:
ButtonControl *pair_galaxy;
QPushButton *galaxy_qr_btn;
ButtonControl *resetCalibBtn;
ButtonControl *resetDmCalibBtn;
};
class TogglesPanel : public ListWidget {
BIN
View File
Binary file not shown.
+8
View File
@@ -324,6 +324,14 @@ class WifiManager:
def ipv4_address(self) -> str:
return self._ipv4_address
@property
def networks(self) -> list[Network]:
return list(self._networks)
@property
def connecting_to_ssid(self) -> str:
return self._connecting_to_ssid
@property
def current_network_metered(self) -> MeteredType:
return self._current_network_metered
+9 -3
View File
@@ -133,11 +133,17 @@ class SoftwareSelectionPage(Widget):
super().__init__()
self._openpilot_slider = LargerSlider("slide to use\nstarpilot", use_openpilot_callback)
self._custom_software_slider = LargerSlider("slide to use\ncustom software", use_custom_software_callback, green=False)
self._custom_software_slider = LargerSlider("slide to use\ncustom software", use_custom_software_callback,
green=False, shimmer_offset=0.4)
def show_event(self):
super().show_event()
self._openpilot_slider.show_event()
self._custom_software_slider.show_event()
def reset(self):
self._openpilot_slider.reset()
self._custom_software_slider.reset()
self._openpilot_slider.reset(reset_shimmer=False)
self._custom_software_slider.reset(reset_shimmer=False)
def _render(self, rect: rl.Rectangle):
self._openpilot_slider.set_opacity(1.0 - self._custom_software_slider.slider_percentage)
+54 -1
View File
@@ -1,3 +1,4 @@
import math
from enum import IntEnum
from collections.abc import Callable
from itertools import zip_longest
@@ -401,6 +402,12 @@ class UnifiedLabel(Widget):
- Proper multiline vertical alignment
- Height calculation for layout purposes
"""
SHIMMER_BAND_WIDTH = 0.3
SHIMMER_BLUR_RADIUS = 0.12
SHIMMER_CYCLE_PERIOD = 2.5
SHIMMER_SWEEP_FRACTION = 0.9
SHIMMER_LOW_OPACITY = 0.65
def __init__(self,
text: str | Callable[[], str],
font_size: int = DEFAULT_TEXT_SIZE,
@@ -414,7 +421,8 @@ class UnifiedLabel(Widget):
wrap_text: bool = True,
scroll: bool = False,
line_height: float = 1.0,
letter_spacing: float = 0.0):
letter_spacing: float = 0.0,
shimmer: bool = False):
super().__init__()
self._text = text
self._font_size = font_size
@@ -431,6 +439,8 @@ class UnifiedLabel(Widget):
self._line_height = line_height * 0.9
self._letter_spacing = letter_spacing # 0.1 = 10%
self._spacing_pixels = font_size * letter_spacing
self._shimmer = shimmer
self._shimmer_start_time = 0.0
# Scroll state
self._scroll = scroll
@@ -489,6 +499,13 @@ class UnifiedLabel(Widget):
self._spacing_pixels = self._font_size * letter_spacing
self._cached_text = None # Invalidate cache
def set_line_height(self, line_height: float):
"""Update line height (multiplier, e.g., 1.0 = default)."""
new_line_height = line_height * 0.9
if self._line_height != new_line_height:
self._line_height = new_line_height
self._cached_text = None
def set_font_weight(self, font_weight: FontWeight):
"""Update the font weight."""
if self._font_weight != font_weight:
@@ -510,6 +527,9 @@ class UnifiedLabel(Widget):
self._scroll_pause_t = None
self._scroll_state = ScrollState.STARTING
def reset_shimmer(self, offset: float = 0.0):
self._shimmer_start_time = rl.get_time() + offset
def set_max_width(self, max_width: int | None):
"""Set the maximum width constraint for wrapping/eliding."""
if self._max_width != max_width:
@@ -627,6 +647,25 @@ class UnifiedLabel(Widget):
return self._cached_total_height
return 0.0
def _compute_shimmer_alpha(self, char_center_x: float, text_left: float, text_width: float) -> float:
if text_width <= 0:
return self.SHIMMER_LOW_OPACITY
elapsed = rl.get_time() - self._shimmer_start_time
sigma = text_width * self.SHIMMER_BLUR_RADIUS
t_raw = (elapsed % self.SHIMMER_CYCLE_PERIOD) / self.SHIMMER_CYCLE_PERIOD
t_clamped = max(0.0, min(t_raw / self.SHIMMER_SWEEP_FRACTION, 1.0))
t = t_clamped * t_clamped * (3.0 - 2.0 * t_clamped)
margin = text_width * self.SHIMMER_BAND_WIDTH
text_right = text_left + text_width
center = text_right + margin - t * (text_width + 2.0 * margin)
d = char_center_x - center
shimmer = math.exp(-0.5 * d * d / (sigma * sigma)) if sigma > 0 else 0.0
return self.SHIMMER_LOW_OPACITY + (1.0 - self.SHIMMER_LOW_OPACITY) * shimmer
def _render(self, _):
"""Render the label."""
if self._rect.width <= 0 or self._rect.height <= 0:
@@ -770,6 +809,20 @@ class UnifiedLabel(Widget):
line_x = self._rect.x + self._text_padding
line_x += self._scroll_offset + x_offset
if self._shimmer and not emojis and line:
base_alpha = self._text_color.a / 255.0
text_width = max(size.x, 1.0)
cursor_x = line_x
for char in line:
char_width = measure_text_cached(self._font, char, self._font_size, self._spacing_pixels).x
char_center_x = cursor_x + char_width / 2.0
shimmer_alpha = self._compute_shimmer_alpha(char_center_x, line_x, text_width)
char_alpha = int(255 * base_alpha * shimmer_alpha)
char_color = rl.Color(self._text_color.r, self._text_color.g, self._text_color.b, char_alpha)
rl.draw_text_ex(self._font, char, rl.Vector2(cursor_x, current_y), self._font_size, self._spacing_pixels, char_color)
cursor_x += char_width
return
# Render line with emojis
line_pos = rl.Vector2(line_x, current_y)
prev_index = 0
+76 -5
View File
@@ -11,6 +11,7 @@ ITEM_SPACING = 20
LINE_COLOR = rl.GRAY
LINE_PADDING = 40
ANIMATION_SCALE = 0.6
EDGE_SHADOW_WIDTH = 20
MIN_ZOOM_ANIMATION_TIME = 0.075 # seconds
DO_ZOOM = False
@@ -33,9 +34,50 @@ class LineSeparator(Widget):
LINE_COLOR)
class ScrollIndicator(Widget):
def __init__(self):
super().__init__()
self._txt_scroll_indicator = gui_app.texture("icons_mici/settings/horizontal_scroll_indicator.png", 96, 48)
self._scroll_offset: float = 0.0
self._content_size: float = 0.0
self._viewport: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
def update(self, scroll_offset: float, content_size: float, viewport: rl.Rectangle) -> None:
self._scroll_offset = scroll_offset
self._content_size = content_size
self._viewport = viewport
def _render(self, _):
if self._viewport.width <= 0 or self._viewport.height <= 0:
return
indicator_w = min(float(np.interp(self._content_size, [1000, 3000], [300, 100])), self._viewport.width)
max_scroll = self._content_size - self._viewport.width
if max_scroll > 0:
scroll_ratio = -self._scroll_offset / max_scroll
slide_range = max(self._viewport.width - indicator_w, 0.0)
x = self._viewport.x + scroll_ratio * slide_range
else:
x = self._viewport.x + (self._viewport.width - indicator_w) / 2
y = max(self._viewport.y, 0) + self._viewport.height - self._txt_scroll_indicator.height / 2
dest_left = max(x, self._viewport.x)
dest_right = min(x + indicator_w, self._viewport.x + self._viewport.width)
dest_w = max(indicator_w / 2, dest_right - dest_left)
dest_left = min(dest_left, self._viewport.x + self._viewport.width - dest_w)
dest_left = max(dest_left, self._viewport.x)
src_rec = rl.Rectangle(0, 0, self._txt_scroll_indicator.width, self._txt_scroll_indicator.height)
dest_rec = rl.Rectangle(dest_left, y, dest_w, self._txt_scroll_indicator.height)
rl.draw_texture_pro(self._txt_scroll_indicator, src_rec, dest_rec, rl.Vector2(0, 0), 0.0,
rl.Color(255, 255, 255, int(255 * 0.45)))
class Scroller(Widget):
def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = True, spacing: int = ITEM_SPACING,
line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING):
line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING,
scroll_indicator: bool = False, edge_shadows: bool = False):
super().__init__()
self._items: list[Widget] = []
self._horizontal = horizontal
@@ -65,11 +107,18 @@ class Scroller(Widget):
self.scroll_panel = GuiScrollPanel2(self._horizontal, handle_out_of_bounds=not self._snap_items)
self._scroll_enabled: bool | Callable[[], bool] = True
self._txt_scroll_indicator = gui_app.texture("icons_mici/settings/vertical_scroll_indicator.png", 40, 80)
self._txt_vertical_scroll_indicator = gui_app.texture("icons_mici/settings/vertical_scroll_indicator.png", 40, 80)
self._show_scroll_indicator = scroll_indicator and self._horizontal
self._scroll_indicator = ScrollIndicator()
self._edge_shadows = edge_shadows and self._horizontal
for item in items:
self.add_widget(item)
@property
def items(self) -> list[Widget]:
return self._items
def set_reset_scroll_at_show(self, scroll: bool):
self._reset_scroll_at_show = scroll
@@ -93,6 +142,14 @@ class Scroller(Widget):
self._items.append(item)
item.set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid() and self.enabled)
def move_item(self, from_index: int, to_index: int) -> None:
if from_index == to_index:
return
if not (0 <= from_index < len(self._items) and 0 <= to_index < len(self._items)):
return
item = self._items.pop(from_index)
self._items.insert(to_index, item)
def set_scrolling_enabled(self, enabled: bool | Callable[[], bool]) -> None:
"""Set whether scrolling is enabled (does not affect widget enabled state)."""
self._scroll_enabled = enabled
@@ -243,13 +300,27 @@ class Scroller(Widget):
# Draw scroll indicator
if SCROLL_BAR and not self._horizontal and len(self._visible_items) > 0:
_real_content_size = self._content_size - self._rect.height + self._txt_scroll_indicator.height
_real_content_size = self._content_size - self._rect.height + self._txt_vertical_scroll_indicator.height
scroll_bar_y = -self._scroll_offset / _real_content_size * self._rect.height
scroll_bar_y = min(max(scroll_bar_y, self._rect.y), self._rect.y + self._rect.height - self._txt_scroll_indicator.height)
rl.draw_texture_ex(self._txt_scroll_indicator, rl.Vector2(self._rect.x, scroll_bar_y), 0, 1.0, rl.WHITE)
scroll_bar_y = min(max(scroll_bar_y, self._rect.y), self._rect.y + self._rect.height - self._txt_vertical_scroll_indicator.height)
rl.draw_texture_ex(self._txt_vertical_scroll_indicator, rl.Vector2(self._rect.x, scroll_bar_y), 0, 1.0, rl.WHITE)
rl.end_scissor_mode()
if self._edge_shadows:
rl.draw_rectangle_gradient_h(int(self._rect.x), int(self._rect.y),
EDGE_SHADOW_WIDTH, int(self._rect.height),
rl.Color(0, 0, 0, 166), rl.BLANK)
right_x = int(self._rect.x + self._rect.width - EDGE_SHADOW_WIDTH)
rl.draw_rectangle_gradient_h(right_x, int(self._rect.y),
EDGE_SHADOW_WIDTH, int(self._rect.height),
rl.BLANK, rl.Color(0, 0, 0, 166))
if self._show_scroll_indicator and len(self._visible_items) > 0:
self._scroll_indicator.update(self._scroll_offset, self._content_size, self._rect)
self._scroll_indicator.render()
def show_event(self):
super().show_event()
if self._reset_scroll_at_show:
+39 -15
View File
@@ -5,17 +5,19 @@ import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter
class SmallSlider(Widget):
HORIZONTAL_PADDING = 8
CONFIRM_DELAY = 0.2
PRESSED_SCALE = 1.07
def __init__(self, title: str, confirm_callback: Callable | None = None):
def __init__(self, title: str, confirm_callback: Callable | None = None, shimmer_offset: float = 0.0):
# TODO: unify this with BigConfirmationDialogV2
super().__init__()
self._confirm_callback = confirm_callback
self._shimmer_offset = shimmer_offset
self._font = gui_app.font(FontWeight.DISPLAY)
@@ -30,29 +32,40 @@ class SmallSlider(Widget):
self._start_x_circle = 0.0
self._scroll_x_circle = 0.0
self._scroll_x_circle_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
self._circle_scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
self._circle_press_time: float | None = None
self._is_dragging_circle = False
self._label = UnifiedLabel(title, font_size=36, font_weight=FontWeight.MEDIUM, text_color=rl.Color(255, 255, 255, int(255 * 0.65)),
self._label = UnifiedLabel(title, font_size=36, font_weight=FontWeight.SEMI_BOLD, text_color=rl.WHITE,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, line_height=0.9)
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, line_height=0.9, shimmer=True)
def _load_assets(self):
self.set_rect(rl.Rectangle(0, 0, 316 + self.HORIZONTAL_PADDING * 2, 100))
self._bg_txt = gui_app.texture("icons_mici/setup/small_slider/slider_bg.png", 316, 100)
self._circle_bg_txt = gui_app.texture("icons_mici/setup/small_slider/slider_red_circle.png", 100, 100)
self._circle_bg_pressed_txt = self._circle_bg_txt
self._circle_arrow_txt = gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 37, 32)
@property
def confirmed(self) -> bool:
return self._confirmed_time > 0.0
def reset(self):
def show_event(self):
super().show_event()
self.reset()
def reset(self, reset_shimmer: bool = True):
# reset all slider state
self._is_dragging_circle = False
self._confirmed_time = 0.0
self._confirm_callback_called = False
self._circle_press_time = None
self._circle_scale_filter.x = 1.0
if reset_shimmer:
self._label.reset_shimmer(self._shimmer_offset)
def set_opacity(self, opacity: float, smooth: bool = False):
if smooth:
@@ -83,6 +96,7 @@ class SmallSlider(Widget):
if rl.check_collision_point_rec(mouse_event.pos, circle_button_rect):
self._start_x_circle = mouse_event.pos.x
self._is_dragging_circle = True
self._circle_press_time = rl.get_time()
elif mouse_event.left_released:
# swiped to left
@@ -129,8 +143,9 @@ class SmallSlider(Widget):
btn_x = bg_txt_x + self._bg_txt.width - self._circle_bg_txt.width + self._scroll_x_circle_filter.x
btn_y = self._rect.y + (self._rect.height - self._circle_bg_txt.height) / 2
if self._confirmed_time == 0.0 or self._scroll_x_circle > 0:
self._label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.65 * (1.0 - self.slider_percentage) * self._opacity_filter.x)))
label_alpha = int(255 * (1.0 - self.slider_percentage) * self._opacity_filter.x)
if label_alpha > 0:
self._label.set_text_color(rl.Color(255, 255, 255, label_alpha))
label_rect = rl.Rectangle(
self._rect.x + 20,
self._rect.y,
@@ -139,18 +154,24 @@ class SmallSlider(Widget):
)
self._label.render(label_rect)
# circle and arrow
rl.draw_texture_ex(self._circle_bg_txt, rl.Vector2(btn_x, btn_y), 0.0, 1.0, white)
circle_pressed = self._is_dragging_circle or self.confirmed or (
self._circle_press_time is not None and rl.get_time() - self._circle_press_time < 0.075
)
circle_bg_txt = self._circle_bg_pressed_txt if circle_pressed else self._circle_bg_txt
scale = self._circle_scale_filter.update(self.PRESSED_SCALE if circle_pressed else 1.0)
scaled_btn_x = btn_x + (self._circle_bg_txt.width * (1 - scale)) / 2
scaled_btn_y = btn_y + (self._circle_bg_txt.height * (1 - scale)) / 2
rl.draw_texture_ex(circle_bg_txt, rl.Vector2(scaled_btn_x, scaled_btn_y), 0.0, scale, white)
arrow_x = btn_x + (self._circle_bg_txt.width - self._circle_arrow_txt.width) / 2
arrow_y = btn_y + (self._circle_bg_txt.height - self._circle_arrow_txt.height) / 2
arrow_x = scaled_btn_x + (self._circle_bg_txt.width * scale - self._circle_arrow_txt.width) / 2
arrow_y = scaled_btn_y + (self._circle_bg_txt.height * scale - self._circle_arrow_txt.height) / 2
rl.draw_texture_ex(self._circle_arrow_txt, rl.Vector2(arrow_x, arrow_y), 0.0, 1.0, white)
class LargerSlider(SmallSlider):
def __init__(self, title: str, confirm_callback: Callable | None = None, green: bool = True):
def __init__(self, title: str, confirm_callback: Callable | None = None, green: bool = True, shimmer_offset: float = 0.0):
self._green = green
super().__init__(title, confirm_callback=confirm_callback)
super().__init__(title, confirm_callback=confirm_callback, shimmer_offset=shimmer_offset)
def _load_assets(self):
self.set_rect(rl.Rectangle(0, 0, 520 + self.HORIZONTAL_PADDING * 2, 115))
@@ -158,6 +179,7 @@ class LargerSlider(SmallSlider):
self._bg_txt = gui_app.texture("icons_mici/setup/small_slider/slider_bg_larger.png", 520, 115)
circle_fn = "slider_green_rounded_rectangle" if self._green else "slider_black_rounded_rectangle"
self._circle_bg_txt = gui_app.texture(f"icons_mici/setup/small_slider/{circle_fn}.png", 180, 115)
self._circle_bg_pressed_txt = self._circle_bg_txt
self._circle_arrow_txt = gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 64, 55)
@@ -165,15 +187,16 @@ class BigSlider(SmallSlider):
def __init__(self, title: str, icon: rl.Texture, confirm_callback: Callable | None = None):
self._icon = icon
super().__init__(title, confirm_callback=confirm_callback)
self._label = UnifiedLabel(title, font_size=48, font_weight=FontWeight.DISPLAY, text_color=rl.Color(255, 255, 255, int(255 * 0.65)),
self._label = UnifiedLabel(title, font_size=48, font_weight=FontWeight.DISPLAY, text_color=rl.WHITE,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
line_height=0.875)
line_height=0.875, shimmer=True)
def _load_assets(self):
self.set_rect(rl.Rectangle(0, 0, 520 + self.HORIZONTAL_PADDING * 2, 180))
self._bg_txt = gui_app.texture("icons_mici/buttons/slider_bg.png", 520, 180)
self._circle_bg_txt = gui_app.texture("icons_mici/buttons/button_circle.png", 180, 180)
self._circle_bg_pressed_txt = gui_app.texture("icons_mici/buttons/button_circle_hover.png", 180, 180)
self._circle_arrow_txt = self._icon
@@ -183,4 +206,5 @@ class RedBigSlider(BigSlider):
self._bg_txt = gui_app.texture("icons_mici/buttons/slider_bg.png", 520, 180)
self._circle_bg_txt = gui_app.texture("icons_mici/buttons/button_circle_red.png", 180, 180)
self._circle_bg_pressed_txt = gui_app.texture("icons_mici/buttons/button_circle_red_hover.png", 180, 180)
self._circle_arrow_txt = self._icon
Binary file not shown.
Binary file not shown.