Compare commits

..

5 Commits

Author SHA1 Message Date
firestar5683 097d63caef and this is my lab 2026-09-04 23:01:30 -05:00
firestar5683 de9cb64165 Four Score & 7 2026-09-04 22:47:02 -05:00
firestar5683 53e5c5246d build 2026-09-04 21:58:10 -05:00
firestar5683 b5ab54ab6d this is my laboratory 2026-09-04 21:56:35 -05:00
Prabhaav Pillai f55ad9162d More Vue native windows. Reduce Duplicate code within API. 2026-09-04 15:52:40 -04:00
197 changed files with 12863 additions and 3823 deletions
Binary file not shown.
+3 -1
View File
@@ -510,6 +510,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ModeButtonControl", {PERSISTENT, INT, "0", "0", 2, SETTINGS_SIMPLE}},
{"ModelDownloadProgress", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"ModelDrivesAndScores", {PERSISTENT, JSON, "{}", "{}"}},
{"ModelLabConfig", {PERSISTENT, JSON, "{}", "{}"}},
{"ModelLabModelToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"ModelLabRuntime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON, "{}", "{}"}},
{"ModelReleasedDates", {PERSISTENT, STRING, "", "", 1}},
{"ModelRandomizer", {PERSISTENT, BOOL, "0", "0", 2}},
{"LatSmoothSeconds", {PERSISTENT, FLOAT, "0.1", "0.1", 3}},
@@ -705,7 +708,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"SubaruSNG", {PERSISTENT, BOOL, "1", "0", 2, SETTINGS_SIMPLE}},
{"SubaruSNGManualParkingBrake", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"SubaruStopStartOff", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"SubaruAvhOnAtStartup", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"TacoTune", {PERSISTENT, BOOL, "0", "0", 2}},
{"TeslaCoopSteering", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"TestAlert", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
Binary file not shown.
+34 -6
View File
@@ -28,6 +28,9 @@ BOLT_CC_BUTTON_CARS = {
BOLT_CC_TARGET_DEADBAND_MPH = 0.75
BOLT_CC_REVERSE_CONFIRM_S = 0.6
BOLT_CC_DIRECTION_MEMORY_S = 1.5
VOLT_CC_CARS = {
CAR.CHEVROLET_VOLT_CC,
}
def malibu_phase_map_for_button(button):
@@ -336,6 +339,28 @@ def stabilize_bolt_cc_button(controller, CP, requested_button):
return requested_button
def _create_volt_cc_spam_command(CS, actuators, ms_convert):
accel = float(actuators.accel)
speed_setpoint = int(round(CS.out.cruiseState.speed * ms_convert))
ego_speed = CS.out.vEgo * ms_convert
if accel == 0.0:
return CruiseButtons.INIT, float("inf")
if accel < 0.0:
if speed_setpoint > ego_speed + 3.0:
rate = 0.2
else:
rate = max(1.0 / (-accel * ms_convert), 0.2)
return CruiseButtons.DECEL_SET, rate
if speed_setpoint < ego_speed - 3.0:
rate = 0.2
else:
rate = max(1.0 / (accel * ms_convert), 0.2)
return CruiseButtons.RES_ACCEL, rate
def create_gm_cc_spam_command(packer, controller, CS, actuators, starpilot_toggles):
accel = actuators.accel
v_ego = CS.out.vEgo
@@ -350,12 +375,15 @@ def create_gm_cc_spam_command(packer, controller, CS, actuators, starpilot_toggl
target_deadband = BOLT_CC_TARGET_DEADBAND_MPH * (CV.MPH_TO_KPH if is_metric else 1.0) if bolt_cc else 0.0
comparison_setpoint = projected_setpoint if bolt_cc else desired_setpoint
if CS.CP.minEnableSpeed - (desired_setpoint / ms_convert) > 3.25:
cruise_btn = CruiseButtons.CANCEL
elif comparison_setpoint < speed_setpoint - target_deadband and speed_setpoint > CS.CP.minEnableSpeed * ms_convert + 1:
cruise_btn = CruiseButtons.DECEL_SET
elif comparison_setpoint > speed_setpoint + target_deadband:
cruise_btn = CruiseButtons.RES_ACCEL
if CS.CP.carFingerprint in VOLT_CC_CARS:
cruise_btn, rate = _create_volt_cc_spam_command(CS, actuators, ms_convert)
else:
if CS.CP.minEnableSpeed - (desired_setpoint / ms_convert) > 3.25:
cruise_btn = CruiseButtons.CANCEL
elif comparison_setpoint < speed_setpoint - target_deadband and speed_setpoint > CS.CP.minEnableSpeed * ms_convert + 1:
cruise_btn = CruiseButtons.DECEL_SET
elif comparison_setpoint > speed_setpoint + target_deadband:
cruise_btn = CruiseButtons.RES_ACCEL
cruise_btn = stabilize_bolt_cc_button(controller, CS.CP, cruise_btn)
if cruise_btn == CruiseButtons.CANCEL:
@@ -657,6 +657,60 @@ class TestGMCarController:
assert [msg[2] for msg in msgs] == [0, 2]
def test_volt_cc_redneck_holds_setpoint_without_planner_acceleration(self):
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
controller = SimpleNamespace(frame=int(2.0 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
cs = SimpleNamespace(
CP=SimpleNamespace(
carFingerprint=CAR.CHEVROLET_VOLT_CC,
flags=GMFlags.NO_CAMERA.value,
networkLocation=structs.CarParams.NetworkLocation.gateway,
minEnableSpeed=0.0,
),
buttons_counter=2,
out=SimpleNamespace(
vEgo=60.0 * CV.KPH_TO_MS,
cruiseState=SimpleNamespace(speed=60.0 * CV.KPH_TO_MS),
),
)
msgs = gmcan.create_gm_cc_spam_command(
packer, controller, cs, SimpleNamespace(accel=0.0), SimpleNamespace(is_metric=True),
)
assert msgs == []
assert controller.apply_speed == 60
def test_volt_cc_redneck_rate_limits_setpoint_changes_by_planner_acceleration(self):
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
controller = SimpleNamespace(frame=int(0.5 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
cs = SimpleNamespace(
CP=SimpleNamespace(
carFingerprint=CAR.CHEVROLET_VOLT_CC,
flags=GMFlags.NO_CAMERA.value,
networkLocation=structs.CarParams.NetworkLocation.gateway,
minEnableSpeed=0.0,
),
buttons_counter=2,
out=SimpleNamespace(
vEgo=60.0 * CV.KPH_TO_MS,
cruiseState=SimpleNamespace(speed=60.0 * CV.KPH_TO_MS),
),
)
msgs = gmcan.create_gm_cc_spam_command(
packer, controller, cs, SimpleNamespace(accel=0.5), SimpleNamespace(is_metric=True),
)
assert msgs == []
controller.frame = int(0.7 / DT_CTRL)
msgs = gmcan.create_gm_cc_spam_command(
packer, controller, cs, SimpleNamespace(accel=0.5), SimpleNamespace(is_metric=True),
)
assert len(msgs) == 1
def test_volt_cc_no_camera_redneck_spam_stays_on_powertrain_bus(self):
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
controller = SimpleNamespace(frame=int(0.3 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
@@ -24,6 +24,9 @@ LongCtrlState = structs.CarControl.Actuators.LongControlState
MAX_ANGLE = 85
MAX_ANGLE_FRAMES = 89
MAX_ANGLE_CONSECUTIVE_FRAMES = 2
CANCEL_BUTTON_DELAY_FRAMES = 10
CANFD_BLINDSPOT_STATUS_STALE_NS = 200_000_000
CANFD_CAMERA_LEAD_STALE_NS = 300_000_000
CANFD_LEAD_MIN_DISTANCE = 0.1
@@ -454,6 +457,7 @@ class CarController(CarControllerBase):
self.apply_angle_last = 0.0
self.car_fingerprint = CP.carFingerprint
self.last_button_frame = 0
self.cancel_counter = 0
self.redneck_button_frame = 0
self.ecu_disable_failed = False
self._ecu_disable_checked = False
@@ -717,6 +721,8 @@ class CarController(CarControllerBase):
if self.CP.flags & HyundaiFlags.ENABLE_BLINKERS:
can_sends.append(make_tester_present_msg(0x7b1, self.CAN.ECAN, suppress_response=True))
self.cancel_counter = self.cancel_counter + 1 if CC.cruiseControl.cancel else 0
# *** CAN/CAN FD specific ***
if self.CP.flags & HyundaiFlags.CANFD:
can_sends.extend(self.create_canfd_msgs(now_nanos, apply_steer_req, apply_torque, apply_angle, set_speed_in_units, accel,
@@ -782,7 +788,7 @@ class CarController(CarControllerBase):
# Button messages
if not self.long_active_ecu:
if CC.cruiseControl.cancel:
if self.cancel_counter > CANCEL_BUTTON_DELAY_FRAMES:
can_sends.append(hyundaican.create_clu11(self.packer, self.frame, CS.clu11, Buttons.CANCEL, self.CP))
elif CC.cruiseControl.resume:
# send resume at a max freq of 10Hz
@@ -1046,7 +1052,7 @@ class CarController(CarControllerBase):
if self.CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS:
can_sends.append(hyundaicanfd.create_acc_cancel(self.packer, self.CP, self.CAN, CS.cruise_info))
self.last_button_frame = self.frame
else:
elif self.cancel_counter > CANCEL_BUTTON_DELAY_FRAMES:
for _ in range(20):
can_sends.append(hyundaicanfd.create_buttons(self.packer, self.CP, self.CAN, CS.buttons_counter + 1, Buttons.CANCEL))
self.last_button_frame = self.frame
@@ -48,7 +48,7 @@ def apply_platform_longitudinal_params(ret: structs.CarParams) -> None:
def apply_kia_ev6_gt_line_longitudinal_params(ret: structs.CarParams) -> None:
ret.startAccel = 1.4
ret.longitudinalActuatorDelay = 0.35
ret.longitudinalActuatorDelay = 0.5
ret.vEgoStarting = 0.5
@@ -7,7 +7,7 @@ from opendbc.can import CANPacker, CANParser
from opendbc.car import Bus, ButtonType, gen_empty_fingerprint, structs
from opendbc.car.structs import CarControl, CarParams
from opendbc.car.fw_versions import build_fw_dict, match_fw_to_car
from opendbc.car.hyundai.carcontroller import CarController, Ioniq6LongitudinalTuningState, GenesisG90LongitudinalTuningState, \
from opendbc.car.hyundai.carcontroller import CarController, CANCEL_BUTTON_DELAY_FRAMES, Ioniq6LongitudinalTuningState, GenesisG90LongitudinalTuningState, \
EV9LongitudinalTuningState, update_ev9_longitudinal_tuning, \
BlindspotWarningState, update_blindspot_warning, \
reset_egmp_longitudinal_tuning, \
@@ -783,6 +783,36 @@ class TestHyundaiFingerprint:
assert not any(addr == 0x340 for addr, _, _ in first)
assert any(addr == 0x340 for addr, _, _ in second)
def test_stock_scc_cancel_waits_for_factory_disengagement(self):
CP = CarInterface.get_params(CAR.HYUNDAI_SANTA_FE_2022, gen_empty_fingerprint(), [], False, False, False, None)
controller = CarController(DBC[CP.carFingerprint], CP)
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS11", 0), ("CLU11", 0)], 0)
hud_control = SimpleNamespace(
visualAlert=CarControl.HUDControl.VisualAlert.none,
leftLaneVisible=True,
rightLaneVisible=True,
leftLaneDepart=False,
rightLaneDepart=False,
)
CS = SimpleNamespace(
lkas11=parser.vl["LKAS11"],
clu11=parser.vl["CLU11"],
redneck_send_button=Buttons.NONE,
is_metric=False,
)
CC = SimpleNamespace(enabled=False, cruiseControl=SimpleNamespace(cancel=True, resume=False))
actuators = SimpleNamespace(longControlState=LongCtrlState.off)
for counter in range(1, CANCEL_BUTTON_DELAY_FRAMES + 1):
controller.cancel_counter = counter
msgs = controller.create_can_msgs(True, 0, False, 0.0, 0.0, False, hud_control, actuators, CS, CC, 2, 2)
assert not any(addr == 0x4F1 for addr, _, _ in msgs)
controller.cancel_counter = CANCEL_BUTTON_DELAY_FRAMES + 1
msgs = controller.create_can_msgs(True, 0, False, 0.0, 0.0, False, hud_control, actuators, CS, CC, 2, 2)
assert any(addr == 0x4F1 for addr, _, _ in msgs)
@pytest.mark.parametrize("candidate", (CAR.HYUNDAI_ELANTRA_2024, CAR.HYUNDAI_ELANTRA_HEV_2024))
def test_hyundai_can_refresh_platforms_use_refresh_dbc_and_safety_param(self, candidate):
CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], False, False, False, None)
@@ -1281,7 +1311,7 @@ class TestHyundaiFingerprint:
assert CP.startAccel == pytest.approx(1.4)
assert CP.vEgoStarting == pytest.approx(0.5)
assert CP.longitudinalActuatorDelay == pytest.approx(0.35)
assert CP.longitudinalActuatorDelay == pytest.approx(0.5)
assert CP.vEgoStopping == pytest.approx(0.3)
assert CP.stoppingDecelRate == pytest.approx(0.4)
assert kia_ev6_gt_line_longitudinal_tuning(CP.carFingerprint, CP.carVin)
@@ -1310,7 +1340,7 @@ class TestHyundaiFingerprint:
assert CP.startAccel == pytest.approx(1.4)
assert CP.vEgoStarting == pytest.approx(0.5)
assert CP.longitudinalActuatorDelay == pytest.approx(0.35)
assert CP.longitudinalActuatorDelay == pytest.approx(0.5)
assert kia_ev6_gt_line_longitudinal_tuning(CP.carFingerprint, CP.carVin, testing_ground_active=True)
assert not kia_ev6_gt_line_longitudinal_tuning(CAR.KIA_EV6_2025, CP.carVin, testing_ground_active=True)
@@ -4,7 +4,7 @@ from opendbc.car import Bus, DT_CTRL, make_tester_present_msg, structs
from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_std_steer_angle_limits, apply_steer_angle_limits_vm, common_fault_avoidance
from opendbc.car.interfaces import CarControllerBase
from opendbc.car.subaru import subarucan
from opendbc.car.subaru.values import CAR, DBC, GLOBAL_ES_ADDR, SUBARU_AVH_CARS, SUBARU_STOP_START_CARS, CanBus, CarControllerParams, SubaruFlags
from opendbc.car.subaru.values import CAR, DBC, GLOBAL_ES_ADDR, SUBARU_STOP_START_CARS, CanBus, CarControllerParams, SubaruFlags
from opendbc.car.vehicle_model import VehicleModel
# FIXME: These limits aren't exact. The real limit is more than likely over a larger time period and
@@ -37,9 +37,6 @@ _STOP_START_STARTUP_DELAY_FRAMES = 100
_STOP_START_STARTUP_DEADLINE_FRAMES = 1000
_STOP_START_PULSE_FRAMES = 30
_STOP_START_PULSE_PERIOD_FRAMES = 5
_AVH_STARTUP_DELAY_FRAMES = _STOP_START_STARTUP_DELAY_FRAMES
_AVH_STARTUP_DEADLINE_FRAMES = _STOP_START_STARTUP_DEADLINE_FRAMES
_AVH_PULSE_MESSAGES = 15 # Match the native 10 Hz AVH frame for roughly 1.5 seconds
def get_safety_CP():
@@ -90,10 +87,6 @@ class CarController(CarControllerBase):
self.stop_start_initial_state = None
self.stop_start_counter = 0
self.stop_start_acknowledged = False
self.avh_attempted = False
self.avh_request_started = False
self.avh_last_counter = None
self.avh_messages_sent = 0
def _stop_start_off_request(self, CC, CS, starpilot_toggles):
"""Send one bounded Subaru Stop/Start OFF request after ignition.
@@ -150,55 +143,6 @@ class CarController(CarControllerBase):
self.stop_start_counter = (self.stop_start_counter + 1) % 0x10
return msg
def _avh_on_request(self, CC, CS, starpilot_toggles):
"""Send a bounded Subaru AVH ON pulse after ignition.
The AVH button frame was identified on the 2025 Legacy only. Keep this
independent from Stop/Start so the existing Outback request is unchanged.
"""
if self.CP.carFingerprint not in SUBARU_AVH_CARS or \
not getattr(starpilot_toggles, "subaru_avh_on", False) or self.avh_attempted:
return None
if self.frame > _AVH_STARTUP_DEADLINE_FRAMES or getattr(CC, "enabled", False):
self.avh_attempted = True
return None
if self.frame < _AVH_STARTUP_DELAY_FRAMES or not getattr(getattr(CS, "out", None), "canValid", True):
return None
out = CS.out
if not getattr(out, "standstill", False) or out.gearShifter not in (
structs.CarState.GearShifter.park,
structs.CarState.GearShifter.neutral,
):
return None
avh_msg = getattr(CS, "avh_msg", None)
avh_dat = getattr(CS, "avh_dat", None)
if not avh_msg or not avh_dat:
return None
if not self.avh_request_started:
self.avh_request_started = True
self.avh_last_counter = int(avh_msg.get("COUNTER", 0)) % 0x10
if self.avh_messages_sent >= _AVH_PULSE_MESSAGES:
self.avh_attempted = True
return None
counter = int(avh_msg.get("COUNTER", 0)) % 0x10
if counter == self.avh_last_counter:
return None
msg = subarucan.create_avh_control(
self.packer, avh_msg, raw_dat=avh_dat,
counter=counter, bus=CanBus.alt_for_cp(self.CP),
)
self.avh_last_counter = counter
self.avh_messages_sent += 1
return msg
def _reset_legacy_2025_handoff(self):
self.driver_override = False
self.angle_override_confirm_frames = 0
@@ -472,10 +416,6 @@ class CarController(CarControllerBase):
if stop_start_msg is not None:
can_sends.append(stop_start_msg)
avh_msg = self._avh_on_request(CC, CS, starpilot_toggles)
if avh_msg is not None:
can_sends.append(avh_msg)
# *** steering ***
if (self.frame % self.p.STEER_STEP) == 0:
if self.CP.flags & SubaruFlags.LKAS_ANGLE:
+2 -10
View File
@@ -4,7 +4,7 @@ from opendbc.can import CANDefine, CANParser
from opendbc.car import Bus, structs
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.interfaces import CarStateBase
from opendbc.car.subaru.values import DBC, CanBus, SUBARU_AVH_CARS, SUBARU_STOP_START_CARS, SubaruFlags
from opendbc.car.subaru.values import DBC, CanBus, SUBARU_STOP_START_CARS, SubaruFlags
from opendbc.car import CanSignalRateCalculator
@@ -18,8 +18,6 @@ class CarState(CarStateBase):
self.dashlights_msg = {}
self.dashlights_dat = b""
self.stop_start_state = 0
self.avh_msg = {}
self.avh_dat = b""
def update(self, can_parsers, starpilot_toggles) -> structs.CarState:
cp = can_parsers[Bus.pt]
@@ -35,11 +33,6 @@ class CarState(CarStateBase):
self.dashlights_dat = stop_start_cp.vl_raw["Dashlights"]
self.stop_start_state = stop_start_cp.vl["Engine_Stop_Start"]["STOP_START_STATE"]
if self.CP.carFingerprint in SUBARU_AVH_CARS:
avh_cp = cp_alt if self.CP.flags & SubaruFlags.GLOBAL_GEN2 else cp
self.avh_msg = copy.copy(avh_cp.vl["AVH"])
self.avh_dat = avh_cp.vl_raw["AVH"]
throttle_msg = cp.vl["Throttle"] if not (self.CP.flags & SubaruFlags.HYBRID) else cp_alt.vl["Throttle_Hybrid"]
ret.gasPressed = throttle_msg["Throttle_Pedal"] > 1e-5
if self.CP.flags & SubaruFlags.PREGLOBAL:
@@ -163,11 +156,10 @@ class CarState(CarStateBase):
@staticmethod
def get_can_parsers(CP):
avh_messages = [("AVH", 0)] if CP.carFingerprint in SUBARU_AVH_CARS else []
parsers = {
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.main_for_cp(CP)),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.camera),
Bus.alt: CANParser(DBC[CP.carFingerprint][Bus.pt], avh_messages, CanBus.alt_for_cp(CP))
Bus.alt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.alt_for_cp(CP))
}
if CP.flags & SubaruFlags.D_PLATFORM:
parsers[Bus.main] = CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.main)
+1 -3
View File
@@ -3,7 +3,7 @@ from opendbc.car.disable_ecu import disable_ecu
from opendbc.car.interfaces import CarInterfaceBase
from opendbc.car.subaru.carcontroller import CarController
from opendbc.car.subaru.carstate import CarState
from opendbc.car.subaru.values import CAR, CanBus, GLOBAL_ES_ADDR, SUBARU_AVH_CARS, SUBARU_STOP_START_CARS, SubaruFlags, SubaruSafetyFlags
from opendbc.car.subaru.values import CAR, CanBus, GLOBAL_ES_ADDR, SUBARU_STOP_START_CARS, SubaruFlags, SubaruSafetyFlags
class CarInterface(CarInterfaceBase):
@@ -42,8 +42,6 @@ class CarInterface(CarInterfaceBase):
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.D_PLATFORM_CAMERA.value
if candidate in SUBARU_STOP_START_CARS:
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.STOP_START_BUTTON.value
if candidate in SUBARU_AVH_CARS:
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.AVH_BUTTON.value
if candidate in (CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023):
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.FIXED_ANGLE_LIMITS.value
@@ -208,31 +208,6 @@ def create_stop_start_control(packer, dashlights_msg, raw_dat=None, counter=None
return packer.make_can_msg("Dashlights", bus, values)
def create_avh_control(packer, avh_msg, raw_dat=None, counter=None, bus=CanBus.alt):
"""Create the supported Subaru Legacy AVH ON request.
AVH is carried in the live 0x32b frame. Preserve the other bytes and update
only the rolling counter, AVH bit, and Subaru additive checksum.
"""
if raw_dat:
dat = bytearray(raw_dat)
if len(dat) != 8:
raise ValueError(f"AVH frame must be 8 bytes, got {len(dat)}")
if counter is None:
counter = (int(avh_msg.get("COUNTER", 0)) + 1) % 0x10
dat[1] = (dat[1] & 0xF0) | (counter % 0x10)
dat[5] |= 0x20 # AVH, big-endian bit 45
dat[0] = ((0x32B & 0xFF) + ((0x32B >> 8) & 0xFF) + sum(dat[1:])) & 0xFF
return 0x32B, bytes(dat), bus
values = dict(avh_msg)
if counter is None:
counter = (int(values.get("COUNTER", 0)) + 1) % 0x10
values["COUNTER"] = counter % 0x10
values["AVH"] = 1
return packer.make_can_msg("AVH", bus, values)
def create_es_brake(packer, frame, es_brake_msg, long_enabled, long_active, brake_value, bus=CanBus.main):
values = {s: es_brake_msg[s] for s in [
"CHECKSUM",
@@ -194,7 +194,6 @@ def test_outback_2023_uses_d_platform_bus_layout():
assert CP.flags & SubaruFlags.D_PLATFORM
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.D_PLATFORM
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.STOP_START_BUTTON
assert not (CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.AVH_BUTTON)
assert not (CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.LEGACY_2025_ANGLE_LIMITS)
assert CanBus.main_for_cp(CP) == CanBus.alt
assert CanBus.angle_for_cp(CP) == CanBus.main
@@ -225,21 +224,6 @@ def test_stop_start_inputs_are_captured_for_supported_models(platform):
assert car_state.stop_start_state == 3
def test_avh_inputs_are_captured_for_legacy_2025():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_LEGACY_2025)
car_state = CarState(CP, None)
parsers = car_state.get_can_parsers(CP)
raw_avh = bytes.fromhex("230f1c4208800000")
parsers[Bus.alt].vl["AVH"]["COUNTER"] = 15
parsers[Bus.alt].vl["AVH"]["AVH"] = 0
parsers[Bus.alt].vl_raw["AVH"] = raw_avh
car_state.update(parsers, SimpleNamespace(subaru_sng=False))
assert car_state.avh_msg["COUNTER"] == 15
assert car_state.avh_dat == raw_avh
@pytest.mark.parametrize("platform, expected_bus, start_frame", [
(CAR.SUBARU_OUTBACK_2023, CanBus.alt, 101),
(CAR.SUBARU_LEGACY_2025, CanBus.alt, 401),
@@ -292,94 +276,6 @@ def test_stop_start_request_is_bounded_and_uses_live_dashlights(platform, expect
assert controller.stop_start_acknowledged
def test_avh_request_sets_observed_bit_and_pulses_at_native_rate():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_LEGACY_2025)
controller = CarController({}, CP)
controller.frame = 101
class TestActuators:
steeringAngleDeg = 0.0
def as_builder(self):
return SimpleNamespace(steeringAngleDeg=self.steeringAngleDeg)
CC = SimpleNamespace(
enabled=False,
latActive=False,
longActive=False,
actuators=TestActuators(),
hudControl=SimpleNamespace(leadVisible=False),
cruiseControl=SimpleNamespace(cancel=False),
)
CS = SimpleNamespace(
canValid=True,
avh_msg={"COUNTER": 15, "AVH": 0},
avh_dat=bytes.fromhex("230f1c4208800000"),
out=SimpleNamespace(
standstill=True,
gearShifter=structs.CarState.GearShifter.park,
vEgoRaw=0.0,
steeringAngleDeg=0.0,
),
)
toggles = SimpleNamespace(subaru_stop_start_off=False, subaru_avh_on=True, subaru_sng=False)
# Start the request from the current live counter. AVH is a native 10 Hz
# frame, so the controller waits for each next live counter before sending
# its matching button frame.
_, can_sends = controller.update(CC, CS, 0, toggles)
avh_msgs = [msg for msg in can_sends if msg[0] == 0x32b]
assert not avh_msgs
CS.avh_msg["COUNTER"] = 0
CS.avh_dat = bytes.fromhex("14001c4208800000")
controller.frame = 103
_, can_sends = controller.update(CC, CS, 0, toggles)
avh_msgs = [msg for msg in can_sends if msg[0] == 0x32b]
assert avh_msgs == [(0x32b, bytes.fromhex("34001c4208a00000"), CanBus.alt)]
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("AVH", 0)], CanBus.alt)
parser.update([(CanBus.alt, avh_msgs)])
assert parser.vl["AVH"]["AVH"] == 1
assert parser.vl["AVH"]["COUNTER"] == 0
controller.frame = 104
_, can_sends = controller.update(CC, CS, 0, toggles)
assert not any(msg[0] == 0x32b for msg in can_sends)
avh_msgs = []
for counter in range(1, 15):
CS.avh_msg["COUNTER"] = counter
raw_dat = bytearray.fromhex("14001c4208800000")
raw_dat[1] = counter
raw_dat[0] = ((0x32B & 0xFF) + ((0x32B >> 8) & 0xFF) + sum(raw_dat[1:])) & 0xFF
CS.avh_dat = bytes(raw_dat)
controller.frame = 103 + (counter * 10)
_, can_sends = controller.update(CC, CS, 0, toggles)
sent = [msg for msg in can_sends if msg[0] == 0x32b]
assert len(sent) == 1
avh_msgs.extend(sent)
assert len(avh_msgs) == 14
assert [msg[1][1] & 0x0F for msg in avh_msgs] == list(range(1, 15))
assert all(msg[1][5] & 0x20 for msg in avh_msgs)
assert not controller.avh_attempted
CS.avh_msg["COUNTER"] = 15
CS.avh_dat = bytes.fromhex("230f1c4208800000")
controller.frame = 253
_, can_sends = controller.update(CC, CS, 0, toggles)
assert not any(msg[0] == 0x32b for msg in can_sends)
assert controller.avh_attempted
CS.avh_msg["COUNTER"] = 0
CS.avh_dat = bytes.fromhex("14001c4208800000")
controller.frame = 131
_, can_sends = controller.update(CC, CS, 0, toggles)
assert not any(msg[0] == 0x32b for msg in can_sends)
assert controller.avh_attempted
def test_legacy_2025_uses_gen2_angle_bus_layout():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_LEGACY_2025)
parsers = CarState.get_can_parsers(CP)
@@ -391,7 +287,6 @@ def test_legacy_2025_uses_gen2_angle_bus_layout():
assert not (CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.D_PLATFORM_CAMERA)
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.FIXED_ANGLE_LIMITS
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.STOP_START_BUTTON
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.AVH_BUTTON
assert CanBus.main_for_cp(CP) == CanBus.main
assert CanBus.angle_for_cp(CP) == CanBus.main
assert parsers[Bus.pt].bus == CanBus.main
@@ -89,7 +89,6 @@ class SubaruSafetyFlags(IntFlag):
D_PLATFORM_CAMERA = 64
FIXED_ANGLE_LIMITS = 128
STOP_START_BUTTON = 256
AVH_BUTTON = 512
LEGACY_2025_ANGLE_LIMITS = FIXED_ANGLE_LIMITS
@@ -276,11 +275,6 @@ SUBARU_STOP_START_CARS = (
CAR.SUBARU_LEGACY_2025,
)
SUBARU_AVH_CARS = (
CAR.SUBARU_LEGACY_2025,
)
SUBARU_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
p16(uds.DATA_IDENTIFIER_TYPE.APPLICATION_DATA_IDENTIFICATION)
SUBARU_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
+4 -2
View File
@@ -75,6 +75,7 @@ class CarState(CarStateBase):
self.distance_button = 0
self.pcm_follow_distance = 0
self.pcm_acc_status = 0
self.acc_type = 1
self.lkas_hud = {}
@@ -208,6 +209,7 @@ class CarState(CarStateBase):
if self.CP.openpilotLongitudinalControl:
ret.accFaulted = ret.accFaulted or cp.vl["PCM_CRUISE_2"]["LOW_SPEED_LOCKOUT"] == 2
prev_pcm_acc_status = self.pcm_acc_status
self.pcm_acc_status = cp.vl["PCM_CRUISE"]["CRUISE_STATE"]
if self.CP.carFingerprint not in (NO_STOP_TIMER_CAR - TSS2_CAR):
# ignore standstill state in certain vehicles, since pcm allows to restart with just an acceleration request
@@ -264,8 +266,8 @@ class CarState(CarStateBase):
buttonEvents += create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
buttonEvents += [
*create_button_events(self.pcm_acc_status == 9, False, {1: ButtonType.accelCruise}),
*create_button_events(self.pcm_acc_status == 10, False, {1: ButtonType.decelCruise}),
*create_button_events(self.pcm_acc_status == 9, prev_pcm_acc_status == 9, {1: ButtonType.accelCruise}),
*create_button_events(self.pcm_acc_status == 10, prev_pcm_acc_status == 10, {1: ButtonType.decelCruise}),
]
fp_ret.dashboardSpeedLimit = calculate_speed_limit(cp_cam)
@@ -11,7 +11,6 @@ TransmissionType = structs.CarParams.TransmissionType
# Must match VOLVO_SPEED_TO_MS in opendbc/safety/modes/volvo.h.
SPEED_TO_MS = 0.003977
STEERING_PRESSED_THRESHOLD = 2
STEERING_DISENGAGE_THRESHOLD = 5
class CarState(CarStateBase):
@@ -75,11 +74,9 @@ class CarState(CarStateBase):
ret.steeringAngleDeg = cp_party.vl['PSCM']['PSCM_ANGLE_SENSOR'] # openpilot expects a negative value for a right turn
#ret.steeringAngleDeg = cp_party.vl['SAS']['SAS_ANGLE_SENSOR']
# Driver steering torque feedback (used for driver override detection)
ret.steeringTorque = -cp_party.vl['DRIVER_INPUT']['STEERING_DRIVER_INPUT'] # Car right turn is negative, openpilot right turn is positive
driver_input = abs(cp_party.vl['DRIVER_INPUT']['STEERING_DRIVER_INPUT'])
ret.steeringPressed = driver_input > STEERING_PRESSED_THRESHOLD
ret.steeringDisengage = driver_input > STEERING_DISENGAGE_THRESHOLD
# EPS status - placeholder until actual signal is found
self.eps_active = True # Assume EPS is active for now
@@ -1,10 +1,5 @@
CM_ "IMPORT _subaru_global.dbc";
BO_ 811 AVH: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
SG_ AVH : 45|1@0+ (1,0) [0|1] "" XXX
BO_ 72 Transmission: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
@@ -307,11 +307,6 @@ VAL_ 544 AEB_Status 12 "AEB related" 8 "AEB actuation" 4 "AEB related" 0 "No AEB
CM_ "subaru_global_2017.dbc starts here";
BO_ 811 AVH: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
SG_ AVH : 45|1@0+ (1,0) [0|1] "" XXX
BO_ 72 Transmission: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
+2 -35
View File
@@ -42,7 +42,6 @@
#define MSG_SUBARU_ES_STATIC_1 0x22aU
#define MSG_SUBARU_ES_STATIC_2 0x325U
#define MSG_SUBARU_Dashlights 0x390U
#define MSG_SUBARU_AVH 0x32bU
#define SUBARU_MAIN_BUS 0U
#define SUBARU_ALT_BUS 1U
@@ -66,13 +65,6 @@
#define SUBARU_STOP_START_TX_MSGS(bus) \
{MSG_SUBARU_Dashlights, bus, 8, .check_relay = false}, \
#define SUBARU_AVH_TX_MSGS(bus) \
{MSG_SUBARU_AVH, bus, 8, .check_relay = false}, \
#define SUBARU_STOP_START_AVH_TX_MSGS(bus) \
SUBARU_STOP_START_TX_MSGS(bus) \
SUBARU_AVH_TX_MSGS(bus)
#define SUBARU_COMMON_LONG_TX_MSGS(alt_bus) \
{MSG_SUBARU_ES_Distance, alt_bus, 8, .check_relay = true}, \
{MSG_SUBARU_ES_Brake, alt_bus, 8, .check_relay = true}, \
@@ -121,7 +113,6 @@ static bool subaru_lkas_angle = false;
static bool subaru_d_platform = false;
static bool subaru_fixed_angle_limits = false;
static bool subaru_stop_start_button = false;
static bool subaru_avh_button = false;
static uint32_t subaru_get_checksum(const CANPacket_t *msg) {
return (uint8_t)msg->data[0];
@@ -306,13 +297,6 @@ static bool subaru_tx_hook(const CANPacket_t *msg) {
violation |= subaru_get_checksum(msg) != subaru_compute_checksum(msg);
}
if (msg->addr == MSG_SUBARU_AVH) {
violation |= !subaru_avh_button;
violation |= msg->bus != (subaru_gen2 ? SUBARU_ALT_BUS : SUBARU_MAIN_BUS);
violation |= !GET_BIT(msg, 45U);
violation |= subaru_get_checksum(msg) != subaru_compute_checksum(msg);
}
if (violation){
tx = false;
}
@@ -363,12 +347,6 @@ static safety_config subaru_init(uint16_t param) {
SUBARU_STOP_START_TX_MSGS(SUBARU_ALT_BUS)
};
static const CanMsg SUBARU_GEN2_LKAS_ANGLE_STOP_START_AVH_TX_MSGS[] = {
SUBARU_BASE_TX_MSGS(SUBARU_ALT_BUS, MSG_SUBARU_ES_LKAS_ANGLE)
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
SUBARU_STOP_START_AVH_TX_MSGS(SUBARU_ALT_BUS)
};
static const CanMsg SUBARU_D_PLATFORM_ANGLE_MAIN_TX_MSGS[] = {
SUBARU_D_PLATFORM_ANGLE_TX_MSGS(SUBARU_MAIN_BUS)
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
@@ -380,12 +358,6 @@ static safety_config subaru_init(uint16_t param) {
SUBARU_STOP_START_TX_MSGS(SUBARU_ALT_BUS)
};
static const CanMsg SUBARU_D_PLATFORM_ANGLE_STOP_START_AVH_MAIN_TX_MSGS[] = {
SUBARU_D_PLATFORM_ANGLE_TX_MSGS(SUBARU_MAIN_BUS)
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
SUBARU_STOP_START_AVH_TX_MSGS(SUBARU_ALT_BUS)
};
static const CanMsg SUBARU_D_PLATFORM_ANGLE_CAMERA_TX_MSGS[] = {
SUBARU_D_PLATFORM_ANGLE_TX_MSGS(SUBARU_CAM_BUS)
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
@@ -433,9 +405,6 @@ static safety_config subaru_init(uint16_t param) {
const uint16_t SUBARU_PARAM_STOP_START_BUTTON = 256;
subaru_stop_start_button = GET_FLAG(param, SUBARU_PARAM_STOP_START_BUTTON);
const uint16_t SUBARU_PARAM_AVH_BUTTON = 512;
subaru_avh_button = GET_FLAG(param, SUBARU_PARAM_AVH_BUTTON);
#ifdef ALLOW_DEBUG
const uint16_t SUBARU_PARAM_LONGITUDINAL = 2;
subaru_longitudinal = GET_FLAG(param, SUBARU_PARAM_LONGITUDINAL);
@@ -443,12 +412,10 @@ static safety_config subaru_init(uint16_t param) {
safety_config ret;
if (subaru_lkas_angle) {
ret = subaru_d_platform ? (subaru_stop_start_button ? (subaru_avh_button ? BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_STOP_START_AVH_MAIN_TX_MSGS) : \
BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_STOP_START_MAIN_TX_MSGS)) : \
ret = subaru_d_platform ? (subaru_stop_start_button ? BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_STOP_START_MAIN_TX_MSGS) : \
(subaru_d_platform_camera ? BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_CAMERA_TX_MSGS) : \
BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_MAIN_TX_MSGS))) : \
subaru_gen2 ? (subaru_stop_start_button ? (subaru_avh_button ? BUILD_SAFETY_CFG(subaru_gen2_lkas_angle_rx_checks, SUBARU_GEN2_LKAS_ANGLE_STOP_START_AVH_TX_MSGS) : \
BUILD_SAFETY_CFG(subaru_gen2_lkas_angle_rx_checks, SUBARU_GEN2_LKAS_ANGLE_STOP_START_TX_MSGS)) : \
subaru_gen2 ? (subaru_stop_start_button ? BUILD_SAFETY_CFG(subaru_gen2_lkas_angle_rx_checks, SUBARU_GEN2_LKAS_ANGLE_STOP_START_TX_MSGS) : \
BUILD_SAFETY_CFG(subaru_gen2_lkas_angle_rx_checks, SUBARU_GEN2_LKAS_ANGLE_TX_MSGS)) : \
BUILD_SAFETY_CFG(subaru_lkas_angle_rx_checks, SUBARU_LKAS_ANGLE_TX_MSGS);
} else if (subaru_gen2) {
@@ -43,7 +43,6 @@
#define VOLVO_ANGLE_DEG_TO_CAN 17.869907f
#define VOLVO_MAX_ANGLE_CAN 9650
#define VOLVO_RELAY_ANGLE_TOLERANCE 54 // approximately 3 degrees
#define VOLVO_DRIVER_OVERRIDE 5
// CAN bus definitions for Volvo
@@ -83,8 +82,6 @@ static const AngleSteeringLimits VOLVO_ANGLE_STEERING_LIMITS = {
};
static void volvo_rx_hook(const CANPacket_t *msg) {
// Monitor the vehicle state required for cruise, disengagement, and angle
// safety. All steering TX frames are separately constrained in volvo_tx_hook.
// Main bus (bus 0) messages
if (msg->bus == VOLVO_MAIN_BUS) {
@@ -148,13 +145,11 @@ static void volvo_rx_hook(const CANPacket_t *msg) {
// DRIVER_INPUT is the signal consumed by carstate.py for driver torque.
// The PSCM frame's DRIVER_INPUT_DEVIATION is a different signal and must
// not be substituted here: doing so leaves the hardware disengage path blind.
if (msg->addr == VOLVO_DRIVER_INPUT) {
// STEERING_DRIVER_INPUT is a Motorola signal starting at bit 55. The
// DBC also carries a +1 offset, so its raw byte is data[6].
const int driver_input = to_signed(msg->data[6], 8) + 1;
update_sample(&torque_driver, driver_input);
steering_disengage = SAFETY_ABS(driver_input) > VOLVO_DRIVER_OVERRIDE;
}
}
@@ -37,7 +37,6 @@ class SubaruMsg(enum.IntEnum):
ES_STATIC_1 = 0x22a
ES_STATIC_2 = 0x325
Dashlights = 0x390
AVH = 0x32b
SUBARU_MAIN_BUS = 0
@@ -386,20 +385,6 @@ class TestSubaruGen2FixedAngleStopStartSafety(TestSubaruGen2FixedAngleSafety):
self.assertFalse(self._tx(self._stop_start_msg(False)))
class TestSubaruGen2FixedAngleStopStartAvhSafety(TestSubaruGen2FixedAngleStopStartSafety):
FLAGS = TestSubaruGen2FixedAngleStopStartSafety.FLAGS | SubaruSafetyFlags.AVH_BUTTON
TX_MSGS = TestSubaruGen2FixedAngleStopStartSafety.TX_MSGS + [[SubaruMsg.AVH, SUBARU_ALT_BUS]]
def _avh_msg(self, pressed):
return self.packer.make_can_msg_safety(
"AVH", SUBARU_ALT_BUS, {"COUNTER": 0, "AVH": pressed},
)
def test_avh_tx_requires_pressed_bit(self):
self.assertTrue(self._tx(self._avh_msg(True)))
self.assertFalse(self._tx(self._avh_msg(False)))
class TestSubaruDPlatformAngleSafety(TestSubaruStockLongitudinalSafetyBase, TestSubaruAngleSafetyBase):
FLAGS = SubaruSafetyFlags.GEN2 | SubaruSafetyFlags.LKAS_ANGLE | SubaruSafetyFlags.D_PLATFORM
ALT_MAIN_BUS = SUBARU_ALT_BUS
@@ -211,21 +211,17 @@ class TestVolvoSafetyBase(common.CarSafetyTest):
self.assertTrue(self._tx(valid))
self.assertFalse(self._tx(invalid))
def test_driver_override_disengages_controls(self):
def test_driver_input_is_a_normal_override(self):
def driver_input_msg(value):
return self.mid_packer.make_can_msg_safety(
"DRIVER_INPUT", VOLVO_PARTY_BUS, {"STEERING_DRIVER_INPUT": value})
for value in (2, 3, 5):
for value in (2, 3, 5, 6, 20, -20):
self._rx(driver_input_msg(0))
self.safety.set_controls_allowed(True)
self._rx(driver_input_msg(value))
self.assertTrue(self.safety.get_controls_allowed(), f"unexpected disengage at {value=}")
self._rx(driver_input_msg(0))
self.safety.set_controls_allowed(True)
self._rx(driver_input_msg(6))
self.assertFalse(self.safety.get_controls_allowed())
self.assertTrue(self.safety.get_controls_allowed(), f"unexpected safety disengage at {value=}")
self.assertFalse(self.safety.get_steering_disengage_prev())
# ---- Volvo-specific consistency tests ----
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,2 +1,2 @@
extern const uint8_t gitversion[19];
const uint8_t gitversion[19] = "DEV-26ce46ba-DEBUG";
const uint8_t gitversion[19] = "DEV-b5ab54ab-DEBUG";
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
DEV-26ce46ba-DEBUG
DEV-b5ab54ab-DEBUG
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Live-reload dev launcher for The Galaxy.
# Usage:
# scripts/galaxy_live.sh # serve repo live on :8083
# scripts/galaxy_live.sh 8099 # or a specific port
# scripts/galaxy_live.sh --sync # sync host runtime first (after big pulls)
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PLATFORM="$(uname -s | tr '[:upper:]' '[:lower:]')"
WT="${ROOT}/.host_runtime/${PLATFORM}/worktree"
GX="starpilot/system/the_galaxy"
PORT="${SP_GALAXY_PORT:-8083}"
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
echo "Usage: scripts/galaxy_live.sh [--sync] [port]"
exit 0
fi
if [[ "${1:-}" == "--sync" ]]; then
shift
"${ROOT}/scripts/host_tool_runner.sh" sync
fi
if [[ -n "${1:-}" && "${1}" =~ ^[0-9]+$ ]]; then
PORT="${1}"
fi
if [[ ! -d "${WT}/.venv" || ! -f "${WT}/${GX}/the_galaxy.py" ]]; then
echo "Host runtime not ready yet. Run once: ${ROOT}/dev galaxy (then stop it)"
exit 1
fi
rm -rf "${WT}/${GX}"
ln -s "${ROOT}/${GX}" "${WT}/${GX}"
echo "Galaxy live-dev -> http://127.0.0.1:${PORT}/ (backend auto-reload ON)"
echo "Edit repo files. Backend .py restarts; frontend needs a hard-refresh. Errors print here."
echo "Stop with Ctrl+C."
cd "${WT}"
export PYTHONPATH="${WT}:${WT}/starpilot/third_party"
for d in "${WT}"/*_repo; do
[[ -d "${d}" ]] && export PYTHONPATH="${PYTHONPATH}:${d}"
done
export SP_GALAXY_DIR="${SP_GALAXY_DIR:-${HOME}/.comma/starpilot/data/galaxy}"
export SP_GALAXY_HOST="0.0.0.0"
export SP_GALAXY_PORT="${PORT}"
export SP_GALAXY_DEBUG="1"
export SP_GALAXY_RELOAD="1"
exec "${WT}/.venv/bin/python3" -m openpilot.starpilot.system.the_galaxy.the_galaxy
+590
View File
@@ -0,0 +1,590 @@
#!/usr/bin/env python3
"""Build and publish Chestnut/AMD variants for every small manifest model.
The queue is intentionally sequential. It keeps only one ONNX source and one
compiler output on the comma, copies each verified artifact back to the host,
uploads it, then publishes a freshly merged manifest. The state file makes an
interrupted run resumable without rebuilding completed models.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import shlex
import shutil
import subprocess
import sys
import time
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPTS_DIR = REPO_ROOT / "scripts"
if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))
from model_compiler import detect_component
from model_rebuild_pipeline import ensure_workspace, extract_model, find_model_paths, ensure_git_ref
DEFAULT_REMOTE = os.environ.get("STAR_PILOT_MODEL_REMOTE", "comma@192.168.3.110")
DEFAULT_BUCKET = os.environ.get("STARPILOT_HF_BUCKET", "StarPilot-Driving/StarPilot-Resources")
DEFAULT_ARTIFACT_DIR = Path.home() / "StarPilot-Model-Lab-Artifacts" / "v25"
DEFAULT_MANIFEST = DEFAULT_ARTIFACT_DIR / "model_names_v25.json"
DEFAULT_SOURCE_MAP = SCRIPTS_DIR / "model_source_map_v25.json"
DEFAULT_OPENPILOT = Path.home() / "openpilot"
REMOTE_ROOT = "/data/openpilot"
SSH_OPTIONS = (
"-o", "ConnectTimeout=10",
"-o", "ConnectionAttempts=1",
"-o", "ServerAliveInterval=30",
"-o", "ServerAliveCountMax=600",
)
RSYNC_SSH = "ssh -o ConnectTimeout=10 -o ConnectionAttempts=1 -o ServerAliveInterval=30 -o ServerAliveCountMax=600"
SAFE_MODEL_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
COMPONENT_FILENAMES = {
"driving_supercombo": "driving_supercombo.onnx",
"driving_vision": "driving_vision.onnx",
"driving_policy": "driving_policy.onnx",
"driving_on_policy": "driving_on_policy.onnx",
"driving_off_policy": "driving_off_policy.onnx",
}
def utc_now() -> str:
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
def load_json(path: Path):
return json.loads(path.read_text(encoding="utf-8"))
def write_json(path: Path, payload: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
temporary.replace(path)
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def run(command: list[str], *, capture: bool = False, check: bool = True, timeout: int | None = None,
stdout=None, stderr=None) -> subprocess.CompletedProcess:
return subprocess.run(
command,
text=capture,
capture_output=capture,
check=check,
timeout=timeout,
stdout=stdout,
stderr=stderr,
)
def validate_model_id(model_id: str) -> str:
if not SAFE_MODEL_ID.fullmatch(model_id):
raise ValueError(f"Unsafe model ID: {model_id!r}")
return model_id
class Batch:
def __init__(self, args: argparse.Namespace):
self.args = args
self.hf = shutil.which("hf")
if not self.hf:
raise FileNotFoundError("Hugging Face CLI (hf) is not installed")
self.artifact_dir = args.artifact_dir.expanduser().resolve()
self.workspace = self.artifact_dir / "batch"
self.sources_workspace = self.workspace / "source-workspace"
self.sources_dir = self.sources_workspace / "onnx"
self.logs_dir = self.workspace / "logs"
self.results_dir = self.workspace / "results"
self.state_path = self.results_dir / "chestnut_batch_state.json"
self.manifest_path = args.manifest.expanduser().resolve()
self.source_map = load_json(args.source_map.expanduser().resolve())
self.manifest = load_json(self.manifest_path)
self.models = self.manifest.get("models", self.manifest)
if not isinstance(self.models, list):
raise ValueError("Manifest must contain a models list")
self.models_by_id = {str(model.get("id") or ""): model for model in self.models}
ensure_workspace(self.sources_workspace)
self.logs_dir.mkdir(parents=True, exist_ok=True)
self.results_dir.mkdir(parents=True, exist_ok=True)
self.state = self._load_state()
self.inventory = self._load_inventory()
@property
def bucket_root(self) -> str:
return f"hf://buckets/{self.args.bucket}"
def _load_state(self) -> dict:
if self.state_path.is_file():
state = load_json(self.state_path)
state.setdefault("models", {})
state["resumed_at"] = utc_now()
return state
return {
"remote": self.args.remote,
"bucket": self.args.bucket,
"started_at": utc_now(),
"models": {},
}
def save_state(self) -> None:
self.state["updated_at"] = utc_now()
write_json(self.state_path, self.state)
def _load_inventory(self) -> dict[str, list[dict]]:
result = run(
[self.hf, "buckets", "ls", "-R", f"{self.bucket_root}/onnx/", "--format", "json"],
capture=True,
)
entries = json.loads(result.stdout)
inventory: dict[str, list[dict]] = {}
for entry in entries:
path = str(entry.get("path") or "")
parts = Path(path).parts
if len(parts) == 3 and parts[0] == "onnx" and path.endswith(".onnx"):
inventory.setdefault(parts[1], []).append(entry)
return inventory
def selected_models(self) -> list[dict]:
requested = {validate_model_id(value) for value in self.args.ids.split(",") if value} if self.args.ids else set()
selected = [model for model in self.models if not bool(model.get("uses_external_gpu", False))]
if requested:
unknown = requested - self.models_by_id.keys()
if unknown:
raise ValueError(f"Unknown manifest model IDs: {', '.join(sorted(unknown))}")
selected = [model for model in selected if model["id"] in requested]
if self.args.limit:
selected = selected[:self.args.limit]
return selected
def source_plan(self, model: dict) -> dict:
model_id = validate_model_id(model["id"])
source = self.source_map.get(model_id)
if not isinstance(source, dict):
raise KeyError(f"No source mapping for small model {model_id}")
source_id = validate_model_id(str(source.get("source_id") or model_id))
archived = self.inventory.get(source_id, [])
if archived:
components = [detect_component(Path(entry["path"])) for entry in archived]
if None in components or len(set(components)) != len(components):
raise ValueError(f"Ambiguous archived ONNX components for {model_id} ({source_id})")
self._validate_components(model_id, source["input_format"], set(components))
signature_payload = {
"input_format": source["input_format"],
"version": str(model.get("version") or ""),
"files": sorted(
(detect_component(Path(entry["path"])), str(entry.get("xet_hash") or ""), int(entry.get("size") or 0))
for entry in archived
),
}
signature = hashlib.sha256(json.dumps(signature_payload, sort_keys=True).encode()).hexdigest()
return {"kind": "archive", "source_id": source_id, "files": archived, "signature": signature, **source}
repo = self.args.openpilot.expanduser().resolve()
ensure_git_ref(repo, source["ref"])
paths = find_model_paths(repo, source["ref"], source["input_format"], False)
components = {detect_component(Path(path)) for path in paths}
self._validate_components(model_id, source["input_format"], components)
signature = hashlib.sha256(
f"git:{source['ref']}:{source['input_format']}:{model.get('version', '')}".encode()
).hexdigest()
return {"kind": "git", "source_id": source_id, "files": paths, "signature": signature, **source}
@staticmethod
def _validate_components(model_id: str, input_format: str, components: set[str | None]) -> None:
if input_format == "supercombo" and components != {"driving_supercombo"}:
raise ValueError(f"{model_id} needs one supercombo source, found {sorted(str(c) for c in components)}")
if input_format == "split" and (
"driving_vision" not in components or not {"driving_policy", "driving_on_policy"} & components
):
raise ValueError(f"{model_id} has incomplete split sources: {sorted(str(c) for c in components)}")
def audit(self) -> dict:
selected = self.selected_models()
report = {"total": len(selected), "archive": [], "git": [], "failures": {}}
for model in selected:
model_id = model["id"]
try:
plan = self.source_plan(model)
report[plan["kind"]].append(model_id)
except Exception as error:
report["failures"][model_id] = str(error)
report["ready"] = report["total"] - len(report["failures"])
print(json.dumps(report, indent=2), flush=True)
return report
def remote(self, command: str, *, capture: bool = False, check: bool = True,
timeout: int | None = None, stdout=None, stderr=None) -> subprocess.CompletedProcess:
return run(
["ssh", *SSH_OPTIONS, self.args.remote, command],
capture=capture,
check=check,
timeout=timeout,
stdout=stdout,
stderr=stderr,
)
def hardware_preflight(self) -> None:
command = (
f"set -eu; cd {shlex.quote(REMOTE_ROOT)}; "
"test \"$(cat /data/params/d/IsOffroad 2>/dev/null)\" = 1; "
"/usr/local/venv/bin/python3 -c "
+ shlex.quote("from openpilot.system.hardware.chestnut.flash import link_up; raise SystemExit(0 if link_up() else 1)")
+ "; test -x /data/openpilot/models"
)
result = self.remote(command, capture=True, check=False, timeout=20)
if result.returncode:
raise RuntimeError("Comma must be reachable, offroad, and connected to an active Chestnut PCIe link")
def active_remote_compiles(self) -> list[str]:
result = self.remote("pgrep -af '[c]ompile_modeld.py' || true", capture=True, check=False, timeout=20)
if result.returncode and not result.stdout:
raise RuntimeError(f"Could not inspect remote compiler: {result.stderr.strip()}")
return [line for line in result.stdout.splitlines() if line.strip()]
def wait_for_remote_idle(self) -> None:
active = self.active_remote_compiles()
while active:
print(f"REMOTE_BUSY processes={len(active)}", flush=True)
time.sleep(30)
active = self.active_remote_compiles()
def _source_dir(self, model_id: str) -> Path:
return self.sources_dir / validate_model_id(model_id)
def prepare_source(self, model: dict, plan: dict) -> Path:
model_id = model["id"]
source_dir = self._source_dir(model_id)
if source_dir.is_dir():
shutil.rmtree(source_dir)
source_dir.mkdir(parents=True)
if plan["kind"] == "archive":
for entry in plan["files"]:
component = detect_component(Path(entry["path"]))
if component is None:
raise ValueError(f"Unknown source component: {entry['path']}")
destination = source_dir / f"{model_id}_{COMPONENT_FILENAMES[component]}"
run([
self.hf, "buckets", "cp",
f"{self.bucket_root}/{entry['path']}", str(destination), "--format", "quiet",
])
expected_size = int(entry.get("size") or 0)
if expected_size and destination.stat().st_size != expected_size:
raise ValueError(f"Downloaded source size mismatch for {destination.name}")
else:
extract_model(model_id, self.source_map[model_id], self.args.openpilot.expanduser().resolve(), self.sources_workspace)
for path in sorted(source_dir.glob("*.onnx")):
component = detect_component(path)
if component is None:
raise ValueError(f"Unknown extracted source component: {path.name}")
archive_name = f"{plan['source_id']}_{COMPONENT_FILENAMES[component]}"
destination = f"{self.bucket_root}/onnx/{plan['source_id']}/{archive_name}"
run([self.hf, "buckets", "cp", str(path), destination, "--format", "quiet"])
print(f"SOURCE_ARCHIVED id={model_id} source_id={plan['source_id']}", flush=True)
return source_dir
def remote_paths(self, model_id: str) -> tuple[str, str]:
validate_model_id(model_id)
return (
f"{REMOTE_ROOT}/uncompiledmodels/{model_id}",
f"{REMOTE_ROOT}/compiledmodels/{model_id}_driving_tinygrad.pkl",
)
def cleanup_remote(self, model_id: str, *, source: bool = True, output: bool = True) -> None:
remote_source, remote_output = self.remote_paths(model_id)
targets = []
if source:
targets.append(shlex.quote(remote_source))
if output:
targets.append(shlex.quote(remote_output))
if targets:
self.remote("rm -rf -- " + " ".join(targets), check=False, timeout=30)
def stage_source(self, model_id: str, source_dir: Path) -> None:
remote_source, _ = self.remote_paths(model_id)
self.cleanup_remote(model_id)
self.remote(f"mkdir -p {shlex.quote(remote_source)} {shlex.quote(REMOTE_ROOT + '/compiledmodels')}")
run([
"rsync", "-az", "-e", RSYNC_SSH, "--exclude=._*",
f"{source_dir}/", f"{self.args.remote}:{remote_source}/",
])
def compile(self, model: dict, plan: dict) -> Path:
model_id = model["id"]
remote_source, remote_output = self.remote_paths(model_id)
command = " ".join([
f"cd {shlex.quote(REMOTE_ROOT)} && ./models",
"--model", shlex.quote(model_id),
"--input-dir", shlex.quote(remote_source),
"--output-dir", shlex.quote(REMOTE_ROOT + "/compiledmodels"),
"--input-format", shlex.quote(plan["input_format"]),
"--version", shlex.quote(str(model.get("version") or "")),
"--gpu", "--no-split",
])
log_path = self.logs_dir / f"{model_id}.log"
print(f"COMPILE_START id={model_id} source={plan['kind']} version={model.get('version', '')}", flush=True)
started = time.monotonic()
with log_path.open("ab") as log:
log.write(f"\n=== START {utc_now()} ===\n".encode())
result = self.remote(command, check=False, stdout=log, stderr=subprocess.STDOUT)
if result.returncode:
self.wait_for_remote_idle()
if not self.remote_file_exists(remote_output):
raise RuntimeError(f"Chestnut compilation failed; see {log_path}")
elapsed = time.monotonic() - started
print(f"COMPILE_DONE id={model_id} seconds={elapsed:.1f}", flush=True)
return self.pull_artifact(model_id)
def remote_file_exists(self, path: str) -> bool:
result = self.remote(f"test -f {shlex.quote(path)}", check=False, timeout=20)
return result.returncode == 0
def pull_artifact(self, model_id: str) -> Path:
_, remote_output = self.remote_paths(model_id)
destination = self.artifact_dir / f"{model_id}_driving_chestnut_tinygrad.pkl"
incoming = destination.with_suffix(destination.suffix + ".incoming")
incoming.unlink(missing_ok=True)
run(["rsync", "-az", "-e", RSYNC_SSH, f"{self.args.remote}:{remote_output}", str(incoming)])
if not incoming.is_file() or incoming.stat().st_size == 0:
raise FileNotFoundError(f"No compiler output for {model_id}")
incoming.replace(destination)
destination.chmod(0o644)
return destination
def artifact_metadata(self, model_id: str, artifact: Path) -> dict:
expected_name = f"{model_id}_driving_chestnut_tinygrad.pkl"
if artifact.name != expected_name or not artifact.is_file():
raise ValueError(f"Invalid local Chestnut artifact path for {model_id}: {artifact}")
return {
"artifact_format": "tinygrad_single_v1",
"artifact_filename": artifact.name,
"artifact_size": artifact.stat().st_size,
"artifact_sha256": sha256_file(artifact),
"artifact_chunk_count": 0,
"execution_device": "AMD",
}
def upload_artifact(self, model_id: str, artifact: Path) -> None:
destination = f"{self.bucket_root}/models/v25/{model_id}/{artifact.name}"
run([self.hf, "buckets", "cp", str(artifact), destination, "--format", "quiet"])
listing = run([self.hf, "buckets", "ls", "-R", destination, "--format", "json"], capture=True)
entries = json.loads(listing.stdout)
if len(entries) != 1 or int(entries[0].get("size") or 0) != artifact.stat().st_size:
raise RuntimeError(f"Uploaded artifact verification failed for {model_id}")
def completed_metadata(self) -> dict[str, dict]:
completed = {}
for model_id, record in self.state.get("models", {}).items():
if record.get("status") == "published" and isinstance(record.get("artifact"), dict):
completed[model_id] = record["artifact"]
return completed
def publish_manifest(self) -> None:
incoming = self.workspace / "live_manifest.json"
run([
self.hf, "buckets", "cp",
f"{self.bucket_root}/manifests/model_names_v25.json", str(incoming), "--format", "quiet",
])
payload = load_json(incoming)
models = payload.get("models", payload)
completed = self.completed_metadata()
for model in models:
if bool(model.get("uses_external_gpu", False)):
continue
model["model_size"] = "small"
model["model_lab_eligible"] = True
if model["id"] in completed:
artifacts = model.get("accelerator_artifacts")
if not isinstance(artifacts, dict):
artifacts = {}
artifacts["chestnut"] = completed[model["id"]]
model["accelerator_artifacts"] = artifacts
write_json(self.manifest_path, payload if isinstance(payload, dict) else {"models": models})
run([
self.hf, "buckets", "cp", str(self.manifest_path),
f"{self.bucket_root}/manifests/model_names_v25.json", "--format", "quiet",
])
print(f"MANIFEST_PUBLISHED completed={len(completed)}", flush=True)
def valid_existing_artifact(self, model_id: str) -> tuple[Path, dict] | None:
path = self.artifact_dir / f"{model_id}_driving_chestnut_tinygrad.pkl"
if not path.is_file():
return None
metadata = self.artifact_metadata(model_id, path)
manifest_artifact = (
self.models_by_id[model_id].get("accelerator_artifacts", {}).get("chestnut", {})
if isinstance(self.models_by_id[model_id].get("accelerator_artifacts"), dict) else {}
)
if (manifest_artifact.get("artifact_size") == metadata["artifact_size"]
and manifest_artifact.get("artifact_sha256") == metadata["artifact_sha256"]):
return path, metadata
state_artifact = self.state.get("models", {}).get(model_id, {}).get("artifact", {})
if (state_artifact.get("artifact_size") == metadata["artifact_size"]
and state_artifact.get("artifact_sha256") == metadata["artifact_sha256"]):
return path, metadata
return None
@staticmethod
def source_record(plan: dict) -> dict:
return {
"kind": plan["kind"],
"source_id": plan["source_id"],
"ref": plan["ref"],
"signature": plan["signature"],
}
def equivalent_artifact(self, model_id: str, plan: dict) -> tuple[str, Path] | None:
"""Find a completed artifact built from byte-identical ONNXs and ABI."""
for candidate_id, record in self.state.get("models", {}).items():
if candidate_id == model_id or record.get("status") != "published":
continue
candidate_signature = record.get("source", {}).get("signature")
if not candidate_signature and candidate_id in self.models_by_id:
try:
candidate_signature = self.source_plan(self.models_by_id[candidate_id])["signature"]
except Exception:
continue
candidate_path = self.artifact_dir / f"{candidate_id}_driving_chestnut_tinygrad.pkl"
if candidate_signature == plan["signature"] and candidate_path.is_file():
return candidate_id, candidate_path
return None
def process_model(self, model: dict) -> None:
model_id = model["id"]
plan = self.source_plan(model)
existing = self.valid_existing_artifact(model_id)
if existing:
artifact, metadata = existing
if self.state.get("models", {}).get(model_id, {}).get("status") != "published":
self.upload_artifact(model_id, artifact)
self.state["models"][model_id] = {
"status": "published",
"source": self.source_record(plan),
"artifact": metadata,
"completed_at": utc_now(),
}
self.save_state()
print(f"SKIP_VERIFIED id={model_id} bytes={metadata['artifact_size']}", flush=True)
return
equivalent = self.equivalent_artifact(model_id, plan)
if equivalent:
source_model_id, source_artifact = equivalent
artifact = self.artifact_dir / f"{model_id}_driving_chestnut_tinygrad.pkl"
shutil.copy2(source_artifact, artifact)
metadata = self.artifact_metadata(model_id, artifact)
self.upload_artifact(model_id, artifact)
self.state["models"][model_id] = {
"status": "published",
"source": self.source_record(plan),
"derived_from": source_model_id,
"artifact": metadata,
"completed_at": utc_now(),
}
self.save_state()
print(f"PUBLISHED_DEDUP id={model_id} identical_to={source_model_id} bytes={metadata['artifact_size']}", flush=True)
return
self.hardware_preflight()
self.wait_for_remote_idle()
source_dir = self.prepare_source(model, plan)
try:
self.stage_source(model_id, source_dir)
artifact = self.compile(model, plan)
metadata = self.artifact_metadata(model_id, artifact)
self.upload_artifact(model_id, artifact)
self.state["models"][model_id] = {
"status": "published",
"source": self.source_record(plan),
"artifact": metadata,
"completed_at": utc_now(),
}
self.save_state()
print(f"PUBLISHED id={model_id} bytes={metadata['artifact_size']} sha256={metadata['artifact_sha256']}", flush=True)
finally:
self.cleanup_remote(model_id)
if source_dir.is_dir():
shutil.rmtree(source_dir)
def run_queue(self) -> int:
selected = self.selected_models()
audit = self.audit()
if audit["failures"]:
raise RuntimeError(f"Source audit failed for {len(audit['failures'])} small models")
if self.args.dry_run:
return 0
failures = 0
for index, model in enumerate(selected, 1):
model_id = model["id"]
print(f"QUEUE index={index}/{len(selected)} id={model_id}", flush=True)
try:
self.process_model(model)
except Exception as error:
failures += 1
self.state["models"][model_id] = {
"status": "failed",
"error": str(error),
"failed_at": utc_now(),
}
self.save_state()
print(f"FAILED id={model_id} error={error}", file=sys.stderr, flush=True)
if self.args.stop_on_failure:
break
continue
if not self.args.no_publish:
try:
self.publish_manifest()
except Exception as error:
failures += 1
self.state["manifest_error"] = {"error": str(error), "at": utc_now(), "after_model": model_id}
self.save_state()
print(f"MANIFEST_FAILED after={model_id} error={error}", file=sys.stderr, flush=True)
if self.args.stop_on_failure:
break
self.state["finished_at"] = utc_now()
self.state["failures"] = failures
self.save_state()
return 1 if failures else 0
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("command", choices=("audit", "run"), nargs="?", default="run")
parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST)
parser.add_argument("--source-map", type=Path, default=DEFAULT_SOURCE_MAP)
parser.add_argument("--artifact-dir", type=Path, default=DEFAULT_ARTIFACT_DIR)
parser.add_argument("--openpilot", type=Path, default=DEFAULT_OPENPILOT)
parser.add_argument("--remote", default=DEFAULT_REMOTE)
parser.add_argument("--bucket", default=DEFAULT_BUCKET)
parser.add_argument("--ids", default="", help="Optional comma-separated manifest model IDs")
parser.add_argument("--limit", type=int, default=0)
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--no-publish", action="store_true")
parser.add_argument("--stop-on-failure", action="store_true")
return parser.parse_args()
def main() -> int:
args = parse_args()
batch = Batch(args)
if args.command == "audit":
return 1 if batch.audit()["failures"] else 0
return batch.run_queue()
if __name__ == "__main__":
raise SystemExit(main())
+25 -1
View File
@@ -359,6 +359,11 @@
"input_format": "supercombo",
"source_id": "deeprl3v2"
},
"deeprl3v2": {
"ref": "702fa71ad4dd8de08425eb11a1a42aaeb64892c9",
"input_format": "supercombo",
"source_id": "deeprl3v2"
},
"rh3": {
"ref": "93f5aa469a72b7621aef7da7901c100e0113e4d9",
"input_format": "supercombo",
@@ -379,6 +384,26 @@
"input_format": "supercombo",
"source_id": "rdf2"
},
"rdf33": {
"ref": "ea2151ba4b82854277f37f03b949f15fe2733dc8",
"input_format": "supercombo",
"source_id": "rdf3"
},
"rdf43": {
"ref": "a5a6412d08474cffb49a69afb910756afdee123e",
"input_format": "supercombo",
"source_id": "rdf4"
},
"rdf53": {
"ref": "7fb03ca474f03e95e59ec0c8a6c5fba831bd5fd1",
"input_format": "supercombo",
"source_id": "rdf5"
},
"rdf63": {
"ref": "35703097905a122c9f3ddf0d12889b4873d7e2a2",
"input_format": "supercombo",
"source_id": "rdf6"
},
"tsf": {
"ref": "4d911346cde4e0d2978a625f31679808284cc19d",
"input_format": "supercombo",
@@ -426,4 +451,3 @@
"source_id": "bmrlnapv6"
}
}
+6 -3
View File
@@ -71,6 +71,10 @@ class VCruiseHelper:
long_interval = self._get_cruise_delta_interval(getattr(starpilot_toggles, "cruise_increase_long", None))
return short_interval, long_interval
def _uses_software_cruise(self) -> bool:
return bool(self.gm_cc_only or self.redneck_non_pcm or
not self.CP.pcmCruise or getattr(self.CP, "openpilotLongitudinalControl", False))
@property
def v_cruise_initialized(self):
return self.v_cruise_kph != V_CRUISE_UNSET
@@ -90,7 +94,7 @@ class VCruiseHelper:
self.v_cruise_kph_last = self.v_cruise_kph
if CS.cruiseState.available:
if self.gm_cc_only or self.redneck_non_pcm or not self.CP.pcmCruise:
if self._uses_software_cruise():
# if stock cruise is completely disabled, then we can use our own set speed logic
self._update_v_cruise_non_pcm(CS, enabled, is_metric, speed_limit_changed, starpilot_toggles, starpilot_car_state,
slc_target_with_offset)
@@ -206,8 +210,7 @@ class VCruiseHelper:
def initialize_v_cruise(self, CS, experimental_mode: bool, resume_prev_button: bool,
starpilot_toggles: SimpleNamespace, desired_speed_limit: float = 0.0) -> None:
# initializing is handled by the PCM
if self.CP.pcmCruise and not (self.gm_cc_only or self.redneck_non_pcm):
if self.CP.pcmCruise and not self._uses_software_cruise():
return
engage_floor_kph = max(V_CRUISE_MIN, 7.0 * CV.MPH_TO_KPH)
+44
View File
@@ -482,6 +482,50 @@ class TestVCruiseHelper:
assert self.v_cruise_helper.v_cruise_kph == pytest.approx(initial_v_cruise_kph + IMPERIAL_INCREMENT)
def test_openpilot_longitudinal_pcm_cruise_uses_custom_intervals(self):
CP = car.CarParams(pcmCruise=True, openpilotLongitudinalControl=True)
helper = VCruiseHelper(CP)
toggles = SimpleNamespace(
cruise_increase=5,
cruise_increase_long=1,
is_metric=True,
set_speed_limit=False,
)
helper.initialize_v_cruise(car.CarState(vEgo=40 * CV.KPH_TO_MS), False, False, toggles)
initial_v_cruise_kph = helper.v_cruise_kph
pressed_cs = car.CarState(cruiseState={"available": True})
pressed_cs.buttonEvents = [ButtonEvent(type=ButtonType.accelCruise, pressed=True)]
helper.update_v_cruise(pressed_cs, True, True, False, toggles)
released_cs = car.CarState(cruiseState={"available": True})
released_cs.buttonEvents = [ButtonEvent(type=ButtonType.accelCruise, pressed=False)]
helper.update_v_cruise(released_cs, True, True, False, toggles)
assert helper.v_cruise_kph == pytest.approx(initial_v_cruise_kph + 5)
pressed_cs.buttonEvents = [ButtonEvent(type=ButtonType.accelCruise, pressed=True)]
helper.update_v_cruise(pressed_cs, True, True, False, toggles)
for _ in range(50):
helper.update_v_cruise(car.CarState(cruiseState={"available": True}), True, True, False, toggles)
assert helper.v_cruise_kph == pytest.approx(initial_v_cruise_kph + 6)
def test_stock_pcm_cruise_still_uses_pcm_speed(self):
CP = car.CarParams(pcmCruise=True, openpilotLongitudinalControl=False)
helper = VCruiseHelper(CP)
toggles = SimpleNamespace(cruise_increase=5, cruise_increase_long=1)
pcm_speed_kph = 72.0
cs = car.CarState(
cruiseState={
"available": True,
"speed": pcm_speed_kph * CV.KPH_TO_MS,
"speedCluster": pcm_speed_kph * CV.KPH_TO_MS,
},
)
helper.update_v_cruise(cs, True, True, False, toggles)
assert helper.v_cruise_kph == pytest.approx(pcm_speed_kph)
class TestVCruiseHelperRedneck:
def setup_method(self):
+6 -1
View File
@@ -519,7 +519,9 @@ class LatControlTorque(LatControl):
CS.vEgo, setpoint, prius_deadzone_max,
)
elif genesis_g70_active:
vehicle_friction_jerk_deadzone = get_genesis_g70_friction_jerk_deadzone(CS.vEgo, setpoint)
vehicle_friction_jerk_deadzone = get_genesis_g70_friction_jerk_deadzone(
CS.vEgo, setpoint, desired_lateral_jerk,
)
elif self.is_genesis_gv70:
vehicle_friction_jerk_deadzone = get_genesis_gv70_friction_jerk_deadzone(CS.vEgo, setpoint)
elif kia_carnival_active:
@@ -652,6 +654,9 @@ class LatControlTorque(LatControl):
output_torque *= get_genesis_gv70_high_speed_error_scale(
setpoint, measurement, desired_lateral_jerk, CS.vEgo,
)
output_torque *= get_genesis_gv70_reversal_output_scale(
setpoint, measurement, desired_lateral_jerk, CS.vEgo,
)
elif sonata_hybrid_active:
output_torque *= sonata_hybrid_center_taper
output_torque *= sonata_hybrid_center_output_taper

Some files were not shown because too many files have changed in this diff Show More