mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-11 02:33:51 +08:00
Blueberry Pancakes
This commit is contained in:
@@ -18,6 +18,7 @@ from opendbc.car.gm.values import (
|
||||
AccState,
|
||||
CanBus,
|
||||
CruiseButtons,
|
||||
GM_AUTO_HOLD_CARS,
|
||||
GMFlags,
|
||||
SDGM_CAR,
|
||||
STEER_THRESHOLD,
|
||||
@@ -68,6 +69,18 @@ def update_auto_hold_drive_timers(in_drive_for_hold: bool, moving_for_hold: bool
|
||||
return auto_hold_drive_time, one_pedal_drive_time
|
||||
|
||||
|
||||
def is_gm_auto_hold_active(car_fingerprint: str, auto_hold_engaged: bool, in_drive_for_hold: bool,
|
||||
cruise_available: bool, standstill: bool, gas_pressed: bool) -> bool:
|
||||
return (
|
||||
auto_hold_engaged and
|
||||
car_fingerprint in GM_AUTO_HOLD_CARS and
|
||||
in_drive_for_hold and
|
||||
cruise_available and
|
||||
standstill and
|
||||
not gas_pressed
|
||||
)
|
||||
|
||||
|
||||
def update_startup_acc_fault_suppression(car_fingerprint: str, system_power_mode: int,
|
||||
previous_system_power_mode: int, timer: float,
|
||||
acc_state: int, friction_brake_unavailable: bool) -> tuple[float, bool]:
|
||||
@@ -431,6 +444,11 @@ class CarState(CarStateBase):
|
||||
self.auto_hold_fault_suppression_timer = max(self.auto_hold_fault_suppression_timer - DT_CTRL, 0.0)
|
||||
ret.accFaulted = False
|
||||
|
||||
ret.brakeHoldActive = is_gm_auto_hold_active(
|
||||
self.CP.carFingerprint, self.auto_hold_engaged, in_drive_for_hold,
|
||||
ret.cruiseState.available, ret.standstill, ret.gasPressed,
|
||||
)
|
||||
|
||||
if self.CP.enableBsm and not sdgm_non_volt:
|
||||
ret.leftBlindspot = pt_cp.vl["BCMBlindSpotMonitor"]["LeftBSM"] == 1
|
||||
ret.rightBlindspot = pt_cp.vl["BCMBlindSpotMonitor"]["RightBSM"] == 1
|
||||
|
||||
@@ -11,6 +11,7 @@ from opendbc.car.gm import gmcan
|
||||
from opendbc.car.gm.carstate import (
|
||||
CarState as GMCarState,
|
||||
get_hard_cruise_buttons,
|
||||
is_gm_auto_hold_active,
|
||||
update_auto_hold_drive_timers,
|
||||
update_startup_acc_fault_suppression,
|
||||
)
|
||||
@@ -210,6 +211,19 @@ class TestBoltGps:
|
||||
|
||||
|
||||
class TestGMCarState:
|
||||
@parameterized.expand([
|
||||
(CAR.BUICK_LACROSSE, True, True, True, True, False, True),
|
||||
(CAR.CHEVROLET_VOLT, True, True, True, True, False, True),
|
||||
(CAR.CHEVROLET_BOLT_CC_2017, True, True, True, True, False, False),
|
||||
(CAR.BUICK_LACROSSE, True, True, True, False, False, False),
|
||||
(CAR.BUICK_LACROSSE, True, True, True, True, True, False),
|
||||
])
|
||||
def test_auto_hold_alert_state_requires_supported_complete_stop(self, car_fingerprint, engaged, in_drive,
|
||||
cruise_available, standstill, gas_pressed, expected):
|
||||
assert is_gm_auto_hold_active(
|
||||
car_fingerprint, engaged, in_drive, cruise_available, standstill, gas_pressed,
|
||||
) is expected
|
||||
|
||||
def test_lacrosse_startup_acc_fault_is_suppressed(self):
|
||||
timer, suppressed = update_startup_acc_fault_suppression(
|
||||
CAR.BUICK_LACROSSE, 2, 0, 0.0, 3, False,
|
||||
|
||||
@@ -16,6 +16,7 @@ from opendbc.car.hyundai.values import HyundaiFlags, HyundaiSafetyFlags, Hyundai
|
||||
from opendbc.car.interfaces import CarControllerBase
|
||||
from opendbc.car.vehicle_model import VehicleModel
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_vehicle_tunes import get_hyundai_canfd_scc_jerk_limits
|
||||
from openpilot.starpilot.common.testing_grounds import testing_ground
|
||||
|
||||
VisualAlert = structs.CarControl.HUDControl.VisualAlert
|
||||
@@ -862,7 +863,6 @@ class CarController(CarControllerBase):
|
||||
longitudinal_active = bool(self.long_active_ecu and getattr(CC, "longActive", False))
|
||||
lfa_status_cars = (
|
||||
CAR.HYUNDAI_IONIQ_6,
|
||||
CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN,
|
||||
CAR.KIA_EV6,
|
||||
)
|
||||
lfa_longitudinal_active = self.CP.openpilotLongitudinalControl \
|
||||
@@ -896,7 +896,8 @@ class CarController(CarControllerBase):
|
||||
if angle_lkas_alt:
|
||||
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)
|
||||
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))
|
||||
)
|
||||
preserve_stock_lfa_status = preserve_stock_canfd_lfa_status(self.CP.carFingerprint)
|
||||
@@ -1023,7 +1024,11 @@ class CarController(CarControllerBase):
|
||||
CC.rightBlinker))
|
||||
if self.frame % 2 == 0:
|
||||
if self.CP.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN:
|
||||
acc_kwargs = {}
|
||||
scc_jerk_limits = get_hyundai_canfd_scc_jerk_limits(self.CP)
|
||||
acc_kwargs = {
|
||||
"jerk_upper": scc_jerk_limits[0],
|
||||
"jerk_lower": scc_jerk_limits[1],
|
||||
}
|
||||
else:
|
||||
lead_visible, lead_distance, lead_rel_speed = self._get_canfd_scc_lead_state(CC, CS, now_nanos)
|
||||
acc_kwargs = {
|
||||
|
||||
@@ -2555,6 +2555,7 @@ class TestHyundaiFingerprint:
|
||||
"DAMP_FACTOR": 100,
|
||||
}
|
||||
cc = SimpleNamespace(enabled=True, latActive=True,
|
||||
longActive=False,
|
||||
actuators=SimpleNamespace(longControlState=LongCtrlState.off),
|
||||
leftBlinker=False, rightBlinker=False,
|
||||
hudControl=SimpleNamespace())
|
||||
@@ -2588,7 +2589,7 @@ class TestHyundaiFingerprint:
|
||||
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
|
||||
if controller.packer.dbc.addr_to_msg[addr].name in ("LFA", "LKAS")]
|
||||
assert steering_names == [("LFA", can_bus.ECAN), ("LKAS", can_bus.ACAN)]
|
||||
assert steering_names == [("LKAS", can_bus.ACAN)]
|
||||
|
||||
controller.frame = 1
|
||||
cc.longActive = True
|
||||
@@ -2755,7 +2756,7 @@ class TestHyundaiFingerprint:
|
||||
assert len([msg for msg in msgs if msg[0] == 0x110]) == expected_lkas_msgs
|
||||
|
||||
@pytest.mark.parametrize("standstill", [False, True])
|
||||
def test_sportage_angle_lkas_alt_publishes_inactive_status(self, standstill):
|
||||
def test_sportage_angle_lkas_alt_forwards_stock_status_when_inactive(self, standstill):
|
||||
CP = CarParams.new_message()
|
||||
CP.carFingerprint = CAR.KIA_SPORTAGE_HEV_2026
|
||||
CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.HYBRID | HyundaiFlags.CANFD_ANGLE_STEERING |
|
||||
@@ -2763,7 +2764,6 @@ class TestHyundaiFingerprint:
|
||||
CP.openpilotLongitudinalControl = False
|
||||
|
||||
controller = CarController(DBC[CP.carFingerprint], CP)
|
||||
can_bus = CanBus(CP)
|
||||
cc = SimpleNamespace(enabled=False, latActive=False,
|
||||
actuators=SimpleNamespace(longControlState=LongCtrlState.off),
|
||||
leftBlinker=False, rightBlinker=False, hudControl=SimpleNamespace())
|
||||
@@ -2773,16 +2773,7 @@ class TestHyundaiFingerprint:
|
||||
|
||||
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)
|
||||
lkas_msgs = [msg for msg in msgs if msg[0] == 0x110]
|
||||
assert len(lkas_msgs) == 1
|
||||
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS_ALT", 0)], can_bus.ACAN)
|
||||
parser.update([(1, lkas_msgs)])
|
||||
assert parser.can_valid
|
||||
assert parser.vl["LKAS_ALT"]["LKA_ICON"] == 1
|
||||
assert parser.vl["LKAS_ALT"]["LKA_SysIndReq"] == 1
|
||||
assert parser.vl["LKAS_ALT"]["LKA_RcgSta"] == 0
|
||||
assert parser.vl["LKAS_ALT"]["LKAS_ANGLE_ACTIVE"] == 1
|
||||
assert not [msg for msg in msgs if msg[0] in (0x110, 0x12A)]
|
||||
|
||||
def test_ev9_inactive_angle_steering_does_not_suppress_stock_lfa(self):
|
||||
CP = CarParams.new_message()
|
||||
|
||||
@@ -222,12 +222,14 @@ class CarController(CarControllerBase):
|
||||
self.angle_reclaim_frames = 0
|
||||
self.angle_reclaim_start_angle = 0.0
|
||||
|
||||
def _angle_manual_handoff(self, CS, lat_active):
|
||||
def _angle_manual_handoff(self, CS, lat_active, use_steering_pressed=False):
|
||||
if not lat_active:
|
||||
self._reset_angle_handoff()
|
||||
return False
|
||||
|
||||
driver_override = self._update_angle_driver_override(CS)
|
||||
if use_steering_pressed:
|
||||
driver_override = driver_override or getattr(CS.out, "steeringPressed", False)
|
||||
if driver_override:
|
||||
self.angle_handoff_active = True
|
||||
self.angle_override_hold_frames = _ANGLE_OVERRIDE_HOLD_FRAMES
|
||||
@@ -325,7 +327,9 @@ class CarController(CarControllerBase):
|
||||
lkas_available = CC.latActive and (not mads_only or mads_only_ok) and \
|
||||
CS.out.gearShifter == structs.CarState.GearShifter.drive and not CS.out.standstill
|
||||
|
||||
manual_handoff = self._angle_manual_handoff(CS, CC.latActive)
|
||||
manual_handoff = self._angle_manual_handoff(
|
||||
CS, CC.latActive, use_steering_pressed=self.CP.carFingerprint == CAR.SUBARU_OUTBACK_2023,
|
||||
)
|
||||
lkas_active = lkas_available and not manual_handoff
|
||||
|
||||
if lkas_active and not self.angle_lkas_active:
|
||||
@@ -408,6 +412,11 @@ class CarController(CarControllerBase):
|
||||
|
||||
return subarucan.create_steering_control(self.packer, apply_torque, apply_steer_req)
|
||||
|
||||
def _lkas_status_active(self, CC):
|
||||
if self.CP.carFingerprint == CAR.SUBARU_OUTBACK_2023:
|
||||
return self.angle_lkas_active
|
||||
return CC.latActive
|
||||
|
||||
def update(self, CC, CS, now_nanos, starpilot_toggles):
|
||||
actuators = CC.actuators
|
||||
hud_control = CC.hudControl
|
||||
@@ -484,7 +493,7 @@ class CarController(CarControllerBase):
|
||||
CC.longActive, hud_control.leadVisible,
|
||||
self.status_bus))
|
||||
|
||||
can_sends.append(subarucan.create_es_lkas_state(self.packer, self.frame // 10, CS.es_lkas_state_msg, CC.latActive, hud_control.visualAlert,
|
||||
can_sends.append(subarucan.create_es_lkas_state(self.packer, self.frame // 10, CS.es_lkas_state_msg, self._lkas_status_active(CC), hud_control.visualAlert,
|
||||
hud_control.leftLaneVisible, hud_control.rightLaneVisible,
|
||||
hud_control.leftLaneDepart, hud_control.rightLaneDepart, self.status_bus))
|
||||
|
||||
|
||||
@@ -744,10 +744,10 @@ def test_ascent_angle_controller_blocks_parking_lot_aol_engagement():
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
|
||||
|
||||
|
||||
def test_lkas_hud_state_uses_lateral_active():
|
||||
def test_lkas_hud_state_uses_outback_angle_request_state():
|
||||
update_source = inspect.getsource(CarController.update)
|
||||
|
||||
assert "create_es_lkas_state(self.packer, self.frame // 10, CS.es_lkas_state_msg, CC.latActive" in update_source
|
||||
assert "create_es_lkas_state(self.packer, self.frame // 10, CS.es_lkas_state_msg, self._lkas_status_active(CC)" in update_source
|
||||
assert "create_es_lkas_state(self.packer, self.frame // 10, CS.es_lkas_state_msg, CC.enabled" not in update_source
|
||||
|
||||
|
||||
@@ -765,3 +765,38 @@ def test_lkas_hud_active_bit_follows_lateral_state(enabled, expected):
|
||||
|
||||
assert parser.can_valid
|
||||
assert parser.vl["ES_LKAS_State"]["LKAS_ACTIVE"] == expected
|
||||
|
||||
|
||||
def test_outback_manual_steering_releases_angle_request_before_lkas_fault():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_OUTBACK_2023)
|
||||
controller = CarController({}, CP)
|
||||
CC = SimpleNamespace(
|
||||
enabled=False,
|
||||
latActive=True,
|
||||
actuators=SimpleNamespace(steeringAngleDeg=-225.0),
|
||||
)
|
||||
CS = SimpleNamespace(out=SimpleNamespace(
|
||||
vEgoRaw=0.9,
|
||||
steeringAngleDeg=-57.0,
|
||||
steeringRateDeg=-45.0,
|
||||
steeringTorque=-127.0,
|
||||
steeringPressed=True,
|
||||
gearShifter=structs.CarState.GearShifter.drive,
|
||||
standstill=False,
|
||||
))
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("ES_LKAS_ANGLE", 0)], CanBus.main)
|
||||
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(1, [msg])])
|
||||
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
|
||||
assert not controller._lkas_status_active(CC)
|
||||
|
||||
|
||||
def test_other_angle_cars_keep_lateral_status_behavior():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_CROSSTREK_2025)
|
||||
controller = CarController({}, CP)
|
||||
controller.angle_lkas_active = False
|
||||
|
||||
assert controller._lkas_status_active(SimpleNamespace(latActive=True))
|
||||
|
||||
@@ -60,6 +60,7 @@ static bool gm_panda_3d1_sched = false;
|
||||
static bool gm_panda_paddle_sched = false;
|
||||
static bool gm_bolt_2022_pedal = false;
|
||||
static bool gm_alt_brake = false;
|
||||
static bool gm_volt_cc_gateway = false;
|
||||
static bool gm_volt_auto_hold = false;
|
||||
static bool gm_volt_one_pedal = false;
|
||||
|
||||
@@ -261,7 +262,8 @@ static void gm_rx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
if ((msg->addr == 0xF1U) && gm_alt_brake) {
|
||||
brake_pressed = msg->data[1] >= 6U;
|
||||
const uint8_t brake_threshold = gm_volt_cc_gateway ? 21U : 6U;
|
||||
brake_pressed = msg->data[1] >= brake_threshold;
|
||||
}
|
||||
|
||||
if ((msg->addr == 0xC9U) && (gm_hw == GM_CAM) && !gm_force_brake_c9) {
|
||||
@@ -720,7 +722,7 @@ static safety_config gm_init(uint16_t param) {
|
||||
gm_cc_long = GET_FLAG(param, GM_PARAM_CC_LONG);
|
||||
gm_has_acc = !GET_FLAG(param, GM_PARAM_NO_ACC);
|
||||
gm_pedal_long = GET_FLAG(param, GM_PARAM_PEDAL_LONG);
|
||||
const bool gm_volt_cc_gateway = GET_FLAG(param, GM_PARAM_VOLT_CC_GATEWAY) && gm_no_camera && !gm_pedal_long && !gm_has_acc;
|
||||
gm_volt_cc_gateway = GET_FLAG(param, GM_PARAM_VOLT_CC_GATEWAY) && gm_no_camera && !gm_pedal_long && !gm_has_acc;
|
||||
enable_gas_interceptor = GET_FLAG(param, GM_PARAM_PEDAL_INTERCEPTOR);
|
||||
gm_force_ascm = GET_FLAG(param, GM_PARAM_HW_ASCM_LONG);
|
||||
gm_force_brake_c9 = GET_FLAG(param, GM_PARAM_FORCE_BRAKE_C9);
|
||||
|
||||
@@ -654,6 +654,31 @@ class TestGmCcLongitudinalNoCameraSafety(TestGmCcLongitudinalSafety):
|
||||
self.safety.init_tests()
|
||||
|
||||
|
||||
def test_gm_volt_cc_gateway_brake_threshold_matches_carstate():
|
||||
safety = libsafety_py.libsafety
|
||||
safety.set_safety_hooks(
|
||||
CarParams.SafetyModel.gm,
|
||||
GMSafetyFlags.FLAG_GM_NO_CAMERA |
|
||||
GMSafetyFlags.FLAG_GM_NO_ACC |
|
||||
GMSafetyFlags.FLAG_GM_CC_LONG |
|
||||
GMSafetyFlags.FLAG_GM_VOLT_CC_GATEWAY,
|
||||
)
|
||||
safety.init_tests()
|
||||
safety.set_controls_allowed(True)
|
||||
|
||||
cruise = common.make_msg(0, 0x3D1, 8, bytes([0, 0, 0, 0, 0x80, 0, 0, 0]))
|
||||
safety.safety_rx_hook(cruise)
|
||||
assert safety.get_controls_allowed()
|
||||
|
||||
noisy_brake = libsafety_py.make_CANPacket(0xF1, 0, b"\x34\x06\x05\x40\x00\x00")
|
||||
safety.safety_rx_hook(noisy_brake)
|
||||
assert safety.get_controls_allowed()
|
||||
|
||||
pressed_brake = libsafety_py.make_CANPacket(0xF1, 0, b"\x34\x15\x05\x40\x00\x00")
|
||||
safety.safety_rx_hook(pressed_brake)
|
||||
assert not safety.get_controls_allowed()
|
||||
|
||||
|
||||
class TestGmCcLongitudinalPandaSchedSafety(TestGmCcLongitudinalSafety):
|
||||
FWD_BLACKLISTED_ADDRS = {2: [0x180, 0x370], 0: [0x184, 0x3D1]}
|
||||
INTERCEPTOR_GAS_PRESSED = 596
|
||||
|
||||
@@ -249,6 +249,19 @@ void ignition_can_hook(CANPacket_t *msg) {
|
||||
ignition_can_cnt = 0U;
|
||||
}
|
||||
|
||||
// Volkswagen MEB exception
|
||||
if ((msg->addr == 0x3C0U) && (len == 4)) {
|
||||
int counter = msg->data[1] & 0xFU;
|
||||
|
||||
static int prev_counter_vw_meb = -1;
|
||||
if ((counter == ((prev_counter_vw_meb + 1) % 16)) && (prev_counter_vw_meb != -1)) {
|
||||
// Klemmen_Status_01->ZAS_Kl_15
|
||||
ignition_can = ((msg->data[2] >> 1) & 1U) != 0U;
|
||||
ignition_can_cnt = 0U;
|
||||
}
|
||||
prev_counter_vw_meb = counter;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,12 @@ typedef struct {
|
||||
|
||||
ffi.cdef("""
|
||||
int set_safety_hooks(uint16_t mode, uint16_t param);
|
||||
|
||||
void ignition_can_hook(CANPacket_t *msg);
|
||||
void set_ignition_can_for_test(bool enabled);
|
||||
bool get_ignition_can_for_test(void);
|
||||
void set_ignition_can_cnt_for_test(uint32_t cnt);
|
||||
uint32_t get_ignition_can_cnt_for_test(void);
|
||||
""")
|
||||
|
||||
ffi.cdef("""
|
||||
|
||||
@@ -24,5 +24,21 @@ can_ring *tx1_q = &can_tx1_q;
|
||||
can_ring *tx2_q = &can_tx2_q;
|
||||
can_ring *tx3_q = &can_tx3_q;
|
||||
|
||||
void set_ignition_can_for_test(bool enabled) {
|
||||
ignition_can = enabled;
|
||||
}
|
||||
|
||||
bool get_ignition_can_for_test(void) {
|
||||
return ignition_can;
|
||||
}
|
||||
|
||||
void set_ignition_can_cnt_for_test(uint32_t cnt) {
|
||||
ignition_can_cnt = cnt;
|
||||
}
|
||||
|
||||
uint32_t get_ignition_can_cnt_for_test(void) {
|
||||
return ignition_can_cnt;
|
||||
}
|
||||
|
||||
#include "comms_definitions.h"
|
||||
#include "can_comms.h"
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
import unittest
|
||||
|
||||
from panda.tests.libpanda import libpanda_py
|
||||
|
||||
|
||||
lpp = libpanda_py.libpanda
|
||||
|
||||
|
||||
def volkswagen_meb_ignition_msg(counter, ignition, bus=0, length=4):
|
||||
dat = bytearray(length)
|
||||
if length >= 2:
|
||||
dat[1] = counter & 0xF
|
||||
if length >= 3:
|
||||
dat[2] = int(ignition) << 1
|
||||
return libpanda_py.make_CANPacket(0x3C0, bus, dat)
|
||||
|
||||
|
||||
class TestIgnitionCanHook(unittest.TestCase):
|
||||
def test_volkswagen_meb_ignition(self):
|
||||
lpp.set_ignition_can_for_test(False)
|
||||
lpp.set_ignition_can_cnt_for_test(123)
|
||||
|
||||
# A single frame is insufficient: the rolling counter must first be established.
|
||||
lpp.ignition_can_hook(volkswagen_meb_ignition_msg(7, True))
|
||||
self.assertFalse(lpp.get_ignition_can_for_test())
|
||||
self.assertEqual(lpp.get_ignition_can_cnt_for_test(), 123)
|
||||
|
||||
lpp.ignition_can_hook(volkswagen_meb_ignition_msg(8, True))
|
||||
self.assertTrue(lpp.get_ignition_can_for_test())
|
||||
self.assertEqual(lpp.get_ignition_can_cnt_for_test(), 0)
|
||||
|
||||
# A duplicate counter must not change ignition state.
|
||||
lpp.set_ignition_can_cnt_for_test(123)
|
||||
lpp.ignition_can_hook(volkswagen_meb_ignition_msg(8, False))
|
||||
self.assertTrue(lpp.get_ignition_can_for_test())
|
||||
self.assertEqual(lpp.get_ignition_can_cnt_for_test(), 123)
|
||||
|
||||
lpp.ignition_can_hook(volkswagen_meb_ignition_msg(9, False))
|
||||
self.assertFalse(lpp.get_ignition_can_for_test())
|
||||
self.assertEqual(lpp.get_ignition_can_cnt_for_test(), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -23,6 +23,8 @@ HONDA_ACCORD_LOW_SPEED_STOP_MAX_LEAD_SPEED = 1.0
|
||||
HONDA_ACCORD_STANDSTILL_GUARD_MAX_EGO_SPEED = 0.25
|
||||
HYUNDAI_ELANTRA_LEAD_FOLLOW_JERK_SCALE = 1.25
|
||||
GENESIS_GV70_ELECTRIFIED_LEAD_FOLLOW_JERK_SCALE = 1.75
|
||||
GENESIS_GV70_ELECTRIFIED_SCC_JERK_UPPER = 1.5
|
||||
GENESIS_GV70_ELECTRIFIED_SCC_JERK_LOWER = 2.0
|
||||
FORD_LIGHTNING_LEAD_FOLLOW_JERK_SCALE = 1.35
|
||||
HONDA_CRV_5G_LEAD_FOLLOW_JERK_SCALE = 1.35
|
||||
GM_SILVERADO_EARLY_FOLLOW_MIN_EGO_SPEED = 18.0
|
||||
@@ -524,6 +526,12 @@ def get_lead_follow_jerk_scale(CP):
|
||||
return 1.0
|
||||
|
||||
|
||||
def get_hyundai_canfd_scc_jerk_limits(CP):
|
||||
if str(getattr(CP, "carFingerprint", "")) == "GENESIS_GV70_ELECTRIFIED_1ST_GEN":
|
||||
return GENESIS_GV70_ELECTRIFIED_SCC_JERK_UPPER, GENESIS_GV70_ELECTRIFIED_SCC_JERK_LOWER
|
||||
return None
|
||||
|
||||
|
||||
def get_honda_accord_lead_departure_tune(CP):
|
||||
if CP.brand == "honda" and str(CP.carFingerprint) == "HONDA_ACCORD":
|
||||
return (
|
||||
|
||||
@@ -5,7 +5,10 @@ from types import SimpleNamespace
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.starpilot.controls.starpilot_planner import StarPilotPlanner, get_force_stop_jerk_scale
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_vehicle_tunes import get_lead_follow_jerk_scale
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_vehicle_tunes import (
|
||||
get_hyundai_canfd_scc_jerk_limits,
|
||||
get_lead_follow_jerk_scale,
|
||||
)
|
||||
import openpilot.starpilot.controls.starpilot_planner as starpilot_planner_module
|
||||
|
||||
|
||||
@@ -48,6 +51,14 @@ def test_lead_follow_jerk_scale_is_platform_specific():
|
||||
assert get_lead_follow_jerk_scale(SimpleNamespace(brand="other", carFingerprint="OTHER_CAR")) == 1.0
|
||||
|
||||
|
||||
def test_genesis_gv70_scc_jerk_limits_are_platform_specific():
|
||||
gv70 = SimpleNamespace(brand="hyundai", carFingerprint="GENESIS_GV70_ELECTRIFIED_1ST_GEN")
|
||||
other = SimpleNamespace(brand="hyundai", carFingerprint="HYUNDAI_IONIQ_6")
|
||||
|
||||
assert get_hyundai_canfd_scc_jerk_limits(gv70) == (1.5, 2.0)
|
||||
assert get_hyundai_canfd_scc_jerk_limits(other) is None
|
||||
|
||||
|
||||
def make_sm(planner, *, frame: int, v_ego: float, left_blinker: bool, right_blinker: bool = False, standstill: bool = False):
|
||||
return FakeSM(frame, {
|
||||
"radarState": SimpleNamespace(
|
||||
|
||||
@@ -265,6 +265,15 @@ def below_steer_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.S
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 1.5)
|
||||
|
||||
|
||||
def brake_hold_alert(CP, *_args) -> Alert:
|
||||
alert_text = "Car in Auto Hold mode" if CP.brand == "gm" else "Press Resume to Exit Brake Hold"
|
||||
return Alert(
|
||||
alert_text,
|
||||
"",
|
||||
AlertStatus.userPrompt, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.none, .2)
|
||||
|
||||
|
||||
def speed_limit_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality, starpilot_toggles: SimpleNamespace) -> Alert:
|
||||
return Alert(
|
||||
"Speed limit changed",
|
||||
@@ -802,11 +811,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
},
|
||||
|
||||
EventName.brakeHold: {
|
||||
ET.WARNING: Alert(
|
||||
"Press Resume to Exit Brake Hold",
|
||||
"",
|
||||
AlertStatus.userPrompt, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.none, .2),
|
||||
ET.WARNING: brake_hold_alert,
|
||||
},
|
||||
|
||||
EventName.parkBrake: {
|
||||
|
||||
@@ -44,6 +44,7 @@ FORD_MANUAL_TURN_LATCH_CARS = frozenset({
|
||||
CAR.FORD_MUSTANG_MACH_E_MK1,
|
||||
})
|
||||
MANUAL_TURN_ENTRY_ANGLE_DEG = 12.0
|
||||
MANUAL_TURN_RELEASE_ANGLE_DEG = 12.0
|
||||
MANUAL_TURN_RECOVERY_SECONDS = 0.25
|
||||
|
||||
|
||||
@@ -207,7 +208,8 @@ class FordLateralController:
|
||||
self.manual_turn_recovery_timer = 0.0
|
||||
return False
|
||||
|
||||
if CS.out.steeringPressed or blinker_direction != 0.0:
|
||||
if (CS.out.steeringPressed or blinker_direction != 0.0 or
|
||||
abs(CS.out.steeringAngleDeg) > MANUAL_TURN_RELEASE_ANGLE_DEG):
|
||||
self.manual_turn_recovery_timer = 0.0
|
||||
else:
|
||||
self.manual_turn_recovery_timer += STEER_DT
|
||||
|
||||
@@ -208,6 +208,55 @@ def test_mach_e_signaled_manual_turn_yields_until_inputs_settle(controller):
|
||||
assert result.curvature > 0.0
|
||||
|
||||
|
||||
def test_mach_e_manual_turn_waits_for_wheel_to_unwind(controller):
|
||||
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||
CC = SimpleNamespace(latActive=True)
|
||||
actuators = SimpleNamespace(curvature=0.006)
|
||||
|
||||
assert not controller.update(CC, car_state(
|
||||
steering_pressed=True, steering_angle=-30.0, steering_torque=-2.0,
|
||||
right_blinker=True), actuators).active
|
||||
|
||||
for _ in range(8):
|
||||
result = controller.update(CC, car_state(steering_angle=-35.0), actuators)
|
||||
assert not result.active
|
||||
|
||||
for _ in range(4):
|
||||
result = controller.update(CC, car_state(steering_angle=-10.0), actuators)
|
||||
assert not result.active
|
||||
result = controller.update(CC, car_state(steering_angle=-10.0), actuators)
|
||||
assert result.active
|
||||
|
||||
|
||||
def test_mach_e_left_manual_turn_waits_for_wheel_to_unwind(controller):
|
||||
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||
CC = SimpleNamespace(latActive=True)
|
||||
actuators = SimpleNamespace(curvature=-0.006)
|
||||
|
||||
assert not controller.update(CC, car_state(
|
||||
steering_pressed=True, steering_angle=30.0, steering_torque=2.0,
|
||||
left_blinker=True), actuators).active
|
||||
|
||||
for _ in range(8):
|
||||
result = controller.update(CC, car_state(steering_angle=35.0), actuators)
|
||||
assert not result.active
|
||||
|
||||
for _ in range(4):
|
||||
result = controller.update(CC, car_state(steering_angle=10.0), actuators)
|
||||
assert not result.active
|
||||
result = controller.update(CC, car_state(steering_angle=10.0), actuators)
|
||||
assert result.active
|
||||
|
||||
|
||||
def test_non_mach_e_signaled_turn_does_not_latch(controller):
|
||||
CC = SimpleNamespace(latActive=True)
|
||||
result = controller.update(CC, car_state(
|
||||
steering_pressed=True, steering_angle=30.0, steering_torque=2.0,
|
||||
left_blinker=True), SimpleNamespace(curvature=-0.006))
|
||||
|
||||
assert result.active
|
||||
|
||||
|
||||
def test_mach_e_opposite_blinker_correction_does_not_start_manual_turn(controller):
|
||||
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||
|
||||
|
||||
@@ -3600,9 +3600,9 @@
|
||||
},
|
||||
{
|
||||
"key": "GMAutoHold",
|
||||
"label": "Volt Auto Hold",
|
||||
"description": "Hold the car at a stop on supported non-CC-only Chevy Volts until the gas pedal is pressed.",
|
||||
"picker_description": "Holds Chevy Volt brakes until the gas pedal is pressed.",
|
||||
"label": "GM Auto Hold",
|
||||
"description": "Hold supported GM vehicles at a stop until the gas pedal is pressed.",
|
||||
"picker_description": "Holds supported GM vehicle brakes until the gas pedal is pressed.",
|
||||
"data_type": "bool",
|
||||
"ui_type": "toggle",
|
||||
"settings_tier": "simple"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { store, navigate, goBack, toolHref, toggleTheme } from "../store.js"
|
||||
import { api } from "../api.js"
|
||||
import { usePolling } from "../composables.js"
|
||||
import { languageState, setLanguage, t } from "../i18n.js"
|
||||
|
||||
const NAV = {
|
||||
recordings: [
|
||||
@@ -37,7 +38,7 @@ export const AppShell = {
|
||||
},
|
||||
computed: {
|
||||
online() { return store.online },
|
||||
statusLabel() { return store.online ? store.deviceStatus : "Offline" },
|
||||
statusLabel() { return store.online ? t(store.deviceStatus, store.deviceStatus) : t("Offline") },
|
||||
isLight() { return store.theme === "light" },
|
||||
drawerOpen: {
|
||||
get() { return store.drawerOpen },
|
||||
@@ -57,6 +58,7 @@ export const AppShell = {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
tr(key, fallback = key) { return t(key, fallback) },
|
||||
closeDrawer() { store.drawerOpen = false },
|
||||
back() { goBack() },
|
||||
async refreshStatus() {
|
||||
@@ -69,6 +71,14 @@ export const AppShell = {
|
||||
store.online = false
|
||||
}
|
||||
},
|
||||
async loadLanguage() {
|
||||
try {
|
||||
const values = await api.getParams()
|
||||
setLanguage(values?.LanguageSetting || languageState.code || "en")
|
||||
} catch (e) {
|
||||
setLanguage(languageState.code || "en")
|
||||
}
|
||||
},
|
||||
clearSearch() {
|
||||
store.search = ""
|
||||
this.$nextTick(() => { const el = this.$refs.searchInput; if (el) el.focus() })
|
||||
@@ -89,6 +99,7 @@ export const AppShell = {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadLanguage()
|
||||
this.statusPoll = usePolling(() => this.refreshStatus(), { interval: 5000 })
|
||||
this.statusPoll.start()
|
||||
},
|
||||
@@ -98,21 +109,21 @@ export const AppShell = {
|
||||
template: `
|
||||
<div class="gx-app">
|
||||
<header class="gx-appbar">
|
||||
<button type="button" class="gx-icon-btn gx-appbar__back gx-back-btn" aria-label="Back" @click="back">
|
||||
<button type="button" class="gx-icon-btn gx-appbar__back gx-back-btn" :aria-label="tr('Back')" @click="back">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
</button>
|
||||
<div class="gx-appbar__pill">
|
||||
<button type="button" class="gx-icon-btn gx-menu-btn" aria-label="Menu" @click="store.drawerOpen = true">
|
||||
<button type="button" class="gx-icon-btn gx-menu-btn" :aria-label="tr('Menu')" @click="store.drawerOpen = true">
|
||||
<i class="bi bi-list"></i>
|
||||
</button>
|
||||
<span class="gx-appbar__home" role="button" tabindex="0"
|
||||
aria-label="Galaxy home" @click="goHome" @keydown.enter="goHome" @keydown.space.prevent="goHome">
|
||||
<span class="gx-appbar__title">Galaxy</span>
|
||||
:aria-label="tr('Galaxy home')" @click="goHome" @keydown.enter="goHome" @keydown.space.prevent="goHome">
|
||||
<span class="gx-appbar__title">{{ tr("Galaxy") }}</span>
|
||||
</span>
|
||||
<div class="gx-searchwrap">
|
||||
<input ref="searchInput" class="gx-search gx-appbar__search" type="search" placeholder="Search toggles..."
|
||||
v-model="search" aria-label="Search toggles" />
|
||||
<button v-if="search" type="button" class="gx-search-clear" aria-label="Clear search" @click="clearSearch">
|
||||
<input ref="searchInput" class="gx-search gx-appbar__search" type="search" :placeholder="tr('Search toggles...')"
|
||||
v-model="search" :aria-label="tr('Search toggles')" />
|
||||
<button v-if="search" type="button" class="gx-search-clear" :aria-label="tr('Clear search')" @click="clearSearch">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -123,8 +134,8 @@ export const AppShell = {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="gx-icon-btn gx-theme-toggle" :aria-label="isLight ? 'Switch to dark mode' : 'Switch to light mode'"
|
||||
:title="isLight ? 'Dark mode' : 'Light mode'" @click="themeToggle">
|
||||
<button type="button" class="gx-icon-btn gx-theme-toggle" :aria-label="isLight ? tr('Switch to dark mode') : tr('Switch to light mode')"
|
||||
:title="isLight ? tr('Dark mode') : tr('Light mode')" @click="themeToggle">
|
||||
<i class="bi" :class="isLight ? 'bi-moon-stars-fill' : 'bi-sun-fill'"></i>
|
||||
</button>
|
||||
</header>
|
||||
@@ -135,24 +146,24 @@ export const AppShell = {
|
||||
<aside class="gx-drawer" :class="{ open: store.drawerOpen }">
|
||||
<div class="gx-drawer__header">
|
||||
<img class="gx-logo" src="/assets/images/main_logo.png" alt="Galaxy logo" />
|
||||
<span class="gx-drawer-title">Galaxy</span>
|
||||
<span class="gx-drawer-title">{{ tr("Galaxy") }}</span>
|
||||
</div>
|
||||
<div class="gx-nav-section">
|
||||
<div class="gx-nav-section__title">Main</div>
|
||||
<div class="gx-nav-section__title">{{ tr("Main") }}</div>
|
||||
<a class="gx-nav-item" :class="{ active: isActive('/') }" @click.prevent="navTo('/')">
|
||||
<i class="bi bi-house-fill"></i><span>Home</span>
|
||||
<i class="bi bi-house-fill"></i><span>{{ tr("Home") }}</span>
|
||||
</a>
|
||||
<a class="gx-nav-item" :class="{ active: isActive('/settings') }" @click.prevent="navTo('/settings')">
|
||||
<i class="bi bi-toggle-on"></i><span>Toggles</span>
|
||||
<i class="bi bi-toggle-on"></i><span>{{ tr("Toggles") }}</span>
|
||||
</a>
|
||||
<a class="gx-nav-item" :class="{ active: isActive('/tools') }" @click.prevent="navTo('/tools')">
|
||||
<i class="bi bi-tools"></i><span>Tools</span>
|
||||
<i class="bi bi-tools"></i><span>{{ tr("Tools") }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div v-for="(links, section) in NAV" :key="section" class="gx-nav-section">
|
||||
<div class="gx-nav-section__title">{{ section }}</div>
|
||||
<div class="gx-nav-section__title">{{ tr(section === 'recordings' ? 'Recordings' : 'Tools') }}</div>
|
||||
<a v-for="link in links" :key="link.link" class="gx-nav-item" @click.prevent="navTo(link.link)">
|
||||
<i class="bi" :class="link.icon"></i><span>{{ link.name }}</span>
|
||||
<i class="bi" :class="link.icon"></i><span>{{ tr(link.name, link.name) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -166,7 +177,7 @@ export const AppShell = {
|
||||
class="nav-item" :class="{ active: isActive(item.link) }"
|
||||
@click="bottomNavTo(item)">
|
||||
<i class="bi" :class="item.icon"></i>
|
||||
<span>{{ item.name }}</span>
|
||||
<span>{{ tr(item.name, item.name) }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
resolveVehicleUnitParam, stepPrecision,
|
||||
} from "../params.js"
|
||||
import { FavoritesEditor } from "./FavoritesEditor.js"
|
||||
import { t } from "../i18n.js"
|
||||
|
||||
export const GalaxyToggleCard = {
|
||||
name: "GalaxyToggleCard",
|
||||
@@ -67,6 +68,7 @@ export const GalaxyToggleCard = {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
tr(key, fallback = key) { return t(key, fallback) },
|
||||
normalizeHexColor,
|
||||
getColorDefault,
|
||||
coerce(v) { return coerceValueByType(v, this.param.data_type) },
|
||||
@@ -177,11 +179,11 @@ export const GalaxyToggleCard = {
|
||||
<div>
|
||||
<div class="gx-row" :class="{ disabled: locked, 'gx-row--favorites': isFavorites, 'gx-row--stack': isSlider || isSelect }">
|
||||
<div class="gx-row__info">
|
||||
<span class="gx-row__label">{{ displayParam.label }}
|
||||
<span v-if="displayParam.settings_tier === 'advanced'" class="gx-chip gx-chip--advanced">Advanced</span>
|
||||
<span class="gx-row__label">{{ tr(displayParam.label, displayParam.label) }}
|
||||
<span v-if="displayParam.settings_tier === 'advanced'" class="gx-chip gx-chip--advanced">{{ tr("Advanced") }}</span>
|
||||
</span>
|
||||
<span v-if="displayParam.description" class="gx-row__desc">{{ displayParam.description }}</span>
|
||||
<div v-if="locked" class="gx-row__desc"><strong>Locked:</strong> {{ lockMessage }}</div>
|
||||
<span v-if="displayParam.description" class="gx-row__desc">{{ tr(displayParam.description, displayParam.description) }}</span>
|
||||
<div v-if="locked" class="gx-row__desc"><strong>{{ tr("Locked:") }}</strong> {{ tr(lockMessage, lockMessage) }}</div>
|
||||
</div>
|
||||
|
||||
<label v-if="isSwitch" class="gx-switch">
|
||||
@@ -202,15 +204,15 @@ export const GalaxyToggleCard = {
|
||||
@touchstart="beginInteract" @mousedown="beginInteract" @keydown="beginInteract" />
|
||||
<div v-if="displayParam.unit_type" class="gx-slider-meta">
|
||||
<span>{{ sliderRangeDisplay }}</span>
|
||||
<span>Step: {{ sliderStepDisplay }}</span>
|
||||
<span>{{ tr("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">{{ tr("Default") }}</button>
|
||||
</div>
|
||||
|
||||
<select v-else-if="isSelect" class="gx-field" :disabled="locked || updating" :value="String(value ?? '')" @change="onSelect">
|
||||
<option v-if="optionsLoading" value="">Loading...</option>
|
||||
<option v-else-if="!selectOptions.length" value="">No options available</option>
|
||||
<option v-for="opt in selectOptions" :key="String(opt.value)" :value="String(opt.value)">{{ opt.label }}</option>
|
||||
<option v-if="optionsLoading" value="">{{ tr("Loading...") }}</option>
|
||||
<option v-else-if="!selectOptions.length" value="">{{ tr("No options available") }}</option>
|
||||
<option v-for="opt in selectOptions" :key="String(opt.value)" :value="String(opt.value)">{{ tr(opt.label, opt.label) }}</option>
|
||||
</select>
|
||||
|
||||
<input v-else-if="isText" class="gx-field" :type="param.input_type || 'text'" :value="value ?? ''"
|
||||
@@ -220,19 +222,19 @@ export const GalaxyToggleCard = {
|
||||
<span class="gx-row__value">{{ displayValue }}</span>
|
||||
<input type="color" class="gx-color" :value="normalizeHexColor(value) || getColorDefault(param)"
|
||||
:disabled="locked || updating" @change="onColor" />
|
||||
<button class="gx-slider-reset" :disabled="locked || updating || !normalizeHexColor(value)" @click="resetColor">Stock</button>
|
||||
<button class="gx-slider-reset" :disabled="locked || updating || !normalizeHexColor(value)" @click="resetColor">{{ tr("Stock") }}</button>
|
||||
</div>
|
||||
|
||||
<span v-else-if="isReadout" class="gx-row__value">{{ displayValue }}</span>
|
||||
|
||||
<button v-else-if="isAction" class="gx-btn" :disabled="locked || updating" @click="runAction">
|
||||
{{ updating ? "Working..." : (param.action_label || "Run") }}
|
||||
{{ updating ? tr("Working...") : tr(param.action_label || "Run", param.action_label || "Run") }}
|
||||
</button>
|
||||
|
||||
<button v-else-if="isGroup" class="gx-btn gx-btn--tonal" @click="$emit('manage', param.key)">Manage</button>
|
||||
<button v-else-if="isGroup" class="gx-btn gx-btn--tonal" @click="$emit('manage', param.key)">{{ tr("Manage") }}</button>
|
||||
</div>
|
||||
<button v-if="manageable" type="button" class="gx-manage-btn" @click="$emit('manage', param.key)">
|
||||
{{ manageOpen ? "Close" : "Manage" }}
|
||||
{{ manageOpen ? tr("Close") : tr("Manage") }}
|
||||
<i class="bi" :class="manageOpen ? 'bi-chevron-up' : 'bi-chevron-down'"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { api, showSnackbar } from "../api.js"
|
||||
import { LANGUAGE_OPTIONS, languageState, normalizeLanguage, setLanguage, t } from "../i18n.js"
|
||||
|
||||
export const LanguageSelector = {
|
||||
name: "LanguageSelector",
|
||||
props: { deviceValue: { type: String, default: "" } },
|
||||
data() {
|
||||
return { languages: LANGUAGE_OPTIONS, selected: languageState.code, saving: false, error: "" }
|
||||
},
|
||||
computed: {
|
||||
languageCode() { return languageState.code },
|
||||
},
|
||||
watch: {
|
||||
languageCode(value) { this.selected = value },
|
||||
deviceValue: {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
if (!value) return
|
||||
this.selected = setLanguage(value)
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
tr(key, fallback = key) { return t(key, fallback) },
|
||||
async change(event) {
|
||||
const next = normalizeLanguage(event.target.value)
|
||||
const previous = languageState.code
|
||||
this.selected = next
|
||||
this.error = ""
|
||||
setLanguage(next)
|
||||
this.saving = true
|
||||
try {
|
||||
// LanguageSetting is shared with the native UI, which stores the
|
||||
// language catalog values as main_<locale>.
|
||||
await api.updateParam({ key: "LanguageSetting", value: `main_${next}` })
|
||||
showSnackbar(t("Language updated.", "Language updated."))
|
||||
} catch (err) {
|
||||
setLanguage(previous)
|
||||
this.selected = previous
|
||||
this.error = err?.message || t("Unable to save language.", "Unable to save language.")
|
||||
showSnackbar(this.error, "error")
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="gx-card" style="margin-bottom:16px;">
|
||||
<div class="gx-section__header">
|
||||
<i class="bi bi-translate"></i>
|
||||
<span class="gx-section__title">{{ tr("Language") }}</span>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||
<label style="display:flex; align-items:center; gap:10px; flex:1; min-width:220px;">
|
||||
<span>{{ tr("Select language") }}</span>
|
||||
<select class="gx-field" style="max-width:220px;" :value="selected" :disabled="saving" @change="change">
|
||||
<option v-for="option in languages" :key="option.value" :value="option.value">{{ tr(option.label, option.label) }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<small class="gx-row__desc">{{ tr("Galaxy uses English when no language is selected.") }}</small>
|
||||
</div>
|
||||
<p v-if="error" class="gx-row__desc" style="color:var(--danger); margin:8px 0 0;">{{ error }}</p>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { reactive } from "vue"
|
||||
|
||||
const STORAGE_KEY = "galaxy-language"
|
||||
|
||||
export const LANGUAGE_OPTIONS = [
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "es", label: "Spanish" },
|
||||
{ value: "fr", label: "French" },
|
||||
{ value: "ko", label: "Korean" },
|
||||
{ value: "zh-CHS", label: "Chinese" },
|
||||
]
|
||||
|
||||
const SUPPORTED_CODES = new Set(LANGUAGE_OPTIONS.map((option) => option.value))
|
||||
|
||||
// Galaxy deliberately keeps English as the fallback. This lets new server-side
|
||||
// labels ship safely before they have been added to every translation below.
|
||||
const TRANSLATIONS = {
|
||||
es: {
|
||||
English: "Inglés", Spanish: "Español", French: "Francés", Korean: "Coreano", Chinese: "Chino",
|
||||
Home: "Inicio", Toggles: "Interruptores", Tools: "Herramientas", Recordings: "Grabaciones",
|
||||
Bluetooth: "Bluetooth", "Cameras & Monitoring": "Cámaras y monitoreo", Galaxy: "Galaxy",
|
||||
"Logs & Diagnostics": "Registros y diagnósticos", "Model Manager": "Administrador de modelos",
|
||||
"Navigation & Maps": "Navegación y mapas", "System Tools": "Herramientas del sistema",
|
||||
"Model Laboratory": "Laboratorio de modelos", Plots: "Gráficas", "Testing Ground": "Área de pruebas",
|
||||
"Theme Maker": "Creador de temas", "Tuning, Plots & Testing": "Ajustes, gráficas y pruebas",
|
||||
"Vehicle Controls": "Controles del vehículo", Main: "Principal", Offline: "Sin conexión", Parked: "Estacionado",
|
||||
Back: "Atrás", Menu: "Menú", "Galaxy home": "Inicio de Galaxy", "Search toggles...": "Buscar interruptores...",
|
||||
"Search toggles": "Buscar interruptores", "Clear search": "Borrar búsqueda", "Dark mode": "Modo oscuro",
|
||||
"Light mode": "Modo claro", "Switch to dark mode": "Cambiar a modo oscuro", "Switch to light mode": "Cambiar a modo claro",
|
||||
Settings: "Configuración", Language: "Idioma", "Select language": "Seleccionar idioma", Advanced: "Avanzado",
|
||||
"result(s)": "resultado(s)",
|
||||
"Galaxy uses English when no language is selected.": "Galaxy usa inglés si no se selecciona un idioma.",
|
||||
"Language updated.": "Idioma actualizado.", "Unable to save language.": "No se pudo guardar el idioma.",
|
||||
"Loading configuration...": "Cargando configuración...", "No settings available.": "No hay ajustes disponibles.",
|
||||
"No settings in this section.": "No hay ajustes en esta sección.", "Locked:": "Bloqueado:", "Step:": "Paso:",
|
||||
"This setting can only be changed while parked.": "Este ajuste solo se puede cambiar mientras el vehículo está estacionado.",
|
||||
Default: "Predeterminado", "Loading...": "Cargando...", "No options available": "No hay opciones disponibles",
|
||||
"Working...": "Procesando...", Run: "Ejecutar", Manage: "Administrar", Close: "Cerrar", Stock: "Original",
|
||||
},
|
||||
fr: {
|
||||
English: "Anglais", Spanish: "Espagnol", French: "Français", Korean: "Coréen", Chinese: "Chinois",
|
||||
Home: "Accueil", Toggles: "Options", Tools: "Outils", Recordings: "Enregistrements",
|
||||
Bluetooth: "Bluetooth", "Cameras & Monitoring": "Caméras et surveillance", Galaxy: "Galaxy",
|
||||
"Logs & Diagnostics": "Journaux et diagnostics", "Model Manager": "Gestionnaire de modèles",
|
||||
"Navigation & Maps": "Navigation et cartes", "System Tools": "Outils système",
|
||||
"Model Laboratory": "Laboratoire de modèles", Plots: "Graphiques", "Testing Ground": "Zone de test",
|
||||
"Theme Maker": "Créateur de thèmes", "Tuning, Plots & Testing": "Réglages, graphiques et tests",
|
||||
"Vehicle Controls": "Commandes du véhicule", Main: "Principal", Offline: "Hors ligne", Parked: "Stationné",
|
||||
Back: "Retour", Menu: "Menu", "Galaxy home": "Accueil Galaxy", "Search toggles...": "Rechercher des options...",
|
||||
"Search toggles": "Rechercher des options", "Clear search": "Effacer la recherche", "Dark mode": "Mode sombre",
|
||||
"Light mode": "Mode clair", "Switch to dark mode": "Passer au mode sombre", "Switch to light mode": "Passer au mode clair",
|
||||
Settings: "Paramètres", Language: "Langue", "Select language": "Choisir la langue", Advanced: "Avancé",
|
||||
"result(s)": "résultat(s)",
|
||||
"Galaxy uses English when no language is selected.": "Galaxy utilise l’anglais si aucune langue n’est sélectionnée.",
|
||||
"Language updated.": "Langue mise à jour.", "Unable to save language.": "Impossible d’enregistrer la langue.",
|
||||
"Loading configuration...": "Chargement de la configuration...", "No settings available.": "Aucun réglage disponible.",
|
||||
"No settings in this section.": "Aucun réglage dans cette section.", "Locked:": "Verrouillé :", "Step:": "Pas :",
|
||||
"This setting can only be changed while parked.": "Ce réglage ne peut être modifié que lorsque le véhicule est stationné.",
|
||||
Default: "Par défaut", "Loading...": "Chargement...", "No options available": "Aucune option disponible",
|
||||
"Working...": "En cours...", Run: "Exécuter", Manage: "Gérer", Close: "Fermer", Stock: "Origine",
|
||||
},
|
||||
ko: {
|
||||
English: "영어", Spanish: "스페인어", French: "프랑스어", Korean: "한국어", Chinese: "중국어",
|
||||
Home: "홈", Toggles: "토글", Tools: "도구", Recordings: "녹화",
|
||||
Bluetooth: "블루투스", "Cameras & Monitoring": "카메라 및 모니터링", Galaxy: "Galaxy",
|
||||
"Logs & Diagnostics": "로그 및 진단", "Model Manager": "모델 관리자",
|
||||
"Navigation & Maps": "내비게이션 및 지도", "System Tools": "시스템 도구",
|
||||
"Model Laboratory": "모델 연구소", Plots: "플롯", "Testing Ground": "테스트 공간",
|
||||
"Theme Maker": "테마 만들기", "Tuning, Plots & Testing": "튜닝, 플롯 및 테스트",
|
||||
"Vehicle Controls": "차량 제어", Main: "메인", Offline: "오프라인", Parked: "주차됨",
|
||||
Back: "뒤로", Menu: "메뉴", "Galaxy home": "Galaxy 홈", "Search toggles...": "토글 검색...",
|
||||
"Search toggles": "토글 검색", "Clear search": "검색 지우기", "Dark mode": "다크 모드",
|
||||
"Light mode": "라이트 모드", "Switch to dark mode": "다크 모드로 전환", "Switch to light mode": "라이트 모드로 전환",
|
||||
Settings: "설정", Language: "언어", "Select language": "언어 선택", Advanced: "고급",
|
||||
"result(s)": "개 결과",
|
||||
"Galaxy uses English when no language is selected.": "언어를 선택하지 않으면 Galaxy는 영어를 사용합니다.",
|
||||
"Language updated.": "언어가 업데이트되었습니다.", "Unable to save language.": "언어를 저장할 수 없습니다.",
|
||||
"Loading configuration...": "설정을 불러오는 중...", "No settings available.": "사용 가능한 설정이 없습니다.",
|
||||
"No settings in this section.": "이 섹션에 설정이 없습니다.", "Locked:": "잠김:", "Step:": "단계:",
|
||||
"This setting can only be changed while parked.": "이 설정은 주차 중에만 변경할 수 있습니다.",
|
||||
Default: "기본값", "Loading...": "로드 중...", "No options available": "사용 가능한 옵션이 없습니다",
|
||||
"Working...": "처리 중...", Run: "실행", Manage: "관리", Close: "닫기", Stock: "기본",
|
||||
},
|
||||
"zh-CHS": {
|
||||
English: "英语", Spanish: "西班牙语", French: "法语", Korean: "韩语", Chinese: "中文",
|
||||
Home: "主页", Toggles: "开关", Tools: "工具", Recordings: "录制内容",
|
||||
Bluetooth: "蓝牙", "Cameras & Monitoring": "摄像头和监控", Galaxy: "Galaxy",
|
||||
"Logs & Diagnostics": "日志和诊断", "Model Manager": "模型管理器",
|
||||
"Navigation & Maps": "导航和地图", "System Tools": "系统工具",
|
||||
"Model Laboratory": "模型实验室", Plots: "图表", "Testing Ground": "测试区",
|
||||
"Theme Maker": "主题制作器", "Tuning, Plots & Testing": "调校、图表和测试",
|
||||
"Vehicle Controls": "车辆控制", Main: "主菜单", Offline: "离线", Parked: "已停车",
|
||||
Back: "返回", Menu: "菜单", "Galaxy home": "Galaxy 主页", "Search toggles...": "搜索开关...",
|
||||
"Search toggles": "搜索开关", "Clear search": "清除搜索", "Dark mode": "深色模式",
|
||||
"Light mode": "浅色模式", "Switch to dark mode": "切换到深色模式", "Switch to light mode": "切换到浅色模式",
|
||||
Settings: "设置", Language: "语言", "Select language": "选择语言", Advanced: "高级",
|
||||
"result(s)": "个结果",
|
||||
"Galaxy uses English when no language is selected.": "未选择语言时,Galaxy 将使用英语。",
|
||||
"Language updated.": "语言已更新。", "Unable to save language.": "无法保存语言。",
|
||||
"Loading configuration...": "正在加载配置...", "No settings available.": "没有可用设置。",
|
||||
"No settings in this section.": "此部分没有设置。", "Locked:": "已锁定:", "Step:": "步长:",
|
||||
"This setting can only be changed while parked.": "此设置只能在车辆停放时更改。",
|
||||
Default: "默认值", "Loading...": "加载中...", "No options available": "没有可用选项",
|
||||
"Working...": "处理中...", Run: "运行", Manage: "管理", Close: "关闭", Stock: "原厂",
|
||||
},
|
||||
}
|
||||
|
||||
function storageValue() {
|
||||
try { return window.localStorage.getItem(STORAGE_KEY) || "en" } catch (e) { return "en" }
|
||||
}
|
||||
|
||||
export function normalizeLanguage(value) {
|
||||
const code = String(value || "").trim().replace(/^main_/i, "")
|
||||
return SUPPORTED_CODES.has(code) ? code : "en"
|
||||
}
|
||||
|
||||
export const languageState = reactive({ code: normalizeLanguage(storageValue()) })
|
||||
|
||||
export function setLanguage(value) {
|
||||
const code = normalizeLanguage(value)
|
||||
languageState.code = code
|
||||
try { window.localStorage.setItem(STORAGE_KEY, code) } catch (e) { /* storage can be unavailable in private webviews */ }
|
||||
if (typeof document !== "undefined") document.documentElement.lang = code === "zh-CHS" ? "zh-CN" : code
|
||||
return code
|
||||
}
|
||||
|
||||
export function t(key, fallback = key) {
|
||||
const source = String(key ?? "")
|
||||
return TRANSLATIONS[languageState.code]?.[source] || fallback || source
|
||||
}
|
||||
|
||||
setLanguage(languageState.code)
|
||||
@@ -11,6 +11,8 @@ import { PersonalityProfiles } from "../components/PersonalityProfiles.js"
|
||||
import { GalaxyToggleCard } from "../components/GalaxyToggleCard.js"
|
||||
import { GalaxySection } from "../components/GalaxySection.js"
|
||||
import { DevModeBanner } from "../components/DevModeBanner.js"
|
||||
import { LanguageSelector } from "../components/LanguageSelector.js"
|
||||
import { setLanguage, t } from "../i18n.js"
|
||||
|
||||
const LEGACY_PERSONALITY_KEYS = new Set([
|
||||
"AccelerationProfile", "AggressiveFollow", "AggressiveFollowHigh", "CustomAccelProfile",
|
||||
@@ -22,7 +24,7 @@ const LEGACY_PERSONALITY_KEYS = new Set([
|
||||
|
||||
export const Settings = {
|
||||
name: "Settings",
|
||||
components: { SettingTree, PersonalityProfiles, GalaxyToggleCard, GalaxySection, DevModeBanner, LongitudinalMode },
|
||||
components: { SettingTree, PersonalityProfiles, GalaxyToggleCard, GalaxySection, DevModeBanner, LongitudinalMode, LanguageSelector },
|
||||
data() {
|
||||
return {
|
||||
layout: [],
|
||||
@@ -68,6 +70,7 @@ export const Settings = {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
tr(key, fallback = key) { return t(key, fallback) },
|
||||
isModeParam(p) { return p.key === LONGITUDINAL_MODE_KEY || !!p.longitudinal_mode },
|
||||
modeSection(s) { return this.layout.find(section => section.name === s.name && section.params.some(p => p.key === LONGITUDINAL_MODE_KEY)) },
|
||||
ordinaryParams(s) { return s.params.filter(p => !this.isModeParam(p)) },
|
||||
@@ -78,6 +81,7 @@ export const Settings = {
|
||||
])
|
||||
this.layout = longitudinalModeLayout(layout)
|
||||
this.values = values || {}
|
||||
setLanguage(this.values.LanguageSetting || "en")
|
||||
this.defaults = defaults || {}
|
||||
if (!this.activeSectionSlug && this.sections.length) {
|
||||
const preferred = this.sections.find((s) => s.slug === this.defaultSectionSlug)
|
||||
@@ -136,22 +140,24 @@ export const Settings = {
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<h2 style="margin-top:0;">Toggles</h2>
|
||||
<h2 style="margin-top:0;">{{ tr("Toggles") }}</h2>
|
||||
|
||||
<LanguageSelector :device-value="String(values.LanguageSetting || '')" />
|
||||
|
||||
<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">{{ tr("Loading configuration...") }}</div>
|
||||
|
||||
<template v-else-if="sections.length">
|
||||
<div v-if="searchActive">
|
||||
<div class="gx-card">
|
||||
<div class="gx-section__header">
|
||||
<i class="bi bi-search"></i>
|
||||
<span class="gx-section__title">{{ searchResults.reduce((n, s) => n + s.matches.length, 0) }} result(s)</span>
|
||||
<span class="gx-section__title">{{ searchResults.reduce((n, s) => n + s.matches.length, 0) }} {{ tr("result(s)") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<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="tr(section.name, section.name) + ' (' + section.matches.length + ')'" :icon="section.icon || 'bi-search'" :default-open="false">
|
||||
<LongitudinalMode v-if="section.matches.some(isModeParam)" :section="modeSection(section)" :values="values" @change="onParamChange" />
|
||||
<template v-for="p in section.matches" :key="p.key">
|
||||
<PersonalityProfiles v-if="p.key === 'CustomPersonalities'" :manage-open="!!expanded[p.key]" @manage="toggleManage(p.key)" @change="onParamChange" />
|
||||
@@ -167,24 +173,24 @@ export const Settings = {
|
||||
<button v-for="s in sections" :key="s.slug" type="button"
|
||||
class="gx-chip" :style="s.slug === activeSection.slug ? 'background: var(--primary); color: var(--on-primary);' : 'background: var(--surface-variant); color: var(--on-surface-variant); cursor:pointer;'"
|
||||
@click="selectSection(s.slug)">
|
||||
{{ s.name }}
|
||||
{{ tr(s.name, s.name) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="gx-card">
|
||||
<div class="gx-section__header">
|
||||
<i class="bi" :class="activeSection.icon"></i>
|
||||
<span class="gx-section__title">{{ activeSection.name }}</span>
|
||||
<span class="gx-section__title">{{ tr(activeSection.name, activeSection.name) }}</span>
|
||||
</div>
|
||||
<LongitudinalMode v-if="modeSection(activeSection)" :section="modeSection(activeSection)" :values="values" @change="onParamChange" />
|
||||
<SettingTree :params="ordinaryParams(activeSection)" :parent-key="null" :values="values"
|
||||
:expanded="expanded" :lock-reason="lockReason" @change="onParamChange" @manage="toggleManage" />
|
||||
<div v-if="!activeSection.params.length" class="gx-empty">No settings in this section.</div>
|
||||
<div v-if="!activeSection.params.length" class="gx-empty">{{ tr("No settings in this section.") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="gx-empty">No settings available.</div>
|
||||
<div v-else class="gx-empty">{{ tr("No settings available.") }}</div>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ def test_ui_app_shell_files_exist():
|
||||
"js/store.js",
|
||||
"js/api.js",
|
||||
"js/params.js",
|
||||
"js/i18n.js",
|
||||
"js/components/AppShell.js",
|
||||
"js/components/GalaxyModal.js",
|
||||
"js/components/GalaxySection.js",
|
||||
@@ -35,6 +36,7 @@ def test_ui_app_shell_files_exist():
|
||||
"js/components/WheelControls.js",
|
||||
"js/components/BluetoothPanel.js",
|
||||
"js/components/DevModeBanner.js",
|
||||
"js/components/LanguageSelector.js",
|
||||
"js/composables.js",
|
||||
"js/views/Home.js",
|
||||
"js/views/Settings.js",
|
||||
@@ -73,6 +75,19 @@ def test_ui_uses_same_backend_endpoints():
|
||||
assert '"/api/params/defaults"' in api
|
||||
|
||||
|
||||
def test_ui_language_selector_uses_shared_device_language_setting():
|
||||
i18n = _read("js/i18n.js")
|
||||
selector = _read("js/components/LanguageSelector.js")
|
||||
settings = _read("js/views/Settings.js")
|
||||
|
||||
for code in ["en", "es", "fr", "ko", "zh-CHS"]:
|
||||
assert f'value: "{code}"' in i18n
|
||||
assert "localStorage" in i18n
|
||||
assert "LanguageSetting" in selector
|
||||
assert "main_${next}" in selector
|
||||
assert "<LanguageSelector" in settings
|
||||
|
||||
|
||||
def test_ui_ports_developer_mode_gating():
|
||||
params = _read("js/params.js")
|
||||
assert "countAdvancedHiddenByDeveloperMode" in params
|
||||
|
||||
Reference in New Issue
Block a user