Skip to content
Draft
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
url = ../../commaai/panda.git
[submodule "opendbc"]
path = opendbc_repo
url = ../../commaai/opendbc.git
url = ../../xieliwei/opendbc.git
[submodule "msgq"]
path = msgq_repo
url = ../../commaai/msgq.git
Expand Down
206 changes: 107 additions & 99 deletions docs/CARS.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions openpilot/common/params_keys.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"AthenadUploadQueue", {PERSISTENT, JSON}},
{"AthenadRecentlyViewedRoutes", {PERSISTENT, STRING}},
{"BootCount", {PERSISTENT, INT}},
{"BydLksEnabled", {PERSISTENT, BOOL, "1"}},
{"CalibrationParams", {PERSISTENT, BYTES}},
{"CameraDebugExpGain", {CLEAR_ON_MANAGER_START, STRING}},
{"CameraDebugExpTime", {CLEAR_ON_MANAGER_START, STRING}},
Expand Down
19 changes: 19 additions & 0 deletions openpilot/selfdrive/car/card.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,21 @@ def __init__(self, CI=None, RI=None) -> None:
safety_config.safetyModel = structs.CarParams.SafetyModel.noOutput
self.CP.safetyConfigs = [safety_config]

# BYD LKS latch: persist last state, first boot ON. Panda reads the same
# bit from safetyParam and then follows the bus-0 LKS button with us.
self._byd_lks_last = None
if self.CP.brand == "byd" and self.CI.CS is not None:
from opendbc.car.byd.values import BydSafetyFlags
if self.params.get("BydLksEnabled") is None:
self.params.put_bool("BydLksEnabled", True)
lks = self.params.get_bool("BydLksEnabled")
self.CI.CS.lks_enabled = lks
self._byd_lks_last = lks
if not self.CP.passive and self.CP.safetyConfigs:
self.CP.safetyConfigs[0].safetyParam &= ~int(BydSafetyFlags.LKS_ON)
if lks:
self.CP.safetyConfigs[0].safetyParam |= int(BydSafetyFlags.LKS_ON)

if self.CP.secOcRequired:
# Copy user key if available
try:
Expand Down Expand Up @@ -243,6 +258,10 @@ def controls_update(self, CS: car.CarState, CC: car.CarControl):
def step(self):
CS, RD = self.state_update()

if self._byd_lks_last is not None and self.CI.CS.lks_enabled != self._byd_lks_last:
self.params.put_bool("BydLksEnabled", self.CI.CS.lks_enabled)
self._byd_lks_last = self.CI.CS.lks_enabled

self.state_publish(CS, RD)

initialized = (not any(e.name == EventName.selfdriveInitializing for e in self.sm['onroadEvents']) and
Expand Down
44 changes: 43 additions & 1 deletion openpilot/selfdrive/controls/controlsd.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,39 @@

ACTUATOR_FIELDS = tuple(car.CarControl.Actuators.schema.fields.keys())

# Follow distance target in seconds per longitudinal personality
GAP_TARGETS = {0: 1.25, 1: 1.45, 2: 1.75}
# Stop distance the longitudinal plan keeps on top of the time gap, see long_mpc
STOP_DISTANCE = 6.0


def lane_flags_from_probs(probs, from_model: bool):
# Brand-opt-in lane HUD: model probs for visibility.
left_visible = True
right_visible = True
if from_model:
left_visible = bool(len(probs) > 1 and probs[1] > 0.5)
right_visible = bool(len(probs) > 2 and probs[2] > 0.5)
return left_visible, right_visible


def lead_follow_status(lead, plan, personality: int, v_ego: float) -> int:
# 1 within the gap, 2 within 75% of the gap or braking for the lead, 3 within 60% of the gap
if not lead.present or v_ego < 2.0:
return 1
if plan is not None and plan.longitudinalPlanSource in (1, 2) and plan.aTarget < -1.5:
return 3
if plan is not None and plan.longitudinalPlanSource in (1, 2) and plan.aTarget < -0.5:
return 2
# Thresholds scale with the distance the longitudinal plan keeps:
# t_follow * v + stop distance, matching the MPC's desired distance
d_target = GAP_TARGETS.get(personality, 1.45) * v_ego + STOP_DISTANCE
if lead.dRel < 0.6 * d_target:
return 3
if lead.dRel < 0.75 * d_target:
return 2
return 1


class Controls:
def __init__(self) -> None:
Expand All @@ -40,7 +73,7 @@ def __init__(self) -> None:

self.sm = messaging.SubMaster(['lateralDelay', 'vehicleParameters', 'lateralTorqueParameters', 'modelV2', 'selfdriveState',
'extrinsicsCalibration', 'deviceMotion', 'longitudinalPlan', 'lateralManeuverPlan', 'carState', 'carOutput',
'driverMonitoringState', 'onroadEvents', 'driverAssistance'], poll='selfdriveState')
'driverMonitoringState', 'onroadEvents', 'driverAssistance', 'radarState'], poll='selfdriveState')
self.pm = messaging.PubMaster(['carControl', 'controlsState'])

self.steer_limited_by_safety = False
Expand Down Expand Up @@ -168,6 +201,10 @@ def publish(self, CC, lac_log):
hudControl.lanesVisible = CC.enabled
hudControl.leadVisible = self.sm['longitudinalPlan'].hasLead
hudControl.leadDistanceBars = self.sm['selfdriveState'].personality.raw + 1
if self.CP.hudLeadShading and self.sm.valid['radarState']:
# Braking-based escalation only reflects the gap when openpilot is controlling the car.
plan = self.sm['longitudinalPlan'] if CC.enabled else None
hudControl.leadFollowStatus = lead_follow_status(self.sm['radarState'].leadOne, plan, self.sm['selfdriveState'].personality.raw, CS.vEgo)
hudControl.visualAlert = self.sm['selfdriveState'].alertHudVisual

hudControl.rightLaneVisible = True
Expand All @@ -176,6 +213,11 @@ def publish(self, CC, lac_log):
hudControl.leftLaneDepart = self.sm['driverAssistance'].leftLaneDeparture
hudControl.rightLaneDepart = self.sm['driverAssistance'].rightLaneDeparture

# Brand-opt-in: model lane visibility. Do not change other brands' HUD lane bits.
if self.CP.hudLaneFromModel:
probs = self.sm['modelV2'].laneLineProbs if self.sm.valid['modelV2'] else []
hudControl.leftLaneVisible, hudControl.rightLaneVisible = lane_flags_from_probs(probs, self.CP.hudLaneFromModel)

if self.sm['selfdriveState'].active:
CO = self.sm['carOutput']
if self.CP.steerControlType == car.CarParams.SteerControlType.angle:
Expand Down
74 changes: 74 additions & 0 deletions openpilot/selfdrive/controls/tests/test_hud_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import unittest
from types import SimpleNamespace

from openpilot.selfdrive.controls.controlsd import lane_flags_from_probs, lead_follow_status


class TestLaneFlagsFromProbs(unittest.TestCase):
def test_flags_off_stock(self):
left_v, right_v = lane_flags_from_probs([], False)
self.assertTrue(left_v and right_v)

def test_missing_probs_grey(self):
left_v, right_v = lane_flags_from_probs([], True)
self.assertFalse(left_v or right_v)

def test_unsure_side_grey(self):
left_v, right_v = lane_flags_from_probs([1.0, 0.9, 0.4], True)
self.assertTrue(left_v)
self.assertFalse(right_v)


class TestLeadFollowStatus(unittest.TestCase):
def _plan(self, source, a_target):
return SimpleNamespace(longitudinalPlanSource=source, aTarget=a_target)

def test_no_lead(self):
lead = SimpleNamespace(present=False, dRel=0.0)
self.assertEqual(lead_follow_status(lead, None, 1, 10.0), 1)

def test_low_speed(self):
lead = SimpleNamespace(present=True, dRel=1.0)
self.assertEqual(lead_follow_status(lead, None, 0, 1.9), 1)
self.assertEqual(lead_follow_status(lead, None, 2, 0.0), 1)

def test_gap_boundaries(self):
lead = SimpleNamespace(present=True)
for personality, target in ((0, 1.25), (1, 1.45), (2, 1.75)):
d_target = target * 10.0 + 6.0
lead.dRel = d_target
self.assertEqual(lead_follow_status(lead, None, personality, 10.0), 1)
lead.dRel = 0.8 * d_target
self.assertEqual(lead_follow_status(lead, None, personality, 10.0), 1)
lead.dRel = 0.7 * d_target
self.assertEqual(lead_follow_status(lead, None, personality, 10.0), 2)
lead.dRel = 0.65 * d_target
self.assertEqual(lead_follow_status(lead, None, personality, 10.0), 2)
lead.dRel = 0.55 * d_target
self.assertEqual(lead_follow_status(lead, None, personality, 10.0), 3)

def test_unknown_personality_is_standard(self):
lead = SimpleNamespace(present=True, dRel=14.0)
self.assertEqual(lead_follow_status(lead, None, 7, 10.0), 2)

def test_negative_distance_clamped(self):
lead = SimpleNamespace(present=True, dRel=-5.0)
self.assertEqual(lead_follow_status(lead, None, 1, 10.0), 3)

def test_lead_braking_warn(self):
lead = SimpleNamespace(present=True, dRel=40.0)
self.assertEqual(lead_follow_status(lead, self._plan(1, -0.6), 1, 10.0), 2)
self.assertEqual(lead_follow_status(lead, self._plan(2, -0.6), 1, 10.0), 2)

def test_lead_hard_braking_too_close(self):
lead = SimpleNamespace(present=True, dRel=40.0)
self.assertEqual(lead_follow_status(lead, self._plan(1, -1.6), 1, 10.0), 3)

def test_cruise_or_e2e_braking_ignored(self):
lead = SimpleNamespace(present=True, dRel=40.0)
self.assertEqual(lead_follow_status(lead, self._plan(0, -2.0), 1, 10.0), 1)
self.assertEqual(lead_follow_status(lead, self._plan(4, -2.0), 1, 10.0), 1)


if __name__ == "__main__":
unittest.main()
2 changes: 2 additions & 0 deletions openpilot/selfdrive/selfdrived/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,8 @@ def invalid_lkas_setting_alert(CP: car.CarParams, CS: car.CarState, sm: messagin
text = "Toggle stock LKAS on or off to engage"
if CP.brand == "tesla":
text = "Switch to Traffic-Aware Cruise Control to engage"
elif CP.brand == "byd":
text = "Turn LKS on to engage"
elif CP.brand == "mazda":
text = "Enable your car's LKAS to engage"
elif CP.brand == "nissan":
Expand Down
10 changes: 9 additions & 1 deletion openpilot/selfdrive/selfdrived/selfdrived.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,15 @@ def update_events(self, CS):

# Decrement personality on distance button press
if self.CP.openpilotLongitudinalControl:
if any(not be.pressed and be.type == ButtonType.gapAdjustCruise for be in CS.buttonEvents):
if self.CP.brand == "byd":
# BYD Distance -: more aggressive. Distance + (altButton2): more relaxed.
dec = any(not be.pressed and be.type == ButtonType.gapAdjustCruise for be in CS.buttonEvents)
inc = any(not be.pressed and be.type == ButtonType.altButton2 for be in CS.buttonEvents)
if dec != inc:
self.personality = (self.personality + (1 if inc else -1)) % 3
self.params.put('LongitudinalPersonality', self.personality)
self.events.add(EventName.personalityChanged)
elif any(not be.pressed and be.type == ButtonType.gapAdjustCruise for be in CS.buttonEvents):
self.personality = (self.personality - 1) % 3
self.params.put('LongitudinalPersonality', self.personality)
self.events.add(EventName.personalityChanged)
Expand Down
2 changes: 2 additions & 0 deletions openpilot/selfdrive/test/process_replay/test_processes.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
("FORD", "54827bf84c38b14f|2023-01-26--21-59-07--4"), # FORD.FORD_BRONCO_SPORT_MK1
("RIVIAN", "bc095dc92e101734|000000db--ee9fe46e57--1"), # RIVIAN.RIVIAN_R1_GEN1
("TESLA", "aebd8f1d4ea16066|000002d8--5850d3ea5c--2"), # TESLA.TESLA_MODEL_Y
("BYD", "148fa33c79475c93|000000d4--c918f5decb--0"), # BYD.BYD_ATTO_3

# Enable when port is tested and dashcamOnly is no longer set
#("VOLKSWAGEN2", "3cfdec54aa035f3f|2022-07-19--23-45-10--2"), # VOLKSWAGEN.VOLKSWAGEN_PASSAT_NMS
Expand All @@ -61,6 +62,7 @@
("FORD", "regen755D8CB1E1F|2025-04-08--23-13-43--0"),
("RIVIAN", "regen5FCAC896BBE|2025-04-08--23-13-35--0"),
("TESLA", "aebd8f1d4ea16066|000002d8--5850d3ea5c--2"),
("BYD", "148fa33c79475c93|000000d4--c918f5decb--0"),
]

# dashcamOnly makes don't need to be tested until a full port is done
Expand Down
Loading