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:
@@ -15,6 +15,7 @@ a.out
|
||||
.cache/
|
||||
.comma_sysroot/
|
||||
.venv-linux-arm64/
|
||||
compiledmodels/
|
||||
|
||||
/docs_site/
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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.")
|
||||
@@ -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" "$@"
|
||||
@@ -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,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 @@
|
||||
DEV-60a7fb11-DEBUG
|
||||
DEV-d17d41fb-DEBUG
|
||||
@@ -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 |
@@ -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.
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ private:
|
||||
ButtonControl *pair_galaxy;
|
||||
QPushButton *galaxy_qr_btn;
|
||||
ButtonControl *resetCalibBtn;
|
||||
ButtonControl *resetDmCalibBtn;
|
||||
};
|
||||
|
||||
class TogglesPanel : public ListWidget {
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
Reference in New Issue
Block a user