forked from IQ.Lvbs/IQ.Pilot
IQ.Pilot Prebuilt Release @ 67fd9c2
This commit is contained in:
490
artifacts/package_runtime/iqdbc/car/honda/carcontroller.py
Normal file
490
artifacts/package_runtime/iqdbc/car/honda/carcontroller.py
Normal 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
|
||||
383
artifacts/package_runtime/iqdbc/car/honda/carstate.py
Normal file
383
artifacts/package_runtime/iqdbc/car/honda/carstate.py
Normal 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
|
||||
186
artifacts/package_runtime/iqdbc/car/honda/dash_lane.py
Normal file
186
artifacts/package_runtime/iqdbc/car/honda/dash_lane.py
Normal 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)
|
||||
314
artifacts/package_runtime/iqdbc/car/honda/dash_objects.py
Normal file
314
artifacts/package_runtime/iqdbc/car/honda/dash_objects.py
Normal 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)
|
||||
1102
artifacts/package_runtime/iqdbc/car/honda/fingerprints.py
Normal file
1102
artifacts/package_runtime/iqdbc/car/honda/fingerprints.py
Normal file
File diff suppressed because it is too large
Load Diff
324
artifacts/package_runtime/iqdbc/car/honda/hondacan.py
Normal file
324
artifacts/package_runtime/iqdbc/car/honda/hondacan.py
Normal 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
|
||||
392
artifacts/package_runtime/iqdbc/car/honda/interface.py
Normal file
392
artifacts/package_runtime/iqdbc/car/honda/interface.py
Normal 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)
|
||||
92
artifacts/package_runtime/iqdbc/car/honda/radar_interface.py
Normal file
92
artifacts/package_runtime/iqdbc/car/honda/radar_interface.py
Normal 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
|
||||
396
artifacts/package_runtime/iqdbc/car/honda/radar_scan.py
Normal file
396
artifacts/package_runtime/iqdbc/car/honda/radar_scan.py
Normal 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
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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}
|
||||
468
artifacts/package_runtime/iqdbc/car/honda/values.py
Normal file
468
artifacts/package_runtime/iqdbc/car/honda/values.py
Normal 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),
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user