mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-08 17:13:45 +08:00
Compare commits
3 Commits
bluescreensonly3
...
Dom
| Author | SHA1 | Date | |
|---|---|---|---|
| b91ea3e1da | |||
| 1588f7041a | |||
| bcf152e6f7 |
Binary file not shown.
@@ -18,6 +18,7 @@ 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}},
|
||||||
|
|||||||
Binary file not shown.
@@ -852,7 +852,6 @@ 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
|
||||||
|
|||||||
@@ -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.0667 # GM radar runs at 15Hz instead of the standard 20Hz
|
ret.radarTimeStepDEPRECATED = 0.15 if candidate == CAR.BUICK_LACROSSE else 0.0667
|
||||||
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 = 27 * CV.MPH_TO_MS
|
ret.minSteerSpeed = 28 * 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
|
||||||
|
|||||||
@@ -205,6 +205,33 @@ 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,
|
||||||
@@ -291,6 +318,14 @@ 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),
|
||||||
|
|||||||
@@ -860,7 +860,9 @@ 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_longitudinal_active = longitudinal_active if self.CP.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN else self.CP.openpilotLongitudinalControl
|
lfa_status_cars = (CAR.HYUNDAI_IONIQ_6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN)
|
||||||
|
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 \
|
||||||
@@ -890,7 +892,8 @@ 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 and angle_lkas_alt and (
|
forward_stock_lkas = (self.CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR or
|
||||||
|
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,10 +2484,11 @@ 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 = False
|
CP.openpilotLongitudinalControl = True
|
||||||
|
|
||||||
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 = {
|
||||||
@@ -2529,7 +2530,6 @@ 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,13 +2537,12 @@ 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 == [("LKAS", can_bus.ACAN)]
|
assert steering_names == [("LFA", can_bus.ECAN), ("LKAS", can_bus.ACAN)]
|
||||||
|
|
||||||
controller.frame = 1
|
controller.frame = 1
|
||||||
cc.longActive = True
|
cc.longActive = True
|
||||||
@@ -2708,7 +2707,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_keeps_inactive_status_in_drive(self, standstill):
|
def test_sportage_angle_lkas_alt_forwards_stock_status_when_inactive(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 |
|
||||||
@@ -2716,60 +2715,16 @@ 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=stock_lkas,
|
cs = SimpleNamespace(stock_lfa_msg=None, stock_lkas_msg={},
|
||||||
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)
|
||||||
lkas_msgs = [msg for msg in msgs if msg[0] == 0x110]
|
assert not [msg for msg in msgs if msg[0] in (0x110, 0x12A)]
|
||||||
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()
|
||||||
|
|||||||
@@ -69,10 +69,6 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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.gm_cc_only or self.redneck_non_pcm)
|
remembered_resume = resume_prev_button and self._uses_software_cruise()
|
||||||
|
|
||||||
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,6 +313,22 @@ 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
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ 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.16
|
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_MAX = 0.22
|
||||||
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
|
||||||
@@ -284,7 +284,7 @@ GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_LAT_CUTOFF = 1.25
|
|||||||
GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_LAT_CUTOFF_WIDTH = 0.25
|
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.22
|
GENESIS_G70_CENTER_OUTPUT_TAPER_MAX = 0.26
|
||||||
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
|
||||||
|
|||||||
@@ -397,6 +397,27 @@ 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
|
||||||
@@ -433,6 +454,11 @@ 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
|
||||||
@@ -3069,6 +3095,28 @@ 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,6 +7,7 @@ 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
|
||||||
@@ -42,6 +43,11 @@ 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
|
||||||
@@ -636,8 +642,9 @@ 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_valid=['starpilotPlan'])
|
ignore_avg_freq=ignore_avg_freq, 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)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ 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,
|
||||||
)
|
)
|
||||||
@@ -96,6 +97,15 @@ 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
|
||||||
|
|||||||
@@ -18,7 +18,13 @@ 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 LongitudinalPlanner, get_coast_accel, get_vehicle_min_accel, should_publish_planner_fcw
|
from openpilot.selfdrive.controls.lib.longitudinal_planner import (
|
||||||
|
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,
|
||||||
@@ -310,6 +316,24 @@ 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)
|
||||||
|
|||||||
@@ -201,6 +201,8 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -339,6 +341,8 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1016,6 +1020,8 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1028,6 +1034,8 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1113,6 +1121,8 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1690,6 +1700,10 @@
|
|||||||
"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,
|
||||||
"parent_key": "QOLLongitudinal",
|
"parent_key": "QOLLongitudinal",
|
||||||
"settings_tier": "simple"
|
"settings_tier": "simple"
|
||||||
},
|
},
|
||||||
@@ -1701,6 +1715,10 @@
|
|||||||
"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,
|
||||||
"parent_key": "QOLLongitudinal",
|
"parent_key": "QOLLongitudinal",
|
||||||
"settings_tier": "simple"
|
"settings_tier": "simple"
|
||||||
},
|
},
|
||||||
@@ -1774,6 +1792,10 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1787,6 +1809,8 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -2223,6 +2247,12 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -2234,6 +2264,12 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -2245,6 +2281,12 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -2256,6 +2298,12 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -2267,6 +2315,12 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -2278,6 +2332,12 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -2289,6 +2349,12 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -2341,6 +2407,8 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -2353,6 +2421,8 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -2385,6 +2455,8 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -2454,6 +2526,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -4818,6 +4891,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ AUDIO_TEST_HOLD_TIME = 3.0
|
|||||||
RECONNECT_INTERVAL_SECONDS = 15.0
|
RECONNECT_INTERVAL_SECONDS = 15.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:
|
||||||
@@ -36,6 +37,9 @@ 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
|
||||||
@@ -272,17 +276,65 @@ 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_disconnected.clear()
|
||||||
|
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_disconnected.clear()
|
||||||
|
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_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:
|
if self._pairing_address or now - self._last_reconnect < RECONNECT_INTERVAL_SECONDS:
|
||||||
continue
|
continue
|
||||||
self._last_reconnect = now
|
self._last_reconnect = now
|
||||||
@@ -298,6 +350,8 @@ class BluetoothController:
|
|||||||
self._reconnect_backoff.pop(address, None)
|
self._reconnect_backoff.pop(address, None)
|
||||||
for device in candidates:
|
for device in candidates:
|
||||||
if device["audio"] or device["controller"]:
|
if device["audio"] or device["controller"]:
|
||||||
|
if suspend_controller_reconnect and device["controller"]:
|
||||||
|
continue
|
||||||
address = device["address"].upper()
|
address = device["address"].upper()
|
||||||
if now < self._manual_disconnect_until.get(address, 0.0):
|
if now < self._manual_disconnect_until.get(address, 0.0):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -432,6 +432,48 @@ 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)
|
||||||
|
controller = BluetoothController(params, FakeBlueZ, FakeRadio())
|
||||||
|
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
|
||||||
|
|
||||||
|
assert not controller._maintain_controller_offroad_policy({"offroad": False, "devices": []}, 220.0)
|
||||||
|
assert controller._offroad_since is None
|
||||||
|
assert controller._policy_disconnected == set()
|
||||||
|
assert controller._policy_disconnect_retry_after == {}
|
||||||
|
assert address not in controller._reconnect_backoff
|
||||||
|
assert controller._last_reconnect == 0.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()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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-2"
|
import { WheelControls } from "/assets/components/tools/wheel_controls.js?v=controllers-3"
|
||||||
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"
|
||||||
|
|||||||
@@ -342,6 +342,24 @@
|
|||||||
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,4 +1,5 @@
|
|||||||
import { html, reactive } from "/assets/vendor/arrow-core.js"
|
import { html, reactive } from "/assets/vendor/arrow-core.js"
|
||||||
|
import { formatNumericParamValue, resolveVehicleUnitParam, vehicleSpeedUnit } from "/assets/mobile/js/params.js"
|
||||||
|
|
||||||
const endpointOptionsCache = {}
|
const endpointOptionsCache = {}
|
||||||
const endpointOptionsInflight = {}
|
const endpointOptionsInflight = {}
|
||||||
@@ -448,40 +449,6 @@ 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)
|
||||||
@@ -505,6 +472,7 @@ 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),
|
||||||
@@ -952,13 +920,7 @@ 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
|
||||||
|
|
||||||
const bounds = numericBounds(param)
|
displayEl.textContent = formatNumericParamValue(param, rawValue, state.values)
|
||||||
displayEl.textContent = formatSliderValue(
|
|
||||||
rawValue,
|
|
||||||
String(bounds.step),
|
|
||||||
param.precision,
|
|
||||||
param.key,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateNumericParam(param, numericValue, options = {}) {
|
async function updateNumericParam(param, numericValue, options = {}) {
|
||||||
@@ -1311,10 +1273,9 @@ 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 label = String(p.label || "").toLowerCase()
|
const displayParam = resolveVehicleUnitParam(p, state.values)
|
||||||
const key = String(p.key || "").toLowerCase()
|
return [displayParam.label, displayParam.key, displayParam.description, displayParam.unit, displayParam.unit_search_terms]
|
||||||
const description = String(p.description || "").toLowerCase()
|
.some(value => String(value || "").toLowerCase().includes(q))
|
||||||
return label.includes(q) || key.includes(q) || description.includes(q)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSearchFilter() {
|
function clearSearchFilter() {
|
||||||
@@ -1376,8 +1337,7 @@ 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") {
|
||||||
const bounds = numericBounds(param)
|
return formatNumericParamValue(param, value, state.values)
|
||||||
return formatSliderValue(value, String(bounds.step), param.precision, param.key)
|
|
||||||
}
|
}
|
||||||
return String(value)
|
return String(value)
|
||||||
}
|
}
|
||||||
@@ -1562,6 +1522,8 @@ 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"
|
||||||
@@ -1604,8 +1566,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>${formatSliderValue(numericBounds(p).min, String(numericBounds(p).step), p.precision, p.key)}</span>
|
<span>${formatNumericParamValue(p, numericBounds(p).min, state.values)}</span>
|
||||||
<span>${formatSliderValue(numericBounds(p).max, String(numericBounds(p).step), p.precision, p.key)}</span>
|
<span>${formatNumericParamValue(p, numericBounds(p).max, state.values)}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="ds-reset-btn"
|
class="ds-reset-btn"
|
||||||
@@ -1631,10 +1593,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
|
||||||
? formatSliderValue(defaultNumeric, String(bounds.step), p.precision, p.key)
|
? formatNumericParamValue(p, defaultNumeric, state.values)
|
||||||
: "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)
|
const stepLabel = p.key === "DeviceShutdown" ? "1 hour" : `${formatStepValue(bounds.step, precision)}${p.unit || ""}`
|
||||||
return html`
|
return html`
|
||||||
<div class="ds-stepper">
|
<div class="ds-stepper">
|
||||||
<button
|
<button
|
||||||
@@ -1642,7 +1604,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>${formatSliderValue(bounds.min, String(bounds.step), p.precision, p.key)} to ${formatSliderValue(bounds.max, String(bounds.step), p.precision, p.key)}</span>
|
<span>${formatNumericParamValue(p, bounds.min, state.values)} to ${formatNumericParamValue(p, bounds.max, state.values)}</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">
|
||||||
@@ -1790,8 +1752,7 @@ 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]
|
||||||
const bounds = numericBounds(p)
|
return currentValue !== undefined ? formatNumericParamValue(p, currentValue, state.values) : ".."
|
||||||
return currentValue !== undefined ? formatSliderValue(currentValue, String(bounds.step), p.precision, p.key) : ".."
|
|
||||||
}}</span>` : ""}
|
}}</span>` : ""}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1857,6 +1818,11 @@ export function DeviceSettings({ params }) {
|
|||||||
<div class="ds-wrapper">
|
<div class="ds-wrapper">
|
||||||
<h2>Toggles</h2>
|
<h2>Toggles</h2>
|
||||||
|
|
||||||
|
<div class="ds-unit-note">
|
||||||
|
<i class="bi bi-speedometer2"></i>
|
||||||
|
<span>Vehicle-unit speed settings use <strong>${() => vehicleSpeedUnit(state.values)}</strong> and follow the comma's <em>Use Metric System</em> toggle. Each control shows its adjustment step.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="ds-search-row">
|
<div class="ds-search-row">
|
||||||
<input
|
<input
|
||||||
class="ds-search"
|
class="ds-search"
|
||||||
|
|||||||
@@ -37,13 +37,44 @@
|
|||||||
.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,6 +11,7 @@ const state = reactive({
|
|||||||
slots: [],
|
slots: [],
|
||||||
controllerSlots: [],
|
controllerSlots: [],
|
||||||
controllerOptions: [],
|
controllerOptions: [],
|
||||||
|
disconnectControllersOffroad: false,
|
||||||
speedUnit: "mph",
|
speedUnit: "mph",
|
||||||
speedMinimum: 5,
|
speedMinimum: 5,
|
||||||
speedMaximum: 90,
|
speedMaximum: 90,
|
||||||
@@ -37,6 +38,7 @@ 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)
|
||||||
@@ -265,6 +267,16 @@ 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>
|
||||||
|
|||||||
@@ -592,6 +592,24 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
[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);
|
||||||
}
|
}
|
||||||
@@ -749,6 +767,13 @@ 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;
|
||||||
@@ -1497,4 +1522,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); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { api, showSnackbar } from "../api.js"
|
import { api, showSnackbar } from "../api.js"
|
||||||
import {
|
import {
|
||||||
coerceValueByType, formatSliderValue, formatReadoutValue, getColorDefault,
|
coerceValueByType, formatNumericParamValue, formatReadoutValue, getColorDefault,
|
||||||
normalizeHexColor, numericBounds, numericEpsilon, snapNumericToBoundsAndStep,
|
normalizeHexColor, numericBounds, numericEpsilon, snapNumericToBoundsAndStep,
|
||||||
stepPrecision,
|
resolveVehicleUnitParam, stepPrecision,
|
||||||
} from "../params.js"
|
} from "../params.js"
|
||||||
import { FavoritesEditor } from "./FavoritesEditor.js"
|
import { FavoritesEditor } from "./FavoritesEditor.js"
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ 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 },
|
||||||
@@ -28,8 +29,9 @@ export const GalaxyToggleCard = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bounds() { return numericBounds(this.param, {}) },
|
displayParam() { return resolveVehicleUnitParam(this.param, this.values) },
|
||||||
precision() { return stepPrecision(this.bounds.step, this.param.precision) },
|
bounds() { return numericBounds(this.displayParam, this.values) },
|
||||||
|
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" },
|
||||||
@@ -38,11 +40,17 @@ 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.param, this.value)
|
if (this.isReadout) return formatReadoutValue(this.displayParam, this.value)
|
||||||
return this.value !== undefined && this.value !== null ? formatSliderValue(this.value, String(this.bounds.step), this.param.precision, this.param.key) : ".."
|
return this.value !== undefined && this.value !== null ? formatNumericParamValue(this.displayParam, this.value, this.values) : ".."
|
||||||
},
|
},
|
||||||
sliderDisplay() {
|
sliderDisplay() {
|
||||||
return this.value !== undefined ? formatSliderValue(this.currentValue, String(this.bounds.step), this.param.precision, this.param.key) : ".."
|
return this.value !== undefined ? formatNumericParamValue(this.displayParam, this.currentValue, this.values) : ".."
|
||||||
|
},
|
||||||
|
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" },
|
||||||
@@ -167,10 +175,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">{{ param.label }}
|
<span class="gx-row__label">{{ displayParam.label }}
|
||||||
<span v-if="param.settings_tier === 'advanced'" class="gx-chip gx-chip--advanced">Advanced</span>
|
<span v-if="displayParam.settings_tier === 'advanced'" class="gx-chip gx-chip--advanced">Advanced</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="param.description" class="gx-row__desc">{{ param.description }}</span>
|
<span v-if="displayParam.description" class="gx-row__desc">{{ displayParam.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>
|
||||||
|
|
||||||
@@ -190,6 +198,10 @@ 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, slugifySectionName, applyParamChange } from "../params.js"
|
import { isSettingVisible, resolveVehicleUnitParam, 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,7 +35,9 @@ 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()
|
||||||
return [p.label, p.key, p.description].some((v) => String(v || "").toLowerCase().includes(q))
|
const displayParam = resolveVehicleUnitParam(p, this.values)
|
||||||
|
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]" :locked="lockReason(p) !== ''"
|
<GalaxyToggleCard :param="p" :value="values[p.key]" :values="values" :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>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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: "",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -29,6 +30,7 @@ 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
|
||||||
@@ -99,6 +101,18 @@ 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,4 +1,45 @@
|
|||||||
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"])
|
||||||
@@ -88,7 +129,8 @@ 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),
|
||||||
@@ -194,6 +236,13 @@ 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)
|
||||||
|
|||||||
@@ -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,
|
||||||
slugifySectionName,
|
resolveVehicleUnitParam, slugifySectionName, vehicleSpeedUnit,
|
||||||
} 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"
|
||||||
@@ -39,6 +39,7 @@ export const Settings = {
|
|||||||
return this.sections.find((s) => s.slug === this.activeSectionSlug) || this.sections[0]
|
return this.sections.find((s) => s.slug === this.activeSectionSlug) || this.sections[0]
|
||||||
},
|
},
|
||||||
hiddenAdvancedCount() { return countAdvancedHiddenByDeveloperMode(this.layout, this.values) },
|
hiddenAdvancedCount() { return countAdvancedHiddenByDeveloperMode(this.layout, this.values) },
|
||||||
|
speedUnit() { return vehicleSpeedUnit(this.values) },
|
||||||
searchActive() { return !!this.searchTerm },
|
searchActive() { return !!this.searchTerm },
|
||||||
searchTerm: {
|
searchTerm: {
|
||||||
get() { return store.search },
|
get() { return store.search },
|
||||||
@@ -82,7 +83,9 @@ 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()
|
||||||
return [p.label, p.key, p.description].some((v) => String(v || "").toLowerCase().includes(q))
|
const displayParam = resolveVehicleUnitParam(p, this.values)
|
||||||
|
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)
|
||||||
@@ -117,6 +120,11 @@ export const Settings = {
|
|||||||
<div>
|
<div>
|
||||||
<h2 style="margin-top:0;">Toggles</h2>
|
<h2 style="margin-top:0;">Toggles</h2>
|
||||||
|
|
||||||
|
<div class="gx-unit-note">
|
||||||
|
<i class="bi bi-speedometer2"></i>
|
||||||
|
<span>Vehicle-unit speed settings use <strong>{{ speedUnit }}</strong> and follow the comma's <em>Use Metric System</em> toggle. Each control shows its adjustment step.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DevModeBanner :hidden-count="hiddenAdvancedCount" :dev-mode-on="devModeOn" />
|
<DevModeBanner :hidden-count="hiddenAdvancedCount" :dev-mode-on="devModeOn" />
|
||||||
|
|
||||||
<div v-if="loading" class="gx-loading">Loading configuration...</div>
|
<div v-if="loading" class="gx-loading">Loading configuration...</div>
|
||||||
@@ -132,7 +140,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]" :locked="lockReason(p) !== ''"
|
<GalaxyToggleCard :param="p" :value="values[p.key]" :values="values" :locked="lockReason(p) !== ''"
|
||||||
@change="onParamChange" />
|
@change="onParamChange" />
|
||||||
</template>
|
</template>
|
||||||
</GalaxySection>
|
</GalaxySection>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Big Dipper",
|
"name": "Galaxy",
|
||||||
"short_name": "Big Dipper",
|
"short_name": "Galaxy",
|
||||||
"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,7 +8,6 @@
|
|||||||
{ "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",
|
||||||
|
|||||||
@@ -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-2">
|
<link rel="stylesheet" href="/assets/components/tools/wheel_controls.css?v=controllers-3">
|
||||||
<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-7").catch((err) => {
|
import("/assets/components/router.js?v=router-cycle-fix-8").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");
|
||||||
|
|||||||
@@ -46,6 +46,18 @@ 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 "vehicleSpeedUnit(state.values)" in source
|
||||||
|
assert "unit_search_terms" in source
|
||||||
|
assert "Use Metric System" in source
|
||||||
|
assert "per click" 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()
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,41 @@ 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_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,6 +7,7 @@ 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"
|
||||||
|
|
||||||
@@ -23,7 +24,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-7" in index
|
assert "/assets/components/router.js?v=router-cycle-fix-8" in index
|
||||||
|
|
||||||
|
|
||||||
def test_bluetooth_actions_use_reactive_disabled_bindings():
|
def test_bluetooth_actions_use_reactive_disabled_bindings():
|
||||||
@@ -76,6 +77,16 @@ 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")
|
||||||
|
|
||||||
|
|||||||
@@ -231,6 +231,25 @@ 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,6 +62,26 @@ 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")
|
||||||
|
|||||||
@@ -190,6 +190,23 @@ 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 "Use Metric System" 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")
|
||||||
@@ -343,7 +360,7 @@ def test_ui_manifest_is_valid_pwa_manifest():
|
|||||||
assert manifest["display"] == "standalone"
|
assert manifest["display"] == "standalone"
|
||||||
assert manifest["name"]
|
assert manifest["name"]
|
||||||
assert manifest["icons"]
|
assert manifest["icons"]
|
||||||
assert manifest["start_url"] == "/mobile/"
|
assert "start_url" not in manifest
|
||||||
|
|
||||||
|
|
||||||
def test_ui_ported_classic_tools_native_no_embed():
|
def test_ui_ported_classic_tools_native_no_embed():
|
||||||
@@ -505,6 +522,23 @@ 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,6 +5015,7 @@ 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",
|
||||||
@@ -5181,6 +5182,7 @@ 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"
|
||||||
@@ -5190,13 +5192,16 @@ 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"}:
|
if operation not in {"action", "learn", "cancel", "delete", "clear", "test", "test-stop", "joystick", "offroad-disconnect"}:
|
||||||
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()
|
||||||
@@ -5296,6 +5301,27 @@ 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": "Big Dipper manifest not found"}), 404
|
||||||
|
|
||||||
|
try:
|
||||||
|
manifest_data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, ValueError, json.JSONDecodeError):
|
||||||
|
return jsonify({"error": "Big Dipper 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():
|
||||||
|
|||||||
Reference in New Issue
Block a user