IQ.Pilot Prebuilt Release @ 27f668a

This commit is contained in:
IQ.Lvbs CI [bot]
2026-09-03 18:23:24 -05:00
commit b073c5182b
2554 changed files with 679696 additions and 0 deletions

View File

@@ -0,0 +1,490 @@
import numpy as np
import math
from iqdbc.can import CANPacker
from iqdbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, DT_CTRL, rate_limit, make_tester_present_msg, structs
from iqdbc.car.common.pid import PIDController
from iqdbc.car.honda import dash_lane, dash_objects, hondacan
from iqdbc.car.honda.values import CAR, CruiseButtons, CruiseSettings, HONDA_BOSCH, HONDA_BOSCH_CANFD, HONDA_BOSCH_RADARLESS, \
HONDA_BOSCH_TJA_CONTROL, HONDA_NIDEC_ALT_PCM_ACCEL, CarControllerParams
from iqdbc.car.interfaces import CarControllerBase
from iqdbc.lvbs.car.honda.aol import AolCarController
from iqdbc.lvbs.car.honda.gas_interceptor import GasInterceptorCarController
VisualAlert = structs.CarControl.HUDControl.VisualAlert
LongCtrlState = structs.CarControl.Actuators.LongControlState
def compute_gb_honda_bosch(accel, speed):
# TODO returns 0s, is unused
return 0.0, 0.0
def compute_gb_honda_nidec(accel, speed):
creep_brake = 0.0
creep_speed = 2.3
creep_brake_value = 0.15
if speed < creep_speed:
creep_brake = (creep_speed - speed) / creep_speed * creep_brake_value
gb = float(accel) / 4.8 - creep_brake
return np.clip(gb, 0.0, 1.0), np.clip(-gb, 0.0, 1.0)
def compute_gas_brake(accel, speed, fingerprint):
if fingerprint in HONDA_BOSCH:
return compute_gb_honda_bosch(accel, speed)
else:
return compute_gb_honda_nidec(accel, speed)
# TODO not clear this does anything useful
def actuator_hysteresis(brake, braking, brake_steady, v_ego, car_fingerprint):
# hyst params
brake_hyst_on = 0.02 # to activate brakes exceed this value
brake_hyst_off = 0.005 # to deactivate brakes below this value
brake_hyst_gap = 0.01 # don't change brake command for small oscillations within this value
# *** hysteresis logic to avoid brake blinking. go above 0.1 to trigger
if (brake < brake_hyst_on and not braking) or brake < brake_hyst_off:
brake = 0.
braking = brake > 0.
# for small brake oscillations within brake_hyst_gap, don't change the brake command
if brake == 0.:
brake_steady = 0.
elif brake > brake_steady + brake_hyst_gap:
brake_steady = brake - brake_hyst_gap
elif brake < brake_steady - brake_hyst_gap:
brake_steady = brake + brake_hyst_gap
brake = brake_steady
return brake, braking, brake_steady
def brake_pump_hysteresis(apply_brake, apply_brake_last, last_pump_ts, ts):
pump_on = False
# reset pump timer if:
# - there is an increment in brake request
# - we are applying steady state brakes and we haven't been running the pump
# for more than 20s (to prevent pressure bleeding)
if apply_brake > apply_brake_last or (ts - last_pump_ts > 20. and apply_brake > 0):
last_pump_ts = ts
# once the pump is on, run it for at least 0.2s
if ts - last_pump_ts < 0.2 and apply_brake > 0:
pump_on = True
return pump_on, last_pump_ts
def process_hud_alert(hud_alert):
alert_fcw = False
alert_steer_required = False
# Make sure FCW is prioritized over steering required
# TODO: implement separate available LDW alert
if hud_alert == VisualAlert.fcw:
alert_fcw = True
elif hud_alert in (VisualAlert.steerRequired, VisualAlert.ldw):
alert_steer_required = True
return alert_fcw, alert_steer_required
class CarController(CarControllerBase, AolCarController, GasInterceptorCarController):
def __init__(self, dbc_names, CP, CP_IQ):
CarControllerBase.__init__(self, dbc_names, CP, CP_IQ)
AolCarController.__init__(self)
GasInterceptorCarController.__init__(self, CP, CP_IQ)
self.packer = CANPacker(dbc_names[Bus.pt])
self.params = CarControllerParams(CP)
self.CAN = hondacan.CanBus(CP)
self.tja_control = CP.carFingerprint in HONDA_BOSCH_TJA_CONTROL
self.lane_renderer = dash_lane.LanePathRenderer()
self.dash_object_author = dash_objects.DashObjectAuthor()
self.rendered_lane = dash_lane.RenderedLane()
self.lkas_hud_key = None
self.lkas_state_change_frames = 0
self.braking = False
self.brake_steady = 0.
self.brake_last = 0.
self.apply_brake_last = 0
self.last_pump_ts = 0.
self.stopping_counter = 0
self.accel = 0.0
self.speed = 0.0
self.gas = 0.0
self.brake = 0.0
self.last_torque = 0.0
self.bosch_last_gas = 0
self.lkas_button_send_remaining = 0
self.last_lkas_button_frame = 0
self.radar_disable_counter = 0
self.radar_mux = 0
# stock RADAR_HUD_CANFD raises its CMBS bit only for a short burst after ACC engages; 10Hz hud ticks
self.radar_hud_pulse = 0
self.last_acc_enabled = False
self.gasfactor = 1.0
self.gasfactor_before_maxgas = 1.0
self.windfactor = 1.0
self.windfactor_before_maxgas = 1.0
self.windfactor_before_brake = 0.0
self.pitch = 0.0
self.brake_pid = PIDController(k_p=0.0, k_i=1.0, pos_limit=0.0, neg_limit=-2.0, rate=50)
self.brake_pid.reset()
def update(self, CC, CC_IQ, CS, now_nanos):
AolCarController.update(self, self.CP, CC, CC_IQ)
gas_pedal_force = 0.0
min_gas = self.params.BOSCH_GAS_LOOKUP_BP[0]
actuators = CC.actuators
hud_control = CC.hudControl
hud_v_cruise = hud_control.setSpeed / CS.v_cruise_factor if hud_control.speedVisible else 255
pcm_cancel_cmd = CC.cruiseControl.cancel
if len(CC.orientationNED) == 3:
self.pitch = CC.orientationNED[1]
hill_brake = math.sin(self.pitch) * ACCELERATION_DUE_TO_GRAVITY
if CC.longActive:
accel = actuators.accel
gas, brake = compute_gas_brake(actuators.accel + hill_brake, CS.out.vEgo, self.CP.carFingerprint)
else:
accel = 0.0
gas, brake = 0.0, 0.0
# *** rate limit steer ***
limited_torque = rate_limit(actuators.torque, self.last_torque, -self.params.STEER_DELTA_DOWN * DT_CTRL,
self.params.STEER_DELTA_UP * DT_CTRL)
self.last_torque = limited_torque
# *** apply brake hysteresis ***
pre_limit_brake, self.braking, self.brake_steady = actuator_hysteresis(brake, self.braking, self.brake_steady,
CS.out.vEgo, self.CP.carFingerprint)
# *** rate limit after the enable check ***
self.brake_last = rate_limit(pre_limit_brake, self.brake_last, -2., 3 * DT_CTRL)
# vehicle hud display, wait for one update from 10Hz 0x304 msg
alert_fcw, alert_steer_required = process_hud_alert(hud_control.visualAlert)
# **** process the car messages ****
# steer torque is converted back to CAN reference (positive when steering right)
apply_torque = int(np.interp(-limited_torque * self.params.STEER_MAX,
self.params.STEER_LOOKUP_BP, self.params.STEER_LOOKUP_V))
# Send CAN commands
can_sends = []
if self.CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS) and self.CP.openpilotLongitudinalControl:
if self.CP.carFingerprint in HONDA_BOSCH_CANFD and CS.stock_acc_alive:
# CAN FD: the radar is silenced from here rather than from CarInterface.init(), and only once
# the comma relay is confirmed open: init() ran under the ELM327 safety mode, so the
# replacement ACC_CONTROL stream was blocked until the safety-mode switch landed, and whenever
# that took longer than ~110ms after radar silence the brake module latched CRUISE_FAULT for
# the whole drive. With the relay open the replacement stream starts within a few frames of
# radar silence (see CS.stock_acc_alive), well inside the fault threshold
if CS.canfd_relay_open:
if self.radar_disable_counter % 50 == 0:
# UDS extended diagnostic session, required before CommunicationControl
can_sends.append((0x18DAB0F1, b'\x02\x10\x03\x00\x00\x00\x00\x00', self.CAN.pt))
elif self.radar_disable_counter % 50 == 5:
# UDS CommunicationControl disableRxAndTx (0x80 suppresses the response), retried every
# 0.5s until the radar goes silent
can_sends.append((0x18DAB0F1, b'\x03\x28\x83\x03\x00\x00\x00\x00', self.CAN.pt))
self.radar_disable_counter += 1
elif self.frame % 10 == 0:
# tester present - w/ no response (keeps radar disabled)
can_sends.append(make_tester_present_msg(0x18DAB0F1, self.CAN.pt, suppress_response=True))
# simulate the disabled canfd radar to prevent faults. These look-alikes are consumed by both the
# camera (behind the relay, on the camera bus) and the powertrain: openpilot's own TX is not
# forwarded across the open relay, so each frame is packed exactly once (the packer's
# counter/checksum only advance once per cycle) and the identical bytes are mirrored onto both
# buses (re-packing would double-increment the counter and desync the buses). While the stock
# radar is still transmitting it authors all of these itself
if self.CP.carFingerprint in HONDA_BOSCH_CANFD and self.CP.openpilotLongitudinalControl and not CS.stock_acc_alive:
if CC.enabled and not self.last_acc_enabled:
self.radar_hud_pulse = 30 # ~3s at 10Hz, matching the stock 2-6s engage burst
self.last_acc_enabled = CC.enabled
radar_msgs = []
if CS.hud_tick:
radar_msgs.append(hondacan.create_radar_hud_canfd(self.packer, self.CAN.pt, CC.enabled, self.radar_hud_pulse > 0))
if self.radar_hud_pulse > 0:
self.radar_hud_pulse -= 1
if CS.supp_tick:
radar_msgs.append(hondacan.create_canfd_supplemental(self.packer, self.CAN.pt))
if CS.radar_50hz_tick:
# Cycle the radar MUX through the stock banks: 1-10, 17-26, 33-42, 49-58. This counter also
# drives the LANE_PATH/HUD_OBJECTS mux below: it advances exactly one step per transmitted
# frame, so the sweep stays contiguous even when a tick is missed (a frame-derived mux left
# holes in the sweep the stock radar never produces).
# These must be elif: a bare `if` at a bank start would fall through to the increment,
# skipping the bank-start values (17, 33, 49)
if self.radar_mux >= 58:
self.radar_mux = 1
elif self.radar_mux == 10:
self.radar_mux = 17
elif self.radar_mux == 26:
self.radar_mux = 33
elif self.radar_mux == 42:
self.radar_mux = 49
else:
self.radar_mux += 1
if CS.radar_5hz_tick:
# RADAR_LEAD's LANE_PATH_LENGTH must track the valid-point count of the LANE_PATH sweep being
# authored, and LEFT_LANE/RIGHT_LANE the per-side line-detected status, in lockstep with the
# stock radar's behavior or the dash won't draw the lane lines
radar_msgs.extend(hondacan.create_canfd_5hz_radar_messages(self.packer, self.CAN.pt, CS.radar_ref_counter,
dash_lane.canfd_lane_length(self.rendered_lane),
dash_lane.LANE_LINE_ON if self.rendered_lane.left_line else 0,
dash_lane.LANE_LINE_ON if self.rendered_lane.right_line else 0))
for addr, dat, _ in radar_msgs:
can_sends.append((addr, dat, self.CAN.pt))
can_sends.append((addr, dat, self.CAN.camera))
# Send steering command.
can_sends.append(hondacan.create_steering_control(self.packer, self.CAN, apply_torque, CC.latActive, self.tja_control))
# wind brake from air resistance decel at high speed
wind_brake = np.interp(CS.out.vEgo, [0.0, 2.3, 35.0], [0.001, 0.002, 0.15]) * self.windfactor # not in m/s2 units
wind_brake_ms2 = np.interp(CS.out.vEgo, [0.0, 13.4, 22.4, 31.3, 40.2], [0.000, 0.049, 0.136, 0.267, 0.441]) # in m/s2 units
# all of this is only relevant for HONDA NIDEC
max_accel = np.interp(CS.out.vEgo, self.params.NIDEC_MAX_ACCEL_BP, self.params.NIDEC_MAX_ACCEL_V)
# TODO this 1.44 is just to maintain previous behavior
pcm_speed_BP = [-wind_brake,
-wind_brake * (3 / 4),
0.0,
0.5]
# The Honda ODYSSEY seems to have different PCM_ACCEL
# msgs, is it other cars too?
if self.CP_IQ.enableGasInterceptor or not CC.longActive:
pcm_speed = 0.0
pcm_accel = int(0.0)
elif self.CP.carFingerprint in HONDA_NIDEC_ALT_PCM_ACCEL:
pcm_speed_V = [0.0,
np.clip(CS.out.vEgo - 3.0, 0.0, 100.0),
np.clip(CS.out.vEgo + 0.0, 0.0, 100.0),
np.clip(CS.out.vEgo + 5.0, 0.0, 100.0)]
pcm_speed = float(np.interp(gas - brake, pcm_speed_BP, pcm_speed_V))
pcm_accel = int(1.0 * self.params.NIDEC_GAS_MAX)
else:
pcm_speed_V = [0.0,
np.clip(CS.out.vEgo - 2.0, 0.0, 100.0),
np.clip(CS.out.vEgo + 2.0, 0.0, 100.0),
np.clip(CS.out.vEgo + 5.0, 0.0, 100.0)]
pcm_speed = float(np.interp(gas - brake, pcm_speed_BP, pcm_speed_V))
pcm_accel = int(np.clip((accel / 1.44) / max_accel, 0.0, 1.0) * self.params.NIDEC_GAS_MAX)
if not self.CP.openpilotLongitudinalControl:
if self.frame % 2 == 0 and self.CP.carFingerprint not in HONDA_BOSCH_RADARLESS | HONDA_BOSCH_CANFD:
can_sends.append(hondacan.create_bosch_supplemental_1(self.packer, self.CAN))
# If using stock ACC, spam cancel command to kill gas when OP disengages.
if pcm_cancel_cmd:
can_sends.append(hondacan.spam_buttons_command(self.packer, self.CAN, CruiseButtons.CANCEL, 0, CS.scm_ambient_light,
self.CP.carFingerprint))
elif CC.cruiseControl.resume:
can_sends.append(hondacan.spam_buttons_command(self.packer, self.CAN, CruiseButtons.RES_ACCEL, 0, CS.scm_ambient_light,
self.CP.carFingerprint))
else:
# Send gas and brake commands.
if self.frame % 2 == 0:
ts = self.frame * DT_CTRL
if self.CP.carFingerprint in HONDA_BOSCH:
# low-speed extra brake: the fixed accel command under-delivers approaching a stop, so an
# integral-only term closes the gap, releasing at 1 m/s^3 once out of the window
if (accel < min_gas) and (CS.out.vEgo < 3.0) and not (-1e-3 < CS.out.vEgo < 1e-3):
brake_addon = self.brake_pid.update(error=accel - CS.out.aEgo, speed=CS.out.vEgo)
target_accel = min(accel, accel + brake_addon)
else:
if (self.brake_pid.i < 0.0) and (accel < min_gas):
self.brake_pid.i = min(0.0, self.brake_pid.i + 0.02)
else:
self.brake_pid.reset()
target_accel = min(accel, accel + self.brake_pid.i)
self.accel = float(np.clip(target_accel, self.params.BOSCH_ACCEL_MIN, self.params.BOSCH_ACCEL_MAX))
# not using self.accel since the brake pid resets with the gas pedal
gas_pedal_force = accel + wind_brake_ms2 * self.windfactor + hill_brake
# Live-learn gas pedal adjustments when openpilot is controlling gas.
if (actuators.longControlState == LongCtrlState.pid) and (not CS.out.gasPressed):
gas_error = accel - CS.out.aEgo
if gas_error != 0.0 and gas_pedal_force > min_gas:
if self.CP.carFingerprint in (CAR.HONDA_INSIGHT, CAR.HONDA_CIVIC_BOSCH): # gas pedal reacts too slowly
learn_speed = 150
elif self.CP.carFingerprint == CAR.ACURA_RDX_3G: # prevent overreacting to turbo lag
learn_speed = 300
else:
learn_speed = 50
self.gasfactor = np.clip(self.gasfactor + gas_error / learn_speed * (gas_pedal_force - min_gas), 0.01, 3.0)
if gas_error != 0.0 and (not CS.out.brakePressed) and (CS.out.vEgo > 0.0):
wind_learn_speed = 100 if self.CP.carFingerprint == CAR.ACURA_RDX_3G else 1000
wind_adjust = 1 + wind_brake_ms2 / wind_learn_speed
self.windfactor = np.clip(self.windfactor * (wind_adjust if (gas_error > 0) else 1.0 / wind_adjust), 0.1, 3.0)
if gas_pedal_force <= min_gas:
self.windfactor = max(self.windfactor, self.windfactor_before_brake)
else:
self.windfactor_before_brake = self.windfactor
if gas_pedal_force >= self.params.BOSCH_ACCEL_MAX:
self.gasfactor = min(self.gasfactor, self.gasfactor_before_maxgas)
self.windfactor = min(self.windfactor, self.windfactor_before_maxgas)
else:
self.gasfactor_before_maxgas = self.gasfactor
self.windfactor_before_maxgas = self.windfactor
self.gas = float(np.interp((gas_pedal_force - min_gas) * self.gasfactor + min_gas,
self.params.BOSCH_GAS_LOOKUP_BP, self.params.BOSCH_GAS_LOOKUP_V))
# limit gas ramp to 60 units per frame, matches stock; higher sometimes makes the powertrain ignore the command
max_gas = max(60, self.bosch_last_gas + 60)
self.gas = min(self.gas, max_gas)
self.bosch_last_gas = self.gas
stopping = actuators.longControlState == LongCtrlState.stopping
self.stopping_counter = self.stopping_counter + 1 if stopping else 0
# CAN FD: never overlap the stock radar's own ACC_CONTROL stream; ours starts within a few
# frames of the radar going silent (see the deferred radar disable above)
if not (self.CP.carFingerprint in HONDA_BOSCH_CANFD and CS.stock_acc_alive):
can_sends.extend(hondacan.create_acc_commands(self.packer, self.CAN, CC.enabled, CC.longActive, self.accel, self.gas,
self.stopping_counter, self.CP, gas_pedal_force))
else:
apply_brake = np.clip(self.brake_last - wind_brake, 0.0, 1.0)
apply_brake = int(np.clip(apply_brake * self.params.NIDEC_BRAKE_MAX, 0, self.params.NIDEC_BRAKE_MAX - 1))
pump_on, self.last_pump_ts = brake_pump_hysteresis(apply_brake, self.apply_brake_last, self.last_pump_ts, ts)
pcm_override = True
can_sends.append(hondacan.create_brake_command(self.packer, self.CAN, apply_brake, pump_on,
pcm_override, pcm_cancel_cmd, alert_fcw,
self.CP.carFingerprint, CS.stock_brake, self.CP_IQ))
self.apply_brake_last = apply_brake
self.brake = apply_brake / self.params.NIDEC_BRAKE_MAX
gas_error = actuators.accel - CS.out.aEgo
if (not CS.out.gasPressed) and (actuators.longControlState == LongCtrlState.pid) and self.CP_IQ.enableGasInterceptor:
if gas_error != 0.0 and gas > 0.0:
self.gasfactor = np.clip(self.gasfactor + gas_error / 50 * (gas * 4.8), 0.1, 3.0)
if gas_error != 0.0 and (not CS.out.brakePressed) and (CS.out.vEgo > 0.0):
wind_adjust = 1 + (wind_brake * 4.8) / 1000
self.windfactor = np.clip(self.windfactor * (wind_adjust if (gas_error > 0) else 1.0 / wind_adjust), 0.1, 5.0)
if gas <= 0.0:
self.windfactor = max(self.windfactor, self.windfactor_before_brake)
else:
self.windfactor_before_brake = self.windfactor
can_sends.extend(GasInterceptorCarController.update(self, CC, CS, gas * self.gasfactor, brake, wind_brake, self.packer, self.frame))
# Send dashboard UI commands. On CAN FD, ACC_HUD is a radar look-alike that openpilot only owns
# once it has disabled the radar; it rides the phase-locked 10Hz hud tick instead of frame % 10
if (self.CP.carFingerprint in HONDA_BOSCH_CANFD and CS.hud_tick and
self.CP.openpilotLongitudinalControl and not CS.stock_acc_alive):
can_sends.append(hondacan.create_acc_hud(self.packer, self.CAN.pt, self.CP, CC.enabled, pcm_speed, actuators.accel,
hud_control, hud_v_cruise, CS.is_metric, CS.acc_hud))
if self.frame % 10 == 0:
if self.CP.openpilotLongitudinalControl and self.CP.carFingerprint not in HONDA_BOSCH_CANFD:
# On Nidec, this also controls longitudinal positive acceleration
can_sends.append(hondacan.create_acc_hud(self.packer, self.CAN.pt, self.CP, CC.enabled, pcm_speed, pcm_accel,
hud_control, hud_v_cruise, CS.is_metric, CS.acc_hud))
steering_available = CS.out.cruiseState.available and CS.out.vEgo > self.CP.minSteerSpeed
reduced_steering = CS.out.steeringPressed
lkas_state_change = None
if self.CP.carFingerprint in HONDA_BOSCH_CANFD:
# The key must contain exactly the signals that change the LKAS_HUD payload, nothing more:
# a flickering input (like steer saturation) re-triggers the pulse continuously, which keeps
# LKAS_STATE_CHANGE high and suppresses the dash lane lines entirely
hud_key = (bool(CC.latActive), bool(self.dashed_lanes), bool(alert_steer_required), bool(CS.out.steerFaultPermanent))
if hud_key != self.lkas_hud_key:
self.lkas_hud_key = hud_key
self.lkas_state_change_frames = 30 # 3s at the 10Hz LKAS_HUD rate, matching the stock pulse length
lkas_state_change = self.lkas_state_change_frames > 0
self.lkas_state_change_frames = max(0, self.lkas_state_change_frames - 1)
can_sends.extend(hondacan.create_lkas_hud(self.packer, self.CAN.lkas, self.CP, hud_control, CC.latActive,
steering_available, reduced_steering, alert_steer_required, CS.lkas_hud, self.dashed_lanes,
steer_fault_permanent=CS.out.steerFaultPermanent, lkas_state_change=lkas_state_change))
if self.CP.openpilotLongitudinalControl:
# TODO: combining with create_acc_hud block above will change message order and will need replay logs regenerated
if self.CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS - HONDA_BOSCH_CANFD):
can_sends.append(hondacan.create_radar_hud(self.packer, self.CAN.pt))
if self.CP.carFingerprint == CAR.HONDA_CIVIC_BOSCH:
can_sends.append(hondacan.create_legacy_brake_command(self.packer, self.CAN.pt))
if self.CP.carFingerprint not in HONDA_BOSCH:
self.speed = pcm_speed
if not self.CP_IQ.enableGasInterceptor:
self.gas = pcm_accel / self.params.NIDEC_GAS_MAX
# Render OP's lane and lead cars on the dash. On CAN FD these are radar look-alikes that only
# exist (and are only allowed by panda safety) when the radar is disabled. Radarless keeps the
# camera as the dash authority (known-good), so OP does not author these there
if (CS.radar_50hz_tick and self.CP.carFingerprint in HONDA_BOSCH_CANFD and self.CP.openpilotLongitudinalControl
and not CS.stock_acc_alive):
leads = dash_objects.leads_from_model(self.model, CS.out.vEgo)
lead = leads[0]
lead_d = lead.dRel if lead.status else 0.0
self.rendered_lane = self.lane_renderer.update(self.model, CS.out.vEgo, lead_d)
mux = self.radar_mux
# no LKAS_HUD_2 on CAN FD: the dash reads the lane length from the in-band terminator, so the
# path is reshaped into the terminated-prefix form
lane_offsets = dash_lane.canfd_lane_offsets(self.rendered_lane)
lane_msg = dash_lane.create_lane_path(self.packer, self.CAN.lkas, lane_offsets, mux)
can_sends.append(lane_msg)
# CAN FD cars have no camera HUD_OBJECTS to poll (the disabled radar owned it): author OP's
# lead in slot 0 with the other slots blank (tracks=None)
tracks = CS.camera_object_tracker.snapshot() if CS.camera_object_tracker is not None else None
hud_msg = self.dash_object_author.create(self.packer, self.CAN.lkas, lead, tracks, mux, now_nanos * 1e-9,
extra_leads=leads[1:])
can_sends.append(hud_msg)
# the camera (behind the relay) also consumes these; mirror the identical packed bytes onto the
# camera bus (packed once, so the counter/checksum stay in lockstep)
for addr, dat, _ in (lane_msg, hud_msg):
can_sends.append((addr, dat, self.CAN.camera))
# CAN FD: when stock LKAS is active, the touch-steering-wheel nag eventually forces an ACC
# disengagement (a brake tap from the VSA). Disable LKAS automatically and block the driver's LKAS
# button by taking over SCM_BUTTONS on the camera bus while engaged (panda blocks the forwarded
# stock SCM_BUTTONS while this stream flows). Radarless keeps the stock camera LKAS untouched
if self.CP.carFingerprint in HONDA_BOSCH_CANFD and CC.enabled and self.frame % 4 == 0 and \
not pcm_cancel_cmd and not CC.cruiseControl.resume:
if self.lkas_button_send_remaining == 0 and CS.lkas_hud["LKAS_READY"] and self.frame >= self.last_lkas_button_frame + 500:
self.lkas_button_send_remaining = 3
if self.lkas_button_send_remaining > 0:
self.last_lkas_button_frame = self.frame
self.lkas_button_send_remaining -= 1
cruise_setting = CruiseSettings.LKAS
elif CS.cruise_setting == CruiseSettings.LKAS:
cruise_setting = 0 # block the driver's LKAS button press
else:
cruise_setting = CS.cruise_setting
can_sends.append(hondacan.spam_buttons_command(self.packer, self.CAN, CS.cruise_buttons, cruise_setting,
CS.scm_ambient_light, self.CP.carFingerprint, bus=self.CAN.camera))
# Finalize actuator state for downstream consumers
new_actuators = actuators.as_builder()
new_actuators.speed = self.speed
new_actuators.accel = self.accel
new_actuators.gas = self.gas
new_actuators.brake = self.brake
new_actuators.torque = self.last_torque
new_actuators.torqueOutputCan = apply_torque
self.frame += 1
return new_actuators, can_sends

View File

@@ -0,0 +1,383 @@
import numpy as np
from collections import defaultdict
from iqdbc.can import CANDefine, CANParser
from iqdbc.car import Bus, create_button_events, structs, DT_CTRL
from iqdbc.car.common.conversions import Conversions as CV
from iqdbc.car.honda.hondacan import CanBus
from iqdbc.car.honda.values import CAR, DBC, STEER_THRESHOLD, HONDA_BOSCH, HONDA_BOSCH_ALT_RADAR, HONDA_BOSCH_CANFD, \
HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_RADARLESS, HONDA_BOSCH_TJA_CONTROL, \
HondaFlags, CruiseButtons, CruiseSettings, GearShifter, CarControllerParams
from iqdbc.car.honda.dash_objects import CameraObjectTracker
from iqdbc.car.interfaces import CarStateBase
from iqdbc.lvbs.car.honda.iq_carstate import IQCarState
TransmissionType = structs.CarParams.TransmissionType
ButtonType = structs.CarState.ButtonEvent.Type
BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.DECEL_SET: ButtonType.decelCruise,
CruiseButtons.MAIN: ButtonType.mainCruise, CruiseButtons.CANCEL: ButtonType.cancel}
SETTINGS_BUTTONS_DICT = {CruiseSettings.DISTANCE: ButtonType.gapAdjustCruise, CruiseSettings.LKAS: ButtonType.lkas}
class CarState(CarStateBase, IQCarState):
def __init__(self, CP, CP_IQ):
CarStateBase.__init__(self, CP, CP_IQ)
IQCarState.__init__(self, CP, CP_IQ)
can_define = CANDefine(DBC[CP.carFingerprint][Bus.pt])
if CP.transmissionType != TransmissionType.manual:
self.gearbox_msg = "GEARBOX_AUTO"
if CP.transmissionType == TransmissionType.cvt:
self.gearbox_msg = "GEARBOX_CVT"
self.shifter_values = can_define.dv[self.gearbox_msg]["GEAR_SHIFTER"]
self.car_state_scm_msg = "SCM_FEEDBACK"
if CP.carFingerprint in HONDA_NIDEC_ALT_SCM_MESSAGES:
self.car_state_scm_msg = "SCM_BUTTONS"
self.brake_error_msg = "HYBRID_BRAKE_ERROR" if CP.flags & HondaFlags.HYBRID else "STANDSTILL"
self.steer_status_values = defaultdict(lambda: "UNKNOWN", can_define.dv["STEER_STATUS"]["STEER_STATUS"])
self.brake_switch_prev = False
self.brake_switch_active = False
self.low_speed_alert = False
self.dynamic_v_cruise_units = self.CP.carFingerprint in (HONDA_BOSCH_RADARLESS | HONDA_BOSCH_ALT_RADAR |
HONDA_BOSCH_TJA_CONTROL | HONDA_BOSCH_CANFD)
self.cruise_setting = 0
self.v_cruise_pcm_prev = 0
# When available we use cp.vl["CAR_SPEED"]["ROUGH_CAR_SPEED_2"] to populate vEgoCluster
# However, on cars without a digital speedometer this is not always present (HRV, FIT, CRV 2016, ILX and RDX)
self.dash_speed_seen = False
self.is_metric = False
self.v_cruise_factor = 1.
self.initial_accFault_cleared = False
self.initial_accFault_cleared_timer = int(10 / DT_CTRL) # 10 seconds after startup for initial faults to clear
self.scm_ambient_light = 0
self.radar_ref_counter = 0
self.radar_5hz_tick_counter = 0
self.radar_5hz_tick = False
self.supp_tick_counter = 0
self.supp_tick = False
self.hud_tick_counter = 0
self.hud_tick = False
self.radar_50hz_tick_counter = 0
self.radar_50hz_tick = False
# CAN FD deferred radar disable (see carcontroller): the stock radar is assumed alive until it has
# been silent for a few frames, and the relay is detected open once the camera's STEERING_CONTROL
# stops being physically visible on the PT bus
self.stock_acc_counter = 0
self.stock_acc_alive = False
self.camera_steer_counter = 0
self.camera_steer_seen = False
self.canfd_frames = 0
self.canfd_relay_open = False
# only radarless cameras emit HUD_OBJECTS to poll for adjacent-car positions; on CAN FD the
# (disabled) radar owned it, so there is nothing to track
self.camera_object_tracker = CameraObjectTracker() if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS else None
def update(self, can_parsers) -> tuple[structs.CarState, structs.IQCarState]:
cp = can_parsers[Bus.pt]
cp_cam = can_parsers[Bus.cam]
if self.CP.enableBsm:
cp_body = can_parsers[Bus.body]
if self.CP.carFingerprint in HONDA_BOSCH_CANFD:
cp_radar = can_parsers[Bus.radar]
ret = structs.CarState()
ret_iq = structs.IQCarState()
# car params
v_weight_v = [0., 1.] # don't trust smooth speed at low values to avoid premature zero snapping
v_weight_bp = [1., 6.] # smooth blending, below ~0.6m/s the smooth speed snaps to zero
# update prevs, update must run once per loop
prev_cruise_buttons = self.cruise_buttons
prev_cruise_setting = self.cruise_setting
self.cruise_setting = cp.vl["SCM_BUTTONS"]["CRUISE_SETTING"]
self.cruise_buttons = cp.vl["SCM_BUTTONS"]["CRUISE_BUTTONS"]
if self.CP.carFingerprint in (HONDA_BOSCH_RADARLESS | HONDA_BOSCH_CANFD):
# The camera consumes SCM_BUTTONS content beyond the buttons (losing/zeroing this byte raises an
# adaptive high beam error), so it must be echoed on frames sent in the SCM's place
self.scm_ambient_light = cp.vl["SCM_BUTTONS"]["AMBIENT_LIGHT_MAYBE"]
# used for car hud message
# TODO: find CAR_SPEED for HONDA_ODYSSEY_TWN or use ACC_HUD w/ detection
self.is_metric = self.CP.carFingerprint in (CAR.HONDA_ODYSSEY_TWN,) or not cp.vl["CAR_SPEED"]["IMPERIAL_UNIT"]
self.v_cruise_factor = CV.MPH_TO_MS if self.dynamic_v_cruise_units and not self.is_metric else CV.KPH_TO_MS
# ******************* parse out can *******************
# blend in transmission speed at low speed, since it has more low speed accuracy
# STANDSTILL->WHEELS_MOVING bit can be noisy around zero, so use XMISSION_SPEED
v_wheel = sum([cp.vl["WHEEL_SPEEDS"][f"WHEEL_SPEED_{s}"] for s in ("FL", "FR", "RL", "RR")]) / 4.0 * CV.KPH_TO_MS
v_weight = float(np.interp(v_wheel, v_weight_bp, v_weight_v))
ret.vEgoRaw = (1. - v_weight) * cp.vl["ENGINE_DATA"]["XMISSION_SPEED"] * CV.KPH_TO_MS * self.CP.wheelSpeedFactor + v_weight * v_wheel
ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
ret.standstill = cp.vl["ENGINE_DATA"]["XMISSION_SPEED"] < 1e-5
# doorOpen is true if we can find any door open, but signal locations vary, and we may only see the driver's door
# TODO: Test the eight Nidec cars without SCM signals for driver's door state, may be able to consolidate further
if self.CP.flags & HondaFlags.HAS_ALL_DOOR_STATES:
ret.doorOpen = any([cp.vl["DOORS_STATUS"]["DOOR_OPEN_FL"], cp.vl["DOORS_STATUS"]["DOOR_OPEN_FR"],
cp.vl["DOORS_STATUS"]["DOOR_OPEN_RL"], cp.vl["DOORS_STATUS"]["DOOR_OPEN_RR"]])
elif "DRIVERS_DOOR_OPEN" in cp.vl["SCM_BUTTONS"]:
ret.doorOpen = bool(cp.vl["SCM_BUTTONS"]["DRIVERS_DOOR_OPEN"])
else:
ret.doorOpen = bool(cp.vl["SCM_FEEDBACK"]["DRIVERS_DOOR_OPEN"])
ret.seatbeltUnlatched = bool(cp.vl["SEATBELT_STATUS"]["SEATBELT_DRIVER_LAMP"] or not cp.vl["SEATBELT_STATUS"]["SEATBELT_DRIVER_LATCHED"])
steer_status = self.steer_status_values[cp.vl["STEER_STATUS"]["STEER_STATUS"]]
ret.steerFaultPermanent = steer_status not in ("NORMAL", "NO_TORQUE_ALERT_1", "NO_TORQUE_ALERT_2", "LOW_SPEED_LOCKOUT", "TMP_FAULT")
if self.CP.carFingerprint in (HONDA_BOSCH_ALT_RADAR | HONDA_BOSCH_CANFD):
# TODO: See if this logic works for all other Honda
min_steer_speed = max(CarControllerParams.STEER_GLOBAL_MIN_SPEED, self.CP.minSteerSpeed)
expected_low_speed_lockout = steer_status == "LOW_SPEED_LOCKOUT" and ret.vEgo < min_steer_speed
ret.steerFaultTemporary = steer_status != "NORMAL" and not expected_low_speed_lockout
else:
# LOW_SPEED_LOCKOUT is not worth a warning
# NO_TORQUE_ALERT_2 can be caused by bump or steering nudge from driver
# FIXME: the stock camera stops steering on NO_TORQUE_ALERT_1
ret.steerFaultTemporary = steer_status not in ("NORMAL", "LOW_SPEED_LOCKOUT", "TJA_LOW_SPEED_LOCKOUT", "NO_TORQUE_ALERT_2")
# All Honda EPS cut off slightly above standstill, some much higher
# Don't alert in the near-standstill range, but alert for per-vehicle configured minimums above that
if CarControllerParams.STEER_GLOBAL_MIN_SPEED < ret.vEgo < (self.CP.minSteerSpeed + 0.5):
self.low_speed_alert = True
elif ret.vEgo > (self.CP.minSteerSpeed + 1.):
# TODO: better handle delayed steering enablement on ALT_RADAR cars
self.low_speed_alert = False
ret.lowSpeedAlert = self.low_speed_alert
if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS:
ret.accFaulted = bool(cp.vl["CRUISE_FAULT_STATUS"]["CRUISE_FAULT"])
else:
if self.CP.openpilotLongitudinalControl:
if self.CP.carFingerprint in (HONDA_BOSCH_CANFD | HONDA_BOSCH_TJA_CONTROL) and (self.CP.flags & HondaFlags.BOSCH_ALT_BRAKE):
ret.accFaulted = bool(cp.vl["BRAKE_MODULE"]["CRUISE_FAULT"])
else:
ret.accFaulted = bool(cp.vl[self.brake_error_msg]["BRAKE_ERROR_1"] or cp.vl[self.brake_error_msg]["BRAKE_ERROR_2"])
# Log non-critical stock ACC/LKAS faults if Nidec (camera)
if self.CP.carFingerprint not in HONDA_BOSCH:
ret.carFaultedNonCritical = bool(cp_cam.vl["ACC_HUD"]["ACC_PROBLEM"] or cp_cam.vl["LKAS_HUD"]["LKAS_PROBLEM"])
ret.espDisabled = cp.vl["VSA_STATUS"]["ESP_DISABLED"] != 0
if self.CP.carFingerprint not in (CAR.HONDA_ODYSSEY_TWN,):
self.dash_speed_seen = self.dash_speed_seen or cp.vl["CAR_SPEED"]["ROUGH_CAR_SPEED_2"] > 1e-3
if self.dash_speed_seen:
conversion = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS
ret.vEgoCluster = cp.vl["CAR_SPEED"]["ROUGH_CAR_SPEED_2"] * conversion
ret.steeringAngleDeg = cp.vl["STEERING_SENSORS"]["STEER_ANGLE"]
ret.steeringRateDeg = cp.vl["STEERING_SENSORS"]["STEER_ANGLE_RATE"]
ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_stalk(
250, cp.vl["SCM_FEEDBACK"]["LEFT_BLINKER"], cp.vl["SCM_FEEDBACK"]["RIGHT_BLINKER"])
ret.brakeHoldActive = cp.vl["VSA_STATUS"]["BRAKE_HOLD_ACTIVE"] == 1
ret.parkingBrake = bool(cp.vl[self.car_state_scm_msg]["PARKING_BRAKE_ON"])
if self.CP.transmissionType == TransmissionType.manual:
ret.gearShifter = GearShifter.reverse if bool(cp.vl["SCM_FEEDBACK"]["REVERSE_LIGHT"]) else GearShifter.drive
else:
gear_position = self.shifter_values.get(cp.vl[self.gearbox_msg]["GEAR_SHIFTER"], None)
ret.gearShifter = self.parse_gear_shifter(gear_position)
ret.gasPressed = cp.vl["POWERTRAIN_DATA"]["PEDAL_GAS"] > 1e-5
ret.steeringTorque = cp.vl["STEER_STATUS"]["STEER_TORQUE_SENSOR"]
ret.steeringPressed = abs(ret.steeringTorque) > STEER_THRESHOLD.get(self.CP.carFingerprint, 1200)
if self.CP.carFingerprint in HONDA_BOSCH:
# The PCM always manages its own cruise control state, but doesn't publish it
if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS:
ret.cruiseState.nonAdaptive = cp_cam.vl["ACC_HUD"]["CRUISE_CONTROL_LABEL"] != 0
if not self.CP.openpilotLongitudinalControl:
# ACC_HUD is on camera bus on radarless cars
acc_hud = cp_cam.vl["ACC_HUD"] if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS else cp.vl["ACC_HUD"]
ret.cruiseState.nonAdaptive = acc_hud["CRUISE_CONTROL_LABEL"] != 0
ret.cruiseState.standstill = acc_hud["CRUISE_SPEED"] == 252.
# On set, cruise set speed pulses between 254~255 and the set speed prev is set to avoid this.
ret.cruiseState.speed = self.v_cruise_pcm_prev if acc_hud["CRUISE_SPEED"] > 160.0 else acc_hud["CRUISE_SPEED"] * self.v_cruise_factor
self.v_cruise_pcm_prev = ret.cruiseState.speed
else:
ret.cruiseState.speed = cp.vl["CRUISE"]["CRUISE_SPEED_PCM"] * CV.KPH_TO_MS
if self.CP.flags & HondaFlags.BOSCH_ALT_BRAKE:
ret.brakePressed = cp.vl["BRAKE_MODULE"]["BRAKE_PRESSED"] != 0
else:
# brake switch has shown some single time step noise, so only considered when
# switch is on for at least 2 consecutive CAN samples
# brake switch rises earlier than brake pressed but is never 1 when in park
brake_switch_vals = cp.vl_all["POWERTRAIN_DATA"]["BRAKE_SWITCH"]
if len(brake_switch_vals):
brake_switch = cp.vl["POWERTRAIN_DATA"]["BRAKE_SWITCH"] != 0
if len(brake_switch_vals) > 1:
self.brake_switch_prev = brake_switch_vals[-2] != 0
self.brake_switch_active = brake_switch and self.brake_switch_prev
self.brake_switch_prev = brake_switch
ret.brakePressed = (cp.vl["POWERTRAIN_DATA"]["BRAKE_PRESSED"] != 0) or self.brake_switch_active
ret.brake = cp.vl["VSA_STATUS"]["USER_BRAKE"]
ret.cruiseState.enabled = cp.vl["POWERTRAIN_DATA"]["ACC_STATUS"] != 0
ret.cruiseState.available = bool(cp.vl[self.car_state_scm_msg]["MAIN_ON"])
# Bosch cars can report stale ACC faults during early startup.
if ret.accFaulted:
if (self.CP.carFingerprint in HONDA_BOSCH) and not self.initial_accFault_cleared:
# Gate initial stale faults via availability (accFaulted is sticky until offroad).
ret.accFaulted = False
ret.cruiseState.available = False
elif self.initial_accFault_cleared_timer == 0:
self.initial_accFault_cleared = True
if self.initial_accFault_cleared_timer > 0:
self.initial_accFault_cleared_timer -= 1
# Gets rid of Pedal Grinding noise when brake is pressed at slow speeds for some models
if self.CP.carFingerprint in (CAR.HONDA_PILOT, CAR.HONDA_RIDGELINE):
if ret.brake > 0.1:
ret.brakePressed = True
if self.CP.carFingerprint in HONDA_BOSCH:
# TODO: find the radarless AEB_STATUS bit and make sure ACCEL_COMMAND is correct to enable AEB alerts
if self.CP.carFingerprint not in HONDA_BOSCH_RADARLESS:
ret.stockAeb = (not self.CP.openpilotLongitudinalControl) and bool(cp.vl["ACC_CONTROL"]["AEB_STATUS"] and cp.vl["ACC_CONTROL"]["ACCEL_COMMAND"] < -1e-5)
else:
ret.stockAeb = bool(cp_cam.vl["BRAKE_COMMAND"]["AEB_REQ_1"] and cp_cam.vl["BRAKE_COMMAND"]["COMPUTER_BRAKE"] > 1e-5)
self.acc_hud = False
self.lkas_hud = False
if self.CP.carFingerprint not in HONDA_BOSCH:
ret.stockFcw = cp_cam.vl["BRAKE_COMMAND"]["FCW"] != 0
self.acc_hud = cp_cam.vl["ACC_HUD"]
self.stock_brake = cp_cam.vl["BRAKE_COMMAND"]
if self.CP.carFingerprint in (HONDA_BOSCH_RADARLESS | HONDA_BOSCH_CANFD):
self.lkas_hud = cp_cam.vl["LKAS_HUD"]
if self.CP.carFingerprint in HONDA_BOSCH_CANFD:
# The radar emits low-rate tick reference messages that keep running even while its data
# messages are disabled, so the look-alikes are phased to the stock cadence off of them.
#
# There is a one-frame (10 ms) delay between reading a tick here in carstate and transmitting the
# response in carcontroller. The stock radar sends each data message in the SAME frame as its
# tick, so we pulse one frame BEFORE the next tick (counter == period-1): the +1 transmit delay
# then lands the message on the next tick frame, matching stock.
# period (frames @100Hz): 0x710=100, 0x730=10, 0x750=2, RADAR_REFERENCE=20
self.radar_ref_counter = cp.vl["RADAR_REFERENCE"]["COUNTER"]
# 5 Hz: RADAR_REFERENCE (0x3A1) is on the powertrain bus (cp), not the radar bus (cp_radar).
# RADAR_LEAD does NOT ride with the reference; stock sends it ~120 ms (12 frames) after, so fire
# at frame 11 (+1 transmit delay -> ~120 ms)
ref_tick_vals = cp.vl_all.get("RADAR_REFERENCE", {}).get("COUNTER", [])
if len(ref_tick_vals) > 0:
self.radar_5hz_tick_counter = 0
else:
self.radar_5hz_tick_counter += 1
self.radar_5hz_tick = (self.radar_5hz_tick_counter == 11)
supp_tick_vals = cp_radar.vl_all.get("RADAR_SUPP_TICK_REFERENCE", {}).get("IGNORE", [])
if len(supp_tick_vals) > 0:
self.supp_tick_counter = 0
else:
self.supp_tick_counter += 1
self.supp_tick = (self.supp_tick_counter == 99)
hud_tick_vals = cp_radar.vl_all.get("RADAR_HUD_TICK_REFERENCE", {}).get("IGNORE", [])
if len(hud_tick_vals) > 0:
self.hud_tick_counter = 0
else:
self.hud_tick_counter += 1
self.hud_tick = (self.hud_tick_counter == 9)
tick_50hz_vals = cp_radar.vl_all.get("RADAR_50HZ_TICK_REFERENCE", {}).get("IGNORE", [])
if len(tick_50hz_vals) > 0:
self.radar_50hz_tick_counter = 0
else:
self.radar_50hz_tick_counter += 1
self.radar_50hz_tick = (self.radar_50hz_tick_counter == 1)
# Deferred radar disable (see carcontroller). The stock radar transmits ACC_CONTROL every 2
# frames, so 4 missed frames means it has been silenced; assume alive until then so the
# replacement stream never overlaps it
self.canfd_frames += 1
if len(cp.vl_all.get("ACC_CONTROL", {}).get("COUNTER", [])) > 0:
self.stock_acc_counter = 0
else:
self.stock_acc_counter += 1
self.stock_acc_alive = self.stock_acc_counter < 4
# While the comma relay is closed the camera's STEERING_CONTROL is physically visible on the PT
# bus; when the relay opens it disappears (openpilot's own 0xE4 TX is not parsed as RX). As a
# fallback, assume the relay is open after 5 s of controls in case the camera was never seen
if len(cp.vl_all.get("STEERING_CONTROL", {}).get("COUNTER", [])) > 0:
self.camera_steer_counter = 0
self.camera_steer_seen = True
else:
self.camera_steer_counter += 1
self.canfd_relay_open = (self.camera_steer_seen and self.camera_steer_counter >= 5) or self.canfd_frames >= 500
else:
self.supp_tick = False
self.hud_tick = False
self.radar_5hz_tick = False
self.radar_50hz_tick = False
if self.CP.enableBsm:
# BSM messages are on B-CAN, requires a panda forwarding B-CAN messages to CAN 0
# more info here: https://github.com/commaai/openpilot/pull/1867
ret.leftBlindspot = cp_body.vl["BSM_STATUS_LEFT"]["BSM_ALERT"] == 1
ret.rightBlindspot = cp_body.vl["BSM_STATUS_RIGHT"]["BSM_ALERT"] == 1
ret.buttonEvents = [
*create_button_events(self.cruise_buttons, prev_cruise_buttons, BUTTONS_DICT),
*create_button_events(self.cruise_setting, prev_cruise_setting, SETTINGS_BUTTONS_DICT),
]
IQCarState.update(self, ret, ret_iq, can_parsers)
if self.camera_object_tracker is not None:
self.camera_object_tracker.update(cp_cam)
return ret, ret_iq
def get_can_parsers(self, CP, CP_IQ):
pt_messages = []
cam_messages = []
if CP.carFingerprint in HONDA_BOSCH_CANFD:
# Radar-alive and relay-open detection for the deferred radar disable (see carcontroller).
# Both messages intentionally go silent (the radar is disabled, the camera ends up behind the
# open relay), so subscribe with NaN frequency to skip the alive/timeout checks
pt_messages += [("ACC_CONTROL", float('nan')), ("STEERING_CONTROL", float('nan'))]
if CP.carFingerprint in HONDA_BOSCH_RADARLESS:
# polled by the CameraObjectTracker, but not every radarless camera emits it
cam_messages += [("HUD_OBJECTS", float('nan'))]
parsers = {
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, CanBus(CP).pt),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, CanBus(CP).camera),
}
if CP.enableBsm:
parsers[Bus.body] = CANParser(DBC[CP.carFingerprint][Bus.body], [], CanBus(CP).radar)
if CP.carFingerprint in HONDA_BOSCH_CANFD:
# The tick references are only read via vl_all, which (unlike vl) does not auto-subscribe
# messages, so they must be listed explicitly or they are never parsed.
# 0x710 RADAR_SUPP_TICK_REFERENCE (1 Hz), 0x730 RADAR_HUD_TICK_REFERENCE (10 Hz),
# 0x750 RADAR_50HZ_TICK_REFERENCE (50 Hz)
parsers[Bus.radar] = CANParser(DBC[CP.carFingerprint][Bus.radar], [
("RADAR_SUPP_TICK_REFERENCE", 0),
("RADAR_HUD_TICK_REFERENCE", 0),
("RADAR_50HZ_TICK_REFERENCE", 0),
], CanBus(CP).radar)
return parsers

View File

@@ -0,0 +1,186 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from dataclasses import dataclass, field
import numpy as np
POINT_COUNT = 40
POINTS_PER_FRAME = 4
SWEEP_INDICES = POINT_COUNT // POINTS_PER_FRAME
# the camera repeats each sweep index across four redundant banks: mux = index + bank*16,
# giving mux values 1-10, 17-26, 33-42 and 49-58 for logical indices 0-9
MUX_CYCLE = tuple(index + bank * 16 for bank in range(4) for index in range(1, SWEEP_INDICES + 1))
OFFSET_UNAVAILABLE = 2047
OFFSET_VALID_MAX = 2046
NEAR_M = 2.0
FAR_M = 100.0
LOOKAHEAD_M = np.linspace(NEAR_M, FAR_M, POINT_COUNT)
# full swing center -> max turn is slewed over this long so model jumps can't teleport the dash lane
SLEW_RATE_HZ = 50.0
SLEW_FULL_SCALE_S = 2.0
SLEW_MAX_STEP = OFFSET_VALID_MAX / (SLEW_FULL_SCALE_S * SLEW_RATE_HZ)
def _stock_gain(d):
# raw offset units per meter of lateral, regressed from stock radar sweeps vs modelV2 lane centers
return 29.3 + 0.243 * d - 0.00228 * d ** 2
def _legacy_gain(d):
return 6.27 + 0.0106 * d + 0.000354 * d ** 2
GAIN = _stock_gain(LOOKAHEAD_M)
def gain_correction(d: float) -> float:
# the HUD lead marker's lateral scale was tuned against lanes drawn with the legacy (flatter) gain
# law, so the lead's lateral must ride this ratio to stay on the corrected lane rendering
d = min(max(float(d), NEAR_M), FAR_M)
return _stock_gain(d) / _legacy_gain(d)
LANE_LINE_ON = 3
LANE_LENGTH_MAX_VALUE = 33
LANE_WIDTH_DEFAULT = 32
LINE_PROB_ON = 0.25
LINE_PROB_OFF = 0.10
HALF_LANE_M = 1.65
FULL_REACH_SPEED = 27.0
FULL_REACH_LEAD_DIST = 70.0
MIN_REACH = 0.15
def encode_lane_path(x, y):
x = np.asarray(x, dtype=float)
y = np.asarray(y, dtype=float)
if x.size < 2 or x.max() < FAR_M:
return [OFFSET_UNAVAILABLE] * POINT_COUNT
lat = np.interp(LOOKAHEAD_M, x, y)
# stock encodes offsets with the opposite lateral sign to openpilot's +left convention
raw = np.clip(np.round(-GAIN * lat), -OFFSET_VALID_MAX, OFFSET_VALID_MAX)
return [int(v) for v in raw]
# The CAN FD dash has no LKAS_HUD_2 to carry the drawn length: it reads the path as a contiguous valid
# prefix ended by an in-band OFFSET_UNAVAILABLE terminator, idles at 6 valid zero offsets (never
# all-unavailable), and cross-checks the prefix length against RADAR_LEAD's LANE_PATH_LENGTH.
CANFD_MAX_VALID_PTS = 23
CANFD_MIN_VALID_PTS = 6
CANFD_IDLE_OFFSETS = [0] * CANFD_MIN_VALID_PTS + [OFFSET_UNAVAILABLE] * (POINT_COUNT - CANFD_MIN_VALID_PTS)
# stock valid-point count is a function of ego speed alone, fit from factory lanes-on RADAR_LEAD frames
CANFD_LEN_INTERCEPT = 6.74
CANFD_LEN_SLOPE = 0.862
@dataclass
class RenderedLane:
offsets: list[int] = field(default_factory=lambda: [OFFSET_UNAVAILABLE] * POINT_COUNT)
reach: float = 0.0
left_line: bool = False
right_line: bool = False
lane_cross: int = 0
v_ego: float = 0.0
@property
def blank(self) -> bool:
return self.reach <= 0.0 or self.offsets[0] == OFFSET_UNAVAILABLE
def canfd_lane_length(lane: RenderedLane) -> int:
if lane.blank:
return CANFD_MIN_VALID_PTS
n = round(CANFD_LEN_INTERCEPT + CANFD_LEN_SLOPE * lane.v_ego)
return max(CANFD_MIN_VALID_PTS, min(CANFD_MAX_VALID_PTS, n))
def canfd_lane_offsets(lane: RenderedLane) -> list[int]:
if lane.blank:
return CANFD_IDLE_OFFSETS
n_valid = canfd_lane_length(lane)
return list(lane.offsets[:n_valid]) + [OFFSET_UNAVAILABLE] * (POINT_COUNT - n_valid)
def create_lane_path(packer, bus, offsets, mux):
base = ((mux - 1) % 16) * POINTS_PER_FRAME
values = {"MUX": mux}
for i in range(POINTS_PER_FRAME):
values[f"PATH_OFFSET_{i + 1}"] = offsets[base + i]
return packer.make_can_msg("LANE_PATH", bus, values)
def create_lkas_hud_2(packer, bus, counter_2, reach=1.0, lane_cross=0, left_line=True, right_line=True):
lane_length = max(0, min(LANE_LENGTH_MAX_VALUE, round(reach * LANE_LENGTH_MAX_VALUE)))
shown = lane_length > 0
values = {
"COUNTER_2": counter_2,
"SET_ME_X01": 1,
"LANE_WIDTH": LANE_WIDTH_DEFAULT,
"LEFT_LANE": LANE_LINE_ON if (shown and left_line) else 0,
"RIGHT_LANE": LANE_LINE_ON if (shown and right_line) else 0,
"LEFT_LANE_CROSSED": 1 if (shown and lane_cross < 0) else 0,
"RIGHT_LANE_CROSSED": 1 if (shown and lane_cross > 0) else 0,
"LANE_LENGTH": lane_length,
}
return packer.make_can_msg("LKAS_HUD_2", bus, values)
class LanePathRenderer:
def __init__(self):
self._left_on = False
self._right_on = False
self._shown = None
def _lane_center(self, model):
lls, probs = model.laneLines, model.laneLineProbs
if len(lls) < 3 or len(probs) < 3 or len(lls[1].x) == 0:
return None, None, False, False
left = probs[1] >= (LINE_PROB_OFF if self._left_on else LINE_PROB_ON)
right = probs[2] >= (LINE_PROB_OFF if self._right_on else LINE_PROB_ON)
x = np.array(lls[1].x)
yl, yr = np.array(lls[1].y), np.array(lls[2].y)
if left and right:
y = (yl + yr) / 2.0
elif right:
y = yr - HALF_LANE_M
elif left:
y = yl + HALF_LANE_M
else:
return None, None, False, False
return x, y, left, right
def _slew(self, offsets):
# an all-sentinel fit draws nothing: pass through and reset so the next real fit shows unslewed
if offsets[0] == OFFSET_UNAVAILABLE:
self._shown = None
return offsets
target = np.asarray(offsets, dtype=float)
if self._shown is None:
self._shown = target
else:
self._shown = self._shown + np.clip(target - self._shown, -SLEW_MAX_STEP, SLEW_MAX_STEP)
return [int(v) for v in np.round(self._shown)]
def update(self, model, v_ego, lead_d) -> RenderedLane:
x = y = None
left_on = right_on = False
if model is not None:
x, y, left_on, right_on = self._lane_center(model)
if x is None:
self._shown = None
return RenderedLane()
self._left_on, self._right_on = left_on, right_on
reach = float(np.clip(max(v_ego / FULL_REACH_SPEED, lead_d / FULL_REACH_LEAD_DIST, MIN_REACH), 0.0, 1.0))
if round(reach * LANE_LENGTH_MAX_VALUE) <= 0:
self._shown = None
return RenderedLane()
return RenderedLane(self._slew(encode_lane_path(x, y)), reach, left_on, right_on, v_ego=v_ego)

View File

@@ -0,0 +1,314 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
import math
from dataclasses import dataclass
from iqdbc.can.parser import CANParser
from iqdbc.car.honda import dash_lane
NUM_SLOTS = 10
LONG_DIST_CAP_M = 195.0
# byte-faithful empty-slot payload decoded from stock HUD_OBJECTS; an inconsistent frame risks the dash rejecting it
INACTIVE = {
"OBJECT_ID": 0,
"IS_LEAD_CAR": 0,
"CAR_TYPE": -1,
"ROTATION": -128,
"LONG_DIST": 196.9,
"LAT_DIST": 204.7,
}
CAR_TYPE_CAR = 7
LONG_DIST_MAX_M = 194.0
LAT_DIST_LIM_M = 204.7
# the dash under-scales LAT_DIST ~0.3x in the ego frame; tuned on-car so the lead marker lands on the lane
LAT_SCALE = 0.35
ROT_BAND_M = 1.5
ROT_MAX = 6
REID_GAP_M = 8.0
REID_TAU = 1.5
REID_REFRACTORY = 1.5
MAX_OBJECT_ID = 31
DREL_SMOOTH_TAU = 0.6
YREL_SMOOTH_TAU = 0.5
FF_VREL_MIN = 0.5
DREL_RESID_CLAMP = 1.5
LEAD_PROB_ON = 0.5
LEAD_PROB_OFF = 0.35
LEAD_HOLD_S = 0.6
# modelV2.leadsV3 entries are one car at three time horizons, not three cars: only render the extra
# horizons when spatially distinct from everything already rendered (a genuinely different vehicle)
EXTRA_LEAD_SLOTS = (1, 2)
EXTRA_LEAD_MIN_SEP_D = 5.0
EXTRA_LEAD_MIN_SEP_Y = 1.5
@dataclass
class CameraObject:
slot: int
object_id: int
d_rel: float
y_rel: float
is_lead_car: bool
valid: bool
car_type: int = -1
rotation: int = -128
class CameraObjectTracker:
def __init__(self):
self._tracks: list[CameraObject] = [
CameraObject(slot=i, object_id=0, d_rel=0.0, y_rel=0.0, is_lead_car=False, valid=False)
for i in range(NUM_SLOTS)
]
def update(self, cp_cam: CANParser) -> None:
vla = cp_cam.vl_all["HUD_OBJECTS"]
for mux, oid, ld, yd, lead, ct, rot in zip(vla["MUX"], vla["OBJECT_ID"], vla["LONG_DIST"], vla["LAT_DIST"],
vla["IS_LEAD_CAR"], vla["CAR_TYPE"], vla["ROTATION"], strict=True):
slot = (int(mux) - 1) % 16
if 0 <= slot < NUM_SLOTS:
self._tracks[slot] = CameraObject(
slot=slot,
object_id=int(oid),
d_rel=float(ld),
y_rel=float(yd),
is_lead_car=bool(lead),
valid=oid != 0 and ld < LONG_DIST_CAP_M,
car_type=int(ct),
rotation=int(rot),
)
def snapshot(self) -> list[CameraObject]:
return self._tracks
@dataclass
class ModelLead:
status: bool
dRel: float
yRel: float
vRel: float
prob: float = 0.0
def leads_from_model(model, v_ego, n=3):
# modelV2's lateral is +right; the dash convention is +left. v is made relative for the smoother.
# Data stays populated below LEAD_PROB_ON (status False, prob carried) so the author's hysteresis
# can keep an already-rendered lead alive down to LEAD_PROB_OFF instead of blinking it
out = []
for i in range(n):
if model is None or len(model.leadsV3) <= i or len(model.leadsV3[i].x) == 0:
out.append(ModelLead(False, 0.0, 0.0, 0.0))
continue
lead = model.leadsV3[i]
out.append(ModelLead(bool(lead.prob >= LEAD_PROB_ON), float(lead.x[0]), -float(lead.y[0]),
float(lead.v[0]) - v_ego, prob=float(lead.prob)))
return out
def lead_rotation(lateral_left_m: float) -> int:
magnitude = min(round(abs(lateral_left_m) / ROT_BAND_M), ROT_MAX)
return -magnitude if lateral_left_m > 0 else magnitude
class LeadIdentity:
"""Mints a stable OBJECT_ID for the rendered lead, re-IDing on a fresh lead or a range discontinuity.
dRel is noisy, so a leaky predictor (feed-forward vRel, leak toward dRel) accumulates the residual
instead of a per-sample range-rate test."""
def __init__(self):
self.object_id = 0
self._on = False
self._pred = 0.0
self._prev_t = 0.0
self._reid_t = -1e9
def update(self, status: bool, d_rel: float, v_rel: float, now: float) -> int:
if not status:
self.object_id = 0
self._on = False
return 0
new_lead = not self._on
if self._on:
dt = max(now - self._prev_t, 1e-3)
self._pred += v_rel * dt
self._pred += min(dt / REID_TAU, 1.0) * (d_rel - self._pred)
if abs(d_rel - self._pred) > REID_GAP_M and now - self._reid_t > REID_REFRACTORY:
new_lead = True
self._prev_t = now
if new_lead:
self.object_id = self.object_id % MAX_OBJECT_ID + 1
self._reid_t = now
self._pred = d_rel
self._on = True
return self.object_id
class MarkerSmoother:
"""Stabilizes a rendered marker without lagging real motion: vRel feed-forward on dRel with a
clamped leak toward the measurement, plain low-pass on yRel, snapping on an identity change."""
def __init__(self):
self._id = 0
self._d = 0.0
self._y = 0.0
self._t = 0.0
def update(self, d_rel: float, y_rel: float, v_rel: float, object_id: int, now: float) -> tuple[float, float]:
if object_id != self._id:
self._id, self._d, self._y, self._t = object_id, d_rel, y_rel, now
return d_rel, y_rel
dt = max(now - self._t, 1e-3)
self._t = now
if abs(v_rel) >= FF_VREL_MIN:
self._d += v_rel * dt
resid = min(max(d_rel - self._d, -DREL_RESID_CLAMP), DREL_RESID_CLAMP)
self._d += (1.0 - math.exp(-dt / DREL_SMOOTH_TAU)) * resid
self._y += (1.0 - math.exp(-dt / YREL_SMOOTH_TAU)) * (y_rel - self._y)
return self._d, self._y
def create_hud_object(packer, bus, mux, track):
values = {"MUX": mux}
if track is None:
values.update(INACTIVE)
else:
values.update({
"OBJECT_ID": int(track["object_id"]),
"IS_LEAD_CAR": int(track["is_lead_car"]),
"CAR_TYPE": int(track["car_type"]),
"ROTATION": int(track["rotation"]),
"LONG_DIST": min(max(track["d_rel"], 0.0), LONG_DIST_MAX_M),
"LAT_DIST": min(max(track["y_rel"], -LAT_DIST_LIM_M), LAT_DIST_LIM_M),
})
return packer.make_can_msg("HUD_OBJECTS", bus, values)
def forward_hud_object(packer, bus, mux, tracks):
slot = (mux - 1) % 16
st = tracks[slot] if (tracks and slot < len(tracks)) else None
track = ({"d_rel": st.d_rel, "y_rel": st.y_rel, "object_id": st.object_id, "is_lead_car": st.is_lead_car,
"car_type": st.car_type, "rotation": st.rotation} if (st is not None and st.valid) else None)
return create_hud_object(packer, bus, mux, track)
class DashObjectAuthor:
"""Authors HUD_OBJECTS: openpilot's lead in slot 0 with a stable identity and smoothed marker, the
camera's non-lead cars forwarded in slots 1-9 (or distinct extra model leads where there is no
camera to forward), one frame per mux tick."""
def __init__(self):
self._identity = LeadIdentity()
self._smoother = MarkerSmoother()
self._lead_id = 0
self._prev_op_id = 0
self._lead_on = False
self._lead_hold: ModelLead | None = None
self._lead_seen_t = -1e9
self._extra_ids = {slot: LeadIdentity() for slot in EXTRA_LEAD_SLOTS}
self._extra_smooth = {slot: MarkerSmoother() for slot in EXTRA_LEAD_SLOTS}
self._extra_emit = dict.fromkeys(EXTRA_LEAD_SLOTS, 0)
def _gate_lead(self, lead: ModelLead, now: float) -> ModelLead:
# leadsV3[0].prob hovers around 0.5 in traffic; hysteresis plus a short dead-reckoned hold keeps
# the marker from blinking at a cadence the stock radar never produces
if lead.prob >= (LEAD_PROB_OFF if self._lead_on else LEAD_PROB_ON):
self._lead_on = True
self._lead_hold = lead
self._lead_seen_t = now
return lead if lead.status else ModelLead(True, lead.dRel, lead.yRel, lead.vRel, lead.prob)
if self._lead_on and self._lead_hold is not None and now - self._lead_seen_t < LEAD_HOLD_S:
h = self._lead_hold
return ModelLead(True, h.dRel + h.vRel * (now - self._lead_seen_t), h.yRel, h.vRel, h.prob)
self._lead_on = False
self._lead_hold = None
return ModelLead(False, 0.0, 0.0, 0.0)
def _lead_object_id(self, status: bool, op_id: int, stock_lead_id: int | None, in_use: set[int]) -> int:
if not status:
self._lead_id = 0
elif stock_lead_id is not None:
self._lead_id = stock_lead_id
elif self._lead_id == 0 or op_id != self._prev_op_id or self._lead_id in in_use:
# advance from the current id rather than picking the lowest free one: with no camera ids in
# use a handoff would keep the same id and the id-keyed smoother would slide between two cars
# instead of snapping
nxt = self._lead_id % MAX_OBJECT_ID + 1
while nxt in in_use:
nxt = nxt % MAX_OBJECT_ID + 1
self._lead_id = nxt
self._prev_op_id = op_id
return self._lead_id
def _update_extras(self, extra_leads, lead, in_use, now):
rendered = [(lead.dRel, lead.yRel)] if lead.status else []
out = {}
for slot, ex in zip(EXTRA_LEAD_SLOTS, extra_leads or (), strict=False):
distinct = ex.status and all(abs(ex.dRel - d) >= EXTRA_LEAD_MIN_SEP_D or
abs(ex.yRel - y) >= EXTRA_LEAD_MIN_SEP_Y
for d, y in rendered)
op_id = self._extra_ids[slot].update(distinct, ex.dRel, ex.vRel, now)
if not distinct:
self._extra_emit[slot] = 0
out[slot] = None
continue
emit = self._extra_emit[slot]
if emit == 0 or emit in in_use:
emit = op_id
while emit in in_use:
emit = emit % MAX_OBJECT_ID + 1
self._extra_emit[slot] = emit
in_use.add(emit)
d_rel, y_rel = self._extra_smooth[slot].update(ex.dRel, LAT_SCALE * ex.yRel, ex.vRel, emit, now)
rendered.append((ex.dRel, ex.yRel))
out[slot] = {"d_rel": d_rel, "y_rel": y_rel, "object_id": emit, "is_lead_car": 0,
"car_type": CAR_TYPE_CAR, "rotation": lead_rotation(y_rel / LAT_SCALE)}
return out
def create(self, packer, bus, lead, tracks, mux: int, now: float, extra_leads=None):
lead = self._gate_lead(lead, now)
op_id = self._identity.update(lead.status, lead.dRel, lead.vRel, now)
stock_lead, in_use = None, set()
for t in (tracks or ()):
if not t.valid:
continue
if t.is_lead_car:
stock_lead = t
elif t.slot != 0:
in_use.add(t.object_id)
stock_lead_id = stock_lead.object_id if stock_lead is not None else None
lead_id = self._lead_object_id(lead.status, op_id, stock_lead_id, in_use)
if lead.status:
in_use.add(lead_id)
# ride the lane gain-law correction at the lead's distance so the marker tracks the lane rendering
lat_scale = LAT_SCALE * dash_lane.gain_correction(lead.dRel)
d_rel, y_rel = self._smoother.update(lead.dRel, lat_scale * lead.yRel, lead.vRel, lead_id, now)
extras = self._update_extras(extra_leads, lead, in_use, now) if tracks is None else {}
slot = (mux - 1) % 16
if slot == 0 and lead.status:
track = {"d_rel": d_rel, "y_rel": y_rel, "object_id": lead_id, "is_lead_car": 1,
"car_type": stock_lead.car_type if stock_lead is not None else CAR_TYPE_CAR,
"rotation": stock_lead.rotation if stock_lead is not None else lead_rotation(y_rel / lat_scale)}
elif slot in extras:
track = extras[slot]
else:
st = tracks[slot] if (tracks and slot < len(tracks)) else None
# never forward the camera's lead: if OP has no lead, the HUD must not flag one OP isn't acting on
track = ({"d_rel": st.d_rel, "y_rel": st.y_rel, "object_id": st.object_id, "is_lead_car": 0,
"car_type": st.car_type, "rotation": st.rotation}
if (st is not None and st.valid and not st.is_lead_car) else None)
return create_hud_object(packer, bus, mux, track)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,324 @@
from iqdbc.car import CanBusBase
from iqdbc.car.common.conversions import Conversions as CV
from iqdbc.car.honda.values import (HondaFlags, HONDA_BOSCH, HONDA_BOSCH_ALT_RADAR, HONDA_BOSCH_RADARLESS,
HONDA_BOSCH_CANFD, CarControllerParams)
from iqdbc.lvbs.car.honda.iq_values import HondaFlagsIQ
# CAN bus layout with relay
# 0 = ACC-CAN - radar side
# 1 = F-CAN B - powertrain
# 2 = ACC-CAN - camera side
# 3 = F-CAN A - OBDII port
class CanBus(CanBusBase):
def __init__(self, CP=None, fingerprint=None) -> None:
# use fingerprint if specified
super().__init__(CP if fingerprint is None else None, fingerprint)
# powertrain bus is split instead of radar on radarless and CAN FD Bosch
if CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS - HONDA_BOSCH_CANFD):
self._pt, self._radar = self.offset + 1, self.offset
# normally steering commands are sent to radar, which forwards them to powertrain bus
# when radar is disabled, steering commands are sent directly to powertrain bus
self._lkas = self._pt if CP.openpilotLongitudinalControl else self._radar
else:
self._pt, self._radar, self._lkas = self.offset, self.offset + 1, self.offset
@property
def pt(self) -> int:
return self._pt
@property
def radar(self) -> int:
return self._radar
@property
def camera(self) -> int:
return self.offset + 2
@property
def lkas(self) -> int:
return self._lkas
# B-CAN is forwarded to ACC-CAN radar side (CAN 0 on fake ethernet port)
@property
def body(self) -> int:
return self.offset
def create_brake_command(packer, CAN, apply_brake, pump_on, pcm_override, pcm_cancel_cmd, fcw, car_fingerprint, stock_brake, CP_IQ):
# TODO: do we loose pressure if we keep pump off for long?
brakelights = apply_brake > 0
brake_rq = apply_brake > 0
pcm_fault_cmd = False
values = {
"CRUISE_OVERRIDE": pcm_override,
"CRUISE_FAULT_CMD": pcm_fault_cmd,
"CRUISE_CANCEL_CMD": pcm_cancel_cmd,
"COMPUTER_BRAKE_REQUEST": brake_rq,
"SET_ME_1": 1,
"BRAKE_LIGHTS": brakelights,
"CHIME": stock_brake["CHIME"] if fcw else 0, # send the chime for stock fcw
"FCW": fcw << 1, # TODO: Why are there two bits for fcw?
"AEB_REQ_1": 0,
"AEB_REQ_2": 0,
"AEB_STATUS": 0,
}
if CP_IQ.flags & HondaFlagsIQ.NIDEC_HYBRID:
values["COMPUTER_BRAKE_HYBRID"] = apply_brake
values["BRAKE_PUMP_REQUEST_HYBRID"] = apply_brake > 0
else:
values["COMPUTER_BRAKE"] = apply_brake
values["BRAKE_PUMP_REQUEST"] = pump_on
return packer.make_can_msg("BRAKE_COMMAND", CAN.pt, values)
def create_acc_commands(packer, CAN, enabled, active, accel, gas, stopping_counter, CP, gas_force):
commands = []
min_gas_accel = CarControllerParams.BOSCH_GAS_LOOKUP_BP[0]
control_on = 5 if enabled else 0
gas_command = gas if active and gas_force > min_gas_accel else -30000
accel_command = accel if active else 0
braking = 1 if active and gas_force < min_gas_accel else 0
standstill = 1 if active and stopping_counter > 0 else 0
standstill_release = 1 if active and stopping_counter == 0 else 0
# common ACC_CONTROL values
acc_control_values = {
'ACCEL_COMMAND': accel_command,
'STANDSTILL': standstill,
}
if CP.flags & HondaFlags.BOSCH_RADARLESS:
acc_control_values.update({
"CONTROL_ON": enabled,
# hybrid and alt-brake cars require this bit whenever braking; others use it for idle stop after 4s at 50Hz
"COMPUTER_BRAKE_ASSIST": braking if CP.flags & (HondaFlags.HYBRID | HondaFlags.BOSCH_ALT_BRAKE) else stopping_counter > 200,
})
else:
acc_control_values.update({
'BRAKE_REQUEST': braking,
# setting CONTROL_ON causes car to set POWERTRAIN_DATA->ACC_STATUS = 1
"CONTROL_ON": control_on,
"GAS_COMMAND": gas_command, # used for gas
"BRAKE_LIGHTS": braking,
"STANDSTILL_RELEASE": standstill_release,
})
acc_control_on_values = {
"SET_TO_3": 0x03,
"CONTROL_ON": enabled,
"SET_TO_FF": 0xff,
"SET_TO_75": 0x75,
"SET_TO_30": 0x30,
}
commands.append(packer.make_can_msg("ACC_CONTROL_ON", CAN.pt, acc_control_on_values))
commands.append(packer.make_can_msg("ACC_CONTROL", CAN.pt, acc_control_values))
return commands
def create_steering_control(packer, CAN, apply_torque, lkas_active, tja_control):
values = {
"STEER_TORQUE": apply_torque if lkas_active else 0,
"STEER_TORQUE_REQUEST": lkas_active,
}
if tja_control:
values["STEER_DOWN_TO_ZERO"] = lkas_active
return packer.make_can_msg("STEERING_CONTROL", CAN.lkas, values)
def create_bosch_supplemental_1(packer, CAN):
# non-active params
values = {
"SET_ME_X04": 0x04,
"SET_ME_X80": 0x80,
"SET_ME_X10": 0x10,
}
return packer.make_can_msg("BOSCH_SUPPLEMENTAL_1", CAN.lkas, values)
def create_acc_hud(packer, bus, CP, enabled, pcm_speed, pcm_accel, hud_control, hud_v_cruise, is_metric, acc_hud):
acc_hud_values = {
'CRUISE_SPEED': hud_v_cruise,
'ENABLE_MINI_CAR': 1 if enabled else 0,
# only moves the lead car without ACC_ON
'HUD_DISTANCE': hud_control.leadDistanceBars, # wraps to 0 at 4 bars
'IMPERIAL_UNIT': int(not is_metric),
'HUD_LEAD': 2 if enabled and hud_control.leadVisible else 1 if enabled else 0,
'SET_ME_X01_2': 1,
}
if CP.flags & HondaFlags.BOSCH_CANFD:
acc_hud_values['SET_ME_X01'] = int(enabled and (bool(acc_hud_values['HUD_LEAD']) or (pcm_accel < 0.2)))
acc_hud_values['SET_ME_X01_2'] = int(enabled and (bool(acc_hud_values['HUD_LEAD']) or (pcm_accel < 0.2)))
if CP.carFingerprint in HONDA_BOSCH:
acc_hud_values['ACC_ON'] = int(enabled)
acc_hud_values['FCM_OFF'] = 0
acc_hud_values['FCM_OFF_2'] = 0
else:
# Shows the distance bars, TODO: stock camera shows updates temporarily while disabled
acc_hud_values['ACC_ON'] = int(enabled)
acc_hud_values['PCM_SPEED'] = pcm_speed * CV.MS_TO_KPH
acc_hud_values['PCM_GAS'] = pcm_accel
acc_hud_values['SET_ME_X01'] = 1
acc_hud_values['FCM_OFF'] = acc_hud['FCM_OFF']
acc_hud_values['FCM_OFF_2'] = acc_hud['FCM_OFF_2']
acc_hud_values['FCM_PROBLEM'] = acc_hud['FCM_PROBLEM']
acc_hud_values['ICONS'] = acc_hud['ICONS']
return packer.make_can_msg("ACC_HUD", bus, acc_hud_values)
def create_lkas_hud(packer, bus, CP, hud_control, lat_active, steering_available, reduced_steering, alert_steer_required, lkas_hud, dashed_lanes,
steer_fault_permanent=False, lkas_state_change=None):
commands = []
lkas_hud_values = {
'LKAS_READY': 1,
'LKAS_STATE_CHANGE': 1,
'STEERING_REQUIRED': alert_steer_required,
'SOLID_LANES': lat_active,
'DASHED_LANES': dashed_lanes,
'BEEP': 0,
}
# the stock camera holds LKAS_STATE_CHANGE low, pulsing it high ~3s around HUD state changes;
# holding it high permanently suppresses the dash lane-line rendering
if lkas_state_change is not None:
lkas_hud_values['LKAS_STATE_CHANGE'] = int(lkas_state_change)
if CP.carFingerprint in (HONDA_BOSCH_RADARLESS | HONDA_BOSCH_CANFD):
lkas_hud_values['LANE_LINES'] = 3
lkas_hud_values['DASHED_LANES'] = lat_active
# car likely needs to see LKAS_PROBLEM fall within a specific time frame, so forward from camera
if CP.carFingerprint in HONDA_BOSCH_RADARLESS:
lkas_hud_values['LKAS_PROBLEM'] = lkas_hud['LKAS_PROBLEM']
if CP.carFingerprint in HONDA_BOSCH_CANFD:
lkas_hud_values['LKAS_PROBLEM'] = steer_fault_permanent
# CAN FD: dashed lanes are the AOL armed indication (dashed_lanes is aol.enabled and not
# latActive, which is not standstill-gated - so parked LKAS button presses produce cluster
# feedback). ORed with lat_active so the engaged payload keeps SOLID and DASHED set together,
# byte-matching the stock camera's lanes-on state
lkas_hud_values['DASHED_LANES'] = dashed_lanes or lat_active
# every payload change must coincide with an LKAS_STATE_CHANGE pulse (see carcontroller); keyed
# on lat_active, not lanesVisible, so the dash LKAS indication follows AOL's lateral state
lkas_hud_values['SOLID_LANES'] = lat_active
if not (CP.flags & HondaFlags.BOSCH_EXT_HUD):
lkas_hud_values['RDM_OFF'] = 1
lkas_hud_values['LANE_ASSIST_BEEP_OFF'] = 1
# New HUD concept for selected Bosch cars, overwrites some of the above
# TODO: make global across all Honda if feedback is favorable
if CP.carFingerprint in HONDA_BOSCH_ALT_RADAR:
lkas_hud_values['DASHED_LANES'] = steering_available and lat_active
lkas_hud_values['SOLID_LANES'] = lat_active
lkas_hud_values['LKAS_PROBLEM'] = lat_active and reduced_steering
if CP.flags & HondaFlags.BOSCH_EXT_HUD and not CP.openpilotLongitudinalControl:
commands.append(packer.make_can_msg('LKAS_HUD_A', bus, lkas_hud_values))
commands.append(packer.make_can_msg('LKAS_HUD_B', bus, lkas_hud_values))
else:
commands.append(packer.make_can_msg('LKAS_HUD', bus, lkas_hud_values))
return commands
def create_radar_hud(packer, bus):
radar_hud_values = {
'CMBS_OFF': 0x01,
'SET_TO_1': 0x01,
}
return packer.make_can_msg('RADAR_HUD', bus, radar_hud_values)
def create_legacy_brake_command(packer, bus):
return packer.make_can_msg("LEGACY_BRAKE_COMMAND", bus, {})
def spam_buttons_command(packer, CAN, cruise_button, cruise_setting, ambient_light, car_fingerprint, bus=None):
values = {
'CRUISE_BUTTONS': cruise_button,
'CRUISE_SETTING': cruise_setting,
# the camera consumes this byte too (adaptive high beam); echo the SCM's live value
'AMBIENT_LIGHT_MAYBE': ambient_light,
}
if bus is None:
# send buttons to camera on radarless (camera does ACC) cars
bus = CAN.camera if car_fingerprint in HONDA_BOSCH_RADARLESS else CAN.pt
return packer.make_can_msg("SCM_BUTTONS", bus, values)
def create_radar_hud_canfd(packer, bus, acc, acc_pulse=False):
values = {
# the stock radar raises this bit only in short bursts right after ACC engages, never held
'CMBS_ENABLED_MAYBE': 1 if (acc and acc_pulse) else 0,
'ACC_ON': acc,
'SET_ME_X01': 0x01,
'SET_ME_X01_2': 0x01,
}
return packer.make_can_msg("RADAR_HUD_CANFD", bus, values)
def create_canfd_supplemental(packer, bus):
values = {
'SET_ME_X01': 0x01,
'SET_ME_X41': 0x41,
}
return packer.make_can_msg("BOSCH_SUPPLEMENTAL_CANFD", bus, values)
def create_canfd_5hz_radar_messages(packer, bus, radar_ref_cntr, lane_path_length=6, left_lane=0, right_lane=0):
commands = []
radar_lead_values = {
'CNTR_REF': radar_ref_cntr,
'SET_ME_X01': 0x01,
# stock radar transmits a constant 140 here; 120 causes a camera mismatch
'TARGET_SPEED_MAYBE': 140,
'LEFT_LANE': left_lane,
'RIGHT_LANE': right_lane,
# the dash cross-checks this against the LANE_PATH in-band terminator; a mismatch suppresses the lane lines
'LANE_PATH_LENGTH': lane_path_length,
}
commands.append(packer.make_can_msg('RADAR_LEAD', bus, radar_lead_values))
radar_lead2_values = {
'SET_ME_X88': 136,
'SET_ME_X78': 120,
'LEAD_DISTANCE_MAYBE': 0,
}
commands.append(packer.make_can_msg('RADAR_LEAD2', bus, radar_lead2_values))
return commands
def honda_checksum(address: int, sig, d: bytearray) -> int:
s = 0
extended = address > 0x7FF
# extended ids above 0x100000 use a different checksum constant, observed on Bosch CAN FD radar messages
high_extended = address > 0x100000
addr = address
while addr:
s += addr & 0xF
addr >>= 4
for i in range(len(d)):
x = d[i]
if i == len(d) - 1:
x >>= 4
s += (x & 0xF) + (x >> 4)
s = 8 - s
if extended:
s += 10 if high_extended else 3
return s & 0xF

View File

@@ -0,0 +1,392 @@
#!/usr/bin/env python3
import numpy as np
from iqdbc.car import get_safety_config, structs, uds
from iqdbc.car.common.conversions import Conversions as CV
from iqdbc.car.disable_ecu import disable_ecu, clear_all_dtcs, clear_ecu_dtcs
from iqdbc.car.honda.hondacan import CanBus
from iqdbc.car.honda.values import CarControllerParams, HondaFlags, CAR, HONDA_BOSCH, HONDA_BOSCH_CANFD, \
HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_RADARLESS, \
HONDA_RADAR_SCAN_VERIFIED, HondaSafetyFlags
from iqdbc.car.honda.carcontroller import CarController
from iqdbc.car.honda.carstate import CarState
from iqdbc.car.honda.radar_interface import RadarInterface
from iqdbc.car.interfaces import CarInterfaceBase
from iqdbc.lvbs.car.honda.iq_values import HondaFlagsIQ, HondaSafetyFlagsIQ
TransmissionType = structs.CarParams.TransmissionType
class CarInterface(CarInterfaceBase):
CarState = CarState
CarController = CarController
RadarInterface = RadarInterface
DRIVABLE_GEARS = (structs.CarState.GearShifter.sport,)
@staticmethod
def get_pid_accel_limits(CP, CP_IQ, current_speed, cruise_speed):
if CP.carFingerprint in HONDA_BOSCH:
return CarControllerParams.BOSCH_ACCEL_MIN, CarControllerParams.BOSCH_ACCEL_MAX
elif CP_IQ.enableGasInterceptor:
return CarControllerParams.NIDEC_ACCEL_MIN, CarControllerParams.NIDEC_ACCEL_MAX
else:
# NIDECs don't allow acceleration near cruise_speed,
# so limit limits of pid to prevent windup
ACCEL_MAX_VALS = [CarControllerParams.NIDEC_ACCEL_MAX, 0.2]
ACCEL_MAX_BP = [cruise_speed - 2., cruise_speed - .2]
return CarControllerParams.NIDEC_ACCEL_MIN, np.interp(current_speed, ACCEL_MAX_BP, ACCEL_MAX_VALS)
@staticmethod
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
ret.brand = "honda"
CAN = CanBus(ret, fingerprint)
if candidate in HONDA_BOSCH:
cfgs = [get_safety_config(structs.CarParams.SafetyModel.hondaBosch)]
if candidate in HONDA_BOSCH_CANFD and CAN.pt >= 4:
cfgs.insert(0, get_safety_config(structs.CarParams.SafetyModel.noOutput))
ret.safetyConfigs = cfgs
# The object scan survives openpilot longitudinal: the radar disable is subnet-scoped to the
# powertrain bus, while the scan rides the camera-side ACC-CAN
ret.radarUnavailable = docs or candidate not in HONDA_RADAR_SCAN_VERIFIED
# Disable the radar and let openpilot control longitudinal
# WARNING: THIS DISABLES AEB!
# If Bosch radarless, this blocks ACC messages from the camera
ret.alphaLongitudinalAvailable = True
ret.openpilotLongitudinalControl = alpha_long
ret.pcmCruise = not ret.openpilotLongitudinalControl
else:
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.hondaNidec)]
ret.openpilotLongitudinalControl = True
ret.pcmCruise = True
if candidate == CAR.HONDA_CRV_5G:
ret.enableBsm = 0x12f8bfa7 in fingerprint[CAN.radar]
# Detect Bosch cars with new HUD msgs
if any(0x33DA in f for f in fingerprint.values()):
ret.flags |= HondaFlags.BOSCH_EXT_HUD.value
if 0x184 in fingerprint[CAN.pt]:
ret.flags |= HondaFlags.HYBRID.value
if ret.flags & HondaFlags.ALLOW_MANUAL_TRANS and all(msg not in fingerprint[CAN.pt] for msg in (0x191, 0x1A3)):
# Manual transmission support for allowlisted cars only, to prevent silent fall-through on auto-detection failures
ret.transmissionType = TransmissionType.manual
elif 0x191 in fingerprint[CAN.pt] and candidate != CAR.ACURA_RDX:
# Traditional CVTs, gearshift position in GEARBOX_CVT
ret.transmissionType = TransmissionType.cvt
else:
# Traditional autos, direct-drive EVs and eCVTs, gearshift position in GEARBOX_AUTO
ret.transmissionType = TransmissionType.automatic
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0], [0]]
ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]]
ret.lateralTuning.pid.kf = 0.00006 # conservative feed-forward
ret.steerActuatorDelay = 0.1
if candidate in HONDA_BOSCH:
if candidate in HONDA_BOSCH_RADARLESS:
ret.stopAccel = CarControllerParams.BOSCH_ACCEL_MIN # stock uses -4.0 m/s^2 once stopped but limited by safety model
ret.longitudinalActuatorDelay = 0.25 # s
elif candidate in HONDA_BOSCH_CANFD:
ret.longitudinalActuatorDelay = 0.05 # near zero, canfd seems to have stock feedforward correction
else:
ret.longitudinalActuatorDelay = 0.25 # s, per Bosch A log
else:
# default longitudinal tuning for all hondas
ret.longitudinalTuning.kiBP = [0., 5., 35.]
ret.longitudinalTuning.kiV = [1.2, 0.8, 0.5]
# Disable control if EPS mod detected
for fw in car_fw:
if fw.ecu == "eps" and b"," in fw.fwVersion:
ret.dashcamOnly = True
if candidate == CAR.HONDA_CIVIC:
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560], [0, 2560]]
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[1.1], [0.33]]
elif candidate in (CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_BOSCH_DIESEL):
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]]
elif candidate == CAR.HONDA_CIVIC_2022:
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 5120], [0, 5120]] # TODO: determine if there is a dead zone at the top end
ret.lateralTuning.pid.kpBP, ret.lateralTuning.pid.kpV = [[0, 10], [0.05, 0.5]]
ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kiV = [[0, 10], [0.0125, 0.125]]
elif candidate == CAR.HONDA_ACCORD:
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.18]]
if ret.transmissionType == TransmissionType.manual:
CarControllerParams.BOSCH_GAS_LOOKUP_BP = [-0.2, 2.0]
elif candidate == CAR.HONDA_ACCORD_11G:
ret.steerActuatorDelay = 0.22
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 12747], [0, 12747]]
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.18]]
elif candidate == CAR.ACURA_ILX:
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 3840], [0, 3840]] # TODO: determine if there is a dead zone at the top end
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]]
elif candidate in (CAR.HONDA_CRV, CAR.HONDA_CRV_EU):
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 1000], [0, 1000]] # TODO: determine if there is a dead zone at the top end
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]]
ret.wheelSpeedFactor = 1.025
elif candidate == CAR.HONDA_CRV_5G:
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 3840], [0, 3840]]
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.64], [0.192]]
ret.wheelSpeedFactor = 1.025
elif candidate == CAR.HONDA_CRV_HYBRID:
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.18]]
ret.wheelSpeedFactor = 1.025
elif candidate == CAR.HONDA_CRV_6G:
ret.steerActuatorDelay = 0.15
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 5100], [0, 5100]]
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
elif candidate == CAR.HONDA_FIT:
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.05]]
elif candidate == CAR.HONDA_FREED:
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]]
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.05]]
elif candidate in (CAR.HONDA_HRV, CAR.HONDA_HRV_3G):
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]]
if candidate == CAR.HONDA_HRV:
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.16], [0.025]]
ret.wheelSpeedFactor = 1.025
else:
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]] # TODO: can probably use some tuning
elif candidate == CAR.ACURA_RDX:
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 1000], [0, 1000]] # TODO: determine if there is a dead zone at the top end
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]]
elif candidate == CAR.ACURA_RDX_3G:
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4095], [0, 4095]]
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.06]]
elif candidate == CAR.HONDA_ODYSSEY:
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.28], [0.08]]
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
elif candidate == CAR.HONDA_ODYSSEY_TWN:
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.28], [0.08]]
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 32767], [0, 32767]] # TODO: determine if there is a dead zone at the top end
elif candidate in (CAR.HONDA_PILOT, CAR.HONDA_PILOT_4G, CAR.HONDA_PASSPORT_4G, CAR.ACURA_MDX_4G_MMR):
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
ret.lateralTuning.pid.kpBP, ret.lateralTuning.pid.kpV = [[0, 10], [0.05, 0.5]]
ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kiV = [[0, 10], [0.0125, 0.125]]
elif candidate == CAR.HONDA_RIDGELINE:
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.38], [0.11]]
elif candidate in (CAR.HONDA_INSIGHT, CAR.HONDA_NBOX_2G):
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.18]]
elif candidate == CAR.HONDA_E:
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.18]] # TODO: can probably use some tuning
elif candidate == CAR.HONDA_ODYSSEY_5G_MMR:
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 3810], [0, 3810]]
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.06]]
ret.steerActuatorDelay = 0.15
CarControllerParams.BOSCH_GAS_LOOKUP_V = [0, 2000]
if not ret.openpilotLongitudinalControl:
# When using stock ACC, the radar intercepts and filters steering commands the EPS would otherwise accept
ret.minSteerSpeed = 70. * CV.KPH_TO_MS
elif candidate == CAR.ACURA_TLX_2G_MMR:
ret.steerActuatorDelay = 0.15
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]]
ret.lateralTuning.pid.kpBP, ret.lateralTuning.pid.kpV = [[0, 10], [0.05, 0.5]]
ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kiV = [[0, 10], [0.0125, 0.125]]
elif candidate == CAR.HONDA_CLARITY:
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560], [0, 2560]]
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]]
else:
ret.steerActuatorDelay = 0.15
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560], [0, 2560]]
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
if candidate == CAR.HONDA_PILOT_4G:
CarControllerParams.BOSCH_GAS_LOOKUP_V = [0, 2200]
elif candidate == CAR.ACURA_RDX_3G:
CarControllerParams.BOSCH_GAS_LOOKUP_V = [0, 2200]
elif candidate == CAR.HONDA_CRV_6G and ret.flags & HondaFlags.HYBRID:
CarControllerParams.BOSCH_GAS_LOOKUP_BP = [-0.3, 2.0]
# These cars use alternate user brake msg (0x1BE)
if 0x1BE in fingerprint[CAN.pt] and candidate in HONDA_BOSCH:
ret.flags |= HondaFlags.BOSCH_ALT_BRAKE.value
if ret.flags & HondaFlags.BOSCH_ALT_BRAKE:
ret.safetyConfigs[-1].safetyParam |= HondaSafetyFlags.ALT_BRAKE.value
if candidate in HONDA_NIDEC_ALT_SCM_MESSAGES:
ret.safetyConfigs[-1].safetyParam |= HondaSafetyFlags.NIDEC_ALT.value
if ret.openpilotLongitudinalControl and candidate in HONDA_BOSCH:
ret.safetyConfigs[-1].safetyParam |= HondaSafetyFlags.BOSCH_LONG.value
if candidate in HONDA_BOSCH_RADARLESS:
ret.safetyConfigs[-1].safetyParam |= HondaSafetyFlags.RADARLESS.value
if candidate in HONDA_BOSCH_CANFD:
ret.safetyConfigs[-1].safetyParam |= HondaSafetyFlags.BOSCH_CANFD.value
# min speed to enable ACC. if car can do stop and go, then set enabling speed
# to a negative value, so it won't matter. Otherwise, add 0.5 mph margin to not
# conflict with PCM acc
if (ret.transmissionType == TransmissionType.manual) and (not ret.openpilotLongitudinalControl):
ret.autoResumeSng = False
else:
ret.autoResumeSng = candidate in (HONDA_BOSCH | {CAR.HONDA_CIVIC})
if ret.autoResumeSng:
ret.minEnableSpeed = -1.
elif candidate == CAR.HONDA_ODYSSEY_TWN:
ret.minEnableSpeed = 19. * CV.MPH_TO_MS
else:
ret.minEnableSpeed = 25.51 * CV.MPH_TO_MS
ret.steerLimitTimer = 0.8
ret.radarDelay = 0.1
return ret
@staticmethod
def _get_params_iq(stock_cp: structs.CarParams, ret: structs.IQCarParams, candidate, fingerprint: dict[int, dict[int, int]],
car_fw: list[structs.CarParams.CarFw], alpha_long: bool, is_release_iq: bool, docs: bool) -> structs.IQCarParams:
CAN = CanBus(stock_cp, fingerprint)
for fw in car_fw:
if fw.ecu == "eps" and b"," in fw.fwVersion:
ret.flags |= HondaFlagsIQ.EPS_MODIFIED.value
stock_cp.dashcamOnly = False
if bool(stock_cp.flags & HondaFlags.NIDEC) and bool(stock_cp.flags & HondaFlags.HYBRID):
ret.flags |= HondaFlagsIQ.NIDEC_HYBRID.value
ret.iqSafetyFlags |= HondaSafetyFlagsIQ.NIDEC_HYBRID
# some hybrids use a different brake hold
if 0x223 in fingerprint[CAN.pt]:
ret.flags |= HondaFlagsIQ.HYBRID_ALT_BRAKEHOLD.value
if 0x35E in fingerprint[CAN.pt]:
ret.flags |= HondaFlagsIQ.HAS_CAMERA_MESSAGES.value
if candidate == CAR.HONDA_CIVIC:
if ret.flags & HondaFlagsIQ.EPS_MODIFIED:
# stock request input values: 0x0000, 0x00DE, 0x014D, 0x01EF, 0x0290, 0x0377, 0x0454, 0x0610, 0x06EE
# stock request output values: 0x0000, 0x0917, 0x0DC5, 0x1017, 0x119F, 0x140B, 0x1680, 0x1680, 0x1680
# modified request output values: 0x0000, 0x0917, 0x0DC5, 0x1017, 0x119F, 0x140B, 0x1680, 0x2880, 0x3180
# stock filter output values: 0x009F, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108
# modified filter output values: 0x009F, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108, 0x0400, 0x0480
# note: max request allowed is 4096, but request is capped at 3840 in firmware, so modifications result in 2x max
stock_cp.lateralParams.torqueBP, stock_cp.lateralParams.torqueV = [[0, 2560, 8000], [0, 2560, 3840]]
stock_cp.lateralTuning.pid.kpV, stock_cp.lateralTuning.pid.kiV = [[0.3], [0.1]]
elif candidate in (CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_BOSCH_DIESEL):
if ret.flags & HondaFlagsIQ.EPS_MODIFIED:
stock_cp.lateralParams.torqueBP, stock_cp.lateralParams.torqueV = [[0, 2564, 8000], [0, 2564, 3840]]
stock_cp.lateralTuning.pid.kpV, stock_cp.lateralTuning.pid.kiV = [[0.3], [0.09]] # 2.5x Modded EPS
elif candidate == CAR.HONDA_CIVIC_2022:
if ret.flags & HondaFlagsIQ.EPS_MODIFIED:
stock_cp.lateralParams.torqueBP, stock_cp.lateralParams.torqueV = [[0, 2564, 8000], [0, 2564, 3840]]
stock_cp.lateralTuning.pid.kpV, stock_cp.lateralTuning.pid.kiV = [[0.3], [0.09]] # 2.5x Modded EPS
elif candidate == CAR.HONDA_ACCORD:
if ret.flags & HondaFlagsIQ.EPS_MODIFIED:
stock_cp.lateralTuning.pid.kpV, stock_cp.lateralTuning.pid.kiV = [[0.3], [0.09]]
elif candidate == CAR.HONDA_CRV_5G:
if ret.flags & HondaFlagsIQ.EPS_MODIFIED:
# stock request input values: 0x0000, 0x00DB, 0x01BB, 0x0296, 0x0377, 0x0454, 0x0532, 0x0610, 0x067F
# stock request output values: 0x0000, 0x0500, 0x0A15, 0x0E6D, 0x1100, 0x1200, 0x129A, 0x134D, 0x1400
# modified request output values: 0x0000, 0x0500, 0x0A15, 0x0E6D, 0x1100, 0x1200, 0x1ACD, 0x239A, 0x2800
stock_cp.lateralParams.torqueBP, stock_cp.lateralParams.torqueV = [[0, 2560, 10000], [0, 2560, 3840]]
stock_cp.lateralTuning.pid.kpV, stock_cp.lateralTuning.pid.kiV = [[0.21], [0.07]]
elif candidate == CAR.HONDA_CLARITY:
stock_cp.autoResumeSng = True
stock_cp.minEnableSpeed = -1
if ret.flags & HondaFlagsIQ.EPS_MODIFIED:
for fw in car_fw:
if fw.ecu == "eps" and b"-" not in fw.fwVersion and b"," in fw.fwVersion:
stock_cp.lateralTuning.pid.kf = 0.00004
stock_cp.lateralParams.torqueBP, stock_cp.lateralParams.torqueV = [[0, 5760, 15360], [0, 2560, 3840]]
stock_cp.lateralTuning.pid.kpV, stock_cp.lateralTuning.pid.kiV = [[0.1575], [0.05175]]
elif fw.ecu == "eps" and b"-" in fw.fwVersion and b"," in fw.fwVersion:
stock_cp.lateralParams.torqueBP, stock_cp.lateralParams.torqueV = [[0, 5760, 10240], [0, 2560, 3840]]
stock_cp.lateralTuning.pid.kpV, stock_cp.lateralTuning.pid.kiV = [[0.3], [0.1]]
else:
stock_cp.lateralParams.torqueBP, stock_cp.lateralParams.torqueV = [[0, 2560], [0, 2560]]
stock_cp.lateralTuning.pid.kpV, stock_cp.lateralTuning.pid.kiV = [[0.8], [0.24]]
if candidate in HONDA_BOSCH:
pass
else:
ret.enableGasInterceptor = 0x201 in fingerprint[CAN.pt]
stock_cp.pcmCruise = not ret.enableGasInterceptor
if ret.enableGasInterceptor and candidate not in HONDA_BOSCH:
ret.iqSafetyFlags |= HondaSafetyFlagsIQ.GAS_INTERCEPTOR
stock_cp.autoResumeSng = stock_cp.autoResumeSng or ret.enableGasInterceptor
if candidate == CAR.HONDA_CITY_7G:
ret.longitudinalStoppingSpeedOverride = 2.0
ret.stoppingDecelRateOverride = 0.3
else:
ret.longitudinalStoppingSpeedOverride = 0.5
ret.stoppingDecelRateOverride = 0.1
return ret
@staticmethod
def init(CP, CP_IQ, can_recv, can_send, communication_control=None):
if CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS) and CP.openpilotLongitudinalControl:
if communication_control is None and CP.carFingerprint in HONDA_BOSCH_CANFD:
# CAN FD: only clear DTCs here; the radar silencing itself is deferred to CarController until
# the comma relay is confirmed open. init() runs while the panda is still in the ELM327 safety
# mode, and silencing the radar from here raced the safety-mode switch: whenever the switch
# took longer than ~110 ms after radar silence, the brake module latched CRUISE_FAULT for the
# entire drive.
#
# The brake module's radar lost-communication DTC matures over trips (Honda two-trip
# detection): once confirmed from a previous drive, the very next comm-loss detection faults
# ~0.16 s after the radar goes silent. Broadcast-clear stored DTCs on the powertrain and
# camera buses every drive to reset the maturation counter, and clear the radar's own stored
# DTCs so codes accumulated while it was disabled don't re-fault a later drive. Clearing must
# precede the radar silence because a DTC clear can take an ECU several hundred ms.
# NOTE: ELM327 safety mode allows the 29-bit functional diagnostic address on every bus, so
# the broadcast needs no TX allowlist entry in the car safety mode
clear_all_dtcs(can_send, [CanBus(CP).pt, CanBus(CP).camera])
clear_ecu_dtcs(can_recv, can_send, bus=CanBus(CP).pt, addr=0x18DAB0F1)
else:
# 0x80 silences response
if communication_control is None:
communication_control = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, 0x80 | uds.CONTROL_TYPE.DISABLE_RX_DISABLE_TX,
uds.MESSAGE_TYPE.NORMAL_AND_NETWORK_MANAGEMENT])
disable_ecu(can_recv, can_send, bus=CanBus(CP).pt, addr=0x18DAB0F1, com_cont_req=communication_control)
@staticmethod
def deinit(CP, can_recv, can_send):
communication_control = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, 0x80 | uds.CONTROL_TYPE.ENABLE_RX_ENABLE_TX,
uds.MESSAGE_TYPE.NORMAL_AND_NETWORK_MANAGEMENT])
CarInterface.init(CP, None, can_recv, can_send, communication_control)

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env python3
from iqdbc.can import CANParser
from iqdbc.car import Bus, structs
from iqdbc.car.interfaces import RadarInterfaceBase
from iqdbc.car.honda.radar_scan import SCAN_DBC_NAME, HondaRadarScanner
from iqdbc.car.honda.values import DBC
def _create_nidec_can_parser(car_fingerprint):
radar_messages = [0x400] + list(range(0x430, 0x43A)) + list(range(0x440, 0x446))
messages = [(m, 20) for m in radar_messages]
return CANParser(DBC[car_fingerprint][Bus.radar], messages, 1)
class RadarInterface(RadarInterfaceBase):
def __init__(self, CP, CP_IQ):
super().__init__(CP, CP_IQ)
self.track_id = 0
self.radar_fault = False
self.radar_wrong_config = False
self.radar_off_can = CP.radarUnavailable
self.scanner = None
if self.radar_off_can:
self.rcp = None
self.trigger_msg = 0x445
elif DBC[CP.carFingerprint].get(Bus.radar) == SCAN_DBC_NAME:
self.scanner = HondaRadarScanner(CP)
self.rcp = self.scanner.rcp
self.pts = self.scanner.pts
self.trigger_msg = self.scanner.trigger_msg
else:
# Nidec
self.rcp = _create_nidec_can_parser(CP.carFingerprint)
self.trigger_msg = 0x445
self.updated_messages = set()
def update(self, can_strings):
# in Bosch radar and we are only steering for now, so sleep 0.05s to keep
# radard at 20Hz and return no points
if self.radar_off_can:
return super().update(None)
vls = self.rcp.update(can_strings)
self.updated_messages.update(vls)
if self.trigger_msg not in self.updated_messages:
if self.scanner is not None and self.scanner.sweep_overdue():
return self.scanner.quiet_bus_radardata()
return None
rr = self._update(self.updated_messages)
self.updated_messages.clear()
return rr
def _update(self, updated_messages):
if self.scanner is not None:
return self.scanner.process_sweep(updated_messages)
ret = structs.RadarData()
for ii in sorted(updated_messages):
cpt = self.rcp.vl[ii]
if ii == 0x400:
# check for radar faults
self.radar_fault = cpt['RADAR_STATE'] != 0x79
self.radar_wrong_config = cpt['RADAR_STATE'] == 0x69
elif cpt['LONG_DIST'] < 255:
if ii not in self.pts or cpt['NEW_TRACK']:
self.pts[ii] = structs.RadarData.RadarPoint()
self.pts[ii].trackId = self.track_id
self.track_id += 1
self.pts[ii].dRel = cpt['LONG_DIST'] # from front of car
self.pts[ii].yRel = -cpt['LAT_DIST'] # in car frame's y axis, left is positive
self.pts[ii].vRel = cpt['REL_SPEED']
self.pts[ii].aRel = float('nan')
self.pts[ii].yvRel = float('nan')
self.pts[ii].measured = True
else:
if ii in self.pts:
del self.pts[ii]
if not self.rcp.can_valid:
ret.errors.canError = True
if self.radar_fault:
ret.errors.radarFault = True
if self.radar_wrong_config:
ret.errors.wrongConfig = True
ret.points = list(self.pts.values())
return ret

View File

@@ -0,0 +1,396 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
import math
from collections import deque
from dataclasses import dataclass, field
from iqdbc.can import CANParser
from iqdbc.car import Bus, structs
from iqdbc.car.honda.hondacan import CanBus
from iqdbc.car.honda.values import DBC
from iqdbc.dbc.generator.honda.honda_radar_scan import QUARTET_KINDS, SCAN_SLOTS, frame_address
SCAN_DBC_NAME = 'honda_radar_scan_generated'
SWEEP_HZ = 15
SLOT_ADDRS = [tuple(frame_address(slot, kind) for kind in (*QUARTET_KINDS, "MOTION")) for slot in range(SCAN_SLOTS)]
ALL_SCAN_ADDRS = [addr for addrs in SLOT_ADDRS for addr in addrs]
# slot 15's IDENT frame closes every observed sweep's quartet family; its MOTION companion follows
# but must never gate an otherwise valid sweep, so the quartet frame stays the trigger
SWEEP_TRIGGER_ADDR = SLOT_ADDRS[SCAN_SLOTS - 1][3]
DIST_LSB_M = 0.05712
DIST_BIAS_M = -3.0
BEARING_LSB_RAD = 1.0 / 2048.0
BEARING_ZERO = 1024
STATE_INVALID = 0xF
DIST_RAW_INVALID = 0xFFF
BEARING_RAW_INVALID = 0x7FF
AGE_RAW_INVALID = 0xFFF
HANDLE_MIN = 1
HANDLE_MAX = 0x3F
CLOSING_SPEED_RAW_INVALID = 0x7FE
CLOSING_SPEED_RAW_MIN = 0
CLOSING_SPEED_RAW_MAX = 1728
CLOSING_SPEED_RAW_ZERO = 864
CLOSING_SPEED_LSB_MPS = 1.0 / 64.0
# replay-derived quality gate: the native speed field degrades gradually with its sigma companion;
# above this the field is no longer authoritative and the decoder coasts instead
CLOSING_SPEED_SIGMA_TRUST_MAX = 511
DIST_RATIO_RAW_INVALID = 0x3FF
DIST_RATIO_LSB = 0.001
DIST_RATIO_BIAS = 0.5
# replay-derived acceptance gates, not recovered firmware constants; innovation is measured from the
# previous ACCEPTED observation so a reset can never become the baseline for following sweeps
DIST_SIGMA_DEGRADED_RAW = 4
DIST_INNOVATION_SOFT_M = 2.0
DIST_INNOVATION_HARD_M = 5.0
RAW_RATE_LIMIT_MPS = 50.0
HISTORY_LEN = 8
QUIET_TIMEOUT_S = 0.20
def decode_closing_speed(raw, sigma_raw=None):
if raw is None:
return None
raw = int(raw)
if raw == CLOSING_SPEED_RAW_INVALID or not CLOSING_SPEED_RAW_MIN <= raw <= CLOSING_SPEED_RAW_MAX:
return None
if sigma_raw is not None and int(sigma_raw) > CLOSING_SPEED_SIGMA_TRUST_MAX:
return None
return (raw - CLOSING_SPEED_RAW_ZERO) * CLOSING_SPEED_LSB_MPS
def decode_dist_ratio(raw):
if raw is None:
return None
raw = int(raw)
if raw == DIST_RATIO_RAW_INVALID or not 0 <= raw < DIST_RATIO_RAW_INVALID:
return None
return DIST_RATIO_BIAS + DIST_RATIO_LSB * raw
def ratio_implied_rate(raw, dist, dt):
ratio = decode_dist_ratio(raw)
if ratio is None or dt <= 0.0 or not math.isfinite(dist):
return None
return dist * (1.0 - ratio) / dt
def reading_degraded(dist_sigma_raw, presence_raw, speed_sigma_raw):
geometry_bad = dist_sigma_raw >= DIST_SIGMA_DEGRADED_RAW or presence_raw in (0, 0x7F)
motion_bad = speed_sigma_raw is not None and speed_sigma_raw > CLOSING_SPEED_SIGMA_TRUST_MAX
return geometry_bad or motion_bad
@dataclass
class SlotReading:
slot: int
cycle: int
age: int
handle: int
handle_ok: bool
coherent: bool
dist_raw: int = 0
bearing_raw: int = 0
dist_sigma_raw: int = 0
presence_raw: int = 0
speed_raw: int | None = None
speed_sigma_raw: int | None = None
ratio_raw: int | None = None
@dataclass
class ObjectLedger:
handle: int
prev_cycle: int | None = None
prev_age: int | None = None
last_seen_nanos: int | None = None
wire_slot: int | None = None
history: deque = field(default_factory=lambda: deque(maxlen=HISTORY_LEN))
held_speed: float | None = None
held_speed_nanos: int | None = None
def continues_incarnation(self, cycle: int, age: int) -> bool:
if self.prev_cycle is None or self.prev_age is None:
return False
cycle_delta = (cycle - self.prev_cycle) & 0xF
age_delta = (age - self.prev_age) & 0xFFF
return age_delta == 2 * cycle_delta
def restart_incarnation(self):
self.history.clear()
self.held_speed = None
self.held_speed_nanos = None
def held_speed_fresh(self, now: int) -> bool:
return (self.held_speed is not None and self.held_speed_nanos is not None and
(now - self.held_speed_nanos) * 1e-9 <= QUIET_TIMEOUT_S)
def create_scan_parser(CP) -> CANParser:
# the object scan is physically on the camera-side ACC-CAN
return CANParser(DBC[CP.carFingerprint][Bus.radar], [(addr, SWEEP_HZ) for addr in ALL_SCAN_ADDRS], CanBus(CP).camera)
class HondaRadarScanner:
def __init__(self, CP):
self.rcp = create_scan_parser(CP)
self.trigger_msg = SWEEP_TRIGGER_ADDR
self.pts: dict[int, structs.RadarData.RadarPoint] = {}
self._ledgers: dict[int, ObjectLedger] = {}
self._slot_handles: list[int | None] = [None] * SCAN_SLOTS
self._last_sweep_nanos = -1
def sweep_overdue(self) -> bool:
if self._last_sweep_nanos < 0:
return False
return (self.rcp._last_update_nanos - self._last_sweep_nanos) * 1e-9 > QUIET_TIMEOUT_S
def quiet_bus_radardata(self) -> structs.RadarData:
# whole-bus silence: drop everything and emit an EMPTY RadarData (not None) so radard sheds any
# lead within a cycle instead of freezing a phantom
self.pts.clear()
self._ledgers.clear()
self._slot_handles = [None] * SCAN_SLOTS
self._last_sweep_nanos = -1
ret = structs.RadarData()
if not self.rcp.can_valid:
ret.errors.canError = True
ret.errors.radarUnavailableTemporary = True
return ret
def _drop_ledger(self, handle: int):
self._ledgers.pop(handle, None)
self.pts.pop(handle, None)
for slot, bound in enumerate(self._slot_handles):
if bound == handle:
self._slot_handles[slot] = None
def _drop_expired_ledgers(self, now: int):
for handle, ledger in list(self._ledgers.items()):
if ledger.last_seen_nanos is not None and (now - ledger.last_seen_nanos) * 1e-9 > QUIET_TIMEOUT_S:
self._drop_ledger(handle)
def _read_slot(self, slot: int, updated_messages) -> SlotReading | None:
pos, shape, life, ident, motion = SLOT_ADDRS[slot]
if not all(addr in updated_messages for addr in (pos, shape, life, ident)):
# a missing CAN frame is not a lifecycle event; ledgers expire on their own staleness only
return None
v_pos, v_shape, v_life, v_ident = (self.rcp.vl[a] for a in (pos, shape, life, ident))
cycle = int(v_pos['CYCLE'])
if not (cycle == int(v_shape['CYCLE']) == int(v_life['CYCLE']) == int(v_ident['CYCLE'])):
# the quartet doesn't share one radar cycle: not a coherent observation this window
return None
state = int(v_pos['SCAN_STATE'])
dist_raw = int(v_pos['DIST_RAW'])
bearing_raw = int(v_pos['BEARING_RAW'])
age = int(v_life['AGE_RAW'])
handle = int(v_ident['OBJECT_HANDLE'])
reading = SlotReading(
slot=slot,
cycle=cycle,
age=age,
handle=handle,
handle_ok=HANDLE_MIN <= handle <= HANDLE_MAX,
coherent=(state != STATE_INVALID and dist_raw != DIST_RAW_INVALID and
bearing_raw != BEARING_RAW_INVALID and age != AGE_RAW_INVALID),
dist_raw=dist_raw,
bearing_raw=bearing_raw,
dist_sigma_raw=int(v_pos['DIST_SIGMA_RAW']),
presence_raw=int(v_shape['PRESENCE_RAW']),
)
# the MOTION companion only contributes when it rides the same cycle; its absence never
# invalidates the quartet, it only removes independent motion evidence
if motion in updated_messages:
v_motion = self.rcp.vl[motion]
if int(v_motion['CYCLE']) == cycle:
reading.speed_raw = int(v_motion['CLOSING_SPEED_RAW'])
reading.speed_sigma_raw = int(v_motion['CLOSING_SPEED_SIGMA_RAW'])
reading.ratio_raw = int(v_motion['DIST_RATIO_RAW'])
return reading
def _elect_by_handle(self, readings: list[SlotReading]) -> dict[int, SlotReading]:
# one CAN identity can never yield two points; ties prefer the wire slot already bound to the
# ledger, then the lower slot for deterministic handling of a malformed duplicate
elected: dict[int, SlotReading] = {}
for reading in readings:
if not (reading.coherent and reading.handle_ok):
# an invalid observation ends publication for the slot's current occupant without destroying
# persistent state; the object may be multiplexed elsewhere or return before its deadline
hidden = {self._slot_handles[reading.slot]}
if reading.handle_ok:
hidden.add(reading.handle)
for handle in hidden - {None}:
self.pts.pop(handle, None)
continue
current = elected.get(reading.handle)
if current is None:
elected[reading.handle] = reading
continue
bound_slot = self._ledgers[reading.handle].wire_slot if reading.handle in self._ledgers else None
current_rank = (0 if bound_slot == current.slot else 1, current.slot)
candidate_rank = (0 if bound_slot == reading.slot else 1, reading.slot)
if candidate_rank < current_rank:
elected[reading.handle] = reading
return elected
def _bind_slot(self, ledger: ObjectLedger, reading: SlotReading, now: int):
ledger.prev_cycle = reading.cycle
ledger.prev_age = reading.age
ledger.last_seen_nanos = now
ledger.wire_slot = reading.slot
for slot, bound in enumerate(self._slot_handles):
if slot != reading.slot and bound == ledger.handle:
self._slot_handles[slot] = None
self._slot_handles[reading.slot] = ledger.handle
def _coast_point(self, ledger: ObjectLedger, now: int, dist: float, y_rel: float):
# coasting keeps trustworthy geometry visible with the last authoritative motion, unmeasured,
# instead of publishing a synthesized rate; without fresh held motion the point drops
if ledger.held_speed_fresh(now):
point = self.pts.get(ledger.handle)
if point is not None:
point.dRel = dist
point.yRel = y_rel
point.vRel = ledger.held_speed
point.measured = False
return
ledger.held_speed = None
ledger.held_speed_nanos = None
self.pts.pop(ledger.handle, None)
def process_sweep(self, updated_messages) -> structs.RadarData:
ret = structs.RadarData()
if not self.rcp.can_valid:
ret.errors.canError = True
now = self.rcp._last_update_nanos
self._last_sweep_nanos = now
self._drop_expired_ledgers(now)
readings = [r for r in (self._read_slot(slot, updated_messages) for slot in range(SCAN_SLOTS)) if r is not None]
elected = self._elect_by_handle(readings)
elected_handles = set(elected)
for handle, reading in sorted(elected.items(), key=lambda item: item[1].slot):
# a wire-slot replacement ends publication for the old occupant, but not its persistent state
old_handle = self._slot_handles[reading.slot]
if old_handle is not None and old_handle != handle and old_handle not in elected_handles:
self.pts.pop(old_handle, None)
ledger = self._ledgers.get(handle)
if ledger is None:
ledger = ObjectLedger(handle=handle)
self._ledgers[handle] = ledger
if not ledger.continues_incarnation(reading.cycle, reading.age):
# the CAN identity stays the external key, but a lifecycle discontinuity starts a new
# incarnation and must not inherit the previous object's range-rate history
ledger.restart_incarnation()
self.pts.pop(handle, None)
dist = DIST_LSB_M * reading.dist_raw + DIST_BIAS_M
bearing = BEARING_LSB_RAD * (reading.bearing_raw - BEARING_ZERO)
# the radar consumes range as a forward-axis quantity; positive bearing is left of center,
# matching the RadarPoint.yRel sign contract
y_rel = dist * math.tan(bearing)
now_s = now * 1e-9
native_speed = decode_closing_speed(reading.speed_raw, reading.speed_sigma_raw)
unqualified_speed = decode_closing_speed(reading.speed_raw)
# a live native speed rejected only by its sigma companion: geometry acceptance is still decided
# on the same terms as every other sweep (high sigma correlates with bad/discontinuous range),
# and only then does the point coast instead of publishing a synthesized rate
sigma_veto = (native_speed is None and unqualified_speed is not None and
reading.speed_sigma_raw is not None and
reading.speed_sigma_raw > CLOSING_SPEED_SIGMA_TRUST_MAX)
degraded = reading_degraded(reading.dist_sigma_raw, reading.presence_raw, reading.speed_sigma_raw)
previous = ledger.history[-1] if ledger.history else None
ratio_rate = None
dist_rejected = False
if previous is not None:
prev_time, prev_dist = previous
dt = now_s - prev_time
if dt <= 0.0:
dist_rejected = True
else:
ratio = decode_dist_ratio(reading.ratio_raw)
ratio_rate = ratio_implied_rate(reading.ratio_raw, dist, dt)
residuals_m = []
if native_speed is not None:
residuals_m.append(abs(dist - (prev_dist + native_speed * dt)))
if ratio is not None:
residuals_m.append(abs(prev_dist - dist * ratio))
if residuals_m:
innovation_m = min(residuals_m)
dist_rejected = (innovation_m > DIST_INNOVATION_HARD_M or
(degraded and innovation_m > DIST_INNOVATION_SOFT_M))
else:
dist_rejected = abs((dist - prev_dist) / dt) > RAW_RATE_LIMIT_MPS
if dist_rejected:
# keep the last accepted point briefly as an unmeasured coast; rejected geometry is never
# published and never becomes the baseline for a later derivative
accepted_fresh = previous is not None and now_s - previous[0] <= QUIET_TIMEOUT_S
point = self.pts.get(handle)
if accepted_fresh and point is not None:
point.measured = False
else:
self.pts.pop(handle, None)
self._bind_slot(ledger, reading, now)
continue
if sigma_veto:
self._coast_point(ledger, now, dist, y_rel)
self._bind_slot(ledger, reading, now)
continue
# with neither a qualified native speed nor a usable ratio, the only remaining source is the
# raw one-sweep derivative, which must never become an authoritative measurement: coast instead.
# A true birth (no previous accepted sample) cannot mature this cycle regardless, so only
# intercept once a derivative would have something to poison
if native_speed is None and (ratio_rate is None or degraded) and previous is not None:
self._coast_point(ledger, now, dist, y_rel)
self._bind_slot(ledger, reading, now)
continue
ledger.history.append((now_s, dist))
speed = native_speed if native_speed is not None else ratio_rate
ledger.held_speed = speed
ledger.held_speed_nanos = now
# a birth observation has no range rate yet: keep it as history, publish only once a second
# coherent observation of the same identity supplies a finite rate
matured = len(ledger.history) >= 2 and math.isfinite(speed)
if matured:
if handle not in self.pts:
point = structs.RadarData.RadarPoint()
point.trackId = handle
point.aRel = float('nan')
point.yvRel = float('nan')
self.pts[handle] = point
self.pts[handle].dRel = dist
self.pts[handle].yRel = y_rel
self.pts[handle].vRel = speed
self.pts[handle].measured = True
else:
self.pts.pop(handle, None)
self._bind_slot(ledger, reading, now)
ret.points = [self.pts[handle] for handle in sorted(self.pts)]
return ret

View File

@@ -0,0 +1,166 @@
from iqdbc.car import DT_CTRL, gen_empty_fingerprint, structs
from iqdbc.car.honda.interface import CarInterface
from iqdbc.car.honda.values import CAR
CANFD_CAR = CAR.HONDA_CRV_6G
RADAR_DIAG_ADDR = 0x18DAB0F1
ACC_CONTROL_ADDR = 0x1DF
ACC_HUD_ADDR = 0x30C
SCM_BUTTONS_ADDR = 0x296
RADAR_HUD_ADDR = 0x310
LANE_PATH_ADDR = 0x6CD5558
HUD_OBJECTS_ADDR = 0x6CD5559
RADAR_LEAD_ADDR = 0xF31AA5C
RADAR_LEAD2_ADDR = 0xF31AA52
SUPPLEMENTAL_ADDR = 0x1A45AA4E
LOOKALIKE_ADDRS = (RADAR_HUD_ADDR, LANE_PATH_ADDR, HUD_OBJECTS_ADDR, RADAR_LEAD_ADDR, RADAR_LEAD2_ADDR, SUPPLEMENTAL_ADDR)
EXT_DIAG_SESSION = b'\x02\x10\x03\x00\x00\x00\x00\x00'
COMM_CONTROL_DISABLE = b'\x03\x28\x83\x03\x00\x00\x00\x00'
def build_long_interface():
fingerprint = gen_empty_fingerprint()
CP = CarInterface.get_params(CANFD_CAR, fingerprint, [], False, False, False)
CP.openpilotLongitudinalControl = True
CP.pcmCruise = False
CP_IQ = CarInterface.get_params_iq(CP, CANFD_CAR, fingerprint, [], False, False, False)
return CarInterface(CP, CP_IQ)
def make_cc(enabled=True):
CC = structs.CarControl()
CC.enabled = enabled
CC.latActive = enabled
CC.longActive = enabled
return CC.as_reader()
class CanfdControllerHarness:
def __init__(self):
self.ci = build_long_interface()
self.cs = self.ci.CS
self.ci.update([])
self.now_nanos = 0
self.set_radar(alive=True, relay_open=False)
self.set_ticks()
def set_radar(self, alive, relay_open):
self.cs.stock_acc_alive = alive
self.cs.canfd_relay_open = relay_open
def set_ticks(self, hud=False, supp=False, five=False, fifty=False):
self.cs.hud_tick = hud
self.cs.supp_tick = supp
self.cs.radar_5hz_tick = five
self.cs.radar_50hz_tick = fifty
def step(self, CC=None, model=None):
self.now_nanos += int(DT_CTRL * 1e9)
_, can_sends = self.ci.apply(CC or make_cc(), structs.IQCarControl(), self.now_nanos, model)
return can_sends
@staticmethod
def by_addr(can_sends, addr):
return [m for m in can_sends if m[0] == addr]
class TestCanfdDeferredRadarDisable:
def setup_method(self):
self.h = CanfdControllerHarness()
def test_no_disable_requests_before_relay_open(self):
for _ in range(20):
sends = self.h.step()
assert not self.h.by_addr(sends, RADAR_DIAG_ADDR)
assert not self.h.by_addr(sends, ACC_CONTROL_ADDR)
assert not any(self.h.by_addr(sends, a) for a in LOOKALIKE_ADDRS)
def test_disable_handshake_after_relay_open(self):
self.h.set_radar(alive=True, relay_open=True)
payloads = []
for _ in range(101):
for msg in self.h.by_addr(self.h.step(), RADAR_DIAG_ADDR):
payloads.append(msg[1])
assert payloads == [EXT_DIAG_SESSION, COMM_CONTROL_DISABLE, EXT_DIAG_SESSION, COMM_CONTROL_DISABLE, EXT_DIAG_SESSION]
def test_tester_present_keeps_radar_down_once_silent(self):
self.h.set_radar(alive=False, relay_open=True)
payloads = []
for _ in range(60):
payloads += [m[1] for m in self.h.by_addr(self.h.step(), RADAR_DIAG_ADDR)]
assert payloads == [b'\x02\x3E\x80\x00\x00\x00\x00\x00'] * 6
class TestCanfdReplacementStream:
def setup_method(self):
self.h = CanfdControllerHarness()
self.h.set_radar(alive=False, relay_open=True)
def test_acc_control_every_second_frame(self):
seen = [bool(self.h.by_addr(self.h.step(), ACC_CONTROL_ADDR)) for _ in range(10)]
assert sum(seen) == 5
def test_no_acc_control_while_stock_alive(self):
self.h.set_radar(alive=True, relay_open=True)
for _ in range(10):
assert not self.h.by_addr(self.h.step(), ACC_CONTROL_ADDR)
def test_lookalikes_mirrored_byte_identical_on_both_buses(self):
self.h.set_ticks(hud=True, supp=True, five=True, fifty=True)
sends = self.h.step()
for addr in LOOKALIKE_ADDRS:
msgs = self.h.by_addr(sends, addr)
assert len(msgs) == 2, hex(addr)
buses = sorted(m[2] for m in msgs)
assert buses == [0, 2], hex(addr)
assert msgs[0][1] == msgs[1][1], hex(addr)
def test_no_lookalikes_without_ticks(self):
sends = self.h.step()
for addr in (RADAR_HUD_ADDR, RADAR_LEAD_ADDR, RADAR_LEAD2_ADDR, SUPPLEMENTAL_ADDR, LANE_PATH_ADDR, HUD_OBJECTS_ADDR):
assert not self.h.by_addr(sends, addr)
def test_mux_sweep_contiguous_across_banks(self):
self.h.set_ticks(fifty=True)
muxes = []
for _ in range(45):
msgs = self.h.by_addr(self.h.step(), LANE_PATH_ADDR)
muxes.append(msgs[0][1][0] >> 2)
sweep = list(range(1, 11)) + list(range(17, 27)) + list(range(33, 43)) + list(range(49, 59))
assert muxes == (sweep + sweep)[:45]
def test_acc_hud_rides_hud_tick(self):
assert not self.h.by_addr(self.h.step(), ACC_HUD_ADDR)
self.h.set_ticks(hud=True)
assert self.h.by_addr(self.h.step(), ACC_HUD_ADDR)
self.h.set_ticks()
assert not self.h.by_addr(self.h.step(), ACC_HUD_ADDR)
class TestCanfdButtonTakeover:
def setup_method(self):
self.h = CanfdControllerHarness()
self.h.set_radar(alive=False, relay_open=True)
def test_buttons_streamed_to_camera_while_engaged(self):
seen = 0
for _ in range(20):
for msg in self.h.by_addr(self.h.step(), SCM_BUTTONS_ADDR):
assert msg[2] == 2
seen += 1
assert seen == 5
def test_no_button_stream_when_disengaged(self):
for _ in range(20):
assert not self.h.by_addr(self.h.step(make_cc(enabled=False)), SCM_BUTTONS_ADDR)
def test_ambient_light_echoed(self):
self.h.cs.scm_ambient_light = 0x77
for _ in range(4):
msgs = self.h.by_addr(self.h.step(), SCM_BUTTONS_ADDR)
if msgs:
assert msgs[0][1][2] == 0x77
return
raise AssertionError("no SCM_BUTTONS takeover frame seen")

View File

@@ -0,0 +1,202 @@
import pytest
from iqdbc.can import CANPacker
from iqdbc.car import Bus, DT_CTRL, gen_empty_fingerprint
from iqdbc.car.honda.interface import CarInterface
from iqdbc.car.honda.values import CAR, DBC
from iqdbc.car.common.conversions import Conversions as CV
CANFD_CAR = CAR.HONDA_CRV_6G
RADARLESS_CAR = CAR.HONDA_CIVIC_2022
CAMERA_MESSAGES_ADDR = 0x35E
def build_car(candidate, extra_pt_addrs=()):
fingerprint = gen_empty_fingerprint()
for addr in extra_pt_addrs:
fingerprint[0][addr] = 8
CP = CarInterface.get_params(candidate, fingerprint, [], False, False, False)
CP_IQ = CarInterface.get_params_iq(CP, candidate, fingerprint, [], False, False, False)
return CarInterface(CP, CP_IQ)
class CanFeed:
def __init__(self, ci, dbc_name):
self.ci = ci
self.packer = CANPacker(dbc_name)
self.nanos = 0
# the first CarState.update lazily subscribes vl-read messages, so run one empty
# cycle before feeding data or the first fed frame of those messages is dropped
self.step()
self.ci.CS.update(self.ci.can_parsers)
def step(self, msgs=()):
self.nanos += int(DT_CTRL * 1e9)
packed = [self.packer.make_can_msg(name, bus, values) for name, bus, values in msgs]
for parser in self.ci.can_parsers.values():
parser.update([self.nanos, packed])
class TestHondaCanfdRadarState:
def setup_method(self):
self.ci = build_car(CANFD_CAR)
self.cs = self.ci.CS
self.feed = CanFeed(self.ci, DBC[CANFD_CAR][Bus.pt])
def update(self, msgs=()):
self.feed.step(msgs)
return self.cs.update(self.ci.can_parsers)
def test_parsers_include_radar_bus(self):
assert Bus.radar in self.ci.can_parsers
assert self.ci.can_parsers[Bus.radar].bus == 1
def test_50hz_tick_fires_one_frame_before_next_tick(self):
ticks = []
for frame in range(20):
msgs = [("RADAR_50HZ_TICK_REFERENCE", 1, {})] if frame % 2 == 0 else []
self.update(msgs)
ticks.append(self.cs.radar_50hz_tick)
assert ticks[2:] == [frame % 2 == 1 for frame in range(2, 20)]
def test_hud_tick_fires_one_frame_before_next_tick(self):
fired = []
for frame in range(40):
msgs = [("RADAR_HUD_TICK_REFERENCE", 1, {})] if frame % 10 == 0 else []
self.update(msgs)
if self.cs.hud_tick:
fired.append(frame)
assert fired == [9, 19, 29, 39]
def test_5hz_tick_fires_at_stock_radar_lead_offset(self):
fired = []
for frame in range(60):
msgs = [("RADAR_REFERENCE", 0, {})] if frame % 20 == 0 else []
self.update(msgs)
if self.cs.radar_5hz_tick:
fired.append(frame)
assert fired == [11, 31, 51]
def test_stock_acc_alive_until_four_silent_frames(self):
for frame in range(11):
msgs = [("ACC_CONTROL", 0, {})] if frame % 2 == 0 else []
self.update(msgs)
assert self.cs.stock_acc_alive
silent_state = []
for _ in range(6):
self.update()
silent_state.append(self.cs.stock_acc_alive)
assert silent_state == [True, True, True, False, False, False]
self.update([("ACC_CONTROL", 0, {})])
assert self.cs.stock_acc_alive
def test_relay_open_when_camera_steering_disappears(self):
for _ in range(10):
self.update([("STEERING_CONTROL", 0, {})])
assert not self.cs.canfd_relay_open
assert self.cs.camera_steer_seen
open_state = []
for _ in range(7):
self.update()
open_state.append(self.cs.canfd_relay_open)
assert open_state == [False, False, False, False, True, True, True]
def test_relay_open_fallback_without_camera(self):
primed_frames = self.cs.canfd_frames
for frame in range(510):
self.update()
assert self.cs.canfd_relay_open == (primed_frames + frame + 1 >= 500)
def test_ambient_light_echoed_from_scm_buttons(self):
self.update([("SCM_BUTTONS", 0, {"AMBIENT_LIGHT_MAYBE": 0x5A})])
assert self.cs.scm_ambient_light == 0x5A
class TestHondaNonCanfdRadarState:
def test_no_radar_parser_and_ticks_stay_low(self):
ci = build_car(RADARLESS_CAR)
feed = CanFeed(ci, DBC[RADARLESS_CAR][Bus.pt])
assert Bus.radar not in ci.can_parsers
for _ in range(5):
feed.step()
ci.CS.update(ci.can_parsers)
assert not ci.CS.radar_50hz_tick
assert not ci.CS.hud_tick
assert not ci.CS.supp_tick
assert not ci.CS.radar_5hz_tick
class TestCanfdLongInterface:
def test_alpha_long_available_on_canfd(self):
CP = CarInterface.get_params(CANFD_CAR, gen_empty_fingerprint(), [], False, False, False)
assert CP.alphaLongitudinalAvailable
assert not CP.openpilotLongitudinalControl
assert CP.pcmCruise
def test_alpha_long_enabled_on_canfd(self):
CP = CarInterface.get_params(CANFD_CAR, gen_empty_fingerprint(), [], True, False, False)
assert CP.openpilotLongitudinalControl
assert not CP.pcmCruise
assert CP.longitudinalActuatorDelay == pytest.approx(0.05)
def test_canfd_long_init_clears_dtcs_without_disabling_radar(self, mocker):
clear_all = mocker.patch("iqdbc.car.honda.interface.clear_all_dtcs")
clear_ecu = mocker.patch("iqdbc.car.honda.interface.clear_ecu_dtcs")
disable = mocker.patch("iqdbc.car.honda.interface.disable_ecu")
CP = CarInterface.get_params(CANFD_CAR, gen_empty_fingerprint(), [], True, False, False)
CarInterface.init(CP, None, None, None)
assert clear_all.call_count == 1
assert clear_all.call_args.args[1] == [0, 2]
assert clear_ecu.call_count == 1
assert disable.call_count == 0
def test_canfd_deinit_reenables_radar(self, mocker):
clear_all = mocker.patch("iqdbc.car.honda.interface.clear_all_dtcs")
disable = mocker.patch("iqdbc.car.honda.interface.disable_ecu")
CP = CarInterface.get_params(CANFD_CAR, gen_empty_fingerprint(), [], True, False, False)
CarInterface.deinit(CP, None, None)
assert clear_all.call_count == 0
assert disable.call_count == 1
def test_bosch_a_long_init_still_disables_radar(self, mocker):
clear_all = mocker.patch("iqdbc.car.honda.interface.clear_all_dtcs")
disable = mocker.patch("iqdbc.car.honda.interface.disable_ecu")
CP = CarInterface.get_params(CAR.HONDA_ACCORD, gen_empty_fingerprint(), [], True, False, False)
CarInterface.init(CP, None, None, None)
assert clear_all.call_count == 0
assert disable.call_count == 1
class TestHondaDashboardSpeedLimit:
def build(self, candidate, with_camera_messages):
extra = (CAMERA_MESSAGES_ADDR,) if with_camera_messages else ()
return build_car(candidate, extra_pt_addrs=extra)
@pytest.mark.parametrize("sign_value,expected_mph", [(101, 25), (97, 5), (113, 85)])
def test_speed_limit_sign_reported(self, sign_value, expected_mph):
ci = self.build(RADARLESS_CAR, True)
feed = CanFeed(ci, DBC[RADARLESS_CAR][Bus.pt])
feed.step([("CAMERA_MESSAGES", 2, {"SPEED_LIMIT_SIGN": sign_value})])
_, ret_iq = ci.CS.update(ci.can_parsers)
assert ret_iq.speedLimit == pytest.approx(expected_mph * CV.MPH_TO_MS)
@pytest.mark.parametrize("sign_value", [125, 0, 32])
def test_invalid_sign_reports_no_limit(self, sign_value):
ci = self.build(RADARLESS_CAR, True)
feed = CanFeed(ci, DBC[RADARLESS_CAR][Bus.pt])
feed.step([("CAMERA_MESSAGES", 2, {"SPEED_LIMIT_SIGN": sign_value})])
_, ret_iq = ci.CS.update(ci.can_parsers)
assert ret_iq.speedLimit == 0.0
def test_without_camera_messages_flag_no_limit(self):
ci = self.build(RADARLESS_CAR, False)
feed = CanFeed(ci, DBC[RADARLESS_CAR][Bus.pt])
feed.step([("CAMERA_MESSAGES", 2, {"SPEED_LIMIT_SIGN": 101})])
_, ret_iq = ci.CS.update(ci.can_parsers)
assert ret_iq.speedLimit == 0.0

View File

@@ -0,0 +1,235 @@
import math
from types import SimpleNamespace
import numpy as np
from iqdbc.can import CANPacker
from iqdbc.car.honda import dash_lane, dash_objects
V_EGO = 30.0
def model_at(center_y):
x = list(np.linspace(0.0, 110.0, 23))
def line(y):
return SimpleNamespace(x=x, y=[y] * len(x))
return SimpleNamespace(laneLines=[line(center_y + 3.3), line(center_y + 1.65), line(center_y - 1.65), line(center_y - 3.3)],
laneLineProbs=[0.0, 1.0, 1.0, 0.0],
leadsV3=[])
def lane_xy(center_y):
m = model_at(center_y)
return m.laneLines[1].x, [(a + b) / 2.0 for a, b in zip(m.laneLines[1].y, m.laneLines[2].y, strict=True)]
class TestLanePathSlew:
def test_first_fit_shown_unslewed(self):
renderer = dash_lane.LanePathRenderer()
lane = renderer.update(model_at(-2.0), V_EGO, 0.0)
assert lane.offsets == dash_lane.encode_lane_path(*lane_xy(-2.0))
def test_step_is_rate_limited(self):
renderer = dash_lane.LanePathRenderer()
prev = renderer.update(model_at(0.0), V_EGO, 0.0).offsets
assert all(o == 0 for o in prev)
target = dash_lane.encode_lane_path(*lane_xy(-2.0))
max_step = math.ceil(dash_lane.SLEW_MAX_STEP)
for _ in range(10):
cur = renderer.update(model_at(-2.0), V_EGO, 0.0).offsets
for p, c, t in zip(prev, cur, target, strict=True):
assert abs(c - p) <= max_step
assert abs(t - c) <= abs(t - p)
prev = cur
assert prev == target
def test_full_scale_takes_two_seconds(self):
renderer = dash_lane.LanePathRenderer()
renderer.update(model_at(0.0), V_EGO, 0.0)
target = dash_lane.encode_lane_path(*lane_xy(-100.0))
assert all(t == dash_lane.OFFSET_VALID_MAX for t in target)
n_updates = round(dash_lane.SLEW_FULL_SCALE_S * dash_lane.SLEW_RATE_HZ)
for i in range(n_updates):
lane = renderer.update(model_at(-100.0), V_EGO, 0.0)
if i < n_updates - 1:
assert lane.offsets != target
assert lane.offsets == target
def test_blank_resets_slew(self):
renderer = dash_lane.LanePathRenderer()
renderer.update(model_at(0.0), V_EGO, 0.0)
lane = renderer.update(None, V_EGO, 0.0)
assert lane.offsets == [dash_lane.OFFSET_UNAVAILABLE] * dash_lane.POINT_COUNT
lane = renderer.update(model_at(-2.0), V_EGO, 0.0)
assert lane.offsets == dash_lane.encode_lane_path(*lane_xy(-2.0))
def test_short_path_passthrough_and_reset(self):
renderer = dash_lane.LanePathRenderer()
renderer.update(model_at(0.0), V_EGO, 0.0)
short = model_at(-2.0)
for ll in short.laneLines:
ll.x = ll.x[:10]
ll.y = ll.y[:10]
lane = renderer.update(short, V_EGO, 0.0)
assert lane.offsets == [dash_lane.OFFSET_UNAVAILABLE] * dash_lane.POINT_COUNT
lane = renderer.update(model_at(-2.0), V_EGO, 0.0)
assert lane.offsets == dash_lane.encode_lane_path(*lane_xy(-2.0))
class TestLaneLineHysteresis:
def test_single_line_offset_and_hysteresis(self):
renderer = dash_lane.LanePathRenderer()
m = model_at(0.0)
m.laneLineProbs = [0.0, 0.0, 1.0, 0.0]
lane = renderer.update(m, V_EGO, 0.0)
assert not lane.left_line and lane.right_line
assert lane.offsets == dash_lane.encode_lane_path(m.laneLines[2].x, [y - dash_lane.HALF_LANE_M for y in m.laneLines[2].y])
# a left prob between OFF and ON must not switch the left line on
m.laneLineProbs = [0.0, (dash_lane.LINE_PROB_OFF + dash_lane.LINE_PROB_ON) / 2, 1.0, 0.0]
lane = renderer.update(m, V_EGO, 0.0)
assert not lane.left_line
# once on, the same mid prob keeps it on
m.laneLineProbs = [0.0, dash_lane.LINE_PROB_ON, 1.0, 0.0]
assert renderer.update(m, V_EGO, 0.0).left_line
m.laneLineProbs = [0.0, (dash_lane.LINE_PROB_OFF + dash_lane.LINE_PROB_ON) / 2, 1.0, 0.0]
assert renderer.update(m, V_EGO, 0.0).left_line
class TestCanfdReshape:
def test_idle_pattern_when_blank(self):
assert dash_lane.canfd_lane_offsets(dash_lane.RenderedLane()) == dash_lane.CANFD_IDLE_OFFSETS
assert dash_lane.canfd_lane_length(dash_lane.RenderedLane()) == dash_lane.CANFD_MIN_VALID_PTS
def test_terminated_prefix_matches_length_law(self):
for v_ego, expected in ((0.0, 7), (10.0, 15), (19.0, 23), (38.0, 23)):
lane = dash_lane.RenderedLane(offsets=[5] * dash_lane.POINT_COUNT, reach=1.0, v_ego=v_ego)
n = dash_lane.canfd_lane_length(lane)
assert n == expected
offs = dash_lane.canfd_lane_offsets(lane)
assert offs[:n] == [5] * n
assert offs[n:] == [dash_lane.OFFSET_UNAVAILABLE] * (dash_lane.POINT_COUNT - n)
class TestMuxMapping:
def test_mux_cycle_covers_all_banks(self):
assert len(dash_lane.MUX_CYCLE) == 40
assert set(dash_lane.MUX_CYCLE) == set(range(1, 11)) | set(range(17, 27)) | set(range(33, 43)) | set(range(49, 59))
def test_lane_path_frame_selects_offsets_by_mux(self):
packer = CANPacker("honda_bosch_radarless_generated")
offsets = list(range(40))
for mux in dash_lane.MUX_CYCLE:
addr, dat, bus = dash_lane.create_lane_path(packer, 0, offsets, mux)
base = ((mux - 1) % 16) * 4
raw_mux = dat[0] >> 2
assert raw_mux == mux
assert base < 40
class TestDashObjectAuthor:
def make_lead(self, prob=0.9, d=30.0, y=0.0, v=0.0):
status = prob >= dash_objects.LEAD_PROB_ON
return dash_objects.ModelLead(status, d, y, v, prob=prob)
def payload(self, msg):
return msg[1]
def test_inactive_slot_bytes_match_stock_sentinel(self):
packer = CANPacker("honda_common_canfd_generated")
author = dash_objects.DashObjectAuthor()
msg = author.create(packer, 0, self.make_lead(prob=0.0), None, 2, 0.0)
parsed_long = ((self.payload(msg)[4] << 2) | (self.payload(msg)[5] >> 6)) & 0x3FF
assert parsed_long == 1023
def test_lead_rendered_in_slot0_only(self):
packer = CANPacker("honda_common_canfd_generated")
author = dash_objects.DashObjectAuthor()
lead = self.make_lead()
slot0 = author.create(packer, 0, lead, None, 1, 0.0)
slot3 = author.create(packer, 0, lead, None, 4, 0.02)
assert self.payload(slot0)[1] != 0
assert self.payload(slot3)[1] & 0xF8 == 0
def test_lead_prob_hysteresis_and_hold(self):
packer = CANPacker("honda_common_canfd_generated")
author = dash_objects.DashObjectAuthor()
now = 0.0
def object_id(prob):
nonlocal now
now += 0.02
msg = author.create(packer, 0, self.make_lead(prob=prob), None, 1, now)
return self.payload(msg)[1] >> 3
assert object_id(0.6) != 0
# dips below ON but above OFF keep rendering
assert object_id(0.4) != 0
# a full drop is bridged for LEAD_HOLD_S
assert object_id(0.0) != 0
now += dash_objects.LEAD_HOLD_S
assert object_id(0.0) == 0
def test_reid_on_range_discontinuity(self):
ident = dash_objects.LeadIdentity()
now = 0.0
first = ident.update(True, 30.0, 0.0, now)
# stay steady past the re-id refractory window
for _ in range(int(dash_objects.REID_REFRACTORY / 0.02) + 10):
now += 0.02
same = ident.update(True, 30.0, 0.0, now)
assert same == first
now += 0.02
assert ident.update(True, 60.0, 0.0, now) != first
def test_camera_lead_never_forwarded(self):
packer = CANPacker("honda_bosch_radarless_generated")
author = dash_objects.DashObjectAuthor()
tracks = [dash_objects.CameraObject(slot=i, object_id=0, d_rel=0.0, y_rel=0.0, is_lead_car=False, valid=False)
for i in range(dash_objects.NUM_SLOTS)]
tracks[0] = dash_objects.CameraObject(slot=0, object_id=9, d_rel=40.0, y_rel=0.0, is_lead_car=True, valid=True,
car_type=7, rotation=0)
msg = author.create(packer, 0, self.make_lead(prob=0.0), tracks, 1, 0.0)
assert self.payload(msg)[1] >> 3 == 0
def test_adjacent_car_forwarded_with_own_mux(self):
packer = CANPacker("honda_bosch_radarless_generated")
tracks = [dash_objects.CameraObject(slot=i, object_id=0, d_rel=0.0, y_rel=0.0, is_lead_car=False, valid=False)
for i in range(dash_objects.NUM_SLOTS)]
tracks[3] = dash_objects.CameraObject(slot=3, object_id=12, d_rel=25.0, y_rel=3.0, is_lead_car=False, valid=True,
car_type=7, rotation=1)
msg = dash_objects.forward_hud_object(packer, 0, 20, tracks)
assert msg[1][0] >> 2 == 20
assert msg[1][1] >> 3 == 12
class TestCameraObjectTracker:
def test_tracks_persist_across_banks(self):
tracker = dash_objects.CameraObjectTracker()
class FakeParser:
vl_all = {"HUD_OBJECTS": {
"MUX": [2, 18], "OBJECT_ID": [5, 5], "LONG_DIST": [30.0, 31.0], "LAT_DIST": [1.0, 1.1],
"IS_LEAD_CAR": [0, 0], "CAR_TYPE": [7, 7], "ROTATION": [0, 0],
}}
tracker.update(FakeParser())
snap = tracker.snapshot()
assert snap[1].valid and snap[1].object_id == 5
assert snap[1].d_rel == 31.0
def test_empty_sentinel_invalid(self):
tracker = dash_objects.CameraObjectTracker()
class FakeParser:
vl_all = {"HUD_OBJECTS": {
"MUX": [1], "OBJECT_ID": [0], "LONG_DIST": [196.9], "LAT_DIST": [204.7],
"IS_LEAD_CAR": [0], "CAR_TYPE": [-1], "ROTATION": [-128],
}}
tracker.update(FakeParser())
assert not tracker.snapshot()[0].valid

View File

@@ -0,0 +1,18 @@
import re
from iqdbc.car.honda.fingerprints import FW_VERSIONS
from iqdbc.car.honda.values import HONDA_BOSCH, HONDA_BOSCH_TJA_CONTROL
HONDA_FW_VERSION_RE = br"[A-Z0-9]{5}(-|,)[A-Z0-9]{3}(-|,)[A-Z0-9]{4}(\x00){2}$"
class TestHondaFingerprint:
def test_fw_version_format(self):
# Asserts all FW versions follow an expected format
for fw_by_ecu in FW_VERSIONS.values():
for fws in fw_by_ecu.values():
for fw in fws:
assert re.match(HONDA_FW_VERSION_RE, fw) is not None, fw
def test_tja_bosch_only(self):
assert set(HONDA_BOSCH_TJA_CONTROL).issubset(set(HONDA_BOSCH)), "Nidec car found in TJA control list"

View File

@@ -0,0 +1,490 @@
import math
import pytest
from iqdbc.can import CANParser
from iqdbc.car.honda.radar_scan import (AGE_RAW_INVALID, ALL_SCAN_ADDRS, BEARING_RAW_INVALID, BEARING_ZERO,
CLOSING_SPEED_RAW_INVALID, CLOSING_SPEED_RAW_ZERO,
CLOSING_SPEED_SIGMA_TRUST_MAX, DIST_BIAS_M, DIST_LSB_M,
DIST_RATIO_RAW_INVALID, DIST_RAW_INVALID, HondaRadarScanner,
QUIET_TIMEOUT_S, SCAN_DBC_NAME, SCAN_SLOTS, STATE_INVALID,
SWEEP_TRIGGER_ADDR, decode_closing_speed, decode_dist_ratio)
from iqdbc.dbc.generator.honda.honda_radar_scan import FRAME_SIGNALS, frame_address
BUS = 2
SWEEP_DT_NS = 66_000_000
def set_bits(data, start_bit, size, value):
value = int(value) & ((1 << size) - 1)
pos = start_bit
for i in range(size):
bit = (value >> (size - 1 - i)) & 1
byte_i, bit_i = pos // 8, pos % 8
if bit:
data[byte_i] |= (1 << bit_i)
pos = pos - 1 if bit_i > 0 else pos + 15
GEOMETRY = {kind: {name: (start, size) for name, start, size in sigs} for kind, sigs in FRAME_SIGNALS.items()}
def build_frame(slot, kind, **fields):
data = bytearray(8)
for name, value in fields.items():
set_bits(data, *GEOMETRY[kind][name], value)
return (frame_address(slot, kind), bytes(data), BUS)
def quartet(slot, cycle, dist_raw=1000, bearing_raw=BEARING_ZERO, state=1, dist_sigma=0, presence=40,
age=100, handle=5):
return [
build_frame(slot, "POS", SCAN_STATE=state, CYCLE=cycle, DIST_RAW=dist_raw, BEARING_RAW=bearing_raw,
DIST_SIGMA_RAW=dist_sigma),
build_frame(slot, "SHAPE", CYCLE=cycle, PRESENCE_RAW=presence),
build_frame(slot, "LIFE", CYCLE=cycle, AGE_RAW=age),
build_frame(slot, "IDENT", CYCLE=cycle, OBJECT_HANDLE=handle),
]
def motion_frame(slot, cycle, speed_raw=CLOSING_SPEED_RAW_ZERO, sigma_raw=0, ratio_raw=500):
return build_frame(slot, "MOTION", CYCLE=cycle, CLOSING_SPEED_RAW=speed_raw,
CLOSING_SPEED_SIGMA_RAW=sigma_raw, DIST_RATIO_RAW=ratio_raw)
def closing_sweep(slot_msgs, cycle):
# slot 15's quartet closes every sweep so the trigger fires
msgs = list(slot_msgs)
if not any(m[0] == SWEEP_TRIGGER_ADDR for m in msgs):
msgs += quartet(15, cycle, state=STATE_INVALID, dist_raw=DIST_RAW_INVALID,
bearing_raw=BEARING_RAW_INVALID, age=AGE_RAW_INVALID, handle=0)
return msgs
class ScanHarness:
def __init__(self):
self.scanner = object.__new__(HondaRadarScanner)
self.scanner.rcp = CANParser(SCAN_DBC_NAME, [(a, 15) for a in ALL_SCAN_ADDRS], BUS)
self.scanner.trigger_msg = SWEEP_TRIGGER_ADDR
self.scanner.pts = {}
self.scanner._ledgers = {}
self.scanner._slot_handles = [None] * SCAN_SLOTS
self.scanner._last_sweep_nanos = -1
self.updated = set()
self.nanos = 0
self.cycle = 0
def feed(self, msgs, dt_ns=SWEEP_DT_NS):
self.nanos += dt_ns
vls = self.scanner.rcp.update([self.nanos, list(msgs)])
self.updated.update(vls)
if self.scanner.trigger_msg not in self.updated:
if self.scanner.sweep_overdue():
return self.scanner.quiet_bus_radardata()
return None
result = self.scanner.process_sweep(self.updated)
self.updated.clear()
return result
def sweep(self, slot_msgs=(), cycle_step=1, dt_ns=SWEEP_DT_NS):
self.cycle = (self.cycle + cycle_step) & 0xF
return self.feed(closing_sweep(slot_msgs, self.cycle), dt_ns=dt_ns)
def object_sweep(self, slot=0, handle=5, dist_raw=1000, with_motion=True, cycle_step=1, age_step=None,
dt_ns=SWEEP_DT_NS, **kwargs):
if age_step is None:
age_step = 2 * cycle_step
self._age = (getattr(self, "_age", 100) + age_step) & 0xFFF
cycle = (self.cycle + cycle_step) & 0xF
msgs = quartet(slot, cycle, dist_raw=dist_raw, age=self._age, handle=handle, **kwargs)
if with_motion:
msgs.append(motion_frame(slot, cycle))
return self.sweep(msgs, cycle_step=cycle_step, dt_ns=dt_ns)
class TestFieldDecoding:
def test_dist_conversion(self):
assert DIST_LSB_M * 1000 + DIST_BIAS_M == pytest.approx(54.12)
def test_closing_speed_decode_and_domain(self):
assert decode_closing_speed(CLOSING_SPEED_RAW_ZERO) == 0.0
assert decode_closing_speed(CLOSING_SPEED_RAW_ZERO + 64) == 1.0
assert decode_closing_speed(CLOSING_SPEED_RAW_INVALID) is None
assert decode_closing_speed(1729) is None
assert decode_closing_speed(None) is None
def test_closing_speed_sigma_veto(self):
assert decode_closing_speed(CLOSING_SPEED_RAW_ZERO, CLOSING_SPEED_SIGMA_TRUST_MAX) == 0.0
assert decode_closing_speed(CLOSING_SPEED_RAW_ZERO, CLOSING_SPEED_SIGMA_TRUST_MAX + 1) is None
def test_dist_ratio_decode(self):
assert decode_dist_ratio(500) == pytest.approx(1.0)
assert decode_dist_ratio(DIST_RATIO_RAW_INVALID) is None
assert decode_dist_ratio(None) is None
def test_bearing_sign_convention(self):
h = ScanHarness()
h.object_sweep(bearing_raw=BEARING_ZERO + 100)
result = h.object_sweep(bearing_raw=BEARING_ZERO + 100)
assert result.points[0].yRel > 0 # left of center is positive
dist = result.points[0].dRel
assert result.points[0].yRel == pytest.approx(dist * math.tan(100 / 2048))
def test_bearing_right_of_center_is_negative(self):
h = ScanHarness()
h.object_sweep(bearing_raw=BEARING_ZERO - 100)
result = h.object_sweep(bearing_raw=BEARING_ZERO - 100)
assert result.points[0].yRel < 0
def test_boresight_is_zero(self):
h = ScanHarness()
h.object_sweep(bearing_raw=BEARING_ZERO)
result = h.object_sweep(bearing_raw=BEARING_ZERO)
assert result.points[0].yRel == 0.0
class TestPublicationRules:
def test_birth_is_withheld_until_second_observation(self):
h = ScanHarness()
result = h.object_sweep()
assert len(result.points) == 0
result = h.object_sweep()
assert len(result.points) == 1
point = result.points[0]
assert point.trackId == 5
assert point.measured
assert math.isnan(point.aRel) and math.isnan(point.yvRel)
def test_handle_is_wire_identity_not_synthetic(self):
h = ScanHarness()
h.object_sweep(handle=0x22)
result = h.object_sweep(handle=0x22)
assert result.points[0].trackId == 0x22
@pytest.mark.parametrize("field,value", [("state", STATE_INVALID), ("dist_raw", DIST_RAW_INVALID),
("bearing_raw", BEARING_RAW_INVALID)])
def test_sentinels_invalidate_observation(self, field, value):
h = ScanHarness()
h.object_sweep()
h.object_sweep()
kwargs = {field: value}
result = h.object_sweep(**kwargs)
assert len(result.points) == 0
def test_age_sentinel_invalidates_observation(self):
h = ScanHarness()
h.object_sweep()
h.object_sweep()
cycle = (h.cycle + 1) & 0xF
msgs = quartet(0, cycle, age=AGE_RAW_INVALID, handle=5) + [motion_frame(0, cycle)]
result = h.sweep(msgs)
assert len(result.points) == 0
@pytest.mark.parametrize("handle", [0, 0x40, 0xFF])
def test_out_of_range_handle_invalidates(self, handle):
h = ScanHarness()
h.object_sweep()
h.object_sweep()
result = h.object_sweep(handle=handle)
assert len(result.points) == 0
def test_incomplete_quartet_is_not_an_observation(self):
h = ScanHarness()
h.object_sweep()
h.object_sweep()
cycle = (h.cycle + 1) & 0xF
h._age = (h._age + 2) & 0xFFF
msgs = quartet(0, cycle, age=h._age, handle=5)[:3] # drop IDENT
result = h.sweep(msgs)
# a dropped CAN frame is not a lifecycle event: the published point persists untouched
assert len(result.points) == 1
result = h.object_sweep()
assert len(result.points) == 1
assert result.points[0].measured
def test_cycle_mismatch_across_quartet_is_incoherent(self):
h = ScanHarness()
h.object_sweep()
h.object_sweep()
cycle = (h.cycle + 1) & 0xF
msgs = quartet(0, cycle, age=200, handle=5)
bad_life = build_frame(0, "LIFE", CYCLE=(cycle + 1) & 0xF, AGE_RAW=200)
msgs[2] = bad_life
result = h.sweep(msgs)
# an incoherent quartet is not an observation: the published point persists untouched
assert len(result.points) == 1
assert result.points[0].measured
class TestLifecycle:
def test_age_advances_two_per_cycle_keeps_identity(self):
h = ScanHarness()
h.object_sweep()
h.object_sweep()
result = h.object_sweep()
assert len(result.points) == 1
def test_continuity_across_skipped_cycles(self):
h = ScanHarness()
h.object_sweep()
h.object_sweep()
result = h.object_sweep(cycle_step=3, age_step=6)
assert len(result.points) == 1
def test_cycle_and_age_wraparound_stay_same_incarnation(self):
h = ScanHarness()
h.cycle = 14
h._age = 4094
h.object_sweep() # cycle 15, age 4094+2 wraps
h.object_sweep() # cycle 0
result = h.object_sweep()
assert len(result.points) == 1
def test_lifecycle_break_starts_new_incarnation(self):
h = ScanHarness()
h.object_sweep()
h.object_sweep()
# same handle, age jumps arbitrarily: history must not carry over, so no publication this sweep
result = h.object_sweep(age_step=500)
assert len(result.points) == 0
result = h.object_sweep()
assert len(result.points) == 1
def test_death_then_rebirth_reuses_handle_with_clean_history(self):
h = ScanHarness()
h.object_sweep()
h.object_sweep()
for _ in range(4):
h.sweep() # object absent long enough to expire its ledger
result = h.object_sweep()
assert len(result.points) == 0
result = h.object_sweep()
assert len(result.points) == 1
class TestMotionPolicy:
def test_native_speed_is_published(self):
h = ScanHarness()
speed_raw = CLOSING_SPEED_RAW_ZERO + 128
cycle = (h.cycle + 1) & 0xF
h.sweep(quartet(0, cycle, age=100, handle=5) + [motion_frame(0, cycle, speed_raw=speed_raw)])
cycle = (h.cycle + 1) & 0xF
result = h.sweep(quartet(0, cycle, age=102, handle=5) + [motion_frame(0, cycle, speed_raw=speed_raw)])
assert result.points[0].vRel == pytest.approx(2.0)
assert result.points[0].measured
def test_missing_motion_frame_never_invalidates_geometry(self):
h = ScanHarness()
h.object_sweep(with_motion=False)
result = h.object_sweep(with_motion=False)
# without any motion source and no held speed, the point is withheld rather than synthesized
assert len(result.points) == 0
def test_stale_motion_cycle_is_ignored(self):
h = ScanHarness()
h.object_sweep()
h.object_sweep()
cycle = (h.cycle + 1) & 0xF
h._age = (h._age + 2) & 0xFFF
msgs = quartet(0, cycle, age=h._age, handle=5) + [motion_frame(0, (cycle - 1) & 0xF)]
result = h.sweep(msgs)
# motion from another cycle contributes nothing: coasts on held speed, unmeasured
assert len(result.points) == 1
assert not result.points[0].measured
def test_high_sigma_speed_coasts_instead_of_synthesizing(self):
h = ScanHarness()
h.object_sweep()
h.object_sweep()
cycle = (h.cycle + 1) & 0xF
h._age = (h._age + 2) & 0xFFF
msgs = quartet(0, cycle, age=h._age, handle=5) + \
[motion_frame(0, cycle, sigma_raw=CLOSING_SPEED_SIGMA_TRUST_MAX + 1)]
result = h.sweep(msgs)
assert len(result.points) == 1
assert not result.points[0].measured
assert result.points[0].vRel == pytest.approx(0.0) # the held speed, not a derivative
def test_ratio_field_supplies_speed_when_native_missing(self):
h = ScanHarness()
dist_raw = 1000
cycle = (h.cycle + 1) & 0xF
h.sweep(quartet(0, cycle, dist_raw=dist_raw, age=100, handle=5) +
[motion_frame(0, cycle, speed_raw=CLOSING_SPEED_RAW_INVALID, ratio_raw=490)])
cycle = (h.cycle + 1) & 0xF
result = h.sweep(quartet(0, cycle, dist_raw=dist_raw, age=102, handle=5) +
[motion_frame(0, cycle, speed_raw=CLOSING_SPEED_RAW_INVALID, ratio_raw=490)])
assert len(result.points) == 1
dist = DIST_LSB_M * dist_raw + DIST_BIAS_M
dt = SWEEP_DT_NS * 1e-9
assert result.points[0].vRel == pytest.approx(dist * (1.0 - 0.99) / dt)
assert result.points[0].measured
def test_fast_clean_range_rate_without_sources_is_withheld(self):
h = ScanHarness()
h.object_sweep(with_motion=False, dist_raw=1000)
# large clean jump with no motion evidence: raw-rate limit rejects the range outright
result = h.object_sweep(with_motion=False, dist_raw=3000)
assert len(result.points) == 0
class TestRangeAcceptance:
def test_discontinuity_is_rejected_and_never_becomes_baseline(self):
h = ScanHarness()
h.object_sweep(dist_raw=1000)
h.object_sweep(dist_raw=1002)
# jump far beyond the hard innovation gate while claiming zero closing speed
result = h.object_sweep(dist_raw=3000)
assert len(result.points) == 1
assert not result.points[0].measured
# the rejected range did not become the derivative baseline: returning to the
# consistent range publishes measured again
result = h.object_sweep(dist_raw=1004)
assert result.points[0].measured
def test_small_innovation_accepted(self):
h = ScanHarness()
h.object_sweep(dist_raw=1000)
result = h.object_sweep(dist_raw=1005)
assert result.points[0].measured
class TestSlotsAndIdentity:
def test_slot_migration_preserves_identity(self):
h = ScanHarness()
h.object_sweep(slot=2)
h.object_sweep(slot=2)
result = h.object_sweep(slot=9)
assert len(result.points) == 1
assert result.points[0].trackId == 5
def test_duplicate_identity_prefers_bound_slot(self):
h = ScanHarness()
h.object_sweep(slot=2, dist_raw=1000)
h.object_sweep(slot=2, dist_raw=1002)
cycle = (h.cycle + 1) & 0xF
h._age = (h._age + 2) & 0xFFF
msgs = quartet(2, cycle, dist_raw=1004, age=h._age, handle=5) + [motion_frame(2, cycle)] + \
quartet(9, cycle, dist_raw=2000, age=h._age, handle=5) + [motion_frame(9, cycle)]
result = h.sweep(msgs)
assert len(result.points) == 1
assert result.points[0].dRel == pytest.approx(DIST_LSB_M * 1004 + DIST_BIAS_M)
def test_slot_replacement_hides_old_occupant(self):
h = ScanHarness()
h.object_sweep(slot=3, handle=7)
h.object_sweep(slot=3, handle=7)
# a different identity takes the slot; the old one is hidden but not destroyed
result = h.object_sweep(slot=3, handle=9, age_step=333)
assert all(p.trackId != 7 for p in result.points)
def test_one_identity_never_two_points(self):
h = ScanHarness()
cycle = (h.cycle + 1) & 0xF
msgs = quartet(1, cycle, age=100, handle=5) + [motion_frame(1, cycle)] + \
quartet(6, cycle, age=100, handle=5) + [motion_frame(6, cycle)]
h.sweep(msgs)
cycle = (h.cycle + 1) & 0xF
msgs = quartet(1, cycle, age=102, handle=5) + [motion_frame(1, cycle)] + \
quartet(6, cycle, age=102, handle=5) + [motion_frame(6, cycle)]
result = h.sweep(msgs)
assert len(result.points) == 1
class TestBusSilence:
def test_quiet_bus_publishes_empty_not_none(self):
h = ScanHarness()
h.object_sweep()
h.object_sweep()
result = None
for _ in range(30):
result = h.feed([], dt_ns=10_000_000)
if result is not None:
break
assert result is not None
assert result.errors.radarUnavailableTemporary
assert len(result.points) == 0
def test_recovery_after_silence_starts_fresh(self):
h = ScanHarness()
h.object_sweep()
h.object_sweep()
for _ in range(30):
if h.feed([], dt_ns=10_000_000) is not None:
break
result = h.object_sweep()
assert len(result.points) == 0
result = h.object_sweep()
assert len(result.points) == 1
def test_no_stale_publication_before_first_sweep(self):
h = ScanHarness()
for _ in range(50):
assert h.feed([], dt_ns=10_000_000) is None
class TestQuietTimeoutValue:
def test_timeout_is_about_three_sweeps(self):
assert QUIET_TIMEOUT_S == pytest.approx(3 / 15, abs=0.01)
class TestScanInterfaceGating:
def build(self, candidate, alpha_long=False, docs=False):
from iqdbc.car import gen_empty_fingerprint
from iqdbc.car.honda.interface import CarInterface
CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], alpha_long, False, docs)
return CP
def test_verified_platform_has_radar(self):
from iqdbc.car.honda.values import CAR
for car in (CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_ACCORD, CAR.HONDA_CRV_5G):
assert not self.build(car).radarUnavailable
def test_radar_survives_openpilot_longitudinal(self):
from iqdbc.car.honda.values import CAR
CP = self.build(CAR.HONDA_CIVIC_BOSCH, alpha_long=True)
assert CP.openpilotLongitudinalControl
assert not CP.radarUnavailable
def test_unverified_family_platform_stays_off(self):
from iqdbc.car.honda.values import CAR
for car in (CAR.HONDA_E, CAR.HONDA_INSIGHT, CAR.HONDA_NBOX_2G, CAR.ACURA_RDX_3G, CAR.HONDA_CRV_HYBRID):
assert self.build(car).radarUnavailable
def test_radarless_and_canfd_stay_off(self):
from iqdbc.car.honda.values import CAR
assert self.build(CAR.HONDA_CIVIC_2022).radarUnavailable
assert self.build(CAR.HONDA_CRV_6G).radarUnavailable
def test_docs_never_claim_radar(self):
from iqdbc.car.honda.values import CAR
assert self.build(CAR.HONDA_CIVIC_BOSCH, docs=True).radarUnavailable
def test_radar_interface_routes_scanner(self):
from iqdbc.car import gen_empty_fingerprint
from iqdbc.car.honda.interface import CarInterface
from iqdbc.car.honda.values import CAR
CP = self.build(CAR.HONDA_CIVIC_BOSCH)
CP_IQ = CarInterface.get_params_iq(CP, CAR.HONDA_CIVIC_BOSCH, gen_empty_fingerprint(), [], False, False, False)
ri = CarInterface.RadarInterface(CP, CP_IQ)
assert ri.scanner is not None
assert ri.trigger_msg == SWEEP_TRIGGER_ADDR
def test_radar_interface_keeps_nidec_path(self):
from iqdbc.car import gen_empty_fingerprint
from iqdbc.car.honda.interface import CarInterface
from iqdbc.car.honda.values import CAR
CP = self.build(CAR.HONDA_CIVIC)
CP_IQ = CarInterface.get_params_iq(CP, CAR.HONDA_CIVIC, gen_empty_fingerprint(), [], False, False, False)
ri = CarInterface.RadarInterface(CP, CP_IQ)
assert ri.scanner is None
assert ri.trigger_msg == 0x445
def test_radar_interface_sleeps_when_unavailable(self):
from iqdbc.car import gen_empty_fingerprint
from iqdbc.car.honda.interface import CarInterface
from iqdbc.car.honda.values import CAR
CP = self.build(CAR.HONDA_E)
CP_IQ = CarInterface.get_params_iq(CP, CAR.HONDA_E, gen_empty_fingerprint(), [], False, False, False)
ri = CarInterface.RadarInterface(CP, CP_IQ)
assert ri.scanner is None and ri.rcp is None

View File

@@ -0,0 +1,84 @@
from iqdbc.can.dbc import DBC as DbcFile
from iqdbc.car import Bus
from iqdbc.car.honda.values import CAR, DBC, HONDA_RADAR_SCAN_CAPABLE, HONDA_RADAR_SCAN_VERIFIED
from iqdbc.dbc.generator.honda.honda_radar_scan import (FRAME_SIGNALS, QUARTET_KINDS, SCAN_SLOTS,
frame_address, motion_address, quartet_base_address)
SCAN_DBC_NAME = 'honda_radar_scan_generated'
class TestScanAddressing:
def test_quartet_bases(self):
assert [quartet_base_address(s) for s in range(SCAN_SLOTS)] == \
[0x280, 0x284, 0x288, 0x28C, 0x2D0, 0x2D4, 0x2D8, 0x2DC, 0x2E0, 0x2E4, 0x2E8, 0x2EC, 0x2F0, 0x2F4, 0x2F8, 0x2FC]
def test_motion_addresses(self):
assert [motion_address(s) for s in range(SCAN_SLOTS)] == \
[0x2C8, 0x2C9, 0x2CA, 0x2CB, 0x2CC, 0x2CD, 0x2CE, 0x2CF, 0x290, 0x291, 0x292, 0x293, 0x294, 0x295, 0x296, 0x297]
def test_eighty_unique_addresses(self):
addrs = [frame_address(s, k) for s in range(SCAN_SLOTS) for k in (*QUARTET_KINDS, "MOTION")]
assert len(addrs) == 80
assert len(set(addrs)) == 80
def test_quartet_kind_order(self):
for slot in range(SCAN_SLOTS):
base = quartet_base_address(slot)
assert [frame_address(slot, k) for k in QUARTET_KINDS] == [base, base + 1, base + 2, base + 3]
class TestScanDbcGeometry:
def setup_method(self):
self.dbc = DbcFile(SCAN_DBC_NAME)
def geometry(self, addr):
msg = self.dbc.addr_to_msg[addr]
return {sig.name: (sig.start_bit, sig.size) for sig in msg.sigs.values()}
def test_every_frame_present_with_size_8(self):
for slot in range(SCAN_SLOTS):
for kind in (*QUARTET_KINDS, "MOTION"):
msg = self.dbc.addr_to_msg[frame_address(slot, kind)]
assert msg.name == f"RADAR_SCAN_{slot:02d}_{kind}"
assert msg.size == 8
def test_bit_geometry_matches_spec(self):
expected = {kind: {name: (start, size) for name, start, size in sigs} for kind, sigs in FRAME_SIGNALS.items()}
for slot in range(SCAN_SLOTS):
for kind in (*QUARTET_KINDS, "MOTION"):
assert self.geometry(frame_address(slot, kind)) == expected[kind], (slot, kind)
def test_pos_frame_field_widths(self):
geo = self.geometry(frame_address(0, "POS"))
assert geo["DIST_RAW"] == (23, 12)
assert geo["BEARING_RAW"] == (39, 11)
assert geo["SCAN_STATE"] == (15, 4)
assert geo["DIST_SIGMA_RAW"] == (7, 7)
def test_ident_handle_is_byte_six(self):
geo = self.geometry(frame_address(0, "IDENT"))
assert geo["OBJECT_HANDLE"] == (55, 8)
def test_motion_field_widths(self):
geo = self.geometry(frame_address(0, "MOTION"))
assert geo["CLOSING_SPEED_RAW"] == (7, 11)
assert geo["CLOSING_SPEED_SIGMA_RAW"] == (23, 10)
assert geo["DIST_RATIO_RAW"] == (55, 10)
def test_cycle_positions_per_kind(self):
positions = {"POS": (27, 4), "SHAPE": (28, 4), "LIFE": (11, 4), "IDENT": (12, 4), "MOTION": (12, 4)}
for kind, expected in positions.items():
assert self.geometry(frame_address(3, kind))["CYCLE"] == expected
class TestScanPlatformWiring:
def test_scan_dbc_on_exactly_the_capable_family(self):
for car in CAR:
has_scan_dbc = DBC[car].get(Bus.radar) == SCAN_DBC_NAME
assert has_scan_dbc == (car in HONDA_RADAR_SCAN_CAPABLE), car
def test_verified_platforms_are_capable(self):
assert HONDA_RADAR_SCAN_VERIFIED <= HONDA_RADAR_SCAN_CAPABLE
def test_verified_set(self):
assert HONDA_RADAR_SCAN_VERIFIED == {CAR.HONDA_ACCORD, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CRV_5G}

View File

@@ -0,0 +1,468 @@
from dataclasses import dataclass, field
from enum import Enum, IntFlag
from iqdbc.car import Bus, CarSpecs, DbcDict, PlatformConfig, Platforms, structs, uds
from iqdbc.car.common.conversions import Conversions as CV
from iqdbc.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column, SupportType
from iqdbc.car.fw_query_definitions import FwQueryConfig, Request, StdQueries, p16
Ecu = structs.CarParams.Ecu
VisualAlert = structs.CarControl.HUDControl.VisualAlert
GearShifter = structs.CarState.GearShifter
class CarControllerParams:
# Allow small margin below -3.5 m/s^2 from ISO 15622:2018 since we
# perform the closed loop control, and might need some
# to apply some more braking if we're on a downhill slope.
# Our controller should still keep the 2 second average above
# -3.5 m/s^2 as per planner limits
NIDEC_ACCEL_MIN = -4.0 # m/s^2
NIDEC_ACCEL_MAX = 1.6 # m/s^2, lower than 2.0 m/s^2 for tuning reasons
NIDEC_ACCEL_LOOKUP_BP = [-1., 0., .6]
NIDEC_ACCEL_LOOKUP_V = [-4.8, 0., 2.0]
NIDEC_MAX_ACCEL_V = [0.5, 2.4, 1.4, 0.6]
NIDEC_MAX_ACCEL_BP = [0.0, 4.0, 10., 20.]
NIDEC_GAS_MAX = 198 # 0xc6
NIDEC_BRAKE_MAX = 1024 // 4
BOSCH_ACCEL_MIN = -3.5 # m/s^2
BOSCH_ACCEL_MAX = 2.0 # m/s^2
BOSCH_GAS_LOOKUP_BP = [0.0, 2.0] # 2m/s^2
BOSCH_GAS_LOOKUP_V = [0, 1600]
STEER_STEP = 1 # 100 Hz
STEER_DELTA_UP = 3 # min/max in 0.33s for all Honda
STEER_DELTA_DOWN = 3
STEER_GLOBAL_MIN_SPEED = 3 * CV.MPH_TO_MS
def __init__(self, CP):
self.STEER_MAX = CP.lateralParams.torqueBP[-1]
# mirror of list (assuming first item is zero) for interp of signed request
# values and verify that both arrays begin at zero
assert CP.lateralParams.torqueBP[0] == 0
assert CP.lateralParams.torqueV[0] == 0
self.STEER_LOOKUP_BP = [v * -1 for v in CP.lateralParams.torqueBP][1:][::-1] + list(CP.lateralParams.torqueBP)
self.STEER_LOOKUP_V = [v * -1 for v in CP.lateralParams.torqueV][1:][::-1] + list(CP.lateralParams.torqueV)
class HondaSafetyFlags(IntFlag):
ALT_BRAKE = 1
BOSCH_LONG = 2
NIDEC_ALT = 4
RADARLESS = 8
BOSCH_CANFD = 16
class HondaFlags(IntFlag):
# Detected flags
# Bosch models with alternate set of LKAS_HUD messages
BOSCH_EXT_HUD = 1
BOSCH_ALT_BRAKE = 2
# Static flags
BOSCH = 4
BOSCH_RADARLESS = 8
NIDEC = 16
NIDEC_ALT_PCM_ACCEL = 32
NIDEC_ALT_SCM_MESSAGES = 64
BOSCH_CANFD = 128
HAS_ALL_DOOR_STATES = 256 # Some Hondas have all door states, others only driver door
BOSCH_ALT_RADAR = 512
ALLOW_MANUAL_TRANS = 1024
HYBRID = 2048
BOSCH_TJA_CONTROL = 4096
# Car button codes
class CruiseButtons:
RES_ACCEL = 4
DECEL_SET = 3
CANCEL = 2
MAIN = 1
class CruiseSettings:
DISTANCE = 3
LKAS = 1
@dataclass
class HondaCarDocs(CarDocs):
package: str = "Honda Sensing"
def init_make(self, CP: structs.CarParams):
if CP.flags & HondaFlags.BOSCH:
if CP.flags & HondaFlags.BOSCH_CANFD:
harness = CarHarness.bosch_c
elif CP.flags & HondaFlags.BOSCH_RADARLESS:
harness = CarHarness.bosch_b
else:
harness = CarHarness.bosch_a
else:
harness = CarHarness.nidec
self.car_parts = CarParts.common([harness])
if CP.carFingerprint in (CAR.HONDA_CLARITY,):
self.car_parts = CarParts.common([CarHarness.honda_clarity])
self.car_parts.custom_parts_url = "https://shop.retropilot.org/product/honda-clarity-proxy-board-kit"
self.support_type: SupportType = SupportType.COMMUNITY
self.support_link: str = "community"
class Footnote(Enum):
CIVIC_DIESEL = CarFootnote(
"2019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph.",
Column.FSR_STEERING)
@dataclass
class HondaBoschPlatformConfig(PlatformConfig):
def init(self):
self.flags |= HondaFlags.BOSCH
@dataclass
class HondaBoschCANFDPlatformConfig(HondaBoschPlatformConfig):
dbc_dict: DbcDict = field(default_factory=lambda: {Bus.pt: 'honda_common_canfd_generated', Bus.radar: 'honda_common_canfd_generated'})
def init(self):
super().init()
self.flags |= HondaFlags.BOSCH_CANFD
@dataclass
class HondaNidecPlatformConfig(PlatformConfig):
def init(self):
self.flags |= HondaFlags.NIDEC
def radar_dbc_dict(pt_dict):
return {Bus.pt: pt_dict, Bus.radar: 'acura_ilx_2016_nidec'}
# Certain Hondas have an extra steering sensor at the bottom of the steering rack,
# which improves controls quality as it removes the steering column torsion from feedback.
# Tire stiffness factor fictitiously lower if it includes the steering column torsion effect.
# For modeling details, see p.198-200 in "The Science of Vehicle Dynamics (2014), M. Guiggiani"
class CAR(Platforms):
# Bosch Cars
HONDA_NBOX_2G = HondaBoschPlatformConfig(
[
HondaCarDocs("Honda N-Box 2018", "All", min_steer_speed=5.),
],
CarSpecs(mass=890., wheelbase=2.520, steerRatio=18.64),
{Bus.pt: 'acura_rdx_2020_can_generated', Bus.radar: 'honda_radar_scan_generated'},
)
HONDA_ACCORD = HondaBoschPlatformConfig(
[
HondaCarDocs("Honda Accord 2018-22", "All", video="https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS),
HondaCarDocs("Honda Inspire 2018", "All", min_steer_speed=3. * CV.MPH_TO_MS),
HondaCarDocs("Honda Accord Hybrid 2018-22", "All", min_steer_speed=3. * CV.MPH_TO_MS),
],
# steerRatio: 11.82 is spec end-to-end
CarSpecs(mass=3279 * CV.LB_TO_KG, wheelbase=2.83, steerRatio=16.33, centerToFrontRatio=0.39, tireStiffnessFactor=0.8467),
{Bus.pt: 'honda_civic_hatchback_ex_2017_can_generated', Bus.radar: 'honda_radar_scan_generated'},
flags=HondaFlags.ALLOW_MANUAL_TRANS,
)
HONDA_ACCORD_11G = HondaBoschCANFDPlatformConfig(
[
HondaCarDocs("Honda Accord 2023-25", "All"),
HondaCarDocs("Honda Accord Hybrid 2023-25", "All"),
],
CarSpecs(mass=3477 * CV.LB_TO_KG, wheelbase=2.83, steerRatio=16.0, centerToFrontRatio=0.39),
)
HONDA_CIVIC_BOSCH = HondaBoschPlatformConfig(
[
HondaCarDocs("Honda Civic 2019-21", "All", video="https://www.youtube.com/watch?v=4Iz1Mz5LGF8",
footnotes=[Footnote.CIVIC_DIESEL], min_steer_speed=2. * CV.MPH_TO_MS),
HondaCarDocs("Honda Civic Hatchback 2017-18", min_steer_speed=12. * CV.MPH_TO_MS),
HondaCarDocs("Honda Civic Hatchback 2019-21", "All", min_steer_speed=12. * CV.MPH_TO_MS),
],
CarSpecs(mass=1326, wheelbase=2.7, steerRatio=15.38, centerToFrontRatio=0.4), # steerRatio: 10.93 is end-to-end spec
{Bus.pt: 'honda_civic_hatchback_ex_2017_can_generated', Bus.radar: 'honda_radar_scan_generated'},
flags=HondaFlags.ALLOW_MANUAL_TRANS,
)
HONDA_CIVIC_BOSCH_DIESEL = HondaBoschPlatformConfig(
[], # don't show in docs
HONDA_CIVIC_BOSCH.specs,
{Bus.pt: 'honda_civic_hatchback_ex_2017_can_generated', Bus.radar: 'honda_radar_scan_generated'},
)
HONDA_CIVIC_2022 = HondaBoschPlatformConfig(
[
HondaCarDocs("Honda Civic 2022-24", "All", video="https://youtu.be/ytiOT5lcp6Q"),
HondaCarDocs("Honda Civic Hybrid 2025-26", "All"),
HondaCarDocs("Honda Civic Hatchback 2022-24", "All", video="https://youtu.be/ytiOT5lcp6Q"),
HondaCarDocs("Honda Civic Hatchback Hybrid (Europe only) 2023", "All"),
# TODO: Confirm 2024
HondaCarDocs("Honda Civic Hatchback Hybrid 2025-26", "All"),
],
HONDA_CIVIC_BOSCH.specs,
{Bus.pt: 'honda_bosch_radarless_generated'},
flags=HondaFlags.BOSCH_RADARLESS | HondaFlags.ALLOW_MANUAL_TRANS
)
HONDA_CRV_5G = HondaBoschPlatformConfig(
[HondaCarDocs("Honda CR-V 2017-22", min_steer_speed=15. * CV.MPH_TO_MS)],
# steerRatio: 12.3 is spec end-to-end
CarSpecs(mass=3410 * CV.LB_TO_KG, wheelbase=2.66, steerRatio=16.0, centerToFrontRatio=0.41, tireStiffnessFactor=0.677),
{Bus.pt: 'honda_civic_hatchback_ex_2017_can_generated', Bus.body: 'honda_crv_ex_2017_body_generated',
Bus.radar: 'honda_radar_scan_generated'},
flags=HondaFlags.BOSCH_ALT_BRAKE,
)
HONDA_CRV_6G = HondaBoschCANFDPlatformConfig(
[
HondaCarDocs("Honda CR-V 2023-26", "All"),
HondaCarDocs("Honda CR-V Hybrid 2023-26", "All"),
],
CarSpecs(mass=1703, wheelbase=2.7, steerRatio=16.2, centerToFrontRatio=0.42),
)
HONDA_CRV_HYBRID = HondaBoschPlatformConfig(
[HondaCarDocs("Honda CR-V Hybrid 2017-22", min_steer_speed=12. * CV.MPH_TO_MS)],
# mass: mean of 4 models in kg, steerRatio: 12.3 is spec end-to-end
CarSpecs(mass=1667, wheelbase=2.66, steerRatio=16, centerToFrontRatio=0.41, tireStiffnessFactor=0.677),
{Bus.pt: 'honda_civic_hatchback_ex_2017_can_generated', Bus.radar: 'honda_radar_scan_generated'},
)
HONDA_HRV_3G = HondaBoschPlatformConfig(
[HondaCarDocs("Honda HR-V 2023-25", "All")],
CarSpecs(mass=3125 * CV.LB_TO_KG, wheelbase=2.61, steerRatio=15.2, centerToFrontRatio=0.41, tireStiffnessFactor=0.5),
{Bus.pt: 'honda_bosch_radarless_generated'},
flags=HondaFlags.BOSCH_RADARLESS,
)
HONDA_CITY_7G = HondaBoschPlatformConfig(
[HondaCarDocs("Honda City (Brazil only) 2023", "All")],
CarSpecs(mass=3125 * CV.LB_TO_KG, wheelbase=2.6, steerRatio=19.0, centerToFrontRatio=0.41, minSteerSpeed=23. * CV.KPH_TO_MS),
{Bus.pt: 'honda_bosch_radarless_generated'},
flags=HondaFlags.BOSCH_RADARLESS,
)
ACURA_RDX_3G = HondaBoschPlatformConfig(
[HondaCarDocs("Acura RDX 2019-21", "All", min_steer_speed=3. * CV.MPH_TO_MS)],
CarSpecs(mass=4068 * CV.LB_TO_KG, wheelbase=2.75, steerRatio=11.95, centerToFrontRatio=0.41, tireStiffnessFactor=0.677), # as spec
{Bus.pt: 'acura_rdx_2020_can_generated', Bus.radar: 'honda_radar_scan_generated'},
flags=HondaFlags.BOSCH_ALT_BRAKE,
)
HONDA_INSIGHT = HondaBoschPlatformConfig(
[HondaCarDocs("Honda Insight 2019-22", "All", min_steer_speed=3. * CV.MPH_TO_MS)],
CarSpecs(mass=2987 * CV.LB_TO_KG, wheelbase=2.7, steerRatio=15.0, centerToFrontRatio=0.39, tireStiffnessFactor=0.82), # as spec
{Bus.pt: 'honda_insight_ex_2019_can_generated', Bus.radar: 'honda_radar_scan_generated'},
)
HONDA_E = HondaBoschPlatformConfig(
[HondaCarDocs("Honda e 2020", "All", min_steer_speed=3. * CV.MPH_TO_MS)],
CarSpecs(mass=3338.8 * CV.LB_TO_KG, wheelbase=2.5, centerToFrontRatio=0.5, steerRatio=16.71, tireStiffnessFactor=0.82),
{Bus.pt: 'acura_rdx_2020_can_generated', Bus.radar: 'honda_radar_scan_generated'},
)
HONDA_PILOT_4G = HondaBoschCANFDPlatformConfig(
[HondaCarDocs("Honda Pilot 2023-25", "All")],
CarSpecs(mass=4660 * CV.LB_TO_KG, wheelbase=2.89, centerToFrontRatio=0.442, steerRatio=17.5),
)
HONDA_PASSPORT_4G = HondaBoschCANFDPlatformConfig(
[HondaCarDocs("Honda Passport 2026", "All")],
CarSpecs(mass=4620 * CV.LB_TO_KG, wheelbase=2.89, centerToFrontRatio=0.442, steerRatio=18.5),
)
# mid-model refresh
ACURA_MDX_4G_MMR = HondaBoschCANFDPlatformConfig(
[HondaCarDocs("Acura MDX 2025-26", "All except Type S")],
CarSpecs(mass=4544 * CV.LB_TO_KG, wheelbase=2.89, centerToFrontRatio=0.428, steerRatio=16.2),
)
HONDA_ODYSSEY_5G_MMR = HondaBoschPlatformConfig(
[HondaCarDocs("Honda Odyssey 2021-26", "All", min_steer_speed=70. * CV.KPH_TO_MS)],
CarSpecs(mass=4590 * CV.LB_TO_KG, wheelbase=3.00, steerRatio=19.4, centerToFrontRatio=0.41),
{Bus.pt: 'acura_rdx_2020_can_generated'},
flags=HondaFlags.BOSCH_ALT_BRAKE | HondaFlags.BOSCH_ALT_RADAR,
)
ACURA_TLX_2G = HondaBoschPlatformConfig(
[HondaCarDocs("Acura TLX 2021", "All")],
CarSpecs(mass=3982 * CV.LB_TO_KG, wheelbase=2.87, steerRatio=14.0, centerToFrontRatio=0.43),
{Bus.pt: 'honda_civic_hatchback_ex_2017_can_generated'},
flags=HondaFlags.BOSCH_ALT_RADAR,
)
# mid-model refresh
ACURA_TLX_2G_MMR = HondaBoschCANFDPlatformConfig(
[HondaCarDocs("Acura TLX 2025", "All")],
CarSpecs(mass=3990 * CV.LB_TO_KG, wheelbase=2.87, centerToFrontRatio=0.43, steerRatio=13.7),
)
# Nidec Cars
ACURA_ILX = HondaNidecPlatformConfig(
[
HondaCarDocs("Acura ILX 2016-18", "Technology Plus Package or AcuraWatch Plus", min_steer_speed=25. * CV.MPH_TO_MS),
HondaCarDocs("Acura ILX 2019", "All", min_steer_speed=25. * CV.MPH_TO_MS),
],
CarSpecs(mass=3095 * CV.LB_TO_KG, wheelbase=2.67, steerRatio=18.61, centerToFrontRatio=0.37, tireStiffnessFactor=0.72), # 15.3 is spec end-to-end
radar_dbc_dict('acura_ilx_2016_can_generated'),
flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES | HondaFlags.HAS_ALL_DOOR_STATES,
)
HONDA_CRV = HondaNidecPlatformConfig(
[HondaCarDocs("Honda CR-V 2015-16", "Touring Trim", min_steer_speed=12. * CV.MPH_TO_MS)],
CarSpecs(mass=3572 * CV.LB_TO_KG, wheelbase=2.62, steerRatio=16.89, centerToFrontRatio=0.41, tireStiffnessFactor=0.444), # as spec
radar_dbc_dict('honda_crv_touring_2016_can_generated'),
flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES | HondaFlags.HAS_ALL_DOOR_STATES,
)
HONDA_CRV_EU = HondaNidecPlatformConfig(
[], # Euro version of CRV Touring, don't show in docs
HONDA_CRV.specs,
radar_dbc_dict('honda_crv_touring_2016_can_generated'),
flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES | HondaFlags.HAS_ALL_DOOR_STATES,
)
HONDA_FIT = HondaNidecPlatformConfig(
[HondaCarDocs("Honda Fit 2018-20", min_steer_speed=12. * CV.MPH_TO_MS)],
CarSpecs(mass=2644 * CV.LB_TO_KG, wheelbase=2.53, steerRatio=13.06, centerToFrontRatio=0.39, tireStiffnessFactor=0.75),
radar_dbc_dict('acura_ilx_2016_can_generated'),
flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES,
)
HONDA_FREED = HondaNidecPlatformConfig(
[HondaCarDocs("Honda Freed 2020", min_steer_speed=12. * CV.MPH_TO_MS)],
CarSpecs(mass=3086. * CV.LB_TO_KG, wheelbase=2.74, steerRatio=13.06, centerToFrontRatio=0.39, tireStiffnessFactor=0.75), # mostly copied from FIT
radar_dbc_dict('acura_ilx_2016_can_generated'),
flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES,
)
HONDA_HRV = HondaNidecPlatformConfig(
[HondaCarDocs("Honda HR-V 2019-22", min_steer_speed=12. * CV.MPH_TO_MS)],
HONDA_HRV_3G.specs,
radar_dbc_dict('acura_ilx_2016_can_generated'),
flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES,
)
HONDA_ODYSSEY = HondaNidecPlatformConfig(
[HondaCarDocs("Honda Odyssey 2018-20")],
CarSpecs(mass=1900, wheelbase=3.0, steerRatio=14.35, centerToFrontRatio=0.41, tireStiffnessFactor=0.82),
radar_dbc_dict('honda_odyssey_exl_2018_generated'),
flags=HondaFlags.NIDEC_ALT_PCM_ACCEL | HondaFlags.HAS_ALL_DOOR_STATES,
)
HONDA_ODYSSEY_TWN = HondaNidecPlatformConfig(
[HondaCarDocs("Honda Odyssey (Taiwan) 2018-19")],
CarSpecs(mass=1865, wheelbase=2.9, steerRatio=14.35, centerToFrontRatio=0.44, tireStiffnessFactor=0.82),
radar_dbc_dict('honda_odyssey_twn_2018_generated'),
flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES,
)
ACURA_RDX = HondaNidecPlatformConfig(
[HondaCarDocs("Acura RDX 2016-18", "AcuraWatch Plus or Advance Package", min_steer_speed=12. * CV.MPH_TO_MS)],
CarSpecs(mass=3925 * CV.LB_TO_KG, wheelbase=2.68, steerRatio=15.0, centerToFrontRatio=0.38, tireStiffnessFactor=0.444), # as spec
radar_dbc_dict('acura_rdx_2018_can_generated'),
flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES | HondaFlags.HAS_ALL_DOOR_STATES,
)
HONDA_PILOT = HondaNidecPlatformConfig(
[
HondaCarDocs("Honda Pilot 2016-22", min_steer_speed=12. * CV.MPH_TO_MS),
HondaCarDocs("Honda Passport 2019-25", "All", min_steer_speed=12. * CV.MPH_TO_MS),
],
HONDA_PILOT_4G.specs,
radar_dbc_dict('acura_ilx_2016_can_generated'),
flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES | HondaFlags.HAS_ALL_DOOR_STATES,
)
HONDA_RIDGELINE = HondaNidecPlatformConfig(
[HondaCarDocs("Honda Ridgeline 2017-25", min_steer_speed=12. * CV.MPH_TO_MS)],
CarSpecs(mass=4515 * CV.LB_TO_KG, wheelbase=3.18, centerToFrontRatio=0.41, steerRatio=15.59, tireStiffnessFactor=0.444), # as spec
radar_dbc_dict('acura_ilx_2016_can_generated'),
flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES | HondaFlags.HAS_ALL_DOOR_STATES,
)
HONDA_CIVIC = HondaNidecPlatformConfig(
[HondaCarDocs("Honda Civic 2016-18", min_steer_speed=12. * CV.MPH_TO_MS, video="https://youtu.be/-IkImTe1NYE")],
CarSpecs(mass=1326, wheelbase=2.70, centerToFrontRatio=0.4, steerRatio=15.38), # 10.93 is end-to-end spec
radar_dbc_dict('honda_civic_touring_2016_can_generated'),
flags=HondaFlags.HAS_ALL_DOOR_STATES
)
# port extensions
HONDA_CLARITY = HondaNidecPlatformConfig(
[HondaCarDocs("Honda Clarity 2018-21", min_steer_speed=12. * CV.MPH_TO_MS)],
CarSpecs(mass=1834, wheelbase=2.75, centerToFrontRatio=0.4, steerRatio=16.5),
radar_dbc_dict('honda_clarity_hybrid_2018_can_generated'),
flags=HondaFlags.HAS_ALL_DOOR_STATES,
)
HONDA_NIDEC_ALT_PCM_ACCEL = CAR.with_flags(HondaFlags.NIDEC_ALT_PCM_ACCEL)
HONDA_NIDEC_ALT_SCM_MESSAGES = CAR.with_flags(HondaFlags.NIDEC_ALT_SCM_MESSAGES)
HONDA_BOSCH = CAR.with_flags(HondaFlags.BOSCH)
HONDA_BOSCH_RADARLESS = CAR.with_flags(HondaFlags.BOSCH_RADARLESS)
HONDA_BOSCH_CANFD = CAR.with_flags(HondaFlags.BOSCH_CANFD)
HONDA_BOSCH_ALT_RADAR = CAR.with_flags(HondaFlags.BOSCH_ALT_RADAR)
HONDA_BOSCH_TJA_CONTROL = CAR.with_flags(HondaFlags.BOSCH_TJA_CONTROL)
# Bosch harness family whose radar broadcasts the decodable 16-slot object scan
HONDA_RADAR_SCAN_CAPABLE = HONDA_BOSCH - HONDA_BOSCH_RADARLESS - HONDA_BOSCH_CANFD - HONDA_BOSCH_ALT_RADAR
# scan decode stays off per platform until a real route capture has been validated
HONDA_RADAR_SCAN_VERIFIED = frozenset({CAR.HONDA_ACCORD, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CRV_5G})
DBC = CAR.create_dbc_map()
STEER_THRESHOLD = {
# default is 1200, overrides go here
CAR.ACURA_RDX: 400,
CAR.HONDA_CRV_EU: 400,
CAR.HONDA_ACCORD_11G: 600,
CAR.HONDA_PILOT_4G: 600,
CAR.HONDA_PASSPORT_4G: 600,
CAR.ACURA_MDX_4G_MMR: 600,
CAR.HONDA_CRV_6G: 600,
CAR.HONDA_CITY_7G: 600,
CAR.HONDA_NBOX_2G: 600,
}
HONDA_ALT_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
p16(0xF112)
HONDA_ALT_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
p16(0xF112)
FW_QUERY_CONFIG = FwQueryConfig(
requests=[
# Currently used to fingerprint
Request(
[StdQueries.UDS_VERSION_REQUEST],
[StdQueries.UDS_VERSION_RESPONSE],
bus=1,
),
# Data collection requests:
# Log manufacturer-specific identifier for current ECUs
Request(
[HONDA_ALT_VERSION_REQUEST],
[HONDA_ALT_VERSION_RESPONSE],
bus=1,
logging=True,
),
# Nidec PT bus
Request(
[StdQueries.UDS_VERSION_REQUEST],
[StdQueries.UDS_VERSION_RESPONSE],
bus=0,
),
# Bosch PT bus
Request(
[StdQueries.UDS_VERSION_REQUEST],
[StdQueries.UDS_VERSION_RESPONSE],
bus=1,
obd_multiplexing=False,
),
],
# We lose these ECUs without the comma power on these cars.
# Note that we still attempt to match with them when they are present
# This is or'd with (ALL_ECUS - ESSENTIAL_ECUS) from fw_versions.py
non_essential_ecus={
Ecu.eps: [CAR.ACURA_RDX_3G, CAR.HONDA_ACCORD, CAR.HONDA_E, *HONDA_BOSCH_ALT_RADAR, *HONDA_BOSCH_RADARLESS, *HONDA_BOSCH_CANFD],
Ecu.vsa: [CAR.ACURA_RDX_3G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CRV_5G, CAR.HONDA_CRV_HYBRID,
CAR.HONDA_E, CAR.HONDA_INSIGHT, CAR.HONDA_NBOX_2G, *HONDA_BOSCH_ALT_RADAR, *HONDA_BOSCH_RADARLESS, *HONDA_BOSCH_CANFD],
},
extra_ecus=[
(Ecu.combinationMeter, 0x18da60f1, None),
(Ecu.programmedFuelInjection, 0x18da10f1, None),
# The only other ECU on PT bus accessible by camera on radarless Civic
# This is likely a manufacturer-specific sub-address implementation: the camera responds to this and 0x18dab0f1
# Unclear what the part number refers to: 8S103 is 'Camera Set Mono', while 36160 is 'Camera Monocular - Honda'
# TODO: add query back, camera does not support querying both in parallel and 0x18dab0f1 often fails to respond
# (Ecu.unknown, 0x18DAB3F1, None),
],
)