mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
1887 lines
108 KiB
Python
1887 lines
108 KiB
Python
from openpilot.starpilot.gps.publisher import CarGpsPublisher
|
|
#!/usr/bin/env python3
|
|
import os
|
|
import time
|
|
import threading
|
|
import uuid
|
|
from collections import deque
|
|
from typing import cast
|
|
|
|
import openpilot.cereal.messaging as messaging
|
|
|
|
from openpilot.cereal import log
|
|
from opendbc.car.structs import car
|
|
|
|
from openpilot.common.params import Params
|
|
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper
|
|
from openpilot.common.swaglog import cloudlog, ForwardingHandler
|
|
|
|
from opendbc.car import DT_CTRL, structs
|
|
from opendbc.car.can_definitions import CanData, CanRecvCallable, CanSendCallable
|
|
from opendbc.car.carlog import carlog
|
|
from opendbc.car.fw_versions import ObdCallback
|
|
from opendbc.car.car_helpers import get_car, interfaces
|
|
from opendbc.car.gm.distance_button import GMDistanceButtons
|
|
from opendbc.car.gm.interface import CarInterface as GMInterface
|
|
from opendbc.car.gm.profiles import profiles_supported as gm_profiles_supported
|
|
from opendbc.car.hyundai.ioniq6_handoff import (IONIQ6_LONG_PREARM_ENABLED, HandoffOutcome,
|
|
IONIQ6_ECAN_BUS, TimestampedCanPacket, build_ioniq6_hda2_long_candidate,
|
|
prepare_ioniq6_long_candidate, confirm_ioniq6_prepared_takeover, restore_ioniq6_adas,
|
|
inspect_ioniq6_long_sources)
|
|
from opendbc.car.hyundai.ioniq6_media import Ioniq6MediaButtons
|
|
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR
|
|
from opendbc.car.interfaces import CarInterfaceBase, RadarInterfaceBase
|
|
from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp
|
|
from openpilot.selfdrive.car.cruise import VCruiseHelper, SlcPendingConfirmation
|
|
from openpilot.starpilot.speed_limits import physical_actions as slc_physical
|
|
from openpilot.starpilot.aol.intent import AolProcessFaultContext, independent_axis_requested, read_settings
|
|
from openpilot.starpilot.aol.transport_pause import TransportPauseFeedback, cp_identity
|
|
from opendbc.car.honda.stock_aol import (
|
|
qualified as qualified_honda_stock_aol, native_observation as honda_native_observation,
|
|
temporary_restriction as honda_temporary_restriction,
|
|
)
|
|
from opendbc.car.volkswagen.stock_aol import (native_observation as volkswagen_native_observation,
|
|
safety_config as volkswagen_aol_safety_config)
|
|
from openpilot.starpilot.car.volkswagen.aol import (qualified as qualified_volkswagen_aol,
|
|
temporary_restriction as volkswagen_temporary_restriction)
|
|
from openpilot.starpilot.car.rivian.aol import retained_brake_lateral_intent as rivian_brake_intent_retained
|
|
from opendbc.car.rivian.stock_aol import (qualified as qualified_rivian_aol,
|
|
native_observation as rivian_native_observation,
|
|
temporary_restriction as rivian_temporary_restriction)
|
|
from opendbc.car.mazda.stock_aol import (qualified as qualified_mazda_aol,
|
|
native_observation as mazda_native_observation,
|
|
temporary_restriction as mazda_temporary_restriction)
|
|
from opendbc.car.tesla.preap.aol import (
|
|
qualified as qualified_tesla_preap, native_observation as preap_native_observation,
|
|
temporary_restriction as preap_temporary_restriction,
|
|
)
|
|
from opendbc.car.ford.aol import (
|
|
qualified as qualified_ford_aol, native_observation as ford_native_observation,
|
|
temporary_restriction as ford_temporary_restriction,
|
|
)
|
|
from opendbc.car.hyundai.canfd_angle_aol import native_observation, temporary_restriction, qualified as qualified_angle_aol
|
|
from opendbc.car.hyundai.canfd_owner import config_index as hyundai_canfd_config_index
|
|
from opendbc.car.hyundai.captured_adrv_startup import CapturedADRVStartup
|
|
from openpilot.starpilot.car.nissan.aol import (
|
|
qualified as qualified_nissan_aol, native_observation as nissan_native_observation,
|
|
temporary_restriction as nissan_temporary_restriction,
|
|
)
|
|
from openpilot.starpilot.car.hyundai.aol import aol_safety_config as hyundai_aol_safety_config, pending_stock_angle_arm
|
|
from openpilot.starpilot.car.hyundai.aol import retained_shared_long_cancel_intent as hkg_cancel_intent_retained
|
|
from openpilot.starpilot.aol.runtime import AXIS_MAX_AGE_NS, current_axis, current_native
|
|
from openpilot.starpilot.aol.vehicle import create_intent as create_aol_intent, native_latch_rejected, policy_for as aol_policy_for
|
|
from openpilot.starpilot.aol.wire import IntentState, encode_intent
|
|
from openpilot.starpilot.conditional_mode.manual import Button, ButtonTracker, IoniqMediaMapCache, WheelMapCache, Press, ioniq6_media_eligible
|
|
from openpilot.starpilot.longitudinal.ioniq6_start import eligible as ioniq6_long_eligible
|
|
from opendbc.car.gm.aol import qualified_gm
|
|
from openpilot.starpilot.aol.vehicle import native_bootstrap_supported
|
|
from opendbc.car.gm.values import CAR as GM_CAR, GMFlags, is_volt_ascm_longitudinal, is_volt_sdgm_profile, is_bolt_present_no_acc_pedal_profile
|
|
from opendbc.car.gm.lateral import lane_centering_supported as gm_lane_centering_supported
|
|
from opendbc.car.gm.ordinary_cc import control_transport_required as gm_cc_transport_required
|
|
from openpilot.starpilot.longitudinal.toyota_output_policy import CLOCK_PAIR_MAX_SKEW_NS, clock_pair_ns
|
|
from openpilot.starpilot.longitudinal.cruise_intervals import read_cruise_intervals
|
|
from openpilot.starpilot.conditional_mode.card_input import conditional_manual_candidate, conditional_traffic_candidate
|
|
from openpilot.starpilot.conditional_mode.policy import ModeChoice
|
|
from openpilot.starpilot.conditional_mode.runtime_settings import ConditionalSettingsOwner
|
|
from openpilot.starpilot.conditional_mode.card_settings import CardSettingsWorker
|
|
from openpilot.starpilot.conditional_mode.status import settings_fingerprint
|
|
from openpilot.starpilot.feature_runtime import (requested as feature_requested, enabled as feature_enabled,
|
|
slc_runtime_settings, slc_transport_capable)
|
|
from openpilot.starpilot.saved_source import read_saved
|
|
from openpilot.starpilot.vehicle_selection import startup_candidate
|
|
from openpilot.starpilot.vehicle_preferences import VehicleStartupPreferences
|
|
from openpilot.starpilot.controller_extensions import configure_controller
|
|
from openpilot.starpilot.vehicle_startup import VehicleStartupOwner
|
|
from openpilot.starpilot.controllers.wheel_actions import WheelPublisher, key_for
|
|
from openpilot.starpilot.controllers.cruise_action import CruiseActionConsumer, eligible as cruise_action_eligible
|
|
from openpilot.starpilot.schema_cache import get_cache, prewarm_cache_contracts, put_cache
|
|
|
|
REPLAY = "REPLAY" in os.environ
|
|
|
|
EventName = log.OnroadEvent.EventName
|
|
DISTANCE_GESTURE_KEYS = {Press.SHORT: ('DistanceButtonControl', 0),
|
|
Press.LONG: ('LongDistanceButtonControl', 1),
|
|
Press.VERY_LONG: ('VeryLongDistanceButtonControl', 2)}
|
|
|
|
# forward
|
|
carlog.addHandler(ForwardingHandler(cloudlog))
|
|
|
|
|
|
def obd_callback(params: Params) -> ObdCallback:
|
|
def set_obd_multiplexing(obd_multiplexing: bool):
|
|
if params.get_bool("ObdMultiplexingEnabled") != obd_multiplexing:
|
|
cloudlog.warning(f"Setting OBD multiplexing to {obd_multiplexing}")
|
|
params.remove("ObdMultiplexingChanged")
|
|
params.put_bool("ObdMultiplexingEnabled", obd_multiplexing, block=True)
|
|
params.get_bool("ObdMultiplexingChanged", block=True)
|
|
cloudlog.warning("OBD multiplexing set successfully")
|
|
return set_obd_multiplexing
|
|
|
|
|
|
def can_comm_callbacks(logcan: messaging.SubSocket, sendcan: messaging.PubSocket) -> tuple[CanRecvCallable, CanSendCallable]:
|
|
def can_recv(wait_for_one: bool = False) -> list[list[CanData]]:
|
|
"""
|
|
wait_for_one: wait the normal logcan socket timeout for a CAN packet, may return empty list if nothing comes
|
|
|
|
Returns: CAN packets comprised of CanData objects for easy access
|
|
"""
|
|
ret = []
|
|
for can in messaging.drain_sock(logcan, wait_for_one=wait_for_one):
|
|
ret.append(TimestampedCanPacket([CanData(msg.address, msg.dat, msg.src) for msg in can.can], int(can.logMonoTime)))
|
|
return ret
|
|
|
|
def can_send(msgs: list[CanData]) -> None:
|
|
sendcan.send(can_list_to_can_capnp(msgs, msgtype='sendcan'))
|
|
|
|
return can_recv, can_send
|
|
|
|
|
|
def curve_press_receipt(CS: car.CarState, helper: VCruiseHelper, sm, *, curve_replay: bool,
|
|
slc_replay: bool, host_long_active: bool, observed_ns: int):
|
|
"""Reserve SLC presentation presses even when their command is stale or rejected."""
|
|
accelerate = frozenset((car.CarState.ButtonEvent.Type.accelCruise,
|
|
car.CarState.ButtonEvent.Type.resumeCruise))
|
|
pressed = {b.type.raw for b in CS.buttonEvents if b.pressed and b.type.raw in accelerate}
|
|
if (not curve_replay or not host_long_active or not CS.canValid or CS.canTimeout or not CS.cruiseState.available or
|
|
not pressed):
|
|
return None
|
|
if slc_replay and (helper.slc_consumed_button is not None or bool(pressed & helper.slc_suppressed_buttons) or
|
|
bool(sm['slcState'].hasPending)):
|
|
return None
|
|
return ('curveAccelPress', None, float(helper.v_cruise_kph_last / 3.6),
|
|
float(helper.v_cruise_kph / 3.6), observed_ns)
|
|
|
|
|
|
def alpha_long_requested(params, *, is_release: bool) -> bool:
|
|
"""Retain saved intent on release branches without granting Alpha authority."""
|
|
return not is_release and params.get_bool("AlphaLongitudinalEnabled")
|
|
|
|
|
|
class Car:
|
|
CI: CarInterfaceBase
|
|
RI: RadarInterfaceBase
|
|
CP: car.CarParams
|
|
controller_cruise_sock = None
|
|
aol_process_fault_context = None
|
|
curve_replay = False
|
|
car_params_published = False
|
|
|
|
def __init__(self, CI=None, RI: RadarInterfaceBase | None = None, startup_owner=None) -> None:
|
|
prewarm_cache_contracts(("CarParamsCache", "CarParamsPersistent", "CarParamsPrevRoute"))
|
|
self.can_sock = messaging.sub_sock('can', timeout=20)
|
|
self.params = Params()
|
|
self.slc_replay = os.getenv('SLC_REPLAY_RUNTIME') == '1' or feature_requested(self.params, 'slc')
|
|
self.curve_replay = os.getenv('CURVE_REPLAY_RUNTIME') == '1' or feature_requested(self.params, 'curve')
|
|
self.aol_replay = os.getenv('AOL_REPLAY_RUNTIME') == '1' or feature_requested(self.params, 'aol')
|
|
self.conditional_replay = os.getenv('CONDITIONAL_MODE_REPLAY_RUNTIME') == '1' or feature_requested(self.params, 'conditional')
|
|
subscribed_services = ['pandaStates', 'carControl', 'onroadEvents', 'deviceState', 'autoSteeringLearning']
|
|
if self.params.get_bool('SubaruRedneckCruise') or self.params.get_bool('SubaruSNG'):
|
|
subscribed_services.extend(('longitudinalPlan', 'selfdriveState'))
|
|
if self.aol_replay:
|
|
subscribed_services.extend(service for service in ('aolSafetyWire', 'managerState', 'aolAxisState', 'selfdriveState')
|
|
if service not in subscribed_services)
|
|
if self.slc_replay:
|
|
subscribed_services.append('slcState')
|
|
self.sm = messaging.SubMaster(subscribed_services, ignore_alive=['autoSteeringLearning'],
|
|
ignore_valid=['autoSteeringLearning'], ignore_avg_freq=['autoSteeringLearning'])
|
|
self.slc_cruise_event_id = 0
|
|
self.slc_producer_session = uuid.uuid4().hex
|
|
self.slc_command_sock = messaging.sub_sock('slcCruiseCommand', conflate=False) if self.slc_replay else None
|
|
self.slc_commands = deque(maxlen=8)
|
|
self.slc_last_command_id = 0
|
|
self.slc_command_session = ''
|
|
self.slc_receipts: list[tuple[str, object, float, float, int]] = []
|
|
self.manual_receipt = None
|
|
self.manual_event_sequence = 0
|
|
self.traffic_receipt = None
|
|
self.traffic_event_sequence = 0
|
|
self.switchback_receipt = None
|
|
self.switchback_event_sequence = 0
|
|
publish_services = ['sendcan', 'carState', 'carParams', 'carOutput', 'radarTracks', 'starpilotCarState']
|
|
self.car_gps_publisher = CarGpsPublisher()
|
|
# One existing event channel also carries source-qualified Switchback gestures.
|
|
publish_services.append('slcCruiseEvent')
|
|
self.wheel_publisher = WheelPublisher(self.params)
|
|
self.wheel_commands = ()
|
|
if self.slc_replay:
|
|
publish_services.append('slcDashboardObservation')
|
|
if self.aol_replay:
|
|
publish_services.append('aolIntentWire')
|
|
self.pm = messaging.PubMaster(publish_services)
|
|
|
|
self.can_rcv_cum_timeout_counter = 0
|
|
|
|
self.CC_prev = car.CarControl.new_message()
|
|
self.CS_prev = car.CarState.new_message()
|
|
self.initialized_prev = False
|
|
self.ci_initialized = False
|
|
|
|
self.last_actuators_output = structs.CarControl.Actuators()
|
|
|
|
self.manual_button_tracker = ButtonTracker() if self.conditional_replay else None
|
|
self.traffic_button_tracker = ButtonTracker() if self.conditional_replay else None
|
|
self.ioniq_media_map = IoniqMediaMapCache() if self.conditional_replay else None
|
|
self.manual_settings_owner = ConditionalSettingsOwner(self.params) if self.conditional_replay else None
|
|
self.ioniq6_long_prearmed = False
|
|
self.ioniq6_long_pending = False
|
|
self.ioniq6_long_selected = False
|
|
self.ioniq6_last_preflight_at = 0.0
|
|
self.ioniq6_panda_armed = False
|
|
self.ioniq6_host_warmed = False
|
|
self.ioniq6_handoff_source_floor_ns = 0
|
|
self.ioniq6_long_lost = False
|
|
self.ioniq6_keepalive = None
|
|
self.ioniq6_restore_callbacks = None
|
|
self.ioniq6_restore_attempted = False
|
|
self.ioniq6_init_complete = False
|
|
|
|
self.vehicle_startup = VehicleStartupOwner(startup_owner)
|
|
self.can_callbacks = can_comm_callbacks(self.can_sock, self.pm.sock['sendcan'])
|
|
|
|
is_release = self.params.get_bool("IsReleaseBranch")
|
|
openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle")
|
|
startup_preferences = VehicleStartupPreferences.read(self.params, enabled=openpilot_enabled_toggle)
|
|
|
|
def initialize_aol_preference(cp):
|
|
from openpilot.starpilot.aol.defaults import enroll_missing_preference
|
|
enroll_missing_preference(self.params, cp)
|
|
if not self.aol_replay and feature_requested(self.params, 'aol'):
|
|
self.aol_replay = True
|
|
subscribed_services.extend(('aolSafetyWire', 'managerState', 'aolAxisState', 'selfdriveState'))
|
|
self.sm = messaging.SubMaster(subscribed_services)
|
|
self.pm.sock['aolIntentWire'] = messaging.pub_sock('aolIntentWire')
|
|
|
|
if CI is None:
|
|
# wait for one pandaState and one CAN packet
|
|
print("Waiting for CAN messages...")
|
|
while True:
|
|
can = messaging.recv_one_retry(self.can_sock)
|
|
if len(can.can) > 0:
|
|
break
|
|
|
|
alpha_long_allowed = alpha_long_requested(self.params, is_release=is_release)
|
|
|
|
cached_params = None
|
|
cached_params_raw = get_cache(self.params, "CarParamsCache")
|
|
if cached_params_raw is not None:
|
|
with car.CarParams.from_bytes(cached_params_raw) as _cached_params:
|
|
cached_params = _cached_params
|
|
|
|
def pre_create_hook(cp, candidate, fingerprints, car_fw):
|
|
initialize_aol_preference(cp)
|
|
if candidate == HYUNDAI_CAR.HYUNDAI_IONIQ_6:
|
|
# This decision must precede CarInterface parser/controller construction
|
|
# and the first CarParams publication below. The exact opt-in is
|
|
# still subject to current source and restore verification.
|
|
eligible = alpha_long_allowed and IONIQ6_LONG_PREARM_ENABLED and not is_release and openpilot_enabled_toggle and not self.params.get_bool("IsOffroad")
|
|
if eligible:
|
|
self.sm.update(0)
|
|
eligible = bool(self.sm.valid['pandaStates'] and self.sm.alive['pandaStates'] and
|
|
any(ps.ignitionLine or ps.ignitionCan for ps in self.sm['pandaStates']))
|
|
long_cp = build_ioniq6_hda2_long_candidate(cp, fingerprints) if eligible else None
|
|
if long_cp is not None:
|
|
preflight = inspect_ioniq6_long_sources(self.can_callbacks[0])
|
|
cloudlog.event('ioniq6_long_preflight', reason=preflight.reason)
|
|
if preflight.source_period is None or preflight.vehicle_ready is not False:
|
|
# Reboot in READY retains original stock CP before CI/parsers and
|
|
# publication. Unknown evidence cannot be treated as Ready=False.
|
|
long_cp = None
|
|
if long_cp is not None:
|
|
initialize_aol_preference(long_cp)
|
|
if long_cp is not None and self.aol_replay and independent_axis_requested(read_settings(self.params)):
|
|
# Bit 11 is AOL only inside the exact Ioniq 6 bit-15 namespace.
|
|
# This immutable choice precedes the ECU transaction and CP publish.
|
|
long_cp.safetyConfigs[-1].safetyParam |= 0x0800
|
|
cp, self.ioniq6_long_pending = prepare_ioniq6_long_candidate(
|
|
cp, long_cp, enabled=eligible, is_release=is_release)
|
|
self.ioniq6_long_selected = self.ioniq6_long_pending
|
|
cp = startup_preferences.prepare(cp, fingerprints=fingerprints)
|
|
from openpilot.starpilot.car.ford.longitudinal_preferences import prepare_release_long as prepare_ford_release_long
|
|
cp = prepare_ford_release_long(cp, self.params, is_release=is_release, enabled=openpilot_enabled_toggle)
|
|
return self.vehicle_startup.prepare(cp, interfaces[candidate], self.can_callbacks,
|
|
requested=alpha_long_allowed and not is_release and openpilot_enabled_toggle,
|
|
admission=self.startup_diagnostic_admission, defer_until_controls=True)
|
|
|
|
self.CI = get_car(*self.can_callbacks, obd_callback(self.params), alpha_long_allowed, is_release, cached_params,
|
|
pre_create_hook=pre_create_hook,
|
|
forced_candidate=startup_candidate(self.params, developer_fingerprint=bool(os.getenv('FINGERPRINT'))))
|
|
self.RI = interfaces[self.CI.CP.carFingerprint].RadarInterface(self.CI.CP)
|
|
self.CP = self.CI.CP
|
|
|
|
# continue onto next fingerprinting step in pandad
|
|
self.params.put_bool("FirmwareQueryDone", True, block=True)
|
|
else:
|
|
self.CI, self.CP = CI, CI.CP
|
|
if RI is None:
|
|
raise ValueError('explicit car interface requires a radar interface')
|
|
self.RI = RI
|
|
|
|
self.volt_cc_selected = gm_cc_transport_required(self.CP)
|
|
self.volt_cc_boot_offset_ns = None
|
|
self.volt_cc_source_floor_ns = 0
|
|
if self.volt_cc_selected and 'deviceState' not in subscribed_services:
|
|
subscribed_services.append('deviceState')
|
|
self.sm = messaging.SubMaster(subscribed_services, ignore_alive=['deviceState'], ignore_valid=['deviceState'],
|
|
ignore_avg_freq=['deviceState'])
|
|
|
|
self.CP.alternativeExperience = 0
|
|
self.switchback_capable = ioniq6_media_eligible(self.CP)
|
|
self.ioniq6_media = Ioniq6MediaButtons(IONIQ6_ECAN_BUS) if self.switchback_capable else None
|
|
self.switchback_button_tracker = ButtonTracker() if self.switchback_capable else None
|
|
self.switchback_settings_owner = ConditionalSettingsOwner(self.params) if self.switchback_capable else None
|
|
self.gm_distance = GMDistanceButtons() if self.conditional_replay and gm_profiles_supported(self.CP) else None
|
|
self.gm_distance_map = WheelMapCache(include_ioniq_media=False, cp=self.CP) if self.gm_distance is not None else None
|
|
self.gm_distance_claim_tracker = ButtonTracker() if self.gm_distance is not None else None
|
|
controller_available = self.CI.CC is not None and openpilot_enabled_toggle and not self.CP.dashcamOnly
|
|
self.CP.passive = not controller_available or self.CP.dashcamOnly
|
|
if self.CP.passive:
|
|
safety_config = structs.CarParams.SafetyConfig()
|
|
safety_config.safetyModel = structs.CarParams.SafetyModel.noOutput
|
|
self.CP.safetyConfigs = [safety_config]
|
|
|
|
startup_preferences.finalize(self.CP)
|
|
from opendbc.car.gm.cancel_remap import migrate_assignments as migrate_gm_cancel_assignments
|
|
migrate_gm_cancel_assignments(self.params, self.CP)
|
|
startup_preferences.configure_controller(self.CI)
|
|
configure_controller(self.CI, self.params)
|
|
self.vehicle_startup.configure(self.CI)
|
|
|
|
if self.CP.secOcRequired:
|
|
# Copy user key if available
|
|
try:
|
|
with open("/cache/params/SecOCKey") as f:
|
|
user_key = f.readline().strip()
|
|
from openpilot.starpilot.car.toyota.security_key import decode_key
|
|
if decode_key(user_key) is not None:
|
|
self.params.put("SecOCKey", user_key, block=True)
|
|
except Exception:
|
|
pass
|
|
|
|
from openpilot.starpilot.car.toyota.security_key import decode_key
|
|
secoc_key = self.params.get("SecOCKey")
|
|
if secoc_key is not None:
|
|
saved_secoc_key = decode_key(secoc_key)
|
|
if saved_secoc_key is not None:
|
|
self.CP.secOcKeyAvailable = True
|
|
self.CI.CS.secoc_key = saved_secoc_key
|
|
if controller_available:
|
|
self.CI.CC.secoc_key = saved_secoc_key
|
|
else:
|
|
cloudlog.warning("Saved SecOC key is invalid")
|
|
|
|
from openpilot.starpilot.car.hyundai.button_migration import migrate_lkas_default
|
|
migrate_lkas_default(self.CP, self.params)
|
|
self.is_metric = self.params.get_bool("IsMetric")
|
|
aol_policy = aol_policy_for(self.CP)
|
|
if aol_policy.full_axis_runtime_required and not self.aol_replay:
|
|
self.aol_replay = True
|
|
subscribed_services.extend(('aolSafetyWire', 'managerState', 'aolAxisState', 'selfdriveState'))
|
|
self.sm = messaging.SubMaster(subscribed_services)
|
|
self.pm.sock['aolIntentWire'] = messaging.pub_sock('aolIntentWire')
|
|
initialize_aol_preference(self.CP)
|
|
self.aol_settings = read_settings(self.params) if self.aol_replay else None
|
|
self.aol_qualified = bool(self.aol_settings is not None and
|
|
(aol_policy.full_axis_runtime_required or aol_policy.physical_stalk_owner or independent_axis_requested(self.aol_settings,
|
|
include_auxiliary=aol_policy.intent_supported and not aol_policy.explicit_latch)) and
|
|
aol_policy.intent_supported and not self.CP.passive)
|
|
self.aol_card_intent = (create_aol_intent(self.CP, self.aol_settings, aol_policy)
|
|
if self.aol_qualified and self.aol_settings is not None else None)
|
|
self.distance_personality_tracker = ButtonTracker() if aol_policy.distance_personality else None
|
|
self.aol_sequence = 0
|
|
self.aol_process_fault_context = AolProcessFaultContext() if self.aol_replay else None
|
|
if self.aol_qualified:
|
|
if self.CP.brand == 'ford':
|
|
from opendbc.car.ford.aol import aol_safety_config as ford_aol_safety_config
|
|
config = ford_aol_safety_config(self.CP)
|
|
else:
|
|
config = (volkswagen_aol_safety_config(self.CP) if self.CP.brand == 'volkswagen' else
|
|
(hyundai_aol_safety_config(self.CP) if self.CP.brand == 'hyundai' else self.CP.safetyConfigs[0]))
|
|
if config is None:
|
|
raise RuntimeError('AOL requires the exact vehicle safety configuration')
|
|
config.safetyParam |= aol_policy.safety_param_addition
|
|
self.CP.alternativeExperience |= aol_policy.alternative_experience_addition
|
|
self.vehicle_startup.finalize_aol_configuration(self.CI)
|
|
self.learning_stamp_observer = None
|
|
self.learning_send_mono_ns = self.learning_send_committed_mono_ns = 0
|
|
self.learning_car_state_source_ns = 0
|
|
self.learning_applied_evidence = self.learning_previous_applied_evidence = None
|
|
try:
|
|
from opendbc.car.steering_projection import install_observer
|
|
self.learning_stamp_observer = self.record_learning_send_stamp if install_observer(self.CI) else None
|
|
except Exception:
|
|
pass # Optional learning setup cannot stop native startup.
|
|
self.aol_transport_feedback = TransportPauseFeedback(self.CP) if self.aol_replay else None
|
|
if isinstance(self.CI, GMInterface) and self.CI.CC is not None and self.CI.CC.volt_sng_plan_input is not None:
|
|
self.CI.CC.volt_sng_plan_input.boottime = self.volt_startup_keepalive()
|
|
|
|
# Reserve optional transport from finalized capability, not saved admission.
|
|
self.slc_transport_available = slc_transport_capable(self.CP, os.environ)
|
|
if self.slc_transport_available:
|
|
if 'slcState' not in subscribed_services:
|
|
subscribed_services.append('slcState')
|
|
self.sm = messaging.SubMaster(subscribed_services)
|
|
for ignored in (self.sm.ignore_alive, self.sm.ignore_valid, self.sm.ignore_average_freq):
|
|
if 'slcState' not in ignored:
|
|
ignored.append('slcState')
|
|
if self.slc_command_sock is None:
|
|
self.slc_command_sock = messaging.sub_sock('slcCruiseCommand', conflate=False)
|
|
if 'slcDashboardObservation' not in self.pm.sock:
|
|
self.pm.sock['slcDashboardObservation'] = messaging.pub_sock('slcDashboardObservation')
|
|
self.slc_requested_configuration = self.read_slc_configuration()
|
|
self.slc_configuration = self.slc_requested_configuration
|
|
self.slc_replay = self.slc_configuration[0]
|
|
self.slc_control_enabled = self.slc_replay and self.slc_configuration[1].enabled
|
|
self.slc_source_floor_ns = 0
|
|
|
|
# Write previous route's CarParams
|
|
prev_cp = get_cache(self.params, "CarParamsPersistent")
|
|
if prev_cp is not None:
|
|
with car.CarParams.from_bytes(prev_cp) as previous:
|
|
put_cache(self.params, "CarParamsPrevRoute", previous, block=True)
|
|
|
|
# Write CarParams for controls and radard
|
|
self.vehicle_startup.seal_publication()
|
|
cp_bytes = self.CP.to_bytes()
|
|
from openpilot.starpilot.car.tesla.pedal_startup import install as install_preap_pedal
|
|
install_preap_pedal(self.CI, self.CP, cp_bytes, startup_preferences.tesla_preap_pedal, self.params)
|
|
from openpilot.starpilot.car.tesla.radar_context import install_startup_context
|
|
self.tesla_preap_radar_context = install_startup_context(
|
|
self.RI, self.CP, cp_bytes, startup_preferences.tesla_preap_radar)
|
|
from openpilot.starpilot.car.subaru.smoothing import install as install_subaru_smoothing
|
|
install_subaru_smoothing(self.CP, cp_bytes, startup_preferences.subaru_smoothing, self.params)
|
|
self.params.put("CarParams", cp_bytes, block=True)
|
|
put_cache(self.params, "CarParamsCache", self.CP)
|
|
put_cache(self.params, "CarParamsPersistent", self.CP)
|
|
|
|
self.controller_cruise_consumer = CruiseActionConsumer()
|
|
self.controller_cruise_sock = messaging.sub_sock('slcAction', conflate=False) if cruise_action_eligible(self.CP) else None
|
|
self.v_cruise_helper = VCruiseHelper(self.CP)
|
|
self.v_cruise_helper.intervals = read_cruise_intervals(self.params, self.CP)
|
|
|
|
self.experimental_mode = self.params.get_bool("ExperimentalMode")
|
|
self.initial_cruise_speed = self.params.get("InitialCruiseSpeedKph", return_default=True)
|
|
|
|
# card is driven by can recv, expected at 100Hz
|
|
self.rk = Ratekeeper(100, print_delay_threshold=None)
|
|
self.ioniq6_init_complete = True
|
|
|
|
def read_slc_configuration(self):
|
|
settings = slc_runtime_settings(self.params, self.CP, os.environ)
|
|
admitted = (self.slc_transport_available and not settings.errors and
|
|
feature_enabled(self.params, self.CP, 'slc', os.environ))
|
|
return bool(admitted), settings
|
|
|
|
def refresh_slc_configuration(self, now_ns: int) -> None:
|
|
configuration = getattr(self, 'slc_requested_configuration', None)
|
|
if configuration is None or configuration == getattr(self, 'slc_configuration', None):
|
|
return
|
|
self.slc_configuration = configuration
|
|
self.slc_replay = configuration[0]
|
|
self.slc_control_enabled = self.slc_replay and configuration[1].enabled
|
|
self.slc_source_floor_ns = max(getattr(self, 'slc_source_floor_ns', 0), now_ns)
|
|
self.slc_commands.clear()
|
|
self.slc_command_session = ''
|
|
self.slc_last_command_id = 0
|
|
# Keep the shared event producer and held suppression until physical release.
|
|
# Only SLC's sourced/issued work predating this admission is retired.
|
|
|
|
def observe_distance_personality(self, CS: car.CarState, now_ns: int) -> None:
|
|
tracker = getattr(self, 'distance_personality_tracker', None)
|
|
if tracker is None:
|
|
return
|
|
for gesture in tracker.observe(CS):
|
|
if gesture.button is not Button.DISTANCE:
|
|
continue
|
|
key, slot = DISTANCE_GESTURE_KEYS[gesture.press]
|
|
try:
|
|
raw, readable = read_saved(self.params, key, 8)
|
|
except (OSError, TypeError, ValueError):
|
|
raw, readable = None, False
|
|
sampled = self.aol_card_intent.settings.distance_actions[slot] if self.aol_card_intent is not None else 0
|
|
handled = sampled in (3, 4) or sampled == 9 and self.aol_card_intent is not None and \
|
|
self.aol_card_intent.settings.enabled and not self.aol_card_intent.explicit_latch
|
|
configured = readable and (raw in (b'3', b'4') or ioniq6_long_eligible(self.CP) and raw == b'5')
|
|
if handled or configured:
|
|
tracker.claim(gesture, now_ns, key)
|
|
|
|
|
|
def state_update(self, *, defer_radar=False) -> tuple[car.CarState, structs.RadarDataT | None]:
|
|
"""carState update loop, driven by can"""
|
|
|
|
can_strs = messaging.drain_sock_raw(self.can_sock, wait_for_one=True)
|
|
can_received_ns = time.monotonic_ns()
|
|
can_list = can_capnp_to_list(can_strs)
|
|
|
|
# Update carState from CAN
|
|
CS = self.CI.update(can_list)
|
|
from openpilot.starpilot.car.gm.aol import f1_conventional_lateral_owner
|
|
cancel_boot_ns = (time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
|
if (getattr(self.CI.CS, 'ev6_aol_source_healthy', False) or
|
|
qualified_honda_stock_aol(self.CP, marked_only=True) or
|
|
qualified_gm(self.CP) and (is_bolt_present_no_acc_pedal_profile(self.CP) or
|
|
f1_conventional_lateral_owner(self.CP)) and
|
|
int(self.CP.alternativeExperience) in (32, 96)) else 0)
|
|
self.observe_ioniq6_long_authority(can_list, CS)
|
|
media_owner = getattr(self, 'ioniq6_media', None)
|
|
media_observation = media_owner.update(can_list) if media_owner is not None else None
|
|
distance_owner = getattr(self, 'gm_distance', None)
|
|
distance_observation = distance_owner.update(can_list) if distance_owner is not None else None
|
|
|
|
# Direct state_update callers retain eager radar behavior. The production
|
|
# loop consumes this exact CAN batch after carState and control publication.
|
|
self._deferred_radar_can = can_list if defer_radar else None
|
|
RD = None if defer_radar else self.radar_update(can_list)
|
|
|
|
self.sm.update(0)
|
|
|
|
self.update_vehicle_state_context(CS)
|
|
|
|
can_rcv_valid = len(can_strs) > 0
|
|
|
|
# Check for CAN timeout
|
|
if not can_rcv_valid:
|
|
self.can_rcv_cum_timeout_counter += 1
|
|
|
|
if can_rcv_valid and REPLAY:
|
|
self.can_log_mono_time = messaging.log_from_bytes(can_strs[0]).logMonoTime
|
|
|
|
self.slc_receipts = []
|
|
now_ns = int(self.can_log_mono_time) if REPLAY and hasattr(self, 'can_log_mono_time') else time.monotonic_ns()
|
|
settings_worker = getattr(self, 'conditional_settings_worker', None)
|
|
if settings_worker is not None:
|
|
settings_worker.adopt()
|
|
self._aol_can_observation = (CS, can_received_ns,
|
|
max((stamp for stamp, _ in can_list), default=0), cancel_boot_ns)
|
|
self.refresh_slc_configuration(now_ns)
|
|
evidence = getattr(self.CI.CS, 'dashboard_limit', None) if self.slc_replay else None
|
|
observation = evidence.observation if evidence is not None else None
|
|
floor_ns = getattr(self, 'slc_source_floor_ns', 0)
|
|
if (observation is not None and floor_ns and
|
|
(observation.observed_ns <= floor_ns or int(self.sm.logMonoTime['slcState']) <= floor_ns or
|
|
int(self.sm['slcState'].frameMonoTime) <= floor_ns)):
|
|
observation = None
|
|
control_log_ns = int(self.sm.logMonoTime['carControl'])
|
|
host_enabled = bool(
|
|
CS.canValid and not CS.canTimeout and
|
|
self.CC_prev.enabled and
|
|
self.sm.valid['carControl'] and self.sm.alive['carControl'] and
|
|
0 < control_log_ns <= now_ns and now_ns - control_log_ns <= slc_physical.STATE_MAX_AGE_NS and
|
|
self.sm['carControl'].enabled)
|
|
host_control_enabled = bool(self.CP.openpilotLongitudinalControl and not self.CP.pcmCruise and host_enabled)
|
|
host_long_active = host_control_enabled and self.sm['carControl'].longActive
|
|
slc_long_active = bool(getattr(self, 'slc_control_enabled', self.slc_replay) and self.CP.openpilotLongitudinalControl and
|
|
host_enabled and (self.sm['carControl'].longActive or CS.gasPressed))
|
|
pending = None
|
|
if self.slc_replay:
|
|
pending = slc_physical.pending_confirmation(
|
|
self.sm, now_ns, long_active=slc_long_active, after_ns=floor_ns)
|
|
approved = (slc_physical.state_current(self.sm, observation, self.slc_producer_session, now_ns)
|
|
if self.v_cruise_helper.toyota is not None and self.slc_control_enabled else None)
|
|
if approved is not None and int(approved.frameMonoTime) <= floor_ns:
|
|
approved = None
|
|
self.update_cruise_speed(CS, pending, approved=approved)
|
|
if self.controller_cruise_sock is not None:
|
|
for _ in range(8):
|
|
request = messaging.recv_one_or_none(self.controller_cruise_sock)
|
|
if request is None:
|
|
break
|
|
self.controller_cruise_consumer.apply(request, self.CP, CS, self.sm, self.v_cruise_helper,
|
|
now_ns=now_ns, is_metric=self.is_metric,
|
|
enabled=bool(host_control_enabled and can_rcv_valid and not self.params.get_bool("SafeMode")))
|
|
consumed = self.v_cruise_helper.slc_consumed_button
|
|
if self.aol_card_intent is not None:
|
|
if self.aol_process_fault_context is None:
|
|
self.aol_process_fault_context = AolProcessFaultContext()
|
|
self.aol_process_fault_context.observe(self.sm, now_ns)
|
|
if getattr(self, 'aol_transport_feedback', None) is None:
|
|
self.aol_transport_feedback = TransportPauseFeedback(self.CP)
|
|
cancel_source_ns = self.aol_cancel_source(CS, now_ns)
|
|
brake_source_ns = self.aol_honda_brake_source(CS, now_ns)
|
|
gm_withdrawal_source_ns = self.aol_gm_withdrawal_source(CS, now_ns)
|
|
hkg_cancel_retained = hkg_cancel_intent_retained(self.CP, CS, self.sm, self.aol_transport_feedback,
|
|
self.aol_card_intent, cancel_source_ns=cancel_source_ns, now_ns=now_ns,
|
|
producer=getattr(self, 'slc_producer_session', ''))
|
|
cancel_feedback = self.aol_transport_feedback
|
|
if (hkg_cancel_retained and self.CP.carFingerprint in (HYUNDAI_CAR.KIA_EV9, HYUNDAI_CAR.HYUNDAI_IONIQ_5_PE) and
|
|
cancel_feedback is not None and not cancel_feedback.longitudinal_cancel_ns):
|
|
# The pure guard proved this exact CCNC raw CANCEL/FULL join. Bind
|
|
# its first local receipt for the existing bounded withdrawal handshake;
|
|
# held samples never renew it or any source/native/output deadline.
|
|
if cancel_source_ns > cancel_feedback.last_cancel_source_ns:
|
|
cancel_feedback.longitudinal_cancel_ns = cancel_source_ns
|
|
cancel_feedback.last_cancel_source_ns = cancel_source_ns
|
|
rivian_brake_retained = rivian_brake_intent_retained(self.CP, CS, self.sm, self.aol_transport_feedback,
|
|
self.aol_card_intent, now_ns=now_ns, producer=self.slc_producer_session)
|
|
angle_arm_pending = pending_stock_angle_arm(self.CP, CS, self.sm, self.aol_card_intent,
|
|
now_ns=now_ns, producer=self.slc_producer_session,
|
|
acknowledged_arm_ns=getattr(self, '_angle_aol_acknowledged_arm_ns', 0))
|
|
transport_reset = not (hkg_cancel_retained or rivian_brake_retained or angle_arm_pending) and self.aol_transport_feedback.observe(
|
|
self.sm, self.CP, CS, now_ns,
|
|
latched=self.aol_card_intent.allowed_latch, cancel_source_ns=cancel_source_ns,
|
|
brake_source_ns=brake_source_ns,
|
|
gm_withdrawal_source_ns=gm_withdrawal_source_ns,
|
|
intent_producer_session=getattr(self, 'slc_producer_session', ''),
|
|
main_arm_ns=getattr(self.aol_card_intent, 'main_arm_ns', 0),
|
|
startup_calibration_pending=getattr(self.aol_card_intent, 'startup_calibration_pending', False),
|
|
intent_paused=getattr(self.aol_card_intent, 'pause_lateral', False))
|
|
fault_active = True if transport_reset else None
|
|
event_ns = int(self.sm.logMonoTime['onroadEvents'])
|
|
if (getattr(self.aol_card_intent, 'requires_fault_observation', self.aol_card_intent.explicit_latch) and
|
|
self.sm.updated['onroadEvents'] and
|
|
self.sm.valid['onroadEvents'] and 0 < event_ns <= now_ns and now_ns - event_ns <= 1_500_000_000):
|
|
feedback = self.aol_transport_feedback
|
|
# Reuse only this call's healthy observation; fault/physical paths reobserve.
|
|
reuse_transport = rivian_brake_retained or bool(not transport_reset and not cancel_source_ns and not brake_source_ns and
|
|
not gm_withdrawal_source_ns and CS.canValid and not CS.canTimeout and
|
|
not CS.steerFaultTemporary and not CS.steerFaultPermanent and not CS.accFaulted and
|
|
feedback.signature is not None and feedback.signature[5] == 'none' and
|
|
not feedback.source_retention_started_ns and not feedback.longitudinal_cancel_ns and
|
|
not feedback.honda_brake_ns and not feedback.gm_withdrawal_ns and not feedback.failed and not feedback.paused and
|
|
not feedback.pending_event and not feedback.awaiting and not feedback.qualified and
|
|
not feedback.retention_started_ns and not feedback.retention_qualified and not feedback.retention_pending and
|
|
not any(e.immediateDisable or e.softDisable or e.userDisable for e in self.sm['onroadEvents']))
|
|
fault_active = transport_reset or self.aol_disarming_fault(CS, event_ns, now_ns,
|
|
transport_observed_healthy=reuse_transport or angle_arm_pending)
|
|
angle_aol, ford_aol, honda_aol, mazda_aol, preap_aol, volkswagen_aol = self.aol_permission_owners()
|
|
nissan_aol = qualified_nissan_aol(self.CP, marked_only=True)
|
|
rivian_aol = qualified_rivian_aol(self.CP, marked_only=True)
|
|
permission_owner = angle_aol or ford_aol or honda_aol or mazda_aol or preap_aol or nissan_aol or volkswagen_aol or rivian_aol
|
|
angle_panda_ready = not permission_owner or self.startup_panda_configured()
|
|
native = (
|
|
current_native(self.sm, self.CP, now_ns=now_ns, axis_session_id=self.aol_axis_session() if permission_owner else None)
|
|
if self.aol_card_intent.explicit_latch and (permission_owner or self.sm.updated['aolSafetyWire'])
|
|
else None
|
|
)
|
|
native_reset = False
|
|
if permission_owner:
|
|
observe = (rivian_native_observation if rivian_aol else
|
|
volkswagen_native_observation if volkswagen_aol else nissan_native_observation if nissan_aol else
|
|
preap_native_observation if preap_aol else mazda_native_observation if mazda_aol else self.aol_honda_native_observation if honda_aol else
|
|
ford_native_observation if ford_aol else native_observation)
|
|
restrict = (rivian_temporary_restriction if rivian_aol else
|
|
volkswagen_temporary_restriction if volkswagen_aol else nissan_temporary_restriction if nissan_aol else
|
|
preap_temporary_restriction if preap_aol else mazda_temporary_restriction if mazda_aol else honda_temporary_restriction if honda_aol else
|
|
ford_temporary_restriction if ford_aol else temporary_restriction)
|
|
preap_observation = {}
|
|
if preap_aol:
|
|
cancel_stamp = int(self.CI.CS.preap.cancel_echo_stamp_ns)
|
|
if cancel_stamp > getattr(self, '_preap_cancel_echo_stamp_ns', 0):
|
|
self._preap_cancel_echo_stamp_ns = cancel_stamp
|
|
self._preap_cancel_echo_pending_ns = now_ns
|
|
preap_observation['pending_cancel_ns'] = getattr(self, '_preap_cancel_echo_pending_ns', 0)
|
|
pending_since, lost, native_reset = observe(
|
|
native,
|
|
latched=self.aol_card_intent.allowed_latch,
|
|
panda_ready=angle_panda_ready,
|
|
restricted=(getattr(self.aol_transport_feedback, 'ordinary_warning_qualified', False) or
|
|
restrict(self.CP, CS) or
|
|
((angle_aol or mazda_aol or preap_aol or rivian_aol) and self.aol_card_intent.pause_lateral) or
|
|
(angle_aol and CS.brakePressed and (not CS.standstill) and (CS.vEgo < self.aol_settings.pause_brake_mps)) or
|
|
(volkswagen_aol and (self.aol_card_intent.pause_lateral or
|
|
(CS.brakePressed and (not CS.standstill) and (CS.vEgo < self.aol_card_intent.settings.pause_brake_mps))))),
|
|
now_ns=now_ns,
|
|
pending_since_ns=getattr(self, '_angle_aol_pending_since_ns', 0),
|
|
**preap_observation,
|
|
)
|
|
if (preap_aol and native is not None and preap_observation['pending_cancel_ns'] and
|
|
native.observedMonoTime >= preap_observation['pending_cancel_ns']):
|
|
self._preap_cancel_echo_pending_ns = 0
|
|
self._angle_aol_pending_since_ns = pending_since
|
|
if lost:
|
|
fault_active = True
|
|
if (angle_aol and native is not None and native.requestedLateral and native.lateralAllowed and
|
|
0 < self.aol_card_intent.physical_arm_ns <= native.observedMonoTime):
|
|
self._angle_aol_acknowledged_arm_ns = self.aol_card_intent.physical_arm_ns
|
|
rejected = native_reset or (not angle_arm_pending and
|
|
native_latch_rejected(self.CP, native, state=CS if angle_panda_ready else None))
|
|
rejection_ns = now_ns if native_reset else int(native.observedMonoTime) if rejected else 0
|
|
observe_physical = getattr(self.aol_card_intent, 'observe_physical_samples', None)
|
|
if observe_physical is not None:
|
|
observe_physical(self.CI.CS, now_ns=now_ns)
|
|
startup_readiness = getattr(self.aol_card_intent, 'observe_startup_readiness', None)
|
|
if startup_readiness is not None:
|
|
startup_readiness(self.sm, CS, native=native, panda_ready=angle_panda_ready,
|
|
now_ns=now_ns, producer=self.slc_producer_session)
|
|
self.aol_card_intent.update(CS, fault_active=fault_active, now_ns=now_ns, native_rejection_ns=rejection_ns,
|
|
standard_enabled=(host_enabled if getattr(self.aol_card_intent, 'observe_stock_engagement', False)
|
|
else host_control_enabled))
|
|
# Observe ordinary state directly: independent AOL actuator activity is
|
|
# not evidence that ordinary preEnabled has become active engagement.
|
|
engagement_ns = int(self.sm.logMonoTime.get('selfdriveState', 0))
|
|
engagement_seen = self.sm.seen.get('selfdriveState', False)
|
|
engagement = self.sm['selfdriveState'] if engagement_seen else None
|
|
self.aol_card_intent.observe_full_engagement(
|
|
CS, enabled=bool(engagement is not None and engagement.enabled),
|
|
active=bool(engagement is not None and engagement.active), source_ns=engagement_ns,
|
|
now_ns=now_ns, fault_active=fault_active, native_rejection_ns=rejection_ns,
|
|
observation_valid=bool(engagement_seen and self.sm.valid['selfdriveState'] and self.sm.alive['selfdriveState'] and
|
|
0 < engagement_ns <= now_ns and now_ns - engagement_ns <= AXIS_MAX_AGE_NS))
|
|
if self.aol_card_intent is not None and preap_aol:
|
|
self.CI.CS.preap_lateral_authorized = bool(
|
|
native is not None and native.lateralAllowed and self.aol_card_intent.allowed_latch and
|
|
not self.aol_card_intent.pause_lateral and CS.canValid and not CS.canTimeout and
|
|
CS.gearShifter == structs.CarState.GearShifter.drive and not CS.doorOpen and
|
|
not CS.steerFaultPermanent and not CS.steeringDisengage)
|
|
if self.aol_card_intent is not None:
|
|
self.aol_card_intent.update_auxiliary(CS, media=media_observation,
|
|
media_eligible=ioniq6_media_eligible(self.CP), fault_active=fault_active)
|
|
self.observe_aol_calibration(CS, now_ns,
|
|
host_enabled if getattr(self.aol_card_intent, 'observe_stock_engagement', False) else host_control_enabled)
|
|
self.observe_distance_personality(CS, now_ns)
|
|
gm_claim = getattr(self, 'gm_distance_claim_tracker', None)
|
|
if gm_claim is not None:
|
|
gm_claim.observe(CS)
|
|
self.manual_receipt = None
|
|
self.traffic_receipt = None
|
|
if (self.conditional_replay and (not REPLAY or hasattr(self, 'can_log_mono_time')) and
|
|
self.manual_button_tracker is not None and self.manual_settings_owner is not None):
|
|
# Live Card uses its worker-verified, frame-local view; replay retains
|
|
# the source-clock owner. Held gestures lose claims on unavailable settings.
|
|
manual_snapshot = self.manual_settings_owner.refresh(now_ns)
|
|
manual_drive_id = int(self.sm['deviceState'].startedMonoTime)
|
|
manual_verdict = self.manual_settings_owner.verdict(manual_snapshot, now_mono_ns=now_ns, drive_id=manual_drive_id)
|
|
manual_fingerprint = (settings_fingerprint(manual_snapshot, owner=self.manual_settings_owner)
|
|
if manual_verdict.status == 'ready' and manual_verdict.safe_mode is False and
|
|
manual_verdict.selection is not None and manual_verdict.selection.choice in (ModeChoice.CEM, ModeChoice.CCM)
|
|
else None)
|
|
self.manual_button_tracker.keep_claim(manual_drive_id, manual_fingerprint)
|
|
shared_media_map = (self.ioniq_media_map.sample(self.params, media_observation, now_ns)
|
|
if self.ioniq_media_map is not None and ioniq6_media_eligible(self.CP) else None)
|
|
shared_media_captured = self.ioniq_media_map is not None and ioniq6_media_eligible(self.CP)
|
|
self.manual_receipt = conditional_manual_candidate(
|
|
CS, self.manual_button_tracker, self.params, self.manual_settings_owner, self.CP, self.sm,
|
|
now_ns=now_ns, explicit_aol_latch=bool(self.aol_card_intent is not None and self.aol_card_intent.explicit_latch),
|
|
media=media_observation, media_buttons=shared_media_map, media_map_captured=shared_media_captured)
|
|
if self.manual_receipt is not None:
|
|
gesture, drive_id, fingerprint, _, _ = self.manual_receipt
|
|
self.manual_button_tracker.claim(gesture, drive_id, fingerprint)
|
|
gm_buttons = (self.gm_distance_map.sample(self.params, distance_observation, now_ns)
|
|
if distance_owner is not None else None)
|
|
if self.traffic_button_tracker is not None:
|
|
self.traffic_receipt = conditional_traffic_candidate(
|
|
CS, self.traffic_button_tracker, self.params, self.manual_settings_owner,
|
|
self.CP, self.sm, now_ns=now_ns, media=media_observation,
|
|
media_buttons=gm_buttons if distance_owner is not None else shared_media_map,
|
|
media_map_captured=distance_owner is not None or shared_media_captured, distance=distance_observation)
|
|
if self.traffic_receipt is not None and self.traffic_receipt[0] and distance_owner is not None and gm_claim is not None:
|
|
gm_claim.claim(self.traffic_receipt[1], self.traffic_receipt[2], self.traffic_receipt[3])
|
|
self.switchback_receipt = None
|
|
if self.switchback_button_tracker is not None and self.switchback_settings_owner is not None:
|
|
self.switchback_receipt = conditional_traffic_candidate(
|
|
CS, self.switchback_button_tracker, self.params, self.switchback_settings_owner,
|
|
self.CP, self.sm, now_ns=now_ns, media=media_observation, action=7)
|
|
if consumed is not None:
|
|
self.slc_receipts.append(('confirmationAccept' if consumed.button == 'accel' else 'confirmationReject',
|
|
consumed, 0.0, 0.0, now_ns))
|
|
curve_receipt = curve_press_receipt(CS, self.v_cruise_helper, self.sm, curve_replay=self.curve_replay,
|
|
slc_replay=self.slc_replay, host_long_active=host_long_active, observed_ns=now_ns)
|
|
if curve_receipt is not None:
|
|
self.slc_receipts.append(curve_receipt)
|
|
if self.slc_command_sock is not None:
|
|
for _ in range(8):
|
|
command_message = messaging.recv_one_or_none(self.slc_command_sock)
|
|
if command_message is None:
|
|
break
|
|
if self.slc_replay and command_message.valid:
|
|
self.slc_commands.append(command_message)
|
|
for _ in range(len(self.slc_commands)):
|
|
command_message = self.slc_commands.popleft()
|
|
command = command_message.slcCruiseCommand
|
|
if int(command.issuedMonoTime) <= floor_ns:
|
|
continue
|
|
if now_ns < int(command.issuedMonoTime):
|
|
if now_ns <= int(command.expiresMonoTime):
|
|
self.slc_commands.append(command_message)
|
|
continue
|
|
state = slc_physical.state_current(self.sm, observation, self.slc_producer_session, now_ns)
|
|
if (state is None or int(self.sm.logMonoTime['slcState']) < int(command.issuedMonoTime)) and now_ns < int(command.expiresMonoTime):
|
|
self.slc_commands.append(command_message)
|
|
continue
|
|
if state is not None and str(state.sessionId) != self.slc_command_session:
|
|
self.slc_command_session = str(state.sessionId)
|
|
self.slc_last_command_id = 0
|
|
if not self.slc_command_session:
|
|
self.slc_command_session = str(command.sessionId)
|
|
if str(command.sessionId) != self.slc_command_session or int(command.commandId) <= self.slc_last_command_id:
|
|
continue
|
|
self.slc_last_command_id = int(command.commandId)
|
|
previous_mps = float(self.v_cruise_helper.v_cruise_kph / 3.6)
|
|
origin = SlcPendingConfirmation(str(command.sessionId), int(command.decisionId), int(command.presentationId))
|
|
owners = {**self.v_cruise_helper.slc_suppression_owner, **self.v_cruise_helper.slc_released_owner}
|
|
allowed_buttons = {button for button, owner in owners.items() if owner == origin} if int(command.decisionId) > 0 else set()
|
|
intervening_button = (bool(self.v_cruise_helper.slc_cruise_change) or
|
|
any(b.type.raw not in allowed_buttons for b in CS.buttonEvents) or
|
|
any(timer > 0 and button not in allowed_buttons
|
|
for button, timer in self.v_cruise_helper.button_timers.items()))
|
|
applicable = slc_physical.command_applicable(
|
|
command, state, observation, self.slc_producer_session, now_ns, previous_mps,
|
|
long_active=slc_long_active,
|
|
button_event=intervening_button)
|
|
if self.v_cruise_helper.toyota is not None:
|
|
applicable = applicable and self.v_cruise_helper.toyota.can_apply(float(command.targetMps))
|
|
if applicable:
|
|
self.v_cruise_helper.apply_slc_target(float(command.targetMps), self.is_metric)
|
|
self.slc_receipts.append(('commandApplied' if applicable else 'commandRejected', command,
|
|
previous_mps, float(self.v_cruise_helper.v_cruise_kph / 3.6), now_ns))
|
|
blocked_keys = set()
|
|
if self.manual_receipt is not None:
|
|
blocked_keys.add(key_for(self.manual_receipt[0]))
|
|
for receipt in (self.traffic_receipt, self.switchback_receipt):
|
|
if receipt is not None and receipt[0] and receipt[1] is not None:
|
|
blocked_keys.add(key_for(receipt[1]))
|
|
self.wheel_commands = self.wheel_publisher.observe(
|
|
self.params, self.CP, CS, now_ns=now_ns, drive_id=int(self.sm['deviceState'].startedMonoTime),
|
|
media=media_observation, blocked_keys=blocked_keys)
|
|
# TODO: mirror the carState.cruiseState struct?
|
|
CS.vCruise = float(self.v_cruise_helper.v_cruise_kph)
|
|
CS.vCruiseCluster = float(self.v_cruise_helper.v_cruise_cluster_kph)
|
|
|
|
return CS, RD
|
|
|
|
def update_cruise_speed(self, CS, pending=None, *, approved=None):
|
|
self.v_cruise_helper.update_v_cruise(CS, self.sm['carControl'].enabled, self.is_metric, pending,
|
|
slc_enabled=getattr(self, 'slc_control_enabled', getattr(self, 'slc_replay', False)),
|
|
slc_approved=approved)
|
|
if self.sm['carControl'].enabled and not self.CC_prev.enabled:
|
|
# Initialization owns this edge; evaluate commands against its final speed.
|
|
self.v_cruise_helper.initialize_v_cruise(self.CS_prev, self.experimental_mode,
|
|
resume=self.vehicle_startup.consume_cruise_resume(), initial_speed=self.initial_cruise_speed)
|
|
|
|
def aol_cancel_source(self, CS, now_ns: int) -> int:
|
|
"""Bind Cancel to this parsed CAN batch and its local monotonic receipt."""
|
|
from opendbc.car.hyundai.values import Buttons
|
|
state = getattr(getattr(self, 'CI', None), 'CS', None)
|
|
observation = getattr(self, '_aol_can_observation', None)
|
|
if (state is None or observation is None or observation[0] is not CS or
|
|
not getattr(state, 'ev6_aol_source_healthy', False)):
|
|
return 0
|
|
_, received_ns, can_source_ns, boot_ns = observation
|
|
stamp = int(state.ev6_aol_sample_stamp_ns)
|
|
# Parser source and CAN batch use the producer clock; compare only those.
|
|
# The fixed intent-retention deadline uses the local MONOTONIC receipt.
|
|
if (not 0 < received_ns <= now_ns or now_ns - received_ns > 30_000_000 or
|
|
not 0 < stamp <= can_source_ns <= boot_ns or
|
|
boot_ns - stamp > min(int(state.ev6_aol_timeout_ns), 30_000_000) or
|
|
can_source_ns - stamp > min(int(state.ev6_aol_timeout_ns), 30_000_000) or
|
|
not any(e.type == structs.CarState.ButtonEvent.Type.cancel and e.pressed for e in CS.buttonEvents) or
|
|
not any(cruise == Buttons.CANCEL for _, _, cruise in state.ev6_aol_samples)):
|
|
return 0
|
|
return received_ns
|
|
|
|
def aol_honda_brake_source(self, CS, now_ns: int) -> int:
|
|
observation = getattr(self, '_aol_can_observation', None)
|
|
if (not CS.brakePressed or not qualified_honda_stock_aol(self.CP, marked_only=True) or
|
|
observation is None or observation[0] is not CS):
|
|
return 0
|
|
from opendbc.car import Bus
|
|
from opendbc.car.honda.values import HondaFlags
|
|
parser = self.CI.can_parsers[Bus.pt]
|
|
message = 'BRAKE_MODULE' if self.CP.flags & HondaFlags.BOSCH_ALT_BRAKE else 'POWERTRAIN_DATA'
|
|
signals = parser.vl.get(message, {})
|
|
# Match the real parser's applied-brake or two-sample switch decision.
|
|
brake = bool(signals.get('BRAKE_PRESSED', 0) or message == 'POWERTRAIN_DATA' and
|
|
signals.get('BRAKE_SWITCH', 0) and self.CI.CS.brake_switch_active)
|
|
stamp = int(parser.ts_nanos.get(message, {}).get('BRAKE_PRESSED', 0))
|
|
_, received_ns, can_source_ns, boot_ns = observation
|
|
# CANParser/CAN batch use BOOTTIME; retain the original MONOTONIC receipt.
|
|
if (not brake or not 0 < received_ns <= now_ns or now_ns - received_ns > 30_000_000 or
|
|
not 0 < stamp <= can_source_ns <= boot_ns or boot_ns - stamp > 30_000_000 or
|
|
can_source_ns - stamp > 30_000_000):
|
|
return 0
|
|
return received_ns
|
|
|
|
def aol_gm_withdrawal_source(self, CS, now_ns: int) -> int:
|
|
if self.CP.brand != 'gm':
|
|
return 0
|
|
from opendbc.car import Bus
|
|
from openpilot.starpilot.car.gm.aol import physical_longitudinal_withdrawal_source
|
|
parser = getattr(getattr(self, 'CI', None), 'can_parsers', {}).get(Bus.pt)
|
|
return physical_longitudinal_withdrawal_source(self.CP, CS, parser,
|
|
getattr(self, '_aol_can_observation', None), now_ns)
|
|
|
|
def aol_disarming_fault(self, CS, event_ns: int, now_ns: int, *, transport_observed_healthy=False) -> bool:
|
|
if not aol_policy_for(self.CP).intent_supported:
|
|
return True
|
|
from openpilot.starpilot.aol.vehicle import intent_disarming_fault
|
|
if getattr(self, 'aol_transport_feedback', None) is None:
|
|
self.aol_transport_feedback = TransportPauseFeedback(self.CP)
|
|
hkg_cancel_retained = hkg_cancel_intent_retained(self.CP, CS, self.sm, self.aol_transport_feedback,
|
|
self.aol_card_intent, cancel_source_ns=self.aol_cancel_source(CS, now_ns), now_ns=now_ns,
|
|
producer=getattr(self, 'slc_producer_session', ''))
|
|
if not transport_observed_healthy and not hkg_cancel_retained and self.aol_transport_feedback.observe(self.sm, self.CP, CS, now_ns,
|
|
latched=getattr(getattr(self, 'aol_card_intent', None), 'allowed_latch', True),
|
|
cancel_source_ns=self.aol_cancel_source(CS, now_ns), brake_source_ns=self.aol_honda_brake_source(CS, now_ns),
|
|
gm_withdrawal_source_ns=self.aol_gm_withdrawal_source(CS, now_ns),
|
|
intent_producer_session=getattr(self, 'slc_producer_session', ''),
|
|
main_arm_ns=getattr(self.aol_card_intent, 'main_arm_ns', 0),
|
|
startup_calibration_pending=getattr(self.aol_card_intent, 'startup_calibration_pending', False),
|
|
intent_paused=getattr(self.aol_card_intent, 'pause_lateral', False)):
|
|
return True
|
|
events = self.sm['onroadEvents']
|
|
if self.aol_transport_feedback.retention_qualified or self.aol_transport_feedback.retention_pending:
|
|
from openpilot.starpilot.aol.transport_pause import AVAILABILITY_EVENTS
|
|
events = [event for event in events if not
|
|
(event.name in AVAILABILITY_EVENTS and event.softDisable and not event.immediateDisable)]
|
|
if getattr(self.aol_transport_feedback, 'temporary_host_qualified', False):
|
|
events = [event for event in events if not (event.name in
|
|
(log.OnroadEvent.EventName.locationdTemporaryError, log.OnroadEvent.EventName.commIssue) and
|
|
event.softDisable and not event.immediateDisable)]
|
|
return intent_disarming_fault(self.CP, events, CS,
|
|
startup_calibration_pending=bool(getattr(self.aol_card_intent, 'startup_calibration_pending', False) and
|
|
self.aol_card_intent.settings.enabled and self.aol_card_intent.main_action != 9),
|
|
temporary_ui_process_failure=self.aol_process_fault_context.temporary_ui_failure(event_ns, now_ns),
|
|
temporary_selfdrive_lagging=(qualified_gm(self.CP) or aol_policy_for(self.CP).intent_supported),
|
|
temporary_source_pause=(self.aol_transport_feedback.qualified or self.aol_transport_feedback.awaiting))
|
|
|
|
def aol_permission_owners(self) -> tuple[bool, bool, bool, bool, bool, bool]:
|
|
# CP is fixed once the drive starts; these checks ran five times per 100 Hz frame.
|
|
cached = getattr(self, '_aol_owner_cache', None)
|
|
if cached is None or cached[0] is not self.CP:
|
|
owners = (bool(qualified_angle_aol(self.CP, marked_only=True)), bool(qualified_ford_aol(self.CP, marked_only=True)),
|
|
bool(qualified_honda_stock_aol(self.CP, marked_only=True)), bool(qualified_mazda_aol(self.CP, marked_only=True)),
|
|
bool(qualified_tesla_preap(self.CP, marked_only=True)), bool(qualified_volkswagen_aol(self.CP, marked_only=True)))
|
|
cached = self._aol_owner_cache = (self.CP, owners)
|
|
return cached[1]
|
|
|
|
def aol_honda_native_observation(self, native, *, latched, panda_ready, restricted, now_ns, pending_since_ns):
|
|
acknowledged = getattr(self, '_honda_aol_acknowledgment', None)
|
|
key = None
|
|
host_requested = False
|
|
if native is not None and qualified_honda_stock_aol(self.CP, marked_only=True):
|
|
key = (cp_identity(self.CP), str(native.axisSessionId), str(native.pandaSerial),
|
|
tuple((p.hardwareSerial, p.safetyModel, p.safetyParam) for p in native.pandaInventory))
|
|
try:
|
|
axis = current_axis(self.sm, now_ns=now_ns)
|
|
feedback = self.aol_transport_feedback
|
|
host_requested = bool(axis is not None and axis.sessionId == native.axisSessionId and
|
|
axis.observedMonoTime == self.sm.logMonoTime['aolAxisState'] and
|
|
axis.validUntilMonoTime == axis.observedMonoTime + AXIS_MAX_AGE_NS and
|
|
axis.faultSessionId == axis.sessionId and str(axis.faultReason) == 'none' and
|
|
self.sm.seen['onroadEvents'] and self.sm.valid['onroadEvents'] and self.sm.alive['onroadEvents'] and
|
|
0 < axis.faultEventMonoTime == self.sm.logMonoTime['onroadEvents'] <= axis.observedMonoTime and
|
|
now_ns - axis.faultEventMonoTime <= 1_500_000_000 and
|
|
not any(event.immediateDisable or event.softDisable for event in self.sm['onroadEvents']) and
|
|
axis.sequence > 0 and axis.nativeAcknowledged and axis.desiredLateral and axis.lateralActive and
|
|
axis.intentProducerSessionId == self.slc_producer_session and
|
|
0 < axis.sourceCarStateMonoTime <= native.observedMonoTime <= now_ns and
|
|
now_ns - axis.sourceCarStateMonoTime <= AXIS_MAX_AGE_NS and
|
|
not feedback.qualified and not feedback.retention_qualified and not feedback.paused and
|
|
not feedback.awaiting and not feedback.retention_pending)
|
|
except (AttributeError, KeyError, TypeError, ValueError):
|
|
host_requested = False
|
|
previously_acknowledged = bool(acknowledged is not None and acknowledged[0] == key and
|
|
0 < acknowledged[1] <= native.observedMonoTime) if native is not None else False
|
|
if not latched or not panda_ready or key is None:
|
|
self._honda_aol_acknowledgment = None
|
|
elif native.requestedLateral and native.lateralAllowed:
|
|
self._honda_aol_acknowledgment = (key, int(native.observedMonoTime))
|
|
elif acknowledged is not None and acknowledged[0] != key:
|
|
self._honda_aol_acknowledgment = None
|
|
return honda_native_observation(native, latched=latched, panda_ready=panda_ready, restricted=restricted,
|
|
now_ns=now_ns, pending_since_ns=pending_since_ns,
|
|
previously_acknowledged=previously_acknowledged, host_requested_lateral=host_requested)
|
|
|
|
def aol_axis_session(self) -> str:
|
|
# pandad stamps aolSafetyWire with selfdrived's aolAxisState session, not card's producer session.
|
|
seen = getattr(self.sm, 'seen', {})
|
|
if 'aolAxisState' in seen and seen['aolAxisState'] and self.sm['aolAxisState'].sessionId:
|
|
return str(self.sm['aolAxisState'].sessionId)
|
|
return self.slc_producer_session
|
|
|
|
def observe_aol_calibration(self, CS, now_ns: int, standard_enabled: bool) -> None:
|
|
calibration_events = None
|
|
event_ns = int(self.sm.logMonoTime['onroadEvents'])
|
|
if (self.sm.updated['onroadEvents'] and self.sm.valid['onroadEvents'] and
|
|
0 < event_ns <= now_ns and now_ns - event_ns <= 1_500_000_000):
|
|
calibration_events = self.sm['onroadEvents']
|
|
self.aol_card_intent.observe_calibration(CS, events=calibration_events, standard_enabled=standard_enabled)
|
|
|
|
def state_publish(self, CS: car.CarState, RD: structs.RadarDataT | None):
|
|
"""carState and carParams publish loop"""
|
|
|
|
# Publish the finalized CP immediately, then retain the segment cadence.
|
|
if not self.car_params_published or self.sm.frame % int(50. / DT_CTRL) == 0:
|
|
cp_send = messaging.new_message('carParams')
|
|
cp_send.valid = True
|
|
cp_send.carParams = self.CP
|
|
self.pm.send('carParams', cp_send)
|
|
self.car_params_published = True
|
|
|
|
# publish new carOutput
|
|
co_send = messaging.new_message('carOutput')
|
|
co_send.valid = self.sm.all_checks(['carControl'])
|
|
co_send.carOutput.actuatorsOutput = self.last_actuators_output
|
|
self.learning_feedback_publication = None
|
|
capture = self._feedback_capture(int(co_send.logMonoTime))
|
|
if capture is not None:
|
|
try:
|
|
self.learning_feedback_publication = capture.publication(co_send)
|
|
except Exception:
|
|
capture.invalidate() # Optional observation cannot affect the original CO.
|
|
self.pm.send('carOutput', co_send)
|
|
|
|
# kick off controlsd step while we actuate the latest carControl packet
|
|
cs_send = messaging.new_message('carState')
|
|
if (self.slc_replay or self.curve_replay or getattr(self, 'conditional_replay', False)) and REPLAY and hasattr(self, 'can_log_mono_time'):
|
|
cs_send.logMonoTime = int(self.can_log_mono_time)
|
|
cs_send.valid = CS.canValid
|
|
cs_send.carState = CS
|
|
manual_tracker = getattr(self, 'manual_button_tracker', None)
|
|
personality_tracker = getattr(self, 'distance_personality_tracker', None)
|
|
gm_claim = getattr(self, 'gm_distance_claim_tracker', None)
|
|
if ((getattr(self, 'conditional_replay', False) and manual_tracker is not None and manual_tracker.suppress_distance_release) or
|
|
(personality_tracker is not None and personality_tracker.suppress_distance_release) or
|
|
(gm_claim is not None and gm_claim.suppress_distance_release) or
|
|
(getattr(self, 'wheel_publisher', None) is not None and self.wheel_publisher.suppress_distance_release)):
|
|
# This is the serialized carState copy. Keep the original parsed state
|
|
# and raw CAN intact for the controller and diagnostics.
|
|
cs_send.carState.buttonEvents = [event for event in CS.buttonEvents if not
|
|
(event.type == car.CarState.ButtonEvent.Type.gapAdjustCruise and not event.pressed)]
|
|
cs_send.carState.canErrorCounter = self.can_rcv_cum_timeout_counter
|
|
cs_send.carState.cumLagMs = -self.rk.remaining * 1000.
|
|
if self.aol_replay:
|
|
self.aol_sequence += 1
|
|
intent_msg = messaging.new_message('aolIntentWire', 0)
|
|
intent_msg.logMonoTime = int(cs_send.logMonoTime)
|
|
intent_msg.valid = bool(cs_send.valid and self.aol_qualified)
|
|
allowed_latch = pause_lateral = pause_longitudinal = False
|
|
lateral_armed = False
|
|
if self.aol_card_intent is not None:
|
|
allowed_latch, pause_lateral, pause_longitudinal = self.aol_card_intent.output(CS)
|
|
lateral_armed = self.aol_card_intent.allowed_latch
|
|
intent_msg.aolIntentWire = encode_intent(IntentState(
|
|
self.slc_producer_session, self.aol_sequence, int(cs_send.logMonoTime), int(cs_send.logMonoTime),
|
|
int(cs_send.logMonoTime) + 200_000_000, allowed_latch, pause_lateral, pause_longitudinal, self.aol_qualified, lateral_armed,
|
|
bool(getattr(self.aol_card_intent, "optional_set_release_policy", lambda: False)())))
|
|
self.pm.send('aolIntentWire', intent_msg)
|
|
|
|
# carState wakes selfdrived. Commit its companion intent first, so a
|
|
# consumer scheduled at the wakeup cannot sample the preceding intent.
|
|
if self.CP.brand == 'tesla':
|
|
from openpilot.starpilot.car.tesla.stock_events import publish_stock_status
|
|
internal = getattr(self.CI.CS, 'preap', None)
|
|
if internal is not None:
|
|
self.slc_cruise_event_id += 1
|
|
publish_stock_status(self.pm, self.CP, internal, session=self.slc_producer_session,
|
|
sequence=self.slc_cruise_event_id, car_state_stamp=int(cs_send.logMonoTime),
|
|
valid=bool(cs_send.valid and not CS.canTimeout))
|
|
companion = None
|
|
if self.CP.brand == 'tesla' and self.CP.openpilotLongitudinalControl and qualified_tesla_preap(self.CP):
|
|
internal = self.CI.CS.preap
|
|
companion = messaging.new_message('starpilotCarState', valid=bool(cs_send.valid), logMonoTime=int(cs_send.logMonoTime))
|
|
companion.starpilotCarState.sourceCarStateMonoTime = int(cs_send.logMonoTime)
|
|
companion.starpilotCarState.pedalLongActive = bool(internal.enableLongControl)
|
|
companion.starpilotCarState.pedalMaxRegen = internal.pccEvent == 'pedalMaxRegen'
|
|
steering_authority = getattr(self.CI.CS, 'steering_authority', None)
|
|
if self.CP.brand == 'gm' and steering_authority is not None and steering_authority.enabled:
|
|
companion = messaging.new_message('starpilotCarState', valid=bool(cs_send.valid), logMonoTime=int(cs_send.logMonoTime))
|
|
companion.starpilotCarState.lateralAuthorityUnavailable = bool(steering_authority.latched)
|
|
companion.starpilotCarState.sourceCarStateMonoTime = int(cs_send.logMonoTime)
|
|
if getattr(self, 'learning_applied_evidence', None) is not None:
|
|
try:
|
|
from openpilot.starpilot.flm.evidence_hooks import applied_message
|
|
# Commit the optional child only after it is complete. A failed writer
|
|
# must not publish partial trust or discard existing GM authority/GPS.
|
|
learning_companion = (companion.as_reader().as_builder() if companion is not None else
|
|
messaging.new_message('starpilotCarState', valid=bool(cs_send.valid), logMonoTime=int(cs_send.logMonoTime)))
|
|
if applied_message(learning_companion, self.learning_applied_evidence) is not True:
|
|
raise ValueError('optional applied evidence write failed')
|
|
companion = learning_companion
|
|
except Exception:
|
|
self._clear_learning_evidence()
|
|
publication = getattr(self, 'learning_feedback_publication', None)
|
|
if publication is not None:
|
|
try:
|
|
from openpilot.starpilot.flm.feedback_wire import write_publication
|
|
feedback_companion = (companion.as_reader().as_builder() if companion is not None else
|
|
messaging.new_message('starpilotCarState', valid=bool(cs_send.valid), logMonoTime=int(cs_send.logMonoTime)))
|
|
write_publication(feedback_companion.starpilotCarState, publication)
|
|
companion = feedback_companion
|
|
except Exception:
|
|
self.learning_feedback_publication = None
|
|
if self.CP.brand == 'mazda':
|
|
from openpilot.starpilot.car.mazda.dashboard import compose
|
|
companion = compose(self.CP, self.CI.CS, CS, companion, car_state_stamp=int(cs_send.logMonoTime))
|
|
if self.v_cruise_helper.toyota is not None:
|
|
if companion is None:
|
|
companion = messaging.new_message('starpilotCarState', valid=bool(cs_send.valid), logMonoTime=int(cs_send.logMonoTime))
|
|
companion.starpilotCarState.sourceCarStateMonoTime = int(cs_send.logMonoTime)
|
|
self.v_cruise_helper.toyota.publish(companion.starpilotCarState.toyotaCruise)
|
|
self.car_gps_publisher.update(self.CI.CS, self.pm, companion)
|
|
self.pm.send('carState', cs_send)
|
|
if getattr(self, 'learning_stamp_observer', None) is not None:
|
|
self.learning_car_state_source_ns = int(cs_send.logMonoTime)
|
|
|
|
wheel = getattr(self, 'wheel_publisher', None)
|
|
if wheel is not None:
|
|
wheel.publish(self.wheel_commands, self.CP, self.pm, now_ns=int(cs_send.logMonoTime),
|
|
drive_id=int(self.sm['deviceState'].startedMonoTime), source_car_ns=int(cs_send.logMonoTime),
|
|
source_control_ns=int(self.sm.logMonoTime['carControl']))
|
|
|
|
if self.slc_replay:
|
|
source = messaging.new_message('slcDashboardObservation')
|
|
source.logMonoTime = int(cs_send.logMonoTime)
|
|
source.valid = bool(cs_send.valid)
|
|
evidence = getattr(self.CI.CS, 'dashboard_limit', None)
|
|
source.slcDashboardObservation.producerSessionId = self.slc_producer_session
|
|
source.slcDashboardObservation.carStateLogMonoTime = int(cs_send.logMonoTime)
|
|
if evidence is not None:
|
|
observation = evidence.observation
|
|
source.slcDashboardObservation.status = observation.status.value
|
|
source.slcDashboardObservation.speedMps = observation.speed_mps
|
|
source.slcDashboardObservation.observedMonoTime = observation.observed_ns
|
|
source.slcDashboardObservation.validUntilMonoTime = observation.valid_until_ns
|
|
source.slcDashboardObservation.episode = observation.episode
|
|
self.pm.send('slcDashboardObservation', source)
|
|
|
|
change = self.v_cruise_helper.slc_cruise_change
|
|
if self.slc_replay or self.curve_replay:
|
|
if change is not None:
|
|
self.slc_receipts.append(('driverChange', change, change[0], change[1], int(cs_send.logMonoTime)))
|
|
for kind, detail, previous_mps, selected_mps, observed_ns in self.slc_receipts:
|
|
self.slc_cruise_event_id += 1
|
|
event = messaging.new_message('slcCruiseEvent')
|
|
event.logMonoTime = int(cs_send.logMonoTime)
|
|
event.valid = bool(cs_send.valid)
|
|
record = event.slcCruiseEvent
|
|
record.eventId = self.slc_cruise_event_id
|
|
record.producerSessionId = self.slc_producer_session
|
|
record.observedMonoTime = observed_ns
|
|
record.kind = kind
|
|
record.previousMps = previous_mps
|
|
record.selectedMps = selected_mps
|
|
if kind == 'driverChange':
|
|
assert isinstance(detail, tuple) and len(detail) == 4
|
|
record.button, record.longPress = detail[2], detail[3]
|
|
elif kind in ('confirmationAccept', 'confirmationReject'):
|
|
record.button = detail.button
|
|
record.sessionId = detail.pending.session_id
|
|
record.decisionId = detail.pending.decision_id
|
|
record.presentationId = detail.pending.presentation_id
|
|
elif kind == 'curveAccelPress':
|
|
record.button = 'accel'
|
|
else:
|
|
record.sessionId = str(detail.sessionId)
|
|
record.commandId = int(detail.commandId)
|
|
record.decisionId = int(detail.decisionId)
|
|
record.presentationId = int(detail.presentationId)
|
|
self.pm.send('slcCruiseEvent', event)
|
|
|
|
if getattr(self, 'conditional_replay', False) and self.manual_receipt is not None:
|
|
gesture, drive_id, fingerprint, choice, observed_ns = self.manual_receipt
|
|
self.slc_cruise_event_id += 1
|
|
self.manual_event_sequence += 1
|
|
event = messaging.new_message('slcCruiseEvent')
|
|
event.logMonoTime = int(cs_send.logMonoTime)
|
|
event.valid = bool(cs_send.valid)
|
|
record = event.slcCruiseEvent
|
|
record.eventId = self.slc_cruise_event_id
|
|
record.producerSessionId = self.slc_producer_session
|
|
record.observedMonoTime = observed_ns
|
|
record.kind = 'conditionalMode'
|
|
record.manualMode = {
|
|
'version': 1, 'sessionId': self.slc_producer_session, 'sequence': self.manual_event_sequence,
|
|
'observedMonoTime': observed_ns, 'driveStartMonoTime': drive_id,
|
|
'settingsFingerprint': fingerprint,
|
|
'choice': 'conditionalExperimental' if choice is ModeChoice.CEM else 'conditionalChill',
|
|
'button': gesture.button.value, 'press': gesture.press.value,
|
|
'sourceCarStateMonoTime': int(cs_send.logMonoTime),
|
|
'validUntilMonoTime': int(cs_send.logMonoTime) + 100_000_000,
|
|
}
|
|
self.pm.send('slcCruiseEvent', event)
|
|
|
|
self.publish_traffic_receipt(cs_send)
|
|
|
|
self.radar_publish(RD)
|
|
|
|
def radar_update(self, can_list):
|
|
RD = self.RI.update(can_list)
|
|
return RD
|
|
|
|
def radar_publish(self, RD):
|
|
if RD is not None:
|
|
tracks_msg = messaging.new_message('radarTracks')
|
|
tracks_msg.valid = not any(RD.errors.to_dict().values())
|
|
tracks_msg.radarTracks = RD
|
|
self.pm.send('radarTracks', tracks_msg)
|
|
|
|
def publish_traffic_receipt(self, cs_send):
|
|
traffic_receipt = getattr(self, 'traffic_receipt', None)
|
|
if getattr(self, 'conditional_replay', False) and traffic_receipt is not None:
|
|
toggle, gesture, drive_id, fingerprint, map_fingerprint, source_epoch, source_boot_ns, observed_ns = traffic_receipt
|
|
self.slc_cruise_event_id += 1
|
|
self.traffic_event_sequence += 1
|
|
event = messaging.new_message('slcCruiseEvent')
|
|
event.logMonoTime = int(cs_send.logMonoTime)
|
|
event.valid = bool(cs_send.valid)
|
|
record = event.slcCruiseEvent
|
|
record.eventId = self.slc_cruise_event_id
|
|
record.producerSessionId = self.slc_producer_session
|
|
record.observedMonoTime = observed_ns
|
|
record.kind = 'trafficMode'
|
|
record.trafficMode = {
|
|
'version': 1, 'sessionId': self.slc_producer_session, 'sequence': self.traffic_event_sequence,
|
|
'observedMonoTime': observed_ns, 'driveStartMonoTime': drive_id,
|
|
'settingsFingerprint': fingerprint, 'buttonMapFingerprint': map_fingerprint,
|
|
'sourceEpoch': source_epoch, 'sourceBootTime': source_boot_ns,
|
|
'sourceCarStateMonoTime': int(cs_send.logMonoTime),
|
|
'validUntilMonoTime': int(cs_send.logMonoTime) + 100_000_000,
|
|
'toggle': toggle, 'button': gesture.button.value if gesture is not None else 'unknown',
|
|
'press': gesture.press.value if gesture is not None else 'unknown',
|
|
}
|
|
self.pm.send('slcCruiseEvent', event)
|
|
|
|
switchback_receipt = getattr(self, 'switchback_receipt', None)
|
|
if getattr(self, 'switchback_capable', False) and switchback_receipt is not None:
|
|
toggle, gesture, drive_id, fingerprint, map_fingerprint, source_epoch, source_boot_ns, observed_ns = switchback_receipt
|
|
self.slc_cruise_event_id += 1
|
|
self.switchback_event_sequence += 1
|
|
event = messaging.new_message('slcCruiseEvent')
|
|
event.logMonoTime = int(cs_send.logMonoTime)
|
|
event.valid = bool(cs_send.valid)
|
|
record = event.slcCruiseEvent
|
|
record.eventId = self.slc_cruise_event_id
|
|
record.producerSessionId = self.slc_producer_session
|
|
record.observedMonoTime = observed_ns
|
|
record.kind = 'switchbackMode'
|
|
record.trafficMode = {
|
|
'version': 1, 'sessionId': self.slc_producer_session, 'sequence': self.switchback_event_sequence,
|
|
'observedMonoTime': observed_ns, 'driveStartMonoTime': drive_id,
|
|
'settingsFingerprint': fingerprint, 'buttonMapFingerprint': map_fingerprint,
|
|
'sourceEpoch': source_epoch, 'sourceBootTime': source_boot_ns,
|
|
'sourceCarStateMonoTime': int(cs_send.logMonoTime),
|
|
'validUntilMonoTime': int(cs_send.logMonoTime) + 100_000_000,
|
|
'toggle': toggle, 'button': gesture.button.value if gesture is not None else 'unknown',
|
|
'press': gesture.press.value if gesture is not None else 'unknown',
|
|
}
|
|
self.pm.send('slcCruiseEvent', event)
|
|
|
|
def ioniq6_post_handoff_sources_current(self):
|
|
"""A parser's invalid grace is not proof of post-transaction warmup."""
|
|
from opendbc.can.parser import MAX_BAD_COUNTER
|
|
floor = getattr(self, 'ioniq6_handoff_source_floor_ns', 0)
|
|
if floor <= 0:
|
|
return False
|
|
now = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
|
sources = [source for parser in self.CI.can_parsers.values()
|
|
for source in parser.message_states.values() if not source.ignore_alive]
|
|
return bool(sources) and all(
|
|
source.timestamps and floor < source.timestamps[-1] <= now and
|
|
now - source.timestamps[-1] <= source.timeout_threshold and source.counter_fail < MAX_BAD_COUNTER
|
|
for source in sources)
|
|
|
|
def observe_ioniq6_long_authority(self, can_list, CS):
|
|
if getattr(self, 'ioniq6_long_selected', False) and self.ioniq6_long_lost:
|
|
CS.canValid = False
|
|
return
|
|
if not self.ioniq6_long_prearmed:
|
|
return
|
|
if any((src == 1 and address in (0x1A0, 0x1BA, 0x1E5)) or (src == 0 and address == 0x100)
|
|
for _, packet in can_list for address, _, src in packet):
|
|
self.ioniq6_long_lost = True
|
|
if self.ioniq6_host_warmed:
|
|
if not CS.canValid:
|
|
self.ioniq6_long_lost = True
|
|
elif CS.canValid and self.ioniq6_post_handoff_sources_current():
|
|
self.ioniq6_host_warmed = True
|
|
if self.ioniq6_long_lost:
|
|
CS.canValid = False
|
|
if self.ioniq6_keepalive is not None:
|
|
self.ioniq6_keepalive.stop()
|
|
|
|
def ioniq6_panda_matches(self):
|
|
panda_states = self.sm['pandaStates']
|
|
return bool(
|
|
self.sm.valid['pandaStates'] and self.sm.alive['pandaStates'] and
|
|
len(panda_states) >= len(self.CP.safetyConfigs) and
|
|
all(ps.safetyModel == config.safetyModel and ps.safetyParam == config.safetyParam and
|
|
ps.alternativeExperience == self.CP.alternativeExperience and not ps.safetyRxChecksInvalid
|
|
for ps, config in zip(panda_states, self.CP.safetyConfigs, strict=False))
|
|
)
|
|
|
|
def ioniq6_panda_diagnostic_ready(self):
|
|
if not self.sm.valid['pandaStates'] or not self.sm.alive['pandaStates']:
|
|
return False
|
|
panda_states = self.sm['pandaStates']
|
|
return bool(
|
|
len(panda_states) == len(self.CP.safetyConfigs) and
|
|
all(ps.safetyModel == structs.CarParams.SafetyModel.elm327 and
|
|
ps.safetyParam in (0, 1) and not ps.controlsAllowed
|
|
for ps in panda_states)
|
|
)
|
|
|
|
def update_vehicle_state_context(self, state):
|
|
holder = getattr(self, 'vehicle_startup', None)
|
|
if holder is None or holder.owner is None:
|
|
return
|
|
# Python controls and receive times use MONOTONIC. Panda/CAN producers
|
|
# use BOOTTIME, which can diverge after suspend.
|
|
clocks = clock_pair_ns()
|
|
mono_ns, boot_ns = clocks if clocks is not None else (0, 0)
|
|
sm = self.sm
|
|
control_stamp = int(sm.logMonoTime['carControl'])
|
|
control_receipt = int(sm.recv_time['carControl'] * 1e9)
|
|
panda_stamp = int(sm.logMonoTime['pandaStates'])
|
|
panda_receipt = int(sm.recv_time['pandaStates'] * 1e9)
|
|
control_current = bool(clocks is not None and
|
|
sm.seen['carControl'] and sm.valid['carControl'] and sm.alive['carControl'] and
|
|
0 < control_stamp <= mono_ns and mono_ns - control_stamp <= 150_000_000 and
|
|
0 < control_receipt <= mono_ns and mono_ns - control_receipt <= 150_000_000)
|
|
panda_current = bool(clocks is not None and
|
|
sm.seen['pandaStates'] and sm.valid['pandaStates'] and sm.alive['pandaStates'] and
|
|
0 < panda_stamp <= boot_ns and boot_ns - panda_stamp <= 300_000_000 and
|
|
0 < panda_receipt <= mono_ns and mono_ns - panda_receipt <= 300_000_000)
|
|
holder.after_state(ci=self.CI, state=state, now_ns=boot_ns,
|
|
control_enabled=bool(sm['carControl'].enabled), control_current=control_current,
|
|
pandas=sm['pandaStates'], panda_log_ns=panda_stamp,
|
|
panda_recv_ns=panda_receipt + boot_ns - mono_ns if panda_current else 0,
|
|
panda_current=panda_current)
|
|
|
|
def startup_diagnostic_admission(self):
|
|
from openpilot.starpilot.aol.panda_faults import blocking_fault_list
|
|
self.sm.update(0)
|
|
now, boot = time.monotonic_ns(), time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
|
sm = self.sm
|
|
stamp, receipt = int(sm.logMonoTime['pandaStates']), int(sm.recv_time['pandaStates'] * 1e9)
|
|
pandas = sm['pandaStates']
|
|
owner = getattr(self.vehicle_startup, 'owner', None)
|
|
count = 2 if isinstance(owner, CapturedADRVStartup) and hyundai_canfd_config_index(owner.cp) == 1 else 1
|
|
ignition_pandas = pandas
|
|
if (count == 2 and len(pandas) == 2 and
|
|
pandas[0].pandaType == log.PandaState.PandaType.dos and
|
|
pandas[1].pandaType == log.PandaState.PandaType.redPanda):
|
|
# pandad clears the internal DOS ignition line in this exact topology.
|
|
# The external captured-ADRV owner must still observe vehicle ignition.
|
|
ignition_pandas = (pandas[1],)
|
|
return bool(not self.params.get_bool('IsOffroad') and not self.params.get_bool('ControlsReady') and
|
|
sm.seen['pandaStates'] and sm.valid['pandaStates'] and sm.alive['pandaStates'] and
|
|
0 < stamp <= boot and boot - stamp <= 300_000_000 and
|
|
0 < receipt <= now and now - receipt <= 300_000_000 and len(pandas) == count and
|
|
all(ps.safetyModel == structs.CarParams.SafetyModel.elm327 and ps.safetyParam == 1 and
|
|
not ps.controlsAllowed and not ps.safetyRxChecksInvalid and
|
|
not blocking_fault_list(getattr(ps, 'faults', None), getattr(owner, 'cp', None)) for ps in pandas) and
|
|
all(ps.ignitionLine or ps.ignitionCan for ps in ignition_pandas))
|
|
|
|
def startup_panda_configured(self, *, inactive_keepalive=False):
|
|
# Inactive keepalives carry no axis authority during the first native RX health tick.
|
|
now, boot = time.monotonic_ns(), time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
|
sm = self.sm
|
|
stamp, receipt = int(sm.logMonoTime['pandaStates']), int(sm.recv_time['pandaStates'] * 1e9)
|
|
pandas = sm['pandaStates']
|
|
return bool(sm.seen['pandaStates'] and sm.valid['pandaStates'] and sm.alive['pandaStates'] and
|
|
0 < stamp <= boot and boot - stamp <= 300_000_000 and
|
|
0 < receipt <= now and now - receipt <= 300_000_000 and
|
|
len(pandas) == len(self.CP.safetyConfigs) and
|
|
all(ps.safetyModel == cfg.safetyModel and ps.safetyParam == cfg.safetyParam and
|
|
ps.alternativeExperience == self.CP.alternativeExperience and
|
|
(not ps.safetyRxChecksInvalid or inactive_keepalive and self.volt_startup_keepalive())
|
|
for ps, cfg in zip(pandas, self.CP.safetyConfigs, strict=True)))
|
|
|
|
def _feedback_capture(self, now_ns):
|
|
# Default OFF performs no feedback-module import, digest or witness allocation.
|
|
try:
|
|
source = self.sm['autoSteeringLearning'].learning.sourceReceipt
|
|
if not source.enabled or source.manual:
|
|
self.learning_feedback_capture = None
|
|
return None
|
|
from openpilot.starpilot.flm.feedback_observation import card_capture_for
|
|
return card_capture_for(self, now_ns)
|
|
except Exception:
|
|
self.learning_feedback_capture = None
|
|
return None
|
|
|
|
def _invalidate_feedback_output(self, *, assignment=False):
|
|
capture = getattr(self, 'learning_feedback_capture', None)
|
|
if capture is not None:
|
|
try:
|
|
if assignment:
|
|
capture.begin_assignment()
|
|
else:
|
|
capture.invalidate()
|
|
except Exception:
|
|
self.learning_feedback_capture = None
|
|
|
|
def _clear_learning_evidence(self):
|
|
self._invalidate_feedback_output()
|
|
self.learning_feedback_publication = None
|
|
self.learning_stamp_observer = None
|
|
self.learning_applied_evidence = self.learning_previous_applied_evidence = None
|
|
self.learning_send_mono_ns = self.learning_send_committed_mono_ns = 0
|
|
self.learning_car_state_source_ns = 0
|
|
|
|
def record_learning_send_stamp(self, observed_ns):
|
|
# Metadata only; no permission, coordinate conversion, or extra clock sample.
|
|
self.learning_send_mono_ns = int(observed_ns)
|
|
|
|
def _observe_learning_send_stamp(self, observed_ns):
|
|
try:
|
|
observer = getattr(self, 'learning_stamp_observer', None)
|
|
if observer is not None:
|
|
observer(observed_ns)
|
|
except Exception:
|
|
self._clear_learning_evidence()
|
|
|
|
def publish_sendcan(self, frames, valid=True):
|
|
steering_authority = getattr(getattr(getattr(self, 'CI', None), 'CS', None), 'steering_authority', None)
|
|
monitored_steering = (getattr(getattr(self, 'CP', None), 'brand', None) == 'gm' and
|
|
steering_authority is not None and steering_authority.enabled)
|
|
observer=getattr(self,'learning_stamp_observer',None)
|
|
self.learning_send_mono_ns=self.learning_send_committed_mono_ns=0
|
|
if observer is None:
|
|
packet = can_list_to_can_capnp(frames, msgtype='sendcan', valid=valid)
|
|
else:
|
|
packet = can_list_to_can_capnp(frames, msgtype='sendcan', valid=valid,source_observer=self._observe_learning_send_stamp)
|
|
if getattr(self, 'volt_cc_selected', False):
|
|
# CAN producer/parser and pandad send age use BOOTTIME; Python controls/device envelopes use MONOTONIC.
|
|
builder = messaging.log_from_bytes(packet).as_builder()
|
|
builder.logMonoTime = int(getattr(self, 'volt_cc_now_boot_ns', 0))
|
|
packet = builder.to_bytes()
|
|
elif self.volt_startup_keepalive() or monitored_steering:
|
|
builder = messaging.log_from_bytes(packet).as_builder()
|
|
builder.logMonoTime = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
|
packet = builder.to_bytes()
|
|
if self.vehicle_startup.owner is not None:
|
|
with self.vehicle_startup.send_lock:
|
|
self.pm.send('sendcan', packet)
|
|
# Successful send acknowledgment is outside the sender lock, before any join.
|
|
self.vehicle_startup.sent(frames, valid=valid)
|
|
elif self.ioniq6_long_prearmed and self.ioniq6_keepalive is not None:
|
|
# The diagnostic sender and the normal controller share one PubSocket.
|
|
with self.ioniq6_keepalive.lock:
|
|
self.pm.send('sendcan', packet)
|
|
else:
|
|
self.pm.send('sendcan', packet)
|
|
self.learning_send_committed_mono_ns=self.learning_send_mono_ns
|
|
|
|
def volt_cc_control_current(self):
|
|
pair = clock_pair_ns()
|
|
if pair is None or REPLAY:
|
|
self.volt_cc_boot_offset_ns = None
|
|
return False
|
|
now_ns, boot_ns = pair
|
|
offset = boot_ns - now_ns
|
|
if self.volt_cc_boot_offset_ns is None or abs(offset - self.volt_cc_boot_offset_ns) > CLOCK_PAIR_MAX_SKEW_NS:
|
|
self.volt_cc_boot_offset_ns = offset
|
|
self.volt_cc_source_floor_ns = now_ns
|
|
return False
|
|
sm = self.sm
|
|
if not all(sm.seen[name] and sm.alive[name] and sm.valid[name] for name in ('carControl', 'deviceState')):
|
|
return False
|
|
drive_id = int(sm['deviceState'].startedMonoTime)
|
|
if not sm['deviceState'].started or drive_id <= 0:
|
|
return False
|
|
for name, limit in (('carControl', 150_000_000), ('deviceState', 2_000_000_000)):
|
|
source_ns, receipt_ns = int(sm.logMonoTime[name]), int(sm.recv_time[name] * 1e9)
|
|
if (not max(drive_id, self.volt_cc_source_floor_ns) < source_ns <= receipt_ns <= now_ns or
|
|
now_ns - source_ns > limit or now_ns - receipt_ns > limit):
|
|
return False
|
|
self.volt_cc_drive_id = drive_id
|
|
self.volt_cc_now_boot_ns = boot_ns
|
|
self.volt_cc_now_mono_ns = now_ns
|
|
return True
|
|
|
|
def volt_sdgm_startup_keepalive(self):
|
|
if getattr(getattr(self, 'CP', None), 'brand', None) != 'gm':
|
|
return False
|
|
return (is_volt_sdgm_profile(self.CP, longitudinal=True) and self.CP.safetyConfigs[0].safetyParam == 0x5007)
|
|
|
|
def volt_startup_keepalive(self):
|
|
if getattr(getattr(self, 'CP', None), 'brand', None) != 'gm':
|
|
return False
|
|
return (self.volt_sdgm_startup_keepalive() or
|
|
self.CP.alternativeExperience == 32 and not self.CP.flags & GMFlags.PEDAL_LONG and
|
|
(is_volt_ascm_longitudinal(self.CP) or
|
|
self.CP.carFingerprint == GM_CAR.CHEVROLET_VOLT_ASCM and self.CP.pcmCruise and
|
|
not self.CP.openpilotLongitudinalControl and gm_lane_centering_supported(self.CP)))
|
|
|
|
def ev6_startup_keepalive(self):
|
|
from opendbc.car.hyundai.ev6_startup import EV6Startup, required
|
|
from opendbc.car.hyundai.ecu_startup import Outcome
|
|
owner = getattr(getattr(self, 'vehicle_startup', None), 'owner', None)
|
|
return (isinstance(owner, EV6Startup) and owner.outcome in (Outcome.PENDING, Outcome.SENT_UNCONFIRMED) and
|
|
owner.ready and owner.published and owner.ci is self.CI and not owner.closed and
|
|
required(self.CP))
|
|
|
|
def classic_startup_keepalive(self):
|
|
from opendbc.car.hyundai.g90_startup import G90Startup
|
|
from opendbc.car.hyundai.classic_long_aol import qualified
|
|
from opendbc.car.hyundai.legacy_long_aol import qualified as legacy_qualified
|
|
from opendbc.car.hyundai.ecu_startup import Outcome
|
|
owner = getattr(getattr(self, 'vehicle_startup', None), 'owner', None)
|
|
return (isinstance(owner, G90Startup) and owner.outcome in (Outcome.PENDING, Outcome.SENT_UNCONFIRMED) and
|
|
owner.ready and owner.published and owner.ci is self.CI and not owner.closed and
|
|
(qualified(self.CP) or legacy_qualified(self.CP)))
|
|
|
|
def torque_ev_startup_keepalive(self):
|
|
from opendbc.car.hyundai.torque_ev_startup import TorqueEVStartup, required
|
|
from opendbc.car.hyundai.ecu_startup import Outcome
|
|
owner = getattr(getattr(self, 'vehicle_startup', None), 'owner', None)
|
|
return (isinstance(owner, TorqueEVStartup) and owner.outcome in (Outcome.PENDING, Outcome.SENT_UNCONFIRMED) and
|
|
owner.ready and owner.published and owner.ci is self.CI and not owner.closed and
|
|
required(self.CP))
|
|
|
|
def mixed_startup_keepalive(self):
|
|
from opendbc.car.hyundai.blended_longitudinal import BlendedStartup
|
|
owner = getattr(getattr(self, 'vehicle_startup', None), 'owner', None)
|
|
from opendbc.car.hyundai.ioniq5pe_startup import PEPendingStartup
|
|
return (isinstance(owner, (BlendedStartup, PEPendingStartup)) and owner.owner.published and not owner.closed and
|
|
(owner.pending_target or owner.owner.active()))
|
|
|
|
def prepare_pe_restoration(self):
|
|
from opendbc.car.hyundai.ioniq5pe_startup import PEPendingStartup
|
|
owner = getattr(getattr(self, 'vehicle_startup', None), 'owner', None)
|
|
if not isinstance(owner, PEPendingStartup) or not (owner.session_attempted or owner.takeover_attempted) or owner.restored:
|
|
return True
|
|
owner.quiesce()
|
|
|
|
|
|
self.params.put_bool('ControlsReady', False)
|
|
deadline = time.monotonic() + 1.
|
|
while time.monotonic() < deadline:
|
|
if self.startup_diagnostic_admission():
|
|
return True
|
|
time.sleep(.01)
|
|
return False
|
|
|
|
def controls_update(self, CS: car.CarState, CC: car.CarControl, *, initialize_only=False):
|
|
"""control update loop, driven by carControl"""
|
|
|
|
if initialize_only:
|
|
ev6_startup = self.ev6_startup_keepalive()
|
|
classic_startup = self.classic_startup_keepalive() or self.torque_ev_startup_keepalive()
|
|
mixed_startup = self.mixed_startup_keepalive()
|
|
now_ns = time.monotonic_ns()
|
|
source_ns = int(self.sm.logMonoTime['carControl'])
|
|
receipt_ns = int(self.sm.recv_time['carControl'] * 1e9)
|
|
# Disabled torque/accel are commands; inactive curvature is only telemetry.
|
|
if (not (mixed_startup or ev6_startup or classic_startup or
|
|
(self.aol_replay and native_bootstrap_supported(self.CP) and self.vehicle_startup.owner is None)) or
|
|
not CS.canValid or CS.canTimeout or
|
|
not (self.sm.seen['carControl'] and self.sm.valid['carControl'] and self.sm.alive['carControl']) or
|
|
not (0 < source_ns <= now_ns and now_ns - source_ns <= 150_000_000 and
|
|
0 < receipt_ns <= now_ns and now_ns - receipt_ns <= 150_000_000) or
|
|
CC.enabled or CC.latActive or CC.longActive or CC.actuators.torque != 0.0 or CC.actuators.accel != 0.0):
|
|
return
|
|
if (ev6_startup or classic_startup) and not self.vehicle_startup.owner.prepared_for(self.CP):
|
|
return
|
|
|
|
if getattr(self, 'volt_cc_selected', False):
|
|
if not self.volt_cc_control_current():
|
|
self.publish_sendcan([], valid=False)
|
|
return
|
|
physical = getattr(self.CI.CS, 'volt_cc_physical', None)
|
|
if physical is None or any(source - self.volt_cc_boot_offset_ns <= max(self.volt_cc_drive_id, self.volt_cc_source_floor_ns)
|
|
for source in (physical.observed_ns, *physical.source_ns)):
|
|
self.publish_sendcan([], valid=False)
|
|
return
|
|
if physical.button_credit_ns - self.volt_cc_boot_offset_ns <= max(self.volt_cc_drive_id, self.volt_cc_source_floor_ns):
|
|
self.CI.CC.volt_cc_consumed_source_ns = physical.button_credit_ns
|
|
self.CI.CC.volt_cc_metric = self.is_metric
|
|
|
|
if getattr(self.CI.CC, "bolt_cc_profile", False):
|
|
self.CI.CC.bolt_cc_metric = self.is_metric
|
|
|
|
if (initialize_only and self.mixed_startup_keepalive() and self.vehicle_startup.owner.pending_target):
|
|
# Fresh disabled controls alone are insufficient: finish the host's
|
|
# selfdriveInitializing phase before suppressing factory ADAS output.
|
|
return
|
|
|
|
if not self.ci_initialized:
|
|
# Keep stock ECU output intact until CAN and controls are ready.
|
|
if (not CS.canValid or CS.canTimeout or not self.sm.valid['carControl'] or
|
|
not self.sm.all_alive(['carControl'])):
|
|
self.publish_sendcan([], valid=False)
|
|
return
|
|
mixed = self.vehicle_startup.owner if self.mixed_startup_keepalive() else None
|
|
from opendbc.car.hyundai.g90_startup import G90Startup
|
|
from opendbc.car.hyundai.torque_ev_startup import TorqueEVStartup
|
|
from opendbc.car.hyundai.ev6_startup import EV6Startup
|
|
from opendbc.car.hyundai.ecu_startup import Outcome
|
|
classic = self.vehicle_startup.owner
|
|
if isinstance(classic, (G90Startup, TorqueEVStartup, EV6Startup)) and classic.outcome is Outcome.PENDING:
|
|
now = time.monotonic_ns()
|
|
source = int(self.sm.logMonoTime['carControl'])
|
|
receipt = int(self.sm.recv_time['carControl'] * 1e9)
|
|
current = bool(self.sm.seen['carControl'] and self.sm.alive['carControl'] and self.sm.valid['carControl'] and
|
|
0 < source <= now and now - source <= 150_000_000 and
|
|
0 < receipt <= now and now - receipt <= 150_000_000)
|
|
inactive = not (CC.enabled or CC.latActive or CC.longActive or CC.actuators.torque != 0.0 or CC.actuators.accel != 0.0)
|
|
sources = classic.sources_current(self.CI, time.clock_gettime_ns(time.CLOCK_BOOTTIME))
|
|
if not current or not inactive or not sources or CS.accFaulted or CS.cruiseState.enabled or not self.startup_diagnostic_admission():
|
|
self.publish_sendcan([], valid=False)
|
|
return
|
|
classic.commit_pending(self.CI, sources_current=sources, control_current=current)
|
|
elif mixed is not None and mixed.pending_target:
|
|
now = time.monotonic_ns()
|
|
current = (self.sm.seen['carControl'] and self.sm.alive['carControl'] and self.sm.valid['carControl'] and
|
|
0 < int(self.sm.logMonoTime['carControl']) <= now and now - int(self.sm.logMonoTime['carControl']) <= 150_000_000 and
|
|
0 < int(self.sm.recv_time['carControl'] * 1e9) <= now and now - int(self.sm.recv_time['carControl'] * 1e9) <= 150_000_000)
|
|
inactive = not (CC.enabled or CC.latActive or CC.longActive or CC.actuators.torque != 0.0 or CC.actuators.accel != 0.0)
|
|
sources = mixed.sources_current(self.CI, time.clock_gettime_ns(time.CLOCK_BOOTTIME))
|
|
if not current or not inactive or not sources:
|
|
self.publish_sendcan([], valid=False)
|
|
return
|
|
mixed.commit_pending(self.CI, sources_current=sources, control_current=current)
|
|
elif getattr(self, 'ioniq6_long_pending', False):
|
|
# Panda is still in its diagnostic safety mode here. Take over only
|
|
# after the control producer is ready, then request Hyundai safety.
|
|
if not self.ioniq6_panda_diagnostic_ready():
|
|
self.publish_sendcan([], valid=False)
|
|
return
|
|
handoff_started = time.monotonic()
|
|
if handoff_started - self.ioniq6_last_preflight_at < 0.5:
|
|
self.publish_sendcan([], valid=False)
|
|
return
|
|
self.ioniq6_last_preflight_at = handoff_started
|
|
try:
|
|
self.ioniq6_handoff_result = confirm_ioniq6_prepared_takeover(*self.can_callbacks)
|
|
except Exception:
|
|
cloudlog.exception("Ioniq 6 takeover transaction failed")
|
|
self.ioniq6_handoff_result = None
|
|
result = self.ioniq6_handoff_result
|
|
cloudlog.event('ioniq6_long_handoff',
|
|
outcome=result.outcome.name if result is not None else 'EXCEPTION',
|
|
reason=result.reason if result is not None else 'transaction_exception',
|
|
elapsed_s=round(time.monotonic() - handoff_started, 3))
|
|
if result is not None and result.outcome is HandoffOutcome.NOT_READY:
|
|
self.publish_sendcan([], valid=False)
|
|
return
|
|
self.ioniq6_long_pending = False
|
|
if result is None or result.outcome is not HandoffOutcome.CONFIRMED:
|
|
self.ioniq6_long_lost = True
|
|
self.ci_initialized = True
|
|
CS.canValid = False
|
|
self.publish_sendcan([], valid=False)
|
|
return
|
|
self.ioniq6_long_prearmed = True
|
|
# The transaction consumed Card's CAN cursor. Rewarm the host parser
|
|
# from post-handoff independent inputs before its first controller TX.
|
|
self.ioniq6_host_warmed = False
|
|
self.ioniq6_handoff_source_floor_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
|
self.ioniq6_restore_callbacks = self.can_callbacks
|
|
elif self.vehicle_startup.owner is None:
|
|
self.CI.init(self.CP, *self.can_callbacks)
|
|
# signal pandad to switch to car safety mode
|
|
self.params.put_bool("ControlsReady", True)
|
|
self.ci_initialized = True
|
|
|
|
if initialize_only:
|
|
if (ev6_startup or classic_startup) and any(ps.faults for ps in self.sm['pandaStates']):
|
|
return
|
|
if mixed_startup:
|
|
owner = self.vehicle_startup.owner
|
|
now_boot_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
|
if (not owner.pending_target and self.startup_panda_configured() and
|
|
owner.sources_current(self.CI, now_boot_ns)):
|
|
inactive = car.CarControl.new_message().as_reader()
|
|
self._invalidate_feedback_output(assignment=True)
|
|
self.last_actuators_output, can_sends = self.CI.apply(inactive, now_boot_ns)
|
|
self.publish_sendcan(can_sends, valid=CS.canValid)
|
|
return
|
|
if (((isinstance(self.CI, GMInterface) and self.volt_startup_keepalive()) or ev6_startup or classic_startup) and
|
|
self.startup_panda_configured(inactive_keepalive=True)):
|
|
inactive = car.CarControl.new_message().as_reader()
|
|
now_boot_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
|
if ev6_startup or classic_startup:
|
|
now_mono_ns = time.monotonic_ns()
|
|
source_ns = int(self.sm.logMonoTime['carControl'])
|
|
receipt_ns = int(self.sm.recv_time['carControl'] * 1e9)
|
|
current = bool(0 < source_ns <= now_mono_ns and now_mono_ns - source_ns <= 150_000_000 and
|
|
0 < receipt_ns <= now_mono_ns and now_mono_ns - receipt_ns <= 150_000_000)
|
|
if not self.vehicle_startup.before_control(configured=True, ci=self.CI, now_ns=now_boot_ns, control_current=current):
|
|
self.publish_sendcan([], valid=False)
|
|
return
|
|
self._invalidate_feedback_output(assignment=True)
|
|
self.last_actuators_output, can_sends = self.CI.apply(inactive, now_boot_ns)
|
|
else:
|
|
self._invalidate_feedback_output(assignment=True)
|
|
self.last_actuators_output, can_sends = cast(GMInterface, self.CI).apply(inactive, now_boot_ns, startup_keepalive=True)
|
|
self.publish_sendcan(can_sends, valid=CS.canValid)
|
|
return
|
|
|
|
self.vehicle_startup.check()
|
|
now_ns = time.monotonic_ns()
|
|
control_current = bool(self.sm.seen['carControl'] and self.sm.alive['carControl'] and self.sm.valid['carControl'] and
|
|
0 < int(self.sm.logMonoTime['carControl']) <= now_ns and
|
|
now_ns - int(self.sm.logMonoTime['carControl']) <= 150_000_000 and
|
|
0 < int(self.sm.recv_time['carControl'] * 1e9) <= now_ns and
|
|
now_ns - int(self.sm.recv_time['carControl'] * 1e9) <= 150_000_000)
|
|
if self.vehicle_startup.owner is not None and not self.vehicle_startup.before_control(
|
|
configured=self.startup_panda_configured(), ci=self.CI,
|
|
now_ns=time.clock_gettime_ns(time.CLOCK_BOOTTIME), control_current=control_current):
|
|
self.publish_sendcan([], valid=False)
|
|
return
|
|
|
|
if getattr(self, 'ioniq6_long_selected', False) and not self.ioniq6_long_prearmed:
|
|
CS.canValid = False
|
|
self.publish_sendcan([], valid=False)
|
|
return
|
|
|
|
if self.ioniq6_long_prearmed:
|
|
panda_matches = self.ioniq6_panda_matches()
|
|
if not panda_matches and self.ioniq6_panda_armed:
|
|
self.ioniq6_long_lost = True
|
|
self.ioniq6_panda_armed |= panda_matches
|
|
if not panda_matches or not self.ioniq6_host_warmed or self.ioniq6_long_lost or not CS.canValid:
|
|
self.publish_sendcan([], valid=False)
|
|
return
|
|
|
|
if self.sm.valid['carControl'] and self.sm.all_alive(['carControl']):
|
|
if self.mixed_startup_keepalive() and not self.vehicle_startup.owner.handed_off:
|
|
# The first admitted replacement cycle is always neutral; the actual
|
|
# send receipt, not reaching apply(), transfers diagnostic continuity.
|
|
CC = car.CarControl.new_message().as_reader()
|
|
if self.ioniq6_long_prearmed and self.ioniq6_keepalive is not None:
|
|
self.ioniq6_keepalive.stop()
|
|
if self.ioniq6_long_lost:
|
|
self.publish_sendcan([], valid=False)
|
|
return
|
|
steering_authority = getattr(self.CI.CS, 'steering_authority', None)
|
|
monitored_steering = self.CP.brand == 'gm' and steering_authority is not None and steering_authority.enabled
|
|
# send car controls over can
|
|
now_nanos = (self.volt_cc_now_boot_ns if getattr(self, 'volt_cc_selected', False) else
|
|
self.can_log_mono_time if REPLAY else
|
|
time.clock_gettime_ns(time.CLOCK_BOOTTIME) if self.volt_startup_keepalive() or monitored_steering else
|
|
int(time.monotonic() * 1e9))
|
|
learning_snapshot = None
|
|
try:
|
|
from openpilot.starpilot.flm.evidence_hooks import begin_applied_frame, applied_snapshot
|
|
begin_applied_frame(self)
|
|
learning_snapshot = applied_snapshot
|
|
except Exception:
|
|
self._clear_learning_evidence()
|
|
self._invalidate_feedback_output(assignment=True)
|
|
if self.CP.brand == "subaru":
|
|
from openpilot.starpilot.car.subaru.software_cruise_caller import update as update_subaru_software_cruise
|
|
update_subaru_software_cruise(self, CS, CC, now_nanos)
|
|
from openpilot.starpilot.car.subaru.sng_caller import update as update_subaru_sng
|
|
update_subaru_sng(self, CS, CC, now_nanos)
|
|
from openpilot.starpilot.car.subaru.stop_start_caller import update as update_subaru_startup
|
|
update_subaru_startup(self, now_nanos)
|
|
self.last_actuators_output, can_sends = self.CI.apply(CC, now_nanos)
|
|
self.publish_sendcan(can_sends, valid=CS.canValid)
|
|
if learning_snapshot is not None and self.learning_send_committed_mono_ns:
|
|
try:
|
|
self.learning_applied_evidence = learning_snapshot(self, CC, CS, self.last_actuators_output,
|
|
apply_ns=self.learning_send_committed_mono_ns, state_ns=self.learning_car_state_source_ns)
|
|
capture = getattr(self, 'learning_feedback_capture', None)
|
|
if capture is not None:
|
|
try:
|
|
capture.committed(self.last_actuators_output, self.learning_applied_evidence)
|
|
except Exception:
|
|
self.learning_feedback_capture = None # Keep the original applied evidence.
|
|
except Exception:
|
|
self._clear_learning_evidence()
|
|
|
|
self.CC_prev = CC
|
|
|
|
def step(self):
|
|
CS, RD = self.state_update(defer_radar=True)
|
|
|
|
if self.vehicle_startup.owner is not None:
|
|
self.vehicle_startup.maintain(configured=self.startup_panda_configured())
|
|
self.state_publish(CS, RD)
|
|
|
|
initialized = (not any(e.name == EventName.selfdriveInitializing for e in self.sm['onroadEvents']) and
|
|
self.sm.seen['onroadEvents'])
|
|
if not self.CP.passive and initialized:
|
|
self.controls_update(CS, self.sm['carControl'])
|
|
elif ((not self.ci_initialized or self.volt_startup_keepalive() or self.ev6_startup_keepalive() or
|
|
self.classic_startup_keepalive() or self.torque_ev_startup_keepalive() or self.mixed_startup_keepalive()) and
|
|
self.sm.seen['onroadEvents'] and
|
|
any(e.name == EventName.selfdriveInitializing for e in self.sm['onroadEvents']) and
|
|
((self.aol_replay and native_bootstrap_supported(self.CP)) or self.ev6_startup_keepalive() or
|
|
self.classic_startup_keepalive() or self.torque_ev_startup_keepalive() or self.mixed_startup_keepalive())):
|
|
self.controls_update(CS, self.sm['carControl'], initialize_only=True)
|
|
|
|
radar_can = self._deferred_radar_can
|
|
self._deferred_radar_can = None
|
|
self.radar_publish(self.radar_update(radar_can))
|
|
self.initialized_prev = initialized
|
|
self.CS_prev = CS
|
|
|
|
def configure_conditional_settings_worker(self):
|
|
self.conditional_settings_worker = None
|
|
if not REPLAY and (getattr(self, 'manual_settings_owner', None) is not None or
|
|
getattr(self, 'switchback_settings_owner', None) is not None):
|
|
worker = CardSettingsWorker(self.params)
|
|
self.conditional_settings_worker = worker
|
|
if getattr(self, 'manual_settings_owner', None) is not None:
|
|
self.manual_settings_owner = worker.view
|
|
if getattr(self, 'switchback_settings_owner', None) is not None:
|
|
self.switchback_settings_owner = worker.view
|
|
|
|
def params_thread(self, evt):
|
|
next_cruise_read = 0.0
|
|
while not evt.is_set():
|
|
settings_worker = getattr(self, 'conditional_settings_worker', None)
|
|
if settings_worker is not None:
|
|
settings_worker.refresh(time.monotonic_ns())
|
|
self.is_metric = self.params.get_bool("IsMetric")
|
|
self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl
|
|
self.initial_cruise_speed = self.params.get("InitialCruiseSpeedKph", return_default=True)
|
|
if self.aol_card_intent is not None:
|
|
self.aol_card_intent.settings = read_settings(self.params)
|
|
now = time.monotonic()
|
|
if now >= next_cruise_read:
|
|
self.slc_requested_configuration = self.read_slc_configuration()
|
|
self.v_cruise_helper.intervals = read_cruise_intervals(self.params, self.CP)
|
|
pitch_input = getattr(self.CI.CC, 'long_pitch_input', None)
|
|
if pitch_input is not None:
|
|
self.CI.CC.long_pitch = pitch_input.update(time.monotonic_ns())
|
|
next_cruise_read = now + 1.0
|
|
time.sleep(0.1)
|
|
|
|
def card_thread(self):
|
|
self.configure_conditional_settings_worker()
|
|
e = threading.Event()
|
|
t = threading.Thread(target=self.params_thread, args=(e, ))
|
|
try:
|
|
t.start()
|
|
while True:
|
|
self.vehicle_startup.check()
|
|
self.step()
|
|
self.rk.monitor_time()
|
|
finally:
|
|
e.set()
|
|
t.join()
|
|
if self.ioniq6_keepalive is not None:
|
|
self.ioniq6_long_lost = True
|
|
self.ioniq6_keepalive.stop()
|
|
|
|
|
|
def main():
|
|
config_realtime_process(4, Priority.CTRL_HIGH)
|
|
# Retain the partially initialized object so its diagnostic worker can be
|
|
# stopped even if construction fails after a confirmed ECU handoff.
|
|
car = Car.__new__(Car)
|
|
try:
|
|
Car.__init__(car)
|
|
car.card_thread()
|
|
finally:
|
|
if getattr(car, 'vehicle_startup', None) is not None:
|
|
try:
|
|
car.prepare_pe_restoration()
|
|
finally:
|
|
car.vehicle_startup.close()
|
|
if getattr(car, 'ioniq6_keepalive', None) is not None:
|
|
car.ioniq6_long_lost = True
|
|
car.ioniq6_keepalive.stop()
|
|
if getattr(car, 'ioniq6_long_prearmed', False) and not getattr(car, 'ioniq6_restore_attempted', False):
|
|
# This Card is no longer an output owner, even if it had published a
|
|
# qualified CP. Restore stock ECU traffic before a later Card starts.
|
|
# Do not mutate the already published CP or claim stock on failure.
|
|
callbacks = getattr(car, 'ioniq6_restore_callbacks', None)
|
|
if callbacks is not None:
|
|
car.ioniq6_restore_attempted = True
|
|
try:
|
|
restored = restore_ioniq6_adas(*callbacks)
|
|
except Exception:
|
|
restored = False
|
|
if not restored:
|
|
cloudlog.error("Ioniq 6 Card exited; stock SCC restoration unverified")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|