mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-09 09:43:47 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee05336586 |
+19
-2
@@ -1,8 +1,25 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from cereal import car
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
|
|
||||||
|
|
||||||
def get_gps_location_service(params: Params) -> str:
|
def gm_car_params_present(params: Params, CP: car.CarParams | None = None) -> bool:
|
||||||
if params.get_bool("UbloxAvailable") or params.get_bool("CarGpsAvailable"):
|
if CP is not None and getattr(CP, "brand", None):
|
||||||
|
return CP.brand == "gm"
|
||||||
|
try:
|
||||||
|
raw_car_params = params.get("CarParams")
|
||||||
|
if raw_car_params is None:
|
||||||
|
return False
|
||||||
|
with car.CarParams.from_bytes(raw_car_params) as parsed_cp:
|
||||||
|
return parsed_cp.brand == "gm"
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_gps_location_service(params: Params, CP: car.CarParams | None = None) -> str:
|
||||||
|
# GM arbitrates device/PPS/OnStar through gpsLocationExternal.
|
||||||
|
if gm_car_params_present(params, CP) or params.get_bool("UbloxAvailable") or params.get_bool("CarGpsAvailable"):
|
||||||
return "gpsLocationExternal"
|
return "gpsLocationExternal"
|
||||||
else:
|
else:
|
||||||
return "gpsLocation"
|
return "gpsLocation"
|
||||||
|
|||||||
Binary file not shown.
@@ -18,7 +18,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"BootCount", {PERSISTENT, INT}},
|
{"BootCount", {PERSISTENT, INT}},
|
||||||
{"BluetoothAudioAddress", {PERSISTENT, STRING}},
|
{"BluetoothAudioAddress", {PERSISTENT, STRING}},
|
||||||
{"BluetoothAudioTestActive", {CLEAR_ON_MANAGER_START | DONT_LOG, BOOL}},
|
{"BluetoothAudioTestActive", {CLEAR_ON_MANAGER_START | DONT_LOG, BOOL}},
|
||||||
{"BluetoothDisconnectControllersOffroad", {PERSISTENT, BOOL, "0"}},
|
|
||||||
{"BluetoothEnabled", {PERSISTENT, BOOL, "0"}},
|
{"BluetoothEnabled", {PERSISTENT, BOOL, "0"}},
|
||||||
{"CalibrationParams", {PERSISTENT, BYTES}},
|
{"CalibrationParams", {PERSISTENT, BYTES}},
|
||||||
{"CameraDebugExpGain", {CLEAR_ON_MANAGER_START, STRING}},
|
{"CameraDebugExpGain", {CLEAR_ON_MANAGER_START, STRING}},
|
||||||
@@ -317,7 +316,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"DeveloperSidebarMetric7", {PERSISTENT, INT, "7", "0", 3}},
|
{"DeveloperSidebarMetric7", {PERSISTENT, INT, "7", "0", 3}},
|
||||||
{"DeveloperUI", {PERSISTENT, BOOL, "0", "0", 3}},
|
{"DeveloperUI", {PERSISTENT, BOOL, "0", "0", 3}},
|
||||||
{"GalaxyDeveloperMode", {PERSISTENT | DONT_LOG, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
{"GalaxyDeveloperMode", {PERSISTENT | DONT_LOG, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||||
{"GalaxyMobileDefault", {PERSISTENT | DONT_LOG, BOOL, "1", "1", 0, SETTINGS_SIMPLE}},
|
{"GalaxyMobileDefault", {PERSISTENT | DONT_LOG, BOOL, "0", "0", 0, SETTINGS_ADVANCED}},
|
||||||
{"DeveloperWidgets", {PERSISTENT, BOOL, "1", "0", 3}},
|
{"DeveloperWidgets", {PERSISTENT, BOOL, "1", "0", 3}},
|
||||||
{"DeviceManagement", {PERSISTENT, BOOL, "1", "0", 1, SETTINGS_SIMPLE}},
|
{"DeviceManagement", {PERSISTENT, BOOL, "1", "0", 1, SETTINGS_SIMPLE}},
|
||||||
{"DeviceShutdown", {PERSISTENT, INT, "6", "6", 1, SETTINGS_SIMPLE}},
|
{"DeviceShutdown", {PERSISTENT, INT, "6", "6", 1, SETTINGS_SIMPLE}},
|
||||||
@@ -464,7 +463,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"LeadDepartingAlert", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
{"LeadDepartingAlert", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||||
{"LeadDetectionThreshold", {PERSISTENT, INT, "35", "50", 3}},
|
{"LeadDetectionThreshold", {PERSISTENT, INT, "35", "50", 3}},
|
||||||
{"LeadIndicator", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
{"LeadIndicator", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||||
{"LeadInfo", {PERSISTENT, BOOL, "0", "0", 3}},
|
{"LeadInfo", {PERSISTENT, BOOL, "1", "0", 3}},
|
||||||
{"LKASButtonControl", {PERSISTENT, INT, "5", "0", 2, SETTINGS_SIMPLE}},
|
{"LKASButtonControl", {PERSISTENT, INT, "5", "0", 2, SETTINGS_SIMPLE}},
|
||||||
{"LockDoors", {PERSISTENT, BOOL, "1", "0", 0}},
|
{"LockDoors", {PERSISTENT, BOOL, "1", "0", 0}},
|
||||||
{"LockDoorsTimer", {PERSISTENT, INT, "0", "0", 0}},
|
{"LockDoorsTimer", {PERSISTENT, INT, "0", "0", 0}},
|
||||||
@@ -609,7 +608,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"RelaxedJerkDeceleration", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
|
{"RelaxedJerkDeceleration", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
|
||||||
{"RelaxedJerkSpeed", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
|
{"RelaxedJerkSpeed", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
|
||||||
{"RelaxedJerkSpeedDecrease", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
|
{"RelaxedJerkSpeedDecrease", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
|
||||||
{"ReverseCruise", {PERSISTENT, BOOL, "0", "0", 1, SETTINGS_SIMPLE}},
|
|
||||||
{"RivianAngleControl", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
{"RivianAngleControl", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||||
{"RivianAngleSaturated", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL, "0", "0"}},
|
{"RivianAngleSaturated", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL, "0", "0"}},
|
||||||
{"RivianToiRecoveryFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL, "0", "0"}},
|
{"RivianToiRecoveryFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL, "0", "0"}},
|
||||||
|
|||||||
Binary file not shown.
@@ -852,6 +852,7 @@ class CarController(CarControllerBase):
|
|||||||
CAR.CHEVROLET_VOLT_CC,
|
CAR.CHEVROLET_VOLT_CC,
|
||||||
CAR.CHEVROLET_MALIBU_CC,
|
CAR.CHEVROLET_MALIBU_CC,
|
||||||
CAR.CHEVROLET_MALIBU_HYBRID_CC,
|
CAR.CHEVROLET_MALIBU_HYBRID_CC,
|
||||||
|
CAR.BUICK_LACROSSE,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.CP.enableGasInterceptorDEPRECATED and self.CP.carFingerprint in CC_REGEN_PADDLE_CAR and
|
if (self.CP.enableGasInterceptorDEPRECATED and self.CP.carFingerprint in CC_REGEN_PADDLE_CAR and
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import copy
|
import copy
|
||||||
import math
|
import math
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
from collections.abc import Mapping
|
||||||
from cereal import custom
|
from cereal import custom
|
||||||
from opendbc.can import CANDefine, CANParser
|
from opendbc.can import CANDefine, CANParser
|
||||||
from opendbc.car import Bus, create_button_events, structs
|
from opendbc.car import Bus, create_button_events, structs
|
||||||
@@ -8,12 +10,10 @@ from opendbc.car.common.conversions import Conversions as CV
|
|||||||
from opendbc.car.gps import get_car_gps_config
|
from opendbc.car.gps import get_car_gps_config
|
||||||
from opendbc.car.interfaces import CarStateBase
|
from opendbc.car.interfaces import CarStateBase
|
||||||
from opendbc.car.gm.values import (
|
from opendbc.car.gm.values import (
|
||||||
ALT_ACCS,
|
|
||||||
ASCM_INT,
|
ASCM_INT,
|
||||||
CAMERA_ACC_CAR,
|
CAMERA_ACC_CAR,
|
||||||
CAR,
|
CAR,
|
||||||
CC_ONLY_CAR,
|
CC_ONLY_CAR,
|
||||||
CC_REGEN_PADDLE_CAR,
|
|
||||||
DBC,
|
DBC,
|
||||||
AccState,
|
AccState,
|
||||||
CanBus,
|
CanBus,
|
||||||
@@ -38,6 +38,112 @@ BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.D
|
|||||||
HARD_BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.DECEL_SET: ButtonType.decelCruise}
|
HARD_BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.DECEL_SET: ButtonType.decelCruise}
|
||||||
NORMAL_CRUISE_BUTTONS = (CruiseButtons.RES_ACCEL, CruiseButtons.DECEL_SET)
|
NORMAL_CRUISE_BUTTONS = (CruiseButtons.RES_ACCEL, CruiseButtons.DECEL_SET)
|
||||||
|
|
||||||
|
# Optional ~10 Hz CT6 PPS GPS messages on the powertrain bus.
|
||||||
|
PPS_GPS_MESSAGES = (
|
||||||
|
"PPS_ElevHdSpd_FO",
|
||||||
|
"PPS_PosLat_FO",
|
||||||
|
"PPS_PosLong_FO",
|
||||||
|
"PPS_Time_FO",
|
||||||
|
"PPS_QualMetrics_FO",
|
||||||
|
)
|
||||||
|
# PPS_SigAcqTime_FO is omitted: its validity bit stays 1 even during valid fixes.
|
||||||
|
|
||||||
|
|
||||||
|
def pps_checksum_ok(data: bytes) -> bool:
|
||||||
|
"""Validate the 11-bit checksum used by the observed PPS frames."""
|
||||||
|
if len(data) < 2:
|
||||||
|
return False
|
||||||
|
received = ((data[-2] & 0x07) << 8) | data[-1]
|
||||||
|
expected = sum(data[:-2]) + (data[-2] >> 3) + 0x4C
|
||||||
|
return (expected & 0x7FF) == received
|
||||||
|
|
||||||
|
|
||||||
|
def decode_gm_pps_gps(values: Mapping[str, Mapping[str, float]], raw: Mapping[str, bytes],
|
||||||
|
timestamp_nanos: int) -> dict | None:
|
||||||
|
"""Decode one coherent PPS bundle into the existing car-GPS sample shape."""
|
||||||
|
if any(not pps_checksum_ok(raw.get(name, b"")) for name in PPS_GPS_MESSAGES):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
pos_lat = values["PPS_PosLat_FO"]
|
||||||
|
pos_long = values["PPS_PosLong_FO"]
|
||||||
|
timestamp_values = values["PPS_Time_FO"]
|
||||||
|
quality = values["PPS_QualMetrics_FO"]
|
||||||
|
if (int(pos_lat.get("PPSLatV", 1)) != 0 or
|
||||||
|
int(pos_long.get("PPSLongV", 1)) != 0 or
|
||||||
|
int(quality.get("PPS2DAbsPosErrEstmtV", 1)) != 0 or
|
||||||
|
int(quality.get("PPSMdV", 1)) != 0 or
|
||||||
|
int(quality.get("PPSPstnDilPrcsV", 1)) != 0 or
|
||||||
|
int(timestamp_values.get("PPSTmdayV", 1)) != 0 or
|
||||||
|
int(timestamp_values.get("PPSCldrDayV", 1)) != 0 or
|
||||||
|
int(timestamp_values.get("PPSCldrYrV", 1)) != 0):
|
||||||
|
return None
|
||||||
|
# Reject mode 6 (dead reckoning only without GNSS).
|
||||||
|
if int(quality["PPSMd"]) == 6:
|
||||||
|
return None
|
||||||
|
|
||||||
|
latitude = float(pos_lat["PPSLat"]) / 3_600_000.0
|
||||||
|
longitude = float(pos_long["PPSLong"]) / 3_600_000.0
|
||||||
|
if not (math.isfinite(latitude) and math.isfinite(longitude) and
|
||||||
|
-90.0 <= latitude <= 90.0 and -180.0 <= longitude <= 180.0 and
|
||||||
|
(latitude != 0.0 or longitude != 0.0)):
|
||||||
|
return None
|
||||||
|
|
||||||
|
year = int(timestamp_values["PPSCldrYr"])
|
||||||
|
day_of_year = int(timestamp_values["PPSCldrDay"])
|
||||||
|
millis_of_day = int(timestamp_values["PPSTmday"])
|
||||||
|
if not 2014 <= year <= 2141 or day_of_year < 1 or not 0 <= millis_of_day < 86_400_000:
|
||||||
|
return None
|
||||||
|
timestamp = datetime(year, 1, 1, tzinfo=UTC) + timedelta(days=day_of_year - 1, milliseconds=millis_of_day)
|
||||||
|
if timestamp.year != year:
|
||||||
|
return None
|
||||||
|
|
||||||
|
elev = values["PPS_ElevHdSpd_FO"]
|
||||||
|
speed = float(elev["PPSVel"]) * CV.KPH_TO_MS
|
||||||
|
if int(elev.get("PPSVelV", 1)) != 0 or not math.isfinite(speed) or not 0.0 <= speed <= 200.0:
|
||||||
|
speed = 0.0
|
||||||
|
heading = float(elev["PPSHedng"])
|
||||||
|
if (int(elev.get("PPSHedngV", 1)) != 0 or
|
||||||
|
not math.isfinite(heading) or not 0.0 <= heading < 360.0):
|
||||||
|
heading = 0.0
|
||||||
|
|
||||||
|
altitude = float(elev["PPSElvtn"]) / 100.0
|
||||||
|
if int(elev.get("PPSElvtnV", 1)) != 0 or not math.isfinite(altitude):
|
||||||
|
altitude = 0.0
|
||||||
|
|
||||||
|
horizontal_accuracy = float(quality["PPS2DAbsPosErrEstmt"])
|
||||||
|
if not math.isfinite(horizontal_accuracy) or horizontal_accuracy < 0.0:
|
||||||
|
horizontal_accuracy = 0.0
|
||||||
|
|
||||||
|
vertical_accuracy = float(quality["PPS3DAbsPosErrEstmt"])
|
||||||
|
if int(quality.get("PPS3DAbsPosErrEstmtV", 1)) != 0 or not math.isfinite(vertical_accuracy) or vertical_accuracy < 0.0:
|
||||||
|
vertical_accuracy = 0.0
|
||||||
|
|
||||||
|
bearing_accuracy = float(quality["PPSAbsHdngErrEstmt"])
|
||||||
|
if int(quality.get("PPSAbsHdngErrEstmtV", 1)) != 0 or not math.isfinite(bearing_accuracy) or bearing_accuracy < 0.0:
|
||||||
|
bearing_accuracy = 180.0
|
||||||
|
except (KeyError, TypeError, ValueError, OverflowError, AttributeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
heading_rad = math.radians(heading)
|
||||||
|
return {
|
||||||
|
"timestamp_nanos": timestamp_nanos,
|
||||||
|
"latitude": latitude,
|
||||||
|
"longitude": longitude,
|
||||||
|
"altitude": altitude,
|
||||||
|
"speed": speed,
|
||||||
|
"bearingDeg": heading,
|
||||||
|
"horizontalAccuracy": horizontal_accuracy,
|
||||||
|
"unixTimestampMillis": round(timestamp.timestamp() * 1000),
|
||||||
|
"verticalAccuracy": vertical_accuracy,
|
||||||
|
"bearingAccuracyDeg": bearing_accuracy,
|
||||||
|
# Velocity error units are undocumented in DBC; omit conversion.
|
||||||
|
"speedAccuracy": 0.0,
|
||||||
|
"hasFix": True,
|
||||||
|
"satelliteCount": 0,
|
||||||
|
"vNED": [speed * math.cos(heading_rad), speed * math.sin(heading_rad), 0.0],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_hard_cruise_buttons(steering_button_msg: dict) -> int:
|
def get_hard_cruise_buttons(steering_button_msg: dict) -> int:
|
||||||
return steering_button_msg.get("ACCButtonsHard", CruiseButtons.INIT)
|
return steering_button_msg.get("ACCButtonsHard", CruiseButtons.INIT)
|
||||||
@@ -109,11 +215,15 @@ class CarState(CarStateBase):
|
|||||||
self.car_gps_config = get_car_gps_config(CP)
|
self.car_gps_config = get_car_gps_config(CP)
|
||||||
self.car_gps_supported = self.car_gps_config is not None
|
self.car_gps_supported = self.car_gps_config is not None
|
||||||
self.car_gps = None
|
self.car_gps = None
|
||||||
|
self.onstar_gps = None
|
||||||
self._car_gps_timestamp_nanos = 0
|
self._car_gps_timestamp_nanos = 0
|
||||||
self._prev_gps_lat = None
|
self._prev_gps_lat = None
|
||||||
self._prev_gps_lon = None
|
self._prev_gps_lon = None
|
||||||
self._last_gps_bearing = None
|
self._last_gps_bearing = None
|
||||||
|
|
||||||
|
self.pps_gps = None
|
||||||
|
self._pps_gps_timestamp_nanos = 0
|
||||||
|
|
||||||
def _update_car_gps(self, cp, v_ego: float = 0.0) -> None:
|
def _update_car_gps(self, cp, v_ego: float = 0.0) -> None:
|
||||||
if self.car_gps_config is None:
|
if self.car_gps_config is None:
|
||||||
return
|
return
|
||||||
@@ -148,12 +258,47 @@ class CarState(CarStateBase):
|
|||||||
else:
|
else:
|
||||||
self._prev_gps_lat = self._prev_gps_lon = None
|
self._prev_gps_lat = self._prev_gps_lon = None
|
||||||
|
|
||||||
|
self.onstar_gps = gps
|
||||||
self.car_gps = gps
|
self.car_gps = gps
|
||||||
self._car_gps_timestamp_nanos = timestamp_nanos
|
self._car_gps_timestamp_nanos = timestamp_nanos
|
||||||
|
|
||||||
def get_car_gps(self):
|
def _update_pps_gps(self, cp) -> None:
|
||||||
|
"""Decode a complete, checksum-valid PPS burst when one is available."""
|
||||||
|
timestamps = [max(cp.ts_nanos[name].values(), default=0) for name in PPS_GPS_MESSAGES]
|
||||||
|
if not all(timestamps):
|
||||||
|
return
|
||||||
|
|
||||||
|
timestamp_nanos = max(timestamps)
|
||||||
|
if timestamp_nanos <= self._pps_gps_timestamp_nanos:
|
||||||
|
return
|
||||||
|
if timestamp_nanos - min(timestamps) > 100_000_000:
|
||||||
|
return
|
||||||
|
|
||||||
|
vl = cp.vl
|
||||||
|
try:
|
||||||
|
first_id = int(vl["PPS_ElevHdSpd_FO"]["PPSElvHedngSpdBrstID"])
|
||||||
|
if not (first_id == int(vl["PPS_PosLat_FO"]["PPSLatBrstID"]) ==
|
||||||
|
int(vl["PPS_PosLong_FO"]["PPSLongBrstID"]) ==
|
||||||
|
int(vl["PPS_Time_FO"]["PPSTmBrstID"]) ==
|
||||||
|
int(vl["PPS_QualMetrics_FO"]["PPSPosQltyMtcBrstID"])):
|
||||||
|
return
|
||||||
|
except (KeyError, ValueError, TypeError, OverflowError):
|
||||||
|
return
|
||||||
|
|
||||||
|
values = {name: cp.vl[name] for name in PPS_GPS_MESSAGES}
|
||||||
|
raw = {name: cp.vl_raw[name] for name in PPS_GPS_MESSAGES}
|
||||||
|
self.pps_gps = decode_gm_pps_gps(values, raw, timestamp_nanos)
|
||||||
|
self._pps_gps_timestamp_nanos = timestamp_nanos
|
||||||
|
|
||||||
|
def get_car_gps(self) -> dict | None:
|
||||||
return self.car_gps
|
return self.car_gps
|
||||||
|
|
||||||
|
def get_car_gps_sources(self) -> dict[str, dict | None]:
|
||||||
|
return {
|
||||||
|
"pps": self.pps_gps,
|
||||||
|
"onstar": self.onstar_gps,
|
||||||
|
}
|
||||||
|
|
||||||
def update_button_enable(self, buttonEvents: list[structs.CarState.ButtonEvent]):
|
def update_button_enable(self, buttonEvents: list[structs.CarState.ButtonEvent]):
|
||||||
if not self.CP.pcmCruise:
|
if not self.CP.pcmCruise:
|
||||||
for b in buttonEvents:
|
for b in buttonEvents:
|
||||||
@@ -239,6 +384,9 @@ class CarState(CarStateBase):
|
|||||||
abs(pt_cp.vl["EBCMWheelSpdRear"]["RRWheelSpd"]) <= STANDSTILL_THRESHOLD
|
abs(pt_cp.vl["EBCMWheelSpdRear"]["RRWheelSpd"]) <= STANDSTILL_THRESHOLD
|
||||||
|
|
||||||
self._update_car_gps(pt_cp, ret.vEgo)
|
self._update_car_gps(pt_cp, ret.vEgo)
|
||||||
|
pps_cp = can_parsers.get(Bus.adas)
|
||||||
|
if pps_cp is not None:
|
||||||
|
self._update_pps_gps(pps_cp)
|
||||||
|
|
||||||
if pt_cp.vl["ECMPRDNL2"]["ManualMode"] == 1:
|
if pt_cp.vl["ECMPRDNL2"]["ManualMode"] == 1:
|
||||||
ret.gearShifter = self.parse_gear_shifter("T")
|
ret.gearShifter = self.parse_gear_shifter("T")
|
||||||
@@ -588,8 +736,16 @@ class CarState(CarStateBase):
|
|||||||
("ASCMLKASteeringCmd", 0),
|
("ASCMLKASteeringCmd", 0),
|
||||||
]
|
]
|
||||||
|
|
||||||
return {
|
parsers = {
|
||||||
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, CanBus.POWERTRAIN),
|
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, CanBus.POWERTRAIN),
|
||||||
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, CanBus.CAMERA),
|
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, CanBus.CAMERA),
|
||||||
Bus.loopback: CANParser(DBC[CP.carFingerprint][Bus.pt], loopback_messages, CanBus.LOOPBACK),
|
Bus.loopback: CANParser(DBC[CP.carFingerprint][Bus.pt], loopback_messages, CanBus.LOOPBACK),
|
||||||
}
|
}
|
||||||
|
if getattr(CP, "brand", None) == "gm":
|
||||||
|
# Optional CT6 PPS parser on Bus.adas; non-PPS vehicles remain CAN-valid.
|
||||||
|
parsers[Bus.adas] = CANParser(
|
||||||
|
"cadillac_ct6_object",
|
||||||
|
[(name, 0) for name in PPS_GPS_MESSAGES],
|
||||||
|
CanBus.POWERTRAIN,
|
||||||
|
)
|
||||||
|
return parsers
|
||||||
|
|||||||
@@ -408,7 +408,7 @@ class CarInterface(CarInterfaceBase):
|
|||||||
ret.steerActuatorDelay = 0.1 # Default delay, not measured yet
|
ret.steerActuatorDelay = 0.1 # Default delay, not measured yet
|
||||||
|
|
||||||
ret.steerLimitTimer = 0.4
|
ret.steerLimitTimer = 0.4
|
||||||
ret.radarTimeStepDEPRECATED = 0.15 if candidate == CAR.BUICK_LACROSSE else 0.0667
|
ret.radarTimeStepDEPRECATED = 0.0667 # GM radar runs at 15Hz instead of the standard 20Hz
|
||||||
ret.longitudinalActuatorDelay = 0.5 # large delay to initially start braking
|
ret.longitudinalActuatorDelay = 0.5 # large delay to initially start braking
|
||||||
|
|
||||||
if candidate in (
|
if candidate in (
|
||||||
@@ -440,7 +440,7 @@ class CarInterface(CarInterfaceBase):
|
|||||||
elif candidate in (CAR.BUICK_LACROSSE, CAR.BUICK_LACROSSE_ASCM, CAR.BUICK_LACROSSE_ASCM_19US):
|
elif candidate in (CAR.BUICK_LACROSSE, CAR.BUICK_LACROSSE_ASCM, CAR.BUICK_LACROSSE_ASCM_19US):
|
||||||
CarInterfaceBase.configure_torque_tune(CAR.BUICK_LACROSSE, ret.lateralTuning)
|
CarInterfaceBase.configure_torque_tune(CAR.BUICK_LACROSSE, ret.lateralTuning)
|
||||||
if candidate == CAR.BUICK_LACROSSE_ASCM_19US:
|
if candidate == CAR.BUICK_LACROSSE_ASCM_19US:
|
||||||
ret.minSteerSpeed = 28 * CV.MPH_TO_MS
|
ret.minSteerSpeed = 27 * CV.MPH_TO_MS
|
||||||
|
|
||||||
elif candidate == CAR.CADILLAC_ESCALADE:
|
elif candidate == CAR.CADILLAC_ESCALADE:
|
||||||
ret.minEnableSpeed = -1. # engage speed is decided by pcm
|
ret.minEnableSpeed = -1. # engage speed is decided by pcm
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from datetime import UTC, datetime
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from parameterized import parameterized
|
from parameterized import parameterized
|
||||||
|
|
||||||
@@ -8,7 +9,14 @@ from opendbc.can import CANPacker, CANParser
|
|||||||
from opendbc.car import Bus, DT_CTRL, structs
|
from opendbc.car import Bus, DT_CTRL, structs
|
||||||
from opendbc.car.car_helpers import interfaces
|
from opendbc.car.car_helpers import interfaces
|
||||||
from opendbc.car.gm import gmcan
|
from opendbc.car.gm import gmcan
|
||||||
from opendbc.car.gm.carstate import CarState as GMCarState, get_hard_cruise_buttons, update_auto_hold_drive_timers
|
from opendbc.car.gm.carstate import (
|
||||||
|
CarState as GMCarState,
|
||||||
|
PPS_GPS_MESSAGES,
|
||||||
|
decode_gm_pps_gps,
|
||||||
|
get_hard_cruise_buttons,
|
||||||
|
pps_checksum_ok,
|
||||||
|
update_auto_hold_drive_timers,
|
||||||
|
)
|
||||||
from opendbc.car.gm.carcontroller import (
|
from opendbc.car.gm.carcontroller import (
|
||||||
VisualAlert,
|
VisualAlert,
|
||||||
get_acc_dashboard_always_one,
|
get_acc_dashboard_always_one,
|
||||||
@@ -95,6 +103,146 @@ class TestBoltGps:
|
|||||||
assert gps["verticalAccuracy"] == 10.0
|
assert gps["verticalAccuracy"] == 10.0
|
||||||
assert gps["speedAccuracy"] == 0.5
|
assert gps["speedAccuracy"] == 0.5
|
||||||
|
|
||||||
|
|
||||||
|
class TestPpsGps:
|
||||||
|
_frames = [
|
||||||
|
(0x260, bytes.fromhex("10ddac000d831277"), 0),
|
||||||
|
(0x261, bytes.fromhex("08386fce09ca"), 0),
|
||||||
|
(0x262, bytes.fromhex("6d98820341de"), 0),
|
||||||
|
(0x264, bytes.fromhex("0018f90578b5eaac"), 0),
|
||||||
|
(0x265, bytes.fromhex("1a0000800a0258fd"), 0),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_observed_bundle_checksum_and_conversion(self):
|
||||||
|
parser = CANParser("cadillac_ct6_object", [(name, 0) for name in PPS_GPS_MESSAGES], 0)
|
||||||
|
parser.update([(1_000_000_000, self._frames)])
|
||||||
|
|
||||||
|
assert all(pps_checksum_ok(parser.vl_raw[name]) for name in PPS_GPS_MESSAGES)
|
||||||
|
gps = decode_gm_pps_gps(
|
||||||
|
{name: parser.vl[name] for name in PPS_GPS_MESSAGES},
|
||||||
|
{name: parser.vl_raw[name] for name in PPS_GPS_MESSAGES},
|
||||||
|
1_000_000_000,
|
||||||
|
)
|
||||||
|
assert gps is not None
|
||||||
|
assert gps["hasFix"]
|
||||||
|
assert gps["latitude"] == pytest.approx(38.3101, abs=1e-4)
|
||||||
|
assert gps["longitude"] == pytest.approx(-85.7701, abs=1e-4)
|
||||||
|
assert gps["altitude"] == pytest.approx(106.9)
|
||||||
|
assert gps["bearingDeg"] == pytest.approx(56.748)
|
||||||
|
assert gps["horizontalAccuracy"] == pytest.approx(1.0)
|
||||||
|
assert gps["unixTimestampMillis"] == 1788655668655
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def bundle(self):
|
||||||
|
parser = CANParser("cadillac_ct6_object", [(name, 0) for name in PPS_GPS_MESSAGES], 0)
|
||||||
|
parser.update([(1_000_000_000, self._frames)])
|
||||||
|
return ({name: dict(parser.vl[name]) for name in PPS_GPS_MESSAGES},
|
||||||
|
{name: parser.vl_raw[name] for name in PPS_GPS_MESSAGES})
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("year,day,date", [
|
||||||
|
(2025, 1, "2025-01-01"), (2025, 365, "2025-12-31"), (2025, 366, None),
|
||||||
|
(2024, 366, "2024-12-31"), (2024, 367, None), (2025, 0, None),
|
||||||
|
])
|
||||||
|
def test_one_based_day_of_year(self, bundle, year, day, date):
|
||||||
|
values, raw = bundle
|
||||||
|
values["PPS_Time_FO"].update(PPSCldrYr=year, PPSCldrDay=day, PPSTmday=1234)
|
||||||
|
gps = decode_gm_pps_gps(values, raw, 1_000_000_000)
|
||||||
|
if date is None:
|
||||||
|
assert gps is None
|
||||||
|
else:
|
||||||
|
expected = int(datetime.fromisoformat(date).replace(tzinfo=UTC).timestamp() * 1000) + 1234
|
||||||
|
assert gps["unixTimestampMillis"] == expected
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bad_data", [b"", b"\x01"])
|
||||||
|
def test_checksum_short_input(self, bad_data):
|
||||||
|
assert not pps_checksum_ok(bad_data)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("lat,lon,valid", [(0.0, 10.0 * 3_600_000, True), (10.0 * 3_600_000, 0.0, True), (0.0, 0.0, False)])
|
||||||
|
def test_coordinate_axes(self, bundle, lat, lon, valid):
|
||||||
|
values, raw = bundle
|
||||||
|
values["PPS_PosLat_FO"]["PPSLat"] = lat
|
||||||
|
values["PPS_PosLong_FO"]["PPSLong"] = lon
|
||||||
|
gps = decode_gm_pps_gps(values, raw, 1_000_000_000)
|
||||||
|
if valid:
|
||||||
|
assert gps is not None
|
||||||
|
assert gps["hasFix"]
|
||||||
|
else:
|
||||||
|
assert gps is None
|
||||||
|
|
||||||
|
def test_get_car_gps_sources_shape(self):
|
||||||
|
cs = GMCarState.__new__(GMCarState)
|
||||||
|
cs.pps_gps = {"hasFix": True}
|
||||||
|
cs.onstar_gps = None
|
||||||
|
sources = cs.get_car_gps_sources()
|
||||||
|
assert sources == {"pps": {"hasFix": True}, "onstar": None}
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("message,signal,value", [
|
||||||
|
("PPS_PosLat_FO", "PPSLatV", 1),
|
||||||
|
("PPS_PosLong_FO", "PPSLongV", 1),
|
||||||
|
("PPS_QualMetrics_FO", "PPS2DAbsPosErrEstmtV", 1),
|
||||||
|
("PPS_PosLat_FO", "PPSLat", float("nan")),
|
||||||
|
("PPS_PosLong_FO", "PPSLong", 181 * 3_600_000),
|
||||||
|
("PPS_QualMetrics_FO", "PPSMd", 6),
|
||||||
|
("PPS_Time_FO", "PPSTmdayV", 1),
|
||||||
|
])
|
||||||
|
def test_unusable_position_rejected(self, bundle, message, signal, value):
|
||||||
|
values, raw = bundle
|
||||||
|
values[message][signal] = value
|
||||||
|
assert decode_gm_pps_gps(values, raw, 1_000_000_000) is None
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("invalidity", ["checksum", "position-validity"])
|
||||||
|
def test_burst_cache_and_explicit_invalidation(self, invalidity):
|
||||||
|
parser = CANParser("cadillac_ct6_object", [(name, 0) for name in PPS_GPS_MESSAGES], 0)
|
||||||
|
cs = GMCarState.__new__(GMCarState)
|
||||||
|
cs.pps_gps = None
|
||||||
|
cs._pps_gps_timestamp_nanos = 0
|
||||||
|
parser.update([(1_000_000_000, self._frames[:-1])])
|
||||||
|
cs._update_pps_gps(parser)
|
||||||
|
assert cs.pps_gps is None # Incomplete startup burst.
|
||||||
|
parser.update([(1_000_000_000, self._frames[-1:])])
|
||||||
|
cs._update_pps_gps(parser)
|
||||||
|
good = cs.pps_gps
|
||||||
|
assert good is not None
|
||||||
|
# No complete new burst: keep its original timestamp for freshness.
|
||||||
|
parser.update([(2_000_000_000, self._frames[:1])])
|
||||||
|
cs._update_pps_gps(parser)
|
||||||
|
assert cs.pps_gps is good
|
||||||
|
parser.update([(2_100_000_000, self._frames)])
|
||||||
|
parser.vl["PPS_PosLat_FO"]["PPSLatBrstID"] = int(parser.vl["PPS_PosLat_FO"]["PPSLatBrstID"]) ^ 1
|
||||||
|
cs._update_pps_gps(parser)
|
||||||
|
assert cs.pps_gps is good # A mismatched burst must not refresh the fix.
|
||||||
|
bad_frames = ([(addr, data[:-1] + bytes([data[-1] ^ 1]), bus) for addr, data, bus in self._frames]
|
||||||
|
if invalidity == "checksum" else self._frames)
|
||||||
|
parser.update([(3_000_000_000, bad_frames)])
|
||||||
|
if invalidity == "position-validity":
|
||||||
|
parser.vl["PPS_PosLat_FO"]["PPSLatV"] = 1
|
||||||
|
cs._update_pps_gps(parser)
|
||||||
|
assert cs.pps_gps is None
|
||||||
|
parser.update([(4_000_000_000, self._frames)])
|
||||||
|
cs._update_pps_gps(parser)
|
||||||
|
assert cs.pps_gps is not None
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("speed_bad,heading_bad,elevation_bad,vertical_bad,bearing_bad", [
|
||||||
|
(0, 0, 0, 0, 0), (1, 0, 0, 0, 0), (0, 1, 0, 0, 0), (0, 0, 1, 0, 0),
|
||||||
|
(0, 0, 0, 1, 0), (0, 0, 0, 0, 1), (1, 1, 1, 1, 1),
|
||||||
|
], ids=["valid", "speed", "heading", "elevation", "vertical-accuracy", "bearing-accuracy", "all-invalid"])
|
||||||
|
def test_optional_field_fallbacks(self, bundle, speed_bad, heading_bad, elevation_bad, vertical_bad, bearing_bad):
|
||||||
|
values, raw = bundle
|
||||||
|
values["PPS_ElevHdSpd_FO"].update(PPSVel=36, PPSVelV=speed_bad, PPSHedng=90, PPSHedngV=heading_bad,
|
||||||
|
PPSElvtn=12345, PPSElvtnV=elevation_bad)
|
||||||
|
values["PPS_QualMetrics_FO"].update(PPS2DAbsPosErrEstmt=3.2, PPS3DAbsPosErrEstmt=4.5, PPS3DAbsPosErrEstmtV=vertical_bad,
|
||||||
|
PPSAbsHdngErrEstmt=6.0, PPSAbsHdngErrEstmtV=bearing_bad)
|
||||||
|
gps = decode_gm_pps_gps(values, raw, 1_000_000_000)
|
||||||
|
assert gps is not None
|
||||||
|
assert gps["hasFix"]
|
||||||
|
assert gps["speed"] == pytest.approx(0.0 if speed_bad else 10.0)
|
||||||
|
assert gps["bearingDeg"] == (0 if heading_bad else 90)
|
||||||
|
assert gps["altitude"] == pytest.approx(0.0 if elevation_bad else 123.45)
|
||||||
|
assert gps["vNED"] == pytest.approx([gps["speed"], 0, 0] if heading_bad else [0, gps["speed"], 0])
|
||||||
|
assert gps["horizontalAccuracy"] == pytest.approx(3.2)
|
||||||
|
assert gps["verticalAccuracy"] == pytest.approx(0.0 if vertical_bad else 4.5)
|
||||||
|
assert gps["bearingAccuracyDeg"] == pytest.approx(180.0 if bearing_bad else 6.0)
|
||||||
|
|
||||||
def test_bolt_gps_heading_and_speed_derivation(self):
|
def test_bolt_gps_heading_and_speed_derivation(self):
|
||||||
cp = SimpleNamespace(
|
cp = SimpleNamespace(
|
||||||
brand="gm",
|
brand="gm",
|
||||||
@@ -205,33 +353,6 @@ class TestBoltGps:
|
|||||||
|
|
||||||
|
|
||||||
class TestGMInterface:
|
class TestGMInterface:
|
||||||
def test_lacrosse_obd_and_ascm_integrations_remain_separate(self):
|
|
||||||
obd_params = interfaces[CAR.BUICK_LACROSSE].get_params(
|
|
||||||
CAR.BUICK_LACROSSE,
|
|
||||||
_empty_fingerprint(),
|
|
||||||
[],
|
|
||||||
alpha_long=False,
|
|
||||||
is_release=False,
|
|
||||||
docs=False,
|
|
||||||
starpilot_toggles=_test_starpilot_toggles(),
|
|
||||||
)
|
|
||||||
ascm_params = interfaces[CAR.BUICK_LACROSSE_ASCM].get_params(
|
|
||||||
CAR.BUICK_LACROSSE_ASCM,
|
|
||||||
_empty_fingerprint(),
|
|
||||||
[],
|
|
||||||
alpha_long=False,
|
|
||||||
is_release=False,
|
|
||||||
docs=False,
|
|
||||||
starpilot_toggles=_test_starpilot_toggles(),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert obd_params.networkLocation == structs.CarParams.NetworkLocation.gateway
|
|
||||||
assert obd_params.openpilotLongitudinalControl
|
|
||||||
assert obd_params.radarTimeStepDEPRECATED == pytest.approx(0.15)
|
|
||||||
assert ascm_params.networkLocation == structs.CarParams.NetworkLocation.fwdCamera
|
|
||||||
assert not ascm_params.openpilotLongitudinalControl
|
|
||||||
assert ascm_params.radarTimeStepDEPRECATED == pytest.approx(0.0667)
|
|
||||||
|
|
||||||
@parameterized.expand([
|
@parameterized.expand([
|
||||||
CAR.CHEVROLET_BOLT_CC_2017,
|
CAR.CHEVROLET_BOLT_CC_2017,
|
||||||
CAR.CHEVROLET_BOLT_CC_2018_2021,
|
CAR.CHEVROLET_BOLT_CC_2018_2021,
|
||||||
@@ -318,14 +439,6 @@ class TestGMInterface:
|
|||||||
|
|
||||||
assert car_params.minSteerSpeed == pytest.approx(7 * CV.MPH_TO_MS)
|
assert car_params.minSteerSpeed == pytest.approx(7 * CV.MPH_TO_MS)
|
||||||
|
|
||||||
def test_lacrosse_2019_ascm_min_steer_speed_is_28_mph(self):
|
|
||||||
car_model = CAR.BUICK_LACROSSE_ASCM_19US
|
|
||||||
CarInterface = interfaces[car_model]
|
|
||||||
car_params = CarInterface.get_params(car_model, _empty_fingerprint(), [], alpha_long=False, is_release=False, docs=False,
|
|
||||||
starpilot_toggles=_test_starpilot_toggles())
|
|
||||||
|
|
||||||
assert car_params.minSteerSpeed == pytest.approx(28 * CV.MPH_TO_MS)
|
|
||||||
|
|
||||||
@parameterized.expand([
|
@parameterized.expand([
|
||||||
("interceptor", True),
|
("interceptor", True),
|
||||||
("ascm_int", False),
|
("ascm_int", False),
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ from collections.abc import Callable, Mapping
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from opendbc.car.common.conversions import Conversions as CV
|
from opendbc.car.common.conversions import Conversions as CV
|
||||||
|
from opendbc.can.dbc import DBC as DBC_FILE
|
||||||
|
from opendbc.car import Bus
|
||||||
from opendbc.car.ford.values import CAR as FORD_CAR
|
from opendbc.car.ford.values import CAR as FORD_CAR
|
||||||
from opendbc.car.gm.values import CAR as GM_CAR
|
from opendbc.car.gm.values import CAR as GM_CAR, DBC as GM_DBC
|
||||||
|
|
||||||
|
|
||||||
CarGpsSample = dict[str, Any]
|
CarGpsSample = dict[str, Any]
|
||||||
@@ -158,8 +160,25 @@ CAR_GPS_CONFIGS: dict[str, CarGpsConfig] = {
|
|||||||
|
|
||||||
|
|
||||||
def get_car_gps_config(CP) -> CarGpsConfig | None:
|
def get_car_gps_config(CP) -> CarGpsConfig | None:
|
||||||
|
cp_brand = getattr(CP, "brand", None)
|
||||||
config = CAR_GPS_CONFIGS.get(CP.carFingerprint)
|
config = CAR_GPS_CONFIGS.get(CP.carFingerprint)
|
||||||
return config if config is not None and config.brand == CP.brand else None
|
if config is not None and config.brand == cp_brand:
|
||||||
|
return config
|
||||||
|
|
||||||
|
# Enable OnStar GPS for GM cars whose powertrain DBC defines it.
|
||||||
|
if cp_brand == "gm":
|
||||||
|
try:
|
||||||
|
dbc_name = GM_DBC[CP.carFingerprint][Bus.pt]
|
||||||
|
if "TCICOnStarGPSPosition" in DBC_FILE(dbc_name).name_to_msg:
|
||||||
|
return CarGpsConfig(
|
||||||
|
brand="gm",
|
||||||
|
messages=CHEVROLET_BOLT_GPS_MESSAGES,
|
||||||
|
decoder=parse_chevrolet_bolt_can_gps,
|
||||||
|
)
|
||||||
|
except (KeyError, OSError, TypeError, RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def car_gps_available(CP) -> bool:
|
def car_gps_available(CP) -> bool:
|
||||||
|
|||||||
@@ -860,9 +860,7 @@ class CarController(CarControllerBase):
|
|||||||
|
|
||||||
lka_steering = self.CP.flags & HyundaiFlags.CANFD_LKA_STEERING
|
lka_steering = self.CP.flags & HyundaiFlags.CANFD_LKA_STEERING
|
||||||
longitudinal_active = bool(self.long_active_ecu and getattr(CC, "longActive", False))
|
longitudinal_active = bool(self.long_active_ecu and getattr(CC, "longActive", False))
|
||||||
lfa_status_cars = (CAR.HYUNDAI_IONIQ_6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN)
|
lfa_longitudinal_active = longitudinal_active if self.CP.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN else self.CP.openpilotLongitudinalControl
|
||||||
lfa_longitudinal_active = self.CP.openpilotLongitudinalControl \
|
|
||||||
if self.CP.carFingerprint in lfa_status_cars else longitudinal_active
|
|
||||||
lka_steering_long = lka_steering and lfa_longitudinal_active
|
lka_steering_long = lka_steering and lfa_longitudinal_active
|
||||||
ccnc_non_hda2 = self.CP.flags & HyundaiFlags.CCNC and not lka_steering
|
ccnc_non_hda2 = self.CP.flags & HyundaiFlags.CCNC and not lka_steering
|
||||||
use_egmp_dynamic_long_tuning = egmp_dynamic_longitudinal_tuning(self.CP) and self.long_active_ecu and \
|
use_egmp_dynamic_long_tuning = egmp_dynamic_longitudinal_tuning(self.CP) and self.long_active_ecu and \
|
||||||
@@ -892,8 +890,7 @@ class CarController(CarControllerBase):
|
|||||||
if angle_lkas_alt:
|
if angle_lkas_alt:
|
||||||
steering_msg_active = bool(steering_msg_active and drive_gear)
|
steering_msg_active = bool(steering_msg_active and drive_gear)
|
||||||
angle_lkas_alt_standstill_handoff = bool(getattr(CS.out, "standstill", False) and not CC.latActive)
|
angle_lkas_alt_standstill_handoff = bool(getattr(CS.out, "standstill", False) and not CC.latActive)
|
||||||
forward_stock_lkas = (self.CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR or
|
forward_stock_lkas = self.CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR and angle_lkas_alt and (
|
||||||
self.CP.carFingerprint == CAR.KIA_SPORTAGE_HEV_2026) and angle_lkas_alt and (
|
|
||||||
angle_lkas_alt_standstill_handoff or not (drive_gear and (CC.latActive or CC.enabled))
|
angle_lkas_alt_standstill_handoff or not (drive_gear and (CC.latActive or CC.enabled))
|
||||||
)
|
)
|
||||||
preserve_stock_lfa_status = preserve_stock_canfd_lfa_status(self.CP.carFingerprint)
|
preserve_stock_lfa_status = preserve_stock_canfd_lfa_status(self.CP.carFingerprint)
|
||||||
|
|||||||
@@ -2484,11 +2484,10 @@ class TestHyundaiFingerprint:
|
|||||||
CP = CarParams.new_message()
|
CP = CarParams.new_message()
|
||||||
CP.carFingerprint = CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN
|
CP.carFingerprint = CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN
|
||||||
CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEERING)
|
CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEERING)
|
||||||
CP.openpilotLongitudinalControl = True
|
CP.openpilotLongitudinalControl = False
|
||||||
|
|
||||||
controller = CarController(DBC[CP.carFingerprint], CP)
|
controller = CarController(DBC[CP.carFingerprint], CP)
|
||||||
controller.frame = 1
|
controller.frame = 1
|
||||||
controller.long_active_ecu = True
|
|
||||||
can_bus = CanBus(CP)
|
can_bus = CanBus(CP)
|
||||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS", 0)], can_bus.ACAN)
|
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS", 0)], can_bus.ACAN)
|
||||||
stock_lkas = {
|
stock_lkas = {
|
||||||
@@ -2530,6 +2529,7 @@ class TestHyundaiFingerprint:
|
|||||||
assert parser.vl["LKAS"]["STEER_MODE"] == 0
|
assert parser.vl["LKAS"]["STEER_MODE"] == 0
|
||||||
assert parser.vl["LKAS"]["NEW_SIGNAL_2"] == 0
|
assert parser.vl["LKAS"]["NEW_SIGNAL_2"] == 0
|
||||||
|
|
||||||
|
CP.openpilotLongitudinalControl = True
|
||||||
lfa_parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LFA", 0)], can_bus.ECAN)
|
lfa_parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LFA", 0)], can_bus.ECAN)
|
||||||
lfa_msgs = hyundaicanfd.create_steering_messages(controller.packer, CP, can_bus, True, True, 0, 0.0)
|
lfa_msgs = hyundaicanfd.create_steering_messages(controller.packer, CP, can_bus, True, True, 0, 0.0)
|
||||||
assert [(controller.packer.dbc.addr_to_msg[addr].name, bus) for addr, _, bus in lfa_msgs] == [("LFA", can_bus.ECAN), ("LKAS", can_bus.ACAN)]
|
assert [(controller.packer.dbc.addr_to_msg[addr].name, bus) for addr, _, bus in lfa_msgs] == [("LFA", can_bus.ECAN), ("LKAS", can_bus.ACAN)]
|
||||||
@@ -2537,12 +2537,13 @@ class TestHyundaiFingerprint:
|
|||||||
assert lfa_parser.can_valid
|
assert lfa_parser.can_valid
|
||||||
assert lfa_parser.vl["LFA"]["DAMP_FACTOR"] == 100
|
assert lfa_parser.vl["LFA"]["DAMP_FACTOR"] == 100
|
||||||
|
|
||||||
|
controller.long_active_ecu = True
|
||||||
cc.longActive = False
|
cc.longActive = False
|
||||||
inactive_msgs = controller.create_canfd_msgs(0, True, 0.44, 0.0, 0.0, 0.0, False,
|
inactive_msgs = controller.create_canfd_msgs(0, True, 0.44, 0.0, 0.0, 0.0, False,
|
||||||
cc.hudControl, cs, cc, get_test_toggles(), lka_icon=2, lfa_icon=2)
|
cc.hudControl, cs, cc, get_test_toggles(), lka_icon=2, lfa_icon=2)
|
||||||
steering_names = [(controller.packer.dbc.addr_to_msg[addr].name, bus) for addr, _, bus in inactive_msgs
|
steering_names = [(controller.packer.dbc.addr_to_msg[addr].name, bus) for addr, _, bus in inactive_msgs
|
||||||
if controller.packer.dbc.addr_to_msg[addr].name in ("LFA", "LKAS")]
|
if controller.packer.dbc.addr_to_msg[addr].name in ("LFA", "LKAS")]
|
||||||
assert steering_names == [("LFA", can_bus.ECAN), ("LKAS", can_bus.ACAN)]
|
assert steering_names == [("LKAS", can_bus.ACAN)]
|
||||||
|
|
||||||
controller.frame = 1
|
controller.frame = 1
|
||||||
cc.longActive = True
|
cc.longActive = True
|
||||||
@@ -2707,7 +2708,7 @@ class TestHyundaiFingerprint:
|
|||||||
assert len([msg for msg in msgs if msg[0] == 0x110]) == expected_lkas_msgs
|
assert len([msg for msg in msgs if msg[0] == 0x110]) == expected_lkas_msgs
|
||||||
|
|
||||||
@pytest.mark.parametrize("standstill", [False, True])
|
@pytest.mark.parametrize("standstill", [False, True])
|
||||||
def test_sportage_angle_lkas_alt_forwards_stock_status_when_inactive(self, standstill):
|
def test_sportage_angle_lkas_alt_keeps_inactive_status_in_drive(self, standstill):
|
||||||
CP = CarParams.new_message()
|
CP = CarParams.new_message()
|
||||||
CP.carFingerprint = CAR.KIA_SPORTAGE_HEV_2026
|
CP.carFingerprint = CAR.KIA_SPORTAGE_HEV_2026
|
||||||
CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.HYBRID | HyundaiFlags.CANFD_ANGLE_STEERING |
|
CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.HYBRID | HyundaiFlags.CANFD_ANGLE_STEERING |
|
||||||
@@ -2715,16 +2716,60 @@ class TestHyundaiFingerprint:
|
|||||||
CP.openpilotLongitudinalControl = False
|
CP.openpilotLongitudinalControl = False
|
||||||
|
|
||||||
controller = CarController(DBC[CP.carFingerprint], CP)
|
controller = CarController(DBC[CP.carFingerprint], CP)
|
||||||
|
can_bus = CanBus(CP)
|
||||||
|
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS_ALT", 0)], can_bus.ACAN)
|
||||||
|
stock_lkas = {
|
||||||
|
"CHECKSUM": 1234,
|
||||||
|
"COUNTER": 42,
|
||||||
|
"LKA_OptUsmSta": 2,
|
||||||
|
"LKA_MODE": 2,
|
||||||
|
"LKA_RcgSta": 3,
|
||||||
|
"LKA_AVAILABLE": 3,
|
||||||
|
"LKA_LHLnWrnSta": 3,
|
||||||
|
"LKA_RHLnWrnSta": 3,
|
||||||
|
"LKA_WARNING": 1,
|
||||||
|
"LKA_HndsoffSnd": 1,
|
||||||
|
"LKA_StrSnd": 1,
|
||||||
|
"LKA_SysIndReq": 4,
|
||||||
|
"LKA_ICON": 2,
|
||||||
|
"FCA_SYSWARN": 1,
|
||||||
|
"StrTqReqVal": 17,
|
||||||
|
"TORQUE_REQUEST": 17,
|
||||||
|
"ActToiSta": 3,
|
||||||
|
"STEER_REQ": 1,
|
||||||
|
"ToiFltSta": 3,
|
||||||
|
"LFA_BUTTON": 1,
|
||||||
|
"LKA_SysWrn": 15,
|
||||||
|
"LKA_ASSIST": 1,
|
||||||
|
"Damping_Gain": 0,
|
||||||
|
"STEER_MODE": 5,
|
||||||
|
"NEW_SIGNAL_2": 0,
|
||||||
|
"LKAS_ANGLE_ACTIVE": 2,
|
||||||
|
"LKA_UsmMod": 3,
|
||||||
|
"HAS_LANE_SAFETY": 1,
|
||||||
|
"ADAS_StrAnglReqVal": 12.3,
|
||||||
|
"ADAS_ACIAnglTqRedcGainVal": 0.42,
|
||||||
|
"DAMP_FACTOR": 0,
|
||||||
|
}
|
||||||
cc = SimpleNamespace(enabled=False, latActive=False,
|
cc = SimpleNamespace(enabled=False, latActive=False,
|
||||||
actuators=SimpleNamespace(longControlState=LongCtrlState.off),
|
actuators=SimpleNamespace(longControlState=LongCtrlState.off),
|
||||||
leftBlinker=False, rightBlinker=False, hudControl=SimpleNamespace())
|
leftBlinker=False, rightBlinker=False, hudControl=SimpleNamespace())
|
||||||
cs = SimpleNamespace(stock_lfa_msg=None, stock_lkas_msg={},
|
cs = SimpleNamespace(stock_lfa_msg=None, stock_lkas_msg=stock_lkas,
|
||||||
out=SimpleNamespace(standstill=standstill, steeringAngleDeg=0.0,
|
out=SimpleNamespace(standstill=standstill, steeringAngleDeg=0.0,
|
||||||
gearShifter=structs.CarState.GearShifter.drive))
|
gearShifter=structs.CarState.GearShifter.drive))
|
||||||
|
|
||||||
msgs = controller.create_canfd_msgs(0, False, 0.0, 0.0, 0.0, 0.0, False, cc.hudControl, cs, cc,
|
msgs = controller.create_canfd_msgs(0, False, 0.0, 0.0, 0.0, 0.0, False, cc.hudControl, cs, cc,
|
||||||
get_test_toggles(), lka_icon=1, lfa_icon=1)
|
get_test_toggles(), lka_icon=1, lfa_icon=1)
|
||||||
assert not [msg for msg in msgs if msg[0] in (0x110, 0x12A)]
|
lkas_msgs = [msg for msg in msgs if msg[0] == 0x110]
|
||||||
|
assert len(lkas_msgs) == 1
|
||||||
|
|
||||||
|
parser.update([(1, lkas_msgs)])
|
||||||
|
assert parser.can_valid
|
||||||
|
assert parser.vl["LKAS_ALT"]["LKA_StrSnd"] == 2
|
||||||
|
assert parser.vl["LKAS_ALT"]["LKA_SysIndReq"] == 1
|
||||||
|
assert parser.vl["LKAS_ALT"]["LKA_RcgSta"] == 0
|
||||||
|
assert parser.vl["LKAS_ALT"]["LKA_AVAILABLE"] == 0
|
||||||
|
assert parser.vl["LKAS_ALT"]["LKAS_ANGLE_ACTIVE"] == 1
|
||||||
|
|
||||||
def test_ev9_inactive_angle_steering_does_not_suppress_stock_lfa(self):
|
def test_ev9_inactive_angle_steering_does_not_suppress_stock_lfa(self):
|
||||||
CP = CarParams.new_message()
|
CP = CarParams.new_message()
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class CarController(CarControllerBase):
|
|||||||
self.angle_bus = CanBus.angle_for_cp(CP)
|
self.angle_bus = CanBus.angle_for_cp(CP)
|
||||||
self.status_bus = CanBus.camera if CP.flags & SubaruFlags.D_PLATFORM_CAMERA else CanBus.main
|
self.status_bus = CanBus.camera if CP.flags & SubaruFlags.D_PLATFORM_CAMERA else CanBus.main
|
||||||
|
|
||||||
if CP.flags & SubaruFlags.LKAS_ANGLE and CP.carFingerprint != CAR.SUBARU_OUTBACK_2023:
|
if CP.flags & SubaruFlags.LKAS_ANGLE:
|
||||||
self.VM = VehicleModel(get_safety_CP())
|
self.VM = VehicleModel(get_safety_CP())
|
||||||
|
|
||||||
self.prev_close_distance = 0
|
self.prev_close_distance = 0
|
||||||
@@ -332,7 +332,7 @@ class CarController(CarControllerBase):
|
|||||||
self.apply_steer_last = CS.out.steeringAngleDeg
|
self.apply_steer_last = CS.out.steeringAngleDeg
|
||||||
|
|
||||||
steer_target = self._angle_reclaim_target(CC.actuators.steeringAngleDeg) if lkas_active else CC.actuators.steeringAngleDeg
|
steer_target = self._angle_reclaim_target(CC.actuators.steeringAngleDeg) if lkas_active else CC.actuators.steeringAngleDeg
|
||||||
if self.CP.carFingerprint in (CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023):
|
if self.CP.carFingerprint == CAR.SUBARU_ASCENT_2023:
|
||||||
apply_steer = apply_std_steer_angle_limits(
|
apply_steer = apply_std_steer_angle_limits(
|
||||||
steer_target,
|
steer_target,
|
||||||
self.apply_steer_last,
|
self.apply_steer_last,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class CarInterface(CarInterfaceBase):
|
|||||||
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.D_PLATFORM_CAMERA.value
|
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.D_PLATFORM_CAMERA.value
|
||||||
if candidate in SUBARU_STOP_START_CARS:
|
if candidate in SUBARU_STOP_START_CARS:
|
||||||
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.STOP_START_BUTTON.value
|
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.STOP_START_BUTTON.value
|
||||||
if candidate in (CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023):
|
if candidate in (CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023):
|
||||||
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.FIXED_ANGLE_LIMITS.value
|
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.FIXED_ANGLE_LIMITS.value
|
||||||
|
|
||||||
ret.steerLimitTimer = 0.4
|
ret.steerLimitTimer = 0.4
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ def test_outback_2023_uses_d_platform_bus_layout():
|
|||||||
assert CP.flags & SubaruFlags.D_PLATFORM
|
assert CP.flags & SubaruFlags.D_PLATFORM
|
||||||
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.D_PLATFORM
|
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.D_PLATFORM
|
||||||
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.STOP_START_BUTTON
|
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.STOP_START_BUTTON
|
||||||
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.FIXED_ANGLE_LIMITS
|
assert not (CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.LEGACY_2025_ANGLE_LIMITS)
|
||||||
assert CanBus.main_for_cp(CP) == CanBus.alt
|
assert CanBus.main_for_cp(CP) == CanBus.alt
|
||||||
assert CanBus.angle_for_cp(CP) == CanBus.main
|
assert CanBus.angle_for_cp(CP) == CanBus.main
|
||||||
assert parsers[Bus.pt].bus == CanBus.alt
|
assert parsers[Bus.pt].bus == CanBus.alt
|
||||||
@@ -622,9 +622,8 @@ def test_angle_controller_blocks_low_speed_mads_engagement():
|
|||||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1
|
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("platform", (CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023))
|
def test_ascent_angle_controller_uses_fixed_angle_rate_limits():
|
||||||
def test_angle_controller_uses_fixed_angle_rate_limits(platform):
|
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
|
||||||
CP = CarInterface.get_non_essential_params(platform)
|
|
||||||
controller = CarController({}, CP)
|
controller = CarController({}, CP)
|
||||||
CC = SimpleNamespace(enabled=True, latActive=True, actuators=SimpleNamespace(steeringAngleDeg=-14.88))
|
CC = SimpleNamespace(enabled=True, latActive=True, actuators=SimpleNamespace(steeringAngleDeg=-14.88))
|
||||||
CS = SimpleNamespace(out=SimpleNamespace(
|
CS = SimpleNamespace(out=SimpleNamespace(
|
||||||
|
|||||||
@@ -417,18 +417,6 @@ class TestSubaruDPlatformAngleSafety(TestSubaruStockLongitudinalSafetyBase, Test
|
|||||||
return self.packer.make_can_msg_safety("Steering_2", SUBARU_MAIN_BUS, {"Steering_Angle": angle})
|
return self.packer.make_can_msg_safety("Steering_2", SUBARU_MAIN_BUS, {"Steering_Angle": angle})
|
||||||
|
|
||||||
|
|
||||||
class TestSubaruDPlatformFixedAngleSafety(TestSubaruDPlatformAngleSafety):
|
|
||||||
FLAGS = SubaruSafetyFlags.GEN2 | SubaruSafetyFlags.LKAS_ANGLE | SubaruSafetyFlags.D_PLATFORM | \
|
|
||||||
SubaruSafetyFlags.FIXED_ANGLE_LIMITS
|
|
||||||
STEER_ANGLE_MAX = 545
|
|
||||||
ANGLE_RATE_BP = [0., 5., 35.]
|
|
||||||
ANGLE_RATE_UP = [5., .8, .15]
|
|
||||||
ANGLE_RATE_DOWN = [5., .8, .15]
|
|
||||||
|
|
||||||
def test_rt_limits(self):
|
|
||||||
raise unittest.SkipTest("Breakpoint angle limits do not enforce a real-time message frequency")
|
|
||||||
|
|
||||||
|
|
||||||
class TestSubaruDPlatformStopStartSafety(TestSubaruDPlatformAngleSafety):
|
class TestSubaruDPlatformStopStartSafety(TestSubaruDPlatformAngleSafety):
|
||||||
FLAGS = SubaruSafetyFlags.GEN2 | SubaruSafetyFlags.LKAS_ANGLE | SubaruSafetyFlags.D_PLATFORM | \
|
FLAGS = SubaruSafetyFlags.GEN2 | SubaruSafetyFlags.LKAS_ANGLE | SubaruSafetyFlags.D_PLATFORM | \
|
||||||
SubaruSafetyFlags.STOP_START_BUTTON
|
SubaruSafetyFlags.STOP_START_BUTTON
|
||||||
|
|||||||
@@ -284,22 +284,32 @@ ensure_host_python_extensions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sync_host_generated_headers() {
|
sync_host_generated_headers() {
|
||||||
local capnpc="${ROOT_DIR}/.venv/bin/capnpc"
|
local capnp_bin=""
|
||||||
local capnpc_cpp
|
local candidate=""
|
||||||
capnpc_cpp="$(find "${ROOT_DIR}/.venv/lib" -path '*/capnproto/install/bin/capnpc-c++' -type f -print -quit)"
|
for candidate in "${HOST_VENV}"/lib/python*/site-packages/capnproto/install/bin; do
|
||||||
if [[ ! -x "${capnpc}" || ! -x "${capnpc_cpp}" ]]; then
|
if [[ -x "${candidate}/capnpc" ]]; then
|
||||||
|
capnp_bin="${candidate}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
local capnpc_cmd="capnpc"
|
||||||
|
if [[ -n "${capnp_bin}" ]]; then
|
||||||
|
capnpc_cmd="${capnp_bin}/capnpc"
|
||||||
|
export PATH="${capnp_bin}:${PATH}"
|
||||||
|
elif ! command -v capnpc >/dev/null 2>&1; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
(
|
(
|
||||||
cd "${WORK_DIR}"
|
cd "${WORK_DIR}"
|
||||||
mkdir -p cereal/gen/cpp
|
mkdir -p cereal/gen/cpp
|
||||||
"${capnpc}" --src-prefix=cereal \
|
"${capnpc_cmd}" --src-prefix=cereal \
|
||||||
cereal/log.capnp \
|
cereal/log.capnp \
|
||||||
cereal/car.capnp \
|
cereal/car.capnp \
|
||||||
cereal/legacy.capnp \
|
cereal/legacy.capnp \
|
||||||
cereal/custom.capnp \
|
cereal/custom.capnp \
|
||||||
-o "${capnpc_cpp}:cereal/gen/cpp/"
|
-o c++:cereal/gen/cpp/
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ def build_compile_env(*, supercombo: bool = False) -> dict[str, str]:
|
|||||||
int(str(env.get(key)), 0)
|
int(str(env.get(key)), 0)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
env[key] = default
|
env[key] = default
|
||||||
|
if supercombo:
|
||||||
|
# Unified supercombo artifacts must use upstream compile defaults. The
|
||||||
|
# legacy QCOM tuning causes a reproducible HCQ timeline failure here.
|
||||||
|
env.pop("QCOM_PRIORITY", None)
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+94
-30
@@ -43,6 +43,8 @@ REDNECK_DECREASE_LOOKAHEAD_POINTS = 10
|
|||||||
SLC_SOURCE_NONE = "None"
|
SLC_SOURCE_NONE = "None"
|
||||||
EventName = log.OnroadEvent.EventName
|
EventName = log.OnroadEvent.EventName
|
||||||
|
|
||||||
|
GM_GPS_SOURCES = (("device", 2.0), ("pps", 1.0), ("onstar", 2.5))
|
||||||
|
|
||||||
# forward
|
# forward
|
||||||
carlog.addHandler(ForwardingHandler(cloudlog))
|
carlog.addHandler(ForwardingHandler(cloudlog))
|
||||||
|
|
||||||
@@ -76,6 +78,22 @@ def can_comm_callbacks(logcan: messaging.SubSocket, sendcan: messaging.PubSocket
|
|||||||
return can_recv, can_send
|
return can_recv, can_send
|
||||||
|
|
||||||
|
|
||||||
|
def _gps_sample_is_healthy(sample: dict) -> bool:
|
||||||
|
if not sample.get("hasFix", False):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
latitude = float(sample["latitude"])
|
||||||
|
longitude = float(sample["longitude"])
|
||||||
|
return (math.isfinite(latitude) and math.isfinite(longitude) and
|
||||||
|
-90.0 <= latitude <= 90.0 and -180.0 <= longitude <= 180.0 and
|
||||||
|
(latitude != 0.0 or longitude != 0.0) and
|
||||||
|
math.isfinite(float(sample["altitude"])) and
|
||||||
|
math.isfinite(float(sample["speed"])) and
|
||||||
|
math.isfinite(float(sample["bearingDeg"])))
|
||||||
|
except (KeyError, TypeError, ValueError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Car:
|
class Car:
|
||||||
CI: CarInterfaceBase
|
CI: CarInterfaceBase
|
||||||
RI: RadarInterfaceBase
|
RI: RadarInterfaceBase
|
||||||
@@ -85,7 +103,9 @@ class Car:
|
|||||||
|
|
||||||
def __init__(self, CI=None, RI=None) -> None:
|
def __init__(self, CI=None, RI=None) -> None:
|
||||||
self.can_sock = messaging.sub_sock('can', timeout=20)
|
self.can_sock = messaging.sub_sock('can', timeout=20)
|
||||||
self.sm = messaging.SubMaster(['pandaStates', 'carControl', 'onroadEvents', 'radarState', 'longitudinalPlan'])
|
self.sm = messaging.SubMaster([
|
||||||
|
'pandaStates', 'carControl', 'onroadEvents', 'radarState', 'longitudinalPlan', 'gpsLocation',
|
||||||
|
])
|
||||||
self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'liveTracks'])
|
self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'liveTracks'])
|
||||||
self.gps_pm = None
|
self.gps_pm = None
|
||||||
|
|
||||||
@@ -93,6 +113,8 @@ class Car:
|
|||||||
self._last_car_gps_timestamp_nanos = 0
|
self._last_car_gps_timestamp_nanos = 0
|
||||||
self._last_car_gps_received_monotonic = 0.0
|
self._last_car_gps_received_monotonic = 0.0
|
||||||
self._last_car_gps_publish_monotonic = 0.0
|
self._last_car_gps_publish_monotonic = 0.0
|
||||||
|
self._gm_gps = dict.fromkeys(source for source, _ in GM_GPS_SOURCES)
|
||||||
|
self._gm_gps_had_fix = False
|
||||||
|
|
||||||
self.CC_prev = car.CarControl.new_message()
|
self.CC_prev = car.CarControl.new_message()
|
||||||
self.CS_prev = car.CarState.new_message()
|
self.CS_prev = car.CarState.new_message()
|
||||||
@@ -141,8 +163,9 @@ class Car:
|
|||||||
self.RI = RI
|
self.RI = RI
|
||||||
|
|
||||||
car_gps_supported = bool(getattr(self.CI.CS, 'car_gps_supported', False))
|
car_gps_supported = bool(getattr(self.CI.CS, 'car_gps_supported', False))
|
||||||
|
self.gm_gps_supported = self.CP.brand == "gm"
|
||||||
self.params.put_bool("CarGpsAvailable", car_gps_supported)
|
self.params.put_bool("CarGpsAvailable", car_gps_supported)
|
||||||
if car_gps_supported:
|
if car_gps_supported or self.gm_gps_supported:
|
||||||
self.gps_pm = messaging.PubMaster(['gpsLocationExternal'])
|
self.gps_pm = messaging.PubMaster(['gpsLocationExternal'])
|
||||||
|
|
||||||
aol_available = always_on_lateral_available(self.CP)
|
aol_available = always_on_lateral_available(self.CP)
|
||||||
@@ -349,39 +372,80 @@ class Car:
|
|||||||
FPCS = self.starpilot_card.update(CS, FPCS, self.sm, self.starpilot_toggles)
|
FPCS = self.starpilot_card.update(CS, FPCS, self.sm, self.starpilot_toggles)
|
||||||
return CS, RD, FPCS
|
return CS, RD, FPCS
|
||||||
|
|
||||||
|
def _publish_gm_gps(self, now: float) -> None:
|
||||||
|
if self.sm.updated.get("gpsLocation", False):
|
||||||
|
self._gm_gps["device"] = (self.sm["gpsLocation"].to_dict(), now) if self.sm.valid["gpsLocation"] else None
|
||||||
|
|
||||||
|
for source, sample in self.CI.CS.get_car_gps_sources().items():
|
||||||
|
previous = self._gm_gps[source]
|
||||||
|
if sample is None:
|
||||||
|
self._gm_gps[source] = None
|
||||||
|
elif previous is None or sample["timestamp_nanos"] > previous[0]["timestamp_nanos"]:
|
||||||
|
self._gm_gps[source] = (sample.copy(), now)
|
||||||
|
|
||||||
|
# Device > PPS > OnStar, with each source's own freshness window.
|
||||||
|
selected = None
|
||||||
|
for source, timeout in GM_GPS_SOURCES:
|
||||||
|
candidate = self._gm_gps[source]
|
||||||
|
if candidate is not None and now - candidate[1] <= timeout and _gps_sample_is_healthy(candidate[0]):
|
||||||
|
selected = candidate[0]
|
||||||
|
break
|
||||||
|
|
||||||
|
if selected is None:
|
||||||
|
if not self._gm_gps_had_fix:
|
||||||
|
return
|
||||||
|
elif self._gm_gps_had_fix and (now - self._last_car_gps_publish_monotonic < 0.2):
|
||||||
|
return
|
||||||
|
|
||||||
|
gps_send = messaging.new_message('gpsLocationExternal', valid=selected is not None)
|
||||||
|
if selected is not None:
|
||||||
|
gps_send.gpsLocationExternal = {key: value for key, value in selected.items() if key != "timestamp_nanos"}
|
||||||
|
if source != "device":
|
||||||
|
gps_send.gpsLocationExternal.source = "car"
|
||||||
|
else:
|
||||||
|
# Clear subscribers' cached fix once, then let the service become stale.
|
||||||
|
gps_send.gpsLocationExternal.hasFix = False
|
||||||
|
assert self.gps_pm is not None
|
||||||
|
self.gps_pm.send('gpsLocationExternal', gps_send)
|
||||||
|
self._gm_gps_had_fix = selected is not None
|
||||||
|
self._last_car_gps_publish_monotonic = now
|
||||||
|
|
||||||
def state_publish(self, CS: car.CarState, RD: structs.RadarDataT | None, FPCS: custom.StarPilotCarState):
|
def state_publish(self, CS: car.CarState, RD: structs.RadarDataT | None, FPCS: custom.StarPilotCarState):
|
||||||
"""carState and carParams publish loop"""
|
"""carState and carParams publish loop"""
|
||||||
|
|
||||||
get_car_gps = getattr(self.CI.CS, 'get_car_gps', None)
|
|
||||||
car_gps = get_car_gps() if get_car_gps is not None else None
|
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
if car_gps is not None and car_gps['timestamp_nanos'] > self._last_car_gps_timestamp_nanos:
|
if self.gm_gps_supported:
|
||||||
self._last_car_gps_timestamp_nanos = car_gps['timestamp_nanos']
|
self._publish_gm_gps(now)
|
||||||
self._last_car_gps_received_monotonic = now
|
else:
|
||||||
|
get_car_gps = getattr(self.CI.CS, 'get_car_gps', None)
|
||||||
|
car_gps = get_car_gps() if get_car_gps is not None else None
|
||||||
|
if car_gps is not None and car_gps['timestamp_nanos'] > self._last_car_gps_timestamp_nanos:
|
||||||
|
self._last_car_gps_timestamp_nanos = car_gps['timestamp_nanos']
|
||||||
|
self._last_car_gps_received_monotonic = now
|
||||||
|
|
||||||
if car_gps is not None and self._last_car_gps_received_monotonic > 0.0 and \
|
if car_gps is not None and self._last_car_gps_received_monotonic > 0.0 and \
|
||||||
now - self._last_car_gps_received_monotonic <= 2.5 and \
|
now - self._last_car_gps_received_monotonic <= 2.5 and \
|
||||||
now - self._last_car_gps_publish_monotonic >= 0.2:
|
now - self._last_car_gps_publish_monotonic >= 0.2:
|
||||||
gps_send = messaging.new_message('gpsLocationExternal', valid=True)
|
gps_send = messaging.new_message('gpsLocationExternal', valid=True)
|
||||||
gps = gps_send.gpsLocationExternal
|
gps = gps_send.gpsLocationExternal
|
||||||
gps.flags = 0
|
gps.flags = 0
|
||||||
gps.latitude = car_gps['latitude']
|
gps.latitude = car_gps['latitude']
|
||||||
gps.longitude = car_gps['longitude']
|
gps.longitude = car_gps['longitude']
|
||||||
gps.altitude = car_gps['altitude']
|
gps.altitude = car_gps['altitude']
|
||||||
gps.speed = car_gps['speed']
|
gps.speed = car_gps['speed']
|
||||||
gps.bearingDeg = car_gps['bearingDeg']
|
gps.bearingDeg = car_gps['bearingDeg']
|
||||||
gps.horizontalAccuracy = car_gps['horizontalAccuracy']
|
gps.horizontalAccuracy = car_gps['horizontalAccuracy']
|
||||||
gps.unixTimestampMillis = car_gps['unixTimestampMillis']
|
gps.unixTimestampMillis = car_gps['unixTimestampMillis']
|
||||||
gps.source = log.GpsLocationData.SensorSource.car
|
gps.source = log.GpsLocationData.SensorSource.car
|
||||||
gps.vNED = car_gps['vNED']
|
gps.vNED = car_gps['vNED']
|
||||||
gps.verticalAccuracy = car_gps['verticalAccuracy']
|
gps.verticalAccuracy = car_gps['verticalAccuracy']
|
||||||
gps.bearingAccuracyDeg = car_gps['bearingAccuracyDeg']
|
gps.bearingAccuracyDeg = car_gps['bearingAccuracyDeg']
|
||||||
gps.speedAccuracy = car_gps['speedAccuracy']
|
gps.speedAccuracy = car_gps['speedAccuracy']
|
||||||
gps.hasFix = car_gps['hasFix']
|
gps.hasFix = car_gps['hasFix']
|
||||||
gps.satelliteCount = car_gps['satelliteCount']
|
gps.satelliteCount = car_gps['satelliteCount']
|
||||||
assert self.gps_pm is not None
|
assert self.gps_pm is not None
|
||||||
self.gps_pm.send('gpsLocationExternal', gps_send)
|
self.gps_pm.send('gpsLocationExternal', gps_send)
|
||||||
self._last_car_gps_publish_monotonic = now
|
self._last_car_gps_publish_monotonic = now
|
||||||
|
|
||||||
# carParams - logged every 50 seconds (> 1 per segment)
|
# carParams - logged every 50 seconds (> 1 per segment)
|
||||||
if self.sm.frame % int(50. / DT_CTRL) == 0:
|
if self.sm.frame % int(50. / DT_CTRL) == 0:
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ class VCruiseHelper:
|
|||||||
|
|
||||||
engage_floor_kph = max(V_CRUISE_MIN, 7.0 * CV.MPH_TO_KPH)
|
engage_floor_kph = max(V_CRUISE_MIN, 7.0 * CV.MPH_TO_KPH)
|
||||||
resume_pressed = any(b.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for b in CS.buttonEvents)
|
resume_pressed = any(b.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for b in CS.buttonEvents)
|
||||||
remembered_resume = resume_prev_button and self._uses_software_cruise()
|
remembered_resume = resume_prev_button and (self.gm_cc_only or self.redneck_non_pcm)
|
||||||
|
|
||||||
if self.v_cruise_initialized and (resume_pressed or remembered_resume):
|
if self.v_cruise_initialized and (resume_pressed or remembered_resume):
|
||||||
self.v_cruise_kph = self.v_cruise_kph_last
|
self.v_cruise_kph = self.v_cruise_kph_last
|
||||||
|
|||||||
@@ -313,22 +313,6 @@ class TestVCruiseHelper:
|
|||||||
assert V_CRUISE_MIN <= self.v_cruise_helper.v_cruise_kph <= V_CRUISE_MAX
|
assert V_CRUISE_MIN <= self.v_cruise_helper.v_cruise_kph <= V_CRUISE_MAX
|
||||||
assert self.v_cruise_helper.v_cruise_initialized
|
assert self.v_cruise_helper.v_cruise_initialized
|
||||||
|
|
||||||
def test_resume_keeps_previous_software_cruise_speed(self):
|
|
||||||
engage_cs = car.CarState(vEgo=75 * CV.MPH_TO_MS)
|
|
||||||
self.v_cruise_helper.initialize_v_cruise(engage_cs, experimental_mode=False, resume_prev_button=False,
|
|
||||||
starpilot_toggles=self.starpilot_toggles)
|
|
||||||
|
|
||||||
disabled_cs = car.CarState(cruiseState={"available": True})
|
|
||||||
self.v_cruise_helper.update_v_cruise(disabled_cs, enabled=False, is_metric=False,
|
|
||||||
speed_limit_changed=False, starpilot_toggles=self.starpilot_toggles)
|
|
||||||
|
|
||||||
resume_cs = car.CarState(vEgo=22 * CV.MPH_TO_MS)
|
|
||||||
self.v_cruise_helper.initialize_v_cruise(resume_cs, experimental_mode=False, resume_prev_button=True,
|
|
||||||
starpilot_toggles=self.starpilot_toggles)
|
|
||||||
|
|
||||||
assert self.v_cruise_helper.v_cruise_kph == pytest.approx(75 * CV.MPH_TO_KPH)
|
|
||||||
assert self.v_cruise_helper.v_cruise_cluster_kph == pytest.approx(75 * CV.MPH_TO_KPH)
|
|
||||||
|
|
||||||
def test_initialize_v_cruise_matches_speed_limit(self):
|
def test_initialize_v_cruise_matches_speed_limit(self):
|
||||||
self.reset_cruise_speed_state()
|
self.reset_cruise_speed_state()
|
||||||
self.starpilot_toggles.set_speed_limit = True
|
self.starpilot_toggles.set_speed_limit = True
|
||||||
@@ -499,33 +483,39 @@ class TestVCruiseHelper:
|
|||||||
assert self.v_cruise_helper.v_cruise_kph == pytest.approx(initial_v_cruise_kph + IMPERIAL_INCREMENT)
|
assert self.v_cruise_helper.v_cruise_kph == pytest.approx(initial_v_cruise_kph + IMPERIAL_INCREMENT)
|
||||||
|
|
||||||
@pytest.mark.parametrize("openpilot_longitudinal", [False, True])
|
@pytest.mark.parametrize("openpilot_longitudinal", [False, True])
|
||||||
def test_pcm_cruise_always_tracks_pcm_speed(self, openpilot_longitudinal):
|
def test_pcm_cruise_uses_pcm_speed(self, openpilot_longitudinal):
|
||||||
CP = car.CarParams(pcmCruise=True, openpilotLongitudinalControl=openpilot_longitudinal)
|
CP = car.CarParams(pcmCruise=True, openpilotLongitudinalControl=openpilot_longitudinal)
|
||||||
helper = VCruiseHelper(CP)
|
helper = VCruiseHelper(CP)
|
||||||
toggles = SimpleNamespace(cruise_increase=5, cruise_increase_long=1, set_speed_limit=False)
|
toggles = SimpleNamespace(cruise_increase=5, cruise_increase_long=1, set_speed_limit=False)
|
||||||
|
pcm_speed_kph = 72.0
|
||||||
|
pcm_cluster_speed_kph = 71.0
|
||||||
|
|
||||||
helper.initialize_v_cruise(car.CarState(vEgo=40 * CV.KPH_TO_MS), False, False, toggles)
|
helper.initialize_v_cruise(car.CarState(vEgo=40 * CV.KPH_TO_MS), False, False, toggles)
|
||||||
assert not helper.v_cruise_initialized
|
assert not helper.v_cruise_initialized
|
||||||
|
|
||||||
samples = (
|
cs = car.CarState(
|
||||||
(72.0, 71.0, None),
|
cruiseState={
|
||||||
(25.0, 25.0, {"type": ButtonType.decelCruise, "pressed": True}),
|
"available": True,
|
||||||
(65.0, 65.0, {"type": ButtonType.decelCruise, "pressed": False}),
|
"speed": pcm_speed_kph * CV.KPH_TO_MS,
|
||||||
(90.0, 90.0, {"type": ButtonType.accelCruise, "pressed": True}),
|
"speedCluster": pcm_cluster_speed_kph * CV.KPH_TO_MS,
|
||||||
(5.0, 5.0, {"type": ButtonType.accelCruise, "pressed": False}),
|
},
|
||||||
)
|
)
|
||||||
for pcm_speed_kph, pcm_cluster_speed_kph, button_event in samples:
|
|
||||||
cs = car.CarState(
|
helper.update_v_cruise(cs, True, True, False, toggles)
|
||||||
cruiseState={
|
assert helper.v_cruise_kph == pytest.approx(pcm_speed_kph)
|
||||||
"available": True,
|
assert helper.v_cruise_cluster_kph == pytest.approx(pcm_cluster_speed_kph)
|
||||||
"speed": pcm_speed_kph * CV.KPH_TO_MS,
|
|
||||||
"speedCluster": pcm_cluster_speed_kph * CV.KPH_TO_MS,
|
next_pcm_speed_kph = 74.0
|
||||||
},
|
next_cs = car.CarState(
|
||||||
buttonEvents=[] if button_event is None else [button_event],
|
cruiseState={
|
||||||
)
|
"available": True,
|
||||||
helper.update_v_cruise(cs, True, True, False, toggles)
|
"speed": next_pcm_speed_kph * CV.KPH_TO_MS,
|
||||||
assert helper.v_cruise_kph == pytest.approx(pcm_speed_kph)
|
"speedCluster": next_pcm_speed_kph * CV.KPH_TO_MS,
|
||||||
assert helper.v_cruise_cluster_kph == pytest.approx(pcm_cluster_speed_kph)
|
},
|
||||||
|
buttonEvents=[{"type": ButtonType.accelCruise, "pressed": False}],
|
||||||
|
)
|
||||||
|
helper.update_v_cruise(next_cs, True, True, False, toggles)
|
||||||
|
assert helper.v_cruise_kph == pytest.approx(next_pcm_speed_kph)
|
||||||
|
|
||||||
|
|
||||||
class TestVCruiseHelperRedneck:
|
class TestVCruiseHelperRedneck:
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cereal import messaging
|
||||||
|
from openpilot.selfdrive.car.card import Car, GM_GPS_SOURCES
|
||||||
|
|
||||||
|
|
||||||
|
def sample():
|
||||||
|
gps = messaging.new_message("gpsLocation", valid=True).gpsLocation
|
||||||
|
gps.hasFix = True
|
||||||
|
gps.latitude = 38.3
|
||||||
|
gps.longitude = -85.7
|
||||||
|
gps.source = "ublox"
|
||||||
|
return dict(gps.to_dict(), timestamp_nanos=1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def selector():
|
||||||
|
card = Car.__new__(Car)
|
||||||
|
card._gm_gps = dict.fromkeys(source for source, _ in GM_GPS_SOURCES)
|
||||||
|
card._gm_gps_had_fix = False
|
||||||
|
card._last_car_gps_publish_monotonic = 0.0
|
||||||
|
card.gps_pm = SimpleNamespace(sent=[])
|
||||||
|
card.gps_pm.send = lambda service, message: card.gps_pm.sent.append((service, message))
|
||||||
|
card.sm = SimpleNamespace(updated={"gpsLocation": False})
|
||||||
|
card.CI = SimpleNamespace(CS=SimpleNamespace(get_car_gps_sources=dict))
|
||||||
|
return card
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("states,winner", [
|
||||||
|
(("good", None, None), "device"),
|
||||||
|
(("bad", "good", None), "pps"),
|
||||||
|
(("stale", "good", None), "pps"),
|
||||||
|
(("bad", "bad", "good"), "onstar"),
|
||||||
|
(("good", "good", "good"), "device"),
|
||||||
|
(("bad", "stale", None), None),
|
||||||
|
(("stale", "stale", "good"), "onstar"),
|
||||||
|
(("bad", "bad", "bad"), None),
|
||||||
|
(("stale", "stale", "stale"), None),
|
||||||
|
])
|
||||||
|
def test_priority(selector, states, winner):
|
||||||
|
for index, ((source, timeout), state) in enumerate(zip(GM_GPS_SOURCES, states, strict=True)):
|
||||||
|
if state is not None:
|
||||||
|
fix = sample() | {"latitude": 38.0 + index, "hasFix": state != "bad"}
|
||||||
|
selector._gm_gps[source] = (fix, 10.0 - timeout - 0.01 if state == "stale" else 10.0)
|
||||||
|
selector._publish_gm_gps(10.0)
|
||||||
|
if winner is None:
|
||||||
|
assert not selector.gps_pm.sent
|
||||||
|
else:
|
||||||
|
assert selector.gps_pm.sent[-1][1].gpsLocationExternal.latitude == selector._gm_gps[winner][0]["latitude"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("source,timeout", [("device", 2.0), ("pps", 1.0), ("onstar", 2.5)])
|
||||||
|
def test_freshness_and_recovery(selector, source, timeout):
|
||||||
|
selector._gm_gps[source] = (sample(), 10.0)
|
||||||
|
selector._publish_gm_gps(10.0 + timeout)
|
||||||
|
assert selector.gps_pm.sent[-1][1].valid
|
||||||
|
selector._publish_gm_gps(10.01 + timeout)
|
||||||
|
assert not selector.gps_pm.sent[-1][1].valid
|
||||||
|
selector._gm_gps[source] = (sample(), 20.0)
|
||||||
|
selector._publish_gm_gps(20.0)
|
||||||
|
assert selector.gps_pm.sent[-1][1].valid
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("source", ["device", "pps"])
|
||||||
|
def test_upward_recovery(selector, source):
|
||||||
|
selector._gm_gps["onstar"] = (sample() | {"latitude": 40.0}, 10.0)
|
||||||
|
selector._publish_gm_gps(10.0)
|
||||||
|
assert selector.gps_pm.sent[-1][1].gpsLocationExternal.latitude == 40.0
|
||||||
|
selector._gm_gps[source] = (sample(), 10.21)
|
||||||
|
selector._publish_gm_gps(10.21)
|
||||||
|
assert selector.gps_pm.sent[-1][1].gpsLocationExternal.latitude == 38.3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("changes,expected", [
|
||||||
|
({"latitude": float("nan")}, False), ({"longitude": float("inf")}, False),
|
||||||
|
({"latitude": 91}, False), ({"longitude": -181}, False), ({"longitude": 181}, False),
|
||||||
|
({"latitude": 0, "longitude": 0}, False), ({"speed": float("nan")}, False),
|
||||||
|
({"latitude": 0, "longitude": 10}, True), ({"latitude": 10, "longitude": 0}, True),
|
||||||
|
({"horizontalAccuracy": 500.0}, True),
|
||||||
|
])
|
||||||
|
def test_health(selector, changes, expected):
|
||||||
|
selector._gm_gps = {source: (sample() | {"latitude": 40.0}, 10.0) for source, _ in GM_GPS_SOURCES}
|
||||||
|
selector._gm_gps["device"] = (sample() | changes, 10.0)
|
||||||
|
selector._publish_gm_gps(10.0)
|
||||||
|
gps = selector.gps_pm.sent[-1][1].gpsLocationExternal
|
||||||
|
assert gps.latitude == (selector._gm_gps["device"][0]["latitude"] if expected else 40.0)
|
||||||
|
assert gps.source == ("ublox" if expected else "car")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("source", [source for source, _ in GM_GPS_SOURCES])
|
||||||
|
def test_selected_fix_fields_are_preserved(selector, source):
|
||||||
|
fix = sample() | {"altitude": 123.0, "speed": 10.0, "vNED": [0.0, 10.0, 0.0], "bearingDeg": 90.0,
|
||||||
|
"horizontalAccuracy": 3.0, "verticalAccuracy": 5.0, "bearingAccuracyDeg": 2.0}
|
||||||
|
if source == "device":
|
||||||
|
selector._gm_gps[source] = (fix, 10.0)
|
||||||
|
else:
|
||||||
|
selector.CI.CS.get_car_gps_sources = lambda: {source: fix}
|
||||||
|
selector._publish_gm_gps(10.0)
|
||||||
|
service, message = selector.gps_pm.sent[-1]
|
||||||
|
assert service == "gpsLocationExternal"
|
||||||
|
assert message.valid
|
||||||
|
expected = {key: value for key, value in fix.items() if key != "timestamp_nanos"}
|
||||||
|
expected["source"] = "ublox" if source == "device" else "car"
|
||||||
|
assert message.gpsLocationExternal.to_dict() == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("source", ["pps", "onstar"])
|
||||||
|
def test_can_cache_freshness_and_explicit_invalidation(selector, source):
|
||||||
|
sources = {"pps": sample(), "onstar": sample() | {"latitude": 40.0}}
|
||||||
|
selector.CI.CS.get_car_gps_sources = lambda: sources
|
||||||
|
selector._publish_gm_gps(10.0)
|
||||||
|
for timestamp in (1, 0):
|
||||||
|
sources[source] = sources[source] | {"timestamp_nanos": timestamp}
|
||||||
|
selector._publish_gm_gps(10.5)
|
||||||
|
assert selector._gm_gps[source][1] == 10.0
|
||||||
|
sources[source] = sources[source] | {"timestamp_nanos": 2}
|
||||||
|
selector._publish_gm_gps(10.6)
|
||||||
|
assert selector._gm_gps[source][1] == 10.6
|
||||||
|
sources[source] = None
|
||||||
|
selector._publish_gm_gps(10.9)
|
||||||
|
assert selector._gm_gps[source] is None
|
||||||
|
assert selector.gps_pm.sent[-1][1].gpsLocationExternal.latitude == (40.0 if source == "pps" else 38.3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_source_publication_transition(selector):
|
||||||
|
selector._publish_gm_gps(10.0)
|
||||||
|
assert not selector.gps_pm.sent
|
||||||
|
selector._gm_gps["device"] = (sample(), 10.0)
|
||||||
|
selector._publish_gm_gps(10.0)
|
||||||
|
valid = selector.gps_pm.sent[-1][1]
|
||||||
|
assert valid.valid and valid.gpsLocationExternal.hasFix
|
||||||
|
assert valid.gpsLocationExternal.latitude == 38.3
|
||||||
|
selector._publish_gm_gps(10.1)
|
||||||
|
assert len(selector.gps_pm.sent) == 1
|
||||||
|
selector._publish_gm_gps(12.1)
|
||||||
|
invalid = selector.gps_pm.sent[-1][1]
|
||||||
|
assert not invalid.valid and not invalid.gpsLocationExternal.hasFix
|
||||||
|
for now in (12.12, 12.13, 12.14):
|
||||||
|
selector._publish_gm_gps(now)
|
||||||
|
assert len(selector.gps_pm.sent) == 2
|
||||||
|
selector._gm_gps["device"] = (sample(), 12.15)
|
||||||
|
selector._publish_gm_gps(12.15) # Recovery bypasses the 200 ms healthy cadence.
|
||||||
|
assert len(selector.gps_pm.sent) == 3
|
||||||
|
assert selector.gps_pm.sent[-1][1].gpsLocationExternal.hasFix
|
||||||
|
for now in (15.0, 20.0, 30.0):
|
||||||
|
selector._publish_gm_gps(now)
|
||||||
|
assert len(selector.gps_pm.sent) == 4 # One more loss marker, then sustained silence.
|
||||||
|
|
||||||
|
|
||||||
|
def test_device_updates_honor_event_validity(selector):
|
||||||
|
message = messaging.new_message("gpsLocation", valid=True)
|
||||||
|
message.gpsLocation = {key: value for key, value in sample().items() if key != "timestamp_nanos"}
|
||||||
|
class DeviceMessages(dict):
|
||||||
|
updated = {"gpsLocation": True}
|
||||||
|
valid = {"gpsLocation": True}
|
||||||
|
selector.sm = DeviceMessages(gpsLocation=message.gpsLocation)
|
||||||
|
selector._publish_gm_gps(10.0)
|
||||||
|
assert selector.gps_pm.sent[-1][1].valid
|
||||||
|
selector.sm.updated["gpsLocation"] = False
|
||||||
|
selector._publish_gm_gps(10.05)
|
||||||
|
assert selector._gm_gps["device"][1] == 10.0
|
||||||
|
selector.sm.updated["gpsLocation"] = True
|
||||||
|
selector.sm.valid["gpsLocation"] = False
|
||||||
|
selector._publish_gm_gps(10.1)
|
||||||
|
assert selector._gm_gps["device"] is None
|
||||||
|
assert len(selector.gps_pm.sent) == 2 # Loss bypasses the healthy cadence too.
|
||||||
|
assert not selector.gps_pm.sent[-1][1].gpsLocationExternal.hasFix
|
||||||
@@ -639,9 +639,6 @@ class LatControlTorque(LatControl):
|
|||||||
output_torque *= tucson_4th_gen_center_taper
|
output_torque *= tucson_4th_gen_center_taper
|
||||||
elif genesis_g70_active:
|
elif genesis_g70_active:
|
||||||
output_torque *= genesis_g70_center_output_taper
|
output_torque *= genesis_g70_center_output_taper
|
||||||
output_torque *= get_genesis_g70_high_speed_transition_scale(
|
|
||||||
setpoint, desired_lateral_jerk, CS.vEgo,
|
|
||||||
)
|
|
||||||
output_torque *= get_genesis_g70_curve_unwind_output_scale(setpoint, desired_lateral_jerk, CS.vEgo)
|
output_torque *= get_genesis_g70_curve_unwind_output_scale(setpoint, desired_lateral_jerk, CS.vEgo)
|
||||||
output_torque *= get_genesis_g70_high_speed_error_scale(
|
output_torque *= get_genesis_g70_high_speed_error_scale(
|
||||||
setpoint, measurement, desired_lateral_jerk, CS.vEgo,
|
setpoint, measurement, desired_lateral_jerk, CS.vEgo,
|
||||||
|
|||||||
@@ -275,27 +275,20 @@ GENESIS_G70_FRICTION_JERK_DEADZONE_LAT = 0.30
|
|||||||
GENESIS_G70_FRICTION_JERK_DEADZONE_LAT_WIDTH = 0.08
|
GENESIS_G70_FRICTION_JERK_DEADZONE_LAT_WIDTH = 0.08
|
||||||
GENESIS_G70_FRICTION_JERK_DEADZONE_SPEED = 12.0
|
GENESIS_G70_FRICTION_JERK_DEADZONE_SPEED = 12.0
|
||||||
GENESIS_G70_FRICTION_JERK_DEADZONE_SPEED_WIDTH = 3.5
|
GENESIS_G70_FRICTION_JERK_DEADZONE_SPEED_WIDTH = 3.5
|
||||||
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_MAX = 0.26
|
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_MAX = 0.16
|
||||||
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_SPEED = 35.0 * CV.MPH_TO_MS
|
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_SPEED = 35.0 * CV.MPH_TO_MS
|
||||||
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_SPEED_WIDTH = 8.0 * CV.MPH_TO_MS
|
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_SPEED_WIDTH = 8.0 * CV.MPH_TO_MS
|
||||||
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_LAT = 0.35
|
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_LAT = 0.35
|
||||||
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_LAT_WIDTH = 0.15
|
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_LAT_WIDTH = 0.15
|
||||||
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_LAT_CUTOFF = 1.75
|
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_LAT_CUTOFF = 1.25
|
||||||
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_LAT_CUTOFF_WIDTH = 0.30
|
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_LAT_CUTOFF_WIDTH = 0.25
|
||||||
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_JERK = 0.20
|
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_JERK = 0.20
|
||||||
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_JERK_WIDTH = 0.12
|
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_JERK_WIDTH = 0.12
|
||||||
GENESIS_G70_CENTER_OUTPUT_TAPER_MAX = 0.30
|
GENESIS_G70_CENTER_OUTPUT_TAPER_MAX = 0.22
|
||||||
GENESIS_G70_CENTER_OUTPUT_TAPER_LAT = 0.30
|
GENESIS_G70_CENTER_OUTPUT_TAPER_LAT = 0.30
|
||||||
GENESIS_G70_CENTER_OUTPUT_TAPER_LAT_WIDTH = 0.10
|
GENESIS_G70_CENTER_OUTPUT_TAPER_LAT_WIDTH = 0.10
|
||||||
GENESIS_G70_CENTER_OUTPUT_TAPER_SPEED = 18.0
|
GENESIS_G70_CENTER_OUTPUT_TAPER_SPEED = 18.0
|
||||||
GENESIS_G70_CENTER_OUTPUT_TAPER_SPEED_WIDTH = 3.0
|
GENESIS_G70_CENTER_OUTPUT_TAPER_SPEED_WIDTH = 3.0
|
||||||
GENESIS_G70_HIGH_SPEED_TRANSITION_DAMPING_MAX = 0.18
|
|
||||||
GENESIS_G70_HIGH_SPEED_TRANSITION_DAMPING_SPEED = 45.0 * CV.MPH_TO_MS
|
|
||||||
GENESIS_G70_HIGH_SPEED_TRANSITION_DAMPING_SPEED_WIDTH = 8.0 * CV.MPH_TO_MS
|
|
||||||
GENESIS_G70_HIGH_SPEED_TRANSITION_DAMPING_LAT = 0.45
|
|
||||||
GENESIS_G70_HIGH_SPEED_TRANSITION_DAMPING_LAT_WIDTH = 0.15
|
|
||||||
GENESIS_G70_HIGH_SPEED_TRANSITION_DAMPING_JERK = 0.35
|
|
||||||
GENESIS_G70_HIGH_SPEED_TRANSITION_DAMPING_JERK_WIDTH = 0.15
|
|
||||||
GENESIS_G70_LOW_SPEED_CENTER_TAPER_MAX = 0.06
|
GENESIS_G70_LOW_SPEED_CENTER_TAPER_MAX = 0.06
|
||||||
GENESIS_G70_LOW_SPEED_CENTER_TAPER_LAT = 0.14
|
GENESIS_G70_LOW_SPEED_CENTER_TAPER_LAT = 0.14
|
||||||
GENESIS_G70_LOW_SPEED_CENTER_TAPER_LAT_WIDTH = 0.05
|
GENESIS_G70_LOW_SPEED_CENTER_TAPER_LAT_WIDTH = 0.05
|
||||||
@@ -314,7 +307,7 @@ GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_LAT = 0.14
|
|||||||
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_LAT_WIDTH = 0.05
|
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_LAT_WIDTH = 0.05
|
||||||
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_SPEED = 6.0
|
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_SPEED = 6.0
|
||||||
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_SPEED_WIDTH = 1.5
|
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_SPEED_WIDTH = 1.5
|
||||||
GENESIS_G70_CURVE_UNWIND_OUTPUT_REDUCTION_MAX = 0.08
|
GENESIS_G70_CURVE_UNWIND_OUTPUT_BOOST = 0.00
|
||||||
GENESIS_G70_CURVE_UNWIND_SPEED = 18.0
|
GENESIS_G70_CURVE_UNWIND_SPEED = 18.0
|
||||||
GENESIS_G70_CURVE_UNWIND_SPEED_WIDTH = 3.0
|
GENESIS_G70_CURVE_UNWIND_SPEED_WIDTH = 3.0
|
||||||
GENESIS_G70_CURVE_UNWIND_LAT = 0.25
|
GENESIS_G70_CURVE_UNWIND_LAT = 0.25
|
||||||
@@ -3253,18 +3246,6 @@ def get_genesis_g70_center_output_scale(desired_lateral_accel: float, v_ego: flo
|
|||||||
return 1.0 - reduction
|
return 1.0 - reduction
|
||||||
|
|
||||||
|
|
||||||
def get_genesis_g70_high_speed_transition_scale(desired_lateral_accel: float,
|
|
||||||
desired_lateral_jerk: float, v_ego: float) -> float:
|
|
||||||
speed_weight = _sigmoid((v_ego - GENESIS_G70_HIGH_SPEED_TRANSITION_DAMPING_SPEED) /
|
|
||||||
GENESIS_G70_HIGH_SPEED_TRANSITION_DAMPING_SPEED_WIDTH)
|
|
||||||
center_weight = _sigmoid((GENESIS_G70_HIGH_SPEED_TRANSITION_DAMPING_LAT - abs(desired_lateral_accel)) /
|
|
||||||
GENESIS_G70_HIGH_SPEED_TRANSITION_DAMPING_LAT_WIDTH)
|
|
||||||
jerk_weight = _sigmoid((abs(desired_lateral_jerk) - GENESIS_G70_HIGH_SPEED_TRANSITION_DAMPING_JERK) /
|
|
||||||
GENESIS_G70_HIGH_SPEED_TRANSITION_DAMPING_JERK_WIDTH)
|
|
||||||
reduction = (GENESIS_G70_HIGH_SPEED_TRANSITION_DAMPING_MAX * speed_weight * center_weight * jerk_weight)
|
|
||||||
return 1.0 - reduction
|
|
||||||
|
|
||||||
|
|
||||||
def get_genesis_g70_low_speed_angle_damping(desired_angle_deg: float, actual_angle_deg: float,
|
def get_genesis_g70_low_speed_angle_damping(desired_angle_deg: float, actual_angle_deg: float,
|
||||||
current_output_torque: float, v_ego: float) -> float:
|
current_output_torque: float, v_ego: float) -> float:
|
||||||
angle_error = desired_angle_deg - actual_angle_deg
|
angle_error = desired_angle_deg - actual_angle_deg
|
||||||
@@ -3313,8 +3294,7 @@ def get_genesis_g70_curve_unwind_output_scale(desired_lateral_accel: float, desi
|
|||||||
GENESIS_G70_CURVE_UNWIND_LAT_WIDTH)
|
GENESIS_G70_CURVE_UNWIND_LAT_WIDTH)
|
||||||
jerk_weight = _sigmoid((abs(desired_lateral_jerk) - GENESIS_G70_CURVE_UNWIND_JERK) /
|
jerk_weight = _sigmoid((abs(desired_lateral_jerk) - GENESIS_G70_CURVE_UNWIND_JERK) /
|
||||||
GENESIS_G70_CURVE_UNWIND_JERK_WIDTH)
|
GENESIS_G70_CURVE_UNWIND_JERK_WIDTH)
|
||||||
reduction = (GENESIS_G70_CURVE_UNWIND_OUTPUT_REDUCTION_MAX * speed_weight * lateral_weight * jerk_weight)
|
return 1.0 + GENESIS_G70_CURVE_UNWIND_OUTPUT_BOOST * speed_weight * lateral_weight * jerk_weight
|
||||||
return 1.0 - reduction
|
|
||||||
|
|
||||||
|
|
||||||
def get_genesis_g70_unwind_ff_scale(setpoint: float, measured_lateral_accel: float,
|
def get_genesis_g70_unwind_ff_scale(setpoint: float, measured_lateral_accel: float,
|
||||||
|
|||||||
@@ -56,10 +56,6 @@ HYUNDAI_ELANTRA_STOPPED_LEAD_MAX_EGO_SPEED = 2.0
|
|||||||
HYUNDAI_ELANTRA_STOPPED_LEAD_MAX_SPEED = 0.5
|
HYUNDAI_ELANTRA_STOPPED_LEAD_MAX_SPEED = 0.5
|
||||||
HYUNDAI_ELANTRA_STOPPED_LEAD_MIN_CLOSING_SPEED = 0.25
|
HYUNDAI_ELANTRA_STOPPED_LEAD_MIN_CLOSING_SPEED = 0.25
|
||||||
HYUNDAI_ELANTRA_STOPPED_LEAD_MAX_CREEP_ACCEL = 0.05
|
HYUNDAI_ELANTRA_STOPPED_LEAD_MAX_CREEP_ACCEL = 0.05
|
||||||
HYUNDAI_ELANTRA_FINAL_STOP_MAX_SPEED = 1.0
|
|
||||||
HYUNDAI_ELANTRA_FINAL_STOP_CAP_BP = [0.0, 0.2, 0.5, HYUNDAI_ELANTRA_FINAL_STOP_MAX_SPEED]
|
|
||||||
HYUNDAI_ELANTRA_FINAL_STOP_CAP_V = [-0.20, -0.25, -0.35, -0.55]
|
|
||||||
HYUNDAI_ELANTRA_FINAL_STOP_URGENCY_MARGIN = 0.45
|
|
||||||
HYUNDAI_SANTA_FE_FINAL_STOP_MAX_SPEED = 1.0
|
HYUNDAI_SANTA_FE_FINAL_STOP_MAX_SPEED = 1.0
|
||||||
HYUNDAI_SANTA_FE_FINAL_STOP_CAP_BP = [0.0, 0.2, 0.5, HYUNDAI_SANTA_FE_FINAL_STOP_MAX_SPEED]
|
HYUNDAI_SANTA_FE_FINAL_STOP_CAP_BP = [0.0, 0.2, 0.5, HYUNDAI_SANTA_FE_FINAL_STOP_MAX_SPEED]
|
||||||
HYUNDAI_SANTA_FE_FINAL_STOP_CAP_V = [-0.25, -0.30, -0.50, -0.90]
|
HYUNDAI_SANTA_FE_FINAL_STOP_CAP_V = [-0.25, -0.30, -0.50, -0.90]
|
||||||
@@ -173,21 +169,7 @@ class LongControlVehicleTuning:
|
|||||||
self.subaru_stop_release_frames = 0
|
self.subaru_stop_release_frames = 0
|
||||||
|
|
||||||
def shape_stopping_accel(self, output_accel, a_target, should_stop, v_ego, has_lead, stop_accel):
|
def shape_stopping_accel(self, output_accel, a_target, should_stop, v_ego, has_lead, stop_accel):
|
||||||
"""Shape low-speed stop braking without overriding urgent targets."""
|
"""Release a stale hard lead brake once the stop target has eased."""
|
||||||
if (
|
|
||||||
self.is_hyundai_elantra_2021 and
|
|
||||||
should_stop and
|
|
||||||
v_ego < HYUNDAI_ELANTRA_FINAL_STOP_MAX_SPEED and
|
|
||||||
a_target <= 0.1
|
|
||||||
):
|
|
||||||
final_stop_cap = float(interp(
|
|
||||||
v_ego,
|
|
||||||
HYUNDAI_ELANTRA_FINAL_STOP_CAP_BP,
|
|
||||||
HYUNDAI_ELANTRA_FINAL_STOP_CAP_V,
|
|
||||||
))
|
|
||||||
if a_target > final_stop_cap - HYUNDAI_ELANTRA_FINAL_STOP_URGENCY_MARGIN:
|
|
||||||
return max(float(output_accel), final_stop_cap)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.is_hyundai_santa_fe_2022 and
|
self.is_hyundai_santa_fe_2022 and
|
||||||
v_ego <= HYUNDAI_SANTA_FE_FINAL_STOP_MAX_SPEED and
|
v_ego <= HYUNDAI_SANTA_FE_FINAL_STOP_MAX_SPEED and
|
||||||
|
|||||||
@@ -397,27 +397,6 @@ def get_vehicle_min_accel(CP, v_ego):
|
|||||||
return float(ACCEL_MIN)
|
return float(ACCEL_MIN)
|
||||||
|
|
||||||
|
|
||||||
def get_far_lead_coast_cap(lead, v_ego, desired_gap, output_a_target):
|
|
||||||
if lead is None or not bool(getattr(lead, "status", False)):
|
|
||||||
return float(output_a_target)
|
|
||||||
|
|
||||||
v_ego = float(v_ego)
|
|
||||||
lead_distance = float(getattr(lead, "dRel", float("inf")))
|
|
||||||
lead_speed = float(getattr(lead, "vLead", v_ego))
|
|
||||||
closing_speed = v_ego - lead_speed
|
|
||||||
if (
|
|
||||||
v_ego <= 10.0 or
|
|
||||||
closing_speed <= 0.5 or
|
|
||||||
lead_distance < FAR_LEAD_COAST_MIN_DISTANCE or
|
|
||||||
lead_distance <= float(desired_gap) + FAR_LEAD_COAST_MIN_GAP_MARGIN or
|
|
||||||
lead_distance / max(closing_speed, 0.1) < FAR_LEAD_COAST_MIN_TTC or
|
|
||||||
max(0.0, -float(getattr(lead, "aLeadK", 0.0))) > FAR_LEAD_COAST_MAX_LEAD_BRAKE
|
|
||||||
):
|
|
||||||
return float(output_a_target)
|
|
||||||
|
|
||||||
return max(float(output_a_target), -FAR_LEAD_COAST_MAX_DECEL)
|
|
||||||
|
|
||||||
|
|
||||||
# Restored planner constants retained by CEM, stop, and departure paths.
|
# Restored planner constants retained by CEM, stop, and departure paths.
|
||||||
A_CRUISE_MIN = -1.0
|
A_CRUISE_MIN = -1.0
|
||||||
# The stop distance runs ~9 m long through the mid-approach, which leaves the obstacle slack
|
# The stop distance runs ~9 m long through the mid-approach, which leaves the obstacle slack
|
||||||
@@ -454,11 +433,6 @@ VEHICLE_FAR_FOLLOW_SLEW_MIN_DISTANCE_TIME = 1.35
|
|||||||
VEHICLE_FAR_FOLLOW_SLEW_MIN_HEADWAY = 1.35
|
VEHICLE_FAR_FOLLOW_SLEW_MIN_HEADWAY = 1.35
|
||||||
VEHICLE_FAR_FOLLOW_SLEW_MIN_TTC = 8.0
|
VEHICLE_FAR_FOLLOW_SLEW_MIN_TTC = 8.0
|
||||||
VEHICLE_FAR_FOLLOW_SLEW_MAX_LATERAL_OFFSET = 1.5
|
VEHICLE_FAR_FOLLOW_SLEW_MAX_LATERAL_OFFSET = 1.5
|
||||||
FAR_LEAD_COAST_MIN_DISTANCE = 45.0
|
|
||||||
FAR_LEAD_COAST_MIN_TTC = 8.0
|
|
||||||
FAR_LEAD_COAST_MIN_GAP_MARGIN = 6.0
|
|
||||||
FAR_LEAD_COAST_MAX_LEAD_BRAKE = 0.35
|
|
||||||
FAR_LEAD_COAST_MAX_DECEL = 0.20
|
|
||||||
RADAR_DEPART_CONFLICT_MAX_EGO_SPEED = 1.6
|
RADAR_DEPART_CONFLICT_MAX_EGO_SPEED = 1.6
|
||||||
RADAR_DEPART_CONFLICT_MIN_RADAR_LATERAL = 1.5
|
RADAR_DEPART_CONFLICT_MIN_RADAR_LATERAL = 1.5
|
||||||
RADAR_DEPART_CONFLICT_MAX_RADAR_DISTANCE = 18.0
|
RADAR_DEPART_CONFLICT_MAX_RADAR_DISTANCE = 18.0
|
||||||
@@ -3095,28 +3069,6 @@ class LongitudinalPlanner:
|
|||||||
panic_bypass,
|
panic_bypass,
|
||||||
)
|
)
|
||||||
|
|
||||||
far_lead_coast_allowed = (
|
|
||||||
not experimental_mode and
|
|
||||||
comfort_lead is not None and
|
|
||||||
desired_gap is not None and
|
|
||||||
not output_should_stop and
|
|
||||||
not vision_low_speed_stop_active and
|
|
||||||
not close_lead_caps and
|
|
||||||
not panic_bypass and
|
|
||||||
not depart_safety_veto and
|
|
||||||
inside_gap_closing_cap is None and
|
|
||||||
not bool(getattr(sm['starpilotPlan'], 'forcingStop', False)) and
|
|
||||||
not bool(getattr(sm['starpilotPlan'], 'redLight', False)) and
|
|
||||||
not bool(getattr(sm['starpilotPlan'], 'stopSignConfirmed', False))
|
|
||||||
)
|
|
||||||
if far_lead_coast_allowed:
|
|
||||||
output_a_target = get_far_lead_coast_cap(
|
|
||||||
comfort_lead,
|
|
||||||
scene_v_ego,
|
|
||||||
desired_gap,
|
|
||||||
output_a_target,
|
|
||||||
)
|
|
||||||
|
|
||||||
if radar_gap_settle_active:
|
if radar_gap_settle_active:
|
||||||
output_a_target = RADAR_STANDSTILL_GAP_SETTLE_ACCEL
|
output_a_target = RADAR_STANDSTILL_GAP_SETTLE_ACCEL
|
||||||
output_should_stop = False
|
output_should_stop = False
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from typing import Any
|
|||||||
|
|
||||||
import capnp
|
import capnp
|
||||||
from cereal import messaging, log, car, custom
|
from cereal import messaging, log, car, custom
|
||||||
from cereal.services import SERVICE_LIST
|
|
||||||
from openpilot.common.filter_simple import FirstOrderFilter
|
from openpilot.common.filter_simple import FirstOrderFilter
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.realtime import DT_MDL, Priority, config_realtime_process
|
from openpilot.common.realtime import DT_MDL, Priority, config_realtime_process
|
||||||
@@ -43,11 +42,6 @@ def is_bosch_a_radar_car(CP) -> bool:
|
|||||||
return CP.brand == "honda" and CP.carFingerprint in HONDA_BOSCH_A and not CP.radarUnavailable
|
return CP.brand == "honda" and CP.carFingerprint in HONDA_BOSCH_A and not CP.radarUnavailable
|
||||||
|
|
||||||
|
|
||||||
def has_slow_radar_tracks(CP) -> bool:
|
|
||||||
radar_ts = float(getattr(CP, "radarTimeStepDEPRECATED", DT_MDL) or DT_MDL)
|
|
||||||
return not CP.radarUnavailable and radar_ts > 2.0 / SERVICE_LIST["liveTracks"].frequency
|
|
||||||
|
|
||||||
|
|
||||||
# Adjacent-lane stopped-vehicle detector, used as a stop-line hint on red-light
|
# Adjacent-lane stopped-vehicle detector, used as a stop-line hint on red-light
|
||||||
# approaches. The qualifier is the DECELERATION HISTORY, not the current speed: roadside
|
# approaches. The qualifier is the DECELERATION HISTORY, not the current speed: roadside
|
||||||
# furniture and curb-parked cars never show a moving -> stopped transition, so testing
|
# furniture and curb-parked cars never show a moving -> stopped transition, so testing
|
||||||
@@ -642,9 +636,8 @@ def main() -> None:
|
|||||||
cloudlog.info("radard got CarParams")
|
cloudlog.info("radard got CarParams")
|
||||||
|
|
||||||
# *** setup messaging
|
# *** setup messaging
|
||||||
ignore_avg_freq = ['liveTracks'] if has_slow_radar_tracks(CP) else None
|
|
||||||
sm = messaging.SubMaster(['modelV2', 'carState', 'liveTracks'], poll='modelV2',
|
sm = messaging.SubMaster(['modelV2', 'carState', 'liveTracks'], poll='modelV2',
|
||||||
ignore_avg_freq=ignore_avg_freq, ignore_valid=['starpilotPlan'])
|
ignore_valid=['starpilotPlan'])
|
||||||
pm = messaging.PubMaster(['radarState'])
|
pm = messaging.PubMaster(['radarState'])
|
||||||
|
|
||||||
radar_ts = float(getattr(CP, "radarTimeStepDEPRECATED", DT_MDL) or DT_MDL)
|
radar_ts = float(getattr(CP, "radarTimeStepDEPRECATED", DT_MDL) or DT_MDL)
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import (
|
|||||||
get_rav4_tss2_pid_output,
|
get_rav4_tss2_pid_output,
|
||||||
get_subaru_impreza_pid_output_scale,
|
get_subaru_impreza_pid_output_scale,
|
||||||
get_genesis_gv70_low_speed_center_overshoot_scale,
|
get_genesis_gv70_low_speed_center_overshoot_scale,
|
||||||
get_genesis_g70_high_speed_transition_scale,
|
|
||||||
normalize_flm_overrides,
|
normalize_flm_overrides,
|
||||||
set_flm_runtime_overrides,
|
set_flm_runtime_overrides,
|
||||||
)
|
)
|
||||||
@@ -961,13 +960,7 @@ class TestLatControl:
|
|||||||
assert get_genesis_g70_low_speed_output_limit(0.0, 2.0) < 0.30
|
assert get_genesis_g70_low_speed_output_limit(0.0, 2.0) < 0.30
|
||||||
assert get_genesis_g70_low_speed_angle_damping(0.0, -20.0, 0.0, 2.0) < 0.0
|
assert get_genesis_g70_low_speed_angle_damping(0.0, -20.0, 0.0, 2.0) < 0.0
|
||||||
assert get_genesis_g70_low_speed_angle_damping(0.0, 20.0, 0.0, 2.0) > 0.0
|
assert get_genesis_g70_low_speed_angle_damping(0.0, 20.0, 0.0, 2.0) > 0.0
|
||||||
assert get_genesis_g70_high_speed_transition_scale(0.0, 0.8, 65.0 * 0.44704) < \
|
assert get_genesis_g70_curve_unwind_output_scale(0.7, -0.5, 25.0) == pytest.approx(1.0)
|
||||||
get_genesis_g70_high_speed_transition_scale(0.0, 0.1, 65.0 * 0.44704)
|
|
||||||
assert get_genesis_g70_high_speed_transition_scale(1.0, 0.8, 65.0 * 0.44704) > \
|
|
||||||
get_genesis_g70_high_speed_transition_scale(0.0, 0.8, 65.0 * 0.44704)
|
|
||||||
assert get_genesis_g70_high_speed_transition_scale(0.0, 0.8, 20.0 * 0.44704) > \
|
|
||||||
get_genesis_g70_high_speed_transition_scale(0.0, 0.8, 65.0 * 0.44704)
|
|
||||||
assert 0.90 < get_genesis_g70_curve_unwind_output_scale(0.7, -0.5, 25.0) < 1.0
|
|
||||||
assert get_genesis_g70_curve_unwind_output_scale(0.7, 0.5, 25.0) == 1.0
|
assert get_genesis_g70_curve_unwind_output_scale(0.7, 0.5, 25.0) == 1.0
|
||||||
assert get_genesis_g70_angle_output_scale(55.0, 1.0) > get_genesis_g70_angle_output_scale(85.0, 1.0)
|
assert get_genesis_g70_angle_output_scale(55.0, 1.0) > get_genesis_g70_angle_output_scale(85.0, 1.0)
|
||||||
assert get_genesis_g70_angle_output_scale(85.0, -1.0) == pytest.approx(1.0)
|
assert get_genesis_g70_angle_output_scale(85.0, -1.0) == pytest.approx(1.0)
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from openpilot.selfdrive.controls.radard import (
|
|||||||
RadarD,
|
RadarD,
|
||||||
g90_low_speed_radar_lead_sane,
|
g90_low_speed_radar_lead_sane,
|
||||||
g90_radar_lead_lateral_sane,
|
g90_radar_lead_lateral_sane,
|
||||||
has_slow_radar_tracks,
|
|
||||||
is_bosch_a_radar_car,
|
is_bosch_a_radar_car,
|
||||||
match_vision_to_track,
|
match_vision_to_track,
|
||||||
)
|
)
|
||||||
@@ -97,15 +96,6 @@ class TestLeads:
|
|||||||
assert bosch_a.lead_prob_filters[0].dt == pytest.approx(DT_MDL)
|
assert bosch_a.lead_prob_filters[0].dt == pytest.approx(DT_MDL)
|
||||||
assert bosch_a.kalman_params.A[0][1] == pytest.approx(HONDA_BOSCH_A_RADAR_TS)
|
assert bosch_a.kalman_params.A[0][1] == pytest.approx(HONDA_BOSCH_A_RADAR_TS)
|
||||||
|
|
||||||
def test_slow_radar_frequency_relaxation_is_scoped(self):
|
|
||||||
slow_radar = SimpleNamespace(radarTimeStepDEPRECATED=0.15, radarUnavailable=False)
|
|
||||||
normal_radar = SimpleNamespace(radarTimeStepDEPRECATED=0.1, radarUnavailable=False)
|
|
||||||
unavailable_radar = SimpleNamespace(radarTimeStepDEPRECATED=0.15, radarUnavailable=True)
|
|
||||||
|
|
||||||
assert has_slow_radar_tracks(slow_radar)
|
|
||||||
assert not has_slow_radar_tracks(normal_radar)
|
|
||||||
assert not has_slow_radar_tracks(unavailable_radar)
|
|
||||||
|
|
||||||
@pytest.mark.skipif(platform.system() == "Darwin", reason="SocketEventHandle requires eventfd")
|
@pytest.mark.skipif(platform.system() == "Darwin", reason="SocketEventHandle requires eventfd")
|
||||||
def test_radar_fault(self):
|
def test_radar_fault(self):
|
||||||
# if there's no radar-related can traffic, radard should either not respond or respond with an error
|
# if there's no radar-related can traffic, radard should either not respond or respond with an error
|
||||||
|
|||||||
@@ -765,15 +765,6 @@ def test_elantra_lead_stop_releases_stale_hard_brake_after_target_eases():
|
|||||||
assert tuning.shape_stopping_accel(-1.20, -0.25, True, 1.0, False, -0.85) == pytest.approx(-1.20)
|
assert tuning.shape_stopping_accel(-1.20, -0.25, True, 1.0, False, -0.85) == pytest.approx(-1.20)
|
||||||
|
|
||||||
|
|
||||||
def test_elantra_final_stop_cap_softens_normal_low_speed_stop():
|
|
||||||
CP = make_longcontrol_cp(brand="hyundai", carFingerprint="HYUNDAI_ELANTRA_2021")
|
|
||||||
tuning = vehicle_tunes.LongControlVehicleTuning(CP)
|
|
||||||
|
|
||||||
assert tuning.shape_stopping_accel(-0.85, -0.25, True, 0.5, False, -0.85) == pytest.approx(-0.35)
|
|
||||||
assert tuning.shape_stopping_accel(-0.85, -1.25, True, 0.5, False, -0.85) == pytest.approx(-0.85)
|
|
||||||
assert tuning.shape_stopping_accel(-0.85, -0.25, False, 0.5, False, -0.85) == pytest.approx(-0.85)
|
|
||||||
|
|
||||||
|
|
||||||
def test_elantra_stopped_lead_handoff_holds_braking_direction_without_touching_brakes():
|
def test_elantra_stopped_lead_handoff_holds_braking_direction_without_touching_brakes():
|
||||||
CP = make_longcontrol_cp(brand="hyundai", carFingerprint="HYUNDAI_ELANTRA_2021")
|
CP = make_longcontrol_cp(brand="hyundai", carFingerprint="HYUNDAI_ELANTRA_2021")
|
||||||
tuning = vehicle_tunes.LongControlVehicleTuning(CP)
|
tuning = vehicle_tunes.LongControlVehicleTuning(CP)
|
||||||
|
|||||||
@@ -18,13 +18,7 @@ from opendbc.car.toyota.values import CAR as TOYOTA_CAR
|
|||||||
import openpilot.selfdrive.controls.lib.longitudinal_planner as longitudinal_planner_module
|
import openpilot.selfdrive.controls.lib.longitudinal_planner as longitudinal_planner_module
|
||||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
|
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
|
||||||
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
|
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
|
||||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import (
|
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner, get_coast_accel, get_vehicle_min_accel, should_publish_planner_fcw
|
||||||
LongitudinalPlanner,
|
|
||||||
get_coast_accel,
|
|
||||||
get_far_lead_coast_cap,
|
|
||||||
get_vehicle_min_accel,
|
|
||||||
should_publish_planner_fcw,
|
|
||||||
)
|
|
||||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import (
|
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import (
|
||||||
LongitudinalMpc,
|
LongitudinalMpc,
|
||||||
build_model_lead_trajectory,
|
build_model_lead_trajectory,
|
||||||
@@ -316,24 +310,6 @@ def test_mpc_panic_bypass_immediately_removes_duplicate_vision_filter():
|
|||||||
assert mpc.lead_v_filter.x == pytest.approx(10.0)
|
assert mpc.lead_v_filter.x == pytest.approx(10.0)
|
||||||
|
|
||||||
|
|
||||||
def test_far_lead_coast_cap_delays_nonurgent_deceleration():
|
|
||||||
lead = make_lead(status=True, d_rel=128.0, v_lead=16.7, a_lead=0.2, radar=True)
|
|
||||||
|
|
||||||
assert get_far_lead_coast_cap(lead, 26.6, 115.0, -0.43) == pytest.approx(-0.20)
|
|
||||||
assert get_far_lead_coast_cap(lead, 26.6, 115.0, 0.10) == pytest.approx(0.10)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("d_rel,v_lead,a_lead,desired_gap", [
|
|
||||||
(50.0, 20.0, 0.2, 45.0), # only a small gap remains
|
|
||||||
(128.0, 8.0, 0.2, 115.0), # urgent closing time
|
|
||||||
(128.0, 16.7, -0.5, 115.0), # the lead is braking materially
|
|
||||||
])
|
|
||||||
def test_far_lead_coast_cap_preserves_urgent_or_close_deceleration(d_rel, v_lead, a_lead, desired_gap):
|
|
||||||
lead = make_lead(status=True, d_rel=d_rel, v_lead=v_lead, a_lead=a_lead, radar=True)
|
|
||||||
|
|
||||||
assert get_far_lead_coast_cap(lead, 26.6, desired_gap, -0.43) == pytest.approx(-0.43)
|
|
||||||
|
|
||||||
|
|
||||||
def test_hrv_far_follow_output_slew_damps_only_continuous_safe_follow():
|
def test_hrv_far_follow_output_slew_damps_only_continuous_safe_follow():
|
||||||
v_ego = 24.0
|
v_ego = 24.0
|
||||||
CP = CarInterface.get_non_essential_params(CAR.HONDA_HRV_3G)
|
CP = CarInterface.get_non_essential_params(CAR.HONDA_HRV_3G)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def make_toggles(**overrides):
|
|||||||
|
|
||||||
def test_force_stop_jerk_scale_is_platform_specific():
|
def test_force_stop_jerk_scale_is_platform_specific():
|
||||||
assert get_force_stop_jerk_scale(SimpleNamespace(carFingerprint="HYUNDAI_ELANTRA_2021")) == 0.80
|
assert get_force_stop_jerk_scale(SimpleNamespace(carFingerprint="HYUNDAI_ELANTRA_2021")) == 0.80
|
||||||
assert get_force_stop_jerk_scale(SimpleNamespace(carFingerprint="OTHER_CAR")) == 0.32
|
assert get_force_stop_jerk_scale(SimpleNamespace(carFingerprint="OTHER_CAR")) == 0.20
|
||||||
|
|
||||||
|
|
||||||
def test_lead_follow_jerk_scale_is_platform_specific():
|
def test_lead_follow_jerk_scale_is_platform_specific():
|
||||||
@@ -255,3 +255,18 @@ def test_untracked_vision_lead_still_uses_strict_entry_gate():
|
|||||||
assert not planner.update_lead_status(16.8)
|
assert not planner.update_lead_status(16.8)
|
||||||
finally:
|
finally:
|
||||||
planner.shutdown()
|
planner.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
def test_gps_location_service_updates_on_carparams(monkeypatch):
|
||||||
|
planner = make_planner(monkeypatch)
|
||||||
|
try:
|
||||||
|
sm = make_sm(planner, frame=1, v_ego=20.0, left_blinker=False)
|
||||||
|
sm.updated = {"carParams": True}
|
||||||
|
sm["carParams"] = SimpleNamespace(brand="gm")
|
||||||
|
sm["gpsLocationExternal"] = SimpleNamespace(latitude=2.0, longitude=2.0, bearingDeg=45.0, hasFix=True)
|
||||||
|
planner.update(0.0, False, sm, make_toggles())
|
||||||
|
assert planner.gps_location_service == "gpsLocationExternal"
|
||||||
|
assert planner.gps_position["latitude"] == 2.0
|
||||||
|
finally:
|
||||||
|
planner.shutdown()
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ class DeveloperLayout(Widget):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._params = Params()
|
self._params = Params()
|
||||||
self._params.put_bool("LongitudinalManeuverMode", False)
|
|
||||||
|
|
||||||
# Build items and keep references for callbacks/state updates
|
# Build items and keep references for callbacks/state updates
|
||||||
self._adb_toggle = toggle_item(
|
self._adb_toggle = toggle_item(
|
||||||
@@ -60,6 +59,13 @@ class DeveloperLayout(Widget):
|
|||||||
enabled=ui_state.is_offroad,
|
enabled=ui_state.is_offroad,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._long_maneuver_toggle = toggle_item(
|
||||||
|
lambda: tr("Longitudinal Maneuver Mode"),
|
||||||
|
description="",
|
||||||
|
initial_state=self._params.get_bool("LongitudinalManeuverMode"),
|
||||||
|
callback=self._on_long_maneuver_mode,
|
||||||
|
)
|
||||||
|
|
||||||
self._alpha_long_toggle = toggle_item(
|
self._alpha_long_toggle = toggle_item(
|
||||||
lambda: tr("openpilot Longitudinal Control (Alpha)"),
|
lambda: tr("openpilot Longitudinal Control (Alpha)"),
|
||||||
description=lambda: tr(DESCRIPTIONS["alpha_longitudinal"]),
|
description=lambda: tr(DESCRIPTIONS["alpha_longitudinal"]),
|
||||||
@@ -81,6 +87,7 @@ class DeveloperLayout(Widget):
|
|||||||
self._ssh_toggle,
|
self._ssh_toggle,
|
||||||
self._ssh_keys,
|
self._ssh_keys,
|
||||||
self._joystick_toggle,
|
self._joystick_toggle,
|
||||||
|
self._long_maneuver_toggle,
|
||||||
self._alpha_long_toggle,
|
self._alpha_long_toggle,
|
||||||
self._ui_debug_toggle,
|
self._ui_debug_toggle,
|
||||||
], line_separator=True, spacing=0)
|
], line_separator=True, spacing=0)
|
||||||
@@ -107,7 +114,13 @@ class DeveloperLayout(Widget):
|
|||||||
else:
|
else:
|
||||||
self._alpha_long_toggle.set_visible(True)
|
self._alpha_long_toggle.set_visible(True)
|
||||||
|
|
||||||
|
long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad()
|
||||||
|
self._long_maneuver_toggle.action_item.set_enabled(long_man_enabled)
|
||||||
|
if not long_man_enabled:
|
||||||
|
self._long_maneuver_toggle.action_item.set_state(False)
|
||||||
|
self._params.put_bool("LongitudinalManeuverMode", False)
|
||||||
else:
|
else:
|
||||||
|
self._long_maneuver_toggle.action_item.set_enabled(False)
|
||||||
self._alpha_long_toggle.set_visible(False)
|
self._alpha_long_toggle.set_visible(False)
|
||||||
|
|
||||||
# TODO: make a param control list item so we don't need to manage internal state as much here
|
# TODO: make a param control list item so we don't need to manage internal state as much here
|
||||||
@@ -116,6 +129,7 @@ class DeveloperLayout(Widget):
|
|||||||
("AdbEnabled", self._adb_toggle),
|
("AdbEnabled", self._adb_toggle),
|
||||||
("SshEnabled", self._ssh_toggle),
|
("SshEnabled", self._ssh_toggle),
|
||||||
("JoystickDebugMode", self._joystick_toggle),
|
("JoystickDebugMode", self._joystick_toggle),
|
||||||
|
("LongitudinalManeuverMode", self._long_maneuver_toggle),
|
||||||
("AlphaLongitudinalEnabled", self._alpha_long_toggle),
|
("AlphaLongitudinalEnabled", self._alpha_long_toggle),
|
||||||
("ShowDebugInfo", self._ui_debug_toggle),
|
("ShowDebugInfo", self._ui_debug_toggle),
|
||||||
):
|
):
|
||||||
@@ -135,6 +149,12 @@ class DeveloperLayout(Widget):
|
|||||||
def _on_joystick_debug_mode(self, state: bool):
|
def _on_joystick_debug_mode(self, state: bool):
|
||||||
self._params.put_bool("JoystickDebugMode", state)
|
self._params.put_bool("JoystickDebugMode", state)
|
||||||
self._params.put_bool("LongitudinalManeuverMode", False)
|
self._params.put_bool("LongitudinalManeuverMode", False)
|
||||||
|
self._long_maneuver_toggle.action_item.set_state(False)
|
||||||
|
|
||||||
|
def _on_long_maneuver_mode(self, state: bool):
|
||||||
|
self._params.put_bool("LongitudinalManeuverMode", state)
|
||||||
|
self._params.put_bool("JoystickDebugMode", False)
|
||||||
|
self._joystick_toggle.action_item.set_state(False)
|
||||||
|
|
||||||
def _on_alpha_long_enabled(self, state: bool):
|
def _on_alpha_long_enabled(self, state: bool):
|
||||||
if state:
|
if state:
|
||||||
|
|||||||
@@ -731,7 +731,7 @@ class StarPilotLongitudinalLayout(_SettingsPage):
|
|||||||
unit=self._speed_unit(),
|
unit=self._speed_unit(),
|
||||||
value_type="float",
|
value_type="float",
|
||||||
current_value=max(1, self._params.get_float("CustomCruise"))),
|
current_value=max(1, self._params.get_float("CustomCruise"))),
|
||||||
visible=lambda: self._params.get_bool("QOLLongitudinal") and not starpilot_state.car_state.isToyota),
|
visible=lambda: self._params.get_bool("QOLLongitudinal")),
|
||||||
SettingRow("CustomCruiseLong", "value", tr_noop("Cruise Long"),
|
SettingRow("CustomCruiseLong", "value", tr_noop("Cruise Long"),
|
||||||
subtitle="",
|
subtitle="",
|
||||||
get_value=lambda: f"{max(1, self._params.get_float('CustomCruiseLong')):g}{self._speed_unit()}",
|
get_value=lambda: f"{max(1, self._params.get_float('CustomCruiseLong')):g}{self._speed_unit()}",
|
||||||
@@ -739,12 +739,7 @@ class StarPilotLongitudinalLayout(_SettingsPage):
|
|||||||
unit=self._speed_unit(),
|
unit=self._speed_unit(),
|
||||||
value_type="float",
|
value_type="float",
|
||||||
current_value=max(1, self._params.get_float("CustomCruiseLong"))),
|
current_value=max(1, self._params.get_float("CustomCruiseLong"))),
|
||||||
visible=lambda: self._params.get_bool("QOLLongitudinal") and not starpilot_state.car_state.isToyota),
|
visible=lambda: self._params.get_bool("QOLLongitudinal")),
|
||||||
SettingRow("ReverseCruise", "toggle", tr_noop("Reverse Cruise Increase"),
|
|
||||||
subtitle=tr_noop("Swap Toyota/Lexus cruise increments: short press changes the dash set speed by 5; hold changes it by 1."),
|
|
||||||
get_state=lambda: self._params.get_bool("ReverseCruise"),
|
|
||||||
set_state=lambda s: self._params.put_bool("ReverseCruise", s),
|
|
||||||
visible=lambda: self._params.get_bool("QOLLongitudinal") and starpilot_state.car_state.isToyota),
|
|
||||||
SettingRow("ForceStops", "toggle", tr_noop("Force Stops"),
|
SettingRow("ForceStops", "toggle", tr_noop("Force Stops"),
|
||||||
subtitle="",
|
subtitle="",
|
||||||
get_state=lambda: self._params.get_bool("ForceStops"),
|
get_state=lambda: self._params.get_bool("ForceStops"),
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ class DeveloperLayoutMici(NavScroller):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._ssh_fetcher = SshKeyFetcher(ui_state.params)
|
self._ssh_fetcher = SshKeyFetcher(ui_state.params)
|
||||||
ui_state.params.put_bool("LongitudinalManeuverMode", False)
|
|
||||||
|
|
||||||
def github_username_callback(username: str):
|
def github_username_callback(username: str):
|
||||||
if username:
|
if username:
|
||||||
@@ -46,6 +45,7 @@ class DeveloperLayoutMici(NavScroller):
|
|||||||
self._ssh_keys_btn = BigButton("SSH keys", "Not set" if not github_username else github_username, icon=txt_ssh)
|
self._ssh_keys_btn = BigButton("SSH keys", "Not set" if not github_username else github_username, icon=txt_ssh)
|
||||||
self._ssh_keys_btn.set_click_callback(ssh_keys_callback)
|
self._ssh_keys_btn.set_click_callback(ssh_keys_callback)
|
||||||
|
|
||||||
|
# adb, ssh, ssh keys, debug mode, joystick debug mode, longitudinal maneuver mode, ip address
|
||||||
# ******** Main Scroller ********
|
# ******** Main Scroller ********
|
||||||
self._adb_toggle = BigCircleParamControl(gui_app.texture("icons_mici/adb_short.png", 82, 82), "AdbEnabled", icon_offset=(0, 12))
|
self._adb_toggle = BigCircleParamControl(gui_app.texture("icons_mici/adb_short.png", 82, 82), "AdbEnabled", icon_offset=(0, 12))
|
||||||
self._ssh_toggle = BigCircleParamControl(gui_app.texture("icons_mici/ssh_short.png", 82, 82), "SshEnabled", icon_offset=(0, 12))
|
self._ssh_toggle = BigCircleParamControl(gui_app.texture("icons_mici/ssh_short.png", 82, 82), "SshEnabled", icon_offset=(0, 12))
|
||||||
@@ -53,6 +53,9 @@ class DeveloperLayoutMici(NavScroller):
|
|||||||
self._joystick_toggle = BigToggle("joystick debug mode",
|
self._joystick_toggle = BigToggle("joystick debug mode",
|
||||||
initial_state=ui_state.params.get_bool("JoystickDebugMode"),
|
initial_state=ui_state.params.get_bool("JoystickDebugMode"),
|
||||||
toggle_callback=self._on_joystick_debug_mode)
|
toggle_callback=self._on_joystick_debug_mode)
|
||||||
|
self._long_maneuver_toggle = BigToggle("longitudinal maneuver mode",
|
||||||
|
initial_state=ui_state.params.get_bool("LongitudinalManeuverMode"),
|
||||||
|
toggle_callback=self._on_long_maneuver_mode)
|
||||||
self._alpha_long_toggle = BigToggle("alpha longitudinal",
|
self._alpha_long_toggle = BigToggle("alpha longitudinal",
|
||||||
initial_state=ui_state.params.get_bool("AlphaLongitudinalEnabled"),
|
initial_state=ui_state.params.get_bool("AlphaLongitudinalEnabled"),
|
||||||
toggle_callback=self._on_alpha_long_enabled)
|
toggle_callback=self._on_alpha_long_enabled)
|
||||||
@@ -66,6 +69,7 @@ class DeveloperLayoutMici(NavScroller):
|
|||||||
self._ssh_keys_btn,
|
self._ssh_keys_btn,
|
||||||
self._disable_wide_road_toggle,
|
self._disable_wide_road_toggle,
|
||||||
self._joystick_toggle,
|
self._joystick_toggle,
|
||||||
|
self._long_maneuver_toggle,
|
||||||
self._alpha_long_toggle,
|
self._alpha_long_toggle,
|
||||||
self._debug_mode_toggle,
|
self._debug_mode_toggle,
|
||||||
])
|
])
|
||||||
@@ -76,6 +80,7 @@ class DeveloperLayoutMici(NavScroller):
|
|||||||
("SshEnabled", self._ssh_toggle),
|
("SshEnabled", self._ssh_toggle),
|
||||||
("DisableWideRoad", self._disable_wide_road_toggle),
|
("DisableWideRoad", self._disable_wide_road_toggle),
|
||||||
("JoystickDebugMode", self._joystick_toggle),
|
("JoystickDebugMode", self._joystick_toggle),
|
||||||
|
("LongitudinalManeuverMode", self._long_maneuver_toggle),
|
||||||
("AlphaLongitudinalEnabled", self._alpha_long_toggle),
|
("AlphaLongitudinalEnabled", self._alpha_long_toggle),
|
||||||
("ShowDebugInfo", self._debug_mode_toggle),
|
("ShowDebugInfo", self._debug_mode_toggle),
|
||||||
)
|
)
|
||||||
@@ -84,7 +89,7 @@ class DeveloperLayoutMici(NavScroller):
|
|||||||
self._disable_wide_road_toggle,
|
self._disable_wide_road_toggle,
|
||||||
self._joystick_toggle,
|
self._joystick_toggle,
|
||||||
)
|
)
|
||||||
engaged_blocked_toggles = (self._alpha_long_toggle,)
|
engaged_blocked_toggles = (self._long_maneuver_toggle, self._alpha_long_toggle)
|
||||||
|
|
||||||
# Disable toggles that require offroad
|
# Disable toggles that require offroad
|
||||||
for item in onroad_blocked_toggles:
|
for item in onroad_blocked_toggles:
|
||||||
@@ -124,7 +129,13 @@ class DeveloperLayoutMici(NavScroller):
|
|||||||
else:
|
else:
|
||||||
self._alpha_long_toggle.set_visible(True)
|
self._alpha_long_toggle.set_visible(True)
|
||||||
|
|
||||||
|
long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad()
|
||||||
|
self._long_maneuver_toggle.set_enabled(long_man_enabled)
|
||||||
|
if not long_man_enabled:
|
||||||
|
self._long_maneuver_toggle.set_checked(False)
|
||||||
|
ui_state.params.put_bool("LongitudinalManeuverMode", False)
|
||||||
else:
|
else:
|
||||||
|
self._long_maneuver_toggle.set_enabled(False)
|
||||||
self._alpha_long_toggle.set_visible(False)
|
self._alpha_long_toggle.set_visible(False)
|
||||||
|
|
||||||
# Refresh toggles from params to mirror external changes
|
# Refresh toggles from params to mirror external changes
|
||||||
@@ -134,8 +145,16 @@ class DeveloperLayoutMici(NavScroller):
|
|||||||
def _on_joystick_debug_mode(self, state: bool):
|
def _on_joystick_debug_mode(self, state: bool):
|
||||||
ui_state.params.put_bool("JoystickDebugMode", state)
|
ui_state.params.put_bool("JoystickDebugMode", state)
|
||||||
ui_state.params.put_bool("LongitudinalManeuverMode", False)
|
ui_state.params.put_bool("LongitudinalManeuverMode", False)
|
||||||
|
self._long_maneuver_toggle.set_checked(False)
|
||||||
ui_state.params.put_bool("LateralManeuverMode", False)
|
ui_state.params.put_bool("LateralManeuverMode", False)
|
||||||
|
|
||||||
|
def _on_long_maneuver_mode(self, state: bool):
|
||||||
|
ui_state.params.put_bool("LongitudinalManeuverMode", state)
|
||||||
|
ui_state.params.put_bool("JoystickDebugMode", False)
|
||||||
|
self._joystick_toggle.set_checked(False)
|
||||||
|
ui_state.params.put_bool("LateralManeuverMode", False)
|
||||||
|
restart_needed_callback(state)
|
||||||
|
|
||||||
def _on_alpha_long_enabled(self, state: bool):
|
def _on_alpha_long_enabled(self, state: bool):
|
||||||
# TODO: show confirmation dialog before enabling
|
# TODO: show confirmation dialog before enabling
|
||||||
ui_state.params.put_bool("AlphaLongitudinalEnabled", state)
|
ui_state.params.put_bool("AlphaLongitudinalEnabled", state)
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ class VisualsLayoutMici(NavScroller):
|
|||||||
self._torque_bar_btn = BigParamControl("torque bar", "EnableTorqueBarWidget")
|
self._torque_bar_btn = BigParamControl("torque bar", "EnableTorqueBarWidget")
|
||||||
self._rainbow_path_btn = BigParamControl("rainbow road", "RainbowPath")
|
self._rainbow_path_btn = BigParamControl("rainbow road", "RainbowPath")
|
||||||
self._lead_indicator_btn = LeadIndicatorBigButton()
|
self._lead_indicator_btn = LeadIndicatorBigButton()
|
||||||
self._lead_info_btn = BigParamControl("show lead speed", "LeadInfo")
|
|
||||||
self._speed_limit_signs_btn = BigParamControl("show speed limits", "ShowSpeedLimits")
|
self._speed_limit_signs_btn = BigParamControl("show speed limits", "ShowSpeedLimits")
|
||||||
self._slc_confirmation_btn = BigParamControl("confirm new speed limits", "SLCConfirmation")
|
self._slc_confirmation_btn = BigParamControl("confirm new speed limits", "SLCConfirmation")
|
||||||
self._slc_confirmation_lower_btn = BigParamControl("confirm lower limits", "SLCConfirmationLower")
|
self._slc_confirmation_lower_btn = BigParamControl("confirm lower limits", "SLCConfirmationLower")
|
||||||
@@ -77,7 +76,6 @@ class VisualsLayoutMici(NavScroller):
|
|||||||
self._torque_bar_btn,
|
self._torque_bar_btn,
|
||||||
self._rainbow_path_btn,
|
self._rainbow_path_btn,
|
||||||
self._lead_indicator_btn,
|
self._lead_indicator_btn,
|
||||||
self._lead_info_btn,
|
|
||||||
self._speed_limit_signs_btn,
|
self._speed_limit_signs_btn,
|
||||||
self._slc_confirmation_btn,
|
self._slc_confirmation_btn,
|
||||||
self._slc_confirmation_lower_btn,
|
self._slc_confirmation_lower_btn,
|
||||||
@@ -95,7 +93,6 @@ class VisualsLayoutMici(NavScroller):
|
|||||||
def _refresh(self):
|
def _refresh(self):
|
||||||
self._camera_view_btn.refresh()
|
self._camera_view_btn.refresh()
|
||||||
self._lead_indicator_btn.refresh()
|
self._lead_indicator_btn.refresh()
|
||||||
self._lead_info_btn.set_enabled(lead_indicator_enabled(self._lead_info_btn.params, hide_by_default=True))
|
|
||||||
confirmation_enabled = self._slc_confirmation_btn.params.get_bool("SLCConfirmation")
|
confirmation_enabled = self._slc_confirmation_btn.params.get_bool("SLCConfirmation")
|
||||||
self._slc_confirmation_lower_btn.set_visible(confirmation_enabled)
|
self._slc_confirmation_lower_btn.set_visible(confirmation_enabled)
|
||||||
self._slc_confirmation_higher_btn.set_visible(confirmation_enabled)
|
self._slc_confirmation_higher_btn.set_visible(confirmation_enabled)
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ from openpilot.selfdrive.ui.onroad.starpilot.rainbow_path import RainbowPath
|
|||||||
from openpilot.selfdrive.ui.lib.starpilot_visuals import blend_colors, lead_indicator_enabled
|
from openpilot.selfdrive.ui.lib.starpilot_visuals import blend_colors, lead_indicator_enabled
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||||
from openpilot.selfdrive.ui.mici.onroad.starpilot_status import get_border_color
|
from openpilot.selfdrive.ui.mici.onroad.starpilot_status import get_border_color
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
|
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
|
|
||||||
CLIP_MARGIN = 500
|
CLIP_MARGIN = 500
|
||||||
@@ -67,7 +66,6 @@ class ModelRenderer(Widget):
|
|||||||
self._lane_line_probs = np.zeros(4, dtype=np.float32)
|
self._lane_line_probs = np.zeros(4, dtype=np.float32)
|
||||||
self._road_edge_stds = np.zeros(2, dtype=np.float32)
|
self._road_edge_stds = np.zeros(2, dtype=np.float32)
|
||||||
self._lead_vehicles = [LeadVehicle(), LeadVehicle()]
|
self._lead_vehicles = [LeadVehicle(), LeadVehicle()]
|
||||||
self._lead_info_enabled = False
|
|
||||||
self._path_offset_z = HEIGHT_INIT[0]
|
self._path_offset_z = HEIGHT_INIT[0]
|
||||||
|
|
||||||
# Initialize ModelPoints objects
|
# Initialize ModelPoints objects
|
||||||
@@ -138,7 +136,6 @@ class ModelRenderer(Widget):
|
|||||||
model = sm['modelV2']
|
model = sm['modelV2']
|
||||||
radar_state = sm['radarState'] if sm.valid['radarState'] else None
|
radar_state = sm['radarState'] if sm.valid['radarState'] else None
|
||||||
lead_one = radar_state.leadOne if radar_state else None
|
lead_one = radar_state.leadOne if radar_state else None
|
||||||
self._lead_info_enabled = self._params.get_bool("LeadInfo")
|
|
||||||
render_lead_indicator = self._should_render_lead_indicator(radar_state)
|
render_lead_indicator = self._should_render_lead_indicator(radar_state)
|
||||||
|
|
||||||
# Update model data when needed
|
# Update model data when needed
|
||||||
@@ -162,7 +159,7 @@ class ModelRenderer(Widget):
|
|||||||
self._draw_path(sm)
|
self._draw_path(sm)
|
||||||
|
|
||||||
if render_lead_indicator and radar_state:
|
if render_lead_indicator and radar_state:
|
||||||
self._draw_lead_indicator(radar_state)
|
self._draw_lead_indicator()
|
||||||
|
|
||||||
def _should_render_lead_indicator(self, radar_state) -> bool:
|
def _should_render_lead_indicator(self, radar_state) -> bool:
|
||||||
return radar_state is not None and lead_indicator_enabled(self._params, hide_by_default=True)
|
return radar_state is not None and lead_indicator_enabled(self._params, hide_by_default=True)
|
||||||
@@ -501,7 +498,7 @@ class ModelRenderer(Widget):
|
|||||||
]
|
]
|
||||||
draw_polygon(self._rect, self._path.projected_points, gradient=self._path_gradient)
|
draw_polygon(self._rect, self._path.projected_points, gradient=self._path_gradient)
|
||||||
|
|
||||||
def _draw_lead_indicator(self, radar_state):
|
def _draw_lead_indicator(self):
|
||||||
# Draw lead vehicles if available
|
# Draw lead vehicles if available
|
||||||
lead_color = get_theme_color("LeadMarker", rl.Color(201, 34, 49, 255))
|
lead_color = get_theme_color("LeadMarker", rl.Color(201, 34, 49, 255))
|
||||||
for lead in self._lead_vehicles:
|
for lead in self._lead_vehicles:
|
||||||
@@ -511,35 +508,6 @@ class ModelRenderer(Widget):
|
|||||||
rl.draw_triangle_fan(lead.glow, len(lead.glow), rl.Color(218, 202, 37, 255))
|
rl.draw_triangle_fan(lead.glow, len(lead.glow), rl.Color(218, 202, 37, 255))
|
||||||
rl.draw_triangle_fan(lead.chevron, len(lead.chevron), with_alpha(lead_color, lead.fill_alpha))
|
rl.draw_triangle_fan(lead.chevron, len(lead.chevron), with_alpha(lead_color, lead.fill_alpha))
|
||||||
|
|
||||||
lead_one = radar_state.leadOne
|
|
||||||
if self._lead_info_enabled and lead_one and lead_one.status:
|
|
||||||
self._draw_lead_speed(lead_one)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _format_lead_speed(lead_speed: float, is_metric: bool, use_si_metrics: bool) -> str:
|
|
||||||
lead_speed = max(float(lead_speed), 0.0)
|
|
||||||
if use_si_metrics:
|
|
||||||
return f"{round(lead_speed)} m/s"
|
|
||||||
if is_metric:
|
|
||||||
return f"{round(lead_speed * CV.MS_TO_KPH)} km/h"
|
|
||||||
return f"{round(lead_speed * CV.MS_TO_MPH)} mph"
|
|
||||||
|
|
||||||
def _draw_lead_speed(self, lead_data) -> None:
|
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.path import _draw_text_with_outline
|
|
||||||
|
|
||||||
text = self._format_lead_speed(
|
|
||||||
getattr(lead_data, "vLead", 0.0),
|
|
||||||
ui_state.is_metric,
|
|
||||||
ui_state.starpilot_toggles.get("UseSiMetrics", False),
|
|
||||||
)
|
|
||||||
font = gui_app.font(FontWeight.SEMI_BOLD)
|
|
||||||
font_size = 40
|
|
||||||
text_size = measure_text_cached(font, text, font_size)
|
|
||||||
center_x = self._rect.x + self._rect.width / 2
|
|
||||||
x = center_x - text_size.x / 2
|
|
||||||
y = self._rect.y + 22
|
|
||||||
_draw_text_with_outline(text, float(x), float(y), font, font_size)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_path_length_idx(pos_x_array: np.ndarray, path_distance: float) -> int:
|
def _get_path_length_idx(pos_x_array: np.ndarray, path_distance: float) -> int:
|
||||||
"""Get the index corresponding to the given path height"""
|
"""Get the index corresponding to the given path height"""
|
||||||
|
|||||||
@@ -1,25 +1,19 @@
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
import openpilot.selfdrive.ui.mici.onroad.model_renderer as model_renderer
|
import openpilot.selfdrive.ui.mici.onroad.model_renderer as model_renderer
|
||||||
|
|
||||||
|
|
||||||
class _FakeParams:
|
class _FakeParams:
|
||||||
def __init__(self, enabled: bool, lead_info: bool = False):
|
def __init__(self, enabled: bool):
|
||||||
self.enabled = enabled
|
self.enabled = enabled
|
||||||
self.lead_info = lead_info
|
|
||||||
|
|
||||||
def get(self, key):
|
def get(self, key):
|
||||||
assert key == "HideLeadMarker"
|
assert key == "HideLeadMarker"
|
||||||
return b"0" if self.enabled else b"1"
|
return b"0" if self.enabled else b"1"
|
||||||
|
|
||||||
def get_bool(self, key):
|
def get_bool(self, key):
|
||||||
if key == "HideLeadMarker":
|
assert key == "HideLeadMarker"
|
||||||
return not self.enabled
|
return not self.enabled
|
||||||
if key == "LeadInfo":
|
|
||||||
return self.lead_info
|
|
||||||
raise AssertionError(key)
|
|
||||||
|
|
||||||
|
|
||||||
def test_lead_indicator_renders_in_aol_without_longitudinal_control(monkeypatch):
|
def test_lead_indicator_renders_in_aol_without_longitudinal_control(monkeypatch):
|
||||||
@@ -37,38 +31,3 @@ def test_lead_indicator_still_honors_disabled_setting():
|
|||||||
|
|
||||||
assert not renderer._should_render_lead_indicator(SimpleNamespace())
|
assert not renderer._should_render_lead_indicator(SimpleNamespace())
|
||||||
assert not renderer._should_render_lead_indicator(None)
|
assert not renderer._should_render_lead_indicator(None)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("is_metric", "use_si_metrics", "expected"),
|
|
||||||
[
|
|
||||||
(False, False, "22 mph"),
|
|
||||||
(True, False, "36 km/h"),
|
|
||||||
(False, True, "10 m/s"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_lead_speed_uses_c3_units(is_metric, use_si_metrics, expected):
|
|
||||||
assert model_renderer.ModelRenderer._format_lead_speed(10.0, is_metric, use_si_metrics) == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_lead_metrics_draw_only_speed_when_enabled(monkeypatch):
|
|
||||||
drawn_metrics = []
|
|
||||||
monkeypatch.setattr(model_renderer, "get_theme_color", lambda *_args: model_renderer.rl.RED)
|
|
||||||
monkeypatch.setattr(model_renderer.rl, "draw_triangle_fan", lambda *_args: None)
|
|
||||||
|
|
||||||
renderer = object.__new__(model_renderer.ModelRenderer)
|
|
||||||
renderer._lead_info_enabled = True
|
|
||||||
renderer._lead_vehicles = [
|
|
||||||
model_renderer.LeadVehicle(
|
|
||||||
glow=[(1.0, 2.0)] * 3,
|
|
||||||
chevron=[(1.0, 2.0)] * 3,
|
|
||||||
fill_alpha=255,
|
|
||||||
),
|
|
||||||
model_renderer.LeadVehicle(),
|
|
||||||
]
|
|
||||||
renderer._draw_lead_speed = drawn_metrics.append
|
|
||||||
lead_one = SimpleNamespace(status=True, vLead=10.0)
|
|
||||||
|
|
||||||
renderer._draw_lead_indicator(SimpleNamespace(leadOne=lead_one, leadTwo=SimpleNamespace(status=False)))
|
|
||||||
|
|
||||||
assert drawn_metrics == [lead_one]
|
|
||||||
|
|||||||
@@ -1022,6 +1022,11 @@ class ModelManager:
|
|||||||
model_key = self._canonical_model_key(model_key)
|
model_key = self._canonical_model_key(model_key)
|
||||||
accelerator = str(accelerator or "").strip().lower()
|
accelerator = str(accelerator or "").strip().lower()
|
||||||
try:
|
try:
|
||||||
|
if accelerator == MODEL_LAB_ACCELERATOR and not external_gpu_available():
|
||||||
|
handle_error(None, "External GPU required...", "Chestnut is not connected and firmware-ready.",
|
||||||
|
MODEL_LAB_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, self.params_memory)
|
||||||
|
return False
|
||||||
|
|
||||||
artifact_metadata = model_accelerator_artifact_metadata(model_key, accelerator)
|
artifact_metadata = model_accelerator_artifact_metadata(model_key, accelerator)
|
||||||
if not model_accelerator_artifact_available(model_key, accelerator):
|
if not model_accelerator_artifact_available(model_key, accelerator):
|
||||||
handle_error(None, "Accelerator artifact unavailable...",
|
handle_error(None, "Accelerator artifact unavailable...",
|
||||||
@@ -1054,7 +1059,7 @@ class ModelManager:
|
|||||||
MODEL_LAB_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, self.params_memory)
|
MODEL_LAB_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, self.params_memory)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.params_memory.put(DOWNLOAD_PROGRESS_PARAM, "eGPU variant downloaded!")
|
self.params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Chestnut artifact downloaded!")
|
||||||
return True
|
return True
|
||||||
finally:
|
finally:
|
||||||
self.params_memory.remove(MODEL_LAB_DOWNLOAD_PARAM)
|
self.params_memory.remove(MODEL_LAB_DOWNLOAD_PARAM)
|
||||||
|
|||||||
@@ -326,8 +326,7 @@ def test_model_manager_downloads_precompiled_accelerator_variant_without_compili
|
|||||||
},
|
},
|
||||||
}])
|
}])
|
||||||
(tmp_path / model_manager.ARTIFACT_METADATA_CACHE).write_text(json.dumps(metadata))
|
(tmp_path / model_manager.ARTIFACT_METADATA_CACHE).write_text(json.dumps(metadata))
|
||||||
# These are precompiled files, so downloading must not require a connected eGPU.
|
monkeypatch.setattr(model_manager, "external_gpu_available", lambda: True)
|
||||||
monkeypatch.setattr(model_manager, "external_gpu_available", lambda: False)
|
|
||||||
monkeypatch.setattr(model_manager, "get_resource_urls", lambda: ["https://models.example"])
|
monkeypatch.setattr(model_manager, "get_resource_urls", lambda: ["https://models.example"])
|
||||||
monkeypatch.setattr(manager, "_load_artifact_url_map", lambda: {})
|
monkeypatch.setattr(manager, "_load_artifact_url_map", lambda: {})
|
||||||
calls = []
|
calls = []
|
||||||
@@ -347,7 +346,7 @@ def test_model_manager_downloads_precompiled_accelerator_variant_without_compili
|
|||||||
)
|
)
|
||||||
assert calls[0][3]["execution_device"] == "AMD"
|
assert calls[0][3]["execution_device"] == "AMD"
|
||||||
assert calls[0][5] == ["https://models.example"]
|
assert calls[0][5] == ["https://models.example"]
|
||||||
assert manager.params_memory.values[model_manager.DOWNLOAD_PROGRESS_PARAM] == "eGPU variant downloaded!"
|
assert manager.params_memory.values[model_manager.DOWNLOAD_PROGRESS_PARAM] == "Chestnut artifact downloaded!"
|
||||||
assert model_manager.MODEL_LAB_DOWNLOAD_PARAM not in manager.params_memory.values
|
assert model_manager.MODEL_LAB_DOWNLOAD_PARAM not in manager.params_memory.values
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -201,8 +201,6 @@
|
|||||||
"min": 0.0,
|
"min": 0.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
"step": 1.0,
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"parent_key": "LaneChanges",
|
"parent_key": "LaneChanges",
|
||||||
"settings_tier": "simple"
|
"settings_tier": "simple"
|
||||||
},
|
},
|
||||||
@@ -341,8 +339,6 @@
|
|||||||
"min": 0.0,
|
"min": 0.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
"step": 1.0,
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"parent_key": "QOLLateral",
|
"parent_key": "QOLLateral",
|
||||||
"settings_tier": "simple"
|
"settings_tier": "simple"
|
||||||
},
|
},
|
||||||
@@ -1020,8 +1016,6 @@
|
|||||||
"min": 0.0,
|
"min": 0.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
"step": 1.0,
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"parent_key": "ConditionalExperimental",
|
"parent_key": "ConditionalExperimental",
|
||||||
"settings_tier": "simple"
|
"settings_tier": "simple"
|
||||||
},
|
},
|
||||||
@@ -1034,8 +1028,6 @@
|
|||||||
"min": 0.0,
|
"min": 0.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
"step": 1.0,
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"parent_key": "ConditionalExperimental",
|
"parent_key": "ConditionalExperimental",
|
||||||
"settings_tier": "simple"
|
"settings_tier": "simple"
|
||||||
},
|
},
|
||||||
@@ -1121,8 +1113,6 @@
|
|||||||
"min": 0.0,
|
"min": 0.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
"step": 1.0,
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"parent_key": "ConditionalExperimental",
|
"parent_key": "ConditionalExperimental",
|
||||||
"settings_tier": "simple"
|
"settings_tier": "simple"
|
||||||
},
|
},
|
||||||
@@ -1700,14 +1690,6 @@
|
|||||||
"ui_type": "numeric",
|
"ui_type": "numeric",
|
||||||
"min": 1.0,
|
"min": 1.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
|
||||||
"precision": 0,
|
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"excluded_vehicle_makes": [
|
|
||||||
"Lexus",
|
|
||||||
"Toyota"
|
|
||||||
],
|
|
||||||
"parent_key": "QOLLongitudinal",
|
"parent_key": "QOLLongitudinal",
|
||||||
"settings_tier": "simple"
|
"settings_tier": "simple"
|
||||||
},
|
},
|
||||||
@@ -1719,28 +1701,6 @@
|
|||||||
"ui_type": "numeric",
|
"ui_type": "numeric",
|
||||||
"min": 1.0,
|
"min": 1.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
|
||||||
"precision": 0,
|
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"excluded_vehicle_makes": [
|
|
||||||
"Lexus",
|
|
||||||
"Toyota"
|
|
||||||
],
|
|
||||||
"parent_key": "QOLLongitudinal",
|
|
||||||
"settings_tier": "simple"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "ReverseCruise",
|
|
||||||
"label": "Reverse Cruise Increase",
|
|
||||||
"description": "Reverse Toyota/Lexus cruise-button behavior so a short press changes the dashboard set speed by 5 and a hold changes it by 1.",
|
|
||||||
"picker_description": "Swaps Toyota/Lexus short-press and hold cruise increments.",
|
|
||||||
"data_type": "bool",
|
|
||||||
"ui_type": "toggle",
|
|
||||||
"vehicle_makes": [
|
|
||||||
"Lexus",
|
|
||||||
"Toyota"
|
|
||||||
],
|
|
||||||
"parent_key": "QOLLongitudinal",
|
"parent_key": "QOLLongitudinal",
|
||||||
"settings_tier": "simple"
|
"settings_tier": "simple"
|
||||||
},
|
},
|
||||||
@@ -1814,10 +1774,6 @@
|
|||||||
"ui_type": "numeric",
|
"ui_type": "numeric",
|
||||||
"min": 0.0,
|
"min": 0.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
|
||||||
"precision": 0,
|
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"parent_key": "QOLLongitudinal",
|
"parent_key": "QOLLongitudinal",
|
||||||
"settings_tier": "simple"
|
"settings_tier": "simple"
|
||||||
},
|
},
|
||||||
@@ -1831,8 +1787,6 @@
|
|||||||
"max": 30.0,
|
"max": 30.0,
|
||||||
"step": 0.5,
|
"step": 0.5,
|
||||||
"precision": 1,
|
"precision": 1,
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"imperial_max": 15.0,
|
|
||||||
"parent_key": "QOLLongitudinal",
|
"parent_key": "QOLLongitudinal",
|
||||||
"settings_tier": "advanced"
|
"settings_tier": "advanced"
|
||||||
},
|
},
|
||||||
@@ -2269,12 +2223,6 @@
|
|||||||
"ui_type": "numeric",
|
"ui_type": "numeric",
|
||||||
"min": -99.0,
|
"min": -99.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
|
||||||
"precision": 0,
|
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_min": -150.0,
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"unit_range_index": 0,
|
|
||||||
"parent_key": "SpeedLimitController",
|
"parent_key": "SpeedLimitController",
|
||||||
"settings_tier": "advanced"
|
"settings_tier": "advanced"
|
||||||
},
|
},
|
||||||
@@ -2286,12 +2234,6 @@
|
|||||||
"ui_type": "numeric",
|
"ui_type": "numeric",
|
||||||
"min": -99.0,
|
"min": -99.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
|
||||||
"precision": 0,
|
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_min": -150.0,
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"unit_range_index": 1,
|
|
||||||
"parent_key": "SpeedLimitController",
|
"parent_key": "SpeedLimitController",
|
||||||
"settings_tier": "advanced"
|
"settings_tier": "advanced"
|
||||||
},
|
},
|
||||||
@@ -2303,12 +2245,6 @@
|
|||||||
"ui_type": "numeric",
|
"ui_type": "numeric",
|
||||||
"min": -99.0,
|
"min": -99.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
|
||||||
"precision": 0,
|
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_min": -150.0,
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"unit_range_index": 2,
|
|
||||||
"parent_key": "SpeedLimitController",
|
"parent_key": "SpeedLimitController",
|
||||||
"settings_tier": "advanced"
|
"settings_tier": "advanced"
|
||||||
},
|
},
|
||||||
@@ -2320,12 +2256,6 @@
|
|||||||
"ui_type": "numeric",
|
"ui_type": "numeric",
|
||||||
"min": -99.0,
|
"min": -99.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
|
||||||
"precision": 0,
|
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_min": -150.0,
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"unit_range_index": 3,
|
|
||||||
"parent_key": "SpeedLimitController",
|
"parent_key": "SpeedLimitController",
|
||||||
"settings_tier": "advanced"
|
"settings_tier": "advanced"
|
||||||
},
|
},
|
||||||
@@ -2337,12 +2267,6 @@
|
|||||||
"ui_type": "numeric",
|
"ui_type": "numeric",
|
||||||
"min": -99.0,
|
"min": -99.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
|
||||||
"precision": 0,
|
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_min": -150.0,
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"unit_range_index": 4,
|
|
||||||
"parent_key": "SpeedLimitController",
|
"parent_key": "SpeedLimitController",
|
||||||
"settings_tier": "advanced"
|
"settings_tier": "advanced"
|
||||||
},
|
},
|
||||||
@@ -2354,12 +2278,6 @@
|
|||||||
"ui_type": "numeric",
|
"ui_type": "numeric",
|
||||||
"min": -99.0,
|
"min": -99.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
|
||||||
"precision": 0,
|
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_min": -150.0,
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"unit_range_index": 5,
|
|
||||||
"parent_key": "SpeedLimitController",
|
"parent_key": "SpeedLimitController",
|
||||||
"settings_tier": "advanced"
|
"settings_tier": "advanced"
|
||||||
},
|
},
|
||||||
@@ -2371,12 +2289,6 @@
|
|||||||
"ui_type": "numeric",
|
"ui_type": "numeric",
|
||||||
"min": -99.0,
|
"min": -99.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
|
||||||
"precision": 0,
|
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_min": -150.0,
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"unit_range_index": 6,
|
|
||||||
"parent_key": "SpeedLimitController",
|
"parent_key": "SpeedLimitController",
|
||||||
"settings_tier": "advanced"
|
"settings_tier": "advanced"
|
||||||
},
|
},
|
||||||
@@ -2429,8 +2341,6 @@
|
|||||||
"min": 0.0,
|
"min": 0.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
"step": 1.0,
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"parent_key": "ConditionalChill",
|
"parent_key": "ConditionalChill",
|
||||||
"settings_tier": "advanced"
|
"settings_tier": "advanced"
|
||||||
},
|
},
|
||||||
@@ -2443,8 +2353,6 @@
|
|||||||
"min": 0.0,
|
"min": 0.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
"step": 1.0,
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_max": 150.0,
|
|
||||||
"parent_key": "ConditionalChill",
|
"parent_key": "ConditionalChill",
|
||||||
"settings_tier": "advanced"
|
"settings_tier": "advanced"
|
||||||
},
|
},
|
||||||
@@ -2477,8 +2385,6 @@
|
|||||||
"min": 0.0,
|
"min": 0.0,
|
||||||
"max": 15.0,
|
"max": 15.0,
|
||||||
"step": 1.0,
|
"step": 1.0,
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"metric_max": 30.0,
|
|
||||||
"parent_key": "ConditionalChill",
|
"parent_key": "ConditionalChill",
|
||||||
"settings_tier": "advanced"
|
"settings_tier": "advanced"
|
||||||
},
|
},
|
||||||
@@ -2548,7 +2454,6 @@
|
|||||||
"min": 5,
|
"min": 5,
|
||||||
"max": 80,
|
"max": 80,
|
||||||
"step": 5,
|
"step": 5,
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"parent_key": "VisionSpeedLimitLowLimitFilter",
|
"parent_key": "VisionSpeedLimitLowLimitFilter",
|
||||||
"settings_tier": "advanced"
|
"settings_tier": "advanced"
|
||||||
},
|
},
|
||||||
@@ -4851,12 +4756,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "GalaxyMobileDefault",
|
"key": "GalaxyMobileDefault",
|
||||||
"label": "Use Galaxy (new) by Default",
|
"label": "Try the Big Dipper Web UI",
|
||||||
"description": "Open Galaxy (new) at the top-level Galaxy link. Turn this off to use Galaxy (old) instead. Galaxy (old) remains available at /classic and Galaxy (new) at /mobile.",
|
"description": "Open the Big Dipper at the top-level Galaxy link instead of the classic Galaxy. The classic UI remains available at /classic and Big Dipper at /mobile regardless of this toggle.",
|
||||||
"picker_description": "Serve Galaxy (new) as the default landing page.",
|
"picker_description": "Serve the Big Dipper as the default landing page.",
|
||||||
"data_type": "bool",
|
"data_type": "bool",
|
||||||
"ui_type": "toggle",
|
"ui_type": "toggle",
|
||||||
"settings_tier": "simple"
|
"settings_tier": "advanced"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "AlphaLongitudinalEnabled",
|
"key": "AlphaLongitudinalEnabled",
|
||||||
@@ -4913,7 +4818,6 @@
|
|||||||
"min": 0.0,
|
"min": 0.0,
|
||||||
"max": 99.0,
|
"max": 99.0,
|
||||||
"step": 1.0,
|
"step": 1.0,
|
||||||
"unit_type": "vehicle_speed",
|
|
||||||
"parent_key": "GalaxyDeveloperMode",
|
"parent_key": "GalaxyDeveloperMode",
|
||||||
"settings_tier": "advanced"
|
"settings_tier": "advanced"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -390,16 +390,6 @@ def speed_limit_controller_available(openpilot_longitudinal: bool, redneck_cruis
|
|||||||
return openpilot_longitudinal or redneck_cruise
|
return openpilot_longitudinal or redneck_cruise
|
||||||
|
|
||||||
|
|
||||||
def software_cruise_intervals_available(quality_of_life: bool, car_make: str, pcm_cruise: bool,
|
|
||||||
openpilot_longitudinal: bool, pcm_cruise_speed: bool) -> bool:
|
|
||||||
return bool(quality_of_life and not (car_make == "toyota" and pcm_cruise) and
|
|
||||||
(openpilot_longitudinal or not pcm_cruise_speed))
|
|
||||||
|
|
||||||
|
|
||||||
def reverse_cruise_available(quality_of_life: bool, car_make: str, pcm_cruise: bool) -> bool:
|
|
||||||
return bool(quality_of_life and car_make == "toyota" and pcm_cruise)
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_cancel_button_controls(params: Params | None = None) -> bool:
|
def migrate_cancel_button_controls(params: Params | None = None) -> bool:
|
||||||
params = params or Params(return_defaults=True)
|
params = params or Params(return_defaults=True)
|
||||||
if params.get_bool(CANCEL_BUTTON_MIGRATION_KEY) or not params.get_bool("RemapCancelToDistance"):
|
if params.get_bool(CANCEL_BUTTON_MIGRATION_KEY) or not params.get_bool("RemapCancelToDistance"):
|
||||||
@@ -1355,17 +1345,10 @@ class StarPilotVariables:
|
|||||||
toggle.pause_lateral_below_signal = self.get_value("PauseLateralOnSignal", condition=toggle.pause_lateral_below_speed != 0)
|
toggle.pause_lateral_below_signal = self.get_value("PauseLateralOnSignal", condition=toggle.pause_lateral_below_speed != 0)
|
||||||
toggle.pause_lateral_signal_delay = self.get_value("LateralResumeDelay", cast=float, condition=toggle.pause_lateral_below_signal, default=0.0, min=0.0, max=5.0)
|
toggle.pause_lateral_signal_delay = self.get_value("LateralResumeDelay", cast=float, condition=toggle.pause_lateral_below_signal, default=0.0, min=0.0, max=5.0)
|
||||||
|
|
||||||
quality_of_life = self.get_value("QOLLongitudinal")
|
quality_of_life_longitudinal = toggle.openpilot_longitudinal and self.get_value("QOLLongitudinal")
|
||||||
quality_of_life_longitudinal = toggle.openpilot_longitudinal and quality_of_life
|
quality_of_life_cruise = self.get_value("QOLLongitudinal") and (toggle.openpilot_longitudinal or not FPCP.pcmCruiseSpeed)
|
||||||
quality_of_life_cruise = software_cruise_intervals_available(
|
|
||||||
quality_of_life, toggle.car_make, pcm_cruise, toggle.openpilot_longitudinal, FPCP.pcmCruiseSpeed,
|
|
||||||
)
|
|
||||||
toggle.cruise_increase = self.get_value("CustomCruise", cast=float, condition=quality_of_life_cruise, default=1.0)
|
toggle.cruise_increase = self.get_value("CustomCruise", cast=float, condition=quality_of_life_cruise, default=1.0)
|
||||||
toggle.cruise_increase_long = self.get_value("CustomCruiseLong", cast=float, condition=quality_of_life_cruise, default=5.0)
|
toggle.cruise_increase_long = self.get_value("CustomCruiseLong", cast=float, condition=quality_of_life_cruise, default=5.0)
|
||||||
toggle.reverse_cruise_increase = self.get_value(
|
|
||||||
"ReverseCruise",
|
|
||||||
condition=reverse_cruise_available(quality_of_life, toggle.car_make, pcm_cruise),
|
|
||||||
)
|
|
||||||
toggle.force_stops = self.get_value("ForceStops", condition=quality_of_life_longitudinal)
|
toggle.force_stops = self.get_value("ForceStops", condition=quality_of_life_longitudinal)
|
||||||
toggle.force_stop_distance_offset = self.get_value("ForceStopDistanceOffset", cast=int, condition=(quality_of_life_longitudinal and toggle.force_stops))
|
toggle.force_stop_distance_offset = self.get_value("ForceStopDistanceOffset", cast=int, condition=(quality_of_life_longitudinal and toggle.force_stops))
|
||||||
toggle.force_standstill = self.get_value("ForceStandstill", condition=quality_of_life_longitudinal)
|
toggle.force_standstill = self.get_value("ForceStandstill", condition=quality_of_life_longitudinal)
|
||||||
|
|||||||
@@ -328,14 +328,4 @@ def test_set_speed_limit_unavailable_on_stock_pcm_without_helper():
|
|||||||
def test_speed_limit_controller_available_on_openpilot_longitudinal_or_redneck():
|
def test_speed_limit_controller_available_on_openpilot_longitudinal_or_redneck():
|
||||||
assert spv.speed_limit_controller_available(openpilot_longitudinal=True, redneck_cruise=False) is True
|
assert spv.speed_limit_controller_available(openpilot_longitudinal=True, redneck_cruise=False) is True
|
||||||
assert spv.speed_limit_controller_available(openpilot_longitudinal=False, redneck_cruise=True) is True
|
assert spv.speed_limit_controller_available(openpilot_longitudinal=False, redneck_cruise=True) is True
|
||||||
|
|
||||||
|
|
||||||
def test_toyota_pcm_cruise_uses_hardware_reverse_instead_of_software_intervals():
|
|
||||||
assert spv.software_cruise_intervals_available(True, "toyota", True, True, True) is False
|
|
||||||
assert spv.reverse_cruise_available(True, "toyota", True) is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_non_toyota_software_cruise_keeps_custom_intervals():
|
|
||||||
assert spv.software_cruise_intervals_available(True, "hyundai", False, True, True) is True
|
|
||||||
assert spv.reverse_cruise_available(True, "hyundai", False) is False
|
|
||||||
assert spv.speed_limit_controller_available(openpilot_longitudinal=False, redneck_cruise=False) is False
|
assert spv.speed_limit_controller_available(openpilot_longitudinal=False, redneck_cruise=False) is False
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ class StarPilotPlanner:
|
|||||||
v_cruise_kph += starpilot_toggles.set_speed_offset
|
v_cruise_kph += starpilot_toggles.set_speed_offset
|
||||||
v_cruise = v_cruise_kph * CV.KPH_TO_MS
|
v_cruise = v_cruise_kph * CV.KPH_TO_MS
|
||||||
v_ego = max(sm["carState"].vEgo, 0)
|
v_ego = max(sm["carState"].vEgo, 0)
|
||||||
|
updated = getattr(sm, "updated", None)
|
||||||
|
if updated is not None and updated.get("carParams", False):
|
||||||
|
cp = sm["carParams"] if "carParams" in sm else None
|
||||||
|
self.gps_location_service = get_gps_location_service(self.params, cp)
|
||||||
|
|
||||||
gps_location = sm[self.gps_location_service]
|
gps_location = sm[self.gps_location_service]
|
||||||
self.gps_position = {
|
self.gps_position = {
|
||||||
|
|||||||
@@ -310,6 +310,9 @@ def starpilot_thread():
|
|||||||
while True:
|
while True:
|
||||||
sm.update()
|
sm.update()
|
||||||
|
|
||||||
|
if sm.updated["carParams"]:
|
||||||
|
gps_location_service = get_gps_location_service(params, sm["carParams"])
|
||||||
|
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
monotonic_now = time.monotonic()
|
monotonic_now = time.monotonic()
|
||||||
|
|
||||||
|
|||||||
@@ -329,9 +329,9 @@ class BlueZClient:
|
|||||||
self.set_device_property(address, "Trusted", "b", True)
|
self.set_device_property(address, "Trusted", "b", True)
|
||||||
self.agent.clear()
|
self.agent.clear()
|
||||||
|
|
||||||
def connect(self, address: str, timeout: float = 30.0) -> None:
|
def connect(self, address: str) -> None:
|
||||||
device = self.device_for_address(address)
|
device = self.device_for_address(address)
|
||||||
self._call(device["path"], DEVICE_IFACE, "Connect", timeout=timeout)
|
self._call(device["path"], DEVICE_IFACE, "Connect", timeout=30.0)
|
||||||
|
|
||||||
def disconnect(self, address: str) -> None:
|
def disconnect(self, address: str) -> None:
|
||||||
device = self.device_for_address(address)
|
device = self.device_for_address(address)
|
||||||
|
|||||||
@@ -18,10 +18,8 @@ SCAN_DURATION = 20.0
|
|||||||
AUDIO_TEST_START_DELAY = 3.0
|
AUDIO_TEST_START_DELAY = 3.0
|
||||||
AUDIO_TEST_HOLD_TIME = 3.0
|
AUDIO_TEST_HOLD_TIME = 3.0
|
||||||
RECONNECT_INTERVAL_SECONDS = 15.0
|
RECONNECT_INTERVAL_SECONDS = 15.0
|
||||||
CONTROLLER_RECONNECT_INTERVAL_SECONDS = 5.0
|
|
||||||
RECONNECT_MAX_BACKOFF_SECONDS = 300.0
|
RECONNECT_MAX_BACKOFF_SECONDS = 300.0
|
||||||
MANUAL_DISCONNECT_SUPPRESSION_SECONDS = 300.0
|
MANUAL_DISCONNECT_SUPPRESSION_SECONDS = 300.0
|
||||||
CONTROLLER_OFFROAD_DISCONNECT_DELAY_SECONDS = 120.0
|
|
||||||
|
|
||||||
|
|
||||||
class BluetoothController:
|
class BluetoothController:
|
||||||
@@ -38,9 +36,6 @@ class BluetoothController:
|
|||||||
self._last_reconnect = 0.0
|
self._last_reconnect = 0.0
|
||||||
self._reconnect_backoff: dict[str, tuple[int, float]] = {}
|
self._reconnect_backoff: dict[str, tuple[int, float]] = {}
|
||||||
self._manual_disconnect_until: dict[str, float] = {}
|
self._manual_disconnect_until: dict[str, float] = {}
|
||||||
self._offroad_since: float | None = None
|
|
||||||
self._policy_disconnected: set[str] = set()
|
|
||||||
self._policy_disconnect_retry_after: dict[str, float] = {}
|
|
||||||
self._scan_deadline = 0.0
|
self._scan_deadline = 0.0
|
||||||
self._audio_test_deadline = 0.0
|
self._audio_test_deadline = 0.0
|
||||||
self._sleep = sleep
|
self._sleep = sleep
|
||||||
@@ -228,8 +223,6 @@ class BluetoothController:
|
|||||||
# report NotConnected, and it must not immediately be auto-reconnected.
|
# report NotConnected, and it must not immediately be auto-reconnected.
|
||||||
self._manual_disconnect_until[normalized_address] = time.monotonic() + MANUAL_DISCONNECT_SUPPRESSION_SECONDS
|
self._manual_disconnect_until[normalized_address] = time.monotonic() + MANUAL_DISCONNECT_SUPPRESSION_SECONDS
|
||||||
self._reconnect_backoff.pop(normalized_address, None)
|
self._reconnect_backoff.pop(normalized_address, None)
|
||||||
self._policy_disconnected.discard(normalized_address)
|
|
||||||
self._policy_disconnect_retry_after.pop(normalized_address, None)
|
|
||||||
try:
|
try:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._client().disconnect(normalized_address)
|
self._client().disconnect(normalized_address)
|
||||||
@@ -241,8 +234,6 @@ class BluetoothController:
|
|||||||
self._client().remove(address)
|
self._client().remove(address)
|
||||||
self._reconnect_backoff.pop(address.upper(), None)
|
self._reconnect_backoff.pop(address.upper(), None)
|
||||||
self._manual_disconnect_until.pop(address.upper(), None)
|
self._manual_disconnect_until.pop(address.upper(), None)
|
||||||
self._policy_disconnected.discard(address.upper())
|
|
||||||
self._policy_disconnect_retry_after.pop(address.upper(), None)
|
|
||||||
if (self.params.get("BluetoothAudioAddress", encoding="utf-8") or "").upper() == address.upper():
|
if (self.params.get("BluetoothAudioAddress", encoding="utf-8") or "").upper() == address.upper():
|
||||||
self.params.remove("BluetoothAudioAddress")
|
self.params.remove("BluetoothAudioAddress")
|
||||||
elif command == "select_audio":
|
elif command == "select_audio":
|
||||||
@@ -281,122 +272,47 @@ class BluetoothController:
|
|||||||
self._client().stop_discovery()
|
self._client().stop_discovery()
|
||||||
self._scan_deadline = 0.0
|
self._scan_deadline = 0.0
|
||||||
|
|
||||||
def _maintain_controller_offroad_policy(self, status: dict[str, Any], now: float) -> bool:
|
|
||||||
if not status["offroad"]:
|
|
||||||
self._offroad_since = None
|
|
||||||
if self._policy_disconnected:
|
|
||||||
for address in self._policy_disconnected:
|
|
||||||
self._reconnect_backoff.pop(address, None)
|
|
||||||
self._policy_disconnect_retry_after.clear()
|
|
||||||
self._last_reconnect = 0.0
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self._offroad_since is None:
|
|
||||||
self._offroad_since = now
|
|
||||||
|
|
||||||
if not self.params.get_bool("BluetoothDisconnectControllersOffroad"):
|
|
||||||
if self._policy_disconnected:
|
|
||||||
self._policy_disconnect_retry_after.clear()
|
|
||||||
self._last_reconnect = 0.0
|
|
||||||
return False
|
|
||||||
|
|
||||||
if now - self._offroad_since < CONTROLLER_OFFROAD_DISCONNECT_DELAY_SECONDS:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for device in status["devices"]:
|
|
||||||
if not device.get("paired") or not device.get("controller") or not device.get("connected"):
|
|
||||||
continue
|
|
||||||
address = str(device["address"]).upper()
|
|
||||||
if now < self._policy_disconnect_retry_after.get(address, 0.0):
|
|
||||||
continue
|
|
||||||
self._policy_disconnected.add(address)
|
|
||||||
self._policy_disconnect_retry_after[address] = now + RECONNECT_INTERVAL_SECONDS
|
|
||||||
try:
|
|
||||||
with self._lock:
|
|
||||||
self._client().disconnect(address)
|
|
||||||
except RuntimeError as error:
|
|
||||||
if "notconnected" not in str(error).replace(" ", "").lower():
|
|
||||||
self._policy_disconnected.discard(address)
|
|
||||||
self._policy_disconnect_retry_after.pop(address, None)
|
|
||||||
cloudlog.warning(f"Bluetooth offroad controller disconnect failed for {address}: {error}")
|
|
||||||
except Exception as error:
|
|
||||||
self._policy_disconnected.discard(address)
|
|
||||||
self._policy_disconnect_retry_after.pop(address, None)
|
|
||||||
cloudlog.warning(f"Bluetooth offroad controller disconnect failed for {address}: {error}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _maintain_reconnects(self, status: dict[str, Any], now: float, suspend_controller_reconnect: bool) -> None:
|
|
||||||
devices = status["devices"]
|
|
||||||
devices_by_address = {device["address"].upper(): device for device in devices}
|
|
||||||
for address in list(self._policy_disconnected):
|
|
||||||
device = devices_by_address.get(address)
|
|
||||||
if device is None or not device["paired"] or not device["trusted"]:
|
|
||||||
self._policy_disconnected.discard(address)
|
|
||||||
self._reconnect_backoff.pop(address, None)
|
|
||||||
elif device["connected"]:
|
|
||||||
self._policy_disconnected.discard(address)
|
|
||||||
self._reconnect_backoff.pop(address, None)
|
|
||||||
|
|
||||||
if self._pairing_address:
|
|
||||||
return
|
|
||||||
|
|
||||||
selected = str(status["selected_audio"])
|
|
||||||
candidates = [device for device in devices if device["paired"] and device["trusted"] and not device["connected"]]
|
|
||||||
candidates.sort(key=lambda device: device["address"].upper() != selected.upper())
|
|
||||||
controller_candidates = {
|
|
||||||
device["address"].upper() for device in candidates
|
|
||||||
if device["controller"] or device["address"].upper() in self._policy_disconnected
|
|
||||||
}
|
|
||||||
reconnect_interval = CONTROLLER_RECONNECT_INTERVAL_SECONDS if controller_candidates else RECONNECT_INTERVAL_SECONDS
|
|
||||||
if now - self._last_reconnect < reconnect_interval:
|
|
||||||
return
|
|
||||||
self._last_reconnect = now
|
|
||||||
|
|
||||||
candidate_addresses = {device["address"].upper() for device in candidates}
|
|
||||||
for address in list(self._manual_disconnect_until):
|
|
||||||
if address not in candidate_addresses or now >= self._manual_disconnect_until[address]:
|
|
||||||
self._manual_disconnect_until.pop(address, None)
|
|
||||||
for address in list(self._reconnect_backoff):
|
|
||||||
if address not in candidate_addresses:
|
|
||||||
self._reconnect_backoff.pop(address, None)
|
|
||||||
|
|
||||||
for device in candidates:
|
|
||||||
address = device["address"].upper()
|
|
||||||
controller = device["controller"] or address in self._policy_disconnected
|
|
||||||
if not device["audio"] and not controller:
|
|
||||||
continue
|
|
||||||
if suspend_controller_reconnect and controller:
|
|
||||||
continue
|
|
||||||
if now < self._manual_disconnect_until.get(address, 0.0):
|
|
||||||
continue
|
|
||||||
attempts, retry_after = self._reconnect_backoff.get(address, (0, 0.0))
|
|
||||||
if now < retry_after:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
with self._lock:
|
|
||||||
self._client().connect(address, timeout=CONTROLLER_RECONNECT_INTERVAL_SECONDS if controller else 30.0)
|
|
||||||
self._reconnect_backoff.pop(address, None)
|
|
||||||
except Exception:
|
|
||||||
attempts += 1
|
|
||||||
delay = (CONTROLLER_RECONNECT_INTERVAL_SECONDS if controller else
|
|
||||||
min(RECONNECT_INTERVAL_SECONDS * (2 ** (attempts - 1)), RECONNECT_MAX_BACKOFF_SECONDS))
|
|
||||||
self._reconnect_backoff[address] = (attempts, now + delay)
|
|
||||||
cloudlog.warning(f"Bluetooth reconnect failed for {address}; retrying in {delay:.0f}s")
|
|
||||||
|
|
||||||
def maintain_connections(self) -> None:
|
def maintain_connections(self) -> None:
|
||||||
while True:
|
while True:
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
now = time.monotonic()
|
|
||||||
if not self.params.get_bool("BluetoothEnabled"):
|
if not self.params.get_bool("BluetoothEnabled"):
|
||||||
self._maintain_controller_offroad_policy({"offroad": self._offroad(), "devices": []}, now)
|
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
status = self.status()
|
status = self.status()
|
||||||
if not status["available"] or not status["powered"]:
|
if not status["available"] or not status["powered"]:
|
||||||
continue
|
continue
|
||||||
|
now = time.monotonic()
|
||||||
self._maintain_scan(status, now)
|
self._maintain_scan(status, now)
|
||||||
suspend_controller_reconnect = self._maintain_controller_offroad_policy(status, now)
|
if self._pairing_address or now - self._last_reconnect < RECONNECT_INTERVAL_SECONDS:
|
||||||
self._maintain_reconnects(status, now, suspend_controller_reconnect)
|
continue
|
||||||
|
self._last_reconnect = now
|
||||||
|
selected = str(status["selected_audio"])
|
||||||
|
candidates = [device for device in status["devices"] if device["paired"] and device["trusted"] and not device["connected"]]
|
||||||
|
candidates.sort(key=lambda device: device["address"].upper() != selected.upper())
|
||||||
|
candidate_addresses = {device["address"].upper() for device in candidates}
|
||||||
|
for address in list(self._manual_disconnect_until):
|
||||||
|
if address not in candidate_addresses or now >= self._manual_disconnect_until[address]:
|
||||||
|
self._manual_disconnect_until.pop(address, None)
|
||||||
|
for address in list(self._reconnect_backoff):
|
||||||
|
if address not in candidate_addresses:
|
||||||
|
self._reconnect_backoff.pop(address, None)
|
||||||
|
for device in candidates:
|
||||||
|
if device["audio"] or device["controller"]:
|
||||||
|
address = device["address"].upper()
|
||||||
|
if now < self._manual_disconnect_until.get(address, 0.0):
|
||||||
|
continue
|
||||||
|
_attempts, retry_after = self._reconnect_backoff.get(address, (0, 0.0))
|
||||||
|
if now < retry_after:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
with self._lock:
|
||||||
|
self._client().connect(address)
|
||||||
|
self._reconnect_backoff.pop(address, None)
|
||||||
|
except Exception:
|
||||||
|
attempts = _attempts + 1
|
||||||
|
delay = min(RECONNECT_INTERVAL_SECONDS * (2 ** (attempts - 1)), RECONNECT_MAX_BACKOFF_SECONDS)
|
||||||
|
self._reconnect_backoff[address] = (attempts, now + delay)
|
||||||
|
cloudlog.warning(f"Bluetooth reconnect failed for {address}; retrying in {delay:.0f}s")
|
||||||
except Exception:
|
except Exception:
|
||||||
cloudlog.exception("Bluetooth connection maintenance failed")
|
cloudlog.exception("Bluetooth connection maintenance failed")
|
||||||
|
|
||||||
|
|||||||
@@ -55,8 +55,6 @@ class FakeBlueZ:
|
|||||||
self.discovering = False
|
self.discovering = False
|
||||||
self.closed = False
|
self.closed = False
|
||||||
self.actions = []
|
self.actions = []
|
||||||
self.connect_timeouts = []
|
|
||||||
self.connect_error = None
|
|
||||||
self.device = {
|
self.device = {
|
||||||
"path": "/fake/device",
|
"path": "/fake/device",
|
||||||
"address": "00:11:22:33:44:55",
|
"address": "00:11:22:33:44:55",
|
||||||
@@ -93,11 +91,8 @@ class FakeBlueZ:
|
|||||||
def pair(self, address, _device_path=None):
|
def pair(self, address, _device_path=None):
|
||||||
self.actions.append(("pair", address))
|
self.actions.append(("pair", address))
|
||||||
|
|
||||||
def connect(self, address, timeout=30.0):
|
def connect(self, address):
|
||||||
self.actions.append(("connect", address))
|
self.actions.append(("connect", address))
|
||||||
self.connect_timeouts.append(timeout)
|
|
||||||
if self.connect_error is not None:
|
|
||||||
raise self.connect_error
|
|
||||||
|
|
||||||
def disconnect(self, address):
|
def disconnect(self, address):
|
||||||
self.actions.append(("disconnect", address))
|
self.actions.append(("disconnect", address))
|
||||||
@@ -302,7 +297,6 @@ def test_disconnect_is_idempotent_and_suppresses_auto_reconnect():
|
|||||||
params = FakeParams(IsOffroad=False, BluetoothEnabled=True)
|
params = FakeParams(IsOffroad=False, BluetoothEnabled=True)
|
||||||
client = FakeBlueZ()
|
client = FakeBlueZ()
|
||||||
controller = BluetoothController(params, lambda: client, FakeRadio())
|
controller = BluetoothController(params, lambda: client, FakeRadio())
|
||||||
controller._policy_disconnected.add(client.device["address"].upper())
|
|
||||||
|
|
||||||
controller.handle({"command": "disconnect", "address": client.device["address"]})
|
controller.handle({"command": "disconnect", "address": client.device["address"]})
|
||||||
|
|
||||||
@@ -310,7 +304,6 @@ def test_disconnect_is_idempotent_and_suppresses_auto_reconnect():
|
|||||||
assert client.actions == [("disconnect", client.device["address"])]
|
assert client.actions == [("disconnect", client.device["address"])]
|
||||||
assert address in controller._manual_disconnect_until
|
assert address in controller._manual_disconnect_until
|
||||||
assert controller._manual_disconnect_until[address] > time.monotonic()
|
assert controller._manual_disconnect_until[address] > time.monotonic()
|
||||||
assert address not in controller._policy_disconnected
|
|
||||||
|
|
||||||
|
|
||||||
def test_power_off_preserves_saved_audio_selection():
|
def test_power_off_preserves_saved_audio_selection():
|
||||||
@@ -439,85 +432,6 @@ def test_scan_stops_after_timeout():
|
|||||||
assert not client.discovering and controller._scan_deadline == 0.0
|
assert not client.discovering and controller._scan_deadline == 0.0
|
||||||
|
|
||||||
|
|
||||||
def test_controller_offroad_disconnect_policy_is_opt_in_and_delayed():
|
|
||||||
params = FakeParams(IsOffroad=True, BluetoothEnabled=True, BluetoothDisconnectControllersOffroad=False)
|
|
||||||
client = FakeBlueZ()
|
|
||||||
controller = BluetoothController(params, lambda: client, FakeRadio())
|
|
||||||
controller._bluez = client
|
|
||||||
controller_status = {
|
|
||||||
"offroad": True,
|
|
||||||
"devices": [
|
|
||||||
{**client.device, "name": "Controller", "audio": False, "controller": True, "connected": True},
|
|
||||||
{**client.device, "address": "AA:BB:CC:DD:EE:FF", "audio": True, "controller": False, "connected": True},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
assert not controller._maintain_controller_offroad_policy(controller_status, 100.0)
|
|
||||||
params.put_bool("BluetoothDisconnectControllersOffroad", True)
|
|
||||||
assert not controller._maintain_controller_offroad_policy(controller_status, 219.9)
|
|
||||||
assert client.actions == []
|
|
||||||
|
|
||||||
assert controller._maintain_controller_offroad_policy(controller_status, 220.0)
|
|
||||||
assert client.actions == [("disconnect", client.device["address"])]
|
|
||||||
assert client.device["address"].upper() in controller._policy_disconnected
|
|
||||||
assert controller._maintain_controller_offroad_policy(controller_status, 221.0)
|
|
||||||
assert client.actions == [("disconnect", client.device["address"])]
|
|
||||||
|
|
||||||
|
|
||||||
def test_controller_offroad_disconnect_policy_reconnects_onroad():
|
|
||||||
params = FakeParams(IsOffroad=True, BluetoothEnabled=True, BluetoothDisconnectControllersOffroad=True)
|
|
||||||
client = FakeBlueZ()
|
|
||||||
controller = BluetoothController(params, lambda: client, FakeRadio())
|
|
||||||
controller._bluez = client
|
|
||||||
address = "00:11:22:33:44:55"
|
|
||||||
controller._offroad_since = 100.0
|
|
||||||
controller._policy_disconnected.add(address)
|
|
||||||
controller._reconnect_backoff[address] = (3, 500.0)
|
|
||||||
controller._last_reconnect = 210.0
|
|
||||||
|
|
||||||
disconnected_status = {
|
|
||||||
"offroad": False,
|
|
||||||
"selected_audio": "",
|
|
||||||
"devices": [{**client.device, "controller": False, "connected": False}],
|
|
||||||
}
|
|
||||||
assert not controller._maintain_controller_offroad_policy(disconnected_status, 220.0)
|
|
||||||
assert controller._offroad_since is None
|
|
||||||
assert controller._policy_disconnected == {address}
|
|
||||||
assert controller._policy_disconnect_retry_after == {}
|
|
||||||
assert address not in controller._reconnect_backoff
|
|
||||||
assert controller._last_reconnect == 0.0
|
|
||||||
|
|
||||||
controller._maintain_reconnects(disconnected_status, 220.0, False)
|
|
||||||
assert client.actions == [("connect", address)]
|
|
||||||
assert client.connect_timeouts == [5.0]
|
|
||||||
|
|
||||||
connected_status = {
|
|
||||||
**disconnected_status,
|
|
||||||
"devices": [{**client.device, "controller": False, "connected": True}],
|
|
||||||
}
|
|
||||||
controller._maintain_reconnects(connected_status, 221.0, False)
|
|
||||||
assert controller._policy_disconnected == set()
|
|
||||||
|
|
||||||
|
|
||||||
def test_controller_auto_reconnect_uses_fixed_short_retry():
|
|
||||||
params = FakeParams(IsOffroad=False, BluetoothEnabled=True)
|
|
||||||
client = FakeBlueZ()
|
|
||||||
client.connect_error = RuntimeError("Host is down")
|
|
||||||
controller = BluetoothController(params, lambda: client, FakeRadio())
|
|
||||||
controller._bluez = client
|
|
||||||
status = {
|
|
||||||
"offroad": False,
|
|
||||||
"selected_audio": "",
|
|
||||||
"devices": [{**client.device, "audio": False, "controller": True, "connected": False}],
|
|
||||||
}
|
|
||||||
|
|
||||||
controller._maintain_reconnects(status, 100.0, False)
|
|
||||||
assert controller._reconnect_backoff[client.device["address"]] == (1, 105.0)
|
|
||||||
controller._maintain_reconnects(status, 105.0, False)
|
|
||||||
assert controller._reconnect_backoff[client.device["address"]] == (2, 110.0)
|
|
||||||
assert client.connect_timeouts == [5.0, 5.0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_pair_keeps_discovery_until_pair_starts():
|
def test_pair_keeps_discovery_until_pair_starts():
|
||||||
params = FakeParams(IsOffroad=True, BluetoothEnabled=True)
|
params = FakeParams(IsOffroad=True, BluetoothEnabled=True)
|
||||||
client = FakeBlueZ()
|
client = FakeBlueZ()
|
||||||
|
|||||||
@@ -55,7 +55,10 @@ class MapSpeedLogger:
|
|||||||
|
|
||||||
self.gps_location_service = get_gps_location_service(self.params)
|
self.gps_location_service = get_gps_location_service(self.params)
|
||||||
|
|
||||||
self.sm = messaging.SubMaster(["deviceState", "starpilotCarState", "starpilotPlan", self.gps_location_service, "mapdOut", "modelV2"])
|
self.sm = messaging.SubMaster([
|
||||||
|
"carParams", "deviceState", "starpilotCarState", "starpilotPlan", "gpsLocation", "gpsLocationExternal",
|
||||||
|
"mapdOut", "modelV2",
|
||||||
|
])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def can_make_overpass_request(self):
|
def can_make_overpass_request(self):
|
||||||
@@ -252,6 +255,8 @@ class MapSpeedLogger:
|
|||||||
return relevant_segments
|
return relevant_segments
|
||||||
|
|
||||||
def log_speed_limit(self):
|
def log_speed_limit(self):
|
||||||
|
if self.sm.updated["carParams"]:
|
||||||
|
self.gps_location_service = get_gps_location_service(self.params, self.sm["carParams"])
|
||||||
if not self.sm.updated[self.gps_location_service]:
|
if not self.sm.updated[self.gps_location_service]:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { createBrowserHistory, createRouter } from "/assets/vendor/remix-router-
|
|||||||
import { hideSidebar } from "/assets/js/utils.js"
|
import { hideSidebar } from "/assets/js/utils.js"
|
||||||
import { DeviceSettings } from "/assets/components/tools/device_settings.js?v=favorite-c4-hint-1"
|
import { DeviceSettings } from "/assets/components/tools/device_settings.js?v=favorite-c4-hint-1"
|
||||||
import { Bluetooth } from "/assets/components/tools/bluetooth.js?v=bluetooth-live-15"
|
import { Bluetooth } from "/assets/components/tools/bluetooth.js?v=bluetooth-live-15"
|
||||||
import { WheelControls } from "/assets/components/tools/wheel_controls.js?v=controllers-3"
|
import { WheelControls } from "/assets/components/tools/wheel_controls.js?v=controllers-2"
|
||||||
import { DoorControl } from "/assets/components/tools/doors.js"
|
import { DoorControl } from "/assets/components/tools/doors.js"
|
||||||
import { ErrorLogs } from "/assets/components/tools/error_logs.js"
|
import { ErrorLogs } from "/assets/components/tools/error_logs.js"
|
||||||
import { VehicleFeatures } from "/assets/components/tools/vehicle_features.js"
|
import { VehicleFeatures } from "/assets/components/tools/vehicle_features.js"
|
||||||
import { TSKManager } from "/assets/components/tools/tsk_manager.js"
|
import { TSKManager } from "/assets/components/tools/tsk_manager.js"
|
||||||
import { GalaxyPairing } from "/assets/components/tools/galaxy.js"
|
import { GalaxyPairing } from "/assets/components/tools/galaxy.js"
|
||||||
import { Home } from "/assets/components/home/home.js"
|
import { Home } from "/assets/components/home/home.js"
|
||||||
|
import { LongitudinalManeuvers } from "/assets/components/tools/longitudinal_maneuvers.js"
|
||||||
import { MapsManager } from "/assets/components/tools/maps.js"
|
import { MapsManager } from "/assets/components/tools/maps.js"
|
||||||
import { NavDestination } from "/assets/components/navigation/navigation_destination.js?v=nav-search-context-2"
|
import { NavDestination } from "/assets/components/navigation/navigation_destination.js?v=nav-search-context-2"
|
||||||
import { NavKeys } from "/assets/components/navigation/navigation_keys.js?v=app-keys-session-1"
|
import { NavKeys } from "/assets/components/navigation/navigation_keys.js?v=app-keys-session-1"
|
||||||
@@ -20,7 +21,7 @@ import { Sidebar } from "/assets/components/sidebar.js?v=controllers-nav-1"
|
|||||||
import { SentryMode } from "/assets/components/tools/sentry.js"
|
import { SentryMode } from "/assets/components/tools/sentry.js"
|
||||||
import { SpeedLimits } from "/assets/components/tools/speed_limits.js"
|
import { SpeedLimits } from "/assets/components/tools/speed_limits.js"
|
||||||
import { ModelManager } from "/assets/components/tools/model_manager.js?v=20260906a"
|
import { ModelManager } from "/assets/components/tools/model_manager.js?v=20260906a"
|
||||||
import { ModelLaboratory } from "/assets/components/tools/model_laboratory.js?v=model-lab-6"
|
import { ModelLaboratory } from "/assets/components/tools/model_laboratory.js?v=model-lab-5"
|
||||||
import { LivePlots } from "/assets/components/tools/plots.js"
|
import { LivePlots } from "/assets/components/tools/plots.js"
|
||||||
import { ThemeMaker } from "/assets/components/tools/theme_maker.js"
|
import { ThemeMaker } from "/assets/components/tools/theme_maker.js"
|
||||||
import { TestingGround } from "/assets/components/tools/testing_ground.js"
|
import { TestingGround } from "/assets/components/tools/testing_ground.js"
|
||||||
@@ -90,6 +91,7 @@ function Root() {
|
|||||||
createRoute("model_laboratory", "/model_laboratory", ModelLaboratory),
|
createRoute("model_laboratory", "/model_laboratory", ModelLaboratory),
|
||||||
createRoute("tuning", "/tuning", Tuning),
|
createRoute("tuning", "/tuning", Tuning),
|
||||||
createRoute("lateral_maneuvers", "/lateral_maneuvers", Tuning),
|
createRoute("lateral_maneuvers", "/lateral_maneuvers", Tuning),
|
||||||
|
createRoute("longitudinal_maneuvers", "/longitudinal_maneuvers", LongitudinalManeuvers),
|
||||||
createRoute("maps", "/manage_maps", MapsManager),
|
createRoute("maps", "/manage_maps", MapsManager),
|
||||||
createRoute("plots", "/plots", LivePlots),
|
createRoute("plots", "/plots", LivePlots),
|
||||||
createRoute("thememaker", "/theme_maker", ThemeMaker),
|
createRoute("thememaker", "/theme_maker", ThemeMaker),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const MENU_ITEMS = {
|
|||||||
{ name: "Sentry Mode", link: "/sentry", icon: "bi-shield-exclamation" },
|
{ name: "Sentry Mode", link: "/sentry", icon: "bi-shield-exclamation" },
|
||||||
{ name: "Controllers", link: "/wheel-controls", icon: "bi-controller" },
|
{ name: "Controllers", link: "/wheel-controls", icon: "bi-controller" },
|
||||||
{ name: "Lateral Tuning", link: "/tuning", icon: "bi-sign-turn-right" },
|
{ name: "Lateral Tuning", link: "/tuning", icon: "bi-sign-turn-right" },
|
||||||
|
{ name: "Long Maneuvers", link: "/longitudinal_maneuvers", icon: "bi-signpost-split" },
|
||||||
{ name: "Maps", link: "/manage_maps", icon: "bi-map" },
|
{ name: "Maps", link: "/manage_maps", icon: "bi-map" },
|
||||||
{ name: "Navigation", link: "/set_navigation_destination", icon: "bi-geo-alt-fill" },
|
{ name: "Navigation", link: "/set_navigation_destination", icon: "bi-geo-alt-fill" },
|
||||||
{ name: "App Keys", link: "/manage_navigation_keys", icon: "bi-key-fill" },
|
{ name: "App Keys", link: "/manage_navigation_keys", icon: "bi-key-fill" },
|
||||||
|
|||||||
@@ -342,24 +342,6 @@
|
|||||||
margin-bottom: var(--margin-sm);
|
margin-bottom: var(--margin-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ds-unit-note {
|
|
||||||
align-items: center;
|
|
||||||
background: var(--input-bg);
|
|
||||||
border: var(--border-style-main);
|
|
||||||
border-radius: var(--border-radius-base);
|
|
||||||
color: var(--text-muted);
|
|
||||||
display: flex;
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
margin-bottom: var(--margin-base);
|
|
||||||
padding: var(--padding-sm) var(--padding-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-unit-note i,
|
|
||||||
.ds-unit-note strong {
|
|
||||||
color: var(--main-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ――― Empty Filter State ――― */
|
/* ――― Empty Filter State ――― */
|
||||||
.ds-empty {
|
.ds-empty {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { html, reactive } from "/assets/vendor/arrow-core.js"
|
import { html, reactive } from "/assets/vendor/arrow-core.js"
|
||||||
import { formatNumericParamValue, resolveVehicleUnitParam } from "/assets/mobile/js/params.js"
|
|
||||||
|
|
||||||
const endpointOptionsCache = {}
|
const endpointOptionsCache = {}
|
||||||
const endpointOptionsInflight = {}
|
const endpointOptionsInflight = {}
|
||||||
@@ -102,11 +101,9 @@ function normalizeVehicleMake(value) {
|
|||||||
|
|
||||||
function isVehicleSettingVisible(section, param) {
|
function isVehicleSettingVisible(section, param) {
|
||||||
const allowedMakes = param.vehicle_makes || (section.name === "Vehicle" ? VEHICLE_SETTING_MAKES[param.key] : null)
|
const allowedMakes = param.vehicle_makes || (section.name === "Vehicle" ? VEHICLE_SETTING_MAKES[param.key] : null)
|
||||||
|
if (!allowedMakes) return true
|
||||||
const selectedMake = normalizeVehicleMake(state.values.CarMake)
|
const selectedMake = normalizeVehicleMake(state.values.CarMake)
|
||||||
if (allowedMakes && !allowedMakes.some(make => normalizeVehicleMake(make) === selectedMake)) return false
|
return allowedMakes.some(make => normalizeVehicleMake(make) === selectedMake)
|
||||||
|
|
||||||
const excludedMakes = param.excluded_vehicle_makes || []
|
|
||||||
return !excludedMakes.some(make => normalizeVehicleMake(make) === selectedMake)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchesSettingValueCondition(param) {
|
function matchesSettingValueCondition(param) {
|
||||||
@@ -451,6 +448,40 @@ async function fetchLayoutAndParams() {
|
|||||||
scheduleSyncInputs()
|
scheduleSyncInputs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatSliderValue(val, stepStr, precisionInt, key) {
|
||||||
|
if (val === null || val === undefined) return "--"
|
||||||
|
const v = parseFloat(val)
|
||||||
|
if (Number.isNaN(v)) return val
|
||||||
|
|
||||||
|
if (key === "SwitchbackModeCooldown") {
|
||||||
|
if (v === 0) return "Off"
|
||||||
|
return v === 1 ? "1 min" : `${v} min`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "DeviceShutdown") {
|
||||||
|
return v === 1 ? "1 hour" : `${v} hours`
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumeKeys = [
|
||||||
|
"BelowSteerSpeedVolume", "DisengageVolume", "EngageVolume", "PromptVolume",
|
||||||
|
"PromptDistractedVolume", "RefuseVolume",
|
||||||
|
"WarningImmediateVolume", "WarningSoftVolume",
|
||||||
|
]
|
||||||
|
if (key && volumeKeys.includes(key)) {
|
||||||
|
if (v === 0) return "Muted"
|
||||||
|
if (v === 101) return "Auto"
|
||||||
|
return `${v}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (precisionInt !== undefined && precisionInt !== null) {
|
||||||
|
return Number(v.toFixed(precisionInt)).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stepStr || !stepStr.includes(".")) return Math.round(v).toString()
|
||||||
|
const dec = stepStr.split(".")[1].length
|
||||||
|
return Number(v.toFixed(dec)).toString()
|
||||||
|
}
|
||||||
|
|
||||||
function formatReadoutValue(p) {
|
function formatReadoutValue(p) {
|
||||||
const raw = state.values[p.key]
|
const raw = state.values[p.key]
|
||||||
const v = parseFloat(raw)
|
const v = parseFloat(raw)
|
||||||
@@ -474,7 +505,6 @@ function formatStepValue(step, precision) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function numericBounds(param) {
|
function numericBounds(param) {
|
||||||
param = resolveVehicleUnitParam(param, state.values)
|
|
||||||
const defaultBounds = {
|
const defaultBounds = {
|
||||||
min: param.min !== undefined ? param.min : (param.data_type === "float" ? 0.0 : 0),
|
min: param.min !== undefined ? param.min : (param.data_type === "float" ? 0.0 : 0),
|
||||||
max: param.max !== undefined ? param.max : (param.data_type === "float" ? 100.0 : 100),
|
max: param.max !== undefined ? param.max : (param.data_type === "float" ? 100.0 : 100),
|
||||||
@@ -922,7 +952,13 @@ function syncNumericDisplay(param, rawValue) {
|
|||||||
const displayEl = document.getElementById(`ds-display-${param.key}`)
|
const displayEl = document.getElementById(`ds-display-${param.key}`)
|
||||||
if (!displayEl) return
|
if (!displayEl) return
|
||||||
|
|
||||||
displayEl.textContent = formatNumericParamValue(param, rawValue, state.values)
|
const bounds = numericBounds(param)
|
||||||
|
displayEl.textContent = formatSliderValue(
|
||||||
|
rawValue,
|
||||||
|
String(bounds.step),
|
||||||
|
param.precision,
|
||||||
|
param.key,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateNumericParam(param, numericValue, options = {}) {
|
async function updateNumericParam(param, numericValue, options = {}) {
|
||||||
@@ -1275,9 +1311,10 @@ function matchesFilter(p) {
|
|||||||
if (!state.filter) return true
|
if (!state.filter) return true
|
||||||
if (isGroupParam(p)) return false
|
if (isGroupParam(p)) return false
|
||||||
const q = state.filter.toLowerCase()
|
const q = state.filter.toLowerCase()
|
||||||
const displayParam = resolveVehicleUnitParam(p, state.values)
|
const label = String(p.label || "").toLowerCase()
|
||||||
return [displayParam.label, displayParam.key, displayParam.description, displayParam.unit, displayParam.unit_search_terms]
|
const key = String(p.key || "").toLowerCase()
|
||||||
.some(value => String(value || "").toLowerCase().includes(q))
|
const description = String(p.description || "").toLowerCase()
|
||||||
|
return label.includes(q) || key.includes(q) || description.includes(q)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSearchFilter() {
|
function clearSearchFilter() {
|
||||||
@@ -1339,7 +1376,8 @@ function formatFlmValue(param, value) {
|
|||||||
if (value === undefined || value === null) return "not set"
|
if (value === undefined || value === null) return "not set"
|
||||||
if (param.data_type === "bool") return value ? "On" : "Off"
|
if (param.data_type === "bool") return value ? "On" : "Off"
|
||||||
if (param.ui_type === "numeric") {
|
if (param.ui_type === "numeric") {
|
||||||
return formatNumericParamValue(param, value, state.values)
|
const bounds = numericBounds(param)
|
||||||
|
return formatSliderValue(value, String(bounds.step), param.precision, param.key)
|
||||||
}
|
}
|
||||||
return String(value)
|
return String(value)
|
||||||
}
|
}
|
||||||
@@ -1524,8 +1562,6 @@ function renderSettingRow(p) {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
p = resolveVehicleUnitParam(p, state.values)
|
|
||||||
|
|
||||||
const isNumeric = p.ui_type === "numeric"
|
const isNumeric = p.ui_type === "numeric"
|
||||||
const isSlider = isNumeric && p.control === "slider"
|
const isSlider = isNumeric && p.control === "slider"
|
||||||
const isText = p.ui_type === "text"
|
const isText = p.ui_type === "text"
|
||||||
@@ -1568,8 +1604,8 @@ function renderSettingRow(p) {
|
|||||||
@input="${(event) => previewSliderParam(p, event.currentTarget.value)}"
|
@input="${(event) => previewSliderParam(p, event.currentTarget.value)}"
|
||||||
@change="${(event) => commitSliderParam(p, event.currentTarget.value)}" />
|
@change="${(event) => commitSliderParam(p, event.currentTarget.value)}" />
|
||||||
<div class="ds-slider-scale">
|
<div class="ds-slider-scale">
|
||||||
<span>${formatNumericParamValue(p, numericBounds(p).min, state.values)}</span>
|
<span>${formatSliderValue(numericBounds(p).min, String(numericBounds(p).step), p.precision, p.key)}</span>
|
||||||
<span>${formatNumericParamValue(p, numericBounds(p).max, state.values)}</span>
|
<span>${formatSliderValue(numericBounds(p).max, String(numericBounds(p).step), p.precision, p.key)}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="ds-reset-btn"
|
class="ds-reset-btn"
|
||||||
@@ -1595,10 +1631,10 @@ function renderSettingRow(p) {
|
|||||||
const updating = isNumericUpdating(p.key)
|
const updating = isNumericUpdating(p.key)
|
||||||
const defaultNumeric = resolveDefaultNumericValue(p, bounds)
|
const defaultNumeric = resolveDefaultNumericValue(p, bounds)
|
||||||
const defaultLabel = defaultNumeric !== null
|
const defaultLabel = defaultNumeric !== null
|
||||||
? formatNumericParamValue(p, defaultNumeric, state.values)
|
? formatSliderValue(defaultNumeric, String(bounds.step), p.precision, p.key)
|
||||||
: "N/A"
|
: "N/A"
|
||||||
const canReset = !updating && defaultNumeric !== null && Math.abs(defaultNumeric - currentNumeric) > epsilon
|
const canReset = !updating && defaultNumeric !== null && Math.abs(defaultNumeric - currentNumeric) > epsilon
|
||||||
const stepLabel = p.key === "DeviceShutdown" ? "1 hour" : `${formatStepValue(bounds.step, precision)}${p.unit || ""}`
|
const stepLabel = p.key === "DeviceShutdown" ? "1 hour" : formatStepValue(bounds.step, precision)
|
||||||
return html`
|
return html`
|
||||||
<div class="ds-stepper">
|
<div class="ds-stepper">
|
||||||
<button
|
<button
|
||||||
@@ -1606,7 +1642,7 @@ function renderSettingRow(p) {
|
|||||||
disabled="${() => isLocked() || isNumericUpdating(p.key) || !canStepNumericParam(p, -1)}"
|
disabled="${() => isLocked() || isNumericUpdating(p.key) || !canStepNumericParam(p, -1)}"
|
||||||
@click="${() => stepNumericParam(p, -1)}">-</button>
|
@click="${() => stepNumericParam(p, -1)}">-</button>
|
||||||
<div class="ds-stepper-meta">
|
<div class="ds-stepper-meta">
|
||||||
<span>${formatNumericParamValue(p, bounds.min, state.values)} to ${formatNumericParamValue(p, bounds.max, state.values)}</span>
|
<span>${formatSliderValue(bounds.min, String(bounds.step), p.precision, p.key)} to ${formatSliderValue(bounds.max, String(bounds.step), p.precision, p.key)}</span>
|
||||||
<span class="ds-step-value">Step: ${stepLabel} per click</span>
|
<span class="ds-step-value">Step: ${stepLabel} per click</span>
|
||||||
<span class="ds-default-value">Default: ${defaultLabel}</span>
|
<span class="ds-default-value">Default: ${defaultLabel}</span>
|
||||||
<div class="ds-manual-row">
|
<div class="ds-manual-row">
|
||||||
@@ -1754,7 +1790,8 @@ function renderSettingRow(p) {
|
|||||||
if (isColor) return formatColorDisplayValue(p)
|
if (isColor) return formatColorDisplayValue(p)
|
||||||
if (isReadout) return formatReadoutValue(p)
|
if (isReadout) return formatReadoutValue(p)
|
||||||
const currentValue = state.sliderPreviewValues[p.key] ?? state.values[p.key]
|
const currentValue = state.sliderPreviewValues[p.key] ?? state.values[p.key]
|
||||||
return currentValue !== undefined ? formatNumericParamValue(p, currentValue, state.values) : ".."
|
const bounds = numericBounds(p)
|
||||||
|
return currentValue !== undefined ? formatSliderValue(currentValue, String(bounds.step), p.precision, p.key) : ".."
|
||||||
}}</span>` : ""}
|
}}</span>` : ""}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -151,11 +151,6 @@
|
|||||||
color: var(--color-black);
|
color: var(--color-black);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ml-button-danger {
|
|
||||||
background: rgba(224, 85, 119, 0.12);
|
|
||||||
color: var(--danger-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ml-button:disabled {
|
.ml-button:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const state = reactive({
|
|||||||
download: {},
|
download: {},
|
||||||
models: [],
|
models: [],
|
||||||
summary: {},
|
summary: {},
|
||||||
|
manifest: { version: "unknown", shortcomings: [], opportunities: [] },
|
||||||
})
|
})
|
||||||
|
|
||||||
let initialized = false
|
let initialized = false
|
||||||
@@ -26,33 +27,31 @@ function modelLabel(modelId) {
|
|||||||
return modelById(modelId)?.label || modelId || "not selected"
|
return modelById(modelId)?.label || modelId || "not selected"
|
||||||
}
|
}
|
||||||
|
|
||||||
function availableModels() {
|
function readyModels() {
|
||||||
return state.models.filter(model => model.modelLabArtifactAvailable)
|
return state.models.filter(model => model.modelLabArtifactAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadedModels() {
|
|
||||||
return availableModels().filter(model => model.modelLabArtifactInstalled)
|
|
||||||
}
|
|
||||||
|
|
||||||
function candidateModels(role) {
|
function candidateModels(role) {
|
||||||
const downloaded = downloadedModels()
|
const ready = readyModels()
|
||||||
if (role !== "longitudinal") return downloaded
|
if (role !== "longitudinal") return ready
|
||||||
const lateral = modelById(state.configuration.lateralModel)
|
const lateral = modelById(state.configuration.lateralModel)
|
||||||
if (!lateral) return downloaded
|
if (!lateral) return ready
|
||||||
return downloaded.filter(model => model.value !== lateral.value)
|
return ready.filter(model => model.value !== lateral.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectionError() {
|
function selectionError() {
|
||||||
|
if (!state.chestnutReady) return "Connect a firmware-ready Chestnut first."
|
||||||
if (state.isOnroad) return "Park before changing the laboratory pair."
|
if (state.isOnroad) return "Park before changing the laboratory pair."
|
||||||
if (downloadedModels().length < 2) return "Download at least two eGPU variants before composing a pair."
|
|
||||||
const lateral = modelById(state.configuration.lateralModel)
|
const lateral = modelById(state.configuration.lateralModel)
|
||||||
const longitudinal = modelById(state.configuration.longitudinalModel)
|
const longitudinal = modelById(state.configuration.longitudinalModel)
|
||||||
if (!lateral || !longitudinal) return "Choose two downloaded eGPU variants."
|
if (!lateral || !longitudinal) return "Choose two small models with published Chestnut artifacts."
|
||||||
if (lateral.value === longitudinal.value) return "Lateral and longitudinal models must be different."
|
if (lateral.value === longitudinal.value) return "Lateral and longitudinal models must be different."
|
||||||
|
if (!lateral.modelLabArtifactAvailable || !longitudinal.modelLabArtifactAvailable) {
|
||||||
|
return "Both models need a precompiled AMD artifact in the manifest."
|
||||||
|
}
|
||||||
if (!lateral.modelLabArtifactInstalled || !longitudinal.modelLabArtifactInstalled) {
|
if (!lateral.modelLabArtifactInstalled || !longitudinal.modelLabArtifactInstalled) {
|
||||||
return "Download both eGPU variants first."
|
return "Prepare both precompiled AMD artifacts first."
|
||||||
}
|
}
|
||||||
if (!state.chestnutReady) return "Connect a firmware-ready Chestnut to enable this pair."
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,14 +75,17 @@ function applyPayload(payload) {
|
|||||||
state.download = payload?.download && typeof payload.download === "object" ? payload.download : {}
|
state.download = payload?.download && typeof payload.download === "object" ? payload.download : {}
|
||||||
state.models = Array.isArray(payload?.models) ? payload.models : []
|
state.models = Array.isArray(payload?.models) ? payload.models : []
|
||||||
state.summary = payload?.summary && typeof payload.summary === "object" ? payload.summary : {}
|
state.summary = payload?.summary && typeof payload.summary === "object" ? payload.summary : {}
|
||||||
|
state.manifest = payload?.manifest && typeof payload.manifest === "object"
|
||||||
|
? payload.manifest
|
||||||
|
: { version: "unknown", shortcomings: [], opportunities: [] }
|
||||||
state.error = String(payload?.configurationError || "")
|
state.error = String(payload?.configurationError || "")
|
||||||
|
|
||||||
const downloaded = downloadedModels()
|
const ready = readyModels()
|
||||||
if (!downloaded.some(model => model.value === state.configuration.lateralModel)) {
|
if (!modelById(state.configuration.lateralModel) && ready.length > 0) {
|
||||||
state.configuration.lateralModel = downloaded[0]?.value || ""
|
state.configuration.lateralModel = ready[0].value
|
||||||
}
|
}
|
||||||
if (!downloaded.some(model => model.value === state.configuration.longitudinalModel)) {
|
if (!modelById(state.configuration.longitudinalModel) && ready.length > 1) {
|
||||||
state.configuration.longitudinalModel = downloaded.find(model => (
|
state.configuration.longitudinalModel = ready.find(model => (
|
||||||
model.value !== state.configuration.lateralModel
|
model.value !== state.configuration.lateralModel
|
||||||
))?.value || ""
|
))?.value || ""
|
||||||
}
|
}
|
||||||
@@ -155,7 +157,7 @@ async function prepareModel(modelId) {
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ model: modelId }),
|
body: JSON.stringify({ model: modelId }),
|
||||||
})
|
})
|
||||||
state.message = String(payload.message || "eGPU variant download queued.")
|
state.message = String(payload.message || "Chestnut artifact download queued.")
|
||||||
await refresh()
|
await refresh()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
state.error = error?.message || String(error)
|
state.error = error?.message || String(error)
|
||||||
@@ -164,29 +166,6 @@ async function prepareModel(modelId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteModel(modelId) {
|
|
||||||
if (state.saving || !modelId) return
|
|
||||||
const model = modelById(modelId)
|
|
||||||
if (!window.confirm(`Delete the eGPU variant for "${model?.label || modelId}"? The normal on-device model will not be removed.`)) return
|
|
||||||
state.saving = true
|
|
||||||
state.error = ""
|
|
||||||
state.message = ""
|
|
||||||
try {
|
|
||||||
const payload = await requestJson("/api/model-laboratory/artifact", {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ model: modelId }),
|
|
||||||
})
|
|
||||||
selectionDirty = false
|
|
||||||
applyPayload(payload)
|
|
||||||
state.message = String(payload.message || "eGPU variant deleted.")
|
|
||||||
} catch (error) {
|
|
||||||
state.error = error?.message || String(error)
|
|
||||||
} finally {
|
|
||||||
state.saving = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindControls() {
|
function bindControls() {
|
||||||
const lateral = document.getElementById("ml-lateral-model")
|
const lateral = document.getElementById("ml-lateral-model")
|
||||||
const longitudinal = document.getElementById("ml-longitudinal-model")
|
const longitudinal = document.getElementById("ml-longitudinal-model")
|
||||||
@@ -198,11 +177,6 @@ function bindControls() {
|
|||||||
button.dataset.bound = "1"
|
button.dataset.bound = "1"
|
||||||
button.addEventListener("click", () => prepareModel(button.dataset.mlDownload))
|
button.addEventListener("click", () => prepareModel(button.dataset.mlDownload))
|
||||||
})
|
})
|
||||||
document.querySelectorAll("[data-ml-delete]").forEach(button => {
|
|
||||||
if (button.dataset.bound === "1") return
|
|
||||||
button.dataset.bound = "1"
|
|
||||||
button.addEventListener("click", () => deleteModel(button.dataset.mlDelete))
|
|
||||||
})
|
|
||||||
|
|
||||||
if (lateral) {
|
if (lateral) {
|
||||||
lateral.value = state.configuration.lateralModel
|
lateral.value = state.configuration.lateralModel
|
||||||
@@ -252,15 +226,15 @@ function ensurePolling() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
await refresh()
|
await refresh()
|
||||||
pollHandle = setTimeout(poll, state.download?.model ? 1000 : 5000)
|
pollHandle = setTimeout(poll, 5000)
|
||||||
}
|
}
|
||||||
pollHandle = setTimeout(poll, 5000)
|
pollHandle = setTimeout(poll, 5000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderModel(model) {
|
function renderModel(model) {
|
||||||
const artifactStatus = model.modelLabArtifactInstalled
|
const artifactStatus = model.modelLabArtifactInstalled
|
||||||
? "eGPU variant downloaded"
|
? "AMD ready"
|
||||||
: "eGPU variant not downloaded"
|
: model.modelLabArtifactAvailable ? "AMD download needed" : "AMD not published"
|
||||||
return html`
|
return html`
|
||||||
<div class="ml-model">
|
<div class="ml-model">
|
||||||
<div>
|
<div>
|
||||||
@@ -274,15 +248,8 @@ function renderModel(model) {
|
|||||||
${artifactStatus}
|
${artifactStatus}
|
||||||
</span>
|
</span>
|
||||||
${model.modelLabArtifactAvailable && !model.modelLabArtifactInstalled ? html`
|
${model.modelLabArtifactAvailable && !model.modelLabArtifactInstalled ? html`
|
||||||
<button class="ml-button" data-ml-download="${model.value}" disabled="${() => state.saving || state.isOnroad || Boolean(state.download?.model)}">
|
<button class="ml-button" data-ml-download="${model.value}" disabled="${() => state.saving || state.isOnroad}">
|
||||||
${() => state.download?.model === model.value
|
Prepare for Chestnut
|
||||||
? `Downloading · ${state.download?.progress || "starting…"}`
|
|
||||||
: "Download eGPU variant"}
|
|
||||||
</button>
|
|
||||||
` : ""}
|
|
||||||
${model.modelLabArtifactInstalled ? html`
|
|
||||||
<button class="ml-button ml-button-danger" data-ml-delete="${model.value}" disabled="${() => state.saving || state.isOnroad || Boolean(state.download?.model)}">
|
|
||||||
Delete eGPU variant
|
|
||||||
</button>
|
</button>
|
||||||
` : ""}
|
` : ""}
|
||||||
</div>
|
</div>
|
||||||
@@ -320,22 +287,11 @@ export function ModelLaboratory() {
|
|||||||
${() => state.loading ? html`<div class="ml-card">Loading laboratory status…</div>` : ""}
|
${() => state.loading ? html`<div class="ml-card">Loading laboratory status…</div>` : ""}
|
||||||
|
|
||||||
${() => !state.loading ? html`
|
${() => !state.loading ? html`
|
||||||
<section class="ml-card">
|
|
||||||
<div class="ml-card-heading">
|
|
||||||
<div>
|
|
||||||
<h3>Available models</h3>
|
|
||||||
<p>Download eGPU-compatible small models. These are separate from the small models in Model Manager because they are compiled for the eGPU.</p>
|
|
||||||
<p>${() => `${state.summary.ready || 0} downloaded · ${Math.max((state.summary.published || 0) - (state.summary.ready || 0), 0)} available to download.`}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-model-list">${() => availableModels().map(renderModel)}</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="ml-card">
|
<section class="ml-card">
|
||||||
<div class="ml-card-heading">
|
<div class="ml-card-heading">
|
||||||
<div>
|
<div>
|
||||||
<h3>Compose a pair</h3>
|
<h3>Compose a pair</h3>
|
||||||
<p>Choose from downloaded eGPU variant combinations below.</p>
|
<p>Both precompiled small models stay resident and run every camera frame on Chestnut's AMD GPU.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="${() => `ml-state ${state.configuration.enabled ? "is-enabled" : ""}`}">
|
<span class="${() => `ml-state ${state.configuration.enabled ? "is-enabled" : ""}`}">
|
||||||
${() => state.configuration.enabled ? "Enabled" : "Disabled"}
|
${() => state.configuration.enabled ? "Enabled" : "Disabled"}
|
||||||
@@ -408,6 +364,20 @@ export function ModelLaboratory() {
|
|||||||
<p class="ml-muted">Both roles evaluate the same frame at 20 Hz. A runtime failure suppresses that frame and falls back to the built-in QCOM model.</p>
|
<p class="ml-muted">Both roles evaluate the same frame at 20 Hz. A runtime failure suppresses that frame and falls back to the built-in QCOM model.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="ml-card">
|
||||||
|
<div class="ml-card-heading">
|
||||||
|
<div>
|
||||||
|
<h3>Available models</h3>
|
||||||
|
<p>${() => `${state.summary.ready || 0} ready to pair · ${Math.max((state.summary.published || 0) - (state.summary.ready || 0), 0)} available to download.`}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-model-list">${() => readyModels().map(renderModel)}</div>
|
||||||
|
<div class="ml-note">
|
||||||
|
Model Manager downloads the manifest's precompiled AMD variants. Nothing is compiled on the comma.
|
||||||
|
A normal installed model may still need its separate Chestnut artifact.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
` : ""}
|
` : ""}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -37,44 +37,13 @@
|
|||||||
.wheelCard,
|
.wheelCard,
|
||||||
.wheelNotice,
|
.wheelNotice,
|
||||||
.wheelError,
|
.wheelError,
|
||||||
.wheelDeviceSummary,
|
.wheelDeviceSummary {
|
||||||
.wheelPolicy {
|
|
||||||
background: var(--sidebar-bg);
|
background: var(--sidebar-bg);
|
||||||
border: 1px solid var(--sidebar-border-color);
|
border: 1px solid var(--sidebar-border-color);
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wheelPolicy {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wheelPolicy span,
|
|
||||||
.wheelPolicy small {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wheelPolicy small {
|
|
||||||
margin-top: 5px;
|
|
||||||
opacity: 0.68;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wheelPolicy input {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
accent-color: #8b6cc5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wheelPolicy:has(input:disabled) {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wheelSlotGrid,
|
.wheelSlotGrid,
|
||||||
.wheelControllerGrid,
|
.wheelControllerGrid,
|
||||||
.wheelMappings {
|
.wheelMappings {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ const state = reactive({
|
|||||||
slots: [],
|
slots: [],
|
||||||
controllerSlots: [],
|
controllerSlots: [],
|
||||||
controllerOptions: [],
|
controllerOptions: [],
|
||||||
disconnectControllersOffroad: false,
|
|
||||||
speedUnit: "mph",
|
speedUnit: "mph",
|
||||||
speedMinimum: 5,
|
speedMinimum: 5,
|
||||||
speedMaximum: 90,
|
speedMaximum: 90,
|
||||||
@@ -38,7 +37,6 @@ async function refresh() {
|
|||||||
state.slots = Array.isArray(payload.slots) ? payload.slots : []
|
state.slots = Array.isArray(payload.slots) ? payload.slots : []
|
||||||
state.controllerSlots = Array.isArray(payload.controller_slots) ? payload.controller_slots : []
|
state.controllerSlots = Array.isArray(payload.controller_slots) ? payload.controller_slots : []
|
||||||
state.controllerOptions = Array.isArray(payload.controller_options) ? payload.controller_options : []
|
state.controllerOptions = Array.isArray(payload.controller_options) ? payload.controller_options : []
|
||||||
state.disconnectControllersOffroad = !!payload.disconnect_controllers_offroad
|
|
||||||
state.speedUnit = typeof payload.speed_unit === "string" ? payload.speed_unit : "mph"
|
state.speedUnit = typeof payload.speed_unit === "string" ? payload.speed_unit : "mph"
|
||||||
state.speedMinimum = Number(payload.speed_minimum || 5)
|
state.speedMinimum = Number(payload.speed_minimum || 5)
|
||||||
state.speedMaximum = Number(payload.speed_maximum || 90)
|
state.speedMaximum = Number(payload.speed_maximum || 90)
|
||||||
@@ -267,16 +265,6 @@ export function WheelControls() {
|
|||||||
${() => !state.loading && !state.available && state.mappings.length ? html`<div class="wheelNotice">The wheel control service is starting.</div>` : ""}
|
${() => !state.loading && !state.available && state.mappings.length ? html`<div class="wheelNotice">The wheel control service is starting.</div>` : ""}
|
||||||
${() => state.testing ? testPanel() : ""}
|
${() => state.testing ? testPanel() : ""}
|
||||||
|
|
||||||
<label class="wheelPolicy">
|
|
||||||
<span>
|
|
||||||
<strong>Disconnect controllers when offroad</strong>
|
|
||||||
<small>After two minutes offroad, paired controllers disconnect to save battery and reconnect when the car starts. Bluetooth and audio-only devices stay connected.</small>
|
|
||||||
</span>
|
|
||||||
<input type="checkbox" checked="${() => state.disconnectControllersOffroad}"
|
|
||||||
disabled="${() => !state.offroad || !!state.busy}"
|
|
||||||
@change="${event => request("offroad-disconnect", { enabled: event.currentTarget.checked })}" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="wheelDeviceSummary">
|
<div class="wheelDeviceSummary">
|
||||||
<div class="wheelDeviceHeading">
|
<div class="wheelDeviceHeading">
|
||||||
<strong>Connected input devices</strong>
|
<strong>Connected input devices</strong>
|
||||||
|
|||||||
@@ -138,13 +138,10 @@
|
|||||||
|
|
||||||
.dh-donut {
|
.dh-donut {
|
||||||
--dh-value: 0;
|
--dh-value: 0;
|
||||||
align-items: center;
|
display: grid;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
gap: 3px;
|
|
||||||
height: 116px;
|
height: 116px;
|
||||||
justify-content: center;
|
place-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 116px;
|
width: 116px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -592,66 +592,6 @@ ul { list-style: none; margin: 0; padding: 0; }
|
|||||||
transition: transform var(--motion-fast), box-shadow var(--motion-fast);
|
transition: transform var(--motion-fast), box-shadow var(--motion-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gx-unit-note {
|
|
||||||
align-items: center;
|
|
||||||
background: var(--primary-container);
|
|
||||||
border: 1px solid var(--primary);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
color: var(--on-primary-container);
|
|
||||||
display: flex;
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
gap: var(--sp-2);
|
|
||||||
margin: 0 0 var(--sp-4);
|
|
||||||
padding: var(--sp-3) var(--sp-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gx-unit-note i {
|
|
||||||
color: var(--primary);
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gx-row.gx-diagnostic-row--changed {
|
|
||||||
background: rgba(139, 108, 197, 0.18) !important;
|
|
||||||
box-shadow: inset 3px 0 var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gx-update-progress {
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
margin-top: var(--sp-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gx-update-progress__track {
|
|
||||||
background: rgba(255, 255, 255, 0.12);
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
height: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gx-update-progress__fill {
|
|
||||||
background: linear-gradient(90deg, #5ec8c8 0%, #8b6cc5 100%);
|
|
||||||
border-radius: inherit;
|
|
||||||
height: 100%;
|
|
||||||
transition: width 0.4s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gx-update-progress__fill--error {
|
|
||||||
background: linear-gradient(90deg, #b43a3a 0%, #de5656 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gx-update-progress__meta {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
gap: var(--sp-2);
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gx-update-progress small {
|
|
||||||
color: var(--text-muted);
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .gx-card {
|
[data-theme="light"] .gx-card {
|
||||||
border: 1px solid rgba(120, 73, 232, 0.22);
|
border: 1px solid rgba(120, 73, 232, 0.22);
|
||||||
}
|
}
|
||||||
@@ -809,13 +749,6 @@ ul { list-style: none; margin: 0; padding: 0; }
|
|||||||
.gx-slider-row .gx-row__value { text-align: left; min-width: 0; }
|
.gx-slider-row .gx-row__value { text-align: left; min-width: 0; }
|
||||||
.gx-slider-row .gx-slider-reset { align-self: flex-end; }
|
.gx-slider-row .gx-slider-reset { align-self: flex-end; }
|
||||||
|
|
||||||
.gx-slider-meta {
|
|
||||||
color: var(--text-muted);
|
|
||||||
display: flex;
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="range"].gx-slider {
|
input[type="range"].gx-slider {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
@@ -1564,4 +1497,4 @@ input[type="color"].gx-color {
|
|||||||
.gx-menu-btn { display: inline-flex; }
|
.gx-menu-btn { display: inline-flex; }
|
||||||
.gx-back-btn { display: none; }
|
.gx-back-btn { display: none; }
|
||||||
.gx-content { padding-bottom: var(--sp-6); }
|
.gx-content { padding-bottom: var(--sp-6); }
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<meta name="apple-mobile-web-app-title" content="Galaxy">
|
<meta name="apple-mobile-web-app-title" content="Big Dipper">
|
||||||
<meta name="format-detection" content="telephone=no">
|
<meta name="format-detection" content="telephone=no">
|
||||||
<meta name="theme-color" content="#8b6cc5" />
|
<meta name="theme-color" content="#8b6cc5" />
|
||||||
<link rel="manifest" href="/assets/mobile/manifest.json" crossorigin="use-credentials">
|
<link rel="manifest" href="/assets/mobile/manifest.json" crossorigin="use-credentials">
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<title>Galaxy</title>
|
<title>Big Dipper</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -136,7 +136,6 @@ export const api = {
|
|||||||
getModelLab() { return request("/api/model-laboratory", { cache: "no-store" }) },
|
getModelLab() { return request("/api/model-laboratory", { cache: "no-store" }) },
|
||||||
saveModelLab(config) { return request("/api/model-laboratory", { method: "PUT", data: config }) },
|
saveModelLab(config) { return request("/api/model-laboratory", { method: "PUT", data: config }) },
|
||||||
prepareModelLabArtifact(model) { return request("/api/model-laboratory/download", { method: "POST", data: { model } }) },
|
prepareModelLabArtifact(model) { return request("/api/model-laboratory/download", { method: "POST", data: { model } }) },
|
||||||
deleteModelLabArtifact(model) { return request("/api/model-laboratory/artifact", { method: "DELETE", data: { model } }) },
|
|
||||||
|
|
||||||
getErrorLogs() { return request("/api/error_logs", { headers: { Accept: "application/json" } }) },
|
getErrorLogs() { return request("/api/error_logs", { headers: { Accept: "application/json" } }) },
|
||||||
getErrorLog(filename) { return fetch(`/api/error_logs/${encodeURIComponent(filename)}`).then((r) => r.text()) },
|
getErrorLog(filename) { return fetch(`/api/error_logs/${encodeURIComponent(filename)}`).then((r) => r.text()) },
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import { Logs } from "./views/Logs.js"
|
|||||||
import { Tuning } from "./views/Tuning.js"
|
import { Tuning } from "./views/Tuning.js"
|
||||||
import { Navigation } from "./views/Navigation.js"
|
import { Navigation } from "./views/Navigation.js"
|
||||||
import { Vehicle } from "./views/Vehicle.js"
|
import { Vehicle } from "./views/Vehicle.js"
|
||||||
import { Bluetooth } from "./views/Bluetooth.js"
|
|
||||||
import { SystemTools } from "./views/SystemTools.js"
|
import { SystemTools } from "./views/SystemTools.js"
|
||||||
import { ToolEmbed } from "./views/ToolEmbed.js"
|
import { ToolEmbed } from "./views/ToolEmbed.js"
|
||||||
import { Doors } from "./views/Doors.js"
|
import { Doors } from "./views/Doors.js"
|
||||||
import { Galaxy } from "./views/Galaxy.js"
|
import { Galaxy } from "./views/Galaxy.js"
|
||||||
import { Tsk } from "./views/Tsk.js"
|
import { Tsk } from "./views/Tsk.js"
|
||||||
|
import { Sentry } from "./views/Sentry.js"
|
||||||
import { ModelManager } from "./views/ModelManager.js"
|
import { ModelManager } from "./views/ModelManager.js"
|
||||||
import { Plots } from "./views/Plots.js"
|
import { Plots } from "./views/Plots.js"
|
||||||
import { TestingGround } from "./views/TestingGround.js"
|
import { TestingGround } from "./views/TestingGround.js"
|
||||||
@@ -43,13 +43,12 @@ const VIEWS = {
|
|||||||
"/tuning": Tuning,
|
"/tuning": Tuning,
|
||||||
"/navigation": Navigation,
|
"/navigation": Navigation,
|
||||||
"/vehicle": Vehicle,
|
"/vehicle": Vehicle,
|
||||||
"/bluetooth": Bluetooth,
|
|
||||||
"/system": SystemTools,
|
"/system": SystemTools,
|
||||||
"/embed": ToolEmbed,
|
"/embed": ToolEmbed,
|
||||||
"/manage_doors": Doors,
|
"/manage_doors": Doors,
|
||||||
"/galaxy": Galaxy,
|
"/galaxy": Galaxy,
|
||||||
"/manage_tsk": Tsk,
|
"/manage_tsk": Tsk,
|
||||||
"/sentry": Cameras,
|
"/sentry": Sentry,
|
||||||
"/manage_models": ModelManager,
|
"/manage_models": ModelManager,
|
||||||
"/plots": Plots,
|
"/plots": Plots,
|
||||||
"/testing_ground": TestingGround,
|
"/testing_ground": TestingGround,
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ const NAV = {
|
|||||||
{ name: "Recordings", link: "/recordings", icon: "bi-camera-reels" },
|
{ name: "Recordings", link: "/recordings", icon: "bi-camera-reels" },
|
||||||
],
|
],
|
||||||
tools: [
|
tools: [
|
||||||
{ name: "Bluetooth", link: "/bluetooth", icon: "bi-bluetooth" },
|
|
||||||
{ name: "Cameras & Monitoring", link: "/cameras", icon: "bi-camera-video" },
|
{ name: "Cameras & Monitoring", link: "/cameras", icon: "bi-camera-video" },
|
||||||
{ name: "Galaxy", link: "/galaxy", icon: "bi-globe2" },
|
{ name: "Galaxy", link: "/galaxy", icon: "bi-globe2" },
|
||||||
{ name: "Logs & Diagnostics", link: "/logs", icon: "bi-exclamation-triangle" },
|
{ name: "Logs & Diagnostics", link: "/logs", icon: "bi-exclamation-triangle" },
|
||||||
{ name: "Model Manager", link: "/manage_models", icon: "bi-cpu" },
|
{ name: "Model Manager", link: "/manage_models", icon: "bi-cpu" },
|
||||||
{ name: "Navigation & Maps", link: "/navigation", icon: "bi-map" },
|
{ name: "Navigation & Maps", link: "/navigation", icon: "bi-map" },
|
||||||
|
{ name: "Sentry Mode", link: "/sentry", icon: "bi-shield-exclamation" },
|
||||||
{ name: "System Tools", link: "/system", icon: "bi-arrow-repeat" },
|
{ name: "System Tools", link: "/system", icon: "bi-arrow-repeat" },
|
||||||
{ name: "Model Laboratory", link: "/model_laboratory", icon: "bi-bezier2" },
|
{ name: "Model Laboratory", link: "/model_laboratory", icon: "bi-bezier2" },
|
||||||
{ name: "Plots", link: "/plots", icon: "bi-graph-up-arrow" },
|
{ name: "Plots", link: "/plots", icon: "bi-graph-up-arrow" },
|
||||||
@@ -102,7 +102,7 @@ export const AppShell = {
|
|||||||
<button type="button" class="gx-icon-btn gx-menu-btn" aria-label="Menu" @click="store.drawerOpen = true">
|
<button type="button" class="gx-icon-btn gx-menu-btn" aria-label="Menu" @click="store.drawerOpen = true">
|
||||||
<i class="bi bi-list"></i>
|
<i class="bi bi-list"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="gx-appbar__title">Galaxy</span>
|
<span class="gx-appbar__title">Big Dipper</span>
|
||||||
<div class="gx-searchwrap">
|
<div class="gx-searchwrap">
|
||||||
<input ref="searchInput" class="gx-search gx-appbar__search" type="search" placeholder="Search toggles..."
|
<input ref="searchInput" class="gx-search gx-appbar__search" type="search" placeholder="Search toggles..."
|
||||||
v-model="search" aria-label="Search toggles" />
|
v-model="search" aria-label="Search toggles" />
|
||||||
@@ -128,8 +128,8 @@ export const AppShell = {
|
|||||||
</transition>
|
</transition>
|
||||||
<aside class="gx-drawer" :class="{ open: store.drawerOpen }">
|
<aside class="gx-drawer" :class="{ open: store.drawerOpen }">
|
||||||
<div class="gx-drawer__header">
|
<div class="gx-drawer__header">
|
||||||
<img class="gx-logo" src="/assets/images/main_logo.png" alt="Galaxy logo" />
|
<img class="gx-logo" src="/assets/images/main_logo.png" alt="Big Dipper logo" />
|
||||||
<span class="gx-drawer-title">Galaxy</span>
|
<span class="gx-drawer-title">Big Dipper</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gx-nav-section">
|
<div class="gx-nav-section">
|
||||||
<div class="gx-nav-section__title">Main</div>
|
<div class="gx-nav-section__title">Main</div>
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export const BluetoothPanel = {
|
|||||||
availableDevices() { return this.devices.filter((d) => !d.paired && !d.trusted && !d.connected) },
|
availableDevices() { return this.devices.filter((d) => !d.paired && !d.trusted && !d.connected) },
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
address,
|
|
||||||
async refresh() {
|
async refresh() {
|
||||||
try {
|
try {
|
||||||
const p = await api.getBluetoothStatus()
|
const p = await api.getBluetoothStatus()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { api, showSnackbar } from "../api.js"
|
import { api, showSnackbar } from "../api.js"
|
||||||
import {
|
import {
|
||||||
coerceValueByType, formatNumericParamValue, formatReadoutValue, getColorDefault,
|
coerceValueByType, formatSliderValue, formatReadoutValue, getColorDefault,
|
||||||
normalizeHexColor, numericBounds, numericEpsilon, snapNumericToBoundsAndStep,
|
normalizeHexColor, numericBounds, numericEpsilon, snapNumericToBoundsAndStep,
|
||||||
resolveVehicleUnitParam, stepPrecision,
|
stepPrecision,
|
||||||
} from "../params.js"
|
} from "../params.js"
|
||||||
import { FavoritesEditor } from "./FavoritesEditor.js"
|
import { FavoritesEditor } from "./FavoritesEditor.js"
|
||||||
|
|
||||||
@@ -12,7 +12,6 @@ export const GalaxyToggleCard = {
|
|||||||
props: {
|
props: {
|
||||||
param: { type: Object, required: true },
|
param: { type: Object, required: true },
|
||||||
value: { default: undefined },
|
value: { default: undefined },
|
||||||
values: { type: Object, default: () => ({}) },
|
|
||||||
locked: { type: Boolean, default: false },
|
locked: { type: Boolean, default: false },
|
||||||
manageable: { type: Boolean, default: false },
|
manageable: { type: Boolean, default: false },
|
||||||
manageOpen: { type: Boolean, default: false },
|
manageOpen: { type: Boolean, default: false },
|
||||||
@@ -29,9 +28,8 @@ export const GalaxyToggleCard = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
displayParam() { return resolveVehicleUnitParam(this.param, this.values) },
|
bounds() { return numericBounds(this.param, {}) },
|
||||||
bounds() { return numericBounds(this.displayParam, this.values) },
|
precision() { return stepPrecision(this.bounds.step, this.param.precision) },
|
||||||
precision() { return stepPrecision(this.bounds.step, this.displayParam.precision) },
|
|
||||||
epsilon() { return numericEpsilon(this.precision) },
|
epsilon() { return numericEpsilon(this.precision) },
|
||||||
isSlider() { return this.isNumeric },
|
isSlider() { return this.isNumeric },
|
||||||
isNumeric() { return this.param.ui_type === "numeric" },
|
isNumeric() { return this.param.ui_type === "numeric" },
|
||||||
@@ -40,17 +38,11 @@ export const GalaxyToggleCard = {
|
|||||||
currentValue() { return this.preview !== undefined ? this.preview : this.value },
|
currentValue() { return this.preview !== undefined ? this.preview : this.value },
|
||||||
displayValue() {
|
displayValue() {
|
||||||
if (this.isColor) return normalizeHexColor(this.value) ? normalizeHexColor(this.value).toUpperCase() : "Stock"
|
if (this.isColor) return normalizeHexColor(this.value) ? normalizeHexColor(this.value).toUpperCase() : "Stock"
|
||||||
if (this.isReadout) return formatReadoutValue(this.displayParam, this.value)
|
if (this.isReadout) return formatReadoutValue(this.param, this.value)
|
||||||
return this.value !== undefined && this.value !== null ? formatNumericParamValue(this.displayParam, this.value, this.values) : ".."
|
return this.value !== undefined && this.value !== null ? formatSliderValue(this.value, String(this.bounds.step), this.param.precision, this.param.key) : ".."
|
||||||
},
|
},
|
||||||
sliderDisplay() {
|
sliderDisplay() {
|
||||||
return this.value !== undefined ? formatNumericParamValue(this.displayParam, this.currentValue, this.values) : ".."
|
return this.value !== undefined ? formatSliderValue(this.currentValue, String(this.bounds.step), this.param.precision, this.param.key) : ".."
|
||||||
},
|
|
||||||
sliderRangeDisplay() {
|
|
||||||
return `${formatNumericParamValue(this.displayParam, this.bounds.min, this.values)} to ${formatNumericParamValue(this.displayParam, this.bounds.max, this.values)}`
|
|
||||||
},
|
|
||||||
sliderStepDisplay() {
|
|
||||||
return formatNumericParamValue(this.displayParam, this.bounds.step, this.values)
|
|
||||||
},
|
},
|
||||||
isColor() { return this.param.ui_type === "color" },
|
isColor() { return this.param.ui_type === "color" },
|
||||||
isAction() { return this.param.ui_type === "action" },
|
isAction() { return this.param.ui_type === "action" },
|
||||||
@@ -175,10 +167,10 @@ export const GalaxyToggleCard = {
|
|||||||
<div>
|
<div>
|
||||||
<div class="gx-row" :class="{ disabled: locked, 'gx-row--favorites': isFavorites, 'gx-row--stack': isSlider || isSelect }">
|
<div class="gx-row" :class="{ disabled: locked, 'gx-row--favorites': isFavorites, 'gx-row--stack': isSlider || isSelect }">
|
||||||
<div class="gx-row__info">
|
<div class="gx-row__info">
|
||||||
<span class="gx-row__label">{{ displayParam.label }}
|
<span class="gx-row__label">{{ param.label }}
|
||||||
<span v-if="displayParam.settings_tier === 'advanced'" class="gx-chip gx-chip--advanced">Advanced</span>
|
<span v-if="param.settings_tier === 'advanced'" class="gx-chip gx-chip--advanced">Advanced</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="displayParam.description" class="gx-row__desc">{{ displayParam.description }}</span>
|
<span v-if="param.description" class="gx-row__desc">{{ param.description }}</span>
|
||||||
<div v-if="locked" class="gx-row__desc"><strong>Locked:</strong> This setting can only be changed while parked.</div>
|
<div v-if="locked" class="gx-row__desc"><strong>Locked:</strong> This setting can only be changed while parked.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -198,10 +190,6 @@ export const GalaxyToggleCard = {
|
|||||||
:value="currentValue" :disabled="locked || updating"
|
:value="currentValue" :disabled="locked || updating"
|
||||||
@input="onSliderInput" @change="onSliderCommit" @blur="onSliderBlur"
|
@input="onSliderInput" @change="onSliderCommit" @blur="onSliderBlur"
|
||||||
@touchstart="beginInteract" @mousedown="beginInteract" @keydown="beginInteract" />
|
@touchstart="beginInteract" @mousedown="beginInteract" @keydown="beginInteract" />
|
||||||
<div v-if="displayParam.unit_type" class="gx-slider-meta">
|
|
||||||
<span>{{ sliderRangeDisplay }}</span>
|
|
||||||
<span>Step: {{ sliderStepDisplay }}</span>
|
|
||||||
</div>
|
|
||||||
<button class="gx-slider-reset" :disabled="locked || updating" @click="resetToDefault">Default</button>
|
<button class="gx-slider-reset" :disabled="locked || updating" @click="resetToDefault">Default</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { api } from "../api.js"
|
import { api } from "../api.js"
|
||||||
import { isSettingVisible, resolveVehicleUnitParam, slugifySectionName, applyParamChange } from "../params.js"
|
import { isSettingVisible, slugifySectionName, applyParamChange } from "../params.js"
|
||||||
import { SettingTree } from "./SettingTree.js"
|
import { SettingTree } from "./SettingTree.js"
|
||||||
import { GalaxySection } from "./GalaxySection.js"
|
import { GalaxySection } from "./GalaxySection.js"
|
||||||
|
|
||||||
@@ -35,9 +35,7 @@ export const ParamSections = {
|
|||||||
matches(p) {
|
matches(p) {
|
||||||
if (!this.search) return true
|
if (!this.search) return true
|
||||||
const q = this.search.toLowerCase()
|
const q = this.search.toLowerCase()
|
||||||
const displayParam = resolveVehicleUnitParam(p, this.values)
|
return [p.label, p.key, p.description].some((v) => String(v || "").toLowerCase().includes(q))
|
||||||
return [displayParam.label, displayParam.key, displayParam.description, displayParam.unit, displayParam.unit_search_terms]
|
|
||||||
.some((v) => String(v || "").toLowerCase().includes(q))
|
|
||||||
},
|
},
|
||||||
async load() {
|
async load() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const SettingTree = {
|
|||||||
template: `
|
template: `
|
||||||
<template v-for="p in children" :key="p.key">
|
<template v-for="p in children" :key="p.key">
|
||||||
<div class="gx-tree-node" :class="{ 'gx-tree-node--child': depth > 0 }" :style="'--gx-depth:' + depth">
|
<div class="gx-tree-node" :class="{ 'gx-tree-node--child': depth > 0 }" :style="'--gx-depth:' + depth">
|
||||||
<GalaxyToggleCard :param="p" :value="values[p.key]" :values="values" :locked="lockReason(p) !== ''"
|
<GalaxyToggleCard :param="p" :value="values[p.key]" :locked="lockReason(p) !== ''"
|
||||||
:manageable="manageable(p)" :manage-open="manageOpen(p)"
|
:manageable="manageable(p)" :manage-open="manageOpen(p)"
|
||||||
@change="$emit('change', $event)" @manage="$emit('manage', $event)" />
|
@change="$emit('change', $event)" @manage="$emit('manage', $event)" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -216,7 +216,6 @@ export const TroubleshootPanel = {
|
|||||||
<div v-if="!itemsVisible(section).length" class="gx-empty">No settings are currently different from their defaults.</div>
|
<div v-if="!itemsVisible(section).length" class="gx-empty">No settings are currently different from their defaults.</div>
|
||||||
<div v-else style="display:grid; gap:8px; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); padding:0 var(--sp-3) var(--sp-3);">
|
<div v-else style="display:grid; gap:8px; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); padding:0 var(--sp-3) var(--sp-3);">
|
||||||
<div v-for="item in itemsVisible(section)" :key="item.label" class="gx-row"
|
<div v-for="item in itemsVisible(section)" :key="item.label" class="gx-row"
|
||||||
:class="{ 'gx-diagnostic-row--changed': isChanged(item) }"
|
|
||||||
style="border:none; background:var(--surface); border-radius:var(--radius-md); margin:0; padding:10px 12px; flex-direction:column; align-items:stretch; gap:8px;">
|
style="border:none; background:var(--surface); border-radius:var(--radius-md); margin:0; padding:10px 12px; flex-direction:column; align-items:stretch; gap:8px;">
|
||||||
<div style="display:flex; align-items:center; gap:6px; min-width:0;">
|
<div style="display:flex; align-items:center; gap:6px; min-width:0;">
|
||||||
<span class="gx-row__label" style="font-size:var(--fs-sm); overflow-wrap:anywhere;">{{ item.label }}</span>
|
<span class="gx-row__label" style="font-size:var(--fs-sm); overflow-wrap:anywhere;">{{ item.label }}</span>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export const WheelControls = {
|
|||||||
loading: true, busy: "", available: false, offroad: false, learning: false,
|
loading: true, busy: "", available: false, offroad: false, learning: false,
|
||||||
devices: [], mappings: [], slots: [], controllerSlots: [], controllerOptions: [],
|
devices: [], mappings: [], slots: [], controllerSlots: [], controllerOptions: [],
|
||||||
joystickDevice: "", learningSlot: null, remainingSeconds: 0, testing: false,
|
joystickDevice: "", learningSlot: null, remainingSeconds: 0, testing: false,
|
||||||
disconnectControllersOffroad: false,
|
|
||||||
lastTested: null, speedUnit: "mph", speedMinimum: 0, speedMaximum: 0, error: "",
|
lastTested: null, speedUnit: "mph", speedMinimum: 0, speedMaximum: 0, error: "",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -30,7 +29,6 @@ export const WheelControls = {
|
|||||||
this.slots = Array.isArray(p.slots) ? p.slots : []
|
this.slots = Array.isArray(p.slots) ? p.slots : []
|
||||||
this.controllerSlots = Array.isArray(p.controller_slots) ? p.controller_slots : []
|
this.controllerSlots = Array.isArray(p.controller_slots) ? p.controller_slots : []
|
||||||
this.controllerOptions = Array.isArray(p.controller_options) ? p.controller_options : []
|
this.controllerOptions = Array.isArray(p.controller_options) ? p.controller_options : []
|
||||||
this.disconnectControllersOffroad = !!p.disconnect_controllers_offroad
|
|
||||||
this.joystickDevice = typeof p.joystick_device === "string" ? p.joystick_device : ""
|
this.joystickDevice = typeof p.joystick_device === "string" ? p.joystick_device : ""
|
||||||
this.learningSlot = Number.isInteger(p.learning_slot) ? p.learning_slot : null
|
this.learningSlot = Number.isInteger(p.learning_slot) ? p.learning_slot : null
|
||||||
this.remainingSeconds = Number.isFinite(Number(p.remaining_seconds)) ? Number(p.remaining_seconds) : 0
|
this.remainingSeconds = Number.isFinite(Number(p.remaining_seconds)) ? Number(p.remaining_seconds) : 0
|
||||||
@@ -101,18 +99,6 @@ export const WheelControls = {
|
|||||||
<button type="button" class="gx-btn" :disabled="disabled() || !mappings.length" @click="request(testing ? 'test-stop' : 'test')">{{ testing ? 'Stop Testing' : 'Test Buttons' }}</button>
|
<button type="button" class="gx-btn" :disabled="disabled() || !mappings.length" @click="request(testing ? 'test-stop' : 'test')">{{ testing ? 'Stop Testing' : 'Test Buttons' }}</button>
|
||||||
<button type="button" class="gx-btn gx-btn--danger" :disabled="disabled() || !mappings.length" @click="request('clear')">Clear All</button>
|
<button type="button" class="gx-btn gx-btn--danger" :disabled="disabled() || !mappings.length" @click="request('clear')">Clear All</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="gx-row" style="margin-bottom:12px;">
|
|
||||||
<div class="gx-row__info">
|
|
||||||
<span class="gx-row__label">Disconnect controllers when offroad</span>
|
|
||||||
<span class="gx-row__desc">After two minutes offroad, paired controllers disconnect to save battery and reconnect when the car starts. Bluetooth and audio-only devices stay connected.</span>
|
|
||||||
</div>
|
|
||||||
<label class="gx-switch">
|
|
||||||
<input type="checkbox" :checked="disconnectControllersOffroad" :disabled="disabled()"
|
|
||||||
@change="request('offroad-disconnect', { enabled: $event.target.checked })" />
|
|
||||||
<span class="gx-switch__track"></span>
|
|
||||||
<span class="gx-switch__thumb"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div v-if="testing && lastTested" style="margin-bottom:12px;">
|
<div v-if="testing && lastTested" style="margin-bottom:12px;">
|
||||||
<span class="gx-chip" :style="lastTested.mapped ? 'background:var(--success);' : 'background:var(--error);'">{{ lastTested.mapped ? 'Successful' : 'Not mapped' }}</span>
|
<span class="gx-chip" :style="lastTested.mapped ? 'background:var(--success);' : 'background:var(--error);'">{{ lastTested.mapped ? 'Successful' : 'Not mapped' }}</span>
|
||||||
<p style="color:var(--text-muted); margin-top:6px;">{{ lastTested.event_name || ('Button ' + lastTested.event_code) }} on {{ lastTested.device_name || 'External input' }} {{ lastTested.mapped ? 'is mapped to slot ' + lastTested.slot : 'has no mapping' }}.</p>
|
<p style="color:var(--text-muted); margin-top:6px;">{{ lastTested.event_name || ('Button ' + lastTested.event_code) }} on {{ lastTested.device_name || 'External input' }} {{ lastTested.mapped ? 'is mapped to slot ' + lastTested.slot : 'has no mapping' }}.</p>
|
||||||
|
|||||||
@@ -1,45 +1,4 @@
|
|||||||
export const GALAXY_DEVELOPER_MODE_KEY = "GalaxyDeveloperMode"
|
export const GALAXY_DEVELOPER_MODE_KEY = "GalaxyDeveloperMode"
|
||||||
export const VEHICLE_SPEED_UNIT_TYPE = "vehicle_speed"
|
|
||||||
|
|
||||||
const SPEED_OFFSET_RANGES = {
|
|
||||||
imperial: ["0–24", "25–34", "35–44", "45–54", "55–64", "65–74", "75–99"],
|
|
||||||
metric: ["0–29", "30–49", "50–59", "60–79", "80–99", "100–119", "120–140"],
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usesMetricUnits(values = {}) {
|
|
||||||
const value = values?.IsMetric
|
|
||||||
if (value === true || value === 1) return true
|
|
||||||
return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase())
|
|
||||||
}
|
|
||||||
|
|
||||||
export function vehicleSpeedUnit(values = {}) {
|
|
||||||
return usesMetricUnits(values) ? "km/h" : "mph"
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveVehicleUnitParam(param, values = {}) {
|
|
||||||
if (!param || param.unit_type !== VEHICLE_SPEED_UNIT_TYPE) return param
|
|
||||||
|
|
||||||
const metric = usesMetricUnits(values)
|
|
||||||
const mode = metric ? "metric" : "imperial"
|
|
||||||
const resolved = {
|
|
||||||
...param,
|
|
||||||
unit: ` ${vehicleSpeedUnit(values)}`,
|
|
||||||
unit_search_terms: "metric imperial mph km/h vehicle speed units",
|
|
||||||
}
|
|
||||||
for (const field of ["min", "max", "step", "precision"]) {
|
|
||||||
const override = param[`${mode}_${field}`]
|
|
||||||
if (override !== undefined && override !== null) resolved[field] = override
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Number.isInteger(param.unit_range_index)) {
|
|
||||||
const range = SPEED_OFFSET_RANGES[mode][param.unit_range_index]
|
|
||||||
if (range) {
|
|
||||||
resolved.label = `Speed Offset (${range} ${vehicleSpeedUnit(values)})`
|
|
||||||
resolved.description = `How much to offset posted speed limits between ${range} ${vehicleSpeedUnit(values)}.`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resolved
|
|
||||||
}
|
|
||||||
|
|
||||||
const HIDDEN_SETTING_KEYS = new Set(["HumanAcceleration"])
|
const HIDDEN_SETTING_KEYS = new Set(["HumanAcceleration"])
|
||||||
const RADAR_REQUIRED_KEYS = new Set(["HumanLaneChanges", "RadarTakeoffs"])
|
const RADAR_REQUIRED_KEYS = new Set(["HumanLaneChanges", "RadarTakeoffs"])
|
||||||
@@ -129,8 +88,7 @@ export function countAdvancedHiddenByDeveloperMode(layout, values) {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
export function numericBounds(param, values = {}) {
|
export function numericBounds(param, values) {
|
||||||
param = resolveVehicleUnitParam(param, values)
|
|
||||||
const defaultBounds = {
|
const defaultBounds = {
|
||||||
min: param.min !== undefined ? param.min : (param.data_type === "float" ? 0.0 : 0),
|
min: param.min !== undefined ? param.min : (param.data_type === "float" ? 0.0 : 0),
|
||||||
max: param.max !== undefined ? param.max : (param.data_type === "float" ? 100.0 : 100),
|
max: param.max !== undefined ? param.max : (param.data_type === "float" ? 100.0 : 100),
|
||||||
@@ -236,13 +194,6 @@ export function formatSliderValue(val, stepStr, precisionInt, key) {
|
|||||||
return Number(v.toFixed(dec)).toString()
|
return Number(v.toFixed(dec)).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatNumericParamValue(param, value, values = {}) {
|
|
||||||
const resolved = resolveVehicleUnitParam(param, values)
|
|
||||||
const bounds = numericBounds(resolved, values)
|
|
||||||
const formatted = formatSliderValue(value, String(bounds.step), resolved.precision, resolved.key)
|
|
||||||
return resolved.unit && formatted !== "--" ? `${formatted}${resolved.unit}` : formatted
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatReadoutValue(p, value) {
|
export function formatReadoutValue(p, value) {
|
||||||
const raw = value
|
const raw = value
|
||||||
const parsed = parseFloat(raw)
|
const parsed = parseFloat(raw)
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export function goBack() {
|
|||||||
window.location.hash = prev
|
window.location.hash = prev
|
||||||
}
|
}
|
||||||
|
|
||||||
const NATIVE_ROOTS = new Set(["/", "/settings", "/tools", "/recordings", "/logs", "/tuning", "/navigation", "/vehicle", "/bluetooth", "/system", "/embed", "/manage_doors", "/galaxy", "/manage_tsk", "/sentry", "/manage_models", "/plots", "/testing_ground", "/theme_maker", "/model_laboratory", "/cameras"])
|
const NATIVE_ROOTS = new Set(["/", "/settings", "/tools", "/recordings", "/logs", "/tuning", "/navigation", "/vehicle", "/system", "/embed", "/manage_doors", "/galaxy", "/manage_tsk", "/sentry", "/manage_models", "/plots", "/testing_ground", "/theme_maker", "/model_laboratory", "/cameras"])
|
||||||
|
|
||||||
export function toolHref(link) {
|
export function toolHref(link) {
|
||||||
const path = link.split("?")[0]
|
const path = link.split("?")[0]
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { BluetoothPanel } from "../components/BluetoothPanel.js"
|
|
||||||
import { GalaxySection } from "../components/GalaxySection.js"
|
|
||||||
|
|
||||||
export const Bluetooth = {
|
|
||||||
name: "Bluetooth",
|
|
||||||
components: { BluetoothPanel, GalaxySection },
|
|
||||||
template: `
|
|
||||||
<div class="gx-view">
|
|
||||||
<h2 style="margin-top:0;">Bluetooth</h2>
|
|
||||||
<GalaxySection title="Bluetooth Devices" icon="bi-bluetooth" :collapsible="false">
|
|
||||||
<BluetoothPanel />
|
|
||||||
</GalaxySection>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
import { GalaxyTabs } from "../components/GalaxyTabs.js"
|
import { GalaxyTabs } from "../components/GalaxyTabs.js"
|
||||||
import { useTabRouting } from "../composables.js"
|
import { useTabRouting } from "../composables.js"
|
||||||
import { Sentry } from "./Sentry.js"
|
|
||||||
import { Vasm } from "./Vasm.js"
|
import { Vasm } from "./Vasm.js"
|
||||||
import { Pip } from "./Pip.js"
|
import { Pip } from "./Pip.js"
|
||||||
|
|
||||||
const TABS = {
|
const TABS = {
|
||||||
sentry: "Sentry Mode",
|
|
||||||
vasm: "V-ASM Spot Monitor",
|
vasm: "V-ASM Spot Monitor",
|
||||||
pip: "PiP Side Camera",
|
pip: "PiP Side Camera",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Cameras = {
|
export const Cameras = {
|
||||||
name: "Cameras",
|
name: "Cameras",
|
||||||
components: { Sentry, Vasm, Pip, GalaxyTabs },
|
components: { Vasm, Pip, GalaxyTabs },
|
||||||
setup() {
|
setup() {
|
||||||
return useTabRouting("/cameras", { sentry: "sentry", vasm: "vasm", pip: "pip" })
|
return useTabRouting("/cameras", { vasm: "vasm", pip: "pip" })
|
||||||
},
|
},
|
||||||
data() { return { TABS } },
|
data() { return { TABS } },
|
||||||
template: `
|
template: `
|
||||||
@@ -22,11 +20,7 @@ export const Cameras = {
|
|||||||
<h2 style="margin-top:0;">Cameras & Monitoring</h2>
|
<h2 style="margin-top:0;">Cameras & Monitoring</h2>
|
||||||
<GalaxyTabs :items="TABS" :active="tab" @select="selectTab" />
|
<GalaxyTabs :items="TABS" :active="tab" @select="selectTab" />
|
||||||
|
|
||||||
<template v-if="tab === 'sentry'">
|
<template v-if="tab === 'vasm'">
|
||||||
<Sentry />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="tab === 'vasm'">
|
|
||||||
<Vasm :embedded="true" />
|
<Vasm :embedded="true" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ import { PwaInstallSection, isFirestarOrigin } from "../components/PwaInstallSec
|
|||||||
|
|
||||||
const isTunnel = () => isFirestarOrigin()
|
const isTunnel = () => isFirestarOrigin()
|
||||||
|
|
||||||
function localDeviceUrl(ip) {
|
|
||||||
const raw = String(ip || "").trim()
|
|
||||||
if (!raw || raw === "unknown") return ""
|
|
||||||
const host = raw.includes(":") && !raw.startsWith("[") ? `[${raw}]` : raw
|
|
||||||
return `http://${host}:8082`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Galaxy = {
|
export const Galaxy = {
|
||||||
name: "Galaxy",
|
name: "Galaxy",
|
||||||
components: { PwaInstallSection },
|
components: { PwaInstallSection },
|
||||||
@@ -21,17 +14,10 @@ export const Galaxy = {
|
|||||||
url: "",
|
url: "",
|
||||||
password: "",
|
password: "",
|
||||||
submitting: false,
|
submitting: false,
|
||||||
localUrl: "",
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
if (this.isTunnel) {
|
if (this.isTunnel) return
|
||||||
try {
|
|
||||||
const status = await api.getDeviceStatus()
|
|
||||||
this.localUrl = localDeviceUrl(status?.lanIp)
|
|
||||||
} catch (e) {}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const data = await api.getGalaxyStatus()
|
const data = await api.getGalaxyStatus()
|
||||||
this.paired = !!data?.paired
|
this.paired = !!data?.paired
|
||||||
@@ -90,11 +76,7 @@ export const Galaxy = {
|
|||||||
<i class="bi bi-satellite gx-alert__icon"></i>
|
<i class="bi bi-satellite gx-alert__icon"></i>
|
||||||
<div class="gx-alert__body">
|
<div class="gx-alert__body">
|
||||||
<strong>Galaxy Pairing Unavailable via Galaxy</strong>
|
<strong>Galaxy Pairing Unavailable via Galaxy</strong>
|
||||||
<span>
|
<span>Galaxy pairing requires a direct connection. Connect to your device's local network to use this feature.</span>
|
||||||
Galaxy pairing requires a direct connection. If you are on the same local network, connect here:
|
|
||||||
<a v-if="localUrl" :href="localUrl" style="color:inherit; font-weight:var(--fw-bold); overflow-wrap:anywhere;">{{ localUrl }}</a>
|
|
||||||
<span v-else>your device's local IP on port 8082.</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export const Home = {
|
|||||||
name: m.name,
|
name: m.name,
|
||||||
label: `${toInt(m.drives)} ${toNum(m.drives) === 1 ? "drive" : "drives"} using this model`,
|
label: `${toInt(m.drives)} ${toNum(m.drives) === 1 ? "drive" : "drives"} using this model`,
|
||||||
}))
|
}))
|
||||||
return { hasModels: true, style: `conic-gradient(${segments.join(", ")})`, rows }
|
return { hasModels: true, style: `background: conic-gradient(${segments.join(", ")})`, rows }
|
||||||
},
|
},
|
||||||
|
|
||||||
storageView() {
|
storageView() {
|
||||||
@@ -510,7 +510,7 @@ export const Home = {
|
|||||||
<section class="gx-card dh-card">
|
<section class="gx-card dh-card">
|
||||||
<div class="dh-card__head"><i class="bi bi-stars"></i><span>Most used models</span></div>
|
<div class="dh-card__head"><i class="bi bi-stars"></i><span>Most used models</span></div>
|
||||||
<div v-if="modelView.hasModels" class="dh-body dh-models">
|
<div v-if="modelView.hasModels" class="dh-body dh-models">
|
||||||
<div class="dh-chart-ring" :style="{ backgroundImage: modelView.style }" role="img" aria-label="Model usage share"></div>
|
<div class="dh-chart-ring" :style="{ background: modelView.style }"></div>
|
||||||
<div class="dh-models__list">
|
<div class="dh-models__list">
|
||||||
<div v-for="m in modelView.rows" :key="m.name" class="dh-model">
|
<div v-for="m in modelView.rows" :key="m.name" class="dh-model">
|
||||||
<span class="dh-swatch" :style="{ background: m.color }"></span>
|
<span class="dh-swatch" :style="{ background: m.color }"></span>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { TroubleshootPanel } from "../components/TroubleshootPanel.js"
|
|||||||
import { GalaxyTabs } from "../components/GalaxyTabs.js"
|
import { GalaxyTabs } from "../components/GalaxyTabs.js"
|
||||||
|
|
||||||
const TABS = {
|
const TABS = {
|
||||||
troubleshoot: "Troubleshoot",
|
|
||||||
errors: "Error Logs",
|
errors: "Error Logs",
|
||||||
tmux: "Tmux Live Log",
|
tmux: "Tmux Live Log",
|
||||||
|
troubleshoot: "Troubleshoot",
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLogDate(filename) {
|
function parseLogDate(filename) {
|
||||||
@@ -35,7 +35,7 @@ export const Logs = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
return useTabRouting("/logs", { troubleshoot: "troubleshoot", errors: "errors", tmux: "tmux" })
|
return useTabRouting("/logs", { errors: "errors", tmux: "tmux", troubleshoot: "troubleshoot" })
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.stream = useLogStream({ endpoint: "/api/tmux_log/live", snapshotFn: () => api.tmuxSnapshot(), interval: 2000 })
|
this.stream = useLogStream({ endpoint: "/api/tmux_log/live", snapshotFn: () => api.tmuxSnapshot(), interval: 2000 })
|
||||||
|
|||||||
@@ -14,17 +14,13 @@ export const ModelLaboratory = {
|
|||||||
isOnroad: false,
|
isOnroad: false,
|
||||||
configuration: { enabled: false, lateralModel: "", longitudinalModel: "" },
|
configuration: { enabled: false, lateralModel: "", longitudinalModel: "" },
|
||||||
runtime: {},
|
runtime: {},
|
||||||
download: {},
|
|
||||||
summary: {},
|
summary: {},
|
||||||
models: [],
|
models: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
availableModels() {
|
|
||||||
return this.models.filter((m) => m && m.modelLabArtifactAvailable)
|
|
||||||
},
|
|
||||||
readyModels() {
|
readyModels() {
|
||||||
return this.availableModels.filter((m) => m.modelLabArtifactInstalled)
|
return this.models.filter((m) => m && m.modelLabArtifactAvailable)
|
||||||
},
|
},
|
||||||
candidates() {
|
candidates() {
|
||||||
const ready = this.readyModels
|
const ready = this.readyModels
|
||||||
@@ -32,16 +28,18 @@ export const ModelLaboratory = {
|
|||||||
return ready.filter((m) => !lat || m.value !== lat)
|
return ready.filter((m) => !lat || m.value !== lat)
|
||||||
},
|
},
|
||||||
selectionError() {
|
selectionError() {
|
||||||
|
if (!this.chestnutReady) return "Connect a firmware-ready Chestnut first."
|
||||||
if (this.isOnroad) return "Park before changing the laboratory pair."
|
if (this.isOnroad) return "Park before changing the laboratory pair."
|
||||||
if (this.readyModels.length < 2) return "Download at least two eGPU variants before composing a pair."
|
|
||||||
const lat = this.modelById(this.configuration.lateralModel)
|
const lat = this.modelById(this.configuration.lateralModel)
|
||||||
const lon = this.modelById(this.configuration.longitudinalModel)
|
const lon = this.modelById(this.configuration.longitudinalModel)
|
||||||
if (!lat || !lon) return "Choose two downloaded eGPU variants."
|
if (!lat || !lon) return "Choose two small models with published Chestnut artifacts."
|
||||||
if (lat.value === lon.value) return "Lateral and longitudinal models must be different."
|
if (lat.value === lon.value) return "Lateral and longitudinal models must be different."
|
||||||
|
if (!lat.modelLabArtifactAvailable || !lon.modelLabArtifactAvailable) {
|
||||||
|
return "Both models need a precompiled AMD artifact in the manifest."
|
||||||
|
}
|
||||||
if (!lat.modelLabArtifactInstalled || !lon.modelLabArtifactInstalled) {
|
if (!lat.modelLabArtifactInstalled || !lon.modelLabArtifactInstalled) {
|
||||||
return "Download both eGPU variants first."
|
return "Prepare both precompiled AMD artifacts first."
|
||||||
}
|
}
|
||||||
if (!this.chestnutReady) return "Connect a firmware-ready Chestnut to enable this pair."
|
|
||||||
return ""
|
return ""
|
||||||
},
|
},
|
||||||
runtimeState() {
|
runtimeState() {
|
||||||
@@ -50,7 +48,7 @@ export const ModelLaboratory = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.poll = usePolling(() => this.refresh(), { interval: 2000 })
|
this.poll = usePolling(() => this.refresh(), { interval: 5000 })
|
||||||
this.poll.start()
|
this.poll.start()
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
@@ -64,8 +62,9 @@ export const ModelLaboratory = {
|
|||||||
return this.modelById(id)?.label || id || "not selected"
|
return this.modelById(id)?.label || id || "not selected"
|
||||||
},
|
},
|
||||||
artifactStatus(m) {
|
artifactStatus(m) {
|
||||||
if (m.modelLabArtifactInstalled) return { text: "eGPU variant downloaded", good: true }
|
if (m.modelLabArtifactInstalled) return { text: "AMD ready", good: true }
|
||||||
return { text: "eGPU variant not downloaded", good: false }
|
if (m.modelLabArtifactAvailable) return { text: "AMD download needed", good: false }
|
||||||
|
return { text: "AMD not published", good: false }
|
||||||
},
|
},
|
||||||
async refresh() {
|
async refresh() {
|
||||||
try {
|
try {
|
||||||
@@ -83,7 +82,6 @@ export const ModelLaboratory = {
|
|||||||
this.isOnroad = Boolean(payload.isOnroad)
|
this.isOnroad = Boolean(payload.isOnroad)
|
||||||
this.error = String(payload.configurationError || "")
|
this.error = String(payload.configurationError || "")
|
||||||
this.runtime = payload.runtime && typeof payload.runtime === "object" ? payload.runtime : {}
|
this.runtime = payload.runtime && typeof payload.runtime === "object" ? payload.runtime : {}
|
||||||
this.download = payload.download && typeof payload.download === "object" ? payload.download : {}
|
|
||||||
this.summary = payload.summary && typeof payload.summary === "object" ? payload.summary : {}
|
this.summary = payload.summary && typeof payload.summary === "object" ? payload.summary : {}
|
||||||
this.models = Array.isArray(payload.models) ? payload.models : []
|
this.models = Array.isArray(payload.models) ? payload.models : []
|
||||||
const cfg = payload.configuration && typeof payload.configuration === "object" ? payload.configuration : {}
|
const cfg = payload.configuration && typeof payload.configuration === "object" ? payload.configuration : {}
|
||||||
@@ -97,10 +95,10 @@ export const ModelLaboratory = {
|
|||||||
},
|
},
|
||||||
normalizeSelection() {
|
normalizeSelection() {
|
||||||
const ready = this.readyModels
|
const ready = this.readyModels
|
||||||
if (!ready.some((m) => m.value === this.configuration.lateralModel)) {
|
if (!this.modelById(this.configuration.lateralModel) && ready.length) {
|
||||||
this.configuration.lateralModel = ready[0]?.value || ""
|
this.configuration.lateralModel = ready[0].value
|
||||||
}
|
}
|
||||||
if (!ready.some((m) => m.value === this.configuration.longitudinalModel)) {
|
if (!this.modelById(this.configuration.longitudinalModel) && ready.length > 1) {
|
||||||
const lon = ready.find((m) => m.value !== this.configuration.lateralModel)
|
const lon = ready.find((m) => m.value !== this.configuration.lateralModel)
|
||||||
this.configuration.longitudinalModel = lon?.value || ""
|
this.configuration.longitudinalModel = lon?.value || ""
|
||||||
}
|
}
|
||||||
@@ -145,8 +143,8 @@ export const ModelLaboratory = {
|
|||||||
this.message = ""
|
this.message = ""
|
||||||
try {
|
try {
|
||||||
const payload = await api.prepareModelLabArtifact(modelId)
|
const payload = await api.prepareModelLabArtifact(modelId)
|
||||||
this.message = String(payload?.message || "eGPU variant download queued.")
|
this.message = String(payload?.message || "Chestnut artifact download queued.")
|
||||||
showSnackbar("eGPU variant download queued", "info")
|
showSnackbar("Chestnut artifact download queued", "info")
|
||||||
await this.refresh()
|
await this.refresh()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.error = e?.message || String(e)
|
this.error = e?.message || String(e)
|
||||||
@@ -154,25 +152,6 @@ export const ModelLaboratory = {
|
|||||||
this.saving = false
|
this.saving = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async deleteModel(modelId) {
|
|
||||||
if (this.saving || !modelId) return
|
|
||||||
const model = this.modelById(modelId)
|
|
||||||
if (!window.confirm(`Delete the eGPU variant for "${model?.label || modelId}"? The normal on-device model will not be removed.`)) return
|
|
||||||
this.saving = true
|
|
||||||
this.error = ""
|
|
||||||
this.message = ""
|
|
||||||
try {
|
|
||||||
const payload = await api.deleteModelLabArtifact(modelId)
|
|
||||||
this.dirty = false
|
|
||||||
this.applyPayload(payload)
|
|
||||||
this.message = String(payload?.message || "eGPU variant deleted.")
|
|
||||||
showSnackbar("eGPU variant deleted", "info")
|
|
||||||
} catch (e) {
|
|
||||||
this.error = e?.message || String(e)
|
|
||||||
} finally {
|
|
||||||
this.saving = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div class="gx-view">
|
<div class="gx-view">
|
||||||
@@ -198,41 +177,12 @@ export const ModelLaboratory = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gx-card">
|
|
||||||
<div class="gx-section__header">
|
|
||||||
<i class="bi bi-cpu"></i>
|
|
||||||
<span class="gx-section__title">Available models</span>
|
|
||||||
<span class="gx-section__count">{{ summary.ready || 0 }} downloaded · {{ Math.max((summary.published || 0) - (summary.ready || 0), 0) }} available to download</span>
|
|
||||||
</div>
|
|
||||||
<div style="padding: 0 var(--sp-4) var(--sp-3); color:var(--text-muted); font-size:var(--fs-sm);">
|
|
||||||
Download eGPU-compatible small models. These are separate from the small models in Model Manager because they are compiled for the eGPU.
|
|
||||||
</div>
|
|
||||||
<article v-for="m in availableModels" :key="m.value" class="gx-row">
|
|
||||||
<div class="gx-row__info">
|
|
||||||
<span class="gx-row__label">{{ m.label }}</span>
|
|
||||||
<span class="gx-row__desc">{{ m.value }} · {{ m.series || 'Unknown series' }}</span>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex; gap:6px; flex-wrap:wrap; align-items:center;">
|
|
||||||
<span class="gx-chip">{{ m.version || 'unknown version' }}</span>
|
|
||||||
<span class="gx-chip">{{ m.modelSize || 'small' }}</span>
|
|
||||||
<span class="gx-chip" :style="artifactStatus(m).good ? 'color:var(--success);' : 'color:var(--warning);'">{{ artifactStatus(m).text }}</span>
|
|
||||||
<button v-if="!m.modelLabArtifactInstalled" type="button" class="gx-btn gx-btn--tonal" :disabled="saving || isOnroad || !!download.model" @click="prepareModel(m.value)">
|
|
||||||
{{ download.model === m.value ? 'Downloading · ' + (download.progress || 'starting…') : 'Download eGPU variant' }}
|
|
||||||
</button>
|
|
||||||
<button v-else type="button" class="gx-btn gx-btn--tonal" style="color:var(--error);" :disabled="saving || isOnroad || !!download.model" @click="deleteModel(m.value)">
|
|
||||||
Delete eGPU variant
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gx-card">
|
<div class="gx-card">
|
||||||
<div class="gx-section__header">
|
<div class="gx-section__header">
|
||||||
<i class="bi bi-collection"></i>
|
<i class="bi bi-collection"></i>
|
||||||
<span class="gx-section__title">Compose a pair</span>
|
<span class="gx-section__title">Compose a pair</span>
|
||||||
<span class="gx-chip" :style="configuration.enabled ? 'background:var(--success);color:var(--on-secondary);' : ''">{{ configuration.enabled ? 'Enabled' : 'Disabled' }}</span>
|
<span class="gx-chip" :style="configuration.enabled ? 'background:var(--success);color:var(--on-secondary);' : ''">{{ configuration.enabled ? 'Enabled' : 'Disabled' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 0 var(--sp-4); color:var(--text-muted); font-size:var(--fs-sm);">Choose from downloaded eGPU variant combinations below.</div>
|
|
||||||
<div style="padding: var(--sp-4); display:grid; gap:var(--sp-3);">
|
<div style="padding: var(--sp-4); display:grid; gap:var(--sp-3);">
|
||||||
<label style="display:grid; gap:4px;">
|
<label style="display:grid; gap:4px;">
|
||||||
<strong style="font-size:var(--fs-sm);">Lateral model</strong>
|
<strong style="font-size:var(--fs-sm);">Lateral model</strong>
|
||||||
@@ -284,6 +234,30 @@ export const ModelLaboratory = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="gx-card">
|
||||||
|
<div class="gx-section__header">
|
||||||
|
<i class="bi bi-cpu"></i>
|
||||||
|
<span class="gx-section__title">Available models</span>
|
||||||
|
<span class="gx-section__count">{{ summary.ready || 0 }} ready to pair · {{ Math.max((summary.published || 0) - (summary.ready || 0), 0) }} available to download</span>
|
||||||
|
</div>
|
||||||
|
<article v-for="m in readyModels" :key="m.value" class="gx-row">
|
||||||
|
<div class="gx-row__info">
|
||||||
|
<span class="gx-row__label">{{ m.label }}</span>
|
||||||
|
<span class="gx-row__desc">{{ m.value }} · {{ m.series || 'Unknown series' }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; gap:6px; flex-wrap:wrap; align-items:center;">
|
||||||
|
<span class="gx-chip">{{ m.version || 'unknown version' }}</span>
|
||||||
|
<span class="gx-chip">{{ m.modelSize || 'small' }}</span>
|
||||||
|
<span class="gx-chip" :style="artifactStatus(m).good ? 'color:var(--success);' : 'color:var(--warning);'">{{ artifactStatus(m).text }}</span>
|
||||||
|
<button v-if="m.modelLabArtifactAvailable && !m.modelLabArtifactInstalled" type="button" class="gx-btn gx-btn--tonal" :disabled="saving || isOnroad" @click="prepareModel(m.value)">
|
||||||
|
Prepare for Chestnut
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<div style="padding: var(--sp-3);">
|
||||||
|
<p class="gx-row__desc" style="margin:0;">Model Manager downloads the manifest's precompiled AMD variants. Nothing is compiled on the comma. A normal installed model may still need its separate Chestnut artifact.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -52,13 +52,6 @@ function normalizeRoute(r) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function localDeviceUrl(ip) {
|
|
||||||
const raw = String(ip || "").trim()
|
|
||||||
if (!raw || raw === "unknown") return ""
|
|
||||||
const host = raw.includes(":") && !raw.startsWith("[") ? `[${raw}]` : raw
|
|
||||||
return `http://${host}:8082`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Recordings = {
|
export const Recordings = {
|
||||||
name: "Recordings",
|
name: "Recordings",
|
||||||
components: { GalaxyTabs, GxNotice },
|
components: { GalaxyTabs, GxNotice },
|
||||||
@@ -82,7 +75,6 @@ export const Recordings = {
|
|||||||
logsRoute: null,
|
logsRoute: null,
|
||||||
logsData: null,
|
logsData: null,
|
||||||
onFirestar: isFirestarOrigin(),
|
onFirestar: isFirestarOrigin(),
|
||||||
localUrl: "",
|
|
||||||
// Screen recordings subtab
|
// Screen recordings subtab
|
||||||
screenLoading: false,
|
screenLoading: false,
|
||||||
screenError: "",
|
screenError: "",
|
||||||
@@ -343,14 +335,7 @@ export const Recordings = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
if (this.onFirestar) {
|
if (!this.onFirestar) await this.loadRoutes()
|
||||||
try {
|
|
||||||
const status = await api.getDeviceStatus()
|
|
||||||
this.localUrl = localDeviceUrl(status?.lanIp)
|
|
||||||
} catch (e) {}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await this.loadRoutes()
|
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.controller?.abort()
|
this.controller?.abort()
|
||||||
@@ -518,11 +503,8 @@ export const Recordings = {
|
|||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<GxNotice v-else tone="info" icon="bi-satellite" title="Recordings unavailable via Galaxy">
|
<GxNotice v-else tone="info" icon="bi-satellite" title="Recordings Unavailable via Galaxy"
|
||||||
Recordings are unavailable via Galaxy for bandwidth reasons. If you are on the same local network, connect here:
|
text="Loading recordings requires a direct connection. Connect to your device's local network to use this feature." />
|
||||||
<a v-if="localUrl" :href="localUrl" style="color:inherit; font-weight:var(--fw-bold); overflow-wrap:anywhere;">{{ localUrl }}</a>
|
|
||||||
<span v-else>your device's local IP on port 8082.</span>
|
|
||||||
</GxNotice>
|
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { api, showSnackbar } from "../api.js"
|
|||||||
import { navigate, store } from "../store.js"
|
import { navigate, store } from "../store.js"
|
||||||
import {
|
import {
|
||||||
applyParamChange, countAdvancedHiddenByDeveloperMode, GALAXY_DEVELOPER_MODE_KEY, isSettingVisible,
|
applyParamChange, countAdvancedHiddenByDeveloperMode, GALAXY_DEVELOPER_MODE_KEY, isSettingVisible,
|
||||||
resolveVehicleUnitParam, slugifySectionName,
|
slugifySectionName,
|
||||||
} from "../params.js"
|
} from "../params.js"
|
||||||
import { SettingTree } from "../components/SettingTree.js"
|
import { SettingTree } from "../components/SettingTree.js"
|
||||||
import { GalaxyToggleCard } from "../components/GalaxyToggleCard.js"
|
import { GalaxyToggleCard } from "../components/GalaxyToggleCard.js"
|
||||||
@@ -82,9 +82,7 @@ export const Settings = {
|
|||||||
matchesFilter(p) {
|
matchesFilter(p) {
|
||||||
if (!this.searchTerm) return true
|
if (!this.searchTerm) return true
|
||||||
const q = this.searchTerm.toLowerCase()
|
const q = this.searchTerm.toLowerCase()
|
||||||
const displayParam = resolveVehicleUnitParam(p, this.values)
|
return [p.label, p.key, p.description].some((v) => String(v || "").toLowerCase().includes(q))
|
||||||
return [displayParam.label, displayParam.key, displayParam.description, displayParam.unit, displayParam.unit_search_terms]
|
|
||||||
.some((v) => String(v || "").toLowerCase().includes(q))
|
|
||||||
},
|
},
|
||||||
selectSection(slug) {
|
selectSection(slug) {
|
||||||
if (slug !== this.activeSectionSlug) navigate("/settings/" + slug)
|
if (slug !== this.activeSectionSlug) navigate("/settings/" + slug)
|
||||||
@@ -134,7 +132,7 @@ export const Settings = {
|
|||||||
<template v-for="section in searchResults" :key="section.slug">
|
<template v-for="section in searchResults" :key="section.slug">
|
||||||
<GalaxySection :title="section.name + ' (' + section.matches.length + ')'" :icon="section.icon || 'bi-search'" :default-open="false">
|
<GalaxySection :title="section.name + ' (' + section.matches.length + ')'" :icon="section.icon || 'bi-search'" :default-open="false">
|
||||||
<template v-for="p in section.matches" :key="p.key">
|
<template v-for="p in section.matches" :key="p.key">
|
||||||
<GalaxyToggleCard :param="p" :value="values[p.key]" :values="values" :locked="lockReason(p) !== ''"
|
<GalaxyToggleCard :param="p" :value="values[p.key]" :locked="lockReason(p) !== ''"
|
||||||
@change="onParamChange" />
|
@change="onParamChange" />
|
||||||
</template>
|
</template>
|
||||||
</GalaxySection>
|
</GalaxySection>
|
||||||
|
|||||||
@@ -30,17 +30,11 @@ export const SystemTools = {
|
|||||||
profileBusy: "",
|
profileBusy: "",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() { this.poll = usePolling(() => this.loadFastStatus(), { interval: 3000 }); this.poll.start() },
|
||||||
this.poll = usePolling(() => this.loadFastStatus(), {
|
|
||||||
interval: 1000,
|
|
||||||
enabled: () => !this.fastStatus || !!this.fastStatus.running,
|
|
||||||
})
|
|
||||||
this.poll.start()
|
|
||||||
},
|
|
||||||
mounted() { this.loadBranches(); this.loadProfiles() },
|
mounted() { this.loadBranches(); this.loadProfiles() },
|
||||||
beforeUnmount() { this.poll?.destroy() },
|
beforeUnmount() { this.poll?.destroy() },
|
||||||
computed: {
|
computed: {
|
||||||
updateAvailable() { return this.checkedForUpdates && !!this.fastStatus?.updateAvailable && !this.fastStatus?.running },
|
updateAvailable() { return !!this.fastStatus?.updateAvailable && !this.fastStatus?.running },
|
||||||
factoryResetStatus() {
|
factoryResetStatus() {
|
||||||
const s = this.fastStatus
|
const s = this.fastStatus
|
||||||
if (!s || String(s?.lastMode || "").trim() !== "factory-reset") return null
|
if (!s || String(s?.lastMode || "").trim() !== "factory-reset") return null
|
||||||
@@ -59,7 +53,6 @@ export const SystemTools = {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
shortCommit,
|
shortCommit,
|
||||||
toPercent,
|
|
||||||
async loadBranches() {
|
async loadBranches() {
|
||||||
try {
|
try {
|
||||||
const data = await api.getUpdateBranches()
|
const data = await api.getUpdateBranches()
|
||||||
@@ -72,15 +65,8 @@ export const SystemTools = {
|
|||||||
this.branchLoading = false
|
this.branchLoading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async loadFastStatus({ throwOnError = false } = {}) {
|
async loadFastStatus() {
|
||||||
try {
|
try { this.fastStatus = await api.getUpdateFastStatus() } catch (e) { this.fastStatus = null }
|
||||||
const status = await api.getUpdateFastStatus()
|
|
||||||
if (!status) throw new Error("Update status unavailable")
|
|
||||||
this.fastStatus = status
|
|
||||||
} catch (e) {
|
|
||||||
this.fastStatus = null
|
|
||||||
if (throwOnError) throw e
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async backupToggles() {
|
async backupToggles() {
|
||||||
try {
|
try {
|
||||||
@@ -174,7 +160,6 @@ export const SystemTools = {
|
|||||||
try {
|
try {
|
||||||
await api.setUpdateBranch(branch)
|
await api.setUpdateBranch(branch)
|
||||||
showSnackbar(`Switching to ${branch}...`)
|
showSnackbar(`Switching to ${branch}...`)
|
||||||
await this.loadFastStatus()
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showSnackbar(e?.message || "Switch failed.", "error")
|
showSnackbar(e?.message || "Switch failed.", "error")
|
||||||
}
|
}
|
||||||
@@ -183,7 +168,7 @@ export const SystemTools = {
|
|||||||
if (this.busy) return
|
if (this.busy) return
|
||||||
this.busy = "check"
|
this.busy = "check"
|
||||||
try {
|
try {
|
||||||
await this.loadFastStatus({ throwOnError: true })
|
await this.loadFastStatus()
|
||||||
this.checkedForUpdates = true
|
this.checkedForUpdates = true
|
||||||
const st = this.fastStatus
|
const st = this.fastStatus
|
||||||
if (st?.running) showSnackbar("An update is already running.")
|
if (st?.running) showSnackbar("An update is already running.")
|
||||||
@@ -242,7 +227,6 @@ export const SystemTools = {
|
|||||||
try {
|
try {
|
||||||
await api.factoryReset()
|
await api.factoryReset()
|
||||||
showSnackbar("SAVE ME initiated — factory resetting...")
|
showSnackbar("SAVE ME initiated — factory resetting...")
|
||||||
await this.loadFastStatus()
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showSnackbar(e?.message || "Factory reset failed.", "error")
|
showSnackbar(e?.message || "Factory reset failed.", "error")
|
||||||
}
|
}
|
||||||
@@ -272,27 +256,14 @@ export const SystemTools = {
|
|||||||
<i class="bi bi-arrow-repeat"></i>
|
<i class="bi bi-arrow-repeat"></i>
|
||||||
<span class="gx-section__title">Update Status</span>
|
<span class="gx-section__title">Update Status</span>
|
||||||
<span v-if="fastStatus.running" class="gx-chip" style="background:var(--primary);color:var(--on-primary);">{{ fastStatus.progressPercent }}%</span>
|
<span v-if="fastStatus.running" class="gx-chip" style="background:var(--primary);color:var(--on-primary);">{{ fastStatus.progressPercent }}%</span>
|
||||||
<span v-else-if="updateAvailable" class="gx-chip" style="background:var(--warning);color:var(--black);">Update available</span>
|
<span v-else-if="fastStatus.updateAvailable" class="gx-chip" style="background:var(--warning);color:var(--black);">Update available</span>
|
||||||
<span v-else-if="checkedForUpdates" class="gx-chip">Up to date</span>
|
<span v-else class="gx-chip">Up to date</span>
|
||||||
<span v-else class="gx-chip">Not checked</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: var(--sp-3); display:grid; gap:6px;">
|
<div style="padding: var(--sp-3); display:grid; gap:6px;">
|
||||||
<div class="gx-row" style="border-top:none; min-height:0; padding:4px 0;"><span class="gx-row__label">Branch</span><span class="gx-row__value">{{ fastStatus.branch || currentBranch || '—' }}</span></div>
|
<div class="gx-row" style="border-top:none; min-height:0; padding:4px 0;"><span class="gx-row__label">Branch</span><span class="gx-row__value">{{ fastStatus.branch || currentBranch || '—' }}</span></div>
|
||||||
<div v-if="fastStatus.running" class="gx-row" style="border-top:none; min-height:0; padding:4px 0;"><span class="gx-row__label">Stage</span><span class="gx-row__value">{{ fastStatus.stage }} · {{ fastStatus.progressLabel }}</span></div>
|
<div v-if="fastStatus.running" class="gx-row" style="border-top:none; min-height:0; padding:4px 0;"><span class="gx-row__label">Stage</span><span class="gx-row__value">{{ fastStatus.stage }} · {{ fastStatus.progressLabel }}</span></div>
|
||||||
<div class="gx-row" style="border-top:none; min-height:0; padding:4px 0;"><span class="gx-row__label">Local</span><span class="gx-row__value" style="font-family:monospace;">{{ shortCommit(fastStatus.localCommit) }}</span></div>
|
<div class="gx-row" style="border-top:none; min-height:0; padding:4px 0;"><span class="gx-row__label">Local</span><span class="gx-row__value" style="font-family:monospace;">{{ shortCommit(fastStatus.localCommit) }}</span></div>
|
||||||
<div class="gx-row" style="border-top:none; min-height:0; padding:4px 0;"><span class="gx-row__label">Remote</span><span class="gx-row__value" style="font-family:monospace;">{{ shortCommit(fastStatus.remoteCommit) }}</span></div>
|
<div class="gx-row" style="border-top:none; min-height:0; padding:4px 0;"><span class="gx-row__label">Remote</span><span class="gx-row__value" style="font-family:monospace;">{{ shortCommit(fastStatus.remoteCommit) }}</span></div>
|
||||||
<div v-if="fastStatus.running" class="gx-update-progress" role="progressbar" aria-label="Update progress"
|
|
||||||
:aria-valuenow="Math.round(fastStatus.progressPercent || 0)" aria-valuemin="0" aria-valuemax="100">
|
|
||||||
<div class="gx-update-progress__track">
|
|
||||||
<div class="gx-update-progress__fill" :class="{ 'gx-update-progress__fill--error': fastStatus.stage === 'error' }"
|
|
||||||
:style="{ width: toPercent(fastStatus.progressPercent) + '%' }"></div>
|
|
||||||
</div>
|
|
||||||
<div class="gx-update-progress__meta">
|
|
||||||
<span>Step {{ fastStatus.progressStep || 0 }}/{{ fastStatus.progressTotalSteps || 5 }}: {{ fastStatus.progressLabel || fastStatus.stage || 'Updating' }}</span>
|
|
||||||
<strong>{{ Math.round(toPercent(fastStatus.progressPercent)) }}%</strong>
|
|
||||||
</div>
|
|
||||||
<small v-if="fastStatus.progressDetail">{{ fastStatus.progressDetail }}</small>
|
|
||||||
</div>
|
|
||||||
<div v-if="fastStatus.message" class="gx-note">{{ fastStatus.message }}</div>
|
<div v-if="fastStatus.message" class="gx-note">{{ fastStatus.message }}</div>
|
||||||
<div v-if="fastStatus.warning && (fastStatus.running || fastStatus.updateAvailable)" class="gx-note gx-note--danger">{{ fastStatus.warning }}</div>
|
<div v-if="fastStatus.warning && (fastStatus.running || fastStatus.updateAvailable)" class="gx-note gx-note--danger">{{ fastStatus.warning }}</div>
|
||||||
<div v-if="fastStatus.agnosUpdate?.available && fastStatus.agnosUpdate?.warnings?.length" style="margin-top:4px;">
|
<div v-if="fastStatus.agnosUpdate?.available && fastStatus.agnosUpdate?.warnings?.length" style="margin-top:4px;">
|
||||||
@@ -316,7 +287,7 @@ export const SystemTools = {
|
|||||||
<i v-if="busy === 'check'" class="bi bi-arrow-repeat gx-spin"></i>
|
<i v-if="busy === 'check'" class="bi bi-arrow-repeat gx-spin"></i>
|
||||||
<i v-else class="bi bi-search"></i> {{ busy === 'check' ? 'Checking...' : 'Check for Updates' }}
|
<i v-else class="bi bi-search"></i> {{ busy === 'check' ? 'Checking...' : 'Check for Updates' }}
|
||||||
</button>
|
</button>
|
||||||
<button v-if="updateAvailable" type="button" class="gx-btn" :disabled="!!busy || isOnroad" @click="applyFastUpdate">
|
<button type="button" class="gx-btn" :disabled="!updateAvailable || !!busy || isOnroad" @click="applyFastUpdate">
|
||||||
<i class="bi bi-arrow-up-circle"></i> {{ busy === 'fast' ? 'Updating...' : 'Update Now' }}
|
<i class="bi bi-arrow-up-circle"></i> {{ busy === 'fast' ? 'Updating...' : 'Update Now' }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="gx-btn gx-btn--tonal" :disabled="!!busy || isOnroad" @click="runUpdate('recover')">Recover</button>
|
<button type="button" class="gx-btn gx-btn--tonal" :disabled="!!busy || isOnroad" @click="runUpdate('recover')">Recover</button>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { navigate, toolHref } from "../store.js"
|
import { navigate, toolHref } from "../store.js"
|
||||||
|
|
||||||
const TOOLS = [
|
const TOOLS = [
|
||||||
{ name: "Bluetooth", link: "/bluetooth", icon: "bi-bluetooth", desc: "Pair devices, controllers, & audio" },
|
{ name: "Cameras & Monitoring", link: "/cameras", icon: "bi-camera-video", desc: "PiP side camera & V-ASM spot monitor" },
|
||||||
{ name: "Cameras & Monitoring", link: "/cameras", icon: "bi-camera-video", desc: "Sentry, PiP side camera, & V-ASM spot monitor" },
|
|
||||||
{ name: "Galaxy & App Install", link: "/galaxy", icon: "bi-globe2", desc: "Remote access, pairing, & app install" },
|
{ name: "Galaxy & App Install", link: "/galaxy", icon: "bi-globe2", desc: "Remote access, pairing, & app install" },
|
||||||
{ name: "Logs & Diagnostics", link: "/logs", icon: "bi-exclamation-triangle", desc: "Error logs, tmux, troubleshoot" },
|
{ name: "Logs & Diagnostics", link: "/logs", icon: "bi-exclamation-triangle", desc: "Error logs, tmux, troubleshoot" },
|
||||||
{ name: "Model Manager", link: "/manage_models", icon: "bi-cpu", desc: "Install/swap models" },
|
{ name: "Model Manager", link: "/manage_models", icon: "bi-cpu", desc: "Install/swap models" },
|
||||||
{ name: "Model Laboratory", link: "/model_laboratory", icon: "bi-bezier2", desc: "Pair lateral and longitudinal models" },
|
{ name: "Model Laboratory", link: "/model_laboratory", icon: "bi-bezier2", desc: "Pair lateral and longitudinal models" },
|
||||||
{ name: "Navigation & Maps", link: "/navigation", icon: "bi-map", desc: "Offline maps & destinations" },
|
{ name: "Navigation & Maps", link: "/navigation", icon: "bi-map", desc: "Offline maps & destinations" },
|
||||||
|
{ name: "Sentry Mode", link: "/sentry", icon: "bi-shield-exclamation", desc: "Sentry alerts & security" },
|
||||||
{ name: "System Tools", link: "/system", icon: "bi-arrow-repeat", desc: "Backup, restore, updates" },
|
{ name: "System Tools", link: "/system", icon: "bi-arrow-repeat", desc: "Backup, restore, updates" },
|
||||||
{ name: "Theme Maker", link: "/theme_maker", icon: "bi-palette-fill", desc: "Customize the look" },
|
{ name: "Theme Maker", link: "/theme_maker", icon: "bi-palette-fill", desc: "Customize the look" },
|
||||||
{ name: "Tuning, Plots & Testing", link: "/tuning", icon: "bi-sign-turn-right", desc: "Steering & speed tuning, live plots, testing grounds" },
|
{ name: "Tuning, Plots & Testing", link: "/tuning", icon: "bi-sign-turn-right", desc: "Steering & speed tuning, live plots, testing grounds" },
|
||||||
{ name: "Vehicle Controls", link: "/vehicle", icon: "bi-car-front", desc: "Controllers & vehicle features" },
|
{ name: "Vehicle Controls", link: "/vehicle", icon: "bi-car-front", desc: "Controllers, bluetooth, vehicle features" },
|
||||||
].sort((a, b) => a.name.localeCompare(b.name))
|
].sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
|
||||||
export const Tools = {
|
export const Tools = {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { LateralTuningPanel } from "../components/LateralTuningPanel.js"
|
import { LateralTuningPanel } from "../components/LateralTuningPanel.js"
|
||||||
|
import { LongitudinalManeuvers } from "../components/LongitudinalManeuvers.js"
|
||||||
import { GalaxyTabs } from "../components/GalaxyTabs.js"
|
import { GalaxyTabs } from "../components/GalaxyTabs.js"
|
||||||
import { Plots } from "./Plots.js"
|
import { Plots } from "./Plots.js"
|
||||||
import { TestingGround } from "./TestingGround.js"
|
import { TestingGround } from "./TestingGround.js"
|
||||||
@@ -6,15 +7,16 @@ import { useTabRouting } from "../composables.js"
|
|||||||
|
|
||||||
const TABS = {
|
const TABS = {
|
||||||
lateral: "Lateral Tuning",
|
lateral: "Lateral Tuning",
|
||||||
|
long: "Long Maneuvers",
|
||||||
plots: "Plots",
|
plots: "Plots",
|
||||||
testing: "Testing Ground",
|
testing: "Testing Ground",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tuning = {
|
export const Tuning = {
|
||||||
name: "Tuning",
|
name: "Tuning",
|
||||||
components: { LateralTuningPanel, Plots, TestingGround, GalaxyTabs },
|
components: { LateralTuningPanel, LongitudinalManeuvers, Plots, TestingGround, GalaxyTabs },
|
||||||
setup() {
|
setup() {
|
||||||
return useTabRouting("/tuning", { lateral: "lateral", plots: "plots", testing: "testing" })
|
return useTabRouting("/tuning", { lateral: "lateral", long: "long", plots: "plots", testing: "testing" })
|
||||||
},
|
},
|
||||||
data() { return { TABS } },
|
data() { return { TABS } },
|
||||||
template: `
|
template: `
|
||||||
@@ -27,6 +29,10 @@ export const Tuning = {
|
|||||||
<LateralTuningPanel />
|
<LateralTuningPanel />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="tab === 'long'">
|
||||||
|
<LongitudinalManeuvers />
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else-if="tab === 'plots'">
|
<template v-else-if="tab === 'plots'">
|
||||||
<Plots :embedded="true" />
|
<Plots :embedded="true" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { api, showSnackbar } from "../api.js"
|
import { api, showSnackbar } from "../api.js"
|
||||||
import { navigate, toolHref } from "../store.js"
|
import { navigate, toolHref } from "../store.js"
|
||||||
import { WheelControls } from "../components/WheelControls.js"
|
import { WheelControls } from "../components/WheelControls.js"
|
||||||
|
import { BluetoothPanel } from "../components/BluetoothPanel.js"
|
||||||
import { GalaxySection } from "../components/GalaxySection.js"
|
import { GalaxySection } from "../components/GalaxySection.js"
|
||||||
import { GalaxyTabs } from "../components/GalaxyTabs.js"
|
import { GalaxyTabs } from "../components/GalaxyTabs.js"
|
||||||
import { useTabRouting } from "../composables.js"
|
import { useTabRouting } from "../composables.js"
|
||||||
@@ -12,12 +13,13 @@ const FEATURES = [
|
|||||||
|
|
||||||
const TABS = {
|
const TABS = {
|
||||||
controllers: "Controllers",
|
controllers: "Controllers",
|
||||||
|
bluetooth: "Bluetooth",
|
||||||
features: "Vehicle Features",
|
features: "Vehicle Features",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Vehicle = {
|
export const Vehicle = {
|
||||||
name: "Vehicle",
|
name: "Vehicle",
|
||||||
components: { WheelControls, GalaxySection, GalaxyTabs },
|
components: { WheelControls, BluetoothPanel, GalaxySection, GalaxyTabs },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
TABS,
|
TABS,
|
||||||
@@ -27,7 +29,7 @@ export const Vehicle = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
return useTabRouting("/vehicle", { controllers: "controllers", features: "features" })
|
return useTabRouting("/vehicle", { controllers: "controllers", bluetooth: "bluetooth", features: "features" })
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
featureList() { return this.features },
|
featureList() { return this.features },
|
||||||
@@ -69,6 +71,10 @@ export const Vehicle = {
|
|||||||
<WheelControls />
|
<WheelControls />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="tab === 'bluetooth'">
|
||||||
|
<BluetoothPanel />
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<GalaxySection title="Vehicle Features" icon="bi-check2-square">
|
<GalaxySection title="Vehicle Features" icon="bi-check2-square">
|
||||||
<div style="padding: var(--sp-3); display:grid; gap:8px;">
|
<div style="padding: var(--sp-3); display:grid; gap:8px;">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Galaxy",
|
"name": "Big Dipper",
|
||||||
"short_name": "Galaxy",
|
"short_name": "Big Dipper",
|
||||||
"description": "Control and configure your openpilot device from anywhere.",
|
"description": "Control and configure your openpilot device from anywhere.",
|
||||||
"icons": [
|
"icons": [
|
||||||
{ "src": "/assets/images/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
|
{ "src": "/assets/images/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
{ "src": "/assets/images/apple-touch-icon.png", "sizes": "180x180", "type": "image/png" }
|
{ "src": "/assets/images/apple-touch-icon.png", "sizes": "180x180", "type": "image/png" }
|
||||||
],
|
],
|
||||||
"id": "46bf2df73deba8e1512c35de",
|
"id": "46bf2df73deba8e1512c35de",
|
||||||
|
"start_url": "/mobile/",
|
||||||
"scope": "/",
|
"scope": "/",
|
||||||
"background_color": "#06060f",
|
"background_color": "#06060f",
|
||||||
"theme_color": "#8b6cc5",
|
"theme_color": "#8b6cc5",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<link rel="stylesheet" href="/assets/components/tools/error_logs.css">
|
<link rel="stylesheet" href="/assets/components/tools/error_logs.css">
|
||||||
<link rel="stylesheet" href="/assets/components/tools/maps.css">
|
<link rel="stylesheet" href="/assets/components/tools/maps.css">
|
||||||
<link rel="stylesheet" href="/assets/components/tools/model_manager.css">
|
<link rel="stylesheet" href="/assets/components/tools/model_manager.css">
|
||||||
<link rel="stylesheet" href="/assets/components/tools/model_laboratory.css?v=model-lab-5">
|
<link rel="stylesheet" href="/assets/components/tools/model_laboratory.css?v=model-lab-4">
|
||||||
<link rel="stylesheet" href="/assets/components/tools/plots.css">
|
<link rel="stylesheet" href="/assets/components/tools/plots.css">
|
||||||
<link rel="stylesheet" href="/assets/components/tools/speed_limits.css">
|
<link rel="stylesheet" href="/assets/components/tools/speed_limits.css">
|
||||||
<link rel="stylesheet" href="/assets/components/tools/theme_maker.css">
|
<link rel="stylesheet" href="/assets/components/tools/theme_maker.css">
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
<link rel="stylesheet" href="/assets/components/tools/update_manager.css">
|
<link rel="stylesheet" href="/assets/components/tools/update_manager.css">
|
||||||
<link rel="stylesheet" href="/assets/components/tools/device_settings.css?v=favorite-actions-1">
|
<link rel="stylesheet" href="/assets/components/tools/device_settings.css?v=favorite-actions-1">
|
||||||
<link rel="stylesheet" href="/assets/components/tools/bluetooth.css?v=bluetooth-6">
|
<link rel="stylesheet" href="/assets/components/tools/bluetooth.css?v=bluetooth-6">
|
||||||
<link rel="stylesheet" href="/assets/components/tools/wheel_controls.css?v=controllers-3">
|
<link rel="stylesheet" href="/assets/components/tools/wheel_controls.css?v=controllers-2">
|
||||||
<link rel="stylesheet" href="/assets/components/tools/galaxy.css">
|
<link rel="stylesheet" href="/assets/components/tools/galaxy.css">
|
||||||
<link rel="stylesheet" href="/assets/components/tools/sentry.css">
|
<link rel="stylesheet" href="/assets/components/tools/sentry.css">
|
||||||
<link rel="stylesheet" href="/assets/components/tools/longitudinal_maneuvers.css">
|
<link rel="stylesheet" href="/assets/components/tools/longitudinal_maneuvers.css">
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
|
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import("/assets/components/router.js?v=router-cycle-fix-8").catch((err) => {
|
import("/assets/components/router.js?v=router-cycle-fix-7").catch((err) => {
|
||||||
console.error("[the_galaxy] bootstrap failed", err);
|
console.error("[the_galaxy] bootstrap failed", err);
|
||||||
const target = document.getElementById("app") || document.body;
|
const target = document.getElementById("app") || document.body;
|
||||||
const pre = document.createElement("pre");
|
const pre = document.createElement("pre");
|
||||||
|
|||||||
@@ -1859,16 +1859,9 @@ def test_model_profiles_can_be_selected_without_external_gpu(monkeypatch, tmp_pa
|
|||||||
assert status["activeBigModel"] == "big-one"
|
assert status["activeBigModel"] == "big-one"
|
||||||
assert status["activeSmallModel"] == "small-one"
|
assert status["activeSmallModel"] == "small-one"
|
||||||
|
|
||||||
monkeypatch.setattr(server, "external_gpu_available", lambda: True)
|
|
||||||
active_big_response = client.put("/api/models/active", json={"profile": "big", "model": "big-one"})
|
|
||||||
assert active_big_response.status_code == 200
|
|
||||||
assert params.values["Model"] == params.values["DrivingModel"] == "big-one"
|
|
||||||
assert params.values["DrivingModelName"] == "Big One"
|
|
||||||
|
|
||||||
disabled = client.put("/api/models/active", json={"profile": "big", "model": ""})
|
disabled = client.put("/api/models/active", json={"profile": "big", "model": ""})
|
||||||
assert disabled.status_code == 200
|
assert disabled.status_code == 200
|
||||||
assert params.values["ActiveBigModel"] == "none"
|
assert params.values["ActiveBigModel"] == "none"
|
||||||
assert params.values["Model"] == params.values["DrivingModel"] == "small-one"
|
|
||||||
assert disabled.get_json()["model"] == ""
|
assert disabled.get_json()["model"] == ""
|
||||||
assert client.get("/api/models/status").get_json()["activeBigModel"] == ""
|
assert client.get("/api/models/status").get_json()["activeBigModel"] == ""
|
||||||
|
|
||||||
@@ -1906,12 +1899,6 @@ def test_model_laboratory_api_uses_installed_models_and_enforces_hardware_size_v
|
|||||||
"ModelManifestVersion": "v25",
|
"ModelManifestVersion": "v25",
|
||||||
"Model": "rdf43",
|
"Model": "rdf43",
|
||||||
"DrivingModel": "rdf43",
|
"DrivingModel": "rdf43",
|
||||||
"ActiveSmallModel": "rdf43",
|
|
||||||
"ActiveSmallModelName": "Regret Driven Framework V4",
|
|
||||||
"ActiveSmallModelVersion": "v15",
|
|
||||||
"ActiveBigModel": "big",
|
|
||||||
"ActiveBigModelName": "Chestnut One Billion",
|
|
||||||
"ActiveBigModelVersion": "v16",
|
|
||||||
})
|
})
|
||||||
metadata = {
|
metadata = {
|
||||||
"lat": {"model_size": "small", "model_size_declared": True, "model_lab_eligible": True,
|
"lat": {"model_size": "small", "model_size_declared": True, "model_lab_eligible": True,
|
||||||
@@ -1987,16 +1974,10 @@ def test_model_laboratory_api_uses_installed_models_and_enforces_hardware_size_v
|
|||||||
queued = client.post("/api/model-laboratory/download", json={"model": "old"})
|
queued = client.post("/api/model-laboratory/download", json={"model": "old"})
|
||||||
assert queued.status_code == 200
|
assert queued.status_code == 200
|
||||||
assert params_memory.values["ModelLabModelToDownload"] == "old"
|
assert params_memory.values["ModelLabModelToDownload"] == "old"
|
||||||
assert "eGPU variant" in params_memory.values["ModelDownloadProgress"]
|
assert "precompiled AMD" in params_memory.values["ModelDownloadProgress"]
|
||||||
params_memory.remove("ModelLabModelToDownload")
|
params_memory.remove("ModelLabModelToDownload")
|
||||||
|
|
||||||
monkeypatch.setattr(server, "external_gpu_available", lambda: False)
|
monkeypatch.setattr(server, "external_gpu_available", lambda: False)
|
||||||
(tmp_path / "old_driving_chestnut_tinygrad.pkl").unlink(missing_ok=True)
|
|
||||||
queued_without_chestnut = client.post("/api/model-laboratory/download", json={"model": "old"})
|
|
||||||
assert queued_without_chestnut.status_code == 200
|
|
||||||
assert params_memory.values["ModelLabModelToDownload"] == "old"
|
|
||||||
params_memory.remove("ModelLabModelToDownload")
|
|
||||||
|
|
||||||
no_chestnut = client.put("/api/model-laboratory", json={
|
no_chestnut = client.put("/api/model-laboratory", json={
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"lateralModel": "lat",
|
"lateralModel": "lat",
|
||||||
@@ -2005,21 +1986,6 @@ def test_model_laboratory_api_uses_installed_models_and_enforces_hardware_size_v
|
|||||||
assert no_chestnut.status_code == 409
|
assert no_chestnut.status_code == 409
|
||||||
assert "Chestnut" in no_chestnut.get_json()["error"]
|
assert "Chestnut" in no_chestnut.get_json()["error"]
|
||||||
|
|
||||||
monkeypatch.setattr(server, "external_gpu_available", lambda: True)
|
|
||||||
disabled = client.put("/api/model-laboratory", json={
|
|
||||||
"enabled": False,
|
|
||||||
"lateralModel": "lat",
|
|
||||||
"longitudinalModel": "long",
|
|
||||||
})
|
|
||||||
assert disabled.status_code == 200
|
|
||||||
assert params.values["Model"] == params.values["DrivingModel"] == "big"
|
|
||||||
assert params.values["DrivingModelName"] == "Chestnut One Billion"
|
|
||||||
|
|
||||||
deleted = client.delete("/api/model-laboratory/artifact", json={"model": "lat"})
|
|
||||||
assert deleted.status_code == 200
|
|
||||||
assert not (tmp_path / "lat_driving_chestnut_tinygrad.pkl").exists()
|
|
||||||
assert (tmp_path / "lat_driving_tinygrad.pkl").exists()
|
|
||||||
|
|
||||||
params.values["IsOnroad"] = True
|
params.values["IsOnroad"] = True
|
||||||
onroad = client.put("/api/model-laboratory", json={"enabled": False})
|
onroad = client.put("/api/model-laboratory", json={"enabled": False})
|
||||||
assert onroad.status_code == 403
|
assert onroad.status_code == 403
|
||||||
|
|||||||
@@ -46,23 +46,6 @@ def test_device_settings_uses_the_params_api_and_layout_json():
|
|||||||
assert 'fetch("/assets/components/tools/device_settings_layout.json?v=settings-tier-1"' in source
|
assert 'fetch("/assets/components/tools/device_settings_layout.json?v=settings-tier-1"' in source
|
||||||
|
|
||||||
|
|
||||||
def test_device_settings_speed_units_follow_the_vehicle():
|
|
||||||
source = _device_settings()
|
|
||||||
|
|
||||||
assert 'from "/assets/mobile/js/params.js"' in source
|
|
||||||
assert "resolveVehicleUnitParam" in source
|
|
||||||
assert "formatNumericParamValue" in source
|
|
||||||
assert "unit_search_terms" in source
|
|
||||||
assert "per click" in source
|
|
||||||
assert "ds-unit-note" not in source
|
|
||||||
|
|
||||||
|
|
||||||
def test_device_settings_supports_vehicle_make_exclusions():
|
|
||||||
source = _device_settings()
|
|
||||||
|
|
||||||
assert "excluded_vehicle_makes" in source
|
|
||||||
|
|
||||||
|
|
||||||
def test_lane_center_offset_can_step_below_zero():
|
def test_lane_center_offset_can_step_below_zero():
|
||||||
source = _device_settings()
|
source = _device_settings()
|
||||||
|
|
||||||
|
|||||||
@@ -66,15 +66,6 @@ def test_galaxy_layout_contains_basic_mode_controls():
|
|||||||
assert {"AlphaLongitudinalEnabled", "ForceOffroad", "GalaxyDeveloperMode"} <= sections["Developer"].keys()
|
assert {"AlphaLongitudinalEnabled", "ForceOffroad", "GalaxyDeveloperMode"} <= sections["Developer"].keys()
|
||||||
|
|
||||||
|
|
||||||
def test_galaxy_new_ui_is_the_visible_default_choice():
|
|
||||||
galaxy_default = _params_by_section(_layout())["Developer"]["GalaxyMobileDefault"]
|
|
||||||
|
|
||||||
assert _declared_default("GalaxyMobileDefault") == "1"
|
|
||||||
assert galaxy_default["settings_tier"] == "simple"
|
|
||||||
assert galaxy_default["label"] == "Use Galaxy (new) by Default"
|
|
||||||
assert "Galaxy (old)" in galaxy_default["description"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_ford_lateral_controls_are_ford_only_and_galaxy_only():
|
def test_ford_lateral_controls_are_ford_only_and_galaxy_only():
|
||||||
lateral = _params_by_section(_layout())["Lateral (Steering)"]
|
lateral = _params_by_section(_layout())["Lateral (Steering)"]
|
||||||
ford_keys = {
|
ford_keys = {
|
||||||
@@ -116,50 +107,6 @@ def test_device_shutdown_uses_literal_hours():
|
|||||||
assert device_shutdown["step"] == 1
|
assert device_shutdown["step"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_speed_settings_follow_vehicle_units_with_one_unit_steps():
|
|
||||||
sections = _params_by_section(_layout())
|
|
||||||
speed_keys = {
|
|
||||||
"MinimumLaneChangeSpeed", "PauseLateralSpeed",
|
|
||||||
"CESpeed", "CESpeedLead", "CESignalSpeed",
|
|
||||||
"CustomCruise", "CustomCruiseLong", "SetSpeedOffset", "PulseGlideSpeedDelta",
|
|
||||||
"Offset1", "Offset2", "Offset3", "Offset4", "Offset5", "Offset6", "Offset7",
|
|
||||||
"CCMSpeed", "CCMSpeedLead", "CCMSetSpeedMargin",
|
|
||||||
"VisionSpeedLimitLowLimitThreshold", "TurnSteeringLimitMuteSpeed",
|
|
||||||
}
|
|
||||||
params = {
|
|
||||||
param["key"]: param
|
|
||||||
for section in sections.values()
|
|
||||||
for param in section.values()
|
|
||||||
if param["key"] in speed_keys
|
|
||||||
}
|
|
||||||
|
|
||||||
assert params.keys() == speed_keys
|
|
||||||
assert all(param["unit_type"] == "vehicle_speed" for param in params.values())
|
|
||||||
|
|
||||||
one_unit_keys = speed_keys - {"PulseGlideSpeedDelta", "VisionSpeedLimitLowLimitThreshold"}
|
|
||||||
assert all(params[key]["step"] == 1 for key in one_unit_keys)
|
|
||||||
assert params["PulseGlideSpeedDelta"]["step"] == 0.5
|
|
||||||
assert params["VisionSpeedLimitLowLimitThreshold"]["step"] == 5
|
|
||||||
|
|
||||||
for index in range(7):
|
|
||||||
offset = params[f"Offset{index + 1}"]
|
|
||||||
assert offset["unit_range_index"] == index
|
|
||||||
assert (offset["metric_min"], offset["metric_max"]) == (-150, 150)
|
|
||||||
|
|
||||||
assert params["CustomCruise"]["metric_max"] == 150
|
|
||||||
assert params["CCMSetSpeedMargin"]["metric_max"] == 30
|
|
||||||
assert params["PulseGlideSpeedDelta"]["imperial_max"] == 15
|
|
||||||
|
|
||||||
|
|
||||||
def test_cruise_controls_are_split_between_toyota_and_software_cruise():
|
|
||||||
longitudinal = _params_by_section(_layout())["Longitudinal (Speed & Following)"]
|
|
||||||
|
|
||||||
assert longitudinal["CustomCruise"]["excluded_vehicle_makes"] == ["Lexus", "Toyota"]
|
|
||||||
assert longitudinal["CustomCruiseLong"]["excluded_vehicle_makes"] == ["Lexus", "Toyota"]
|
|
||||||
assert longitudinal["ReverseCruise"]["vehicle_makes"] == ["Lexus", "Toyota"]
|
|
||||||
assert _declared_default("ReverseCruise") == "0"
|
|
||||||
|
|
||||||
|
|
||||||
def test_curve_speed_controller_no_lead_toggle_is_nested_under_csc():
|
def test_curve_speed_controller_no_lead_toggle_is_nested_under_csc():
|
||||||
csc_no_lead = _params_by_section(_layout())["Longitudinal (Speed & Following)"]["CurveSpeedControllerNoLead"]
|
csc_no_lead = _params_by_section(_layout())["Longitudinal (Speed & Following)"]["CurveSpeedControllerNoLead"]
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ ROUTER_PATH = REPO_ROOT / "starpilot/system/the_galaxy/assets/components/router.
|
|||||||
INDEX_PATH = REPO_ROOT / "starpilot/system/the_galaxy/templates/index.html"
|
INDEX_PATH = REPO_ROOT / "starpilot/system/the_galaxy/templates/index.html"
|
||||||
BLUETOOTH_PATH = REPO_ROOT / "starpilot/system/the_galaxy/assets/components/tools/bluetooth.js"
|
BLUETOOTH_PATH = REPO_ROOT / "starpilot/system/the_galaxy/assets/components/tools/bluetooth.js"
|
||||||
CONTROLLERS_PATH = REPO_ROOT / "starpilot/system/the_galaxy/assets/components/tools/wheel_controls.js"
|
CONTROLLERS_PATH = REPO_ROOT / "starpilot/system/the_galaxy/assets/components/tools/wheel_controls.js"
|
||||||
MOBILE_CONTROLLERS_PATH = REPO_ROOT / "starpilot/system/the_galaxy/assets/mobile/js/components/WheelControls.js"
|
|
||||||
SIDEBAR_PATH = REPO_ROOT / "starpilot/system/the_galaxy/assets/components/sidebar.js"
|
SIDEBAR_PATH = REPO_ROOT / "starpilot/system/the_galaxy/assets/components/sidebar.js"
|
||||||
MODEL_LAB_PATH = REPO_ROOT / "starpilot/system/the_galaxy/assets/components/tools/model_laboratory.js"
|
MODEL_LAB_PATH = REPO_ROOT / "starpilot/system/the_galaxy/assets/components/tools/model_laboratory.js"
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ def test_router_and_settings_cache_bust_is_consistent():
|
|||||||
index = INDEX_PATH.read_text(encoding="utf-8")
|
index = INDEX_PATH.read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "/assets/components/settings.js?v=router-cycle-fix-5" in router
|
assert "/assets/components/settings.js?v=router-cycle-fix-5" in router
|
||||||
assert "/assets/components/router.js?v=router-cycle-fix-8" in index
|
assert "/assets/components/router.js?v=router-cycle-fix-7" in index
|
||||||
|
|
||||||
|
|
||||||
def test_bluetooth_actions_use_reactive_disabled_bindings():
|
def test_bluetooth_actions_use_reactive_disabled_bindings():
|
||||||
@@ -77,16 +76,6 @@ def test_controller_joystick_mode_requires_explicit_device_selection():
|
|||||||
assert 'request("joystick", { device_id: device.device_id, enabled: !selected() })' in source
|
assert 'request("joystick", { device_id: device.device_id, enabled: !selected() })' in source
|
||||||
|
|
||||||
|
|
||||||
def test_controller_offroad_disconnect_is_opt_in():
|
|
||||||
source = CONTROLLERS_PATH.read_text(encoding="utf-8")
|
|
||||||
mobile_source = MOBILE_CONTROLLERS_PATH.read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
for frontend in (source, mobile_source):
|
|
||||||
assert "Disconnect controllers when offroad" in frontend
|
|
||||||
assert "After two minutes offroad" in frontend
|
|
||||||
assert "offroad-disconnect" in frontend
|
|
||||||
|
|
||||||
|
|
||||||
def test_controller_page_has_ten_controller_only_action_slots():
|
def test_controller_page_has_ten_controller_only_action_slots():
|
||||||
source = CONTROLLERS_PATH.read_text(encoding="utf-8")
|
source = CONTROLLERS_PATH.read_text(encoding="utf-8")
|
||||||
|
|
||||||
@@ -128,13 +117,8 @@ def test_model_laboratory_frontend_exposes_guards_and_role_copy():
|
|||||||
assert 'if (!state.chestnutReady)' in source
|
assert 'if (!state.chestnutReady)' in source
|
||||||
assert 'if (state.isOnroad)' in source
|
assert 'if (state.isOnroad)' in source
|
||||||
assert "model.modelLabArtifactInstalled" in source
|
assert "model.modelLabArtifactInstalled" in source
|
||||||
assert "Download eGPU-compatible small models" in source
|
assert "Nothing is compiled on the comma" in source
|
||||||
assert "Choose from downloaded eGPU variant combinations below" in source
|
assert "run every camera frame on Chestnut's AMD GPU" in source
|
||||||
assert "Download eGPU variant" in source
|
|
||||||
assert "Delete eGPU variant" in source
|
|
||||||
assert "Nothing is compiled on the comma" not in source
|
|
||||||
assert "availableModels().filter(model => model.modelLabArtifactInstalled)" in source
|
|
||||||
assert source.index("<h3>Available models</h3>") < source.index("<h3>Compose a pair</h3>")
|
|
||||||
assert 'lateral.value === longitudinal.value' in source
|
assert 'lateral.value === longitudinal.value' in source
|
||||||
assert 'lateral.version !== longitudinal.version' not in source
|
assert 'lateral.version !== longitudinal.version' not in source
|
||||||
assert 'class="ml-chip ${' not in source
|
assert 'class="ml-chip ${' not in source
|
||||||
@@ -151,5 +135,5 @@ def test_model_laboratory_frontend_exposes_guards_and_role_copy():
|
|||||||
assert "longitudinalModel: selectionDirty" in source
|
assert "longitudinalModel: selectionDirty" in source
|
||||||
assert source.count("selectionDirty = true") == 2
|
assert source.count("selectionDirty = true") == 2
|
||||||
assert "selectionDirty = false\n applyPayload(payload)" in source
|
assert "selectionDirty = false\n applyPayload(payload)" in source
|
||||||
assert 'model_laboratory.js?v=model-lab-6' in ROUTER_PATH.read_text(encoding="utf-8")
|
assert 'model_laboratory.js?v=model-lab-5' in ROUTER_PATH.read_text(encoding="utf-8")
|
||||||
assert 'model_laboratory.css?v=model-lab-5' in INDEX_PATH.read_text(encoding="utf-8")
|
assert 'model_laboratory.css?v=model-lab-4' in INDEX_PATH.read_text(encoding="utf-8")
|
||||||
|
|||||||
@@ -231,25 +231,6 @@ def test_wheel_controls_status_includes_favorite_slots(monkeypatch):
|
|||||||
"__starpilot_controller_action__:disengage_openpilot",
|
"__starpilot_controller_action__:disengage_openpilot",
|
||||||
}
|
}
|
||||||
assert response.get_json()["speed_unit"] == "mph"
|
assert response.get_json()["speed_unit"] == "mph"
|
||||||
assert response.get_json()["disconnect_controllers_offroad"] is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_wheel_controls_configures_offroad_controller_disconnect(monkeypatch):
|
|
||||||
client, fake_params = _params_client(monkeypatch, {"IsOffroad": True}, "mici")
|
|
||||||
|
|
||||||
response = client.post("/api/wheel-controls/offroad-disconnect", json={"enabled": True})
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert fake_params.get_bool("BluetoothDisconnectControllersOffroad")
|
|
||||||
|
|
||||||
|
|
||||||
def test_wheel_controls_offroad_controller_disconnect_requires_offroad(monkeypatch):
|
|
||||||
client, fake_params = _params_client(monkeypatch, {"IsOffroad": False}, "mici")
|
|
||||||
|
|
||||||
response = client.post("/api/wheel-controls/offroad-disconnect", json={"enabled": True})
|
|
||||||
|
|
||||||
assert response.status_code == 409
|
|
||||||
assert not fake_params.get_bool("BluetoothDisconnectControllersOffroad")
|
|
||||||
|
|
||||||
|
|
||||||
def test_wheel_controls_configures_a_controller_only_action(monkeypatch):
|
def test_wheel_controls_configures_a_controller_only_action(monkeypatch):
|
||||||
|
|||||||
@@ -62,26 +62,6 @@ def test_slug_middleware_service_worker_and_headers(client):
|
|||||||
assert response_direct.headers.get("Service-Worker-Allowed") == "/"
|
assert response_direct.headers.get("Service-Worker-Allowed") == "/"
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_manifest_embeds_device_slug_for_fresh_app_login(client):
|
|
||||||
galaxy_dir = the_galaxy._get_galaxy_dir()
|
|
||||||
galaxy_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
(galaxy_dir / "glxyslug").write_text("df70390ca648d7c3")
|
|
||||||
|
|
||||||
response = client.get("/assets/mobile/manifest.json")
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.mimetype == "application/manifest+json"
|
|
||||||
assert response.get_json()["start_url"] == "https://galaxy.firestar.link/df70390ca648d7c3"
|
|
||||||
assert "no-store" in response.headers.get("Cache-Control", "")
|
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_manifest_falls_back_to_local_mobile_route_without_slug(client):
|
|
||||||
response = client.get("/assets/mobile/manifest.json")
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.get_json()["start_url"] == "/mobile/"
|
|
||||||
|
|
||||||
|
|
||||||
def test_404_api_returns_json_not_html(client):
|
def test_404_api_returns_json_not_html(client):
|
||||||
# Non-existent API route without slug
|
# Non-existent API route without slug
|
||||||
res1 = client.get("/api/nonexistent")
|
res1 = client.get("/api/nonexistent")
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ def test_ui_app_shell_files_exist():
|
|||||||
"js/views/Tuning.js",
|
"js/views/Tuning.js",
|
||||||
"js/views/Navigation.js",
|
"js/views/Navigation.js",
|
||||||
"js/views/Vehicle.js",
|
"js/views/Vehicle.js",
|
||||||
"js/views/Bluetooth.js",
|
|
||||||
"js/views/SystemTools.js",
|
"js/views/SystemTools.js",
|
||||||
]
|
]
|
||||||
for rel in required:
|
for rel in required:
|
||||||
@@ -56,8 +55,6 @@ def test_ui_index_wires_vue_and_mount_point():
|
|||||||
assert 'id="galaxy-app"' in index
|
assert 'id="galaxy-app"' in index
|
||||||
assert 'src="/assets/mobile/js/app.js"' in index
|
assert 'src="/assets/mobile/js/app.js"' in index
|
||||||
assert '"vue": "/assets/vendor/vue/vue.esm-browser.js"' in index
|
assert '"vue": "/assets/vendor/vue/vue.esm-browser.js"' in index
|
||||||
assert '<title>Galaxy</title>' in index
|
|
||||||
assert 'apple-mobile-web-app-title" content="Galaxy"' in index
|
|
||||||
|
|
||||||
|
|
||||||
def test_ui_uses_same_backend_endpoints():
|
def test_ui_uses_same_backend_endpoints():
|
||||||
@@ -110,7 +107,7 @@ def test_ui_ports_all_tool_views():
|
|||||||
"js/views/Recordings.js": ["/api/routes", "getRoutesStream", "getRouteLogs"],
|
"js/views/Recordings.js": ["/api/routes", "getRoutesStream", "getRouteLogs"],
|
||||||
"js/views/Logs.js": ["getErrorLogs", "tmuxSnapshot"],
|
"js/views/Logs.js": ["getErrorLogs", "tmuxSnapshot"],
|
||||||
"js/components/TroubleshootPanel.js": ["getTroubleshoot", "resetTroubleshootSection", "GalaxyConfirm"],
|
"js/components/TroubleshootPanel.js": ["getTroubleshoot", "resetTroubleshootSection", "GalaxyConfirm"],
|
||||||
"js/views/Tuning.js": ["LateralTuningPanel"],
|
"js/views/Tuning.js": ["LateralTuningPanel", "LongitudinalManeuvers"],
|
||||||
"js/views/Navigation.js": ["getNavigation", "setNavigation", "MapsPanel", "NavigationKeysPanel"],
|
"js/views/Navigation.js": ["getNavigation", "setNavigation", "MapsPanel", "NavigationKeysPanel"],
|
||||||
"js/views/ToolEmbed.js": ["/manage_maps", "/manage_navigation_keys"],
|
"js/views/ToolEmbed.js": ["/manage_maps", "/manage_navigation_keys"],
|
||||||
"js/views/SystemTools.js": [
|
"js/views/SystemTools.js": [
|
||||||
@@ -125,9 +122,7 @@ def test_ui_ports_all_tool_views():
|
|||||||
for ep in endpoints:
|
for ep in endpoints:
|
||||||
assert ep in src, f"{rel} should use api.{ep}"
|
assert ep in src, f"{rel} should use api.{ep}"
|
||||||
vehicle = _read("js/views/Vehicle.js")
|
vehicle = _read("js/views/Vehicle.js")
|
||||||
bluetooth = _read("js/views/Bluetooth.js")
|
assert "WheelControls" in vehicle and "BluetoothPanel" in vehicle and "carFeaturesCheck" in vehicle
|
||||||
assert "WheelControls" in vehicle and "BluetoothPanel" not in vehicle and "carFeaturesCheck" in vehicle
|
|
||||||
assert "BluetoothPanel" in bluetooth
|
|
||||||
|
|
||||||
|
|
||||||
def test_ui_routes_ported_views_natively_no_classic_fallback():
|
def test_ui_routes_ported_views_natively_no_classic_fallback():
|
||||||
@@ -135,16 +130,16 @@ def test_ui_routes_ported_views_natively_no_classic_fallback():
|
|||||||
shell = _read("js/components/AppShell.js")
|
shell = _read("js/components/AppShell.js")
|
||||||
tools = _read("js/views/Tools.js")
|
tools = _read("js/views/Tools.js")
|
||||||
|
|
||||||
for view in ["Recordings", "Logs", "Tuning", "Navigation", "Vehicle", "Bluetooth", "SystemTools"]:
|
for view in ["Recordings", "Logs", "Tuning", "Navigation", "Vehicle", "SystemTools"]:
|
||||||
assert view in app, f"app.js should register {view}"
|
assert view in app, f"app.js should register {view}"
|
||||||
|
|
||||||
# Ported routes must resolve natively in the Vue app (zero /classic redirect).
|
# Ported routes must resolve natively in the Vue app (zero /classic redirect).
|
||||||
for route in ["/recordings", "/logs", "/tuning", "/navigation", "/vehicle", "/bluetooth", "/system"]:
|
for route in ["/recordings", "/logs", "/tuning", "/navigation", "/vehicle", "/system"]:
|
||||||
assert route in shell, f"AppShell should route {route} natively"
|
assert route in shell, f"AppShell should route {route} natively"
|
||||||
assert route in app, f"app.js should resolve {route} natively"
|
assert route in app, f"app.js should resolve {route} natively"
|
||||||
# Tools grid routes the native categories (Recordings lives in the bottom nav
|
# Tools grid routes the native categories (Recordings lives in the bottom nav
|
||||||
# and is intentionally absent from the Tools page).
|
# and is intentionally absent from the Tools page).
|
||||||
for tool in ["/tuning", "/logs", "/navigation", "/vehicle", "/bluetooth", "/system"]:
|
for tool in ["/tuning", "/logs", "/navigation", "/vehicle", "/system"]:
|
||||||
assert tool in tools, f"Tools grid should route {tool} natively"
|
assert tool in tools, f"Tools grid should route {tool} natively"
|
||||||
assert "/cameras" in tools, "Tools grid should route the camera hub natively"
|
assert "/cameras" in tools, "Tools grid should route the camera hub natively"
|
||||||
assert "/manage_v_asm" not in tools and "/manage_pip_sidecam" not in tools
|
assert "/manage_v_asm" not in tools and "/manage_pip_sidecam" not in tools
|
||||||
@@ -195,23 +190,6 @@ def test_ui_numeric_toggles_are_sliders_with_default():
|
|||||||
assert 'title="Set to zero"' not in card
|
assert 'title="Set to zero"' not in card
|
||||||
|
|
||||||
|
|
||||||
def test_ui_speed_units_follow_the_vehicle():
|
|
||||||
params = _read("js/params.js")
|
|
||||||
card = _read("js/components/GalaxyToggleCard.js")
|
|
||||||
tree = _read("js/components/SettingTree.js")
|
|
||||||
settings = _read("js/views/Settings.js")
|
|
||||||
|
|
||||||
assert "resolveVehicleUnitParam" in params
|
|
||||||
assert "formatNumericParamValue" in params
|
|
||||||
assert "unit_search_terms" in params and "unit_search_terms" in settings
|
|
||||||
assert "IsMetric" in params
|
|
||||||
assert ':values="values"' in tree
|
|
||||||
assert "displayParam" in card and "formatNumericParamValue" in card
|
|
||||||
assert "sliderStepDisplay" in card and "Step:" in card
|
|
||||||
assert ':values="values"' in settings
|
|
||||||
assert "gx-unit-note" not in settings
|
|
||||||
|
|
||||||
|
|
||||||
def test_ui_centralizes_api_and_uses_composables():
|
def test_ui_centralizes_api_and_uses_composables():
|
||||||
api = _read("js/api.js")
|
api = _read("js/api.js")
|
||||||
composables = _read("js/composables.js")
|
composables = _read("js/composables.js")
|
||||||
@@ -231,10 +209,10 @@ def test_ui_centralizes_api_and_uses_composables():
|
|||||||
def test_ui_schema_driven_param_engine_reused():
|
def test_ui_schema_driven_param_engine_reused():
|
||||||
tuning = _read("js/views/Tuning.js")
|
tuning = _read("js/views/Tuning.js")
|
||||||
assert "GalaxyEmbed" not in tuning and 'src="/tuning"' not in tuning, "Tuning must be native, not a classic embed"
|
assert "GalaxyEmbed" not in tuning and 'src="/tuning"' not in tuning, "Tuning must be native, not a classic embed"
|
||||||
assert "LateralTuningPanel" in tuning and "LongitudinalManeuvers" not in tuning
|
assert "LateralTuningPanel" in tuning and "LongitudinalManeuvers" in tuning
|
||||||
vehicle = _read("js/views/Vehicle.js")
|
vehicle = _read("js/views/Vehicle.js")
|
||||||
assert "ParamSections" not in vehicle, "Vehicle must not render redundant toggles"
|
assert "ParamSections" not in vehicle, "Vehicle must not render redundant toggles"
|
||||||
assert "WheelControls" in vehicle and "BluetoothPanel" not in vehicle
|
assert "WheelControls" in vehicle and "BluetoothPanel" in vehicle
|
||||||
assert "GalaxySection" in vehicle
|
assert "GalaxySection" in vehicle
|
||||||
engine = _read("js/components/ParamSections.js")
|
engine = _read("js/components/ParamSections.js")
|
||||||
assert "SettingTree" in engine
|
assert "SettingTree" in engine
|
||||||
@@ -290,7 +268,6 @@ def test_ui_has_bottom_navigation_and_drawer():
|
|||||||
assert "gx-drawer" in shell
|
assert "gx-drawer" in shell
|
||||||
assert "gx-appbar" in shell
|
assert "gx-appbar" in shell
|
||||||
assert "Search toggles" in shell
|
assert "Search toggles" in shell
|
||||||
assert ">Galaxy</span>" in shell
|
|
||||||
|
|
||||||
|
|
||||||
def test_ui_search_visible_on_mobile_and_content_full_width():
|
def test_ui_search_visible_on_mobile_and_content_full_width():
|
||||||
@@ -348,6 +325,8 @@ def test_ui_galaxy_background_is_css_only_and_lightweight():
|
|||||||
|
|
||||||
def test_galaxy_py_serves_classic_at_root_and_new_ui_at_mobile():
|
def test_galaxy_py_serves_classic_at_root_and_new_ui_at_mobile():
|
||||||
source = GALAXY_PY.read_text(encoding="utf-8")
|
source = GALAXY_PY.read_text(encoding="utf-8")
|
||||||
|
# The classic Galaxy SPA is the default landing at / (original behaviour) unless
|
||||||
|
# the "New Galaxy by Default" (GalaxyMobileDefault) toggle is enabled.
|
||||||
assert '@app.route("/", methods=["GET"])' in source
|
assert '@app.route("/", methods=["GET"])' in source
|
||||||
assert 'render_template("index.html")' in source
|
assert 'render_template("index.html")' in source
|
||||||
assert 'params.get_bool("GalaxyMobileDefault")' in source
|
assert 'params.get_bool("GalaxyMobileDefault")' in source
|
||||||
@@ -362,10 +341,9 @@ def test_galaxy_py_serves_classic_at_root_and_new_ui_at_mobile():
|
|||||||
def test_ui_manifest_is_valid_pwa_manifest():
|
def test_ui_manifest_is_valid_pwa_manifest():
|
||||||
manifest = json.loads((UI_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
manifest = json.loads((UI_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||||
assert manifest["display"] == "standalone"
|
assert manifest["display"] == "standalone"
|
||||||
assert manifest["name"] == "Galaxy"
|
assert manifest["name"]
|
||||||
assert manifest["short_name"] == "Galaxy"
|
|
||||||
assert manifest["icons"]
|
assert manifest["icons"]
|
||||||
assert "start_url" not in manifest
|
assert manifest["start_url"] == "/mobile/"
|
||||||
|
|
||||||
|
|
||||||
def test_ui_ported_classic_tools_native_no_embed():
|
def test_ui_ported_classic_tools_native_no_embed():
|
||||||
@@ -422,6 +400,7 @@ def test_ui_all_remaining_classic_tools_native_no_embed():
|
|||||||
|
|
||||||
# Standalone native views + their routes.
|
# Standalone native views + their routes.
|
||||||
native = {
|
native = {
|
||||||
|
"/sentry": "Sentry",
|
||||||
"/manage_models": "ModelManager",
|
"/manage_models": "ModelManager",
|
||||||
"/plots": "Plots",
|
"/plots": "Plots",
|
||||||
"/testing_ground": "TestingGround",
|
"/testing_ground": "TestingGround",
|
||||||
@@ -435,10 +414,6 @@ def test_ui_all_remaining_classic_tools_native_no_embed():
|
|||||||
assert src, f"missing view: {view}"
|
assert src, f"missing view: {view}"
|
||||||
assert "GalaxyEmbed" not in src and "fetch(" not in src, f"{view} should be native with no raw fetch"
|
assert "GalaxyEmbed" not in src and "fetch(" not in src, f"{view} should be native with no raw fetch"
|
||||||
|
|
||||||
assert '"/sentry": Cameras' in app
|
|
||||||
sentry = _read("js/views/Sentry.js")
|
|
||||||
assert "GalaxyEmbed" not in sentry and "fetch(" not in sentry
|
|
||||||
|
|
||||||
# Navigation maps + App Keys and Tuning lateral are native tabs now.
|
# Navigation maps + App Keys and Tuning lateral are native tabs now.
|
||||||
nav = _read("js/views/Navigation.js")
|
nav = _read("js/views/Navigation.js")
|
||||||
assert "GalaxyEmbed" not in nav and "MapsPanel" in nav and "NavigationKeysPanel" in nav
|
assert "GalaxyEmbed" not in nav and "MapsPanel" in nav and "NavigationKeysPanel" in nav
|
||||||
@@ -474,9 +449,8 @@ def test_ui_cameras_hub_vasm_and_pip_native_no_embed():
|
|||||||
assert "/cameras" in app and "Cameras" in app, "app.js should register the camera hub"
|
assert "/cameras" in app and "Cameras" in app, "app.js should register the camera hub"
|
||||||
assert "/cameras" in store, "store NATIVE_ROOTS should include /cameras"
|
assert "/cameras" in store, "store NATIVE_ROOTS should include /cameras"
|
||||||
assert "GalaxyEmbed" not in cameras and "fetch(" not in cameras
|
assert "GalaxyEmbed" not in cameras and "fetch(" not in cameras
|
||||||
assert "Sentry" in cameras and "Vasm" in cameras and "Pip" in cameras, "camera hub should embed Sentry, V-ASM, and PiP"
|
assert "Vasm" in cameras and "Pip" in cameras, "camera hub should embed V-ASM and PiP"
|
||||||
assert "GalaxyTabs" in cameras
|
assert "GalaxyTabs" in cameras
|
||||||
assert cameras.index('sentry: "Sentry Mode"') < cameras.index('vasm: "V-ASM Spot Monitor"')
|
|
||||||
|
|
||||||
# Removed standalone pages are no longer routed or listed as native roots.
|
# Removed standalone pages are no longer routed or listed as native roots.
|
||||||
for route in ["/manage_v_asm", "/manage_pip_sidecam"]:
|
for route in ["/manage_v_asm", "/manage_pip_sidecam"]:
|
||||||
@@ -496,40 +470,6 @@ def test_ui_cameras_hub_vasm_and_pip_native_no_embed():
|
|||||||
assert method in api, f"api.js should expose {method}"
|
assert method in api, f"api.js should expose {method}"
|
||||||
|
|
||||||
|
|
||||||
def test_ui_mobile_polish_regressions():
|
|
||||||
system = _read("js/views/SystemTools.js")
|
|
||||||
css = _read("css/material.css")
|
|
||||||
assert 'button v-if="updateAvailable"' in system
|
|
||||||
assert "checkedForUpdates && !!this.fastStatus?.updateAvailable" in system
|
|
||||||
assert "gx-update-progress__fill" in system
|
|
||||||
assert "linear-gradient(90deg, #5ec8c8 0%, #8b6cc5 100%)" in css
|
|
||||||
|
|
||||||
bluetooth = _read("js/components/BluetoothPanel.js")
|
|
||||||
assert "methods: {\n address," in bluetooth
|
|
||||||
|
|
||||||
logs = _read("js/views/Logs.js")
|
|
||||||
assert logs.index('troubleshoot: "Troubleshoot"') < logs.index('errors: "Error Logs"') < logs.index('tmux: "Tmux Live Log"')
|
|
||||||
troubleshoot = _read("js/components/TroubleshootPanel.js")
|
|
||||||
assert "gx-diagnostic-row--changed" in troubleshoot and ".gx-row.gx-diagnostic-row--changed" in css
|
|
||||||
|
|
||||||
recordings = _read("js/views/Recordings.js")
|
|
||||||
galaxy = _read("js/views/Galaxy.js")
|
|
||||||
assert "bandwidth reasons" in recordings and "status?.lanIp" in recordings
|
|
||||||
assert "status?.lanIp" in galaxy
|
|
||||||
assert ':href="localUrl"' in recordings and ':href="localUrl"' in galaxy
|
|
||||||
|
|
||||||
home = _read("js/views/Home.js")
|
|
||||||
home_css = _read("css/home.css")
|
|
||||||
assert "backgroundImage: modelView.style" in home
|
|
||||||
assert "display: flex" in home_css and "flex-direction: column" in home_css
|
|
||||||
|
|
||||||
tuning = _read("js/views/Tuning.js")
|
|
||||||
classic_sidebar = (REPO_ROOT / "starpilot/system/the_galaxy/assets/components/sidebar.js").read_text(encoding="utf-8")
|
|
||||||
c4_developer = (REPO_ROOT / "selfdrive/ui/layouts/settings/developer.py").read_text(encoding="utf-8")
|
|
||||||
assert "LongitudinalManeuvers" not in tuning and "Long Maneuvers" not in classic_sidebar
|
|
||||||
assert 'tr("Longitudinal Maneuver Mode")' not in c4_developer
|
|
||||||
|
|
||||||
|
|
||||||
def _node_exe():
|
def _node_exe():
|
||||||
candidates = [
|
candidates = [
|
||||||
shutil.which("node"),
|
shutil.which("node"),
|
||||||
@@ -565,23 +505,6 @@ assert(P.countAdvancedHiddenByDeveloperMode([sec], { GalaxyDeveloperMode: true }
|
|||||||
const slider = { key: "DeviceShutdown", data_type: "int", min: 1, max: 30, step: 1 }
|
const slider = { key: "DeviceShutdown", data_type: "int", min: 1, max: 30, step: 1 }
|
||||||
assert(P.snapNumericToBoundsAndStep(17.9, P.numericBounds(slider, {}), 0) === 18, "snap")
|
assert(P.snapNumericToBoundsAndStep(17.9, P.numericBounds(slider, {}), 0) === 18, "snap")
|
||||||
assert(P.formatSliderValue(6, "1", 0, "DeviceShutdown") === "6 hours", "format")
|
assert(P.formatSliderValue(6, "1", 0, "DeviceShutdown") === "6 hours", "format")
|
||||||
const speed = {
|
|
||||||
key: "Offset2", data_type: "float", unit_type: "vehicle_speed",
|
|
||||||
min: -99, max: 99, step: 1, precision: 0,
|
|
||||||
metric_min: -150, metric_max: 150, unit_range_index: 1,
|
|
||||||
}
|
|
||||||
const imperialSpeed = P.resolveVehicleUnitParam(speed, { IsMetric: false })
|
|
||||||
assert(imperialSpeed.unit === " mph", "imperial unit")
|
|
||||||
assert(imperialSpeed.label === "Speed Offset (25–34 mph)", "imperial offset band")
|
|
||||||
assert(P.formatNumericParamValue(speed, 3, { IsMetric: false }) === "3 mph", "imperial value")
|
|
||||||
const metricSpeed = P.resolveVehicleUnitParam(speed, { IsMetric: true })
|
|
||||||
assert(metricSpeed.unit === " km/h", "metric unit")
|
|
||||||
assert(metricSpeed.label === "Speed Offset (30–49 km/h)", "metric offset band")
|
|
||||||
assert(metricSpeed.unit_search_terms.includes("metric"), "metric settings are searchable")
|
|
||||||
assert(P.numericBounds(speed, { IsMetric: true }).max === 150, "metric bounds")
|
|
||||||
assert(P.numericBounds(speed, { IsMetric: true }).step === 1, "one km/h per step")
|
|
||||||
assert(P.formatNumericParamValue(speed, 3, { IsMetric: true }) === "3 km/h", "metric value")
|
|
||||||
assert(P.usesMetricUnits({ IsMetric: "1" }) === true, "serialized metric bool")
|
|
||||||
const laneOffset = { key: "LaneCenterOffset", data_type: "float", min: 0, max: 0.3, step: 0.01 }
|
const laneOffset = { key: "LaneCenterOffset", data_type: "float", min: 0, max: 0.3, step: 0.01 }
|
||||||
const laneBounds = P.numericBounds(laneOffset, {})
|
const laneBounds = P.numericBounds(laneOffset, {})
|
||||||
assert(laneBounds.min === -0.3, "lane offset keeps signed lower bound")
|
assert(laneBounds.min === -0.3, "lane offset keeps signed lower bound")
|
||||||
|
|||||||
@@ -5015,7 +5015,6 @@ def setup(app):
|
|||||||
"/assets/components/settings.js",
|
"/assets/components/settings.js",
|
||||||
"/assets/components/home/home.js",
|
"/assets/components/home/home.js",
|
||||||
"/assets/components/home/home.css",
|
"/assets/components/home/home.css",
|
||||||
"/assets/mobile/js/params.js",
|
|
||||||
"/assets/components/tools/device_settings.js",
|
"/assets/components/tools/device_settings.js",
|
||||||
"/assets/components/tools/device_settings.css",
|
"/assets/components/tools/device_settings.css",
|
||||||
"/assets/components/tools/device_settings_layout.json",
|
"/assets/components/tools/device_settings_layout.json",
|
||||||
@@ -5182,7 +5181,6 @@ def setup(app):
|
|||||||
status["slots"] = slots
|
status["slots"] = slots
|
||||||
status["controller_slots"] = controller_slots
|
status["controller_slots"] = controller_slots
|
||||||
status["controller_options"] = controller_options
|
status["controller_options"] = controller_options
|
||||||
status["disconnect_controllers_offroad"] = params.get_bool("BluetoothDisconnectControllersOffroad")
|
|
||||||
is_metric = params.get_bool("IsMetric")
|
is_metric = params.get_bool("IsMetric")
|
||||||
speed_minimum, speed_maximum = controller_speed_bounds(is_metric)
|
speed_minimum, speed_maximum = controller_speed_bounds(is_metric)
|
||||||
status["speed_unit"] = "km/h" if is_metric else "mph"
|
status["speed_unit"] = "km/h" if is_metric else "mph"
|
||||||
@@ -5192,16 +5190,13 @@ def setup(app):
|
|||||||
|
|
||||||
@app.route("/api/wheel-controls/<operation>", methods=["POST"])
|
@app.route("/api/wheel-controls/<operation>", methods=["POST"])
|
||||||
def wheel_controls_operation(operation):
|
def wheel_controls_operation(operation):
|
||||||
if operation not in {"action", "learn", "cancel", "delete", "clear", "test", "test-stop", "joystick", "offroad-disconnect"}:
|
if operation not in {"action", "learn", "cancel", "delete", "clear", "test", "test-stop", "joystick"}:
|
||||||
return jsonify({"error": "Unknown wheel control operation."}), 404
|
return jsonify({"error": "Unknown wheel control operation."}), 404
|
||||||
if not params.get_bool("IsOffroad"):
|
if not params.get_bool("IsOffroad"):
|
||||||
return jsonify({"error": "Wheel controls can only be configured offroad."}), 409
|
return jsonify({"error": "Wheel controls can only be configured offroad."}), 409
|
||||||
|
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
try:
|
try:
|
||||||
if operation == "offroad-disconnect":
|
|
||||||
params.put_bool("BluetoothDisconnectControllersOffroad", bool(data.get("enabled", False)))
|
|
||||||
return jsonify({"message": "Offroad controller disconnect updated."}), 200
|
|
||||||
if operation == "action":
|
if operation == "action":
|
||||||
slot_index = int(data.get("slot", -1))
|
slot_index = int(data.get("slot", -1))
|
||||||
key = str(data.get("key") or "").strip()
|
key = str(data.get("key") or "").strip()
|
||||||
@@ -5301,27 +5296,6 @@ def setup(app):
|
|||||||
return "Settings catalog not found", 404
|
return "Settings catalog not found", 404
|
||||||
return send_file(str(SETTINGS_CATALOG_PATH), mimetype="application/json")
|
return send_file(str(SETTINGS_CATALOG_PATH), mimetype="application/json")
|
||||||
|
|
||||||
@app.route("/assets/mobile/manifest.json", methods=["GET"])
|
|
||||||
def mobile_manifest():
|
|
||||||
manifest_path = Path(app.static_folder) / "mobile" / "manifest.json"
|
|
||||||
if not manifest_path.is_file():
|
|
||||||
return jsonify({"error": "Galaxy manifest not found"}), 404
|
|
||||||
|
|
||||||
try:
|
|
||||||
manifest_data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
||||||
except (OSError, ValueError, json.JSONDecodeError):
|
|
||||||
return jsonify({"error": "Galaxy manifest is invalid"}), 500
|
|
||||||
|
|
||||||
slug = _read_galaxy_text(_get_galaxy_dir() / "glxyslug")
|
|
||||||
if re.fullmatch(r"[A-Za-z0-9]{16}", slug):
|
|
||||||
manifest_data["start_url"] = f"https://galaxy.firestar.link/{slug}"
|
|
||||||
else:
|
|
||||||
manifest_data["start_url"] = "/mobile/"
|
|
||||||
|
|
||||||
response = jsonify(manifest_data)
|
|
||||||
response.mimetype = "application/manifest+json"
|
|
||||||
return _no_store_response(response)
|
|
||||||
|
|
||||||
@app.route("/manifest.json", methods=["GET"])
|
@app.route("/manifest.json", methods=["GET"])
|
||||||
@app.route("/assets/manifest.json", methods=["GET"])
|
@app.route("/assets/manifest.json", methods=["GET"])
|
||||||
def manifest():
|
def manifest():
|
||||||
@@ -6348,24 +6322,21 @@ def setup(app):
|
|||||||
},
|
},
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"version": params.get("ModelManifestVersion", encoding="utf-8") or "unknown",
|
"version": params.get("ModelManifestVersion", encoding="utf-8") or "unknown",
|
||||||
|
"shortcomings": [
|
||||||
|
"The current manifest does not consistently declare model size; legacy non-Chestnut entries are treated as small.",
|
||||||
|
"The current manifest does not publish AMD-compiled variants for its ordinary small-model downloads.",
|
||||||
|
"The current manifest does not declare lateral or longitudinal quality/capability tags.",
|
||||||
|
"The current manifest does not declare output-contract compatibility, memory, or frame-time measurements.",
|
||||||
|
],
|
||||||
|
"opportunities": [
|
||||||
|
"Publish model_size and model_lab_eligible for every model.",
|
||||||
|
"Publish an accelerator_artifacts.chestnut entry pointing to a precompiled AMD pickle for each supported small model.",
|
||||||
|
"Publish role scores and pairing notes from replay evaluations.",
|
||||||
|
"Publish architecture, output-contract, peak-memory, and p50/p95 execution metadata.",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def _activate_preferred_model_profile():
|
|
||||||
"""Restore the model that the normal small/big profile system would run."""
|
|
||||||
profile = "big" if external_gpu_available() and _active_model_key("big") else "small"
|
|
||||||
model_key, model_name, model_version = get_model_profile(params, profile)
|
|
||||||
if not model_key:
|
|
||||||
model_key, model_name, model_version = _default_model_key(), _default_model_name(), _default_model_version()
|
|
||||||
|
|
||||||
params.put("Model", model_key)
|
|
||||||
params.put("DrivingModel", model_key)
|
|
||||||
params.put("DrivingModelName", model_name or model_key)
|
|
||||||
if model_version:
|
|
||||||
params.put("ModelVersion", model_version)
|
|
||||||
params.put("DrivingModelVersion", model_version)
|
|
||||||
return model_name or model_key
|
|
||||||
|
|
||||||
@app.route("/api/model-laboratory", methods=["GET", "PUT"])
|
@app.route("/api/model-laboratory", methods=["GET", "PUT"])
|
||||||
def model_laboratory():
|
def model_laboratory():
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
@@ -6404,8 +6375,7 @@ def setup(app):
|
|||||||
params.put("DrivingModelVersion", lateral["version"])
|
params.put("DrivingModelVersion", lateral["version"])
|
||||||
message = "Model Laboratory enabled. The pair will load on the next drive."
|
message = "Model Laboratory enabled. The pair will load on the next drive."
|
||||||
else:
|
else:
|
||||||
restored_model = _activate_preferred_model_profile()
|
message = "Model Laboratory disabled."
|
||||||
message = f"Model Laboratory disabled. {restored_model} will be used next."
|
|
||||||
|
|
||||||
return jsonify({"message": message, **_model_lab_status_payload()}), 200
|
return jsonify({"message": message, **_model_lab_status_payload()}), 200
|
||||||
|
|
||||||
@@ -6413,6 +6383,8 @@ def setup(app):
|
|||||||
def download_model_laboratory_artifact():
|
def download_model_laboratory_artifact():
|
||||||
if params.get_bool("IsOnroad"):
|
if params.get_bool("IsOnroad"):
|
||||||
return jsonify({"error": "Model Laboratory artifacts can only be downloaded while parked."}), 403
|
return jsonify({"error": "Model Laboratory artifacts can only be downloaded while parked."}), 403
|
||||||
|
if not external_gpu_available():
|
||||||
|
return jsonify({"error": "Chestnut is not connected and firmware-ready."}), 409
|
||||||
if (
|
if (
|
||||||
params_memory.get_bool(MODEL_DOWNLOAD_ALL_PARAM)
|
params_memory.get_bool(MODEL_DOWNLOAD_ALL_PARAM)
|
||||||
or (params_memory.get(MODEL_DOWNLOAD_PARAM, encoding="utf-8") or "")
|
or (params_memory.get(MODEL_DOWNLOAD_PARAM, encoding="utf-8") or "")
|
||||||
@@ -6426,50 +6398,16 @@ def setup(app):
|
|||||||
if model is None:
|
if model is None:
|
||||||
return jsonify({"error": f"Unknown model '{model_key}'."}), 404
|
return jsonify({"error": f"Unknown model '{model_key}'."}), 404
|
||||||
if not model.get("modelLabEligible"):
|
if not model.get("modelLabEligible"):
|
||||||
return jsonify({"error": "Only compatible small models have Model Laboratory eGPU variants."}), 409
|
return jsonify({"error": "Only compatible small models can be prepared for Model Laboratory."}), 409
|
||||||
if not model.get("modelLabArtifactAvailable"):
|
if not model.get("modelLabArtifactAvailable"):
|
||||||
return jsonify({"error": "The manifest does not publish a precompiled AMD artifact for this model."}), 409
|
return jsonify({"error": "The manifest does not publish a precompiled AMD artifact for this model."}), 409
|
||||||
if model.get("modelLabArtifactInstalled"):
|
if model.get("modelLabArtifactInstalled"):
|
||||||
return jsonify({"message": f"The eGPU variant for \"{model['label']}\" is already downloaded."}), 200
|
return jsonify({"message": f"\"{model['label']}\" is already prepared for Chestnut."}), 200
|
||||||
|
|
||||||
params_memory.remove(MODEL_CANCEL_DOWNLOAD_PARAM)
|
params_memory.remove(MODEL_CANCEL_DOWNLOAD_PARAM)
|
||||||
params_memory.put(MODEL_LAB_DOWNLOAD_PARAM, model_key)
|
params_memory.put(MODEL_LAB_DOWNLOAD_PARAM, model_key)
|
||||||
params_memory.put(MODEL_DOWNLOAD_PROGRESS_PARAM, "Starting eGPU variant download...")
|
params_memory.put(MODEL_DOWNLOAD_PROGRESS_PARAM, "Downloading precompiled AMD artifact...")
|
||||||
return jsonify({"message": f"Started downloading the eGPU variant for \"{model['label']}\"."}), 200
|
return jsonify({"message": f"Started preparing \"{model['label']}\" for Chestnut."}), 200
|
||||||
|
|
||||||
@app.route("/api/model-laboratory/artifact", methods=["DELETE"])
|
|
||||||
def delete_model_laboratory_artifact():
|
|
||||||
if params.get_bool("IsOnroad"):
|
|
||||||
return jsonify({"error": "Model Laboratory eGPU variants can only be deleted while parked."}), 403
|
|
||||||
if (
|
|
||||||
params_memory.get_bool(MODEL_DOWNLOAD_ALL_PARAM)
|
|
||||||
or (params_memory.get(MODEL_DOWNLOAD_PARAM, encoding="utf-8") or "")
|
|
||||||
or (params_memory.get(MODEL_LAB_DOWNLOAD_PARAM, encoding="utf-8") or "")
|
|
||||||
):
|
|
||||||
return jsonify({"error": "Cannot delete an eGPU variant while a model download is in progress."}), 409
|
|
||||||
|
|
||||||
data = request.get_json(silent=True) or {}
|
|
||||||
model_key = canonical_model_key(str(data.get("model") or "").strip())
|
|
||||||
model = next((entry for entry in get_model_catalog() if entry["value"] == model_key), None)
|
|
||||||
if model is None:
|
|
||||||
return jsonify({"error": f"Unknown model '{model_key}'."}), 404
|
|
||||||
if not model.get("modelLabArtifactInstalled"):
|
|
||||||
return jsonify({"message": f"No eGPU variant is downloaded for \"{model['label']}\"."}), 200
|
|
||||||
|
|
||||||
config = normalize_model_lab_config(params.get(MODEL_LAB_CONFIG_PARAM, encoding="utf-8") or "")
|
|
||||||
if config["enabled"] and model_key in (config["lateralModel"], config["longitudinalModel"]):
|
|
||||||
return jsonify({"error": "Disable Model Laboratory or choose a different pair before deleting this eGPU variant."}), 409
|
|
||||||
|
|
||||||
artifact_path = MODELS_PATH / model_accelerator_artifact_filename(model_key)
|
|
||||||
try:
|
|
||||||
artifact_path.unlink(missing_ok=True)
|
|
||||||
Path(get_manifest_path(artifact_path)).unlink(missing_ok=True)
|
|
||||||
for chunk_path in artifact_path.parent.glob(f"{artifact_path.name}.chunk*of*"):
|
|
||||||
chunk_path.unlink(missing_ok=True)
|
|
||||||
except Exception as exception:
|
|
||||||
return jsonify({"error": f"Failed deleting the eGPU variant: {exception}"}), 500
|
|
||||||
|
|
||||||
return jsonify({"message": f"Deleted the eGPU variant for \"{model['label']}\".", **_model_lab_status_payload()}), 200
|
|
||||||
|
|
||||||
@app.route("/api/models/preferences", methods=["GET", "PUT"])
|
@app.route("/api/models/preferences", methods=["GET", "PUT"])
|
||||||
def get_or_set_models_preferences():
|
def get_or_set_models_preferences():
|
||||||
@@ -6523,9 +6461,8 @@ def setup(app):
|
|||||||
params.remove(MODEL_LAB_RUNTIME_PARAM)
|
params.remove(MODEL_LAB_RUNTIME_PARAM)
|
||||||
|
|
||||||
disable_big_model_profile(params)
|
disable_big_model_profile(params)
|
||||||
restored_model = _activate_preferred_model_profile()
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": f"Active Big disabled. {restored_model} will be used even when Chestnut is connected.",
|
"message": "Active Big disabled. Active Small will be used even when Chestnut is connected.",
|
||||||
"profile": profile,
|
"profile": profile,
|
||||||
"model": "",
|
"model": "",
|
||||||
}), 200
|
}), 200
|
||||||
@@ -6547,9 +6484,8 @@ def setup(app):
|
|||||||
params.remove(MODEL_LAB_RUNTIME_PARAM)
|
params.remove(MODEL_LAB_RUNTIME_PARAM)
|
||||||
|
|
||||||
set_model_profile(params, profile, model_key, model["label"], model["version"])
|
set_model_profile(params, profile, model_key, model["label"], model["version"])
|
||||||
active_model = _activate_preferred_model_profile()
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": f"Active {profile.title()} set to '{model['label']}'. {active_model} will be used next.",
|
"message": f"Active {profile.title()} set to '{model['label']}'.",
|
||||||
"profile": profile,
|
"profile": profile,
|
||||||
"model": model_key,
|
"model": model_key,
|
||||||
}), 200
|
}), 200
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ DEVELOPER_METRIC_DISPLAY_KEYS = (
|
|||||||
)
|
)
|
||||||
DEVICE_SHUTDOWN_KEY = "DeviceShutdown"
|
DEVICE_SHUTDOWN_KEY = "DeviceShutdown"
|
||||||
CAMERA_VIEW_KEY = "CameraView"
|
CAMERA_VIEW_KEY = "CameraView"
|
||||||
GALAXY_NEW_DEFAULT_KEY = "GalaxyMobileDefault"
|
REVERSE_CRUISE_KEY = "ReverseCruise"
|
||||||
|
|
||||||
DEFAULT_STEER_KP = 0.6
|
DEFAULT_STEER_KP = 0.6
|
||||||
LEGACY_STEER_KP = 0.7
|
LEGACY_STEER_KP = 0.7
|
||||||
@@ -39,8 +39,7 @@ LANE_CHANGE_SMOOTHING_MIGRATION_MARKER = ".starpilot_lane_change_smoothing_defau
|
|||||||
SPEED_LIMIT_VISIBILITY_MIGRATION_MARKER = ".starpilot_speed_limit_visibility_v1"
|
SPEED_LIMIT_VISIBILITY_MIGRATION_MARKER = ".starpilot_speed_limit_visibility_v1"
|
||||||
DEVICE_SHUTDOWN_HOURS_MIGRATION_MARKER = ".starpilot_device_shutdown_hours_v1"
|
DEVICE_SHUTDOWN_HOURS_MIGRATION_MARKER = ".starpilot_device_shutdown_hours_v1"
|
||||||
CAMERA_VIEW_DEFAULT_MIGRATION_MARKER = ".starpilot_camera_view_default_v1"
|
CAMERA_VIEW_DEFAULT_MIGRATION_MARKER = ".starpilot_camera_view_default_v1"
|
||||||
REVERSE_CRUISE_RESTORE_MIGRATION_MARKER = ".starpilot_restore_reverse_cruise_v1"
|
REVERSE_CRUISE_REMOVAL_MIGRATION_MARKER = ".starpilot_remove_reverse_cruise_v1"
|
||||||
GALAXY_NEW_DEFAULT_MIGRATION_MARKER = ".starpilot_galaxy_new_default_v1"
|
|
||||||
MARKER_DIRNAME = ".starpilot_param_migrations"
|
MARKER_DIRNAME = ".starpilot_param_migrations"
|
||||||
|
|
||||||
LATERAL_METHOD_PARAM_SUFFIXES = (
|
LATERAL_METHOD_PARAM_SUFFIXES = (
|
||||||
@@ -148,12 +147,8 @@ def _camera_view_default_marker_path(params: ParamsLike) -> Path:
|
|||||||
return _marker_dir_path(params) / CAMERA_VIEW_DEFAULT_MIGRATION_MARKER
|
return _marker_dir_path(params) / CAMERA_VIEW_DEFAULT_MIGRATION_MARKER
|
||||||
|
|
||||||
|
|
||||||
def _reverse_cruise_restore_marker_path(params: ParamsLike) -> Path:
|
def _reverse_cruise_removal_marker_path(params: ParamsLike) -> Path:
|
||||||
return _marker_dir_path(params) / REVERSE_CRUISE_RESTORE_MIGRATION_MARKER
|
return _marker_dir_path(params) / REVERSE_CRUISE_REMOVAL_MIGRATION_MARKER
|
||||||
|
|
||||||
|
|
||||||
def _galaxy_new_default_marker_path(params: ParamsLike) -> Path:
|
|
||||||
return _marker_dir_path(params) / GALAXY_NEW_DEFAULT_MIGRATION_MARKER
|
|
||||||
|
|
||||||
|
|
||||||
def _marker_dir_path(params: ParamsLike) -> Path:
|
def _marker_dir_path(params: ParamsLike) -> Path:
|
||||||
@@ -337,24 +332,12 @@ def _apply_camera_view_default_migration(params: ParamsLike, marker: Path) -> No
|
|||||||
marker.touch()
|
marker.touch()
|
||||||
|
|
||||||
|
|
||||||
def _restore_reverse_cruise_param(params: ParamsLike, marker: Path) -> None:
|
def _remove_reverse_cruise_param(params: ParamsLike, marker: Path) -> None:
|
||||||
if marker.exists():
|
if marker.exists():
|
||||||
return
|
return
|
||||||
|
|
||||||
marker.parent.mkdir(parents=True, exist_ok=True)
|
marker.parent.mkdir(parents=True, exist_ok=True)
|
||||||
if (not _param_file_exists(params, "ReverseCruise") and
|
Path(params.get_param_path(REVERSE_CRUISE_KEY)).unlink(missing_ok=True)
|
||||||
_approx_equal(params.get_float("CustomCruise"), 5.0) and
|
|
||||||
_approx_equal(params.get_float("CustomCruiseLong"), 1.0)):
|
|
||||||
params.put_bool("ReverseCruise", True)
|
|
||||||
marker.touch()
|
|
||||||
|
|
||||||
|
|
||||||
def _enable_galaxy_new_default(params: ParamsLike, marker: Path) -> None:
|
|
||||||
if marker.exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
marker.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
params.put_bool(GALAXY_NEW_DEFAULT_KEY, True)
|
|
||||||
marker.touch()
|
marker.touch()
|
||||||
|
|
||||||
|
|
||||||
@@ -369,8 +352,7 @@ def apply_launch_param_migrations(params: ParamsLike, marker_path: Path | None =
|
|||||||
speed_limit_visibility_marker_path: Path | None = None,
|
speed_limit_visibility_marker_path: Path | None = None,
|
||||||
device_shutdown_hours_marker_path: Path | None = None,
|
device_shutdown_hours_marker_path: Path | None = None,
|
||||||
camera_view_default_marker_path: Path | None = None,
|
camera_view_default_marker_path: Path | None = None,
|
||||||
reverse_cruise_restore_marker_path: Path | None = None,
|
reverse_cruise_removal_marker_path: Path | None = None) -> None:
|
||||||
galaxy_new_default_marker_path: Path | None = None) -> None:
|
|
||||||
_apply_legacy_launch_param_migrations(params, marker_path or _default_marker_path(params))
|
_apply_legacy_launch_param_migrations(params, marker_path or _default_marker_path(params))
|
||||||
# Keep branch-default rollout on its own marker so older installs that already
|
# Keep branch-default rollout on its own marker so older installs that already
|
||||||
# have the legacy marker still receive this one-time param reset.
|
# have the legacy marker still receive this one-time param reset.
|
||||||
@@ -402,11 +384,8 @@ def apply_launch_param_migrations(params: ParamsLike, marker_path: Path | None =
|
|||||||
_apply_camera_view_default_migration(
|
_apply_camera_view_default_migration(
|
||||||
params, camera_view_default_marker_path or _camera_view_default_marker_path(params)
|
params, camera_view_default_marker_path or _camera_view_default_marker_path(params)
|
||||||
)
|
)
|
||||||
_restore_reverse_cruise_param(
|
_remove_reverse_cruise_param(
|
||||||
params, reverse_cruise_restore_marker_path or _reverse_cruise_restore_marker_path(params)
|
params, reverse_cruise_removal_marker_path or _reverse_cruise_removal_marker_path(params)
|
||||||
)
|
|
||||||
_enable_galaxy_new_default(
|
|
||||||
params, galaxy_new_default_marker_path or _galaxy_new_default_marker_path(params)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ STARPILOT_DEFAULT_MODEL_MIGRATION_FLAG = Path("/data") / "starpilot_default_mode
|
|||||||
STARPILOT_CE_MODEL_STOP_TIME_MIGRATION_FLAG = Path("/data") / "starpilot_ce_model_stop_time_v2"
|
STARPILOT_CE_MODEL_STOP_TIME_MIGRATION_FLAG = Path("/data") / "starpilot_ce_model_stop_time_v2"
|
||||||
STARPILOT_LEGACY_CACHE_MARKER_KEYS = ("RemapCancelToDistance",)
|
STARPILOT_LEGACY_CACHE_MARKER_KEYS = ("RemapCancelToDistance",)
|
||||||
STARPILOT_REMOVED_PARAM_KEYS = (
|
STARPILOT_REMOVED_PARAM_KEYS = (
|
||||||
"CoastUpToLeads", "HumanAcceleration", "HumanFollowing", "PrioritizeSmoothFollowing",
|
"CoastUpToLeads", "HumanAcceleration", "HumanFollowing", "PrioritizeSmoothFollowing", "ReverseCruise",
|
||||||
)
|
)
|
||||||
LEGACY_CARMODEL_MIGRATIONS = {
|
LEGACY_CARMODEL_MIGRATIONS = {
|
||||||
"CHEVROLET_BOLT_CC_2019_2021": "CHEVROLET_BOLT_CC_2018_2021",
|
"CHEVROLET_BOLT_CC_2019_2021": "CHEVROLET_BOLT_CC_2018_2021",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import operator
|
import operator
|
||||||
import platform
|
import platform
|
||||||
@@ -6,6 +8,7 @@ import sys
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
from cereal import car
|
from cereal import car
|
||||||
|
from openpilot.common.gps import gm_car_params_present
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from opendbc.car.gps import car_gps_available
|
from opendbc.car.gps import car_gps_available
|
||||||
from openpilot.system.hardware import HARDWARE, PC, TICI
|
from openpilot.system.hardware import HARDWARE, PC, TICI
|
||||||
@@ -36,9 +39,12 @@ def update_car_gps_param(params: Params) -> bool | None:
|
|||||||
car_params = params.get("CarParams")
|
car_params = params.get("CarParams")
|
||||||
if car_params is None:
|
if car_params is None:
|
||||||
return None
|
return None
|
||||||
|
try:
|
||||||
|
with car.CarParams.from_bytes(car_params) as parsed_cp:
|
||||||
|
available = car_gps_available(parsed_cp)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
with car.CarParams.from_bytes(car_params) as CP:
|
|
||||||
available = car_gps_available(CP)
|
|
||||||
if available != params.get_bool("CarGpsAvailable"):
|
if available != params.get_bool("CarGpsAvailable"):
|
||||||
params.put_bool("CarGpsAvailable", available)
|
params.put_bool("CarGpsAvailable", available)
|
||||||
return available
|
return available
|
||||||
@@ -48,7 +54,9 @@ def ublox(started: bool, params: Params, CP: car.CarParams, starpilot_toggles: S
|
|||||||
use_ublox = ublox_available()
|
use_ublox = ublox_available()
|
||||||
if use_ublox != params.get_bool("UbloxAvailable"):
|
if use_ublox != params.get_bool("UbloxAvailable"):
|
||||||
params.put_bool("UbloxAvailable", use_ublox)
|
params.put_bool("UbloxAvailable", use_ublox)
|
||||||
return started and use_ublox and car_gps is False
|
is_gm = gm_car_params_present(params)
|
||||||
|
# On GM, card arbitrates u-blox and CAN GPS; wait for CarParams.
|
||||||
|
return started and use_ublox and car_gps is not None and (not car_gps or is_gm)
|
||||||
|
|
||||||
def joystick(started: bool, params: Params, CP: car.CarParams, starpilot_toggles: SimpleNamespace) -> bool:
|
def joystick(started: bool, params: Params, CP: car.CarParams, starpilot_toggles: SimpleNamespace) -> bool:
|
||||||
return started and params.get_bool("JoystickDebugMode")
|
return started and params.get_bool("JoystickDebugMode")
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from openpilot.system.manager.launch_param_migrations import (
|
|||||||
DEFAULT_CAMERA_VIEW,
|
DEFAULT_CAMERA_VIEW,
|
||||||
DEVELOPER_METRIC_DISPLAY_KEYS,
|
DEVELOPER_METRIC_DISPLAY_KEYS,
|
||||||
DEVELOPER_METRIC_DISPLAY_MIGRATION_MARKER,
|
DEVELOPER_METRIC_DISPLAY_MIGRATION_MARKER,
|
||||||
GALAXY_NEW_DEFAULT_MIGRATION_MARKER,
|
|
||||||
DEFAULT_LANE_CHANGE_SMOOTHING,
|
DEFAULT_LANE_CHANGE_SMOOTHING,
|
||||||
DEFAULT_STEER_KP,
|
DEFAULT_STEER_KP,
|
||||||
DEVICE_SHUTDOWN_HOURS_MIGRATION_MARKER,
|
DEVICE_SHUTDOWN_HOURS_MIGRATION_MARKER,
|
||||||
@@ -15,7 +14,7 @@ from openpilot.system.manager.launch_param_migrations import (
|
|||||||
LAUNCH_PARAM_MIGRATION_MARKER,
|
LAUNCH_PARAM_MIGRATION_MARKER,
|
||||||
LATERAL_METHOD_REBRAND_MIGRATION_MARKER,
|
LATERAL_METHOD_REBRAND_MIGRATION_MARKER,
|
||||||
MARKER_DIRNAME,
|
MARKER_DIRNAME,
|
||||||
REVERSE_CRUISE_RESTORE_MIGRATION_MARKER,
|
REVERSE_CRUISE_REMOVAL_MIGRATION_MARKER,
|
||||||
STANDARD_ACCELERATION_PROFILE,
|
STANDARD_ACCELERATION_PROFILE,
|
||||||
SPEED_LIMIT_VISIBILITY_MIGRATION_MARKER,
|
SPEED_LIMIT_VISIBILITY_MIGRATION_MARKER,
|
||||||
LEGACY_UI_SELECTION_MIGRATION_MARKER,
|
LEGACY_UI_SELECTION_MIGRATION_MARKER,
|
||||||
@@ -179,42 +178,14 @@ def test_apply_launch_param_migrations_preserves_custom_camera_view(tmp_path):
|
|||||||
assert params.get_int("CameraView") == 0
|
assert params.get_int("CameraView") == 0
|
||||||
|
|
||||||
|
|
||||||
def test_apply_launch_param_migrations_restores_reverse_cruise_from_swapped_intervals(tmp_path):
|
def test_apply_launch_param_migrations_removes_reverse_cruise_param(tmp_path):
|
||||||
params = FileBackedFakeParams(tmp_path / "params")
|
params = FileBackedFakeParams(tmp_path / "params")
|
||||||
params.put_float("CustomCruise", 5.0)
|
params.put_bool("ReverseCruise", True)
|
||||||
params.put_float("CustomCruiseLong", 1.0)
|
|
||||||
|
|
||||||
apply_launch_param_migrations(params)
|
apply_launch_param_migrations(params)
|
||||||
|
|
||||||
assert params.get_bool("ReverseCruise")
|
assert not Path(params.get_param_path("ReverseCruise")).exists()
|
||||||
assert marker_path(tmp_path, REVERSE_CRUISE_RESTORE_MIGRATION_MARKER).is_file()
|
assert marker_path(tmp_path, REVERSE_CRUISE_REMOVAL_MIGRATION_MARKER).is_file()
|
||||||
|
|
||||||
|
|
||||||
def test_apply_launch_param_migrations_preserves_explicit_reverse_cruise_choice(tmp_path):
|
|
||||||
params = FileBackedFakeParams(tmp_path / "params")
|
|
||||||
params.put_float("CustomCruise", 5.0)
|
|
||||||
params.put_float("CustomCruiseLong", 1.0)
|
|
||||||
params.put_bool("ReverseCruise", False)
|
|
||||||
|
|
||||||
apply_launch_param_migrations(params)
|
|
||||||
|
|
||||||
assert not params.get_bool("ReverseCruise")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_launch_param_migrations_enables_galaxy_new_default_once(tmp_path):
|
|
||||||
params = FileBackedFakeParams(tmp_path / "params")
|
|
||||||
params.put_bool("GalaxyMobileDefault", False)
|
|
||||||
|
|
||||||
apply_launch_param_migrations(params)
|
|
||||||
|
|
||||||
assert params.get_bool("GalaxyMobileDefault")
|
|
||||||
marker = marker_path(tmp_path, GALAXY_NEW_DEFAULT_MIGRATION_MARKER)
|
|
||||||
assert marker.is_file()
|
|
||||||
|
|
||||||
params.put_bool("GalaxyMobileDefault", False)
|
|
||||||
apply_launch_param_migrations(params)
|
|
||||||
|
|
||||||
assert not params.get_bool("GalaxyMobileDefault")
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_launch_param_migrations_applies_branch_defaults_for_existing_installs(tmp_path):
|
def test_apply_launch_param_migrations_applies_branch_defaults_for_existing_installs(tmp_path):
|
||||||
|
|||||||
@@ -397,6 +397,7 @@ class TestManager:
|
|||||||
params_cache = FileBackedFakeParams(tmp_path / "cache", {
|
params_cache = FileBackedFakeParams(tmp_path / "cache", {
|
||||||
"HumanFollowing": False,
|
"HumanFollowing": False,
|
||||||
"PrioritizeSmoothFollowing": True,
|
"PrioritizeSmoothFollowing": True,
|
||||||
|
"ReverseCruise": True,
|
||||||
})
|
})
|
||||||
|
|
||||||
manager.cleanup_removed_starpilot_params(params, params_cache)
|
manager.cleanup_removed_starpilot_params(params, params_cache)
|
||||||
@@ -404,9 +405,10 @@ class TestManager:
|
|||||||
assert not Path(params.get_param_path("CoastUpToLeads")).exists()
|
assert not Path(params.get_param_path("CoastUpToLeads")).exists()
|
||||||
assert not Path(params.get_param_path("HumanAcceleration")).exists()
|
assert not Path(params.get_param_path("HumanAcceleration")).exists()
|
||||||
assert not Path(params.get_param_path("HumanFollowing")).exists()
|
assert not Path(params.get_param_path("HumanFollowing")).exists()
|
||||||
assert params.get_bool("ReverseCruise")
|
assert not Path(params.get_param_path("ReverseCruise")).exists()
|
||||||
assert not Path(params_cache.get_param_path("HumanFollowing")).exists()
|
assert not Path(params_cache.get_param_path("HumanFollowing")).exists()
|
||||||
assert not Path(params_cache.get_param_path("PrioritizeSmoothFollowing")).exists()
|
assert not Path(params_cache.get_param_path("PrioritizeSmoothFollowing")).exists()
|
||||||
|
assert not Path(params_cache.get_param_path("ReverseCruise")).exists()
|
||||||
|
|
||||||
def test_migrate_legacy_starpilot_params_cache_copies_marker_sources(self, tmp_path, monkeypatch):
|
def test_migrate_legacy_starpilot_params_cache_copies_marker_sources(self, tmp_path, monkeypatch):
|
||||||
monkeypatch.setattr(manager, "STARPILOT_PARAMS_CACHE_MIGRATION_FLAG", tmp_path / "starpilot_params_cache_v1")
|
monkeypatch.setattr(manager, "STARPILOT_PARAMS_CACHE_MIGRATION_FLAG", tmp_path / "starpilot_params_cache_v1")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import pytest
|
|||||||
|
|
||||||
from cereal import car
|
from cereal import car
|
||||||
from opendbc.car.ford.values import CAR as FORD_CAR
|
from opendbc.car.ford.values import CAR as FORD_CAR
|
||||||
|
from opendbc.car.gm.values import CAR as GM_CAR
|
||||||
import openpilot.system.manager.process_config as process_config
|
import openpilot.system.manager.process_config as process_config
|
||||||
from openpilot.system.manager.process_config import (
|
from openpilot.system.manager.process_config import (
|
||||||
allow_uploads,
|
allow_uploads,
|
||||||
@@ -152,25 +153,33 @@ class GpsParams:
|
|||||||
self.values[key] = value
|
self.values[key] = value
|
||||||
|
|
||||||
|
|
||||||
def test_ublox_waits_for_current_carparams(monkeypatch):
|
@pytest.mark.parametrize("brand,fingerprint,persisted,live_brand,expected,car_gps", [
|
||||||
monkeypatch.setattr("openpilot.system.manager.process_config.ublox_available", lambda: True)
|
("gm", "", False, "gm", False, False),
|
||||||
params = GpsParams()
|
("mock", "", False, "mock", False, False),
|
||||||
|
("ford", "", False, "ford", False, False),
|
||||||
|
("gm", GM_CAR.CHEVROLET_BOLT_CC_2018_2021, False, "gm", False, False),
|
||||||
|
("gm", GM_CAR.CHEVROLET_BOLT_CC_2018_2021, True, "gm", True, True),
|
||||||
|
("gm", GM_CAR.CHEVROLET_BOLT_CC_2018_2021, True, "", True, True),
|
||||||
|
("gm", "unknown GM", True, "gm", True, False),
|
||||||
|
("mock", "mock", True, "", True, False),
|
||||||
|
("ford", FORD_CAR.FORD_MUSTANG_MACH_E_MK1, True, "", False, True),
|
||||||
|
("gm", "", "corrupt", "", False, False),
|
||||||
|
], ids=["missing-gm", "missing-mock", "missing-ford", "stale-live-cp", "persisted-gm",
|
||||||
|
"empty-live-cp", "unknown-gm", "non-car-gps", "vehicle-gps", "corrupt-cp"])
|
||||||
|
def test_ublox_startup(monkeypatch, brand, fingerprint, persisted, live_brand, expected, car_gps):
|
||||||
|
monkeypatch.setattr(process_config, "ublox_available", lambda: True)
|
||||||
|
current = car.CarParams.new_message(brand=brand, carFingerprint=fingerprint)
|
||||||
|
params = GpsParams(current if persisted is True else None)
|
||||||
|
if persisted == "corrupt":
|
||||||
|
params.values["CarParams"] = b"invalid_corrupt_data"
|
||||||
|
live_cp = car.CarParams.new_message(brand=live_brand, carFingerprint=fingerprint if live_brand else "")
|
||||||
|
|
||||||
assert not ublox(True, params, car.CarParams.new_message(), SimpleNamespace())
|
assert ublox(True, params, live_cp, SimpleNamespace()) is expected
|
||||||
assert params.get_bool("UbloxAvailable")
|
assert params.get_bool("UbloxAvailable")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("car_gps,expected", [(False, True), (True, False)])
|
|
||||||
def test_ublox_has_single_external_gps_publisher(monkeypatch, car_gps, expected):
|
|
||||||
monkeypatch.setattr("openpilot.system.manager.process_config.ublox_available", lambda: True)
|
|
||||||
CP = car.CarParams.new_message()
|
|
||||||
if car_gps:
|
|
||||||
CP.brand = "ford"
|
|
||||||
CP.carFingerprint = FORD_CAR.FORD_MUSTANG_MACH_E_MK1
|
|
||||||
else:
|
|
||||||
CP.brand = "mock"
|
|
||||||
CP.carFingerprint = "mock"
|
|
||||||
params = GpsParams(CP)
|
|
||||||
|
|
||||||
assert ublox(True, params, car.CarParams.new_message(), SimpleNamespace()) is expected
|
|
||||||
assert params.get_bool("CarGpsAvailable") is car_gps
|
assert params.get_bool("CarGpsAvailable") is car_gps
|
||||||
|
assert not ublox(False, params, live_cp, SimpleNamespace())
|
||||||
|
if persisted is False and fingerprint:
|
||||||
|
params.values["CarParams"] = current.to_bytes()
|
||||||
|
assert ublox(True, params, live_cp, SimpleNamespace()) # Same stale live CP, now with current persisted CP.
|
||||||
|
monkeypatch.setattr(process_config, "ublox_available", lambda: False)
|
||||||
|
assert not ublox(True, params, live_cp, SimpleNamespace())
|
||||||
|
|||||||
+3
-1
@@ -63,7 +63,8 @@ def main() -> NoReturn:
|
|||||||
gps_location_service = get_gps_location_service(params)
|
gps_location_service = get_gps_location_service(params)
|
||||||
|
|
||||||
pm = messaging.PubMaster(['clocks'])
|
pm = messaging.PubMaster(['clocks'])
|
||||||
sm = messaging.SubMaster([gps_location_service])
|
# Subscribe to both services to support dynamic GM GPS routing.
|
||||||
|
sm = messaging.SubMaster(['gpsLocation', 'gpsLocationExternal'])
|
||||||
|
|
||||||
# StarPilot variables
|
# StarPilot variables
|
||||||
tf = TimezoneFinder() if TimezoneFinder is not None else None
|
tf = TimezoneFinder() if TimezoneFinder is not None else None
|
||||||
@@ -75,6 +76,7 @@ def main() -> NoReturn:
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
sm.update(1000)
|
sm.update(1000)
|
||||||
|
gps_location_service = get_gps_location_service(params)
|
||||||
|
|
||||||
msg = messaging.new_message('clocks')
|
msg = messaging.new_message('clocks')
|
||||||
msg.valid = system_time_valid()
|
msg.valid = system_time_valid()
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cereal import car, messaging
|
||||||
|
from openpilot.system.ubloxd import ubloxd
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("brand,service", [("gm", "gpsLocation"), ("mock", "gpsLocationExternal")])
|
||||||
|
def test_main_routes_every_parsed_fix(monkeypatch, brand, service):
|
||||||
|
cp = car.CarParams.new_message(brand=brand)
|
||||||
|
monkeypatch.setattr(ubloxd, "Params", lambda: SimpleNamespace(get=lambda key: cp.to_bytes()))
|
||||||
|
sent = []
|
||||||
|
pm = SimpleNamespace(send=lambda service, message: sent.append((service, message)))
|
||||||
|
monkeypatch.setattr(messaging, "PubMaster", lambda services: pm)
|
||||||
|
monkeypatch.setattr(messaging, "sub_sock", lambda *args, **kwargs: None)
|
||||||
|
# Several fixes in one receive cycle must all reach the publisher.
|
||||||
|
incoming = iter([SimpleNamespace(ubloxRaw=b"", logMonoTime=1)])
|
||||||
|
monkeypatch.setattr(messaging, "recv_one", lambda sock: next(incoming))
|
||||||
|
parser = ubloxd.UbloxMsgParser(service)
|
||||||
|
fixes = [messaging.new_message(service, valid=True) for _ in range(5)]
|
||||||
|
monkeypatch.setattr(parser.framer, "add_data", lambda *args: range(5))
|
||||||
|
monkeypatch.setattr(parser, "parse_frame", lambda index: (parser.gps_service, fixes[index]))
|
||||||
|
def make_parser(actual_service):
|
||||||
|
assert actual_service == service
|
||||||
|
return parser
|
||||||
|
monkeypatch.setattr(ubloxd, "UbloxMsgParser", make_parser)
|
||||||
|
with pytest.raises(StopIteration):
|
||||||
|
ubloxd.main()
|
||||||
|
assert sent == [(service, fix) for fix in fixes]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user