diff --git a/opendbc_repo/opendbc/car/gm/carstate.py b/opendbc_repo/opendbc/car/gm/carstate.py index 81145c7e72..3fe7306054 100644 --- a/opendbc_repo/opendbc/car/gm/carstate.py +++ b/opendbc_repo/opendbc/car/gm/carstate.py @@ -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 diff --git a/opendbc_repo/opendbc/car/gm/tests/test_gm.py b/opendbc_repo/opendbc/car/gm/tests/test_gm.py index db4c52c134..e29af56be5 100644 --- a/opendbc_repo/opendbc/car/gm/tests/test_gm.py +++ b/opendbc_repo/opendbc/car/gm/tests/test_gm.py @@ -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, diff --git a/opendbc_repo/opendbc/car/hyundai/carcontroller.py b/opendbc_repo/opendbc/car/hyundai/carcontroller.py index 44cdbd5590..27a8a809c9 100644 --- a/opendbc_repo/opendbc/car/hyundai/carcontroller.py +++ b/opendbc_repo/opendbc/car/hyundai/carcontroller.py @@ -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 = { diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py index ad3d320ff4..a45d6c1802 100644 --- a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py @@ -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() diff --git a/opendbc_repo/opendbc/car/subaru/carcontroller.py b/opendbc_repo/opendbc/car/subaru/carcontroller.py index 6e3edbc5b5..e2d4fdf779 100644 --- a/opendbc_repo/opendbc/car/subaru/carcontroller.py +++ b/opendbc_repo/opendbc/car/subaru/carcontroller.py @@ -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)) diff --git a/opendbc_repo/opendbc/car/subaru/tests/test_subaru.py b/opendbc_repo/opendbc/car/subaru/tests/test_subaru.py index 962e194f66..c5901320b2 100644 --- a/opendbc_repo/opendbc/car/subaru/tests/test_subaru.py +++ b/opendbc_repo/opendbc/car/subaru/tests/test_subaru.py @@ -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)) diff --git a/opendbc_repo/opendbc/safety/modes/gm.h b/opendbc_repo/opendbc/safety/modes/gm.h index 69c722c3b9..fef65fc1e1 100644 --- a/opendbc_repo/opendbc/safety/modes/gm.h +++ b/opendbc_repo/opendbc/safety/modes/gm.h @@ -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); diff --git a/opendbc_repo/opendbc/safety/tests/test_gm.py b/opendbc_repo/opendbc/safety/tests/test_gm.py index 286c441c94..d054077b79 100755 --- a/opendbc_repo/opendbc/safety/tests/test_gm.py +++ b/opendbc_repo/opendbc/safety/tests/test_gm.py @@ -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 diff --git a/panda/board/drivers/can_common.h b/panda/board/drivers/can_common.h index 92c0090a21..0b551dc1b8 100644 --- a/panda/board/drivers/can_common.h +++ b/panda/board/drivers/can_common.h @@ -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; + } + } } diff --git a/panda/tests/libpanda/libpanda_py.py b/panda/tests/libpanda/libpanda_py.py index eef722c06a..ed7306f4f2 100644 --- a/panda/tests/libpanda/libpanda_py.py +++ b/panda/tests/libpanda/libpanda_py.py @@ -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(""" diff --git a/panda/tests/libpanda/panda.c b/panda/tests/libpanda/panda.c index 2d17d64e34..e3ea0d1b04 100644 --- a/panda/tests/libpanda/panda.c +++ b/panda/tests/libpanda/panda.c @@ -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" diff --git a/panda/tests/usbprotocol/test_ignition.py b/panda/tests/usbprotocol/test_ignition.py new file mode 100644 index 0000000000..4197bc5657 --- /dev/null +++ b/panda/tests/usbprotocol/test_ignition.py @@ -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() diff --git a/selfdrive/controls/lib/longitudinal_vehicle_tunes.py b/selfdrive/controls/lib/longitudinal_vehicle_tunes.py index 0e05946964..bb66a5874c 100644 --- a/selfdrive/controls/lib/longitudinal_vehicle_tunes.py +++ b/selfdrive/controls/lib/longitudinal_vehicle_tunes.py @@ -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 ( diff --git a/selfdrive/controls/tests/test_starpilot_planner.py b/selfdrive/controls/tests/test_starpilot_planner.py index df00fd98da..77d6872ee5 100644 --- a/selfdrive/controls/tests/test_starpilot_planner.py +++ b/selfdrive/controls/tests/test_starpilot_planner.py @@ -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( diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index 6d530cab47..cb5f4c64b1 100644 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -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: { diff --git a/starpilot/car/ford/lateral.py b/starpilot/car/ford/lateral.py index 1da986882e..10726cf9af 100644 --- a/starpilot/car/ford/lateral.py +++ b/starpilot/car/ford/lateral.py @@ -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 diff --git a/starpilot/car/ford/tests/test_lateral.py b/starpilot/car/ford/tests/test_lateral.py index f6ee1304a5..8c495a431f 100644 --- a/starpilot/car/ford/tests/test_lateral.py +++ b/starpilot/car/ford/tests/test_lateral.py @@ -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 diff --git a/starpilot/common/assets/device_settings_layout.json b/starpilot/common/assets/device_settings_layout.json index eeee2a09b1..7b7d0ff201 100644 --- a/starpilot/common/assets/device_settings_layout.json +++ b/starpilot/common/assets/device_settings_layout.json @@ -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" diff --git a/starpilot/system/the_galaxy/assets/mobile/js/components/AppShell.js b/starpilot/system/the_galaxy/assets/mobile/js/components/AppShell.js index e13ee4d1b7..11d5217a0b 100644 --- a/starpilot/system/the_galaxy/assets/mobile/js/components/AppShell.js +++ b/starpilot/system/the_galaxy/assets/mobile/js/components/AppShell.js @@ -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: `
-
- - Galaxy + :aria-label="tr('Galaxy home')" @click="goHome" @keydown.enter="goHome" @keydown.space.prevent="goHome"> + {{ tr("Galaxy") }}
- -
@@ -123,8 +134,8 @@ export const AppShell = {
- @@ -135,24 +146,24 @@ export const AppShell = { @@ -166,7 +177,7 @@ export const AppShell = { class="nav-item" :class="{ active: isActive(item.link) }" @click="bottomNavTo(item)"> - {{ item.name }} + {{ tr(item.name, item.name) }} diff --git a/starpilot/system/the_galaxy/assets/mobile/js/components/GalaxyToggleCard.js b/starpilot/system/the_galaxy/assets/mobile/js/components/GalaxyToggleCard.js index 396e845b03..9c0b21cd77 100644 --- a/starpilot/system/the_galaxy/assets/mobile/js/components/GalaxyToggleCard.js +++ b/starpilot/system/the_galaxy/assets/mobile/js/components/GalaxyToggleCard.js @@ -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 = {
- {{ displayParam.label }} - Advanced + {{ tr(displayParam.label, displayParam.label) }} + {{ tr("Advanced") }} - {{ displayParam.description }} -
Locked: {{ lockMessage }}
+ {{ tr(displayParam.description, displayParam.description) }} +
{{ tr("Locked:") }} {{ tr(lockMessage, lockMessage) }}
{{ displayValue }} - +
{{ displayValue }} - + diff --git a/starpilot/system/the_galaxy/assets/mobile/js/components/LanguageSelector.js b/starpilot/system/the_galaxy/assets/mobile/js/components/LanguageSelector.js new file mode 100644 index 0000000000..eef82dd317 --- /dev/null +++ b/starpilot/system/the_galaxy/assets/mobile/js/components/LanguageSelector.js @@ -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_. + 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: ` +
+
+ + {{ tr("Language") }} +
+
+ + {{ tr("Galaxy uses English when no language is selected.") }} +
+

{{ error }}

+
+ `, +} diff --git a/starpilot/system/the_galaxy/assets/mobile/js/i18n.js b/starpilot/system/the_galaxy/assets/mobile/js/i18n.js new file mode 100644 index 0000000000..a39c472e42 --- /dev/null +++ b/starpilot/system/the_galaxy/assets/mobile/js/i18n.js @@ -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) diff --git a/starpilot/system/the_galaxy/assets/mobile/js/views/Settings.js b/starpilot/system/the_galaxy/assets/mobile/js/views/Settings.js index d7ff0199c2..5deb4bb0ae 100644 --- a/starpilot/system/the_galaxy/assets/mobile/js/views/Settings.js +++ b/starpilot/system/the_galaxy/assets/mobile/js/views/Settings.js @@ -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: `
-

Toggles

+

{{ tr("Toggles") }}

+ + -
Loading configuration...
+
{{ tr("Loading configuration...") }}