This commit is contained in:
ajouatom
2026-02-14 17:16:35 +09:00
parent 8e4b088424
commit c7e5ea0ad0
148 changed files with 5011 additions and 7046 deletions

View File

@@ -18,43 +18,6 @@ RUN /tmp/tools/install_ubuntu_dependencies.sh && \
cd /usr/lib/gcc/arm-none-eabi/* && \
rm -rf arm/ thumb/nofp thumb/v6* thumb/v8* thumb/v7+fp thumb/v7-r+fp.sp
# Add OpenCL
RUN apt-get update && apt-get install -y --no-install-recommends \
apt-utils \
alien \
unzip \
tar \
curl \
xz-utils \
dbus \
gcc-arm-none-eabi \
tmux \
vim \
libx11-6 \
wget \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /tmp/opencl-driver-intel && \
cd /tmp/opencl-driver-intel && \
wget https://github.com/intel/llvm/releases/download/2024-WW14/oclcpuexp-2024.17.3.0.09_rel.tar.gz && \
wget https://github.com/oneapi-src/oneTBB/releases/download/v2021.12.0/oneapi-tbb-2021.12.0-lin.tgz && \
mkdir -p /opt/intel/oclcpuexp_2024.17.3.0.09_rel && \
cd /opt/intel/oclcpuexp_2024.17.3.0.09_rel && \
tar -zxvf /tmp/opencl-driver-intel/oclcpuexp-2024.17.3.0.09_rel.tar.gz && \
mkdir -p /etc/OpenCL/vendors && \
echo /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64/libintelocl.so > /etc/OpenCL/vendors/intel_expcpu.icd && \
cd /opt/intel && \
tar -zxvf /tmp/opencl-driver-intel/oneapi-tbb-2021.12.0-lin.tgz && \
ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbb.so /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \
ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbbmalloc.so /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \
ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbb.so.12 /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \
ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbbmalloc.so.2 /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \
mkdir -p /etc/ld.so.conf.d && \
echo /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 > /etc/ld.so.conf.d/libintelopenclexp.conf && \
ldconfig -f /etc/ld.so.conf.d/libintelopenclexp.conf && \
cd / && \
rm -rf /tmp/opencl-driver-intel
ENV NVIDIA_VISIBLE_DEVICES=all
ENV NVIDIA_DRIVER_CAPABILITIES=graphics,utility,compute
ENV QTWEBENGINE_DISABLE_SANDBOX=1

16
Jenkinsfile vendored
View File

@@ -216,24 +216,16 @@ node {
step("test manager", "pytest system/manager/test/test_manager.py"),
])
},
'loopback': {
deviceStage("loopback", "tizi-loopback", ["UNSAFE=1"], [
step("build openpilot", "cd system/manager && ./build.py"),
step("test pandad loopback", "pytest selfdrive/pandad/tests/test_pandad_loopback.py"),
])
},
'camerad OX03C10': {
deviceStage("OX03C10", "tizi-ox03c10", ["UNSAFE=1"], [
step("build", "cd system/manager && ./build.py"),
step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 60]),
step("test exposure", "pytest system/camerad/test/test_exposure.py"),
step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 90]),
])
},
'camerad OS04C10': {
deviceStage("OS04C10", "tici-os04c10", ["UNSAFE=1"], [
step("build", "cd system/manager && ./build.py"),
step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 60]),
step("test exposure", "pytest system/camerad/test/test_exposure.py"),
step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 90]),
])
},
'sensord': {
@@ -251,11 +243,9 @@ node {
'tizi': {
deviceStage("tizi", "tizi", ["UNSAFE=1"], [
step("build openpilot", "cd system/manager && ./build.py"),
step("test pandad loopback", "SINGLE_PANDA=1 pytest selfdrive/pandad/tests/test_pandad_loopback.py"),
step("test pandad loopback", "pytest selfdrive/pandad/tests/test_pandad_loopback.py"),
step("test pandad spi", "pytest selfdrive/pandad/tests/test_pandad_spi.py"),
step("test amp", "pytest system/hardware/tici/tests/test_amplifier.py"),
// TODO: enable once new AGNOS is available
// step("test esim", "pytest system/hardware/tici/tests/test_esim.py"),
step("test qcomgpsd", "pytest system/qcomgpsd/tests/test_qcomgpsd.py", [diffPaths: ["system/qcomgpsd/"]]),
])
},

View File

@@ -211,10 +211,8 @@ SConscript(['third_party/SConscript'])
SConscript(['selfdrive/SConscript'])
if Dir('#tools/cabana/').exists() and GetOption('extras'):
SConscript(['tools/replay/SConscript'])
if arch != "larch64":
SConscript(['tools/cabana/SConscript'])
if Dir('#tools/cabana/').exists() and arch != "larch64":
SConscript(['tools/cabana/SConscript'])
env.CompilationDatabase('compile_commands.json')

View File

@@ -1185,7 +1185,8 @@ struct ModelDataV2 {
laneChangeProb @15 :Float32;
desireLog @16 : Text;
modelTurnSpeed @17 :Float32;
laneChangeAvailableLeft @18 :Bool;
laneChangeAvailableRight @19 :Bool;
# deprecated
brakeDisengageProbDEPRECATED @2 :Float32;
@@ -2296,9 +2297,9 @@ struct DriverMonitoringState @0xb83cda094a1da284 {
isActiveMode @16 :Bool;
isRHD @4 :Bool;
uncertainCount @19 :UInt32;
phoneProbOffset @20 :Float32;
phoneProbValidCount @21 :UInt32;
phoneProbOffsetDEPRECATED @20 :Float32;
phoneProbValidCountDEPRECATED @21 :UInt32;
isPreviewDEPRECATED @15 :Bool;
rhdCheckedDEPRECATED @5 :Bool;
eventsDEPRECATED @0 :List(Car.OnroadEventDEPRECATED);

View File

@@ -5,7 +5,7 @@ import numbers
import random
import threading
import time
from parameterized import parameterized
from openpilot.common.parameterized import parameterized
import pytest
from cereal import log, car

View File

@@ -1,7 +1,7 @@
import os
import tempfile
from typing import Dict
from parameterized import parameterized
from openpilot.common.parameterized import parameterized
import cereal.services as services
from cereal.services import SERVICE_LIST

47
common/parameterized.py Normal file
View File

@@ -0,0 +1,47 @@
import sys
import pytest
import inspect
class parameterized:
@staticmethod
def expand(cases):
cases = list(cases)
if not cases:
return lambda func: pytest.mark.skip("no parameterized cases")(func)
def decorator(func):
params = [p for p in inspect.signature(func).parameters if p != 'self']
normalized = [c if isinstance(c, tuple) else (c,) for c in cases]
# Infer arg count from first case so extra params (e.g. from @given) are left untouched
expand_params = params[: len(normalized[0])]
if len(expand_params) == 1:
return pytest.mark.parametrize(expand_params[0], [c[0] for c in normalized])(func)
return pytest.mark.parametrize(', '.join(expand_params), normalized)(func)
return decorator
def parameterized_class(attrs, input_list=None):
if isinstance(attrs, list) and (not attrs or isinstance(attrs[0], dict)):
params_list = attrs
else:
assert input_list is not None
attr_names = (attrs,) if isinstance(attrs, str) else tuple(attrs)
params_list = [dict(zip(attr_names, v if isinstance(v, (tuple, list)) else (v,), strict=False)) for v in input_list]
def decorator(cls):
globs = sys._getframe(1).f_globals
for i, params in enumerate(params_list):
name = f"{cls.__name__}_{i}"
new_cls = type(name, (cls,), dict(params))
new_cls.__module__ = cls.__module__
new_cls.__test__ = True # override inherited False so pytest collects this subclass
globs[name] = new_cls
# Don't collect the un-parametrised base, but return it so outer decorators
# (e.g. @pytest.mark.skip) land on it and propagate to subclasses via MRO.
cls.__test__ = False
return cls
return decorator

View File

@@ -146,24 +146,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"GMapKey", {PERSISTENT, STRING}},
{"SearchInput", {PERSISTENT, INT}},
{"CarSelected3", {PERSISTENT, STRING}},
{"CarSelected3", {PERSISTENT, STRING, "MOCK"}},
{"SupportedCars", {PERSISTENT, STRING}},
{"SupportedCars_gm", {PERSISTENT, STRING}},
{"ShowDebugUI", {PERSISTENT, INT, "0"}},
{"ShowTpms", {PERSISTENT, INT, "1"}},
{"ShowDateTime", {PERSISTENT, INT, "1"}},
{"ShowPathEnd", {PERSISTENT, INT, "1"}},
{"ShowCustomBrightness", {PERSISTENT, INT, "100"}},
{"ShowLaneInfo", {PERSISTENT, INT, "1"}},
{"ShowRadarInfo", {PERSISTENT, INT, "1"}},
{"ShowDeviceState", {PERSISTENT, INT, "1"}},
{"ShowRouteInfo", {PERSISTENT, INT, "1"}},
{"ShowPathMode", {PERSISTENT, INT, "9"}},
{"ShowPathColor", {PERSISTENT, INT, "13"}},
{"ShowPathColorCruiseOff", {PERSISTENT, INT, "19"}},
{"ShowPathModeLane", {PERSISTENT, INT, "14"}},
{"ShowPathColorLane", {PERSISTENT, INT, "13"}},
{"ShowPlotMode", {PERSISTENT, INT, "0"}},
{"RecordRoadCam", {PERSISTENT, INT, "0"}},
{"HDPuse", {PERSISTENT, INT, "0"}},
@@ -227,6 +212,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"SteerActuatorDelay", {PERSISTENT, INT, "0"}},
{"LatSmoothSec", {PERSISTENT, INT, "13"}},
{"LatSuspendAngleDeg", {PERSISTENT, INT, "300"}},
{"CruiseOnDist", {PERSISTENT, INT, "400"}},
{"CruiseMaxVals0", {PERSISTENT, INT, "160"}},

View File

@@ -9,7 +9,7 @@ class CANPacker:
self.dbc = DBC(dbc_name)
self.counters: dict[int, int] = {}
def pack(self, address: int, values: dict[str, float]) -> bytearray:
def pack(self, address: int, values: dict[str, float], rx_counter: int | None = None) -> bytearray:
msg = self.dbc.addr_to_msg.get(address)
if msg is None:
carlog.error(f"msg not found for {address=}")
@@ -30,8 +30,8 @@ class CANPacker:
counter_set = True
sig_counter = next((s for s in msg.sigs.values() if s.type == SignalType.COUNTER or s.name == "COUNTER"), None)
if sig_counter and not counter_set:
if address not in self.counters:
self.counters[address] = 0
if address not in self.counters:
self.counters[address] = 0 if rx_counter is None else (int(rx_counter) + 1) % (1 << sig_counter.size)
set_value(dat, sig_counter, self.counters[address])
self.counters[address] = (self.counters[address] + 1) % (1 << sig_counter.size)
sig_checksum = next((s for s in msg.sigs.values() if s.type > SignalType.COUNTER), None)
@@ -40,7 +40,7 @@ class CANPacker:
set_value(dat, sig_checksum, checksum)
return dat
def make_can_msg(self, name_or_addr, bus: int, values: dict[str, float]):
def make_can_msg(self, name_or_addr, bus: int, values: dict[str, float], rx_counter: int | None = None):
if isinstance(name_or_addr, int):
addr = name_or_addr
else:
@@ -49,7 +49,7 @@ class CANPacker:
carlog.error(f"msg not found for {name_or_addr=}")
return 0, b'', bus
addr = msg.address
dat = self.pack(addr, values)
dat = self.pack(addr, values, rx_counter = rx_counter)
if len(dat) == 0:
return 0, b'', bus
return addr, bytes(dat), bus

View File

@@ -351,8 +351,10 @@ class CarController(CarControllerBase):
can_sends.extend(hyundaicanfd.create_fca_warning_light(self.CP, self.packer, self.CAN, self.frame))
if self.frame % 2 == 0:
if self.CP.flags & HyundaiFlags.CAMERA_SCC.value:
can_sends.append(hyundaicanfd.create_acc_control_scc2(self.packer, self.CAN, CC.enabled, self.accel_last, accel, stopping, CC.cruiseControl.override,
set_speed_in_units, hud_control, self.hyundai_jerk, CS))
msg = hyundaicanfd.create_acc_control_scc2(self.packer, self.CAN, CC.enabled, self.accel_last, accel, stopping, CC.cruiseControl.override,
set_speed_in_units, hud_control, self.hyundai_jerk, CS)
if msg is not None:
can_sends.append(msg)
can_sends.extend(hyundaicanfd.create_tcs_messages(self.packer, self.CAN, CS)) # for sorento SCC radar...
else:
can_sends.append(hyundaicanfd.create_acc_control(self.packer, self.CAN, CC.enabled, self.accel_last, accel, stopping, CC.cruiseControl.override,
@@ -462,7 +464,7 @@ class CarController(CarControllerBase):
if (self.frame - self.last_button_frame) * DT_CTRL > 0.1:
print("cruiseControl.cancel222222")
if self.CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS:
#can_sends.append(hyundaicanfd.create_acc_cancel(self.packer, self.CP, self.CAN, CS.cruise_info))
#can_sends.append(hyundaicanfd.create_acc_cancel(self.packer, self.CP, self.CAN, CS.scc_control))
if self.cruise_buttons_msg_values is not None:
can_sends.append(hyundaicanfd.alt_cruise_buttons(self.packer, self.CP, self.CAN, Buttons.CANCEL, self.cruise_buttons_msg_values, self.cruise_buttons_msg_cnt))

View File

@@ -76,24 +76,32 @@ class CarState(CarStateBase):
self.is_metric = False
self.buttons_counter = 0
self.cruise_info = {}
self.lfa_info = {}
self.lfa_alt_info = {}
self.lfahda_cluster_info = None
self.adrv_info_161 = None
self.adrv_info_200 = None
self.adrv_info_1ea = None
self.adrv_info_160 = None
self.adrv_info_162 = None
self.hda_info_4a3 = None
self.new_msg_4b4 = None
self.tcs_info_373 = None
self.mdps_info = {}
self.steer_touch_info = {}
# for generic CAN parsing
self.fca11 = None
self.scc11 = None
self.scc12 = None
self.scc13 = None
self.scc14 = None
self.lkas11 = None
self.clu11 = None
# for CANFD parsing
self.scc_control = None
self.lfa = None
self.lfa_alt = None
self.lfahda_cluster = None
self.adrv_0x161 = None
self.adrv_0x200 = None
self.adrv_0x1ea = None
self.adrv_0x160 = None
self.ccnc_0x162 = None
self.hda_info_4a3 = None
self.tcs = None
self.mdps = None
self.steer_touch_2af = None
self.cruise_buttons_msg = None
self.msg_0x362 = None
self.msg_0x2a4 = None
self.cam_0x362 = None
self.cam_0x2a4 = None
# On some cars, CLU15->CF_Clu_VehicleSpeed can oscillate faster than the dash updates. Sample at 5 Hz
self.cluster_speed = 0
@@ -128,67 +136,115 @@ class CarState(CarStateBase):
if self.CP.openpilotLongitudinalControl and not (self.CP.flags & HyundaiFlags.CANFD_CAMERA_SCC):
ecu_disabled = True
if ecu_disabled:
self.SCC11 = self.SCC12 = self.SCC13 = self.SCC14 = self.FCA11 = False
else:
bus_cruise = 2 if self.CP.flags & HyundaiFlags.CAMERA_SCC else 0
self.SCC11 = True if 1056 in fingerprints[bus_cruise] else False
self.SCC12 = True if 1057 in fingerprints[bus_cruise] else False
self.SCC13 = True if 1290 in fingerprints[bus_cruise] else False
self.SCC14 = True if 905 in fingerprints[bus_cruise] else False
self.FCA11 = False
self.FCA11_bus = Bus.cam
self.HAS_LFA_BUTTON = True if 913 in fingerprints[0] else False
self.CRUISE_BUTTON_ALT = True if 1007 in fingerprints[0] else False
cam_bus = CanBus(CP).CAM
pt_bus = CanBus(CP).ECAN
alt_bus = CanBus(CP).ACAN
self.CCNC_0x161 = True if 0x161 in fingerprints[cam_bus] else False
self.CCNC_0x162 = True if 0x162 in fingerprints[cam_bus] else False
self.ADRV_0x200 = True if 0x200 in fingerprints[cam_bus] else False
self.ADRV_0x1ea = True if 0x1ea in fingerprints[cam_bus] else False
self.ADRV_0x160 = True if 0x160 in fingerprints[cam_bus] else False
self.LFAHDA_CLUSTER = True if 480 in fingerprints[cam_bus] else False
self.HDA_INFO_4A3 = True if 0x4a3 in fingerprints[pt_bus] else False
self.NEW_MSG_4B4 = True if 0x4b4 in fingerprints[pt_bus] else False
self.GEAR = True if 69 in fingerprints[pt_bus] else False
self.GEAR_ALT = True if 64 in fingerprints[pt_bus] else False
self.CAM_0x362 = True if 0x362 in fingerprints[alt_bus] else False
self.CAM_0x2a4 = True if 0x2a4 in fingerprints[alt_bus] else False
self.STEER_TOUCH_2AF = True if 0x2af in fingerprints[pt_bus] else False
self.TPMS = True if 0x3a0 in fingerprints[pt_bus] else False
self.LOCAL_TIME = True if 1264 in fingerprints[pt_bus] else False
self.cp_bsm = None
self.time_zone = "UTC"
self.cp = None
self.cp_cam = None
self.cp_alt = None
self.controls_ready_count = 0
def update(self, can_parsers) -> structs.CarState:
def monitor_fingerprint(self, can_parsers, canfd):
if self.controls_ready_count <= 200:
if Params().get_bool("ControlsReady"):
self.controls_ready_count += 1
self.cp = can_parsers[Bus.pt]
self.cp_cam = can_parsers[Bus.cam]
self.cp_alt = can_parsers[Bus.alt] if Bus.alt in can_parsers else None
def add_if_seen(parser, name):
msg = parser.dbc.name_to_msg.get(name)
if not msg:
print(f"{name} not in DBC")
return
if msg.address not in parser.seen_addresses:
return
if msg.address in parser.addresses:
return
parser._add_message(name) # ← 이름으로 등록
def add_and_cache(parser, name: str, attr: str):
add_if_seen(parser, name)
if name in parser.vl: # 등록 성공했을 때만
setattr(self, attr, parser.vl[name])
return True
return False
if self.controls_ready_count == 50:
self.cp.controls_ready = self.cp_cam.controls_ready = True
if self.cp_alt is not None:
self.cp_alt.controls_ready = True
elif self.controls_ready_count == 100:
self.cp.enable_capture = self.cp_cam.enable_capture = False
if self.cp_alt is not None:
self.cp_alt.enable_capture = False
elif self.controls_ready_count == 101:
print("cp_cam.seen_addresses =", self.cp_cam.seen_addresses)
elif self.controls_ready_count == 102:
print("cp.seen_addresses =", self.cp.seen_addresses)
elif self.controls_ready_count == 103:
if self.cp_alt is not None:
print("cp_alt.seen_addresses =", self.cp_alt.seen_addresses)
else:
print("cp_alt.seen_addresses = None")
if not canfd:
if self.controls_ready_count == 104:
if not add_and_cache(self.cp_cam, "FCA11", "fca11"):
add_and_cache(self.cp, "FCA11", "fca11")
add_and_cache(self.cp_cam, "LKAS11", "lkas11")
add_and_cache(self.cp, "CLU11", "clu11")
elif self.controls_ready_count == 105:
cp_cruise = self.cp_cam if self.CP.flags & HyundaiFlags.CAMERA_SCC else self.cp
add_and_cache(cp_cruise, "SCC11", "scc11")
add_and_cache(cp_cruise, "SCC12", "scc12")
add_and_cache(cp_cruise, "SCC13", "scc13")
add_and_cache(cp_cruise, "SCC14", "scc14")
else: # canfd
if self.controls_ready_count == 120:
cp_cruise = self.cp_cam if self.CP.flags & HyundaiFlags.CANFD_CAMERA_SCC else self.cp
add_and_cache(cp_cruise, "SCC_CONTROL", "scc_control")
elif self.controls_ready_count == 121:
add_and_cache(self.cp_cam, "LFA", "lfa")
add_and_cache(self.cp_cam, "LFA_ALT", "lfa_alt")
add_and_cache(self.cp_cam, "LFAHDA_CLUSTER", "lfahda_cluster")
elif self.controls_ready_count == 122:
add_and_cache(self.cp_cam, "ADRV_0x161", "adrv_0x161")
add_and_cache(self.cp_cam, "ADRV_0x200", "adrv_0x200")
add_and_cache(self.cp_cam, "ADRV_0x1ea", "adrv_0x1ea")
add_and_cache(self.cp_cam, "ADRV_0x160", "adrv_0x160")
add_and_cache(self.cp_cam, "CCNC_0x162", "ccnc_0x162")
elif self.controls_ready_count == 123:
add_and_cache(self.cp, "HDA_INFO_4A3", "hda_info_4a3")
add_and_cache(self.cp, "TCS", "tcs")
add_and_cache(self.cp, "MDPS", "mdps")
add_and_cache(self.cp, "STEER_TOUCH_2AF", "steer_touch_2af")
elif self.controls_ready_count == 124:
add_and_cache(self.cp, self.cruise_btns_msg_canfd, "cruise_buttons_msg")
if not add_and_cache(self.cp_cam, "CAM_0x362", "cam_0x362") and self.cp_alt is not None:
add_and_cache(self.cp_alt, "CAM_0x362", "cam_0x362")
if not add_and_cache(self.cp_alt, "CAM_0x2a4", "cam_0x2a4") and self.cp_cam is not None:
add_and_cache(self.cp_cam, "CAM_0x2a4", "cam_0x2a4")
def update(self, can_parsers) -> structs.CarState:
self.monitor_fingerprint(can_parsers, self.CP.flags & HyundaiFlags.CANFD)
cp = can_parsers[Bus.pt]
cp_cam = can_parsers[Bus.cam]
cp_alt = can_parsers[Bus.alt] if Bus.alt in can_parsers else None
if self.controls_ready_count == 50:
cp.controls_ready = cp_cam.controls_ready = True
if cp_alt is not None:
cp_alt.controls_ready = True
elif self.controls_ready_count == 100:
print("cp_cam.seen_addresses =", cp_cam.seen_addresses)
print("cp.seen_addresses =", cp.seen_addresses)
if 909 in cp_cam.seen_addresses:
self.FCA11 = True
self.FCA11_bus = Bus.cam
elif 909 in cp.seen_addresses:
self.FCA11 = True
self.FCA11_bus = Bus.pt
if cp_alt is not None:
print("cp_alt.seen_addresses =", cp_alt.seen_addresses)
if self.CP.flags & HyundaiFlags.CANFD:
return self.update_canfd(can_parsers)
@@ -325,9 +381,6 @@ class CarState(CarStateBase):
ret.leftBlindspot = cp.vl["LCA11"]["CF_Lca_IndLeft"] != 0
ret.rightBlindspot = cp.vl["LCA11"]["CF_Lca_IndRight"] != 0
# save the entire LKAS11 and CLU11
self.lkas11 = copy.copy(cp_cam.vl["LKAS11"])
self.clu11 = copy.copy(cp.vl["CLU11"])
self.steer_state = cp.vl["MDPS12"]["CF_Mdps_ToiActive"] # 0 NOT ACTIVE, 1 ACTIVE
prev_cruise_buttons = self.cruise_buttons[-1]
#self.cruise_buttons.extend(cp.vl_all["CLU11"]["CF_Clu_CruiseSwState"])
@@ -364,11 +417,6 @@ class CarState(CarStateBase):
ret.tpms.rl = tpms_unit * cp.vl["TPMS11"]["PRESSURE_RL"]
ret.tpms.rr = tpms_unit * cp.vl["TPMS11"]["PRESSURE_RR"]
self.scc11 = cp_cruise.vl["SCC11"] if self.SCC11 else None
self.scc12 = cp_cruise.vl["SCC12"] if self.SCC12 else None
self.scc13 = cp_cruise.vl["SCC13"] if self.SCC13 else None
self.scc14 = cp_cruise.vl["SCC14"] if self.SCC14 else None
self.fca11 = can_parsers[self.FCA11_bus].vl["FCA11"] if self.FCA11 else None
cluSpeed = cp.vl["CLU11"]["CF_Clu_Vanz"]
decimal = cp.vl["CLU11"]["CF_Clu_VanzDecimal"]
if 0. < decimal < 0.5:
@@ -466,10 +514,6 @@ class CarState(CarStateBase):
ret.steerFaultTemporary = cp.vl["MDPS"]["LKA_FAULT"] != 0 or cp.vl["MDPS"]["LFA2_FAULT"] != 0
#ret.steerFaultTemporary = False
self.mdps_info = copy.copy(cp.vl["MDPS"])
if self.STEER_TOUCH_2AF:
self.steer_touch_info = cp.vl["STEER_TOUCH_2AF"]
blinkers_info = cp.vl["BLINKERS"]
left_blinker_lamp = blinkers_info["LEFT_LAMP"] or blinkers_info["LEFT_LAMP_ALT"]
right_blinker_lamp = blinkers_info["RIGHT_LAMP"] or blinkers_info["RIGHT_LAMP_ALT"]
@@ -512,40 +556,27 @@ class CarState(CarStateBase):
ret.pcmCruiseGap = int(np.clip(cp_cruise_info.vl["SCC_CONTROL"]["DISTANCE_SETTING"], 1, 4))
ret.cruiseState.standstill = cp_cruise_info.vl["SCC_CONTROL"]["InfoDisplay"] >= 4
ret.cruiseState.speed = cp_cruise_info.vl["SCC_CONTROL"]["VSetDis"] * speed_factor
self.cruise_info = copy.copy(cp_cruise_info.vl["SCC_CONTROL"])
ret.brakeHoldActive = cp.vl["ESP_STATUS"]["AUTO_HOLD"] == 1 and cp_cruise_info.vl["SCC_CONTROL"]["ACCMode"] not in (1, 2)
speed_limit_cam = False
if self.CP.flags & HyundaiFlags.CAMERA_SCC.value:
self.cruise_info = copy.copy(cp_cam.vl["SCC_CONTROL"])
self.lfa_info = copy.copy(cp_cam.vl["LFA"])
if self.CP.flags & HyundaiFlags.ANGLE_CONTROL.value:
self.lfa_alt_info = copy.copy(cp_cam.vl["LFA_ALT"])
if self.LFAHDA_CLUSTER:
self.lfahda_cluster_info = cp_cam.vl["LFAHDA_CLUSTER"]
corner = False
self.adrv_info_161 = cp_cam.vl["ADRV_0x161"] if self.CCNC_0x161 else None
self.adrv_info_162 = cp_cam.vl["CCNC_0x162"] if self.CCNC_0x162 else None
if self.adrv_info_161 is not None:
ret.leftLongDist = self.lf_distance = self.adrv_info_162["LF_DETECT_DISTANCE"]
ret.rightLongDist = self.rf_distance = self.adrv_info_162["RF_DETECT_DISTANCE"]
self.lr_distance = self.adrv_info_162["LR_DETECT_DISTANCE"]
self.rr_distance = self.adrv_info_162["RR_DETECT_DISTANCE"]
ret.leftLatDist = self.adrv_info_162["LF_DETECT_LATERAL"]
ret.rightLatDist = self.adrv_info_162["RF_DETECT_LATERAL"]
if self.ccnc_0x162 is not None:
ret.leftLongDist = self.lf_distance = self.ccnc_0x162["LF_DETECT_DISTANCE"]
ret.rightLongDist = self.rf_distance = self.ccnc_0x162["RF_DETECT_DISTANCE"]
self.lr_distance = self.ccnc_0x162["LR_DETECT_DISTANCE"]
self.rr_distance = self.ccnc_0x162["RR_DETECT_DISTANCE"]
ret.leftLatDist = self.ccnc_0x162["LF_DETECT_LATERAL"]
ret.rightLatDist = self.ccnc_0x162["RF_DETECT_LATERAL"]
corner = True
self.adrv_info_200 = cp_cam.vl["ADRV_0x200"] if self.ADRV_0x200 else None
self.adrv_info_1ea = cp_cam.vl["ADRV_0x1ea"] if self.ADRV_0x1ea else None
if self.adrv_info_1ea is not None:
if self.adrv_0x1ea is not None:
if not corner:
ret.leftLongDist = self.adrv_info_1ea["LF_DETECT_DISTANCE"]
ret.rightLongDist = self.adrv_info_1ea["RF_DETECT_DISTANCE"]
self.lr_distance = self.adrv_info_1ea["LR_DETECT_DISTANCE"]
self.rr_distance = self.adrv_info_1ea["RR_DETECT_DISTANCE"]
ret.leftLatDist = self.adrv_info_1ea["LF_DETECT_LATERAL"]
ret.rightLatDist = self.adrv_info_1ea["RF_DETECT_LATERAL"]
ret.leftLongDist = self.adrv_0x1ea["LF_DETECT_DISTANCE"]
ret.rightLongDist = self.adrv_0x1ea["RF_DETECT_DISTANCE"]
self.lr_distance = self.adrv_0x1ea["LR_DETECT_DISTANCE"]
self.rr_distance = self.adrv_0x1ea["RR_DETECT_DISTANCE"]
ret.leftLatDist = self.adrv_0x1ea["LF_DETECT_LATERAL"]
ret.rightLatDist = self.adrv_0x1ea["RF_DETECT_LATERAL"]
corner = True
if corner:
left_block = True if 0 < ret.leftLongDist < 7.0 or 0 < self.lr_distance < 7.0 else False
@@ -555,9 +586,6 @@ class CarState(CarStateBase):
if right_block:
ret.rightBlindspot = True
self.adrv_info_160 = cp_cam.vl["ADRV_0x160"] if self.ADRV_0x160 else None
self.hda_info_4a3 = cp.vl["HDA_INFO_4A3"] if self.HDA_INFO_4A3 else None
if self.hda_info_4a3 is not None:
speedLimit = self.hda_info_4a3["SPEED_LIMIT"]
if not self.is_metric:
@@ -570,18 +598,13 @@ class CarState(CarStateBase):
country_code = int(self.hda_info_4a3["CountryCode"])
self.time_zone = ZoneInfo(NUMERIC_TO_TZ.get(country_code, "UTC"))
self.new_msg_4b4 = cp.vl["NEW_MSG_4B4"] if self.NEW_MSG_4B4 else None
self.tcs_info_373 = cp.vl["TCS"]
ret.gearStep = cp.vl["GEAR"]["GEAR_STEP"] if self.GEAR else 0
if 1 <= ret.gearStep <= 8 and ret.gearShifter == GearShifter.unknown:
ret.gearShifter = GearShifter.drive
ret.gearStep = cp.vl["GEAR_ALT"]["GEAR_STEP"] if self.GEAR_ALT else ret.gearStep
if cp_alt and self.CP.flags & HyundaiFlags.CAMERA_SCC:
lane_info = None
lane_info = cp_alt.vl["CAM_0x362"] if self.CAM_0x362 else None
lane_info = cp_alt.vl["CAM_0x2a4"] if self.CAM_0x2a4 else lane_info
lane_info = self.cam_0x2a4 if self.cam_0x2a4 is not None else self.cam_0x362
if lane_info is not None:
left_lane_prob = lane_info["LEFT_LANE_PROB"]
@@ -644,12 +667,6 @@ class CarState(CarStateBase):
self.buttons_counter = cp.vl[self.cruise_btns_msg_canfd]["COUNTER"]
ret.accFaulted = cp.vl["TCS"]["ACCEnable"] != 0 # 0 ACC CONTROL ENABLED, 1-3 ACC CONTROL DISABLED
if not (self.CP.flags & HyundaiFlags.CAMERA_SCC):
if self.msg_0x362 is not None or 0x362 in cp_cam.seen_addresses:
self.msg_0x362 = cp_cam.vl["CAM_0x362"]
elif self.msg_0x2a4 is not None or 0x2a4 in cp_cam.seen_addresses:
self.msg_0x2a4 = cp_cam.vl["CAM_0x2a4"]
speed_conv = CV.KPH_TO_MS # if self.is_metric else CV.MPH_TO_MS
cluSpeed = cp.vl["CRUISE_BUTTONS_ALT"]["CLU_SPEED"]
ret.vEgoCluster = cluSpeed * speed_conv # MPH단위에서도 KPH로 나오는듯..

View File

@@ -89,27 +89,29 @@ class CanBus(CanBusBase):
def create_steering_messages_camera_scc(frame, packer, CP, CAN, CC, lat_active, apply_steer, CS, apply_angle, max_torque, angle_control):
emergency_steering = False
if CS.adrv_info_161 is not None:
values = CS.adrv_info_161
if CS.adrv_0x161 is not None:
values = CS.adrv_0x161
emergency_steering = values["ALERTS_1"] in [11, 12, 13, 14, 15, 21, 22, 23, 24, 25, 26]
ret = []
values = copy.copy(CS.mdps_info)
if angle_control:
if CS.lfa_alt_info is not None:
values["LFA2_ACTIVE"] = CS.lfa_alt_info["LKAS_ANGLE_ACTIVE"]
else:
if CS.lfa_info is not None:
values["LKA_ACTIVE"] = 1 if CS.lfa_info["STEER_REQ"] == 1 else 0
if CS.mdps is not None:
values = copy.copy(CS.mdps)
rx_counter = values.pop("COUNTER", None)
if angle_control:
if CS.lfa_alt is not None:
values["LFA2_ACTIVE"] = CS.lfa_alt["LKAS_ANGLE_ACTIVE"]
else:
if CS.lfa is not None:
values["LKA_ACTIVE"] = 1 if CS.lfa["STEER_REQ"] == 1 else 0
if frame % 1000 < 40:
values["STEERING_COL_TORQUE"] += 220
ret.append(packer.make_can_msg("MDPS", CAN.CAM, values))
if frame % 1000 < 40:
values["STEERING_COL_TORQUE"] += 220
ret.append(packer.make_can_msg("MDPS", CAN.CAM, values, rx_counter = rx_counter))
if frame % 10 == 0:
if CS.steer_touch_info is not None:
values = copy.copy(CS.steer_touch_info)
if CS.steer_touch_2af is not None:
values = copy.copy(CS.steer_touch_2af)
if frame % 1000 < 40:
values["TOUCH_DETECT"] = 3
values["TOUCH1"] = 50
@@ -121,29 +123,35 @@ def create_steering_messages_camera_scc(frame, packer, CP, CAN, CC, lat_active,
ret.append(packer.make_can_msg("STEER_TOUCH_2AF", CAN.CAM, values))
if angle_control:
if emergency_steering:
values = copy.copy(CS.lfa_alt_info)
else:
values = {} #CS.lfa_alt_info
values["LKAS_ANGLE_ACTIVE"] = 2 if CC.latActive else 1
values["LKAS_ANGLE_CMD"] = -apply_angle
values["LKAS_ANGLE_MAX_TORQUE"] = max_torque if CC.latActive else 0
ret.append(packer.make_can_msg("LFA_ALT", CAN.ECAN, values))
if CS.lfa_alt is not None:
values = copy.copy(CS.lfa_alt)
rx_counter = values.pop("COUNTER", None)
if emergency_steering:
pass
else:
#values = {} #CS.lfa_alt
values["LKAS_ANGLE_ACTIVE"] = 2 if CC.latActive else 1
values["LKAS_ANGLE_CMD"] = -apply_angle
values["LKAS_ANGLE_MAX_TORQUE"] = max_torque if CC.latActive else 0
ret.append(packer.make_can_msg("LFA_ALT", CAN.ECAN, values, rx_counter = rx_counter))
values = copy.copy(CS.lfa_info)
if not emergency_steering:
values["LKA_MODE"] = 0
values["LKA_ICON"] = 2 if CC.latActive else 1
values["TORQUE_REQUEST"] = -1024 # apply_steer,
values["VALUE63"] = 0 # LKA_ASSIST
values["STEER_REQ"] = 0 # 1 if lat_active else 0,
values["HAS_LANE_SAFETY"] = 0 # hide LKAS settings
values["LKA_ACTIVE"] = 3 if CC.latActive else 0 # this changes sometimes, 3 seems to indicate engaged
values["VALUE64"] = 0 #STEER_MODE, NEW_SIGNAL_2
values["LKAS_ANGLE_CMD"] = -25.6 #-apply_angle,
values["LKAS_ANGLE_ACTIVE"] = 0 #2 if lat_active else 1,
values["LKAS_ANGLE_MAX_TORQUE"] = 0 #max_torque if lat_active else 0,
values["NEW_SIGNAL_1"] = 10
if CS.lfa is not None:
values = copy.copy(CS.lfa)
rx_counter = values.pop("COUNTER", None)
if not emergency_steering:
values["LKA_MODE"] = 0
values["LKA_ICON"] = 2 if CC.latActive else 1
values["TORQUE_REQUEST"] = -1024 # apply_steer,
values["VALUE63"] = 0 # LKA_ASSIST
values["STEER_REQ"] = 0 # 1 if lat_active else 0,
values["HAS_LANE_SAFETY"] = 0 # hide LKAS settings
values["LKA_ACTIVE"] = 3 if CC.latActive else 0 # this changes sometimes, 3 seems to indicate engaged
values["VALUE64"] = 0 #STEER_MODE, NEW_SIGNAL_2
values["LKAS_ANGLE_CMD"] = -25.6 #-apply_angle,
values["LKAS_ANGLE_ACTIVE"] = 0 #2 if lat_active else 1,
values["LKAS_ANGLE_MAX_TORQUE"] = 0 #max_torque if lat_active else 0,
values["NEW_SIGNAL_1"] = 10
ret.append(packer.make_can_msg("LFA", CAN.ECAN, values, rx_counter = rx_counter))
else:
values = {}
@@ -160,7 +168,7 @@ def create_steering_messages_camera_scc(frame, packer, CP, CAN, CC, lat_active,
#values["VALUE82_SET256"] = 0
ret.append(packer.make_can_msg("LFA", CAN.ECAN, values))
ret.append(packer.make_can_msg("LFA", CAN.ECAN, values))
return ret
@@ -214,12 +222,12 @@ def create_steering_messages(packer, CP, CAN, enabled, lat_active, apply_steer,
return ret
def create_suppress_lfa(packer, CAN, CS):
if CS.msg_0x362 is not None:
if CS.cam_0x362 is not None:
suppress_msg = "CAM_0x362"
lfa_block_msg = CS.msg_0x362
elif CS.msg_0x2a4 is not None:
lfa_block_msg = CS.cam_0x362
elif CS.cam_0x2a4 is not None:
suppress_msg = "CAM_0x2a4"
lfa_block_msg = CS.msg_0x2a4
lfa_block_msg = CS.cam_0x2a4
else:
return []
@@ -275,7 +283,7 @@ def create_acc_cancel(packer, CP, CAN, cruise_info_copy):
def create_lfahda_cluster(packer, CS, CAN, long_active, lat_active):
if CS.lfahda_cluster_info is not None:
if CS.lfahda_cluster is not None:
values = {} #
values["HDA_CntrlModSta"] = 2 if long_active else 0
values["HDA_LFA_SymSta"] = 2 if lat_active else 0
@@ -287,6 +295,9 @@ def create_lfahda_cluster(packer, CS, CAN, long_active, lat_active):
def create_acc_control_scc2(packer, CAN, enabled, accel_last, accel, stopping, gas_override, set_speed, hud_control, hyundai_jerk, CS):
if CS.scc_control is None:
return None
enabled = (enabled or CS.softHoldActive > 0) and CS.paddle_button_prev == 0
acc_mode = 0 if not enabled else (2 if gas_override else 1)
@@ -309,8 +320,8 @@ def create_acc_control_scc2(packer, CAN, enabled, accel_last, accel, stopping, g
a_raw = accel
a_val = accel #np.clip(accel, accel_last - jn, accel_last + jn)
values = copy.copy(CS.cruise_info)
values.pop("COUNTER", None)
values = copy.copy(CS.scc_control)
rx_counter = values.pop("COUNTER", None)
values["ACCMode"] = acc_mode
values["MainMode_ACC"] = 1
values["StopReq"] = 1 if stopping or CS.softHoldActive > 0 else 0 # 1: Stop control is required, 2: Not used, 3: Error Indicator
@@ -358,7 +369,7 @@ def create_acc_control_scc2(packer, CAN, enabled, accel_last, accel, stopping, g
values["ZEROS_7"] = 1
return packer.make_can_msg("SCC_CONTROL", CAN.ECAN, values)
return packer.make_can_msg("SCC_CONTROL", CAN.ECAN, values, rx_counter = rx_counter)
def create_acc_control(packer, CAN, enabled, accel_last, accel, stopping, gas_override, set_speed, hud_control, jerk_u, jerk_l, CS):
@@ -437,8 +448,9 @@ def create_fca_warning_light(CP, packer, CAN, frame):
def create_tcs_messages(packer, CAN, CS):
ret = []
if CS.tcs_info_373 is not None:
values = copy.copy(CS.tcs_info_373)
if CS.tcs is not None:
values = copy.copy(CS.tcs)
rx_counter = values.pop("COUNTER", None)
values["DriverBraking"] = 0
values["NEW_SIGNAL_20"] = 0
values["NEW_SIGNAL_11"] = 0
@@ -446,7 +458,7 @@ def create_tcs_messages(packer, CAN, CS):
#values["NEW_SIGNAL_1"] = 0 # accel과 관련.. 옆두부 꺼지는것과 관련? 확인필요
#values["ACC_REQ"] = 1 # 옆두부 꺼지는것과 관련? 확인필요.. 항상 켜지게함..
values["NEW_SIGNAL_1"] = 0 if values["ACC_REQ"] == 1 else 1 # 옆두부..
ret.append(packer.make_can_msg("TCS", CAN.CAM, values))
ret.append(packer.make_can_msg("TCS", CAN.CAM, values, rx_counter = rx_counter))
return ret
def forward_button_message(packer, CAN, frame, CS, cruise_button, MainMode_ACC_trigger, LFA_trigger):
@@ -454,6 +466,7 @@ def forward_button_message(packer, CAN, frame, CS, cruise_button, MainMode_ACC_t
if frame % 2 == 0:
if CS.cruise_buttons_msg is not None:
values = copy.copy(CS.cruise_buttons_msg)
rx_counter = values.pop("COUNTER", None)
cruise_button_driver = values["CRUISE_BUTTONS"]
if cruise_button_driver == 0:
values["CRUISE_BUTTONS"] = cruise_button
@@ -463,288 +476,9 @@ def forward_button_message(packer, CAN, frame, CS, cruise_button, MainMode_ACC_t
elif LFA_trigger > 0:
values["LFA_BTN"] = 1
ret.append(packer.make_can_msg(CS.cruise_btns_msg_canfd, CAN.CAM, values))
ret.append(packer.make_can_msg(CS.cruise_btns_msg_canfd, CAN.CAM, values, rx_counter = rx_counter))
return ret
"""
def _make_ccnc_values___(values, CS, lat_active, frame, hud_control, lane_line = True, corner_radar = True):
if lane_line:
curvature = round(CS.out.steeringAngleDeg / 3)
values["LANELINE_CURVATURE"] = (min(abs(curvature), 15) + (-1 if curvature < 0 else 0)) if lat_active else 0
values["LANELINE_CURVATURE_DIRECTION"] = 1 if curvature < 0 and lat_active else 0
md = CS.MD
if md is not None:
desire = md.meta.desire.raw
if desire == 1: # # 좌회전
values['LANE_CHANGING'] = 1 # 왼쪽 화살표
values["LANELINE_CURVATURE"] = 15 # 커브 최대
values["LANELINE_CURVATURE_DIRECTION"] = 0 # 왼쪽으로
elif desire == 2: # 우회전
values['LANE_CHANGING'] = 2 # 오른쪽 화살표
values["LANELINE_CURVATURE"] = 15 # 차선커브 최대로
values["LANELINE_CURVATURE_DIRECTION"] = 1 # 오른쪽으로
elif desire == 3: # 좌차선변경
values['LANE_CHANGING'] = 3 # 왼쪽 화살표 + 바닥
elif desire == 4: # 우차선변경
values['LANE_CHANGING'] = 4 # 오른쪽 화살표 + 바닥
if corner_radar:
if values['LF_DETECT'] >= 4 and values['LF_DETECT_DISTANCE'] != 0: values['LF_DETECT'] = 1
if values['RF_DETECT'] >= 4 and values['RF_DETECT_DISTANCE'] != 0: values['RF_DETECT'] = 1
if values['LR_DETECT'] >= 4 and values['LR_DETECT_DISTANCE'] != 0: values['LR_DETECT'] = 1
if values['RR_DETECT'] >= 4 and values['RR_DETECT_DISTANCE'] != 0: values['RR_DETECT'] = 1
disp_dist = 30.0
min_dist = 14.0
max_interval = 100
t = 1.0 # 이 값만 바꾸면 전체 깜빡임 속도 조절됨 (0.6 빠름, 1.0 기본, 1.5 느림)
def apply_one(detect_key, dist_key):
dist = values.get(dist_key, 0.0)
if dist <= min_dist:
return
d = min(dist, disp_dist)
interval = int((1 + (max_interval - 1) * (d / disp_dist)) * t)
interval = max(1, min(interval, max_interval))
blink = (frame // interval) & 1
values[detect_key] = 2 - blink
values[dist_key] = min_dist
apply_one('LR_DETECT', 'LR_DETECT_DISTANCE')
apply_one('RR_DETECT', 'RR_DETECT_DISTANCE')
def create_ccnc_messages___(CP, packer, CAN, frame, CC, CS, hud_control, disp_angle, left_lane_warning, right_lane_warning, enable_corner_radar):
ret = []
md = CS.MD
desire = 0
lane_changing = 0
if md is not None:
desire = md.meta.desire.raw
desire_state = md.meta.desireState
if len(desire_state) > 4:
if desire_state[1] > 0.3 : lane_changing = 1
if desire_state[2] > 0.3 : lane_changing = 2
if desire_state[3] > 0.3 : lane_changing = 3
if desire_state[4] > 0.3 : lane_changing = 4
if CP.flags & HyundaiFlags.CAMERA_SCC.value:
HDA_CntrlModSta = 0
if CS.lfahda_cluster_info is not None:
HDA_CntrlModSta = CS.lfahda_cluster_info["HDA_CntrlModSta"]
if frame % 2 == 0:
if CS.adrv_info_160 is not None:
values = copy.copy(CS.adrv_info_160)
#values["NEW_SIGNAL_1"] = 0 # steer_temp관련없음, 계기판에러
#values["SET_ME_9"] = 17 # steer_temp관련없음, 계기판에러
#values["SET_ME_2"] = 0 #커멘트해도 steer_temp에러남, 2값은 콤마에서 찾은거니...
#values["DATA102"] = 0 # steer_temp관련없음
ret.append(packer.make_can_msg("ADRV_0x160", CAN.ECAN, values))
if CS.cruise_buttons_msg is not None:
values = copy.copy(CS.cruise_buttons_msg)
if CS.lfahda_cluster_info["HDA_LFA_SymSta"] == 0 and 0 < frame % 200 < 12:
values["LFA_BTN"] = 1
#else:
# values["LFA_BTN"] = 0
if CC.enabled and CS.MainMode_ACC:
if CS.ACCMode in [0, 4] and 10 < frame % 200 < 22:
values["CRUISE_BUTTONS"] = 2
elif CC.enabled and not CS.MainMode_ACC and 10 < frame % 200 <= 16 and CS.out.vEgo > 3.:
values["ADAPTIVE_CRUISE_MAIN_BTN"] = 1
else:
values["ADAPTIVE_CRUISE_MAIN_BTN"] = 0
ret.append(packer.make_can_msg(CS.cruise_btns_msg_canfd, CAN.CAM, values))
if frame % 5 == 0:
lat_active = CC.latActive
if CS.adrv_info_161 is not None:
main_enabled = CS.out.cruiseState.available
cruise_enabled = CC.enabled
lat_enabled = CS.out.latEnabled
nav_active = hud_control.activeCarrot > 1
# hdpuse carrot
hdp_use = int(Params().get("HDPuse"))
hdp_active = False
if hdp_use == 1:
hdp_active = cruise_enabled and nav_active
elif hdp_use == 2:
hdp_active = cruise_enabled
# hdpuse carrot
values = copy.copy(CS.adrv_info_161)
#print("adrv_info_161 = ", CS.adrv_info_161)
values["SETSPEED"] = (6 if hdp_active else 3 if cruise_enabled else 1) if main_enabled else 0
values["SETSPEED_HUD"] = (5 if hdp_active else 3 if cruise_enabled else 1) if main_enabled else 0
set_speed_in_units = hud_control.setSpeed * (CV.MS_TO_KPH if CS.is_metric else CV.MS_TO_MPH)
values["vSetDis"] = int(set_speed_in_units + 0.5)
values["DISTANCE"] = 4 if hdp_active else hud_control.leadDistanceBars
values["DISTANCE_LEAD"] = 2 if cruise_enabled and hud_control.leadVisible else 1 if main_enabled and hud_control.leadVisible else 0
values["DISTANCE_CAR"] = 3 if hdp_active else 2 if cruise_enabled else 1 if main_enabled else 0
values["DISTANCE_SPACING"] = 5 if hdp_active else 1 if cruise_enabled else 0
values["TARGET"] = 1 if main_enabled else 0
values["TARGET_DISTANCE"] = int(hud_control.leadDistance)
values["BACKGROUND"] = 6 if CS.paddle_button_prev > 0 else 1 if cruise_enabled else 3 if main_enabled else 7
values["CENTERLINE"] = 1 if HDA_CntrlModSta > 0 else 0 #lat_enabled else 0
values["CAR_CIRCLE"] = 2 if hdp_active else 1 if cruise_enabled else 0
values["NAV_ICON"] = 2 if nav_active else 0
values["HDA_ICON"] = 5 if hdp_active else 2 if cruise_enabled else 1 if main_enabled else 0
values["LFA_ICON"] = 5 if hdp_active else 2 if lat_active else 1 if lat_enabled else 0
values["LKA_ICON"] = 4 if lat_active else 3 if lat_enabled else 0
values["FCA_ALT_ICON"] = 0
if values["ALERTS_2"] in [1, 2, 5, 6, 10, 21, 22]: # 10,21,22: 운전자모니터 알람/경고, 6: enable lanechange alert
values["ALERTS_2"] = 0
values["DAW_ICON"] = 0
values["SOUNDS_1"] = 0 # 운전자모니터경고음.
values["SOUNDS_2"] = 0 # 2: STEER중지 경고후에도 사운드가 나옴.
values["SOUNDS_4"] = 0 # 차선변경알림? 에이 그냥0으로..
if values["ALERTS_3"] in [3, 4, 13, 17, 19, 26, 7, 8, 9, 10]:
values["ALERTS_3"] = 0
values["SOUNDS_3"] = 0
if values["ALERTS_5"] in [1, 2, 4, 5]:
values["ALERTS_5"] = 0
if values["ALERTS_5"] in [11] and CS.softHoldActive == 0:
values["ALERTS_5"] = 0
curvature = round(CS.out.steeringAngleDeg / 3)
values["LANELINE_CURVATURE"] = (min(abs(curvature), 15) + (-1 if curvature < 0 else 0)) if lat_active else 0
values["LANELINE_CURVATURE_DIRECTION"] = 1 if curvature < 0 and lat_active else 0
# lane_color = 6 if lat_active else 2
#lane_color = 2 # 6: green, 2: white, 4: yellow
lane_color = 2 if CS.out.leftLaneLine < 20 else 4
if hud_control.leftLaneDepart:
values["LANELINE_LEFT"] = 4 if (frame // 50) % 2 == 0 else 1
else:
values["LANELINE_LEFT"] = lane_color if hud_control.leftLaneVisible else 0
lane_color = 2 if CS.out.rightLaneLine < 20 else 4
if hud_control.rightLaneDepart:
values["LANELINE_RIGHT"] = 4 if (frame // 50) % 2 == 0 else 1
else:
values["LANELINE_RIGHT"] = lane_color if hud_control.rightLaneVisible else 0
#values["LANELINE_LEFT_POSITION"] = 15
#values["LANELINE_RIGHT_POSITION"] = 15
values["LCA_LEFT_ARROW"] = 2 if CS.out.leftBlinker else 0
values["LCA_RIGHT_ARROW"] = 2 if CS.out.rightBlinker else 0
values["LCA_LEFT_ICON"] = 1 if CS.out.leftBlindspot else 2
values["LCA_RIGHT_ICON"] = 1 if CS.out.rightBlindspot else 2
values["LANE_LEFT"] = 1 if desire in (1, 3) else 0
values["LANE_RIGHT"] = 1 if desire in (2, 4) else 0
ret.append(packer.make_can_msg("ADRV_0x161", CAN.ECAN, values))
if CS.adrv_info_200 is not None:
values = copy.copy(CS.adrv_info_200)
values["TauGapSet"] = hud_control.leadDistanceBars
ret.append(packer.make_can_msg("ADRV_0x200", CAN.ECAN, values))
if CS.adrv_info_1ea is not None:
values = copy.copy(CS.adrv_info_1ea)
#values["HDA_MODE1"] = 8
#values["HDA_MODE2"] = 1
if lane_changing == 3:
values['LEFT_BLINK_HOLD'] = 1
elif lane_changing == 4:
values['RIGHT_BLINK_HOLD'] = 1
_make_ccnc_values(values, CS, lat_active, frame, hud_control)
# values['AUTOLANECHANGE_MSG'] = 1 # 주변 상황을 확인하세요
# values['AUTOLANECHANGE_MSG'] = 2 # 작동 조건이 아닙니다
# values['AUTOLANECHANGE_MSG'] = 3 # 주행 차로를 분석중입니다
# values['AUTOLANECHANGE_MSG'] = 4 # 급커브 구간입니다
# values['AUTOLANECHANGE_MSG'] = 5 # 주행 중인 차로의 폭이 좁습니다
# values['AUTOLANECHANGE_MSG'] = 6 # 작동 구간이 아닙니다.
# values['AUTOLANECHANGE_MSG'] = 7 # 비상등이 켜져있습니다
# values['AUTOLANECHANGE_MSG'] = 8 # 주행속도가 낮습니다
# values['AUTOLANECHANGE_MSG'] = 9 # 핸들을 잡으십시오
# values['AUTOLANECHANGE_MSG'] = 10 # 작동 가능한 차로가 아닙니다
# values['AUTOLANECHANGE_MSG'] = 11 # 핸들 조작이 감지되었습니다.
# 얘는 우측 RPM 게이지에 크게 나옴
# values['AUTOLANECHANGE_MSG'] = 12 # ok 버튼을 누르면 차로변경 보조기능이 켜집니다
# values['AUTOLANECHANGE_MSG'] = 13 # 없음.
# values['AUTOLANECHANGE_MSG'] = 14 # 없음.
# values['AUTOLANECHANGE_MSG'] = 15 # 없음.
ret.append(packer.make_can_msg("ADRV_0x1ea", CAN.ECAN, values))
if CS.adrv_info_162 is not None:
values = copy.copy(CS.adrv_info_162)
if hud_control.leadDistance > 0:
values["FF_DISTANCE"] = hud_control.leadDistance
#values["FF_DETECT"] = 11 if hud_control.leadRelSpeed > -0.1 else 12 # bicycle
#values["FF_DETECT"] = 5 if hud_control.leadRelSpeed > -0.1 else 6 # truck
ff_type = 3 if hud_control.leadRadar == 1 else 13
values["FF_DETECT"] = ff_type if hud_control.leadRelSpeed > -0.1 else ff_type + 1
#values["FF_DETECT_LAT"] = - hud_control.leadDPath
_make_ccnc_values(values, CS, lat_active, frame, hud_control, lane_line = False, corner_radar= True)
#values["FAULT_FCA"] = 0
#values["FAULT_LSS"] = 0
#values["FAULT_LFA"] = 0
#values["FAULT_LCA"] = 0
#values["FAULT_DAS"] = 0
#values["FAULT_HDA"] = 0
if (left_lane_warning and not CS.out.leftBlinker) or (right_lane_warning and not CS.out.rightBlinker):
values["VIBRATE"] = 1
ret.append(packer.make_can_msg("CCNC_0x162", CAN.ECAN, values))
if enable_corner_radar > 0:
if HDA_CntrlModSta == 0:
if frame % 500 in [10,20,30]:
values = {
'BYTE_1': 0,
'BYTE_2': 0,
'BYTE_3': 0x80,
'BYTE_4': 0x8A,
'BYTE_5': 0x32,
'BYTE_6': 0x30,
'BYTE_7': 0x01,
'BYTE_8': 0x00,
}
ret.append(packer.make_can_msg("NEW_MSG_4B9", CAN.CAM, values))
elif frame % 500 in [40,50,60]:
values = {
'BYTE_1': 0xff,
'BYTE_2': 0xff,
'BYTE_3': 0xff,
'BYTE_4': 0xff,
'BYTE_5': 0xff,
'BYTE_6': 0xff,
'BYTE_7': 0xff,
'BYTE_8': 0xff,
}
ret.append(packer.make_can_msg("NEW_MSG_4B9", CAN.CAM, values))
if False: #canfd_debug > 1 and frame % 20 == 0: # 아직 시험중..
if CS.hda_info_4a3 is not None:
values = copy.copy(CS.hda_info_4a3)
values["LinkClass"] = 1
values["SPEED_LIMIT"] = 100
ret.append(packer.make_can_msg("HDA_INFO_4A3", CAN.CAM, values))
return ret
"""
def create_adrv_messages(CP, packer, CAN, frame):
# messages needed to car happy after disabling
# the ADAS Driving ECU to do longitudinal control
@@ -911,18 +645,20 @@ def create_ccnc_messages(CP, packer, CAN, frame, CC, CS, hud_control,
if CP.flags & HyundaiFlags.CAMERA_SCC.value:
HDA_CntrlModSta = 0
if CS.lfahda_cluster_info is not None:
HDA_CntrlModSta = CS.lfahda_cluster_info["HDA_CntrlModSta"]
HDA_LFA_SymSta = 0
if CS.lfahda_cluster is not None:
HDA_CntrlModSta = CS.lfahda_cluster["HDA_CntrlModSta"]
HDA_LFA_SymSta = CS.lfahda_cluster["HDA_LFA_SymSta"]
if frame % 2 == 0:
#if CS.adrv_info_160 is not None:
# values = copy.copy(CS.adrv_info_160)
#if CS.adrv_0x160 is not None:
# values = copy.copy(CS.adrv_0x160)
# ret.append(packer.make_can_msg("ADRV_0x160", CAN.ECAN, values))
if CS.cruise_buttons_msg is not None:
values = copy.copy(CS.cruise_buttons_msg)
if CS.lfahda_cluster_info["HDA_LFA_SymSta"] == 0 and 0 < frame % 200 < 12:
if HDA_LFA_SymSta == 0 and 0 < frame % 200 < 12:
values["LFA_BTN"] = 1
if CC.enabled and CS.MainMode_ACC:
@@ -939,7 +675,7 @@ def create_ccnc_messages(CP, packer, CAN, frame, CC, CS, hud_control,
if frame % 5 == 0:
lat_active = CC.latActive
if CS.adrv_info_161 is not None:
if CS.adrv_0x161 is not None:
main_enabled = CS.out.cruiseState.available
cruise_enabled = CC.enabled
lat_enabled = CS.out.latEnabled
@@ -954,8 +690,8 @@ def create_ccnc_messages(CP, packer, CAN, frame, CC, CS, hud_control,
hdp_active = cruise_enabled
# hdpuse carrot
values = copy.copy(CS.adrv_info_161)
values = copy.copy(CS.adrv_0x161)
rx_counter = values.pop("COUNTER", None)
values["SETSPEED"] = (6 if hdp_active else 3 if cruise_enabled else 1) if main_enabled else 0
values["SETSPEED_HUD"] = (5 if hdp_active else 3 if cruise_enabled else 1) if main_enabled else 0
@@ -989,7 +725,7 @@ def create_ccnc_messages(CP, packer, CAN, frame, CC, CS, hud_control,
values["SOUNDS_2"] = 0
values["SOUNDS_4"] = 0
if values["ALERTS_3"] in [3, 4, 13, 17, 19, 26, 7, 8, 9, 10]:
if values["ALERTS_3"] in [3, 4, 11, 12, 13, 14, 17, 19, 26, 7, 8, 9, 10]: # hide gap distance msg.(11,12,13,14)
values["ALERTS_3"] = 0
values["SOUNDS_3"] = 0
@@ -1004,13 +740,15 @@ def create_ccnc_messages(CP, packer, CAN, frame, CC, CS, hud_control,
values["LANELINE_CURVATURE"] = (min(abs(curvature), 15) + (-1 if curvature < 0 else 0)) if lat_active else 0
values["LANELINE_CURVATURE_DIRECTION"] = 1 if curvature < 0 and lat_active else 0
lane_color = 4 if CS.out.leftLaneLine >= 20 or CS.out.leftBlindspot else 2
lane_color = 6 if md is not None and md.meta.laneChangeAvailableLeft else 2
lane_color = 4 if CS.out.leftLaneLine >= 20 or CS.out.leftBlindspot else lane_color
if hud_control.leftLaneDepart:
values["LANELINE_LEFT"] = 4 if (frame // 50) % 2 == 0 else 1
else:
values["LANELINE_LEFT"] = lane_color if hud_control.leftLaneVisible else 0
lane_color = 4 if CS.out.rightLaneLine >= 20 or CS.out.rightBlindspot else 2
lane_color = 6 if md is not None and md.meta.laneChangeAvailableRight else 2
lane_color = 4 if CS.out.rightLaneLine >= 20 or CS.out.rightBlindspot else lane_color
if hud_control.rightLaneDepart:
values["LANELINE_RIGHT"] = 4 if (frame // 50) % 2 == 0 else 1
else:
@@ -1025,16 +763,17 @@ def create_ccnc_messages(CP, packer, CAN, frame, CC, CS, hud_control,
values["LANE_LEFT"] = 1 if desire in (1, 3) else 0
values["LANE_RIGHT"] = 1 if desire in (2, 4) else 0
ret.append(packer.make_can_msg("ADRV_0x161", CAN.ECAN, values))
ret.append(packer.make_can_msg("ADRV_0x161", CAN.ECAN, values, rx_counter = rx_counter))
if CS.adrv_info_200 is not None:
values = copy.copy(CS.adrv_info_200)
if CS.adrv_0x200 is not None:
values = copy.copy(CS.adrv_0x200)
rx_counter = values.pop("COUNTER", None)
values["TauGapSet"] = hud_control.leadDistanceBars
ret.append(packer.make_can_msg("ADRV_0x200", CAN.ECAN, values))
if CS.adrv_info_1ea is not None:
values = copy.copy(CS.adrv_info_1ea)
ret.append(packer.make_can_msg("ADRV_0x200", CAN.ECAN, values, rx_counter = rx_counter))
if CS.adrv_0x1ea is not None:
values = copy.copy(CS.adrv_0x1ea)
rx_counter = values.pop("COUNTER", None)
# blinker hold
values['LEFT_BLINK_HOLD'] = 1 if lane_changing == 3 else 0
values['RIGHT_BLINK_HOLD'] = 1 if lane_changing == 4 else 0
@@ -1050,10 +789,10 @@ def create_ccnc_messages(CP, packer, CAN, frame, CC, CS, hud_control,
blink_t=1.0
)
ret.append(packer.make_can_msg("ADRV_0x1ea", CAN.ECAN, values))
ret.append(packer.make_can_msg("ADRV_0x1ea", CAN.ECAN, values, rx_counter = rx_counter))
if CS.adrv_info_162 is not None:
values = copy.copy(CS.adrv_info_162)
if CS.ccnc_0x162 is not None:
values = copy.copy(CS.ccnc_0x162)
if hud_control.leadDistance > 0:
values["FF_DISTANCE"] = hud_control.leadDistance

View File

@@ -1,162 +0,0 @@
from pickle import NONE
from types import NoneType
import numpy as np
from opendbc.car import DT_CTRL, structs
from opendbc.car.interfaces import CarControllerBase
from opendbc.car.hyundai.values import HyundaiFlags
from opendbc.carrot.hyundai import carrot_hyundaicanfd
VisualAlert = structs.CarControl.HUDControl.VisualAlert
def rate_limit(x, x_last, lo, hi):
return float(np.clip(x, x_last + lo, x_last + hi))
def process_hud_alert(enabled, fingerprint, hud_control):
sys_warning = (hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw))
# initialize to no line visible
# TODO: this is not accurate for all cars
sys_state = 1
if hud_control.leftLaneVisible and hud_control.rightLaneVisible or sys_warning: # HUD alert only display when LKAS status is active
sys_state = 3 if enabled or sys_warning else 4
elif hud_control.leftLaneVisible:
sys_state = 5
elif hud_control.rightLaneVisible:
sys_state = 6
# initialize to no warnings
left_lane_warning = 0
right_lane_warning = 0
if hud_control.leftLaneDepart:
left_lane_warning = 1 if fingerprint in (CAR.GENESIS_G90, CAR.GENESIS_G80) else 2
if hud_control.rightLaneDepart:
right_lane_warning = 1 if fingerprint in (CAR.GENESIS_G90, CAR.GENESIS_G80) else 2
return sys_warning, sys_state, left_lane_warning, right_lane_warning
def apply_steer_angle_limits_physics(desired_sw_deg: float,
last_sw_deg: float,
v_ego: float,
steering_sw_deg: float,
lat_active: bool,
wheelbase_m: float,
steer_ratio: float,
steer_sw_max_deg: float) -> float:
max_lat_accel = 5.0 # m/s^2
max_lat_jerk = 4.0 # m/s^3
max_sw_rate_deg_per_tick = 2.0 # EPS 보호용 상한
v = max(float(v_ego), 1.0)
target_sw = float(np.clip(desired_sw_deg, -steer_sw_max_deg, steer_sw_max_deg))
target_rw = target_sw / steer_ratio
last_rw = float(last_sw_deg) / steer_ratio
# --- accel limit ---
rw_max_rad = np.arctan((max_lat_accel * wheelbase_m) / (v * v))
rw_max = float(np.degrees(rw_max_rad))
# --- jerk -> rate limit ---
sec2 = 1.2
max_drw_dt = (max_lat_jerk * wheelbase_m) / (v * v * sec2) # rad/s
max_drw_per_tick = max_drw_dt * DT_CTRL # rad/tick
max_drw_per_tick_deg = float(np.degrees(max_drw_per_tick))
max_drw_per_tick_deg = min(
max_drw_per_tick_deg,
max_sw_rate_deg_per_tick / steer_ratio
)
err = abs(target_sw - last_sw_deg)
if err > 20.0:
max_drw_per_tick_deg *= 0.5
# --- rate limit ---
cmd_rw = rate_limit(target_rw, last_rw, -max_drw_per_tick_deg, max_drw_per_tick_deg)
# --- accel clip ---
cmd_rw = float(np.clip(cmd_rw, -rw_max, rw_max))
if not lat_active:
cmd_rw = float(steering_sw_deg) / steer_ratio
cmd_sw = cmd_rw * steer_ratio
return float(np.clip(cmd_sw, -steer_sw_max_deg, steer_sw_max_deg))
class CarrotCarController(CarControllerBase):
def _carrot_init(self, CP):
self.angle_control = CP.flags & HyundaiFlags.ANGLE_CONTROL
self.apply_angle_last = 0
self.lkas_max_torque = 0
self.angle_max_torque = self.params.ANGLE_MAX_TORQUE
def _carrot_process_steering_angle(self, CS, CC):
apply_angle = apply_steer_angle_limits_physics(
CC.actuators.steeringAngleDeg,
self.apply_angle_last,
CS.out.vEgoRaw,
CS.out.steeringAngleDeg,
CC.latActive,
self.CP.wheelbase,
self.CP.steerRatio,
self.params.ANGLE_LIMITS.STEER_ANGLE_MAX
)
if CS.out.steeringPressed:
self.lkas_max_torque = max(self.lkas_max_torque - 20, self.params.ANGLE_MIN_TORQUE)
else:
target_torque = self.angle_max_torque
max_steering_tq = self.params.STEER_DRIVER_ALLOWANCE * 0.7
rate_ratio = max(20, max_steering_tq - abs(CS.out.steeringTorque)) / max_steering_tq
rate_up = self.params.ANGLE_TORQUE_UP_RATE * rate_ratio
rate_down = self.params.ANGLE_TORQUE_DOWN_RATE * rate_ratio
if self.lkas_max_torque > target_torque:
self.lkas_max_torque = max(self.lkas_max_torque - rate_down, target_torque)
else:
self.lkas_max_torque = min(self.lkas_max_torque + rate_up, target_torque)
if not CC.latActive:
self.lkas_max_torque = 0
self.apply_angle_last = apply_angle
def _carrot_canfd_camera_scc_msg(self, apply_steer_req, apply_torque, accel, stopping, hud_control, CS, CC, set_speed_in_units, actuators):
sys_warning, sys_state, left_lane_warning, right_lane_warning = process_hud_alert(CC.enabled, self.car_fingerprint,
hud_control)
can_sends = []
self._carrot_process_steering_angle(CS, CC)
if self.angle_control:
apply_steer_req = CC.latActive
can_sends.extend(carrot_hyundaicanfd.create_steering_messages_camera_scc(self.frame, self.packer, self.CAN, CC, apply_steer_req, apply_torque, CS, self.apply_angle_last, self.lkas_max_torque, self.angle_control))
if self.frame % 5 == 0:
can_sends.extend(carrot_hyundaicanfd.create_lfahda_cluster(self.packer, CS, self.CAN, CC.longActive, CC.latActive))
#if self.camera_scc_params in [2, 3]:
# self.canfd_toggle_adas(CC, CS)
if self.CP.openpilotLongitudinalControl:
#self.hyundai_jerk.make_jerk(self.CP, CS, accel, actuators, hud_control)
#self.hyundai_jerk.check_carrot_cruise(CC, CS, hud_control, stopping, accel, actuators.aTarget)
can_sends.extend(carrot_hyundaicanfd.create_ccnc_messages(self.CP, self.packer, self.CAN, self.frame, CC, CS, hud_control, self.apply_angle_last, left_lane_warning, right_lane_warning))
if self.frame % 2 == 0:
msg = carrot_hyundaicanfd.create_acc_control_scc2(self.packer, self.CAN, CC.enabled, self.accel_last, accel, stopping, CC.cruiseControl.override,
set_speed_in_units, hud_control, CS)
if msg is not None:
can_sends.append(msg)
can_sends.extend(carrot_hyundaicanfd.create_tcs_messages(self.packer, self.CAN, CS)) # for sorento SCC radar...
self.accel_last = accel
else:
# button presses
"""
if self.camera_scc_params == 3: # camera scc but stock long
send_button = self.make_spam_button(CC, CS)
can_sends.extend(hyundaicanfd.forward_button_message(self.packer, self.CAN, self.frame, CS, send_button, self.MainMode_ACC_trigger, self.LFA_trigger))
else:
can_sends.extend(self.create_button_messages(CC, CS, use_clu11=False))
"""
pass
return can_sends

View File

@@ -1,114 +0,0 @@
from opendbc.car.interfaces import CarStateBase
from openpilot.common.params import Params
from opendbc.car import Bus, create_button_events, structs, DT_CTRL
class CarrotCarState(CarStateBase):
def _carrot_init(self, CP):
self.controls_ready_count = 0
self.cp_bsm = None
self._params = Params()
self.cp = None
self.cp_cam = None
self.cp_alt = None
self.mdps = None
self.steer_touch_2af = None
self.tcs = None
self.adrv_161 = None
self.lfa_alt = None
self.lfa = None
self.ccnc_162 = None
self.lfahda_cluster = None
self.scc_control = None
self.adrv_200 = None
self.adrv_1ea = None
self.cruise_buttons_msg = None
def _carrot_monitor_fingerprint(self, can_parsers):
self.cp = can_parsers[Bus.pt]
self.cp_cam = can_parsers[Bus.cam]
self.cp_alt = can_parsers[Bus.alt] if Bus.alt in can_parsers else None
if self.controls_ready_count <= 200:
if self._params.get_bool("ControlsReady"):
self.controls_ready_count += 1
if self.controls_ready_count == 50: # after 500msec
self.cp.enable_capture = self.cp_cam.enable_capture = True
if self.cp_alt is not None:
self.cp_alt.enable_capture = True
elif self.controls_ready_count == 100: # after 1sec
print("cp_cam.seen_addresses =", self.cp_cam.seen_addresses)
print("cp.seen_addresses =", self.cp.seen_addresses)
self.cp.enable_capture = self.cp_cam.enable_capture = False
if self.cp_alt is not None:
print("cp_alt.seen_addresses =", self.cp_alt.seen_addresses)
self.cp_alt.enable_capture = False
if 69 in self.cp.seen_addresses:
self.gear_msg_canfd = "GEAR"
if 442 in self.cp.seen_addresses:
self.cp_bsm = self.cp
elif 442 in self.cp_cam.seen_addresses:
self.cp_bsm = self.cp_cam
def add_if_seen(parser, name):
msg = parser.dbc.name_to_msg.get(name)
if not msg:
print(f"{name} not in DBC")
return
if msg.address not in parser.seen_addresses:
return
if msg.address in parser.addresses:
return
parser._add_message(name) # ← 이름으로 등록
add_if_seen(self.cp, "MDPS")
add_if_seen(self.cp, "STEER_TOUCH_2AF")
add_if_seen(self.cp, "TCS")
add_if_seen(self.cp_cam, "ADRV_0x161")
add_if_seen(self.cp_cam, "LFA_ALT")
add_if_seen(self.cp_cam, "LFA")
add_if_seen(self.cp_cam, "CCNC_0x162")
add_if_seen(self.cp_cam, "LFAHDA_CLUSTER")
add_if_seen(self.cp_cam, "SCC_CONTROL")
add_if_seen(self.cp_cam, "ADRV_0x200")
add_if_seen(self.cp_cam, "ADRV_0x1ea")
def _carrot_update_rx(self):
self.mdps = self.cp.vl.get("MDPS")
self.steer_touch_2af = self.cp.vl.get("STEER_TOUCH_2AF")
self.tcs = self.cp.vl.get("TCS")
self.adrv_161 = self.cp_cam.vl.get("ADRV_0x161")
self.lfa_alt = self.cp_cam.vl.get("LFA_ALT")
self.lfa = self.cp_cam.vl.get("LFA")
self.ccnc_162 = self.cp_cam.vl.get("CCNC_0x162")
self.lfahda_cluster = self.cp_cam.vl.get("LFAHDA_CLUSTER")
self.scc_control = self.cp_cam.vl.get("SCC_CONTROL")
self.adrv_200 = self.cp_cam.vl.get("ADRV_0x200")
self.adrv_1ea = self.cp_cam.vl.get("ADRV_0x1ea")
self.cruise_buttons_msg = self.cp.vl.get(self.cruise_btns_msg_canfd)
def _carrot_update_canfd(self, ret):
self._carrot_update_rx()
ret.cruiseState.available = self.scc_control is not None and self.scc_control["MainMode_ACC"] == 1
# TPMS
#tpms_unit = self.cp.vl["TPMS"]["UNIT"] * 0.725 if int(self.cp.vl["TPMS"]["UNIT"]) > 0 else 1.
#ret.tpms.fl = tpms_unit * self.cp.vl["TPMS"]["PRESSURE_FL"]
#ret.tpms.fr = tpms_unit * self.cp.vl["TPMS"]["PRESSURE_FR"]
#ret.tpms.rl = tpms_unit * self.cp.vl["TPMS"]["PRESSURE_RL"]
#ret.tpms.rr = tpms_unit * self.cp.vl["TPMS"]["PRESSURE_RR"]
# BSM
if self.cp_bsm is not None:
bsm_info = self.cp_bsm.vl["BLINDSPOTS_REAR_CORNERS"]
ret.leftBlindspot = (bsm_info["FL_INDICATOR"] + bsm_info["INDICATOR_LEFT_TWO"] + bsm_info["INDICATOR_LEFT_FOUR"]) > 0
ret.rightBlindspot = (bsm_info["FR_INDICATOR"] + bsm_info["INDICATOR_RIGHT_TWO"] + bsm_info["INDICATOR_RIGHT_FOUR"]) > 0
# brakeLights
#ret.brakeLights = ret.brakePressed or self.cp.vl["TCS"]["BrakeLight"] == 1
return ret

View File

@@ -1,494 +0,0 @@
import numpy as np
import copy
from opendbc.car.common.conversions import Conversions as CV
def hyundai_crc8(data: bytes) -> int:
poly = 0x2F
crc = 0xFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 0x80:
crc = ((crc << 1) ^ poly) & 0xFF
else:
crc = (crc << 1) & 0xFF
return crc ^ 0xFF
def is_emergency_steering(CS):
emergency_steering = False
if CS.adrv_161 is not None:
emergency_steering = CS.adrv_161["ALERTS_1"] in [11, 12, 13, 14, 15, 21, 22, 23, 24, 25, 26]
return emergency_steering
def create_fake_mdps_active(ret, frame, packer, CAN, CS, angle_control):
if CS.mdps is not None:
mdps = copy.copy(CS.mdps)
if angle_control:
if CS.lfa_alt is not None:
mdps["LFA2_ACTIVE"] = CS.lfa_alt["LKAS_ANGLE_ACTIVE"]
else:
if CS.lfa is not None:
mdps["LKA_ACTIVE"] = 1 if CS.lfa["STEER_REQ"] == 1 else 0
if frame % 1000 < 40:
mdps["STEERING_COL_TORQUE"] += 220
ret.append(packer.make_can_msg("MDPS", CAN.CAM, mdps))
def create_fake_steering_touch(ret, frame, packer, CAN, CS):
if frame % 10 == 0:
if CS.teer_touch_2af is not None:
steer_touch = copy.copy(CS.teer_touch_2af)
if frame % 1000 < 40:
steer_touch["TOUCH_DETECT"] = 3
steer_touch["TOUCH1"] = 50
steer_touch["TOUCH2"] = 50
steer_touch["CHECKSUM_"] = 0
dat = packer.make_can_msg("STEER_TOUCH_2AF", 0, steer_touch)[1]
steer_touch["CHECKSUM_"] = hyundai_crc8(dat[1:8])
ret.append(packer.make_can_msg("STEER_TOUCH_2AF", CAN.CAM, steer_touch))
def create_steering_messages_camera_scc(frame, packer, CAN, CC, lat_active, apply_steer, CS, apply_angle, max_torque, angle_control):
emergency_steering = is_emergency_steering(CS)
ret = []
create_fake_mdps_active(ret, frame, packer, CAN, CS, angle_control)
#create_fake_steering_touch(ret, frame, packer, CAN, CS)
if angle_control:
if CS.lfa_alt is not None:
if emergency_steering:
values = CS.lfa_alt
else:
values = copy.copy(CS.lfa_alt) #{} #CS.lfa_alt_info
values["LKAS_ANGLE_ACTIVE"] = 2 if CC.latActive else 1
values["LKAS_ANGLE_CMD"] = -apply_angle
values["LKAS_ANGLE_MAX_TORQUE"] = max_torque if CC.latActive else 0
ret.append(packer.make_can_msg("LFA_ALT", CAN.ECAN, values))
if CS.lfa is not None:
if frame % 100 == 0:
if CS.lfa["FCA_SYSWARN"] != 0:
print("FCA_SYSWARN")
if CS.ccnc_162 is not None:
if CS.ccnc_162["FAULT_FSS"] != 0:
print("FAULT_FSS")
values = copy.copy(CS.lfa)
if not emergency_steering:
values["LKA_MODE"] = 0
values["LKA_ICON"] = 2 if CC.latActive else 1
values["TORQUE_REQUEST"] = -1024 # apply_steer,
values["VALUE63"] = 0
values["STEER_REQ"] = 0
values["HAS_LANE_SAFETY"] = 0
values["LKA_ACTIVE"] = 3 if CC.latActive else 0 # this changes sometimes, 3 seems to indicate engaged
values["VALUE64"] = 0
values["LKAS_ANGLE_CMD"] = -25.6
values["LKAS_ANGLE_ACTIVE"] = 0
values["LKAS_ANGLE_MAX_TORQUE"] = 0 #max_torque if lat_active else 0,
values["NEW_SIGNAL_1"] = 10
ret.append(packer.make_can_msg("LFA", CAN.ECAN, values))
else:
values = {}
values["LKA_MODE"] = 2
values["LKA_ICON"] = 2 if lat_active else 1
values["TORQUE_REQUEST"] = apply_steer
values["STEER_REQ"] = 1 if lat_active else 0
values["VALUE64"] = 0
values["HAS_LANE_SAFETY"] = 0
values["LKA_ACTIVE"] = 0
values["DampingGain"] = 0 if lat_active else 100
ret.append(packer.make_can_msg("LFA", CAN.ECAN, values))
return ret
def create_lfahda_cluster(packer, CS, CAN, long_active, lat_active):
if CS.lfahda_cluster is not None:
values = {}
values["HDA_CntrlModSta"] = 2 if long_active else 0
values["HDA_LFA_SymSta"] = 2 if lat_active else 0
else:
return []
return [packer.make_can_msg("LFAHDA_CLUSTER", CAN.ECAN, values)]
def create_acc_control_scc2(packer, CAN, enabled, accel_last, accel, stopping, gas_override, set_speed, hud_control, CS):
#enabled = (enabled or CS.softHoldActive > 0) and CS.paddle_button_prev == 0
acc_mode = 0 if not enabled else (2 if gas_override else 1)
softHoldActive = 0
"""
if hyundai_jerk.carrot_cruise == 1:
acc_mode = 4 if enabled else 0
enabled = False
accel = accel_last = 0.5
elif hyundai_jerk.carrot_cruise == 2:
accel = accel_last = hyundai_jerk.carrot_cruise_accel
jerk_u = hyundai_jerk.jerk_u
jerk_l = hyundai_jerk.jerk_l
"""
jerk_u = 3
jerk_l = 5
jn = jerk_l / 50
if not enabled or gas_override:
a_val, a_raw = 0, 0
else:
a_raw = accel
a_val = np.clip(accel, accel_last - jn, accel_last + jn)
if CS.scc_control is None:
return None
values = copy.copy(CS.scc_control)
values.pop("COUNTER", None)
values["ACCMode"] = acc_mode
values["MainMode_ACC"] = 1
values["StopReq"] = 1 if stopping or softHoldActive > 0 else 0 # 1: Stop control is required, 2: Not used, 3: Error Indicator
values["aReqValue"] = a_val
values["aReqRaw"] = a_raw
values["VSetDis"] = set_speed
values["JerkLowerLimit"] = jerk_l if enabled else 1
values["JerkUpperLimit"] = 2.0 if stopping or softHoldActive else jerk_u
values["DISTANCE_SETTING"] = hud_control.leadDistanceBars # + 5
values["DriveMode"] = 0 # 0: Default, 1: Comfort Mode, 2:Normal mode, 3:Dynamic mode, reserved
hud_lead_info = 0
if hud_control.leadVisible:
hud_lead_info = 1 if values["ACC_ObjRelSpd"] > 0 else 2
values["HUD_LEAD_INFO"] = hud_lead_info #1: in-path object detected(uncontrollable), 2: controllable long, 3: controllable long & lat, ... reserved
values["DriverAlert"] = 0 # 1: SCC Disengaged, 2: No SCC Engage condition, 3: SCC Disenganed when the vehicle stops
values["TARGET_DISTANCE"] = CS.out.vEgo * 1.0 + 4.0
soft_hold_info = 1 if softHoldActive > 1 and enabled else 0
# 이거안하면 정지중 뒤로 밀리는 현상 발생하는듯.. (신호정지중에 뒤로 밀리는 경험함.. 시험해봐야)
if values["InfoDisplay"] != 5: #5: Front Car Departure Notice
values["InfoDisplay"] = 4 if stopping and CS.out.aEgo > -0.3 else 0 # 1: SCC Mode, 2: Convention Cruise Mode, 3: Object disappered at low speed, 4: Available to resume acceleration control, 5: Front vehicle departure notice, 6: Reserved, 7: Invalid
values["TakeOverReq"] = 0 # 1: Takeover request, 2: Not used, 3: Error indicator , 이것이 켜지면 가속을 안하는듯함.
values["SysFailState"] = 0 # 1: Performance degredation, 2: system temporairy unavailble, 3: SCC Service required , 눈이 묻어 레이더오류시... 2가 됨. 이때 가속을 안함...
values["AccelLimitBandUpper"] = 0.0 # 이값이 1.26일때 가속을 안하는 증상이 보임..
values["AccelLimitBandLower"] = 0.0
values["ZEROS_7"] = 1
return packer.make_can_msg("SCC_CONTROL", CAN.ECAN, values)
def create_tcs_messages(packer, CAN, CS):
ret = []
if CS.tcs is not None:
values = copy.copy(CS.tcs)
values["DriverBraking"] = 0
values["NEW_SIGNAL_20"] = 0
values["NEW_SIGNAL_11"] = 0
values["DriverBrakingLowSens"] = 0
#values["NEW_SIGNAL_1"] = 0 # accel과 관련.. 옆두부 꺼지는것과 관련? 확인필요
#values["ACC_REQ"] = 1 # 옆두부 꺼지는것과 관련? 확인필요.. 항상 켜지게함..
values["NEW_SIGNAL_1"] = 0 if values["ACC_REQ"] == 1 else 1 # 옆두부..
ret.append(packer.make_can_msg("TCS", CAN.CAM, values))
return ret
def alt_cruise_buttons(packer, CP, CAN, buttons, cruise_btns_msg, cnt):
cruise_btns_msg["CRUISE_BUTTONS"] = buttons
cruise_btns_msg["COUNTER"] = (cruise_btns_msg["COUNTER"] + 1 + cnt) % 256
bus = CAN.ECAN if CP.flags & HyundaiFlags.CANFD_HDA2 else CAN.CAM
return packer.make_can_msg("CRUISE_BUTTONS_ALT", bus, cruise_btns_msg)
def _clip_int(x, lo, hi):
return lo if x < lo else hi if x > hi else int(x)
def _get_desire_and_lane_changing(md):
desire = 0
lane_changing = 0
if md is not None:
desire = md.meta.desire.raw
ds = md.meta.desireState
if len(ds) > 4:
if ds[1] > 0.3: lane_changing = 1
if ds[2] > 0.3: lane_changing = 2
if ds[3] > 0.3: lane_changing = 3
if ds[4] > 0.3: lane_changing = 4
return desire, lane_changing
def _apply_lane_desire(values, desire):
#values['LANE_CHANGING'] = 0
if desire == 1: # 좌회전
values['LANE_CHANGING'] = 1
values["LANELINE_CURVATURE"] = 15
values["LANELINE_CURVATURE_DIRECTION"] = 0
elif desire == 2: # 우회전
values['LANE_CHANGING'] = 2
values["LANELINE_CURVATURE"] = 15
values["LANELINE_CURVATURE_DIRECTION"] = 1
elif desire == 3: # 좌차선변경
values['LANE_CHANGING'] = 3
elif desire == 4: # 우차선변경
values['LANE_CHANGING'] = 4
def _apply_radar_blink(values, radar_pairs, frame, *,
disp_dist=30.0, min_dist=14.0,
max_interval=100, t=1.0):
"""
거리 > min_dist 일 때만 깜빡임.
거리 멀수록 interval 커짐(느리게).
"""
for det_key, dist_key in radar_pairs:
dist = values[dist_key]
if dist <= min_dist:
continue
d = min(dist, disp_dist)
interval = int((1 + (max_interval - 1) * (d / disp_dist)) * t)
interval = _clip_int(interval, 1, max_interval)
blink = (frame // interval) & 1
values[det_key] = 2 - blink
values[dist_key] = min_dist
def _make_ccnc_values(values, CS, lat_active, frame, hud_control,
lane_line=True, corner_radar=True,
desire=0,
blink_pairs=None,
blink_t=1.0):
if lane_line:
curvature = round(CS.out.steeringAngleDeg / 3)
mag = min(abs(curvature), 15)
curv = mag + (-1 if curvature < 0 else 0)
direction = 1 if curvature < 0 else 0
values["LANELINE_CURVATURE"] = curv if lat_active else 0
values["LANELINE_CURVATURE_DIRECTION"] = direction if lat_active else 0
if desire:
_apply_lane_desire(values, desire)
if corner_radar:
radar_all = [
('LF_DETECT', 'LF_DETECT_DISTANCE'),
('RF_DETECT', 'RF_DETECT_DISTANCE'),
('LR_DETECT', 'LR_DETECT_DISTANCE'),
('RR_DETECT', 'RR_DETECT_DISTANCE'),
]
for det_key, dist_key in radar_all:
if values[det_key] >= 4 and values[dist_key] != 0:
values[det_key] = 1
if blink_pairs:
_apply_radar_blink(values, blink_pairs, frame, t=blink_t)
def enable_corner_radar(ret, packer, CAN, frame):
if frame % 500 in [10, 20, 30]:
values = {
'BYTE_1': 0,
'BYTE_2': 0,
'BYTE_3': 0x80,
'BYTE_4': 0x8A,
'BYTE_5': 0x32,
'BYTE_6': 0x30,
'BYTE_7': 0x01,
'BYTE_8': 0x00,
}
ret.append(packer.make_can_msg("NEW_MSG_4B9", CAN.CAM, values))
elif frame % 500 in [40, 50, 60]:
values = {
'BYTE_1': 0xff,
'BYTE_2': 0xff,
'BYTE_3': 0xff,
'BYTE_4': 0xff,
'BYTE_5': 0xff,
'BYTE_6': 0xff,
'BYTE_7': 0xff,
'BYTE_8': 0xff,
}
ret.append(packer.make_can_msg("NEW_MSG_4B9", CAN.CAM, values))
def activate_scc_lfa(ret, packer, CAN, frame, CC, CS, lfahda_cluster):
if frame % 2 == 0:
MainMode_ACC = CS.scc_control["MainMode_ACC"] == 1 if CS.scc_control is not None else False
ACCMode = CS.scc_control["ACCMode"] if CS.scc_control is not None else 0
if CS.cruise_buttons_msg is not None:
values = copy.copy(CS.cruise_buttons_msg)
if lfahda_cluster["HDA_LFA_SymSta"] == 0 and 0 < frame % 200 < 12:
values["LDA_BTN"] = 1
if CC.enabled and MainMode_ACC:
if ACCMode in [0, 4] and 10 < frame % 200 < 22:
values["CRUISE_BUTTONS"] = 2
elif CC.enabled and (not MainMode_ACC) and 10 < frame % 200 <= 16 and CS.out.vEgo > 3.:
values["ADAPTIVE_CRUISE_MAIN_BTN"] = 1
else:
values["ADAPTIVE_CRUISE_MAIN_BTN"] = 0
ret.append(packer.make_can_msg(CS.cruise_btns_msg_canfd, CAN.CAM, values))
def create_ccnc_messages(CP, packer, CAN, frame, CC, CS, hud_control,
disp_angle, left_lane_warning, right_lane_warning):
ret = []
#md = CS.MD
desire, lane_changing = 0, 0 #_get_desire_and_lane_changing(md)
HDA_CntrlModSta = CS.lfahda_cluster["HDA_CntrlModSta"] if CS.lfahda_cluster is not None else False
if CS.lfahda_cluster is not None:
activate_scc_lfa(ret, packer, CAN, frame, CC, CS, CS.lfahda_cluster)
# --- 0x161/0x200/0x1ea/0x162 (frame%5) ---
if frame % 5 == 0:
lat_active = CC.latActive
if CS.adrv_161 is not None:
main_enabled = CS.out.cruiseState.available
cruise_enabled = CC.enabled
lat_enabled = main_enabled #CS.out.latEnabled
nav_active = False #hud_control.activeCarrot > 1
# hdpuse carrot
hdp_active = False
values = copy.copy(CS.adrv_161)
values["SETSPEED"] = (6 if hdp_active else 3 if cruise_enabled else 1) if main_enabled else 0
values["SETSPEED_HUD"] = (5 if hdp_active else 3 if cruise_enabled else 1) if main_enabled else 0
set_speed_in_units = hud_control.setSpeed * (CV.MS_TO_KPH if CS.is_metric else CV.MS_TO_MPH)
values["vSetDis"] = int(set_speed_in_units + 0.5)
values["DISTANCE"] = 4 if hdp_active else hud_control.leadDistanceBars
values["DISTANCE_LEAD"] = 2 if cruise_enabled and hud_control.leadVisible else 1 if main_enabled and hud_control.leadVisible else 0
values["DISTANCE_CAR"] = 3 if hdp_active else 2 if cruise_enabled else 1 if main_enabled else 0
values["DISTANCE_SPACING"] = 5 if hdp_active else 1 if cruise_enabled else 0
values["TARGET"] = 1 if main_enabled else 0
#values["TARGET_DISTANCE"] = int(hud_control.leadDistance)
#values["BACKGROUND"] = 6 if CS.paddle_button_prev > 0 else 1 if cruise_enabled else 3 if main_enabled else 7
values["CENTERLINE"] = 1 if HDA_CntrlModSta > 0 else 0
values["CAR_CIRCLE"] = 2 if hdp_active else 1 if cruise_enabled else 0
values["NAV_ICON"] = 2 if nav_active else 0
values["HDA_ICON"] = 5 if hdp_active else 2 if cruise_enabled else 1 if main_enabled else 0
values["LFA_ICON"] = 5 if hdp_active else 2 if lat_active else 1 if lat_enabled else 0
values["LKA_ICON"] = 4 if lat_active else 3 if lat_enabled else 0
values["FCA_ALT_ICON"] = 0
if values["ALERTS_2"] in [1, 2, 5, 6, 10, 21, 22]:
values["ALERTS_2"] = 0
values["DAW_ICON"] = 0
if values["ALERTS_1"] == 0: # alerts가 있으면 사운드도 같이 나옴
values["SOUNDS_1"] = 0
values["SOUNDS_2"] = 0
values["SOUNDS_4"] = 0
if values["ALERTS_3"] in [3, 4, 13, 17, 19, 26, 7, 8, 9, 10]:
values["ALERTS_3"] = 0
values["SOUNDS_3"] = 0
if values["ALERTS_5"] in [1, 2, 3, 4, 5]:
values["ALERTS_5"] = 0
if values["ALERTS_5"] in [11] and CS.softHoldActive == 0:
values["ALERTS_5"] = 0
# curvature 표시(0x161쪽 기존 로직 유지)
curvature = round(CS.out.steeringAngleDeg / 3)
values["LANELINE_CURVATURE"] = (min(abs(curvature), 15) + (-1 if curvature < 0 else 0)) if lat_active else 0
values["LANELINE_CURVATURE_DIRECTION"] = 1 if curvature < 0 and lat_active else 0
"""
lane_color = 4 if CS.out.leftLaneLine >= 20 or CS.out.leftBlindspot else 2
if hud_control.leftLaneDepart:
values["LANELINE_LEFT"] = 4 if (frame // 50) % 2 == 0 else 1
else:
values["LANELINE_LEFT"] = lane_color if hud_control.leftLaneVisible else 0
lane_color = 4 if CS.out.rightLaneLine >= 20 or CS.out.rightBlindspot else 2
if hud_control.rightLaneDepart:
values["LANELINE_RIGHT"] = 4 if (frame // 50) % 2 == 0 else 1
else:
values["LANELINE_RIGHT"] = lane_color if hud_control.rightLaneVisible else 0
"""
values["LCA_LEFT_ARROW"] = 2 if CS.out.leftBlinker else 0
values["LCA_RIGHT_ARROW"] = 2 if CS.out.rightBlinker else 0
values["LCA_LEFT_ICON"] = 1 if CS.out.leftBlindspot else 2
values["LCA_RIGHT_ICON"] = 1 if CS.out.rightBlindspot else 2
values["LANE_LEFT"] = 1 if desire in (1, 3) else 0
values["LANE_RIGHT"] = 1 if desire in (2, 4) else 0
ret.append(packer.make_can_msg("ADRV_0x161", CAN.ECAN, values))
if CS.adrv_200 is not None:
values = copy.copy(CS.adrv_200)
values["TauGapSet"] = hud_control.leadDistanceBars
ret.append(packer.make_can_msg("ADRV_0x200", CAN.ECAN, values))
if CS.adrv_1ea is not None:
values = copy.copy(CS.adrv_1ea)
# blinker hold
values['LEFT_BLINK_HOLD'] = 1 if lane_changing == 3 else 0
values['RIGHT_BLINK_HOLD'] = 1 if lane_changing == 4 else 0
_make_ccnc_values(
values, CS, lat_active, frame, hud_control,
lane_line=True,
corner_radar=True,
desire=desire,
# 기존대로 LR/RR만 깜빡임
blink_pairs=[('LR_DETECT', 'LR_DETECT_DISTANCE'),
('RR_DETECT', 'RR_DETECT_DISTANCE')],
blink_t=1.0
)
ret.append(packer.make_can_msg("ADRV_0x1ea", CAN.ECAN, values))
if False: #CS.ccnc_162 is not None:
values = copy.copy(CS.ccnc_162)
if hud_control.leadDistance > 0:
values["FF_DISTANCE"] = hud_control.leadDistance
ff_type = 3 if hud_control.leadRadar == 1 else 13
values["FF_DETECT"] = ff_type if hud_control.leadRelSpeed > -0.1 else ff_type + 1
_make_ccnc_values(
values, CS, lat_active, frame, hud_control,
lane_line=False,
corner_radar=True,
desire=0,
# 필요하면 162도 깜빡임 적용(원래 코드처럼 LR/RR만)
blink_pairs=[('LR_DETECT', 'LR_DETECT_DISTANCE'),
('RR_DETECT', 'RR_DETECT_DISTANCE')],
blink_t=1.0
)
if (left_lane_warning and not CS.out.leftBlinker) or (right_lane_warning and not CS.out.rightBlinker):
values["VIBRATE"] = 1
ret.append(packer.make_can_msg("CCNC_0x162", CAN.ECAN, values))
if HDA_CntrlModSta == 0:
enable_corner_radar(ret, packer, CAN, frame)
return ret

View File

@@ -1,6 +0,0 @@
from opendbc.car.interfaces import CarInterfaceBase
class CarrotCarInterface(CarInterfaceBase):
def _carrot_init(self, CP, ret):
return ret
pass

View File

@@ -1,183 +0,0 @@
from opendbc.car.interfaces import RadarInterfaceBase
from opendbc.car.hyundai.values import DBC, HyundaiFlags
from opendbc.can import CANParser
from opendbc.car import Bus, structs
from opendbc.car.hyundai.hyundaicanfd import CanBus
from openpilot.common.params import Params
import ast
RADAR_START_ADDR_CANFD1 = 0x210 # Group 1 (msg당 2 타겟)
RADAR_MSG_COUNT1 = 16
RADAR_START_ADDR_CANFD2 = 0x3A5 # Group 2 (msg당 1 타겟)
RADAR_MSG_COUNT2 = 32
# ===== 레이더 파서 클래스들 =====
class _RadarBase:
start_addr: int = 0
msg_count: int = 0
def build_parser(self, CP, CAN: CanBus) -> CANParser:
raise NotImplementedError
def parse_points(self, iface: "CarrotRadarInterface") -> list[structs.RadarData.RadarPoint]:
raise NotImplementedError
# 공통: trackId/pts 관리
def _get_pt(self, iface: "CarrotRadarInterface", key: int) -> structs.RadarData.RadarPoint:
if key not in iface.pts:
iface.pts[key] = structs.RadarData.RadarPoint()
iface.pts[key].trackId = iface.track_id
iface.track_id += 1
return iface.pts[key]
def _del_pt(self, iface: "CarrotRadarInterface", key: int) -> None:
if key in iface.pts:
del iface.pts[key]
class RadarCanFdGroup1(_RadarBase):
start_addr = RADAR_START_ADDR_CANFD1
msg_count = RADAR_MSG_COUNT1
def build_parser(self, CP, CAN: CanBus) -> CANParser:
# hyundai_canfd_radar_generated DBC를 쓰는 구조를 가정 (당신 기존 코드 방향 그대로)
messages = [(f"RADAR_TRACK_{addr:x}", 20) for addr in range(self.start_addr, self.start_addr + self.msg_count)]
return CANParser("hyundai_canfd_radar_generated", messages, CAN.ACAN)
def parse_points(self, iface: "CarrotRadarInterface"):
for addr in range(self.start_addr, self.start_addr + self.msg_count):
msg = iface.rcp.vl[f"RADAR_TRACK_{addr:x}"]
# target1: key = addr
k1 = addr
valid1 = msg.get("VALID_CNT1", 0) > 10
if valid1:
pt = self._get_pt(iface, k1)
pt.measured = True
pt.dRel = msg["LONG_DIST1"]
pt.yRel = msg["LAT_DIST1"]
pt.vRel = msg["REL_SPEED1"]
pt.aRel = msg["REL_ACCEL1"]
pt.yvRel = msg["LAT_SPEED1"]
else:
self._del_pt(iface, k1)
# target2: key = addr + msg_count
k2 = addr + self.msg_count
valid2 = msg.get("VALID_CNT2", 0) > 10
if valid2:
pt = self._get_pt(iface, k2)
pt.measured = True
pt.dRel = msg["LONG_DIST2"]
pt.yRel = msg["LAT_DIST2"]
pt.vRel = msg["REL_SPEED2"]
pt.aRel = msg["REL_ACCEL2"]
pt.yvRel = msg["LAT_SPEED2"]
else:
self._del_pt(iface, k2)
return list(iface.pts.values())
class RadarCanFdGroup2(_RadarBase):
start_addr = RADAR_START_ADDR_CANFD2
msg_count = RADAR_MSG_COUNT2
def build_parser(self, CP, CAN: CanBus) -> CANParser:
messages = [(f"RADAR_TRACK_{addr:x}", 20) for addr in range(self.start_addr, self.start_addr + self.msg_count)]
return CANParser("hyundai_canfd_radar_generated", messages, CAN.ACAN)
def parse_points(self, iface: "CarrotRadarInterface"):
for addr in range(self.start_addr, self.start_addr + self.msg_count):
msg = iface.rcp.vl[f"RADAR_TRACK_{addr:x}"]
valid = msg.get("VALID_CNT", 0) > 10
if valid:
pt = self._get_pt(iface, addr)
pt.measured = True
pt.dRel = msg["LONG_DIST"]
pt.yRel = msg["LAT_DIST"]
pt.vRel = msg["REL_SPEED"]
pt.aRel = msg["REL_ACCEL"]
pt.yvRel = msg["LAT_SPEED"]
else:
self._del_pt(iface, addr)
return list(iface.pts.values())
class RadarSccFallback(_RadarBase):
# SCC_CONTROL 기반 (레이더 트랙이 없을 때 임시)
start_addr = 0x1A0
msg_count = 1
def build_parser(self, CP, CAN: CanBus) -> CANParser:
msgs = [("SCC_CONTROL", 50)]
bus = CAN.ECAN
bus = CAN.CAM if CP.flags & HyundaiFlags.CAMERA_SCC else bus
return CANParser(DBC[CP.carFingerprint][Bus.pt], msgs, bus)
def parse_points(self, iface: "CarrotRadarInterface"):
msg = iface.rcp.vl["SCC_CONTROL"]
d_rel = msg.get("ACC_ObjDist", 0.0)
valid = 0.0 < d_rel < 150.0
key = self.start_addr
if valid:
pt = self._get_pt(iface, key)
pt.measured = True
pt.dRel = d_rel
pt.yRel = 0.0
pt.vRel = msg.get("ACC_ObjRelSpd", 0.0)
pt.aRel = float("nan")
pt.yvRel = float("nan")
else:
self._del_pt(iface, key)
return list(iface.pts.values())
# ===== CarrotRadarInterface =====
class CarrotRadarInterface(RadarInterfaceBase):
def _carrot_init(self, CP):
CAN = CanBus(CP)
self.canfd = bool(CP.flags & HyundaiFlags.CANFD)
self.radar = None
self.track_id = 0
if not self.canfd:
return
fingerprints_str = Params().get("FingerPrints")
fingerprints = ast.literal_eval(fingerprints_str)
if RADAR_START_ADDR_CANFD1 in fingerprints[CAN.ACAN]:
self.radar = RadarCanFdGroup1()
elif RADAR_START_ADDR_CANFD2 in fingerprints[CAN.ACAN]:
self.radar = RadarCanFdGroup2()
else:
self.radar = RadarSccFallback()
self.radar_start_addr = self.radar.start_addr
self.radar_msg_count = self.radar.msg_count
self.rcp = self.radar.build_parser(CP, CAN)
self.trigger_msg = self.radar_start_addr + self.radar_msg_count - 1
def _carrot_update(self, updated_messages):
ret = structs.RadarData()
if self.rcp is None:
return ret
if not self.rcp.can_valid:
ret.errors.canError = True
return ret
if self.radar is None:
return ret
ret.points = self.radar.parse_points(self)
return ret

View File

@@ -43,8 +43,8 @@ static void cuatro_set_bootkick(BootState state) {
//set_gpio_output(GPIOC, 12, state != BOOT_RESET);
}
static void cuatro_set_amp_enabled(bool enabled){
set_gpio_output(GPIOA, 5, enabled);
static void cuatro_set_amp_enabled(bool enabled) {
set_gpio_output(GPIOB, 0, enabled);
}
static void cuatro_init(void) {

View File

@@ -14,8 +14,22 @@ void sound_tick(void) {
if (sound_idle_count > 0U) {
sound_idle_count--;
if (sound_idle_count == 0U) {
current_board->set_amp_enabled(false);
// 1) stop DMA cleanly
register_clear_bits(&DMA1_Stream1->CR, DMA_SxCR_EN);
while ((DMA1_Stream1->CR & DMA_SxCR_EN) != 0U) {}
// 2) clear DMA flags (2번이 하던 것)
DMA1->LIFCR = (0x3FU << 6);
// 3) force DAC to silence
DAC1->DHR12R1 = (1UL << 11);
DAC1->DHR12R2 = (1UL << 11);
// 4) (선택) disable DAC까지 해버리면 더 조용해질 수 있음
// register_clear_bits(&DAC1->CR, DAC_CR_EN1 | DAC_CR_EN2);
// 5) amp off last
current_board->set_amp_enabled(false);
}
}
@@ -105,9 +119,11 @@ void sound_init(void) {
REGISTER_INTERRUPT(DMA1_Stream0_IRQn, DMA1_Stream0_IRQ_Handler, 128U, FAULT_INTERRUPT_RATE_SOUND_DMA)
// Init DAC
DAC1->DHR12R1 = (1UL << 11);
DAC1->DHR12R2 = (1UL << 11);
register_set(&DAC1->MCR, 0U, 0xFFFFFFFFU);
register_set(&DAC1->CR, DAC_CR_TEN1 | (4U << DAC_CR_TSEL1_Pos) | DAC_CR_DMAEN1, 0xFFFFFFFFU);
register_set_bits(&DAC1->CR, DAC_CR_EN1);
register_set_bits(&DAC1->CR, DAC_CR_EN1 | DAC_CR_EN2);
// Setup DMAMUX (DAC_CH1_DMA as input)
register_set(&DMAMUX1_Channel1->CCR, 67U, DMAMUX_CxCR_DMAREQ_ID_Msk);

View File

@@ -20,26 +20,21 @@ dependencies = [
# core
"cffi",
"scons",
"pycapnp==2.1.0",
"pycapnp",
"Cython",
"setuptools",
"numpy >=2.0",
# body / webrtcd
"av",
"aiohttp",
"aiortc",
# aiortc does not put an upper bound on pyopenssl and is now incompatible
# with the latest release
"pyopenssl < 24.3.0",
"pyaudio",
# panda
"libusb1",
"spidev; platform_system == 'Linux'",
# modeld
"onnx >= 1.14.0",
# logging
"pyzmq",
"sentry-sdk",
@@ -67,7 +62,6 @@ dependencies = [
# ui
"raylib > 5.5.0.3",
"qrcode",
"mapbox-earcut",
"jeepney",
]
@@ -86,7 +80,6 @@ testing = [
"pytest-subtests",
# https://github.com/pytest-dev/pytest-xdist/pull/1229
"pytest-xdist @ git+https://github.com/sshane/pytest-xdist@2b4372bd62699fb412c4fe2f95bf9f01bd2018da",
"pytest-timeout",
"pytest-asyncio",
"pytest-mock",
"ruff",
@@ -95,17 +88,12 @@ testing = [
]
dev = [
"av",
"dictdiffer",
"matplotlib",
"opencv-python-headless",
"parameterized >=0.8, <0.9",
"pyautogui",
"pywinctl",
]
tools = [
"metadrive-simulator @ https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl ; (platform_machine != 'aarch64')",
"metadrive-simulator @ git+https://github.com/commaai/metadrive.git@minimal ; (platform_machine != 'aarch64')",
"dearpygui>=2.1.0; (sys_platform != 'linux' or platform_machine != 'aarch64')", # not vended for linux aarch64
]
@@ -129,7 +117,6 @@ cpp_files = "test_*"
cpp_harness = "selfdrive/test/cpp_harness.py"
python_files = "test_*.py"
asyncio_default_fixture_loop_scope = "function"
#timeout = "30" # you get this long by default
markers = [
"slow: tests that take awhile to run and can be skipped with -m 'not slow'",
"tici: tests that are only meant to run on the C3/C3X",

View File

@@ -8,10 +8,13 @@ from collections.abc import Callable
from cereal import log, car
import cereal.messaging as messaging
from openpilot.common.constants import CV
from openpilot.common.params import Params
from openpilot.common.git import get_short_branch
from openpilot.common.params import Params
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER
from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER
from openpilot.selfdrive.ui.feedback.feedbackd import FEEDBACK_MAX_DURATION
from openpilot.system.hardware import HARDWARE
AlertSize = log.SelfdriveState.AlertSize
AlertStatus = log.SelfdriveState.AlertStatus
@@ -194,11 +197,17 @@ class NormalPermanentAlert(Alert):
class StartupAlert(Alert):
def __init__(self, alert_text_1: str, alert_text_2: str = "항상 핸들을 잡고 도로를 주시하세요", alert_status=AlertStatus.normal):
alert_size = AlertSize.mid
if HARDWARE.get_device_type() == 'mici':
if alert_text_2 == "Always keep hands on wheel and eyes on road":
alert_text_2 = ""
alert_size = AlertSize.small
super().__init__(alert_text_1, alert_text_2,
alert_status, AlertSize.mid,
alert_status, alert_size,
Priority.LOWER, VisualAlert.none, AudibleAlert.none, 5.),
# ********** helper functions **********
def get_display_speed(speed_ms: float, metric: bool) -> str:
speed = int(round(speed_ms * (CV.MS_TO_KPH if metric else CV.MS_TO_MPH)))
@@ -252,6 +261,15 @@ def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messag
AlertStatus.normal, AlertSize.mid,
Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2)
def audio_feedback_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
duration = FEEDBACK_MAX_DURATION - ((sm['audioFeedback'].blockNum + 1) * SAMPLE_BUFFER / SAMPLE_RATE)
return NormalPermanentAlert(
"Recording Audio Feedback",
f"{round(duration)} second{'s' if round(duration) != 1 else ''} remaining. Press again to save early.",
priority=Priority.LOW)
def torque_nn_load_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
model_name = Params().get("NNFFModelName")
if model_name == "":
@@ -305,6 +323,21 @@ def calibration_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging
pitch = math.degrees(rpy[1] if len(rpy) == 3 else math.nan)
angles = f"장치 재장착 (Pitch: {pitch:.1f}°, Yaw: {yaw:.1f}°)"
return NormalPermanentAlert("캘리브레이션 오류", angles)
def paramsd_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
if not sm['liveParameters'].angleOffsetValid:
angle_offset_deg = sm['liveParameters'].angleOffsetDeg
title = "Steering misalignment detected"
text = f"Angle offset too high (Offset: {angle_offset_deg:.1f}°)"
elif not sm['liveParameters'].steerRatioValid:
steer_ratio = sm['liveParameters'].steerRatio
title = "Steer ratio mismatch"
text = f"Steering rack geometry may be off (Ratio: {steer_ratio:.1f})"
elif not sm['liveParameters'].stiffnessFactorValid:
stiffness_factor = sm['liveParameters'].stiffnessFactor
title = "Abnormal tire stiffness"
text = f"Check tires, pressure, or alignment (Factor: {stiffness_factor:.1f})"
else:
return NoEntryAlert("paramsd Temporary Error")
def overheat_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
@@ -355,6 +388,17 @@ def personality_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging
personality = str(personality).title()
return NormalPermanentAlert(f"Driving Personality: {personality}", duration=1.5)
def invalid_lkas_setting_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
text = "Toggle stock LKAS on or off to engage"
if CP.brand == "tesla":
text = "Switch to Traffic-Aware Cruise Control to engage"
elif CP.brand == "mazda":
text = "Enable your car's LKAS to engage"
elif CP.brand == "nissan":
text = "Disable your car's stock LKAS to engage"
return NormalPermanentAlert("Invalid LKAS setting", text)
def car_parser_result(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
results = Params().get("CanParserResult")
if results is None:
@@ -674,6 +718,11 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
visual_alert=VisualAlert.brakePressed),
},
EventName.steerDisengage: {
ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage),
ET.NO_ENTRY: NoEntryAlert("Steering Pressed"),
},
EventName.preEnableStandstill: {
ET.PRE_ENABLE: Alert(
"브레이크 해제 후 활성화하세요",
@@ -753,6 +802,11 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
ET.NO_ENTRY: NoEntryAlert("방해 수준이 너무높습니다"),
},
EventName.excessiveActuation: {
ET.SOFT_DISABLE: soft_disable_alert("Excessive Actuation"),
ET.NO_ENTRY: NoEntryAlert("Excessive Actuation"),
},
EventName.overheat: {
ET.PERMANENT: overheat_alert,
ET.SOFT_DISABLE: soft_disable_alert("장치 과열됨"),
@@ -978,6 +1032,14 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
ET.WARNING: personality_changed_alert,
},
EventName.userBookmark: {
ET.PERMANENT: NormalPermanentAlert("Bookmark Saved", duration=1.5),
},
EventName.audioFeedback: {
ET.PERMANENT: audio_feedback_alert,
},
EventName.softHold: {
ET.WARNING: Alert(
"SoftHold",
@@ -1076,6 +1138,72 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
},
}
if HARDWARE.get_device_type() == 'mici':
EVENTS.update({
EventName.preDriverDistracted: {
ET.PERMANENT: Alert(
"Pay Attention",
"",
AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, 2),
},
EventName.promptDriverDistracted: {
ET.PERMANENT: Alert(
"Pay Attention",
"Driver Distracted",
AlertStatus.userPrompt, AlertSize.mid,
Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, 1),
},
EventName.resumeRequired: {
ET.WARNING: Alert(
"Press Resume",
"",
AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, .2),
},
EventName.preLaneChangeLeft: {
ET.WARNING: Alert(
"Steer Left",
"Confirm Lane Change",
AlertStatus.normal, AlertSize.mid,
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1),
},
EventName.preLaneChangeRight: {
ET.WARNING: Alert(
"Steer Right",
"Confirm Lane Change",
AlertStatus.normal, AlertSize.mid,
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1),
},
EventName.laneChangeBlocked: {
ET.WARNING: Alert(
"Car in Blindspot",
"",
AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, .1),
},
EventName.steerSaturated: {
ET.WARNING: Alert(
"take control",
"turn exceeds limit",
AlertStatus.userPrompt, AlertSize.mid,
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 2.),
},
EventName.calibrationIncomplete: {
ET.PERMANENT: calibration_incomplete_alert,
ET.SOFT_DISABLE: soft_disable_alert("Calibration Incomplete"),
ET.NO_ENTRY: NoEntryAlert("Calibrating"),
},
EventName.reverseGear: {
ET.PERMANENT: Alert(
"Reverse",
"",
AlertStatus.normal, AlertSize.full,
Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5),
ET.USER_DISABLE: ImmediateDisableAlert("Reverse"),
ET.NO_ENTRY: NoEntryAlert("Reverse"),
},
})
if __name__ == '__main__':

View File

@@ -12,6 +12,9 @@ from openpilot.common.git import get_short_branch
from openpilot.common.params import Params
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER
from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER
from openpilot.selfdrive.ui.feedback.feedbackd import FEEDBACK_MAX_DURATION
from openpilot.system.hardware import HARDWARE
AlertSize = log.SelfdriveState.AlertSize
AlertStatus = log.SelfdriveState.AlertStatus
@@ -147,7 +150,7 @@ EmptyAlert = Alert("" , "", AlertStatus.normal, AlertSize.none, Priority.LOWEST,
class NoEntryAlert(Alert):
def __init__(self, alert_text_2: str,
alert_text_1: str = "openpilot不可用",
alert_text_1: str = "openpilot Unavailable",
visual_alert: car.CarControl.HUDControl.VisualAlert=VisualAlert.none):
super().__init__(alert_text_1, alert_text_2, AlertStatus.normal,
AlertSize.mid, Priority.LOW, visual_alert,
@@ -194,11 +197,17 @@ class NormalPermanentAlert(Alert):
class StartupAlert(Alert):
def __init__(self, alert_text_1: str, alert_text_2: str = "小鸽温馨提醒:请始终握住方向盘并注视前方道路", alert_status=AlertStatus.normal):
alert_size = AlertSize.mid
if HARDWARE.get_device_type() == 'mici':
if alert_text_2 == "Always keep hands on wheel and eyes on road":
alert_text_2 = ""
alert_size = AlertSize.small
super().__init__(alert_text_1, alert_text_2,
alert_status, AlertSize.mid,
alert_status, alert_size,
Priority.LOWER, VisualAlert.none, AudibleAlert.none, 5.),
# ********** helper functions **********
def get_display_speed(speed_ms: float, metric: bool) -> str:
speed = int(round(speed_ms * (CV.MS_TO_KPH if metric else CV.MS_TO_MPH)))
@@ -252,6 +261,15 @@ def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messag
AlertStatus.normal, AlertSize.mid,
Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2)
def audio_feedback_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
duration = FEEDBACK_MAX_DURATION - ((sm['audioFeedback'].blockNum + 1) * SAMPLE_BUFFER / SAMPLE_RATE)
return NormalPermanentAlert(
"Recording Audio Feedback",
f"{round(duration)} second{'s' if round(duration) != 1 else ''} remaining. Press again to save early.",
priority=Priority.LOW)
def torque_nn_load_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
model_name = Params().get("NNFFModelName")
if model_name == "":
@@ -373,6 +391,16 @@ def personality_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging
personality = str(personality).title()
return NormalPermanentAlert(f"驾驶风格:{personality}", duration=1.5)
def invalid_lkas_setting_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
text = "Toggle stock LKAS on or off to engage"
if CP.brand == "tesla":
text = "Switch to Traffic-Aware Cruise Control to engage"
elif CP.brand == "mazda":
text = "Enable your car's LKAS to engage"
elif CP.brand == "nissan":
text = "Disable your car's stock LKAS to engage"
return NormalPermanentAlert("Invalid LKAS setting", text)
def car_parser_result(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
results = Params().get("CanParserResult")
if results is None:
@@ -470,6 +498,9 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
AlertStatus.critical, AlertSize.full,
Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.stopStop, 2.),
ET.NO_ENTRY: NoEntryAlert("原厂AEB存在碰撞风险"),
EventName.stockLkas: {
ET.NO_ENTRY: NoEntryAlert("Stock LKAS: Lane Departure Detected"),
},
EventName.fcw: {
@@ -694,6 +725,11 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
visual_alert=VisualAlert.brakePressed),
},
EventName.steerDisengage: {
ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage),
ET.NO_ENTRY: NoEntryAlert("Steering Pressed"),
},
EventName.preEnableStandstill: {
ET.PRE_ENABLE: Alert(
"松开刹车以接入",
@@ -772,6 +808,10 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventName.tooDistracted: {
ET.NO_ENTRY: NoEntryAlert("分心程度过高"),
},
EventName.excessiveActuation: {
ET.SOFT_DISABLE: soft_disable_alert("Excessive Actuation"),
ET.NO_ENTRY: NoEntryAlert("Excessive Actuation"),
},
EventName.overheat: {
ET.PERMANENT: overheat_alert,
@@ -1004,6 +1044,14 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
ET.WARNING: personality_changed_alert,
},
EventName.userBookmark: {
ET.PERMANENT: NormalPermanentAlert("Bookmark Saved", duration=1.5),
},
EventName.audioFeedback: {
ET.PERMANENT: audio_feedback_alert,
},
EventName.softHold: {
ET.WARNING: Alert(
"软保持",
@@ -1102,6 +1150,72 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
},
}
if HARDWARE.get_device_type() == 'mici':
EVENTS.update({
EventName.preDriverDistracted: {
ET.PERMANENT: Alert(
"Pay Attention",
"",
AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, 2),
},
EventName.promptDriverDistracted: {
ET.PERMANENT: Alert(
"Pay Attention",
"Driver Distracted",
AlertStatus.userPrompt, AlertSize.mid,
Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, 1),
},
EventName.resumeRequired: {
ET.WARNING: Alert(
"Press Resume",
"",
AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, .2),
},
EventName.preLaneChangeLeft: {
ET.WARNING: Alert(
"Steer Left",
"Confirm Lane Change",
AlertStatus.normal, AlertSize.mid,
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1),
},
EventName.preLaneChangeRight: {
ET.WARNING: Alert(
"Steer Right",
"Confirm Lane Change",
AlertStatus.normal, AlertSize.mid,
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1),
},
EventName.laneChangeBlocked: {
ET.WARNING: Alert(
"Car in Blindspot",
"",
AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, .1),
},
EventName.steerSaturated: {
ET.WARNING: Alert(
"take control",
"turn exceeds limit",
AlertStatus.userPrompt, AlertSize.mid,
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 2.),
},
EventName.calibrationIncomplete: {
ET.PERMANENT: calibration_incomplete_alert,
ET.SOFT_DISABLE: soft_disable_alert("Calibration Incomplete"),
ET.NO_ENTRY: NoEntryAlert("Calibrating"),
},
EventName.reverseGear: {
ET.PERMANENT: Alert(
"Reverse",
"",
AlertStatus.normal, AlertSize.full,
Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5),
ET.USER_DISABLE: ImmediateDisableAlert("Reverse"),
ET.NO_ENTRY: NoEntryAlert("Reverse"),
},
})
if __name__ == '__main__':

View File

@@ -1,17 +1,33 @@
#!/usr/bin/env python3
import os
import glob
import onnx
from tinygrad.nn.onnx import OnnxPBParser
BASEDIR = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../"))
MASTER_PATH = os.getenv("MASTER_PATH", BASEDIR)
MODEL_PATH = "/selfdrive/modeld/models/"
class MetadataOnnxPBParser(OnnxPBParser):
def _parse_ModelProto(self) -> dict:
obj = {"metadata_props": []}
for fid, wire_type in self._parse_message(self.reader.len):
match fid:
case 14:
obj["metadata_props"].append(self._parse_StringStringEntryProto())
case _:
self.reader.skip_field(wire_type)
return obj
def get_checkpoint(f):
model = onnx.load(f)
metadata = {prop.key: prop.value for prop in model.metadata_props}
model = MetadataOnnxPBParser(f).parse()
metadata = {prop["key"]: prop["value"] for prop in model["metadata_props"]}
return metadata['model_checkpoint'].split('/')[0]
if __name__ == "__main__":
print("| | master | PR branch |")
print("|-| ----- | --------- |")
@@ -24,8 +40,4 @@ if __name__ == "__main__":
fn = os.path.basename(f)
master = get_checkpoint(MASTER_PATH + MODEL_PATH + fn)
pr = get_checkpoint(BASEDIR + MODEL_PATH + fn)
print(
"|", fn, "|",
f"[{master}](https://reporter.comma.life/experiment/{master})", "|",
f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|"
)
print("|", fn, "|", f"[{master}](https://reporter.comma.life/experiment/{master})", "|", f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|")

View File

@@ -6,7 +6,6 @@ cd $BASEDIR
export MAX_EXAMPLES=300
export INTERNAL_SEG_CNT=300
export FILEREADER_CACHE=1
export INTERNAL_SEG_LIST=selfdrive/car/tests/test_models_segs.txt
cd selfdrive/car/tests && pytest test_models.py test_car_interfaces.py

View File

@@ -1,15 +1,12 @@
import os
import math
import hypothesis.strategies as st
from hypothesis import Phase, given, settings
from parameterized import parameterized
from openpilot.common.parameterized import parameterized
from cereal import car
from opendbc.car import DT_CTRL
from opendbc.car.car_helpers import interfaces
from opendbc.car.structs import CarParams
from opendbc.car.tests.test_car_interfaces import get_fuzzy_car_interface_args
from opendbc.car.fw_versions import FW_VERSIONS, FW_QUERY_CONFIGS
from opendbc.car.tests.test_car_interfaces import get_fuzzy_car_interface
from opendbc.car.mock.values import CAR as MOCK
from opendbc.car.values import PLATFORMS
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle
@@ -18,11 +15,6 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
from openpilot.selfdrive.controls.lib.longcontrol import LongControl
from openpilot.selfdrive.test.fuzzy_generation import FuzzyGenerator
ALL_ECUS = {ecu for ecus in FW_VERSIONS.values() for ecu in ecus.keys()}
ALL_ECUS |= {ecu for config in FW_QUERY_CONFIGS.values() for ecu in config.extra_ecus}
ALL_REQUESTS = {tuple(r.request) for config in FW_QUERY_CONFIGS.values() for r in config.requests}
MAX_EXAMPLES = int(os.environ.get('MAX_EXAMPLES', '60'))
@@ -34,39 +26,8 @@ class TestCarInterfaces:
phases=(Phase.reuse, Phase.generate, Phase.shrink))
@given(data=st.data())
def test_car_interfaces(self, car_name, data):
CarInterface = interfaces[car_name]
args = get_fuzzy_car_interface_args(data.draw)
car_params = CarInterface.get_params(car_name, args['fingerprints'], args['car_fw'],
alpha_long=args['alpha_long'], is_release=False, docs=False)
car_params = car_params.as_reader()
car_interface = CarInterface(car_params)
assert car_params
assert car_interface
assert car_params.mass > 1
assert car_params.wheelbase > 0
# centerToFront is center of gravity to front wheels, assert a reasonable range
assert car_params.wheelbase * 0.3 < car_params.centerToFront < car_params.wheelbase * 0.7
assert car_params.maxLateralAccel > 0
# Longitudinal sanity checks
assert len(car_params.longitudinalTuning.kpV) == len(car_params.longitudinalTuning.kpBP)
assert len(car_params.longitudinalTuning.kiV) == len(car_params.longitudinalTuning.kiBP)
# Lateral sanity checks
if car_params.steerControlType != CarParams.SteerControlType.angle:
tune = car_params.lateralTuning
if tune.which() == 'pid':
if car_name != MOCK.MOCK:
assert not math.isnan(tune.pid.kf) and tune.pid.kf > 0
assert len(tune.pid.kpV) > 0 and len(tune.pid.kpV) == len(tune.pid.kpBP)
assert len(tune.pid.kiV) > 0 and len(tune.pid.kiV) == len(tune.pid.kiBP)
elif tune.which() == 'torque':
assert not math.isnan(tune.torque.kf) and tune.torque.kf > 0
assert not math.isnan(tune.torque.friction) and tune.torque.friction > 0
car_interface = get_fuzzy_car_interface(car_name, data.draw)
car_params = car_interface.CP.as_reader()
cc_msg = FuzzyGenerator.get_random_msg(data.draw, car.CarControl, real_floats=True)
# Run car interface
@@ -93,8 +54,8 @@ class TestCarInterfaces:
# hypothesis also slows down significantly with just one more message draw
LongControl(car_params)
if car_params.steerControlType == CarParams.SteerControlType.angle:
LatControlAngle(car_params, car_interface)
LatControlAngle(car_params, car_interface, DT_CTRL)
elif car_params.lateralTuning.which() == 'pid':
LatControlPID(car_params, car_interface)
LatControlPID(car_params, car_interface, DT_CTRL)
elif car_params.lateralTuning.which() == 'torque':
LatControlTorque(car_params, car_interface)
LatControlTorque(car_params, car_interface, DT_CTRL)

View File

@@ -2,11 +2,11 @@ import pytest
import itertools
import numpy as np
from parameterized import parameterized_class
from openpilot.common.parameterized import parameterized_class
from cereal import log
from openpilot.selfdrive.car.cruise import VCruiseHelper, V_CRUISE_MIN, V_CRUISE_MAX, V_CRUISE_INITIAL, IMPERIAL_INCREMENT
from cereal import car
from openpilot.common.conversions import Conversions as CV
from openpilot.common.constants import CV
from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver
ButtonEvent = car.CarState.ButtonEvent

View File

@@ -6,7 +6,7 @@ import unittest # noqa: TID251
from collections import defaultdict, Counter
import hypothesis.strategies as st
from hypothesis import Phase, given, settings
from parameterized import parameterized_class
from openpilot.common.parameterized import parameterized_class
from opendbc.car import DT_CTRL, gen_empty_fingerprint, structs
from opendbc.car.can_definitions import CanData
@@ -183,7 +183,7 @@ class TestCarModelBase(unittest.TestCase):
if tuning == 'pid':
self.assertTrue(len(self.CP.lateralTuning.pid.kpV))
elif tuning == 'torque':
self.assertTrue(self.CP.lateralTuning.torque.kf > 0)
self.assertTrue(self.CP.lateralTuning.torque.latAccelFactor > 0)
else:
raise Exception("unknown tuning")

View File

@@ -0,0 +1,40 @@
from openpilot.common.realtime import DT_CTRL
from openpilot.common.params import Params
class CarrotControls:
def __init__(self, CP):
self.CP = CP
self.params = Params()
self.lat_suspend_active = False
self.lat_suspend_enter_t = 0.0
self.lat_suspend_hold_t = 0.0
def lat_suspend_control(self, CS, latActive):
suspend_angle = float(self.params.get_int("LatSuspendAngleDeg"))
resume_angle = 15
delay_sec = 1.0
hold_sec = 0.5
# 1) enter condition timer
enter_cond = CS.steeringPressed and abs(CS.steeringAngleDeg) > suspend_angle
if not self.lat_suspend_active:
if enter_cond:
self.lat_suspend_enter_t += DT_CTRL
if self.lat_suspend_enter_t >= delay_sec:
self.lat_suspend_active = True
self.lat_suspend_hold_t = 0.0
else:
self.lat_suspend_enter_t = 0.0
# 2) while suspended: enforce minimum hold time + hysteresis exit
if self.lat_suspend_active:
self.lat_suspend_hold_t += DT_CTRL
exit_cond = (abs(CS.steeringAngleDeg) < resume_angle) and (not CS.steeringPressed)
if (self.lat_suspend_hold_t >= hold_sec) and exit_cond:
self.lat_suspend_active = False
self.lat_suspend_enter_t = 0.0
if self.lat_suspend_active:
latActive = False
return latActive

View File

@@ -73,7 +73,7 @@ class CarrotPlanner:
self.traffic_starting_count = 0
self.user_stop_distance = -1
#self.t_follow = 0
self.t_follow_last = 1.5
self.startSignCount = 0
self.stopSignCount = 0
@@ -281,8 +281,9 @@ class CarrotPlanner:
self.jerk_factor_apply = self.jerk_factor
if self.desireState > 0.9 and self.desireStateCount < int(1.5 / DT_MDL): # lane change state, 1.5초동안만.
t_follow *= self.dynamicTFollowLC # 차선변경시 t_follow를 줄임.
self.jerk_factor_apply = self.jerk_factor * self.dynamicTFollowLC # 차선변경시 jerk factor를 줄여 aggresive하게
dynamicTFollowLC = max(0.2, self.dynamicTFollowLC)
t_follow *= dynamicTFollowLC # 차선변경시 t_follow를 줄임.
self.jerk_factor_apply = self.jerk_factor * dynamicTFollowLC # 차선변경시 jerk factor를 줄여 aggresive하게
elif lead.status:
t_follow += np.interp(prev_a[0], [-2.0, -0.5], [0.1, 0.0])
if self.dynamicTFollow > 0.0:
@@ -292,6 +293,10 @@ class CarrotPlanner:
self.jerk_factor_apply = self.jerk_factor * 0.5 # 전방차량을 따라갈때는 aggressive하게.
#self.jerk_factor_apply = np.interp(abs(lead.jLead), [0, 2], [self.jerk_factor, self.jerk_factor * self.j_lead_factor])
if t_follow > self.t_follow_last:
t_follow = min(t_follow, self.t_follow_last + 0.1 * DT_MDL)
pass
self.t_follow_last = t_follow
return t_follow
def update_stop_dist(self, stop_x):

View File

@@ -260,55 +260,7 @@ class CarrotMan:
except Exception as e:
return f"Error: {e}"
def register_my_ip(self):
try:
token = "12345678"
local_ip = self.get_local_ip()
version = self.params.get("Version")
github_id = self.params.get("GithubUsername")
port = 7000
is_onroad = self.params.get_bool("IsOnroad")
ts = int(time.time())
url = "https://shind0.synology.me/carrot/api_heartbeat.php"
timeout_s = 3.5
payload = {
"github_id": github_id,
"token": token,
"local_ip": local_ip,
"port": int(port),
"version": version,
"is_onroad": bool(is_onroad),
"ts": int(time.time()),
}
#if extra:
# payload.update(extra)
data = json.dumps(payload).encode("utf-8")
print(data)
req = urllib.request.Request(
url=url,
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
try:
ctx = ssl._create_unverified_context()
with urllib.request.urlopen(req, timeout=timeout_s, context=ctx) as resp:
body = resp.read().decode("utf-8", errors="replace")
# 서버가 {"ok":true} 같은 JSON을 주는 경우가 많음
return (200 <= resp.status < 300), body
except urllib.error.HTTPError as e:
try:
body = e.read().decode("utf-8", errors="replace")
except Exception:
body = ""
return False, f"HTTPError {e.code}: {body}"
except Exception as e:
return False, f"Exception: {e}"
except Exception as e:
print(f"register_my_ip error: {e}")
# 브로드캐스트 메시지 전송
def broadcast_version_info(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -355,9 +307,6 @@ class CarrotMan:
if carrot_speed_active_count > 0:
self.carrot_speed_serv(carrot_speed, frame)
if frame % (20 * 30) == 0:
ok, msg = self.register_my_ip()
print(f"[heartbeat] ok: {ok}, msg: {msg}")
if frame % 20 == 0 or remote_addr is not None:
try:
self.broadcast_ip = self.get_broadcast_address() if remote_addr is None else remote_addr[0]

View File

@@ -31,6 +31,10 @@ from cereal import messaging
from opendbc.car import structs
import shlex
import shutil
import socket
import urllib.request
import urllib.error
import ssl
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -83,6 +87,93 @@ async def log_mw(request, handler):
WEBRTCD_URL = "http://127.0.0.1:5001/stream"
def _get_local_ip() -> str:
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("8.8.8.8", 80))
return s.getsockname()[0]
except Exception:
# fallback: hostname 방식(가끔 127.0.1.1 나올 수 있음)
try:
return socket.gethostbyname(socket.gethostname())
except Exception:
return "0.0.0.0"
def _register_my_ip_sync(params: "Params") -> tuple[bool, str]:
"""
기존 carrot_man.py의 register_my_ip()를 그대로 옮긴 버전 (동기)
"""
try:
token = "12345678"
local_ip = _get_local_ip()
version = params.get("Version")
github_id = params.get("GithubUsername")
port = 7000
is_onroad = params.get_bool("IsOnroad")
url = "https://shind0.synology.me/carrot/api_heartbeat.php"
timeout_s = 3.5
payload = {
"github_id": github_id,
"token": token,
"local_ip": local_ip,
"port": int(port),
"version": version,
"is_onroad": bool(is_onroad),
"ts": int(time.time()),
}
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
url=url,
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
ctx = ssl._create_unverified_context()
with urllib.request.urlopen(req, timeout=timeout_s, context=ctx) as resp:
body = resp.read().decode("utf-8", errors="replace")
return (200 <= resp.status < 300), body
except urllib.error.HTTPError as e:
try:
body = e.read().decode("utf-8", errors="replace")
except Exception:
body = ""
return False, f"HTTPError {e.code}: {body}"
except Exception as e:
return False, f"Exception: {e}"
async def heartbeat_loop(app: web.Application):
"""
aiohttp startup에서 create_task로 돌릴 백그라운드 루프
- 이벤트 루프 블로킹 방지 위해 to_thread 사용
"""
if not HAS_PARAMS:
app["hb_last"] = {"ok": False, "msg": "Params not available"}
return
params = Params()
interval_s = 30.0 # 기존: frame%(20*30) = 30초
while True:
try:
ok, msg = await asyncio.to_thread(_register_my_ip_sync, params)
app["hb_last"] = {
"ok": bool(ok),
"msg": str(msg)[:800],
"ts": time.time(),
"local_ip": _get_local_ip(),
}
# 원하면 로그
# print(f"[heartbeat] ok:{ok}, msg:{msg}")
except asyncio.CancelledError:
break
except Exception as e:
app["hb_last"] = {"ok": False, "msg": f"Exception: {e}", "ts": time.time()}
await asyncio.sleep(interval_s)
async def proxy_stream(request: web.Request) -> web.StreamResponse:
@@ -103,14 +194,28 @@ async def proxy_stream(request: web.Request) -> web.StreamResponse:
except Exception as e:
return web.json_response({"ok": False, "error": str(e)}, status=502)
async def api_heartbeat_status(request: web.Request) -> web.Response:
return web.json_response({"ok": True, "hb": request.app.get("hb_last")})
async def on_startup(app: web.Application):
app["http"] = ClientSession()
app["hb_last"] = {"ok": None, "msg": "not yet", "ts": 0}
if HAS_PARAMS:
app["hb_task"] = asyncio.create_task(heartbeat_loop(app))
async def on_cleanup(app: web.Application):
t = app.get("hb_task")
if t:
t.cancel()
try:
await t
except Exception:
pass
sess = app.get("http")
if sess:
await sess.close()
# -----------------------
# Settings cache (mtime based)
# -----------------------
@@ -134,20 +239,28 @@ def _group_index(settings: Dict[str, Any]) -> Tuple[Dict[str, list], Dict[str, D
params = settings.get("params", [])
for p in params:
g = p.get("group", "UNGROUPED")
g = p.get("group", "기타")
if g == "기타":
if "egroup" not in p: p["egroup"] = "Other"
if "cgroup" not in p: p["cgroup"] = "其他"
groups.setdefault(g, []).append(p)
n = p.get("name")
if n:
by_name[n] = p
# group list with egroup guess
# group list with egroup/cgroup guess
for g, items in groups.items():
egroup = None
cgroup = None
for it in items:
if it.get("egroup"):
if not egroup and it.get("egroup"):
egroup = it.get("egroup")
if not cgroup and it.get("cgroup"):
cgroup = it.get("cgroup")
if egroup and cgroup:
break
groups_list.append({"group": g, "egroup": egroup, "count": len(items)})
groups_list.append({"group": g, "egroup": egroup, "cgroup": cgroup, "count": len(items)})
return groups, by_name, groups_list
@@ -671,7 +784,7 @@ async def api_tools(request: web.Request) -> web.Response:
except Exception as e:
return web.json_response({"ok": False, "error": str(e)}, status=500)
async def ws_state(request: web.Request) -> web.WebSocketResponse:
ws = web.WebSocketResponse(heartbeat=20)
await ws.prepare(request)
@@ -776,13 +889,13 @@ async def ws_carstate(request: web.Request) -> web.WebSocketResponse:
temp_speed = { "speed": apply_speed, "source": apply_source if apply_speed >= v_cruise else "", "is_decel": True if apply_speed < v_cruise else False}
drive_mode = lp.myDrivingMode
if drive_mode == 1:
drive_mode_obj = {"name": "연비", "kind": "eco"}
drive_mode_obj = {"name": "Eco", "kind": "eco"}
elif drive_mode == 2:
drive_mode_obj = {"name": "안전", "kind": "safe"}
drive_mode_obj = {"name": "Safe", "kind": "safe"}
elif drive_mode == 4:
drive_mode_obj = {"name": "고속", "kind": "sport"}
drive_mode_obj = {"name": "Sport", "kind": "sport"}
else:
drive_mode_obj = {"name": "일반", "kind": "normal"}
drive_mode_obj = {"name": "Normal", "kind": "normal"}
gps_ok = True
@@ -831,10 +944,23 @@ async def ws_carstate(request: web.Request) -> web.WebSocketResponse:
"apm": " ",
}
await ws.send_str(json.dumps(payload))
try:
await ws.send_str(json.dumps(payload))
except (asyncio.CancelledError, GeneratorExit):
raise
except (ConnectionResetError, BrokenPipeError, web.HTTPException):
break
except Exception as e:
# aiohttp에서 클라이언트가 끊길 때 나는 대표 예외
if isinstance(e, (aiohttp.client_exceptions.ClientConnectionResetError,)):
break
if "Cannot write to closing transport" in str(e):
break
# traceback.print_exc()
break
await asyncio.sleep(0.1) # 10Hz
except Exception:
#traceback.print_exc()
traceback.print_exc()
pass
try:
@@ -982,7 +1108,7 @@ def make_app() -> web.Application:
app = web.Application(middlewares=[log_mw])
app.on_startup.append(on_startup)
app.on_cleanup.append(on_cleanup)
# static-like routes
app.router.add_get("/", handle_index)
app.router.add_get("/app.js", handle_appjs)

View File

@@ -2,7 +2,148 @@ const DEBUG_UI = false;
let SETTINGS = null;
let CURRENT_GROUP = null;
let LANG = "ko"; // "ko" | "en"
let LANG = "ko"; // "ko" | "en" | "zh"
const UI_STRINGS = {
ko: {
home: "홈",
setting: "설정",
tools: "도구",
fleet: "플릿",
lang: "언어",
server_state: "서버 상태",
quick_link: "퀵 링크",
car_select: "차량 선택",
makers: "제조사",
models: "모델",
groups: "그룹",
items: "항목",
back: "뒤로",
change: "변경",
git_commands: "Git 명령",
user_system: "사용자 / 시스템",
reboot: "재부팅",
backup: "백업",
restore: "복구",
apply: "적용",
confirm_car: "이 차량을 선택하시겠습니까?",
confirm_reboot: "지금 재부팅하시겠습니까?",
reboot_later: "선택되었습니다. 적용하려면 나중에 재부팅하세요.",
rebooting: "재부팅 중...",
git_sync_confirm: "Git sync를 실행하시겠습니까?",
git_reset_confirm: "Git reset을 실행하시겠습니까? (위험)",
delete_videos_confirm: "모든 비디오를 삭제하시겠습니까? (위험)",
delete_logs_confirm: "모든 로그를 삭제하시겠습니까? (위험)",
select_backup_file: "먼저 백업 json 파일을 선택하세요.",
restore_confirm: "파일에서 설정을 복구하시겠습니까?\n\n많은 Params 값이 덮어씌워집니다.",
restore_done_reboot: "복구가 완료되었습니다.\n지금 재부팅하시겠습니까?",
checkout_confirm: "브랜치를 체크아웃하시겠습니까?",
branch_changed: "브랜치가 변경되었습니다.",
quick_link_hint: "* 길게 눌러 링크저장",
git_hint: "* reset/branch는 위험할 수 있으니 confirm 뜹니다.",
sys_hint: "* delete/reboot는 confirm 후 실행합니다.",
restore_hint: "* restore 후 reboot 권장",
failed_set_car: "차량 선택 저장 실패: ",
reboot_failed: "재부팅 실패: ",
set_failed: "설정 실패: ",
branch_dom_missing: "브랜치 DOM 요소를 찾을 수 없습니다.",
fullscreen_not_supported: "이 브라우저는 전체화면을 지원하지 않습니다.",
},
en: {
home: "Home",
setting: "Setting",
tools: "Tools",
fleet: "Fleet",
lang: "Lang",
server_state: "Server State",
quick_link: "Quick Link",
car_select: "Car Select",
makers: "Makers",
models: "Models",
groups: "Groups",
items: "Items",
back: "Back",
change: "Change",
git_commands: "Git Commands",
user_system: "User / System",
reboot: "Reboot",
backup: "Backup",
restore: "Restore",
apply: "Apply",
confirm_car: "Select this car?",
confirm_reboot: "Reboot now?",
reboot_later: "Selected. Reboot later to apply.",
rebooting: "Rebooting...",
git_sync_confirm: "Run git sync?",
git_reset_confirm: "Run git reset? (DANGEROUS)",
delete_videos_confirm: "Delete ALL videos? (DANGEROUS)",
delete_logs_confirm: "Delete ALL logs? (DANGEROUS)",
select_backup_file: "Select a backup json file first.",
restore_confirm: "Restore settings from file?\n\nThis will overwrite many Params values.",
restore_done_reboot: "Restore done.\nReboot now?",
checkout_confirm: "Checkout branch?",
branch_changed: "Branch changed.",
quick_link_hint: "* Long press to save link",
git_hint: "* Reset/branch will prompt for confirmation.",
sys_hint: "* Delete/reboot will prompt for confirmation.",
restore_hint: "* Reboot recommended after restore.",
failed_set_car: "Failed to set car: ",
reboot_failed: "Reboot failed: ",
set_failed: "Set failed: ",
branch_dom_missing: "Branch DOM elements missing.",
fullscreen_not_supported: "Fullscreen not supported on this browser.",
},
zh: {
home: "首页",
setting: "设置",
tools: "工具",
fleet: "车队",
lang: "语言",
server_state: "服务器状态",
quick_link: "快速链接",
car_select: "车辆选择",
makers: "制造商",
models: "车型",
groups: "分组",
items: "项",
back: "返回",
change: "修改",
git_commands: "Git 命令",
user_system: "用户 / 系统",
reboot: "重启",
backup: "备份",
restore: "还原",
apply: "应用",
confirm_car: "选择此车辆吗?",
confirm_reboot: "现在重启吗?",
reboot_later: "已选择。请稍后重启以应用更改。",
rebooting: "正在重启...",
git_sync_confirm: "执行 Git 同步吗?",
git_reset_confirm: "执行 Git 重置吗?(危险)",
delete_videos_confirm: "删除所有视频吗?(危险)",
delete_logs_confirm: "删除所有日志吗?(危险)",
select_backup_file: "请先选择一个备份 JSON 文件。",
restore_confirm: "从文件还原设置吗?\n\n这将覆盖许多参数值。",
restore_done_reboot: "还原完成。\n现在重启吗",
checkout_confirm: "切换分支吗?",
branch_changed: "分支已切换。",
quick_link_hint: "* 长按保存链接",
git_hint: "* 重置/分支操作会弹出确认提示。",
sys_hint: "* 删除/重启操作会弹出确认提示。",
restore_hint: "* 还原后建议重启。",
failed_set_car: "保存车辆选择失败: ",
reboot_failed: "重启失败: ",
set_failed: "设置失败: ",
branch_dom_missing: "找不到分支 DOM 元素。",
fullscreen_not_supported: "此浏览器不支持全屏。",
}
};
const DRIVE_MODES = {
ko: { normal: "일반", eco: "연비", safe: "안전", sport: "고속" },
en: { normal: "Normal", eco: "Eco", safe: "Safe", sport: "Sport" },
zh: { normal: "标准", eco: "经济", safe: "安全", sport: "运动" }
};
let UNIT_CYCLE = [1, 2, 5, 10, 50, 100];
const UNIT_INDEX = {}; // per name
@@ -58,7 +199,7 @@ btnLang.onclick = () => toggleLang();
btnChangeCar.onclick = () => showPage("car", true);
btnBackCar.onclick = () => history.back();
carTitle.onclick = () => history.back();
modelTitle.onclick = () => showCarScreen("makers"); // 모델화면에서 타이틀 눌러 makers
modelTitle.onclick = () => showCarScreen("makers"); // <EFBFBD><EFBFBD>ȭ<EFBFBD><EFBFBD><EFBFBD> Ÿ<><C5B8>Ʋ <20><><EFBFBD><EFBFBD> makers<EFBFBD><EFBFBD>
// Branch select
let BRANCHES = [];
@@ -155,14 +296,69 @@ function showCarScreen(which, pushHistory = false) {
}
function toggleLang() {
LANG = (LANG === "ko") ? "en" : "ko";
langLabel.textContent = (LANG === "ko") ? "KO" : "EN";
if (LANG === "ko") LANG = "en";
else if (LANG === "en") LANG = "zh";
else LANG = "ko";
langLabel.textContent = LANG.toUpperCase();
// Update static UI text
renderUIText();
if (SETTINGS) {
renderGroups();
if (CURRENT_GROUP) renderItems(CURRENT_GROUP);
}
}
function renderUIText() {
const s = UI_STRINGS[LANG];
if (!s) return;
setText("btnHome", s.home);
setText("btnSetting", s.setting);
setText("btnTools", s.tools);
setText("btnFleet", s.fleet);
// langLabel is handled in toggleLang
// Home
setText("homeTitle", s.home);
setText("serverStateTitle", s.server_state);
setText("quickLinkTitle", s.quick_link);
// Car Select
setText("carTitle", s.car_select);
setText("btnBackCar", s.back);
setText("makersTitle", s.makers);
setText("modelTitle", s.models);
// Setting
setText("settingTitleText", s.setting); // Use a specific ID if needed
setText("btnBackGroups", s.back);
setText("btnChangeCar", s.change);
setText("groupsTitle", s.groups);
setText("itemsTitle", s.items);
// Tools
setText("toolsTitle", s.tools);
setText("btnToolsBack", s.back);
setText("gitCommandsTitle", s.git_commands);
setText("userSystemTitle", s.user_system);
setText("btnReboot", s.reboot);
setText("btnBackupSettings", s.backup);
setText("btnRestoreSettings", s.restore);
setText("quickLinkHint", s.quick_link_hint);
setText("gitHint", s.git_hint);
setText("sysHint", s.sys_hint);
setText("restoreHint", s.restore_hint);
}
function setText(id, txt) {
const el = document.getElementById(id);
if (el) el.textContent = txt;
}
function escapeHtml(s) {
return String(s)
.replaceAll("&", "&amp;")
@@ -173,6 +369,7 @@ function escapeHtml(s) {
}
function formatItemText(p, keyKo, keyEn, fallback = "") {
if (LANG === "zh") return (p["c" + keyEn.slice(1)] || p[keyEn] || p[keyKo] || fallback);
if (LANG === "ko") return (p[keyKo] ?? fallback);
return (p[keyEn] ?? p[keyKo] ?? fallback);
}
@@ -263,10 +460,10 @@ function renderModels(maker) {
modelTitle.textContent = maker;
modelMeta.textContent = `${arr.length} models`;
// 긴 목록이니까 버튼 폭/탭 편하게: groupBtn 재사용
// <EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>̴ϱ<CCB4> <20><>ư <20><>/<2F><> <20><><EFBFBD>ϰ<EFBFBD>: groupBtn <EFBFBD><EFBFBD><EFBFBD><EFBFBD>
for (const fullLine of arr) {
// fullLine : "Hyundai Grandeur 2018-19"
// CarSelected3에는 maker를 빼고 넣어야 함 → "Grandeur 2018-19"
// fullLine <EFBFBD><EFBFBD>: "Hyundai Grandeur 2018-19"
// CarSelected3<EFBFBD><EFBFBD><EFBFBD><EFBFBD> maker<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20>־<EFBFBD><D6BE> <20><> <20><> "Grandeur 2018-19"
const modelOnly = stripMaker(fullLine, maker);
const b = document.createElement("button");
@@ -278,33 +475,33 @@ function renderModels(maker) {
}
function stripMaker(fullLine, maker) {
// maker + 공백을 1번만 제거
// maker + <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 1<><31><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
const prefix = maker + " ";
if (fullLine.startsWith(prefix)) return fullLine.slice(prefix.length).trim();
// 혹시 "Hyundai"가 아닌 다른 표기면 fallback: 첫 단어 제거
// Ȥ<EFBFBD><EFBFBD> "Hyundai"<EFBFBD><EFBFBD> <20>ƴ<EFBFBD> <20>ٸ<EFBFBD> ǥ<><C7A5><EFBFBD> fallback: ù <20>ܾ<EFBFBD> <20><><EFBFBD><EFBFBD>
const sp = fullLine.split(" ");
if (sp.length >= 2) return sp.slice(1).join(" ").trim();
return fullLine.trim();
}
async function onSelectCar(maker, modelOnly, fullLine) {
const ok = confirm(`Select this car?\n\n${maker} ${modelOnly}\n\nThis will set CarSelected3 = "${modelOnly}".`);
if (!ok) return;
const msg = (UI_STRINGS[LANG].confirm_car || "Select this car?") + `\n\n${maker} ${modelOnly}\n\nThis will set CarSelected3 = "${modelOnly}".`;
if (!confirm(msg)) return;
try {
await setParam("CarSelected3", fullLine);
} catch (e) {
alert("Failed to set CarSelected3: " + e.message);
alert((UI_STRINGS[LANG].failed_set_car || "Failed to set car: ") + e.message);
return;
}
// Home 표시 업데이트
// Home ǥ<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʈ
curCarLabelCar.textContent = modelOnly;
curCarLabelSetting.textContent = modelOnly;
const rb = confirm("Reboot now?");
const rb = confirm(UI_STRINGS[LANG].confirm_reboot || "Reboot now?");
if (!rb) {
alert("Selected. Reboot later to apply.");
alert(UI_STRINGS[LANG].reboot_later || "Selected. Reboot later to apply.");
return;
}
@@ -312,9 +509,9 @@ async function onSelectCar(maker, modelOnly, fullLine) {
const r = await fetch("/api/reboot", { method: "POST" });
const j = await r.json();
if (!j.ok) throw new Error(j.error || "reboot failed");
alert("Rebooting...");
alert(UI_STRINGS[LANG].rebooting || "Rebooting...");
} catch (e) {
alert("Reboot failed: " + e.message);
alert((UI_STRINGS[LANG].reboot_failed || "Reboot failed: ") + e.message);
}
}
@@ -353,7 +550,10 @@ function renderGroups() {
box.innerHTML = "";
(SETTINGS.groups || []).forEach(g => {
const label = (LANG === "ko") ? g.group : (g.egroup || g.group);
let label = g.group;
if (LANG === "zh") label = g.cgroup || g.egroup || g.group;
else if (LANG === "en") label = g.egroup || g.group;
const b = document.createElement("button");
b.className = "btn groupBtn";
b.textContent = `${label} (${g.count})`;
@@ -466,7 +666,7 @@ async function renderItems(group) {
await setParam(name, next);
val.textContent = String(next);
} catch (e) {
alert("set failed: " + e.message);
alert((UI_STRINGS[LANG].set_failed || "set failed: ") + e.message);
}
}
@@ -546,7 +746,7 @@ window.addEventListener("popstate", async (ev) => {
if (st.page === "branch") {
showPage("branch", false);
// 브랜치 목록이 없으면 다시 로드
// <EFBFBD>귣ġ <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ٽ<EFBFBD> <20>ε<EFBFBD>
if (!BRANCHES || !BRANCHES.length) {
loadBranchesAndShow().catch(() => {});
}
@@ -580,7 +780,7 @@ async function runTool(action, payload) {
toolsMetaSet("running: " + action);
toolsOutSet("...");
// 서버에서 { ok:true, out:"...", rc:0 } 이런 형태로 주면 가장 좋음
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> { ok:true, out:"...", rc:0 } <EFBFBD>̷<EFBFBD> <20><><EFBFBD>·<EFBFBD> <20>ָ<EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
const j = await postJson("/api/tools", { action, ...(payload || {}) });
toolsMetaSet("done: " + action);
@@ -601,7 +801,7 @@ function confirmText(msg, placeholder = "") {
function initToolsPage() {
// 버튼 바인딩 (한 번만)
// <EFBFBD><EFBFBD>ư <20><><EFBFBD>ε<EFBFBD> (<28><> <20><><EFBFBD><EFBFBD>)
const bindOnce = (id, fn) => {
const el = document.getElementById(id);
if (!el || el.dataset.bound === "1") return;
@@ -622,7 +822,7 @@ function initToolsPage() {
});
bindOnce("btnGitSync", async () => {
if (!confirm("Run git sync?")) return;
if (!confirm(UI_STRINGS[LANG].git_sync_confirm || "Run git sync?")) return;
try {
await runTool("git_sync");
} catch (e) {
@@ -633,10 +833,10 @@ function initToolsPage() {
});
bindOnce("btnGitReset", async () => {
if (!confirm("Run git reset? (DANGEROUS)")) return;
if (!confirm(UI_STRINGS[LANG].git_reset_confirm || "Run git reset? (DANGEROUS)")) return;
// 옵션 필요하면 prompt로 받기
// : hard / soft, target
// <EFBFBD>ɼ<EFBFBD> <20>ʿ<EFBFBD><CABF>ϸ<EFBFBD> prompt<EFBFBD><EFBFBD> <20>ޱ<EFBFBD>
// <EFBFBD><EFBFBD>: hard / soft, target
const mode = confirmText("reset mode? (hard/soft/mixed)", "hard");
if (!mode) return;
@@ -671,7 +871,7 @@ function initToolsPage() {
});
bindOnce("btnDeleteVideos", async () => {
if (!confirm("Delete ALL videos? (DANGEROUS)")) return;
if (!confirm(UI_STRINGS[LANG].delete_videos_confirm || "Delete ALL videos? (DANGEROUS)")) return;
try {
await runTool("delete_all_videos");
} catch (e) {
@@ -682,7 +882,7 @@ function initToolsPage() {
});
bindOnce("btnDeleteLogs", async () => {
if (!confirm("Delete ALL logs? (DANGEROUS)")) return;
if (!confirm(UI_STRINGS[LANG].delete_logs_confirm || "Delete ALL logs? (DANGEROUS)")) return;
try {
await runTool("delete_all_logs");
} catch (e) {
@@ -695,7 +895,7 @@ function initToolsPage() {
bindOnce("btnBackupSettings", async () => {
try {
const j = await runTool("backup_settings");
if (j.file) window.location.href = j.file; // 다운로드
if (j.file) window.location.href = j.file; // <EFBFBD>ٿ<EFBFBD>ε<EFBFBD>
} catch (e) {
toolsMetaSet("error");
toolsOutSet("backup failed: " + e.message);
@@ -706,11 +906,11 @@ function initToolsPage() {
bindOnce("btnRestoreSettings", async () => {
const inp = document.getElementById("restoreFile");
if (!inp || !inp.files || !inp.files[0]) {
alert("Select a backup json file first.");
alert(UI_STRINGS[LANG].select_backup_file || "Select a backup json file first.");
return;
}
if (!confirm("Restore settings from file?\n\nThis will overwrite many Params values.")) return;
if (!confirm(UI_STRINGS[LANG].restore_confirm || "Restore settings from file?\n\nThis will overwrite many Params values.")) return;
try {
toolsMetaSet("uploading...");
@@ -726,7 +926,7 @@ function initToolsPage() {
toolsMetaSet("restore done");
toolsOutSet(JSON.stringify(j.result, null, 2));
if (confirm("Restore done.\nReboot now?")) {
if (confirm(UI_STRINGS[LANG].restore_done_reboot || "Restore done.\nReboot now?")) {
await runTool("reboot");
toolsMetaSet("rebooting...");
toolsOutSet("reboot requested");
@@ -739,9 +939,9 @@ function initToolsPage() {
});
bindOnce("btnReboot", async () => {
if (!confirm("Reboot now?")) return;
if (!confirm(UI_STRINGS[LANG].confirm_reboot || "Reboot now?")) return;
try {
// 네가 이미 만든 /api/reboot를 쓸 거면 이걸로 바꿔도 됨:
// <EFBFBD>װ<EFBFBD> <20>̹<EFBFBD> <20><><EFBFBD><EFBFBD> /api/reboot<6F><74> <20><> <20>Ÿ<EFBFBD> <20>̰ɷ<CCB0> <20>ٲ㵵 <20><>:
// await postJson("/api/reboot", {});
await runTool("reboot");
toolsMetaSet("rebooting...");
@@ -762,7 +962,7 @@ function initToolsPage() {
try {
const j = await runTool("shell_cmd", { cmd });
// j.out stdout/stderr 합친 결과
// j.out<EFBFBD><EFBFBD> stdout/stderr <EFBFBD><EFBFBD>ģ <20><><EFBFBD>
toolsOutSet(j.out || "(no output)");
} catch (e) {
toolsOutSet("error: " + e.message);
@@ -774,7 +974,7 @@ function initToolsPage() {
async function loadBranchesAndShow() {
showPage("branch", true);
if (!branchMeta || !branchList) {
alert("Branch DOM missing (branchMeta / branchList)");
alert(UI_STRINGS[LANG].branch_dom_missing || "Branch DOM missing");
return;
}
branchMeta.textContent = "loading...";
@@ -805,22 +1005,22 @@ function renderBranchList() {
}
async function onSelectBranch(branch) {
if (!confirm(`Checkout branch?\n\n${branch}\n\nContinue?`)) return;
if (!confirm((UI_STRINGS[LANG].checkout_confirm || "Checkout branch?") + `\n\n${branch}\n\nContinue?`)) return;
try {
await runTool("git_checkout", { branch });
alert("Branch changed.");
alert(UI_STRINGS[LANG].branch_changed || "Branch changed.");
} catch (e) {
alert("Checkout failed: " + e.message);
alert((UI_STRINGS[LANG].set_failed || "Checkout failed: ") + e.message);
return;
}
const rb = confirm("Reboot now?");
const rb = confirm(UI_STRINGS[LANG].confirm_reboot || "Reboot now?");
if (!rb) return;
try {
await runTool("reboot"); // 또는 /api/reboot
alert("Rebooting...");
await runTool("reboot"); // 또는 /api/reboot
alert(UI_STRINGS[LANG].rebooting || "Rebooting...");
} catch (e) {
alert("Reboot failed: " + e.message);
}
@@ -846,7 +1046,7 @@ function rtcCancelRetry() {
}
async function rtcDisconnect() {
rtcCancelRetry(); // 추가
rtcCancelRetry(); // <EFBFBD>߰<EFBFBD>
try { if (RTC_PC) RTC_PC.close(); } catch {}
RTC_PC = null;
const v = document.getElementById("rtcVideo");
@@ -859,7 +1059,7 @@ async function rtcDisconnect() {
}
function rtcScheduleRetry(ms = 2000) {
rtcCancelRetry(); // 항상 새로 잡는다
rtcCancelRetry(); // <EFBFBD>׻<EFBFBD> <20><><EFBFBD><EFBFBD> <20><>´<EFBFBD>
RTC_RETRY_T = setTimeout(async () => {
RTC_RETRY_T = null;
await rtcConnectOnce().catch(() => {});
@@ -968,7 +1168,7 @@ async function rtcConnectOnce() {
await waitIceComplete(pc, 8000);
const url = "/stream";
const url = "/stream";
const body = {
sdp: pc.localDescription.sdp,
cameras: ["road"],
@@ -997,8 +1197,8 @@ async function rtcConnectOnce() {
} catch (e) {
rtcStatusSet("error: " + e.message);
await rtcDisconnect(); // 실패 시 깨끗이 정리
rtcScheduleRetry(2000); // 여기서도 무조건 재시도
await rtcDisconnect(); // <EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
rtcScheduleRetry(2000); // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><>õ<EFBFBD>
throw e;
}
}
@@ -1007,7 +1207,7 @@ async function waitServerReady(timeoutMs = 8000) {
const t0 = Date.now();
while (Date.now() - t0 < timeoutMs) {
try {
// 서버 살아있는지만 확인 (가벼운 API)
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>ִ<EFBFBD><D6B4><EFBFBD><EFBFBD><EFBFBD> Ȯ<><C8AE> (<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> API)
const r = await fetch("/api/settings", { cache: "no-store" });
if (r.ok) return true;
} catch {}
@@ -1019,7 +1219,7 @@ async function waitServerReady(timeoutMs = 8000) {
function rtcInitAuto() {
(async () => {
rtcStatusSet("waiting server...");
await waitServerReady(8000); // 실패해도 계속 진행
await waitServerReady(8000); // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ص<EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
await rtcConnectOnce().catch(() => {});
})();
@@ -1031,11 +1231,11 @@ const btnRtcFs = document.getElementById("btnRtcFs");
const rtcVideoEl = document.getElementById("rtcVideo");
const rtcWrap = document.getElementById("rtcWrap");
// 유저 제스처에서만 호출되도록: 버튼 클릭 / 비디오 탭 이벤트에서만 실행
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>ó<EFBFBD><C3B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ȣ<><C8A3>ǵ<EFBFBD><C7B5><EFBFBD>: <20><>ư Ŭ<><C5AC> / <20><><EFBFBD><EFBFBD> <20><> <20>̺<EFBFBD>Ʈ<EFBFBD><C6AE><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
async function rtcToggleFullscreen() {
const target = rtcWrap || rtcVideoEl;
// 이미 풀스크린이면 종료
// <EFBFBD>̹<EFBFBD> Ǯ<><C7AE>ũ<EFBFBD><C5A9><EFBFBD≯<EFBFBD> <20><><EFBFBD><EFBFBD>
const fsEl = document.fullscreenElement || document.webkitFullscreenElement;
if (fsEl) {
if (document.exitFullscreen) await document.exitFullscreen().catch(()=>{});
@@ -1043,32 +1243,32 @@ async function rtcToggleFullscreen() {
return;
}
// 1) 표준 Fullscreen API (대부분의 크롬/안드/데스크탑)
// 1) ǥ<EFBFBD><EFBFBD> Fullscreen API (<EFBFBD><EFBFBD>κ<EFBFBD><EFBFBD><EFBFBD> ũ<><C5A9>/<2F>ȵ<EFBFBD>/<2F><><EFBFBD><EFBFBD>ũž)
if (target.requestFullscreen) {
await target.requestFullscreen().catch(()=>{});
return;
}
// 2) Safari (일부는 webkitRequestFullscreen)
// 2) Safari (<EFBFBD>Ϻδ<EFBFBD> webkitRequestFullscreen)
if (target.webkitRequestFullscreen) {
target.webkitRequestFullscreen();
return;
}
// 3) iOS Safari: video 전용 전체화면 (가장 잘 먹힘)
// (주의: iOS inline 재생/정책 때문에 이 방법이 더 안정적)
// 3) iOS Safari: video <EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><>üȭ<C3BC><C8AD> (<28><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD>)
// (<EFBFBD><EFBFBD><EFBFBD><EFBFBD>: iOS<EFBFBD><EFBFBD> inline <EFBFBD><EFBFBD><EFBFBD>/<2F><>å <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)
if (target.webkitEnterFullscreen) {
target.webkitEnterFullscreen();
return;
}
alert("Fullscreen not supported on this browser.");
alert(UI_STRINGS[LANG].fullscreen_not_supported || "Fullscreen not supported on this browser.");
}
// 버튼
// <EFBFBD><EFBFBD>ư
if (btnRtcFs) btnRtcFs.onclick = rtcToggleFullscreen;
// 비디오 탭(원하면)
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><>(<28><><EFBFBD>ϸ<EFBFBD>)
if (rtcVideoEl) {
rtcVideoEl.style.cursor = "pointer";
rtcVideoEl.addEventListener("click", rtcToggleFullscreen);
@@ -1153,7 +1353,7 @@ function drivingHudUpdateFromCarPayload(j) {
window.DrivingHud.update(payload);
}
function carWsConnect() {
// 이미 살아있으면 패스
// <EFBFBD>̹<EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>н<EFBFBD>
if (CAR_WS && (CAR_WS.readyState === WebSocket.OPEN || CAR_WS.readyState === WebSocket.CONNECTING)) return;
const wsProto = (location.protocol === "https:") ? "wss" : "ws";
@@ -1224,6 +1424,7 @@ async function updateQuickLink() {
function startAll() {
renderUIText();
showPage("home", false);
rtcInitAuto();
updateQuickLink().catch(() => {});

View File

@@ -49,7 +49,13 @@
function setDriveMode(name, kind){
const el = $("hudDriveMode");
if (!el) return;
el.textContent = name || "일반";
let modeName = name;
if (window.DRIVE_MODES && window.LANG) {
modeName = window.DRIVE_MODES[window.LANG][kind] || name;
}
el.textContent = modeName || (window.DRIVE_MODES && window.LANG ? window.DRIVE_MODES[window.LANG].normal : "Normal");
el.classList.remove("mode_normal","mode_eco","mode_safe","mode_sport");
if (kind === "eco") el.classList.add("mode_eco");
else if (kind === "safe") el.classList.add("mode_safe");
@@ -123,8 +129,16 @@
function setSys(cpuTempC, memPct, diskPct, diskLabel){
setText("hudCpuVal", (cpuTempC==null || !isFinite(cpuTempC)) ? "--°C" : `${cpuTempC.toFixed(0)}°C`);
setText("hudMemVal", (memPct==null || !isFinite(memPct)) ? "--%" : `${memPct.toFixed(0)}%`);
setText("hudDiskVal", (diskPct==null || !isFinite(diskPct)) ? "--%" : `${diskPct.toFixed(0)}%`);
if (diskLabel) setText("hudDiskLabel", diskLabel);
// VOLT 표시로 변경
if (diskPct == null || !isFinite(diskPct)) {
setText("hudDiskVal", "--V");
} else {
const volt = Number(diskPct) / 100.0;
setText("hudDiskVal", `${volt.toFixed(1)}V`);
}
if (diskLabel) setText("hudDiskLabel", "VOLT");
}
const DrivingHud = {
@@ -133,7 +147,7 @@
setBars(0);
setSignalDot("off");
setGps(false);
setDriveMode("일반","normal");
setDriveMode("","normal");
setRoadLimit(null, false);
setGapNum(null);
setGear("U");

View File

@@ -261,7 +261,7 @@
<div class="wrap">
<!-- HOME -->
<div id="pageHome" class="card">
<h3 style="margin:0 0 8px 0;">Home</h3>
<h3 id="homeTitle" style="margin:0 0 8px 0;">Home</h3>
<!-- Driving HUD (card mode). JS will auto-dock this into WebRTC overlay when video is visible. -->
<div id="driveHudCard" class="card" style="margin:12px 0 0 0;">
@@ -301,8 +301,8 @@
<div class="hudBottom">
<div class="hudLeftStack">
<div class="hudGps" id="hudGps">GPS</div>
<div class="hudDriveMode mode_normal" id="hudDriveMode">일반</div>
<div id="hudGps" class="hudGps">GPS</div>
<div class="hudDriveMode mode_normal" id="hudDriveMode">Normal</div>
</div>
<div class="hudRoadLimitBox">
@@ -324,7 +324,7 @@
<!-- Server State (기존 유지) -->
<div id="rtcCard" class="card" style="margin:12px 0 0 0; display:none;">
<h4 style="margin:0 0 8px 0;">WebRTC</h4>
<h4 id="webrtcTitle" style="margin:0 0 8px 0;">WebRTC</h4>
<div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
<div class="muted" id="rtcStatus">connecting...</div>
<button id="btnRtcFs" class="smallBtn">Full</button>
@@ -345,17 +345,17 @@
<div style="height:10px;"></div>
<div class="card" style="margin:0;">
<h4 style="margin:0 0 6px 0;">Server State</h4>
<h4 id="serverStateTitle" style="margin:0 0 6px 0;">Server State</h4>
<pre id="stateBox" class="pill" style="display:block; padding:10px; border-radius:14px;">connecting...</pre>
</div>
<div class="card" style="margin-top:12px;">
<h4 style="margin:0 0 6px 0;">Quick Link</h4>
<h4 id="quickLinkTitle" style="margin:0 0 6px 0;">Quick Link</h4>
<a id="quickLink" href="#" target="_blank" rel="noopener"
style="word-break:break-all; display:none; text-decoration:none; color:#9bb0c6;">
</a>
<div class="muted" style="margin-top:6px;">
<div id="quickLinkHint" class="muted" style="margin-top:6px;">
* 길게 눌러 링크저장
</div>
</div>
@@ -381,7 +381,7 @@
<!-- Maker screen -->
<div id="carScreenMakers" class="screen">
<div class="card" style="margin:0;">
<h4 style="margin:0 0 10px 0;">Makers</h4>
<h4 id="makersTitle" style="margin:0 0 10px 0;">Makers</h4>
<div id="makerList"></div>
</div>
</div>
@@ -402,7 +402,7 @@
<!-- SETTING -->
<div id="pageSetting" class="card" style="display:none;">
<div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
<h3 id="settingTitle" style="margin:0 0 8px 0; cursor:pointer;">Setting</h3>
<h3 id="settingTitle" style="margin:0 0 8px 0; cursor:pointer;"><span id="settingTitleText">Setting</span></h3>
<button id="btnBackGroups" class="btn" style="display:none;">Back</button>
</div>
@@ -421,7 +421,7 @@
<!-- Groups Screen -->
<div id="settingScreenGroups" class="screen">
<div class="card" style="margin:0;">
<h4 style="margin:0 0 10px 0;">Groups</h4>
<h4 id="groupsTitle" style="margin:0 0 10px 0;">Groups</h4>
<div id="groupList"></div>
</div>
</div>
@@ -442,7 +442,7 @@
<!-- TOOLS -->
<div id="pageTools" class="card" style="display:none;">
<div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
<h3 style="margin:0 0 8px 0;">Tools</h3>
<h3 id="toolsTitle" style="margin:0 0 8px 0;">Tools</h3>
<button id="btnToolsBack" class="btn">Back</button>
</div>
@@ -451,7 +451,7 @@
<!-- Git / Update -->
<div class="card" style="margin:0 0 12px 0;">
<h4 style="margin:0 0 10px 0;">Git Commands</h4>
<h4 id="gitCommandsTitle" style="margin:0 0 10px 0;">Git Commands</h4>
<div style="display:flex; gap:10px; flex-wrap:wrap;">
<button id="btnGitPull" class="btn">git pull</button>
@@ -460,14 +460,14 @@
<button id="btnGitBranch" class="btn">change branch</button>
</div>
<div class="muted" style="margin-top:10px;">
<div id="gitHint" class="muted" style="margin-top:10px;">
* reset/branch는 위험할 수 있으니 confirm 뜹니다.
</div>
</div>
<!-- System / User -->
<div class="card" style="margin:0;">
<h4 style="margin:0 0 10px 0;">User / System</h4>
<h4 id="userSystemTitle" style="margin:0 0 10px 0;">User / System</h4>
<div style="display:flex; gap:10px; flex-wrap:wrap;">
<button id="btnSendTmuxLog" class="btn">tmux log</button>
@@ -476,7 +476,7 @@
<button id="btnReboot" class="btn">reboot</button>
</div>
<div class="muted" style="margin-top:10px;">
<div id="sysHint" class="muted" style="margin-top:10px;">
* delete/reboot는 confirm 후 실행합니다.
</div>
@@ -491,7 +491,7 @@
<button id="btnRestoreSettings" class="btn">restore settings</button>
</div>
<div class="muted">
<div id="restoreHint" class="muted">
* restore 후 reboot 권장
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,8 @@ from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
from selfdrive.modeld.modeld import LAT_SMOOTH_SECONDS
from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose
from openpilot.selfdrive.carrot.carrot_controls import CarrotControls
State = log.SelfdriveState.OpenpilotState
LaneChangeState = log.LaneChangeState
LaneChangeDirection = log.LaneChangeDirection
@@ -73,6 +75,7 @@ class Controls:
self.LaC = LatControlPID(self.CP, self.CI)
elif self.CP.lateralTuning.which() == 'torque':
self.LaC = LatControlTorque(self.CP, self.CI)
self.carrot_controls = CarrotControls(self.CP)
def update(self):
self.sm.update(15)
@@ -119,6 +122,7 @@ class Controls:
standstill = abs(CS.vEgo) <= max(self.CP.minSteerSpeed, MIN_LATERAL_CONTROL_SPEED) or CS.standstill
CC.latActive = ((self.sm['selfdriveState'].active or lateral_enabled) and CS.latEnabled and
not CS.steerFaultTemporary and not CS.steerFaultPermanent and not standstill)
CC.latActive = self.carrot_controls.lat_suspend_control(CS, CC.latActive)
CC.longActive = CC.enabled and not any(e.overrideLongitudinal for e in self.sm['onroadEvents']) and self.CP.openpilotLongitudinalControl
actuators = CC.actuators

View File

@@ -2,103 +2,16 @@ from cereal import log
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
import numpy as np
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.common.params import Params
from collections import deque
LaneChangeState = log.LaneChangeState
LaneChangeDirection = log.LaneChangeDirection
TurnDirection = log.Desire
LANE_CHANGE_SPEED_MIN = 30 * CV.KPH_TO_MS
LANE_CHANGE_TIME_MAX = 10.
BLINKER_NONE = 0
BLINKER_LEFT = 1
BLINKER_RIGHT = 2
BLINKER_BOTH = 3
DESIRES = {
LaneChangeDirection.none: {
LaneChangeState.off: log.Desire.none,
LaneChangeState.preLaneChange: log.Desire.none,
LaneChangeState.laneChangeStarting: log.Desire.none,
LaneChangeState.laneChangeFinishing: log.Desire.none,
},
LaneChangeDirection.left: {
LaneChangeState.off: log.Desire.none,
LaneChangeState.preLaneChange: log.Desire.none,
LaneChangeState.laneChangeStarting: log.Desire.laneChangeLeft,
LaneChangeState.laneChangeFinishing: log.Desire.laneChangeLeft,
},
LaneChangeDirection.right: {
LaneChangeState.off: log.Desire.none,
LaneChangeState.preLaneChange: log.Desire.none,
LaneChangeState.laneChangeStarting: log.Desire.laneChangeRight,
LaneChangeState.laneChangeFinishing: log.Desire.laneChangeRight,
},
}
TURN_DESIRES = {
TurnDirection.none: log.Desire.none,
TurnDirection.turnLeft: log.Desire.turnLeft,
TurnDirection.turnRight: log.Desire.turnRight,
}
def calculate_lane_width_frog(lane, current_lane, road_edge):
lane_x, lane_y = np.array(lane.x), np.array(lane.y)
edge_x, edge_y = np.array(road_edge.x), np.array(road_edge.y)
current_x, current_y = np.array(current_lane.x), np.array(current_lane.y)
lane_y_interp = np.interp(current_x, lane_x[lane_x.argsort()], lane_y[lane_x.argsort()])
road_edge_y_interp = np.interp(current_x, edge_x[edge_x.argsort()], edge_y[edge_x.argsort()])
distance_to_lane = np.mean(np.abs(current_y - lane_y_interp))
distance_to_road_edge = np.mean(np.abs(current_y - road_edge_y_interp))
return min(distance_to_lane, distance_to_road_edge), distance_to_road_edge
def calculate_lane_width(lane, lane_prob, current_lane, road_edge):
# t ≈ 1초 앞 기준으로 차선/도로 가장자리까지의 거리 계산
t = 1.0
current_lane_y = np.interp(t, ModelConstants.T_IDXS, current_lane.y)
lane_y = np.interp(t, ModelConstants.T_IDXS, lane.y)
distance_to_lane = abs(current_lane_y - lane_y)
road_edge_y = np.interp(t, ModelConstants.T_IDXS, road_edge.y)
distance_to_road_edge = abs(current_lane_y - road_edge_y)
distance_to_road_edge_far = abs(current_lane_y - np.interp(2.0, ModelConstants.T_IDXS, road_edge.y))
lane_valid = lane_prob > 0.5
return min(distance_to_lane, distance_to_road_edge), distance_to_road_edge, distance_to_road_edge_far, lane_valid
class ExistCounter:
"""
존재 여부가 노이즈처럼 튀는 신호를 히스테리시스로 안정화해 주는 카운터.
- true가 일정 시간 이상 지속되면 counter를 양수로 증가
- false가 일정 시간 이상 지속되면 counter를 음수로 감소
"""
def __init__(self):
self.counter = 0
self.true_count = 0
self.false_count = 0
self.threshold = int(0.2 / DT_MDL) # 약 0.2초 이상 지속 시 유효로 판단
def update(self, exist_flag: bool):
if exist_flag:
self.true_count += 1
self.false_count = 0
if self.true_count >= self.threshold:
self.counter = max(self.counter + 1, 1)
else:
self.false_count += 1
self.true_count = 0
if self.false_count >= self.threshold:
self.counter = min(self.counter - 1, -1)
return self.true_count
from openpilot.selfdrive.controls.lib.desire_lib.constants import (
LaneChangeState, LaneChangeDirection, TurnDirection,
LANE_CHANGE_SPEED_MIN, LANE_CHANGE_TIME_MAX,
BLINKER_NONE, BLINKER_LEFT, BLINKER_RIGHT,
DESIRES, TURN_DESIRES
)
from openpilot.selfdrive.controls.lib.desire_lib.side_state import SideState
from openpilot.selfdrive.controls.lib.desire_lib.maneuver_classifier import classify_maneuver_type
class DesireHelper:
@@ -106,7 +19,7 @@ class DesireHelper:
self.params = Params()
self.frame = 0
# Lane change / turn 상태
# FSM core
self.lane_change_state = LaneChangeState.off
self.lane_change_direction = LaneChangeDirection.none
self.lane_change_timer = 0.0
@@ -114,82 +27,51 @@ class DesireHelper:
self.lane_change_delay = 0.0
self.maneuver_type = "none" # "none" / "turn" / "lane_change"
# Desire / turn 관련
self.desire = log.Desire.none
self.turn_direction = TurnDirection.none
self.enable_turn_desires = True
self.turn_desire_state = False
self.desire_disable_count = 0 # turn 후 일정 시간 동안 차선변경 금지
self.turn_disable_count = 0 # steeringAngle 매우 클 때 turn 금지
self.desire_disable_count = 0
self.turn_disable_count = 0
# Lane / road edge 상태
self.lane_width_left = 0.0
self.lane_width_right = 0.0
self.lane_width_left_diff = 0.0
self.lane_width_right_diff = 0.0
self.distance_to_road_edge_left = 0.0
self.distance_to_road_edge_right = 0.0
self.distance_to_road_edge_left_far = 0.0
self.distance_to_road_edge_right_far = 0.0
# per-side states
self.left = SideState("left")
self.right = SideState("right")
self.lane_exist_left_count = ExistCounter()
self.lane_exist_right_count = ExistCounter()
self.lane_width_left_count = ExistCounter()
self.lane_width_right_count = ExistCounter()
self.road_edge_left_count = ExistCounter()
self.road_edge_right_count = ExistCounter()
self.available_left_lane = False
self.available_right_lane = False
self.available_left_edge = False
self.available_right_edge = False
self.lane_width_left_queue = deque(maxlen=int(1.0 / DT_MDL))
self.lane_width_right_queue = deque(maxlen=int(1.0 / DT_MDL))
self.lane_available_last = False
self.edge_available_last = False
self.lane_available_trigger = False
self.lane_appeared = False
self.lane_line_info = 0
# Blinkers / ATC
# blinker/ATC state (원본 변수들 유지)
self.blinker_ignore = False
self.driver_blinker_state = BLINKER_NONE
self.carrot_blinker_state = BLINKER_NONE
self.carrot_lane_change_count = 0
self.carrot_cmd_index_last = 0
self.atc_type = ""
self.atc_active = 0 # 0: 없음, 1: ATC 동작, 2: 운전자와 ATC 상
self.atc_active = 0 # 0: 없음, 1: ATC 동작, 2: 충
# Auto lane change 관련
# auto lane change
self.auto_lane_change_enable = False
self.next_lane_change = False
self.blindspot_detected_counter = 0
# Keep pulse
# keep pulse
self.keep_pulse_timer = 0.0
# 파라미터
# params
self.laneChangeNeedTorque = 0
self.laneChangeBsd = 0
self.laneChangeDelay = 0.0
self.modelTurnSpeedFactor = 0.0
self.model_turn_speed = 0.0
self.model_turn_speed = 200.0
# 기타
# misc
self.prev_desire_enabled = False
self.desireLog = ""
self.object_detected_count = 0
# ★ 현재 차선 확률 (Ego 기준 좌/우)
self.cur_left_prob = 1.0 # laneLineProbs[1]
self.cur_right_prob = 1.0 # laneLineProbs[2]
self.current_lane_missing = False
# ─────────────────────────────────────────────
# Config / Model 관련
# ─────────────────────────────────────────────
# externally readable flags
self.lane_change_available_left = False
self.lane_change_available_right = False
# ─────────────────────────────────────────────
# params/model
# ─────────────────────────────────────────────
def _update_params_periodic(self):
if self.frame % 100 == 0:
self.laneChangeNeedTorque = self.params.get_int("LaneChangeNeedTorque")
@@ -206,97 +88,36 @@ class DesireHelper:
else:
self.model_turn_speed = 200.0
# ─────────────────────────────────────────────
# Lane / Edge 상태 계산
# ─────────────────────────────────────────────
def _check_lane_state(self, modeldata):
# 왼쪽: laneLines[0] vs 현재차선 laneLines[1], roadEdges[0]
lane_width_left, self.distance_to_road_edge_left, self.distance_to_road_edge_left_far, lane_prob_left = \
calculate_lane_width(modeldata.laneLines[0], modeldata.laneLineProbs[0],
modeldata.laneLines[1], modeldata.roadEdges[0])
# 오른쪽: laneLines[3] vs 현재차선 laneLines[2], roadEdges[1]
lane_width_right, self.distance_to_road_edge_right, self.distance_to_road_edge_right_far, lane_prob_right = \
calculate_lane_width(modeldata.laneLines[3], modeldata.laneLineProbs[3],
modeldata.laneLines[2], modeldata.roadEdges[1])
# 차선 존재 카운터 업데이트
self.lane_exist_left_count.update(lane_prob_left)
self.lane_exist_right_count.update(lane_prob_right)
# 1초 이동 평균 (노이즈 줄이기)
self.lane_width_left_queue.append(lane_width_left)
self.lane_width_right_queue.append(lane_width_right)
self.lane_width_left = float(np.mean(self.lane_width_left_queue))
self.lane_width_right = float(np.mean(self.lane_width_right_queue))
self.lane_width_left_diff = self.lane_width_left_queue[-1] - self.lane_width_left_queue[0]
self.lane_width_right_diff = self.lane_width_right_queue[-1] - self.lane_width_right_queue[0]
# 유효 차선/엣지 판단
min_lane_width = 2.5
self.lane_width_left_count.update(self.lane_width_left > min_lane_width)
self.lane_width_right_count.update(self.lane_width_right > min_lane_width)
self.road_edge_left_count.update(self.distance_to_road_edge_left > min_lane_width)
self.road_edge_right_count.update(self.distance_to_road_edge_right > min_lane_width)
available_count = int(0.2 / DT_MDL)
self.available_left_lane = self.lane_width_left_count.counter > available_count
self.available_right_lane = self.lane_width_right_count.counter > available_count
self.available_left_edge = self.road_edge_left_count.counter > available_count and self.distance_to_road_edge_left_far > min_lane_width
self.available_right_edge = self.road_edge_right_count.counter > available_count and self.distance_to_road_edge_right_far > min_lane_width
self.cur_left_prob = modeldata.laneLineProbs[1]
self.cur_right_prob = modeldata.laneLineProbs[2]
# ─────────────────────────────────────────────
# 모델 내 desire 상태 (turn 예측 등)
# ─────────────────────────────────────────────
def _check_desire_state(self, modeldata, carstate, maneuver_type):
desire_state = modeldata.meta.desireState
orientation_rate = abs(modeldata.orientationRate.z[5])
orientation_rate_future = abs(modeldata.orientationRate.z[15])
# turnLeft + turnRight 확률
self.turn_desire_state = (desire_state[1] + desire_state[2]) > 0.1
#if self.turn_desire_state:
# self.desire_disable_count = int(2.0 / DT_MDL)
#else:
# self.desire_disable_count = max(0, self.desire_disable_count - 1)
# steeringAngle 너무 크면 turn 자체를 일정 시간 막기
if maneuver_type == "turn" and abs(carstate.steeringAngleDeg) > 80 and orientation_rate_future < orientation_rate:
self.turn_disable_count = int(10.0 / DT_MDL)
else:
self.turn_disable_count = max(0, self.turn_disable_count - 1)
# ─────────────────────────────────────────────
# Blinkers / ATC 상태 업데이트
# blinkers/ATC (원본 로직 유지, side 계산은 별개)
# ─────────────────────────────────────────────
def _update_driver_blinker(self, carstate):
driver_blinker_state = carstate.leftBlinker * 1 + carstate.rightBlinker * 2
driver_blinker_changed = driver_blinker_state != self.driver_blinker_state
self.driver_blinker_state = driver_blinker_state
st = carstate.leftBlinker * 1 + carstate.rightBlinker * 2
changed = st != self.driver_blinker_state
self.driver_blinker_state = st
driver_desire_enabled = driver_blinker_state in [BLINKER_LEFT, BLINKER_RIGHT]
enabled = st in (BLINKER_LEFT, BLINKER_RIGHT)
if self.laneChangeNeedTorque < 0:
# 운전자 깜빡이를 무시하고 차선변경 안 하는 설정
driver_desire_enabled = False
enabled = False
return st, changed, enabled
return driver_blinker_state, driver_blinker_changed, driver_desire_enabled
def _update_atc_blinker(self, carrotMan, v_ego, driver_blinker_state):
"""
ATC에서 온 turn/lanechange 명령 기반 깜빡이 상태 갱신.
"""
def _update_atc_blinker(self, carrotMan, driver_blinker_state):
atc_type = carrotMan.atcType
atc_blinker_state = BLINKER_NONE
# ATC 기반 자동 차선변경 유지 시간
# 유지 카운트는 DesireHelper에서 관리
if self.carrot_lane_change_count > 0:
atc_blinker_state = self.carrot_blinker_state
elif carrotMan.carrotCmdIndex != self.carrot_cmd_index_last and carrotMan.carrotCmd == "LANECHANGE":
@@ -304,36 +125,33 @@ class DesireHelper:
self.carrot_lane_change_count = int(0.2 / DT_MDL)
self.carrot_blinker_state = BLINKER_LEFT if carrotMan.carrotArg == "LEFT" else BLINKER_RIGHT
atc_blinker_state = self.carrot_blinker_state
elif atc_type in ["turn left", "turn right"]:
# 네비 turn 안내: 속도 조건을 턴 쪽으로 강제
elif atc_type in ("turn left", "turn right"):
if self.atc_active != 2:
atc_blinker_state = BLINKER_LEFT if atc_type == "turn left" else BLINKER_RIGHT
self.atc_active = 1
self.blinker_ignore = False
elif atc_type in ["fork left", "fork right", "atc left", "atc right"]:
# 분기(lanechange에 가까움)
elif atc_type in ("fork left", "fork right", "atc left", "atc right"):
if self.atc_active != 2:
atc_blinker_state = BLINKER_LEFT if atc_type in ["fork left", "atc left"] else BLINKER_RIGHT
atc_blinker_state = BLINKER_LEFT if atc_type in ("fork left", "atc left") else BLINKER_RIGHT
self.atc_active = 1
else:
self.atc_active = 0
# 운전자 깜빡이와 ATC 깜빡이가 충돌할 경우 ATC 무효
# 충돌 시 ATC 무효
if driver_blinker_state != BLINKER_NONE and atc_blinker_state != BLINKER_NONE and driver_blinker_state != atc_blinker_state:
atc_blinker_state = BLINKER_NONE
self.atc_active = 2
atc_desire_enabled = atc_blinker_state in [BLINKER_LEFT, BLINKER_RIGHT]
atc_desire_enabled = atc_blinker_state in (BLINKER_LEFT, BLINKER_RIGHT)
# blinker_ignore일 때는 깜빡이 신호를 잠시 무시
# blinker_ignore
if driver_blinker_state == BLINKER_NONE:
self.blinker_ignore = False
if self.blinker_ignore:
driver_blinker_state = BLINKER_NONE
atc_blinker_state = BLINKER_NONE
atc_desire_enabled = False
# ATC 타입이 바뀌었으면 이번 프레임 무시 (안정화 목적)
# 타입 변경 1프레임 무시
if self.atc_type != atc_type:
atc_desire_enabled = False
self.atc_type = atc_type
@@ -341,186 +159,114 @@ class DesireHelper:
return atc_blinker_state, atc_desire_enabled
# ─────────────────────────────────────────────
# Turn / LaneChange 모드 분류
# per-side processing (핵심: 좌/우 모두 매 프레임 계산)
# ─────────────────────────────────────────────
def _process_sides(self, carstate, modeldata, radarState):
# geometry (좌/우)
# left: outer laneLines[0], current laneLines[1], edge[0], cur_prob laneLineProbs[1]
self.left.update_lane_geometry(
modeldata.laneLines[0], modeldata.laneLineProbs[0],
modeldata.laneLines[1],
modeldata.roadEdges[0],
cur_prob=modeldata.laneLineProbs[1],
)
# right: outer laneLines[3], current laneLines[2], edge[1], cur_prob laneLineProbs[2]
self.right.update_lane_geometry(
modeldata.laneLines[3], modeldata.laneLineProbs[3],
modeldata.laneLines[2],
modeldata.roadEdges[1],
cur_prob=modeldata.laneLineProbs[2],
)
def _classify_maneuver_type(self, blinker_state, carstate, old_type):
"""
깜빡이가 들어왔을 때 이번 조작이 turn인지 lane_change인지 분류.
- 너무 복잡하게 가지 않고, 현재 속도/감속/차선상태/모델 turn 상태 기준으로 점수화.
"""
if blinker_state == BLINKER_NONE:
return "none"
# lane line info (HUD용 raw는 기존대로 leftLaneLine/rightLaneLine)
self.left.update_lane_line_info(carstate.leftLaneLine)
self.right.update_lane_line_info(carstate.rightLaneLine)
v_kph = carstate.vEgo * CV.MS_TO_KPH
accel = carstate.aEgo
# BSD 설정
ignore_bsd = (self.laneChangeBsd < 0)
# 깜빡이 방향에 따라 참조할 lane/edge 선택
if blinker_state == BLINKER_LEFT:
lane_exist_counter = self.lane_exist_left_count.counter
lane_available = self.available_left_lane
edge_available = self.available_left_edge
lane_prob_side = self.cur_left_prob
edge_dist = self.distance_to_road_edge_left_far
else:
lane_exist_counter = self.lane_exist_right_count.counter
lane_available = self.available_right_lane
edge_available = self.available_right_edge
lane_prob_side = self.cur_right_prob
edge_dist = self.distance_to_road_edge_right_far
# obstacles
v_ego = carstate.vEgo
self.left.update_obstacles(v_ego, radarState.leadLeft, carstate.leftBlindspot, ignore_bsd, bsd_hold_sec=2.0)
self.right.update_obstacles(v_ego, radarState.leadRight, carstate.rightBlindspot, ignore_bsd, bsd_hold_sec=2.0)
score_turn = 0
# compute available (include BSD+object)
self.left.compute_lane_change_available(lane_line_info_lt_20=(self.left.lane_line_info_raw < 20), ignore_bsd=ignore_bsd)
self.right.compute_lane_change_available(lane_line_info_lt_20=(self.right.lane_line_info_raw < 20), ignore_bsd=ignore_bsd)
if v_kph < 30.0:
score_turn += 1
elif v_kph < 40.0 and accel < -1.0:
score_turn += 1
self.left.update_triggers()
self.right.update_triggers()
# 차로가 없고, 로드에지도 여유없고..
if v_kph < 40.0 and not lane_available and not edge_available:
score_turn += 1
# externally readable
self.lane_change_available_left = self.left.lane_change_available
self.lane_change_available_right = self.right.lane_change_available
# 차선이 잘 안 보이거나(교차로/삼거리 등)
if v_kph < 40.0 and lane_exist_counter < int(0.5 / DT_MDL):
score_turn += 1
# steeringAngle이 크면 턴에 가깝다고 본다
#if abs(carstate.steeringAngleDeg) > 45.0:
# score_turn += 1
# 모델이 이미 turn을 예측 중이면 가중치
if self.turn_desire_state:
score_turn += 1
# ATC가 turn 안내 중이면 가중치
if self.atc_type in ["turn left", "turn right"]:
score_turn += 2
elif self.atc_type in ["fork left", "fork right", "atc left", "atc right"]:
score_turn -= 2 # fork/atc는 lanechange 쪽에 더 가깝게
# ★ road edge가 충분히 멀면(교차로/넓은 공간으로 판단) 턴 쪽으로 가산점
edge_far = edge_dist > 4.0 # 튜닝 포인트 (4~6m 정도가 무난)
#if edge_far:
# score_turn += 1
current_lane_missing = lane_prob_side < 0.3
self.current_lane_missing = current_lane_missing
# 튜닝 포인트: score_turn 임계값
if score_turn >= 2:
#if current_lane_missing and edge_far:
if edge_far:
return "turn"
else:
return old_type
else:
return "lane_change"
def _get_selected_side(self, blinker_state: int) -> SideState:
return self.left if blinker_state == BLINKER_LEFT else self.right
# ─────────────────────────────────────────────
# 메인 업데이트 루틴
# main update
# ─────────────────────────────────────────────
def update(self, carstate, modeldata, lateral_active, lane_change_prob, carrotMan, radarState):
self.frame += 1
self._update_params_periodic()
self._make_model_turn_speed(modeldata)
# 카운터 감소
# counts
self.carrot_lane_change_count = max(0, self.carrot_lane_change_count - 1)
self.lane_change_delay = max(0.0, self.lane_change_delay - DT_MDL)
self.blindspot_detected_counter = max(0, self.blindspot_detected_counter - 1)
v_ego = carstate.vEgo
below_lane_change_speed = v_ego < LANE_CHANGE_SPEED_MIN
# Lane / desire 상태 갱신
self._check_lane_state(modeldata)
# per-side compute (좌/우 모두)
self._process_sides(carstate, modeldata, radarState)
# desire state from model
self._check_desire_state(modeldata, carstate, self.maneuver_type)
# 운전자 깜빡이
driver_blinker_state, driver_blinker_changed, driver_desire_enabled = self._update_driver_blinker(carstate)
# blinkers
driver_st, driver_changed, driver_enabled = self._update_driver_blinker(carstate)
atc_st, atc_enabled = self._update_atc_blinker(carrotMan, driver_st)
# BSD 설정
ignore_bsd = (self.laneChangeBsd < 0)
block_lanechange_bsd = (self.laneChangeBsd == 1)
desire_enabled = driver_enabled or atc_enabled
blinker_state = driver_st if driver_enabled else atc_st
# ATC 깜빡이
atc_blinker_state, atc_desire_enabled = self._update_atc_blinker(carrotMan, v_ego, driver_blinker_state)
# 선택된 side (FSM은 이 side만 참고)
side = self._get_selected_side(blinker_state) if blinker_state in (BLINKER_LEFT, BLINKER_RIGHT) else None
# 최종 깜빡이/Desire enabled 판단
desire_enabled = driver_desire_enabled or atc_desire_enabled
blinker_state = driver_blinker_state if driver_desire_enabled else atc_blinker_state
# lane_line_info (HUD용 등)
lane_line_info = carstate.leftLaneLine if blinker_state == BLINKER_LEFT else carstate.rightLaneLine
# BSD / 주변 차량 감지
if desire_enabled:
lane_exist_counter = self.lane_exist_left_count.counter if blinker_state == BLINKER_LEFT else self.lane_exist_right_count.counter
lane_available = self.available_left_lane if blinker_state == BLINKER_LEFT else self.available_right_lane
edge_available = self.available_left_edge if blinker_state == BLINKER_LEFT else self.available_right_edge
self.lane_appeared = self.lane_appeared or lane_exist_counter == int(0.2 / DT_MDL)
radar = radarState.leadLeft if blinker_state == BLINKER_LEFT else radarState.leadRight
side_object_dist = radar.dRel + radar.vLead * 4.0 if radar.status else 255
object_detected = side_object_dist < v_ego * 3.0
if object_detected:
self.object_detected_count = max(1, self.object_detected_count + 1)
# auto lane change trigger (기존 로직 유지하되 side 기반)
auto_lane_change_trigger = False
if desire_enabled and side is not None:
# carrot_lane_change_count>0이면 강제 허용
if self.carrot_lane_change_count > 0:
auto_lane_change_trigger = side.lane_change_available
else:
self.object_detected_count = min(-1, self.object_detected_count - 1)
lane_line_info_edge_detect = (lane_line_info % 10 in [0, 5] and self.lane_line_info not in [0, 5])
self.lane_line_info = lane_line_info % 10
# 기존 조건: edge_available + (trigger or appeared) + not side_object_detected
auto_lane_change_trigger = (
self.auto_lane_change_enable and
side.edge_available and
(side.lane_available_trigger or side.lane_appeared) and
(not side.side_object_detected) and
(side.bsd_hold_counter == 0)
)
self.desireLog = (
f"{side.name}:ALC={self.auto_lane_change_enable}, "
#f"L={side.lane_available},E={side.edge_available}, "
#f"T={side.lane_available_trigger},A={side.lane_appeared}, "
#f"OBJ={side.side_object_detected},BSD={side.bsd_hold_counter>0}"
)
else:
lane_exist_counter = 0
lane_available = True
edge_available = True
self.lane_appeared = False
self.lane_available_trigger = False
self.object_detected_count = 0
lane_line_info_edge_detect = False
self.lane_line_info = lane_line_info % 10
self.auto_lane_change_enable = False
self.next_lane_change = False
# 차선/엣지 기반 lane change 가능 여부
lane_change_available = (lane_available or edge_available) and lane_line_info < 20 # 20 미만이면 흰색 라인
# lane_available 변화 & 폭 변화로 lane_available_trigger 계산
self.lane_available_trigger = False
if blinker_state == BLINKER_LEFT:
lane_width_diff = self.lane_width_left_diff
distance_to_road_edge = self.distance_to_road_edge_left
lane_width_side = self.lane_width_left
else:
lane_width_diff = self.lane_width_right_diff
distance_to_road_edge = self.distance_to_road_edge_right
lane_width_side = self.lane_width_right
if lane_width_diff > 0.8 and (lane_width_side < distance_to_road_edge):
self.lane_available_trigger = True
edge_availabled = (not self.edge_available_last and edge_available)
side_object_detected = self.object_detected_count > -0.3 / DT_MDL
self.lane_appeared = self.lane_appeared and distance_to_road_edge < 4.0
# Auto lane change 트리거
if self.carrot_lane_change_count > 0:
auto_lane_change_blocked = False
auto_lane_change_trigger = lane_change_available
else:
auto_lane_change_blocked = ((atc_blinker_state == BLINKER_LEFT) and (driver_blinker_state != BLINKER_LEFT))
self.auto_lane_change_enable = self.auto_lane_change_enable and not auto_lane_change_blocked
auto_lane_change_trigger = self.auto_lane_change_enable and edge_available and (self.lane_available_trigger or self.lane_appeared) and not side_object_detected
self.desireLog = f"L:{self.auto_lane_change_enable},{auto_lane_change_blocked},E:{lane_available},{edge_available},A:{self.lane_available_trigger},{self.lane_appeared},{lane_width_diff:.1f},{lane_width_side:.1f},{distance_to_road_edge:.1f}={auto_lane_change_trigger}"
# 메인 상태머신
# 0) lateral 끊기거나 너무 오래 지속되면 리셋
# ───────────────────────── FSM ─────────────────────────
if not lateral_active or self.lane_change_timer > LANE_CHANGE_TIME_MAX:
self.lane_change_state = LaneChangeState.off
self.lane_change_direction = LaneChangeDirection.none
self.turn_direction = TurnDirection.none
self.maneuver_type = "none"
# 1) turn 후 일정시간 동안은 아무 것도 하지 않음
elif self.desire_disable_count > 0:
self.lane_change_state = LaneChangeState.off
self.lane_change_direction = LaneChangeDirection.none
@@ -528,95 +274,108 @@ class DesireHelper:
self.maneuver_type = "none"
else:
# 깜빡이 켜져 있을 때, 이번 조작이 turn인지 lane_change인지 먼저 분류
if desire_enabled:
new_type = self._classify_maneuver_type(blinker_state, carstate, self.maneuver_type)
# classify maneuver type using selected side
if desire_enabled and side is not None:
new_type = classify_maneuver_type(
blinker_state=blinker_state,
carstate=carstate,
side=side,
turn_desire_state=self.turn_desire_state,
atc_type=self.atc_type,
old_type=self.maneuver_type,
)
else:
new_type = "none"
# ★ 1) 원래 lane_change였는데 새로 보니 turn 조건 + 차선 없음이면 → 강제 전환 허용
if self.maneuver_type == "lane_change" and new_type == "turn" and self.lane_change_state not in [LaneChangeState.preLaneChange, LaneChangeState.laneChangeStarting]:
# 차선변경 도중에도 조건 만족 시 턴으로 스위칭
# switching rules
if self.maneuver_type == "lane_change" and new_type == "turn" and self.lane_change_state not in (
LaneChangeState.preLaneChange, LaneChangeState.laneChangeStarting
):
self.maneuver_type = "turn"
self.lane_change_state = LaneChangeState.off # FSM 리셋 후 turn 루트로
# ★ 2) 그 외에는 off/pre 상태에서만 모드 변경
self.lane_change_state = LaneChangeState.off
elif self.lane_change_state in (LaneChangeState.off, LaneChangeState.preLaneChange):
self.maneuver_type = new_type
# ─ TURN 모드 처리
if desire_enabled and self.maneuver_type == "turn" and self.enable_turn_desires: # and not carstate.standstill:
# ─ TURN mode
if desire_enabled and self.maneuver_type == "turn" and self.enable_turn_desires:
self.lane_change_state = LaneChangeState.off
if self.turn_disable_count > 0:
self.turn_direction = TurnDirection.none
self.lane_change_direction = LaneChangeDirection.none
else:
self.turn_direction = TurnDirection.turnLeft if blinker_state == BLINKER_LEFT else TurnDirection.turnRight
# 호환성을 위해 lane_change_direction도 turn과 동일하게 세팅
self.lane_change_direction = self.turn_direction
# ─ Lane Change FSM 처리
# ─ Lane change FSM ─
else:
self.turn_direction = TurnDirection.none
# LaneChangeState.off
if self.lane_change_state == LaneChangeState.off:
if desire_enabled and not self.prev_desire_enabled and not below_lane_change_speed:
if desire_enabled and not self.prev_desire_enabled and not below_lane_change_speed and side is not None:
self.lane_change_state = LaneChangeState.preLaneChange
self.lane_change_ll_prob = 1.0
self.lane_change_delay = self.laneChangeDelay
# 맨 끝 차선이 아니면, ATC 자동 차선변경 비활성
lane_exist_counter_side = self.lane_exist_left_count.counter if blinker_state == BLINKER_LEFT else self.lane_exist_right_count.counter
self.auto_lane_change_enable = False if lane_exist_counter_side > 0 or lane_change_available else True
# (원본 유지: 차선 존재하거나 geom 가능하면 auto off, 아니면 on)
lane_exist_counter_side = side.lane_exist_count.counter
lane_change_available_geom = side.lane_change_available_geom
self.auto_lane_change_enable = False if (lane_exist_counter_side > 0 or lane_change_available_geom) else True
self.next_lane_change = False
# LaneChangeState.preLaneChange
elif self.lane_change_state == LaneChangeState.preLaneChange:
self.lane_change_direction = LaneChangeDirection.left if blinker_state == BLINKER_LEFT else LaneChangeDirection.right
dir_map = {
LaneChangeDirection.left: (carstate.steeringTorque > 0, carstate.leftBlindspot),
LaneChangeDirection.right: (carstate.steeringTorque < 0, carstate.rightBlindspot),
}
torque_cond, blindspot_cond = dir_map.get(self.lane_change_direction, (False, False))
torque_applied = carstate.steeringPressed and torque_cond
blindspot_detected = blindspot_cond
# 차선이 일정시간 이상 안보이면 자동차선변경 허용
lane_exist_counter_side = self.lane_exist_left_count.counter if blinker_state == BLINKER_LEFT else self.lane_exist_right_count.counter
if not lane_available or lane_exist_counter_side < int(2.0 / DT_MDL):
self.auto_lane_change_enable = True
# BSD
if blindspot_detected and not ignore_bsd:
self.blindspot_detected_counter = int(1.5 / DT_MDL)
if not desire_enabled or below_lane_change_speed:
if side is None:
self.lane_change_state = LaneChangeState.off
self.lane_change_direction = LaneChangeDirection.none
else:
# 차선변경 시작 조건
if (lane_change_available and self.lane_change_delay == 0) or lane_line_info_edge_detect:
if self.blindspot_detected_counter > 0 and not ignore_bsd:
if torque_applied and not block_lanechange_bsd:
self.lane_change_state = LaneChangeState.laneChangeStarting
elif self.laneChangeNeedTorque > 0 or self.next_lane_change:
if torque_applied:
self.lane_change_state = LaneChangeState.laneChangeStarting
elif driver_desire_enabled:
self.lane_change_state = LaneChangeState.laneChangeStarting
elif torque_applied or auto_lane_change_trigger or lane_line_info_edge_detect:
self.lane_change_state = LaneChangeState.laneChangeStarting
self.lane_change_direction = LaneChangeDirection.left if blinker_state == BLINKER_LEFT else LaneChangeDirection.right
# torque direction cond
torque_cond = (carstate.steeringTorque > 0) if blinker_state == BLINKER_LEFT else (carstate.steeringTorque < 0)
torque_applied = carstate.steeringPressed and torque_cond
# BSD config
ignore_bsd = (self.laneChangeBsd < 0)
block_lanechange_bsd = (self.laneChangeBsd == 1)
bsd_active = (side.bsd_hold_counter > 0) and (not ignore_bsd)
# 차선이 일정시간 이상 안보이면 auto 허용(원본 유지)
if (not side.lane_available) or (side.lane_exist_count.counter < int(2.0 / DT_MDL)):
self.auto_lane_change_enable = True
if not desire_enabled or below_lane_change_speed:
self.lane_change_state = LaneChangeState.off
self.lane_change_direction = LaneChangeDirection.none
else:
# 차선변경 시작 조건:
# - side.lane_change_available는 BSD+object 포함(요구사항)
# - 하지만 BSD 중에도 torque override 허용해야 하므로, BSD 분기를 별도로 둠(원본 동작 유지)
start_gate = (side.lane_change_available_geom and self.lane_change_delay == 0) or side.lane_line_info_edge_detect
if start_gate:
if bsd_active:
if torque_applied and (not block_lanechange_bsd):
self.lane_change_state = LaneChangeState.laneChangeStarting
elif self.laneChangeNeedTorque > 0 or self.next_lane_change:
if torque_applied:
self.lane_change_state = LaneChangeState.laneChangeStarting
elif driver_enabled:
# driver blinker면 바로 시작(원본 유지)
# 단, object/bzd 막힘은 side.lane_change_available에서 걸림
if side.lane_change_available:
self.lane_change_state = LaneChangeState.laneChangeStarting
else:
if torque_applied or auto_lane_change_trigger or side.lane_line_info_edge_detect:
# 여기서는 시작 직전 안전성 체크
if side.lane_change_available:
self.lane_change_state = LaneChangeState.laneChangeStarting
# LaneChangeState.laneChangeStarting
elif self.lane_change_state == LaneChangeState.laneChangeStarting:
# 원래 차선라인을 0.5초 동안 서서히 fade-out
self.lane_change_ll_prob = max(self.lane_change_ll_prob - 2 * DT_MDL, 0.0)
if lane_change_prob < 0.02 and self.lane_change_ll_prob < 0.01:
self.lane_change_state = LaneChangeState.laneChangeFinishing
# LaneChangeState.laneChangeFinishing
elif self.lane_change_state == LaneChangeState.laneChangeFinishing:
# 1초 동안 서서히 lane line 복귀
self.lane_change_ll_prob = min(self.lane_change_ll_prob + DT_MDL, 1.0)
if self.lane_change_ll_prob > 0.99:
self.lane_change_direction = LaneChangeDirection.none
@@ -626,34 +385,36 @@ class DesireHelper:
else:
self.lane_change_state = LaneChangeState.off
# lane_change_timer 관리
# timer
if self.lane_change_state in (LaneChangeState.off, LaneChangeState.preLaneChange):
self.lane_change_timer = 0.0
else:
self.lane_change_timer += DT_MDL
self.lane_available_last = lane_available
self.edge_available_last = edge_available
# commit last per-side
self.left.commit_last()
self.right.commit_last()
self.prev_desire_enabled = desire_enabled
# 운전자가 반대 방향으로 강하게 조향하면 해당 차선변경/턴 취소
steering_pressed_cancel = carstate.steeringPressed and \
((carstate.steeringTorque < 0 and blinker_state == BLINKER_LEFT) or
(carstate.steeringTorque > 0 and blinker_state == BLINKER_RIGHT))
# 반대 방향 토크로 cancel (기존 유지)
steering_pressed_cancel = carstate.steeringPressed and (
(carstate.steeringTorque < 0 and blinker_state == BLINKER_LEFT) or
(carstate.steeringTorque > 0 and blinker_state == BLINKER_RIGHT)
)
if steering_pressed_cancel and self.lane_change_state != LaneChangeState.off:
self.lane_change_direction = LaneChangeDirection.none
self.lane_change_state = LaneChangeState.off
self.blinker_ignore = True
# 최종 desire 결정
# final desire
if self.turn_direction != TurnDirection.none:
self.desire = TURN_DESIRES[self.turn_direction]
self.lane_change_direction = self.turn_direction
else:
self.desire = DESIRES[self.lane_change_direction][self.lane_change_state]
# keep pulse (LaneChangeState.preLaneChange에서 유지)
# keep pulse
if self.lane_change_state in (LaneChangeState.off, LaneChangeState.laneChangeStarting):
self.keep_pulse_timer = 0.0
elif self.lane_change_state == LaneChangeState.preLaneChange:
@@ -662,3 +423,5 @@ class DesireHelper:
self.keep_pulse_timer = 0.0
elif self.desire in (log.Desire.keepLeft, log.Desire.keepRight):
self.desire = log.Desire.none
return self.desire

View File

@@ -0,0 +1,106 @@
from dataclasses import dataclass
from .constants import BLINKER_NONE, BLINKER_LEFT, BLINKER_RIGHT
@dataclass
class BlinkerOutput:
driver_blinker_state: int
driver_blinker_changed: bool
driver_desire_enabled: bool
atc_blinker_state: int
atc_desire_enabled: bool
blinker_state: int
desire_enabled: bool
class BlinkerManager:
def __init__(self):
self.driver_blinker_state = BLINKER_NONE
self.carrot_blinker_state = BLINKER_NONE
self.carrot_lane_change_count = 0
self.carrot_cmd_index_last = 0
self.atc_type = ""
self.atc_active = 0 # 0: 없음, 1: ATC 동작, 2: 충돌
self.blinker_ignore = False
def tick(self):
self.carrot_lane_change_count = max(0, self.carrot_lane_change_count - 1)
def update_driver(self, carstate, laneChangeNeedTorque: int):
st = carstate.leftBlinker * 1 + carstate.rightBlinker * 2
changed = st != self.driver_blinker_state
self.driver_blinker_state = st
enabled = st in (BLINKER_LEFT, BLINKER_RIGHT)
if laneChangeNeedTorque < 0:
enabled = False
return st, changed, enabled
def update_atc(self, carrotMan, driver_blinker_state: int):
atc_type = carrotMan.atcType
atc_blinker_state = BLINKER_NONE
if self.carrot_lane_change_count > 0:
atc_blinker_state = self.carrot_blinker_state
elif carrotMan.carrotCmdIndex != self.carrot_cmd_index_last and carrotMan.carrotCmd == "LANECHANGE":
self.carrot_cmd_index_last = carrotMan.carrotCmdIndex
self.carrot_lane_change_count = int(0.2 / carrotMan.DT_MDL) if hasattr(carrotMan, "DT_MDL") else 0
self.carrot_blinker_state = BLINKER_LEFT if carrotMan.carrotArg == "LEFT" else BLINKER_RIGHT
atc_blinker_state = self.carrot_blinker_state
elif atc_type in ("turn left", "turn right"):
if self.atc_active != 2:
atc_blinker_state = BLINKER_LEFT if atc_type == "turn left" else BLINKER_RIGHT
self.atc_active = 1
self.blinker_ignore = False
elif atc_type in ("fork left", "fork right", "atc left", "atc right"):
if self.atc_active != 2:
atc_blinker_state = BLINKER_LEFT if atc_type in ("fork left", "atc left") else BLINKER_RIGHT
self.atc_active = 1
else:
self.atc_active = 0
# 운전자와 충돌하면 ATC 무효
if driver_blinker_state != BLINKER_NONE and atc_blinker_state != BLINKER_NONE and driver_blinker_state != atc_blinker_state:
atc_blinker_state = BLINKER_NONE
self.atc_active = 2
atc_desire_enabled = atc_blinker_state in (BLINKER_LEFT, BLINKER_RIGHT)
# ignore 처리
if driver_blinker_state == BLINKER_NONE:
self.blinker_ignore = False
if self.blinker_ignore:
atc_blinker_state = BLINKER_NONE
atc_desire_enabled = False
# 타입 바뀌면 1프레임 무시(안정화)
if self.atc_type != atc_type:
atc_desire_enabled = False
self.atc_type = atc_type
return atc_blinker_state, atc_desire_enabled, atc_type
def run(self, carstate, carrotMan, laneChangeNeedTorque: int):
self.tick()
driver_st, driver_changed, driver_enabled = self.update_driver(carstate, laneChangeNeedTorque)
atc_st, atc_enabled, _ = self.update_atc(carrotMan, driver_st)
desire_enabled = driver_enabled or atc_enabled
blinker_state = driver_st if driver_enabled else atc_st
return BlinkerOutput(
driver_blinker_state=driver_st,
driver_blinker_changed=driver_changed,
driver_desire_enabled=driver_enabled,
atc_blinker_state=atc_st,
atc_desire_enabled=atc_enabled,
blinker_state=blinker_state,
desire_enabled=desire_enabled,
)

View File

@@ -0,0 +1,41 @@
from cereal import log
from openpilot.common.constants import CV
LaneChangeState = log.LaneChangeState
LaneChangeDirection = log.LaneChangeDirection
TurnDirection = log.Desire
LANE_CHANGE_SPEED_MIN = 30 * CV.KPH_TO_MS
LANE_CHANGE_TIME_MAX = 10.0
BLINKER_NONE = 0
BLINKER_LEFT = 1
BLINKER_RIGHT = 2
BLINKER_BOTH = 3
DESIRES = {
LaneChangeDirection.none: {
LaneChangeState.off: log.Desire.none,
LaneChangeState.preLaneChange: log.Desire.none,
LaneChangeState.laneChangeStarting: log.Desire.none,
LaneChangeState.laneChangeFinishing: log.Desire.none,
},
LaneChangeDirection.left: {
LaneChangeState.off: log.Desire.none,
LaneChangeState.preLaneChange: log.Desire.none,
LaneChangeState.laneChangeStarting: log.Desire.laneChangeLeft,
LaneChangeState.laneChangeFinishing: log.Desire.laneChangeLeft,
},
LaneChangeDirection.right: {
LaneChangeState.off: log.Desire.none,
LaneChangeState.preLaneChange: log.Desire.none,
LaneChangeState.laneChangeStarting: log.Desire.laneChangeRight,
LaneChangeState.laneChangeFinishing: log.Desire.laneChangeRight,
},
}
TURN_DESIRES = {
TurnDirection.none: log.Desire.none,
TurnDirection.turnLeft: log.Desire.turnLeft,
TurnDirection.turnRight: log.Desire.turnRight,
}

View File

@@ -0,0 +1,21 @@
from openpilot.common.realtime import DT_MDL
class ExistCounter:
def __init__(self, sustain_sec: float = 0.2):
self.counter = 0
self.true_count = 0
self.false_count = 0
self.threshold = int(sustain_sec / DT_MDL)
def update(self, exist_flag: bool):
if exist_flag:
self.true_count += 1
self.false_count = 0
if self.true_count >= self.threshold:
self.counter = max(self.counter + 1, 1)
else:
self.false_count += 1
self.true_count = 0
if self.false_count >= self.threshold:
self.counter = min(self.counter - 1, -1)
return self.counter

View File

@@ -0,0 +1,15 @@
import numpy as np
from openpilot.selfdrive.modeld.constants import ModelConstants
def calculate_lane_width(lane, lane_prob, current_lane, road_edge):
t = 1.0
current_lane_y = np.interp(t, ModelConstants.T_IDXS, current_lane.y)
lane_y = np.interp(t, ModelConstants.T_IDXS, lane.y)
distance_to_lane = abs(current_lane_y - lane_y)
road_edge_y = np.interp(t, ModelConstants.T_IDXS, road_edge.y)
distance_to_road_edge = abs(current_lane_y - road_edge_y)
distance_to_road_edge_far = abs(current_lane_y - np.interp(2.0, ModelConstants.T_IDXS, road_edge.y))
lane_valid = lane_prob > 0.5
return min(distance_to_lane, distance_to_road_edge), distance_to_road_edge, distance_to_road_edge_far, lane_valid

View File

@@ -0,0 +1,45 @@
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
from .constants import BLINKER_LEFT, BLINKER_RIGHT
def classify_maneuver_type(blinker_state: int,
carstate,
side, # SideState
turn_desire_state: bool,
atc_type: str,
old_type: str):
if blinker_state == 0:
return "none"
v_kph = carstate.vEgo * CV.MS_TO_KPH
accel = carstate.aEgo
score_turn = 0
if v_kph < 30.0:
score_turn += 1
elif v_kph < 40.0 and accel < -1.0:
score_turn += 1
# 차로 없고 edge 여유도 없으면 turn 가산
if v_kph < 40.0 and (not side.lane_available) and (not side.edge_available):
score_turn += 1
# 차선이 잘 안 보이면(교차로 등)
if v_kph < 40.0 and side.lane_exist_count.counter < int(0.5 / DT_MDL):
score_turn += 1
if turn_desire_state:
score_turn += 1
if atc_type in ("turn left", "turn right"):
score_turn += 2
elif atc_type in ("fork left", "fork right", "atc left", "atc right"):
score_turn -= 2
edge_far = side.dist_to_edge_far > 4.0
if score_turn >= 2:
if edge_far:
return "turn"
return old_type
return "lane_change"

View File

@@ -0,0 +1,156 @@
from dataclasses import dataclass, field
from collections import deque
from typing import Optional
import numpy as np
from openpilot.common.realtime import DT_MDL
from openpilot.common.constants import CV
from .lane_math import calculate_lane_width
from .hysteresis import ExistCounter
@dataclass
class SideState:
name: str # "left" / "right"
# lane/edge distances
lane_width: float = 0.0
lane_width_diff: float = 0.0
dist_to_edge: float = 0.0
dist_to_edge_far: float = 0.0
# current lane prob (ego lane line prob on that side)
cur_prob: float = 1.0
current_lane_missing: bool = False
# counters
lane_exist_count: ExistCounter = field(default_factory=lambda: ExistCounter(0.2))
lane_width_count: ExistCounter = field(default_factory=lambda: ExistCounter(0.2))
edge_count: ExistCounter = field(default_factory=lambda: ExistCounter(0.2))
# availability
lane_available: bool = False
edge_available: bool = False
# smoothing
lane_width_queue: deque = field(default_factory=lambda: deque(maxlen=int(1.0 / DT_MDL)))
# lane line info
lane_line_info_raw: int = 0
lane_line_info_mod: int = 0
last_lane_line_mod: int = 0
lane_line_info_edge_detect: bool = False
# transitions
lane_available_last: bool = False
edge_available_last: bool = False
lane_available_trigger: bool = False
lane_appeared: bool = False
# obstacles
object_detected_count: int = 0
side_object_detected: bool = False
# BSD hold (after detection)
bsd_hold_counter: int = 0
bsd_detected_now: bool = False
# computed “lane change available” (includes BSD+object)
lane_change_available_geom: bool = False
lane_change_available: bool = False
lane_width_sum: float = 0.0
def update_lane_geometry(self,
lane_outer, lane_outer_prob,
lane_current,
road_edge,
cur_prob: float):
lane_w, dist_edge, dist_edge_far, lane_valid = calculate_lane_width(
lane_outer, lane_outer_prob, lane_current, road_edge
)
self.lane_exist_count.update(bool(lane_valid))
# running mean (O(1))
if len(self.lane_width_queue) == self.lane_width_queue.maxlen:
self.lane_width_sum -= self.lane_width_queue.popleft()
self.lane_width_queue.append(lane_w)
self.lane_width_sum += lane_w
self.lane_width = self.lane_width_sum / len(self.lane_width_queue)
self.lane_width_diff = (self.lane_width_queue[-1] - self.lane_width_queue[0]) if len(self.lane_width_queue) >= 2 else 0.0
self.dist_to_edge = float(dist_edge)
self.dist_to_edge_far = float(dist_edge_far)
min_lane_width = 2.5
self.lane_width_count.update(self.lane_width > min_lane_width)
self.edge_count.update(self.dist_to_edge > min_lane_width)
available_count = int(0.2 / DT_MDL)
self.lane_available = self.lane_width_count.counter > available_count
self.edge_available = (self.edge_count.counter > available_count) and (self.dist_to_edge_far > min_lane_width)
self.cur_prob = float(cur_prob)
self.current_lane_missing = self.cur_prob < 0.3
def update_lane_line_info(self, lane_line_info_raw: int):
self.lane_line_info_raw = int(lane_line_info_raw)
mod = self.lane_line_info_raw % 10
# edge_detect: 0/5로 바뀌는 순간 (기존은 좌/우가 같은 self.lane_line_info 공유라 버그성)
self.lane_line_info_edge_detect = (mod in (0, 5)) and (self.last_lane_line_mod not in (0, 5))
self.last_lane_line_mod = mod
self.lane_line_info_mod = mod
def update_obstacles(self,
v_ego: float,
radar_obj, # radarState.leadLeft / leadRight
blindspot: bool, # carstate.leftBlindspot/rightBlindspot
ignore_bsd: bool,
bsd_hold_sec: float = 2.0):
# object_detected (radar 기반)
if radar_obj is not None and radar_obj.status:
d = radar_obj.dRel
v = radar_obj.vLead
side_object_dist = d + v * 4.0
else:
side_object_dist = 255.0
object_detected = side_object_dist < (v_ego * 3.0)
if object_detected:
self.object_detected_count = max(1, self.object_detected_count + 1)
else:
self.object_detected_count = min(-1, self.object_detected_count - 1)
self.side_object_detected = self.object_detected_count > int(-0.3 / DT_MDL)
# BSD hold (요구사항: 검출 후 2초 유지)
self.bsd_detected_now = bool(blindspot)
if self.bsd_detected_now and not ignore_bsd:
self.bsd_hold_counter = int(bsd_hold_sec / DT_MDL)
else:
self.bsd_hold_counter = max(0, self.bsd_hold_counter - 1)
def compute_lane_change_available(self, lane_line_info_lt_20: bool, ignore_bsd: bool):
# geometric availability
self.lane_change_available_geom = (self.lane_available or self.edge_available) and lane_line_info_lt_20
# include bsd/object into lane_change_available (요구사항)
bsd_active = (self.bsd_hold_counter > 0) and (not ignore_bsd)
self.lane_change_available = self.lane_change_available_geom and (not self.side_object_detected) and (not bsd_active)
def update_triggers(self):
# lane_available_trigger (기존 로직 유지)
self.lane_available_trigger = False
if self.lane_width_diff > 0.8 and (self.lane_width < self.dist_to_edge):
self.lane_available_trigger = True
# lane_appeared (bugfix: == 말고 >=가 자연스러움)
# + edge가 너무 멀면(교차로) lane_appeared를 과도하게 true로 만들지 않게 제한
appeared_now = self.lane_exist_count.counter >= int(0.2 / DT_MDL)
self.lane_appeared = (self.lane_appeared or appeared_now) and (self.dist_to_edge < 4.0)
def commit_last(self):
self.lane_available_last = self.lane_available
self.edge_available_last = self.edge_available

View File

@@ -1,13 +1,18 @@
import pytest
import itertools
from parameterized import parameterized_class
from openpilot.common.parameterized import parameterized_class
from cereal import log
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import desired_follow_distance, get_T_FOLLOW
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import get_safe_obstacle_distance, get_stopped_equivalence_factor, get_T_FOLLOW
from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver
def desired_follow_distance(v_ego, v_lead, t_follow=None):
if t_follow is None:
t_follow = get_T_FOLLOW()
return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead)
def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personality=0):
man = Maneuver(
'',
@@ -37,4 +42,5 @@ class TestFollowingDistance:
simulation_steady_state = run_following_distance_simulation(v_lead, e2e=self.e2e, personality=self.personality)
correct_steady_state = desired_follow_distance(v_lead, v_lead, get_T_FOLLOW(self.personality))
err_ratio = 0.2 if self.e2e else 0.1
assert simulation_steady_state == pytest.approx(correct_steady_state, abs=err_ratio * correct_steady_state + .5)
abs_err_margin = 0.5 if v_lead > 0.0 else 1.15
assert simulation_steady_state == pytest.approx(correct_steady_state, abs=err_ratio * correct_steady_state + abs_err_margin)

View File

@@ -0,0 +1,45 @@
from openpilot.common.parameterized import parameterized
from cereal import car, log
from opendbc.car.car_helpers import interfaces
from opendbc.car.honda.values import CAR as HONDA
from opendbc.car.toyota.values import CAR as TOYOTA
from opendbc.car.nissan.values import CAR as NISSAN
from opendbc.car.gm.values import CAR as GM
from opendbc.car.vehicle_model import VehicleModel
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle
class TestLatControl:
@parameterized.expand([(HONDA.HONDA_CIVIC, LatControlPID), (TOYOTA.TOYOTA_RAV4, LatControlTorque),
(NISSAN.NISSAN_LEAF, LatControlAngle), (GM.CHEVROLET_BOLT_EUV, LatControlTorque)])
def test_saturation(self, car_name, controller):
CarInterface = interfaces[car_name]
CP = CarInterface.get_non_essential_params(car_name)
CI = CarInterface(CP)
VM = VehicleModel(CP)
controller = controller(CP.as_reader(), CI, DT_CTRL)
CS = car.CarState.new_message()
CS.vEgo = 30
CS.steeringPressed = False
params = log.LiveParametersData.new_message()
# Saturate for curvature limited and controller limited
for _ in range(1000):
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, True, 0.2)
assert lac_log.saturated
for _ in range(1000):
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, False, 0.2)
assert not lac_log.saturated
for _ in range(1000):
_, _, lac_log = controller.update(True, CS, VM, params, False, 1, False, 0.2)
assert lac_log.saturated

View File

@@ -0,0 +1,36 @@
from openpilot.common.parameterized import parameterized
from cereal import car, log
from opendbc.car.car_helpers import interfaces
from opendbc.car.toyota.values import CAR as TOYOTA
from opendbc.car.vehicle_model import VehicleModel
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque, LAT_ACCEL_REQUEST_BUFFER_SECONDS
def get_controller(car_name):
CarInterface = interfaces[car_name]
CP = CarInterface.get_non_essential_params(car_name)
CI = CarInterface(CP)
VM = VehicleModel(CP)
controller = LatControlTorque(CP.as_reader(), CI, DT_CTRL)
return controller, VM
class TestLatControlTorqueBuffer:
@parameterized.expand([(TOYOTA.TOYOTA_COROLLA_TSS2,)])
def test_request_buffer_consistency(self, car_name):
buffer_steps = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / DT_CTRL)
controller, VM = get_controller(car_name)
CS = car.CarState.new_message()
CS.vEgo = 30
CS.steeringPressed = False
params = log.LiveParametersData.new_message()
for _ in range(buffer_steps):
controller.update(True, CS, VM, params, False, 0.001, False, 0.2)
assert all(val != 0 for val in controller.lat_accel_request_buffer)
for _ in range(buffer_steps):
controller.update(False, CS, VM, params, False, 0.0, False, 0.2)
assert all(val == 0 for val in controller.lat_accel_request_buffer)

View File

@@ -26,6 +26,6 @@ class TestLeads:
msgs = [m for _ in range(3) for m in single_iter_pkg()]
out = replay_process_with_name("card", msgs, fingerprint=TOYOTA.TOYOTA_COROLLA_TSS2)
states = [m for m in out if m.which() == "liveTracks"]
failures = [not state.valid and len(state.liveTracks.errors) for state in states]
failures = [not state.valid for state in states]
assert len(states) == 0 or all(failures)

View File

@@ -0,0 +1,70 @@
import numpy as np
from cereal import car, messaging
from opendbc.car import ACCELERATION_DUE_TO_GRAVITY
from opendbc.car import structs
from opendbc.car.lateral import get_friction, FRICTION_THRESHOLD
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.locationd.torqued import TorqueEstimator, MIN_BUCKET_POINTS, POINTS_PER_BUCKET, STEER_BUCKET_BOUNDS
np.random.seed(0)
LA_ERR_STD = 1.0
INPUT_NOISE_STD = 0.08
V_EGO = 30.0
WARMUP_BUCKET_POINTS = (1.5*MIN_BUCKET_POINTS).astype(int)
STRAIGHT_ROAD_LA_BOUNDS = (0.02, 0.03)
ROLL_BIAS_DEG = 2.0
ROLL_COMPENSATION_BIAS = ACCELERATION_DUE_TO_GRAVITY*float(np.sin(np.deg2rad(ROLL_BIAS_DEG)))
TORQUE_TUNE = structs.CarParams.LateralTorqueTuning(latAccelFactor=2.0, latAccelOffset=0.0, friction=0.2)
TORQUE_TUNE_BIASED = structs.CarParams.LateralTorqueTuning(latAccelFactor=2.0, latAccelOffset=-ROLL_COMPENSATION_BIAS, friction=0.2)
def generate_inputs(torque_tune, la_err_std, input_noise_std=None):
rng = np.random.default_rng(0)
steer_torques = np.concat([rng.uniform(bnd[0], bnd[1], pts) for bnd, pts in zip(STEER_BUCKET_BOUNDS, WARMUP_BUCKET_POINTS, strict=True)])
la_errs = rng.normal(scale=la_err_std, size=steer_torques.size)
frictions = np.array([get_friction(la_err, 0.0, FRICTION_THRESHOLD, torque_tune) for la_err in la_errs])
lat_accels = torque_tune.latAccelFactor*steer_torques + torque_tune.latAccelOffset + frictions
if input_noise_std is not None:
steer_torques += rng.normal(scale=input_noise_std, size=steer_torques.size)
lat_accels += rng.normal(scale=input_noise_std, size=steer_torques.size)
return steer_torques, lat_accels
def get_warmed_up_estimator(steer_torques, lat_accels):
est = TorqueEstimator(car.CarParams())
for steer_torque, lat_accel in zip(steer_torques, lat_accels, strict=True):
est.filtered_points.add_point(steer_torque, lat_accel)
return est
def simulate_straight_road_msgs(est):
carControl = messaging.new_message('carControl').carControl
carOutput = messaging.new_message('carOutput').carOutput
carState = messaging.new_message('carState').carState
livePose = messaging.new_message('livePose').livePose
carControl.latActive = True
carState.vEgo = V_EGO
carState.steeringPressed = False
ts = DT_MDL*np.arange(2*POINTS_PER_BUCKET)
steer_torques = np.concat((np.linspace(-0.03, -0.02, POINTS_PER_BUCKET), np.linspace(0.02, 0.03, POINTS_PER_BUCKET)))
lat_accels = TORQUE_TUNE.latAccelFactor * steer_torques
for t, steer_torque, lat_accel in zip(ts, steer_torques, lat_accels, strict=True):
carOutput.actuatorsOutput.torque = float(-steer_torque)
livePose.orientationNED.x = float(np.deg2rad(ROLL_BIAS_DEG))
livePose.angularVelocityDevice.z = float(lat_accel / V_EGO)
for which, msg in (('carControl', carControl), ('carOutput', carOutput), ('carState', carState), ('livePose', livePose)):
est.handle_log(t, which, msg)
def test_estimated_offset():
steer_torques, lat_accels = generate_inputs(TORQUE_TUNE_BIASED, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD)
est = get_warmed_up_estimator(steer_torques, lat_accels)
msg = est.get_msg()
# TODO add lataccelfactor and friction check when we have more accurate estimates
assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.1
def test_straight_road_roll_bias():
steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD)
est = get_warmed_up_estimator(steer_torques, lat_accels)
simulate_straight_road_msgs(est)
msg = est.get_msg()
assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw)

View File

@@ -0,0 +1,135 @@
import random
import numpy as np
import time
import pytest
from cereal import messaging, log, car
from openpilot.selfdrive.locationd.lagd import LateralLagEstimator, retrieve_initial_lag, masked_normalized_cross_correlation, \
BLOCK_NUM_NEEDED, BLOCK_SIZE, MIN_OKAY_WINDOW_SEC
from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_carParams
from openpilot.selfdrive.locationd.test.test_locationd_scenarios import TEST_ROUTE
from openpilot.common.params import Params
from openpilot.tools.lib.logreader import LogReader
from openpilot.system.hardware import PC
MAX_ERR_FRAMES = 1
DT = 0.05
def process_messages(estimator, lag_frames, n_frames, vego=20.0, rejection_threshold=0.0):
for i in range(n_frames):
t = i * estimator.dt
desired_la = np.cos(10 * t) * 0.1
actual_la = np.cos(10 * (t - lag_frames * estimator.dt)) * 0.1
# if sample is masked out, set it to desired value (no lag)
rejected = random.uniform(0, 1) < rejection_threshold
if rejected:
actual_la = desired_la
desired_cuvature = float(desired_la / (vego ** 2))
actual_yr = float(actual_la / vego)
msgs = [
(t, "carControl", car.CarControl(latActive=not rejected)),
(t, "carState", car.CarState(vEgo=vego, steeringPressed=False)),
(t, "controlsState", log.ControlsState(desiredCurvature=desired_cuvature)),
(t, "livePose", log.LivePose(angularVelocityDevice=log.LivePose.XYZMeasurement(z=actual_yr, valid=True),
posenetOK=True, inputsOK=True)),
(t, "liveCalibration", log.LiveCalibrationData(rpyCalib=[0, 0, 0], calStatus=log.LiveCalibrationData.Status.calibrated)),
]
for t, w, m in msgs:
estimator.handle_log(t, w, m)
estimator.update_points()
estimator.update_estimate()
class TestLagd:
def test_read_saved_params(self):
params = Params()
lr = migrate(LogReader(TEST_ROUTE), [migrate_carParams])
CP = next(m for m in lr if m.which() == "carParams").carParams
msg = messaging.new_message('liveDelay')
msg.liveDelay.lateralDelayEstimate = random.random()
msg.liveDelay.validBlocks = random.randint(1, 10)
params.put("LiveDelay", msg.to_bytes())
params.put("CarParamsPrevRoute", CP.as_builder().to_bytes())
saved_lag_params = retrieve_initial_lag(params, CP)
assert saved_lag_params is not None
lag, valid_blocks = saved_lag_params
assert lag == msg.liveDelay.lateralDelayEstimate
assert valid_blocks == msg.liveDelay.validBlocks
def test_ncc(self):
lag_frames = random.randint(1, 19)
desired_sig = np.sin(np.arange(0.0, 10.0, 0.1))
actual_sig = np.sin(np.arange(0.0, 10.0, 0.1) - lag_frames * 0.1)
mask = np.ones(len(desired_sig), dtype=bool)
corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20]
assert np.argmax(corr) == lag_frames
# add some noise
desired_sig += np.random.normal(0, 0.05, len(desired_sig))
actual_sig += np.random.normal(0, 0.05, len(actual_sig))
corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20]
assert np.argmax(corr) in range(lag_frames - MAX_ERR_FRAMES, lag_frames + MAX_ERR_FRAMES + 1)
# mask out 40% of the values, and make them noise
mask = np.random.choice([True, False], size=len(desired_sig), p=[0.6, 0.4])
desired_sig[~mask] = np.random.normal(0, 1, size=np.sum(~mask))
actual_sig[~mask] = np.random.normal(0, 1, size=np.sum(~mask))
corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20]
assert np.argmax(corr) in range(lag_frames - MAX_ERR_FRAMES, lag_frames + MAX_ERR_FRAMES + 1)
def test_empty_estimator(self):
mocked_CP = car.CarParams(steerActuatorDelay=0.8)
estimator = LateralLagEstimator(mocked_CP, DT)
msg = estimator.get_msg(True)
assert msg.liveDelay.status == 'unestimated'
assert np.allclose(msg.liveDelay.lateralDelay, estimator.initial_lag)
assert np.allclose(msg.liveDelay.lateralDelayEstimate, estimator.initial_lag)
assert msg.liveDelay.validBlocks == 0
assert msg.liveDelay.calPerc == 0
def test_estimator_basics(self, subtests):
for lag_frames in range(5):
with subtests.test(msg=f"lag_frames={lag_frames}"):
mocked_CP = car.CarParams(steerActuatorDelay=0.8)
estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0)
process_messages(estimator, lag_frames, int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_NUM_NEEDED * BLOCK_SIZE)
msg = estimator.get_msg(True)
assert msg.liveDelay.status == 'estimated'
assert np.allclose(msg.liveDelay.lateralDelay, lag_frames * DT, atol=0.01)
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
assert msg.liveDelay.calPerc == 100
def test_estimator_masking(self):
mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(1, 19)
estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, min_valid_block_count=1)
process_messages(estimator, lag_frames, (int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_SIZE) * 2, rejection_threshold=0.4)
msg = estimator.get_msg(True)
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
assert msg.liveDelay.calPerc == 100
@pytest.mark.skipif(PC, reason="only on device")
def test_estimator_performance(self):
mocked_CP = car.CarParams(steerActuatorDelay=0.8)
estimator = LateralLagEstimator(mocked_CP, DT)
ds = []
for _ in range(1000):
st = time.perf_counter()
estimator.update_points()
estimator.update_estimate()
d = time.perf_counter() - st
ds.append(d)
assert np.mean(ds) < DT

View File

@@ -0,0 +1,67 @@
import random
import numpy as np
from cereal import messaging
from openpilot.selfdrive.locationd.paramsd import retrieve_initial_vehicle_params, migrate_cached_vehicle_params_if_needed
from openpilot.selfdrive.locationd.models.car_kf import CarKalman
from openpilot.selfdrive.locationd.test.test_locationd_scenarios import TEST_ROUTE
from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_carParams
from openpilot.common.params import Params
from openpilot.tools.lib.logreader import LogReader
def get_random_live_parameters(CP):
msg = messaging.new_message("liveParameters")
msg.liveParameters.steerRatio = (random.random() + 0.5) * CP.steerRatio
msg.liveParameters.stiffnessFactor = random.random()
msg.liveParameters.angleOffsetAverageDeg = random.random()
msg.liveParameters.debugFilterState.std = [random.random() for _ in range(CarKalman.P_initial.shape[0])]
return msg
class TestParamsd:
def test_read_saved_params(self):
params = Params()
lr = migrate(LogReader(TEST_ROUTE), [migrate_carParams])
CP = next(m for m in lr if m.which() == "carParams").carParams
msg = get_random_live_parameters(CP)
params.put("LiveParametersV2", msg.to_bytes())
params.put("CarParamsPrevRoute", CP.as_builder().to_bytes())
migrate_cached_vehicle_params_if_needed(params) # this is not tested here but should not mess anything up or throw an error
sr, sf, offset, p_init = retrieve_initial_vehicle_params(params, CP, replay=True, debug=True)
np.testing.assert_allclose(sr, msg.liveParameters.steerRatio)
np.testing.assert_allclose(sf, msg.liveParameters.stiffnessFactor)
np.testing.assert_allclose(offset, msg.liveParameters.angleOffsetAverageDeg)
np.testing.assert_equal(p_init.shape, CarKalman.P_initial.shape)
np.testing.assert_allclose(np.diagonal(p_init), msg.liveParameters.debugFilterState.std)
# TODO Remove this test after the support for old format is removed
def test_read_saved_params_old_format(self):
params = Params()
lr = migrate(LogReader(TEST_ROUTE), [migrate_carParams])
CP = next(m for m in lr if m.which() == "carParams").carParams
msg = get_random_live_parameters(CP)
params.put("LiveParameters", msg.liveParameters.to_dict())
params.put("CarParamsPrevRoute", CP.as_builder().to_bytes())
params.remove("LiveParametersV2")
migrate_cached_vehicle_params_if_needed(params)
sr, sf, offset, _ = retrieve_initial_vehicle_params(params, CP, replay=True, debug=True)
np.testing.assert_allclose(sr, msg.liveParameters.steerRatio)
np.testing.assert_allclose(sf, msg.liveParameters.stiffnessFactor)
np.testing.assert_allclose(offset, msg.liveParameters.angleOffsetAverageDeg)
assert params.get("LiveParametersV2") is not None
def test_read_saved_params_corrupted_old_format(self):
params = Params()
params.put("LiveParameters", {})
params.remove("LiveParametersV2")
migrate_cached_vehicle_params_if_needed(params)
assert params.get("LiveParameters") is None
assert params.get("LiveParametersV2") is None

View File

@@ -0,0 +1,25 @@
from cereal import car
from openpilot.selfdrive.locationd.torqued import TorqueEstimator
def test_cal_percent():
est = TorqueEstimator(car.CarParams())
msg = est.get_msg()
assert msg.liveTorqueParameters.calPerc == 0
for (low, high), min_pts in zip(est.filtered_points.buckets.keys(),
est.filtered_points.buckets_min_points.values(), strict=True):
for _ in range(int(min_pts)):
est.filtered_points.add_point((low + high) / 2.0, 0.0)
# enough bucket points, but not enough total points
msg = est.get_msg()
assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2
# add enough points to bucket with most capacity
key = list(est.filtered_points.buckets)[0]
for _ in range(est.min_points_total - len(est.filtered_points)):
est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0)
msg = est.get_msg()
assert msg.liveTorqueParameters.calPerc == 100

View File

@@ -3,6 +3,7 @@ import glob
Import('env', 'arch')
lenv = env.Clone()
CHUNK_BYTES = int(os.environ.get("TG_CHUNK_BYTES", str(45 * 1024 * 1024)))
tinygrad_root = env.Dir("#").abspath
tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=tinygrad_root)
@@ -36,12 +37,28 @@ lenv.Command(warp_targets, tinygrad_files + script_files, cmd)
def tg_compile(flags, model_name):
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"'
fn = File(f"models/{model_name}").abspath
return lenv.Command(
fn + "_tinygrad.pkl",
out = fn + "_tinygrad.pkl"
full = out + ".full"
parts = out + ".parts"
full_node = lenv.Command(
full,
[fn + ".onnx"] + tinygrad_files,
f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {fn}_tinygrad.pkl'
f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {full}'
)
split_script = File(Dir("#selfdrive/modeld").File("external_pickle.py").abspath)
parts_node = lenv.Command(
parts,
[full_node, split_script, Value(str(CHUNK_BYTES))],
[f'python3 {split_script.abspath} {full} {out} {CHUNK_BYTES}', Delete(full)],
)
lenv.NoCache(parts_node)
lenv.AlwaysBuild(parts_node)
return parts_node
# Compile small models
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
tg_compile(tg_flags, model_name)

View File

@@ -17,6 +17,7 @@ from openpilot.common.transformations.model import dmonitoringmodel_intrinsics
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.selfdrive.modeld.parse_model_outputs import sigmoid, safe_exp
from openpilot.selfdrive.modeld.external_pickle import load_external_pickle
PROCESS_NAME = "selfdrive.modeld.dmonitoringmodeld"
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
@@ -44,8 +45,7 @@ class ModelState:
self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
self._blob_cache : dict[int, Tensor] = {}
self.image_warp = None
with open(MODEL_PKL_PATH, "rb") as f:
self.model_run = pickle.load(f)
self.model_run = load_external_pickle(MODEL_PKL_PATH)
def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]:
self.numpy_inputs['calib'][0,:] = calib

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python3
import hashlib
import pickle
import sys
from pathlib import Path
def split_pickle(full_path: Path, out_prefix: Path, chunk_bytes: int) -> None:
data = full_path.read_bytes()
out_dir = out_prefix.parent
for p in out_dir.glob(f"{out_prefix.name}.data-*"):
p.unlink()
total = (len(data) + chunk_bytes - 1) // chunk_bytes
names = []
for i in range(0, len(data), chunk_bytes):
name = f"{out_prefix.name}.data-{(i // chunk_bytes) + 1:04d}-of-{total:04d}"
(out_dir / name).write_bytes(data[i:i + chunk_bytes])
names.append(name)
manifest = hashlib.sha256(data).hexdigest() + "\n" + "\n".join(names) + "\n"
(out_dir / (out_prefix.name + ".parts")).write_text(manifest)
def load_external_pickle(prefix: Path):
parts = prefix.parent / (prefix.name + ".parts")
lines = parts.read_text().splitlines()
expected_hash, chunk_names = lines[0], lines[1:]
data = bytearray()
for name in chunk_names:
data += (prefix.parent / name).read_bytes()
if hashlib.sha256(data).hexdigest() != expected_hash:
raise RuntimeError(f"hash mismatch loading {prefix}")
return pickle.loads(data)
if __name__ == "__main__":
split_pickle(Path(sys.argv[1]), Path(sys.argv[2]), int(sys.argv[3]))

View File

@@ -1,33 +1,51 @@
#!/usr/bin/env python3
import sys
import pathlib
import onnx
import codecs
import pickle
from typing import Any
def get_name_and_shape(value_info:onnx.ValueInfoProto) -> tuple[str, tuple[int,...]]:
shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim])
name = value_info.name
from tinygrad.nn.onnx import OnnxPBParser
class MetadataOnnxPBParser(OnnxPBParser):
def _parse_ModelProto(self) -> dict:
obj: dict[str, Any] = {"graph": {"input": [], "output": []}, "metadata_props": []}
for fid, wire_type in self._parse_message(self.reader.len):
match fid:
case 7:
obj["graph"] = self._parse_GraphProto()
case 14:
obj["metadata_props"].append(self._parse_StringStringEntryProto())
case _:
self.reader.skip_field(wire_type)
return obj
def get_name_and_shape(value_info: dict[str, Any]) -> tuple[str, tuple[int, ...]]:
shape = tuple(int(dim) if isinstance(dim, int) else 0 for dim in value_info["parsed_type"].shape)
name = value_info["name"]
return name, shape
def get_metadata_value_by_name(model:onnx.ModelProto, name:str) -> str | Any:
for prop in model.metadata_props:
if prop.key == name:
return prop.value
def get_metadata_value_by_name(model: dict[str, Any], name: str) -> str | Any:
for prop in model["metadata_props"]:
if prop["key"] == name:
return prop["value"]
return None
if __name__ == "__main__":
model_path = pathlib.Path(sys.argv[1])
model = onnx.load(str(model_path))
model = MetadataOnnxPBParser(model_path).parse()
output_slices = get_metadata_value_by_name(model, 'output_slices')
assert output_slices is not None, 'output_slices not found in metadata'
metadata = {
'model_checkpoint': get_metadata_value_by_name(model, 'model_checkpoint'),
'output_slices': pickle.loads(codecs.decode(output_slices.encode(), "base64")),
'input_shapes': dict([get_name_and_shape(x) for x in model.graph.input]),
'output_shapes': dict([get_name_and_shape(x) for x in model.graph.output])
'input_shapes': dict(get_name_and_shape(x) for x in model["graph"]["input"]),
'output_shapes': dict(get_name_and_shape(x) for x in model["graph"]["output"]),
}
metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl')

View File

@@ -28,6 +28,7 @@ from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan,
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
from openpilot.selfdrive.modeld.external_pickle import load_external_pickle
PROCESS_NAME = "selfdrive.modeld.modeld"
@@ -180,11 +181,8 @@ class ModelState:
self.parser = Parser()
self.frame_buf_params : dict[str, tuple[int, int, int, int]] = {}
self.update_imgs = None
with open(VISION_PKL_PATH, "rb") as f:
self.vision_run = pickle.load(f)
with open(POLICY_PKL_PATH, "rb") as f:
self.policy_run = pickle.load(f)
self.vision_run = load_external_pickle(VISION_PKL_PATH)
self.policy_run = load_external_pickle(POLICY_PKL_PATH)
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
@@ -208,9 +206,10 @@ class ModelState:
ptr = bufs[key].data.ctypes.data
yuv_size = self.frame_buf_params[key][3]
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
if ptr not in self._blob_cache:
self._blob_cache[ptr] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8')
self.full_frames[key] = self._blob_cache[ptr]
cache_key = (key, ptr)
if cache_key not in self._blob_cache:
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8')
self.full_frames[key] = self._blob_cache[cache_key]
for key in bufs.keys():
self.transforms_np[key][:,:] = transforms[key][:,:]
@@ -426,13 +425,15 @@ def main(demo=False):
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction
modelv2_send.modelV2.meta.laneWidthLeft = float(DH.lane_width_left)
modelv2_send.modelV2.meta.laneWidthRight = float(DH.lane_width_right)
modelv2_send.modelV2.meta.distanceToRoadEdgeLeft = float(DH.distance_to_road_edge_left)
modelv2_send.modelV2.meta.distanceToRoadEdgeRight = float(DH.distance_to_road_edge_right)
modelv2_send.modelV2.meta.laneWidthLeft = float(DH.left.lane_width)
modelv2_send.modelV2.meta.laneWidthRight = float(DH.right.lane_width)
modelv2_send.modelV2.meta.distanceToRoadEdgeLeft = float(DH.left.dist_to_edge)
modelv2_send.modelV2.meta.distanceToRoadEdgeRight = float(DH.right.dist_to_edge)
modelv2_send.modelV2.meta.desire = DH.desire
modelv2_send.modelV2.meta.laneChangeProb = DH.lane_change_ll_prob
modelv2_send.modelV2.meta.modelTurnSpeed = float(DH.model_turn_speed)
modelv2_send.modelV2.meta.laneChangeAvailableLeft = DH.lane_change_available_left
modelv2_send.modelV2.meta.laneChangeAvailableRight = DH.lane_change_available_right
fill_pose_msg(posenet_send, model_output, meta_main.frame_id, vipc_dropped_frames, meta_main.timestamp_eof, live_calib_seen)
pm.send('modelV2', modelv2_send)

View File

@@ -35,14 +35,7 @@ class DRIVER_MONITOR_SETTINGS:
self._EYE_THRESHOLD = 0.65
self._SG_THRESHOLD = 0.9
self._BLINK_THRESHOLD = 0.865
self._PHONE_THRESH = 0.75 if device_type == 'mici' else 0.4
self._PHONE_THRESH2 = 15.0
self._PHONE_MAX_OFFSET = 0.06
self._PHONE_MIN_OFFSET = 0.025
self._PHONE_DATA_AVG = 0.05
self._PHONE_DATA_VAR = 3*0.005
self._PHONE_MAX_COUNT = int(360 / self._DT_DMON)
self._PHONE_THRESH = 0.68
self._POSE_PITCH_THRESHOLD = 0.3133
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
@@ -152,11 +145,10 @@ class DriverMonitoring:
# init driver status
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
phone_filter_raw_priors = (self.settings._PHONE_DATA_AVG, self.settings._PHONE_DATA_VAR, 2)
self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
self.phone = DriverProb(raw_priors=phone_filter_raw_priors, max_trackable=self.settings._PHONE_MAX_COUNT)
self.pose = DriverPose(settings=self.settings)
self.blink = DriverBlink()
self.phone_prob = 0.
self.always_on = always_on
self.distracted_types = []
@@ -257,12 +249,7 @@ class DriverMonitoring:
if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
distracted_types.append(DistractedType.DISTRACTED_BLINK)
if self.phone.prob_calibrated:
using_phone = self.phone.prob > max(min(self.phone.prob_offseter.filtered_stat.M, self.settings._PHONE_MAX_OFFSET), self.settings._PHONE_MIN_OFFSET) \
* self.settings._PHONE_THRESH2
else:
using_phone = self.phone.prob > self.settings._PHONE_THRESH
if using_phone:
if self.phone_prob > self.settings._PHONE_THRESH:
distracted_types.append(DistractedType.DISTRACTED_PHONE)
return distracted_types
@@ -301,7 +288,7 @@ class DriverMonitoring:
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
self.phone.prob = driver_data.phoneProb
self.phone_prob = driver_data.phoneProb
self.distracted_types = self._get_distracted_types()
self.driver_distracted = (DistractedType.DISTRACTED_PHONE in self.distracted_types
@@ -315,11 +302,9 @@ class DriverMonitoring:
if self.face_detected and car_speed > self.settings._POSE_CALIB_MIN_SPEED and self.pose.low_std and (not op_engaged or not self.driver_distracted):
self.pose.pitch_offseter.push_and_update(self.pose.pitch)
self.pose.yaw_offseter.push_and_update(self.pose.yaw)
self.phone.prob_offseter.push_and_update(self.phone.prob)
self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \
self.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
self.phone.prob_calibrated = self.phone.prob_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
if self.face_detected and not self.driver_distracted:
if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD:
@@ -425,8 +410,6 @@ class DriverMonitoring:
"posePitchValidCount": self.pose.pitch_offseter.filtered_stat.n,
"poseYawOffset": self.pose.yaw_offseter.filtered_stat.mean(),
"poseYawValidCount": self.pose.yaw_offseter.filtered_stat.n,
"phoneProbOffset": self.phone.prob_offseter.filtered_stat.mean(),
"phoneProbValidCount": self.phone.prob_offseter.filtered_stat.n,
"stepChange": self.step_change,
"awarenessActive": self.awareness_active,
"awarenessPassive": self.awareness_passive,

View File

@@ -393,6 +393,17 @@ def personality_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging
personality = str(personality).title()
return NormalPermanentAlert(f"Driving Personality: {personality}", duration=1.5)
def invalid_lkas_setting_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
text = "Toggle stock LKAS on or off to engage"
if CP.brand == "tesla":
text = "Switch to Traffic-Aware Cruise Control to engage"
elif CP.brand == "mazda":
text = "Enable your car's LKAS to engage"
elif CP.brand == "nissan":
text = "Disable your car's stock LKAS to engage"
return NormalPermanentAlert("Invalid LKAS setting", text)
def car_parser_result(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
results = Params().get("CanParserResult")
if results is None:
@@ -492,6 +503,10 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
ET.NO_ENTRY: NoEntryAlert("Stock AEB: Risk of Collision"),
},
EventName.stockLkas: {
ET.NO_ENTRY: NoEntryAlert("Stock LKAS: Lane Departure Detected"),
},
EventName.fcw: {
ET.PERMANENT: Alert(
"BRAKE!",
@@ -714,6 +729,11 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
visual_alert=VisualAlert.brakePressed),
},
EventName.steerDisengage: {
ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage),
ET.NO_ENTRY: NoEntryAlert("Steering Pressed"),
},
EventName.preEnableStandstill: {
ET.PRE_ENABLE: Alert(
"Release Brake to Engage",
@@ -793,6 +813,11 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
ET.NO_ENTRY: NoEntryAlert("Distraction Level Too High"),
},
EventName.excessiveActuation: {
ET.SOFT_DISABLE: soft_disable_alert("Excessive Actuation"),
ET.NO_ENTRY: NoEntryAlert("Excessive Actuation"),
},
EventName.overheat: {
ET.PERMANENT: overheat_alert,
ET.SOFT_DISABLE: soft_disable_alert("System Overheated"),

View File

@@ -58,7 +58,7 @@ class SelfdriveD:
self.CP = CP
self.car_events = CarSpecificEvents(self.CP)
self.disengage_on_accelerator = False #not (self.CP.alternativeExperience & ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS)
self.pose_calibrator = PoseCalibrator()
self.calibrated_pose: Pose | None = None
self.excessive_actuation_check = ExcessiveActuationCheck()

View File

@@ -111,7 +111,7 @@ class TestAlerts:
alert = copy.copy(self.offroad_alerts[a])
set_offroad_alert(a, True)
alert['extra'] = ''
assert json.dumps(alert) == params.get(a, encoding='utf8')
assert alert == params.get(a)
# then delete it
set_offroad_alert(a, False)
@@ -125,6 +125,6 @@ class TestAlerts:
alert = self.offroad_alerts[a]
set_offroad_alert(a, True, extra_text="a"*i)
written_alert = json.loads(params.get(a, encoding='utf8'))
written_alert = params.get(a)
assert "a"*i == written_alert['extra']
assert alert["text"] == written_alert['text']

View File

@@ -36,10 +36,12 @@ class MainLayout(Widget):
# Set callbacks
self._setup_callbacks()
# Start onboarding if terms or training not completed
gui_app.push_widget(self)
# Start onboarding if terms or training not completed, make sure to push after self
self._onboarding_window = OnboardingWindow()
if not self._onboarding_window.completed:
gui_app.set_modal_overlay(self._onboarding_window)
gui_app.push_widget(self._onboarding_window)
def _render(self, _):
self._handle_onroad_transition()

View File

@@ -81,6 +81,9 @@ class TrainingGuide(Widget):
if self._completed_callback:
self._completed_callback()
# NOTE: this pops OnboardingWindow during real onboarding
gui_app.pop_widget()
def _update_state(self):
if len(self._image_objs):
self._textures.append(gui_app._load_texture_from_image(self._image_objs.pop(0)))
@@ -194,11 +197,10 @@ class OnboardingWindow(Widget):
ui_state.params.put("HasAcceptedTerms", terms_version)
self._state = OnboardingState.ONBOARDING
if self._training_done:
gui_app.set_modal_overlay(None)
gui_app.pop_widget()
def _on_completed_training(self):
ui_state.params.put("CompletedTrainingVersion", training_version)
gui_app.set_modal_overlay(None)
def _render(self, _):
if self._training_guide is None:

View File

@@ -164,7 +164,7 @@ class DeveloperLayout(Widget):
def _on_alpha_long_enabled(self, state: bool):
if state:
def confirm_callback(result: int):
def confirm_callback(result: DialogResult):
if result == DialogResult.CONFIRM:
self._params.put_bool("AlphaLongitudinalEnabled", True)
self._params.put_bool("OnroadCycleRequested", True)
@@ -176,8 +176,8 @@ class DeveloperLayout(Widget):
content = (f"<h1>{self._alpha_long_toggle.title}</h1><br>" +
f"<p>{self._alpha_long_toggle.description}</p>")
dlg = ConfirmDialog(content, tr("Enable"), rich=True)
gui_app.set_modal_overlay(dlg, callback=confirm_callback)
dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback)
gui_app.push_widget(dlg)
else:
self._params.put_bool("AlphaLongitudinalEnabled", False)

View File

@@ -9,7 +9,6 @@ from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialo
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.layouts.onboarding import TrainingGuide
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
from openpilot.system.hardware import TICI
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.lib.multilang import multilang, tr, tr_noop
from openpilot.system.ui.widgets import Widget, DialogResult
@@ -34,8 +33,6 @@ class DeviceLayout(Widget):
self._params = Params()
self._select_language_dialog: MultiOptionDialog | None = None
self._driver_camera: DriverCameraDialog | None = None
self._pair_device_dialog: PairingDialog | None = None
self._fcc_dialog: HtmlModal | None = None
self._training_guide: TrainingGuide | None = None
@@ -45,7 +42,8 @@ class DeviceLayout(Widget):
ui_state.add_offroad_transition_callback(self._offroad_transition)
def _initialize_items(self):
self._pair_device_btn = button_item(lambda: tr("Pair Device"), lambda: tr("PAIR"), lambda: tr(DESCRIPTIONS['pair_device']), callback=self._pair_device)
self._pair_device_btn = button_item(lambda: tr("Pair Device"), lambda: tr("PAIR"), lambda: tr(DESCRIPTIONS['pair_device']),
callback=lambda: gui_app.push_widget(PairingDialog()))
self._pair_device_btn.set_visible(lambda: not ui_state.prime_state.is_paired())
self._reset_calib_btn = button_item(lambda: tr("Reset Calibration"), lambda: tr("RESET"), lambda: tr(DESCRIPTIONS['reset_calibration']),
@@ -60,15 +58,14 @@ class DeviceLayout(Widget):
text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))),
self._pair_device_btn,
button_item(lambda: tr("Driver Camera"), lambda: tr("PREVIEW"), lambda: tr(DESCRIPTIONS['driver_camera']),
callback=self._show_driver_camera, enabled=ui_state.is_offroad),
callback=lambda: gui_app.push_widget(DriverCameraDialog()), enabled=ui_state.is_offroad),
self._reset_calib_btn,
button_item(lambda: tr("Review Training Guide"), lambda: tr("REVIEW"), lambda: tr(DESCRIPTIONS['review_guide']),
self._on_review_training_guide, enabled=ui_state.is_offroad),
regulatory_btn := button_item(lambda: tr("Regulatory"), lambda: tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad),
button_item(lambda: tr("Regulatory"), lambda: tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad),
button_item(lambda: tr("Change Language"), lambda: tr("CHANGE"), callback=self._show_language_dialog),
self._power_off_btn,
]
regulatory_btn.set_visible(TICI)
return items
def _offroad_transition(self):
@@ -81,29 +78,23 @@ class DeviceLayout(Widget):
self._scroller.render(rect)
def _show_language_dialog(self):
def handle_language_selection(result: int):
if result == 1 and self._select_language_dialog:
def handle_language_selection(result: DialogResult):
if result == DialogResult.CONFIRM and self._select_language_dialog:
selected_language = multilang.languages[self._select_language_dialog.selection]
multilang.change_language(selected_language)
self._update_calib_description()
self._select_language_dialog = None
self._select_language_dialog = MultiOptionDialog(tr("Select a language"), multilang.languages, multilang.codes[multilang.language],
option_font_weight=FontWeight.UNIFONT)
gui_app.set_modal_overlay(self._select_language_dialog, callback=handle_language_selection)
def _show_driver_camera(self):
if not self._driver_camera:
self._driver_camera = DriverCameraDialog()
gui_app.set_modal_overlay(self._driver_camera, callback=lambda result: setattr(self, '_driver_camera', None))
option_font_weight=FontWeight.UNIFONT, callback=handle_language_selection)
gui_app.push_widget(self._select_language_dialog)
def _reset_calibration_prompt(self):
if ui_state.engaged:
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reset Calibration")))
gui_app.push_widget(alert_dialog(tr("Disengage to Reset Calibration")))
return
def reset_calibration(result: int):
def reset_calibration(result: DialogResult):
# Check engaged again in case it changed while the dialog was open
if ui_state.engaged or result != DialogResult.CONFIRM:
return
@@ -116,8 +107,8 @@ class DeviceLayout(Widget):
self._params.put_bool("OnroadCycleRequested", True)
self._update_calib_description()
dialog = ConfirmDialog(tr("Are you sure you want to reset calibration?"), tr("Reset"))
gui_app.set_modal_overlay(dialog, callback=reset_calibration)
dialog = ConfirmDialog(tr("Are you sure you want to reset calibration?"), tr("Reset"), callback=reset_calibration)
gui_app.push_widget(dialog)
def _update_calib_description(self):
desc = tr(DESCRIPTIONS['reset_calibration'])
@@ -169,42 +160,34 @@ class DeviceLayout(Widget):
def _reboot_prompt(self):
if ui_state.engaged:
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reboot")))
gui_app.push_widget(alert_dialog(tr("Disengage to Reboot")))
return
dialog = ConfirmDialog(tr("Are you sure you want to reboot?"), tr("Reboot"))
gui_app.set_modal_overlay(dialog, callback=self._perform_reboot)
def perform_reboot(result: DialogResult):
if not ui_state.engaged and result == DialogResult.CONFIRM:
self._params.put_bool_nonblocking("DoReboot", True)
def _perform_reboot(self, result: int):
if not ui_state.engaged and result == DialogResult.CONFIRM:
self._params.put_bool_nonblocking("DoReboot", True)
dialog = ConfirmDialog(tr("Are you sure you want to reboot?"), tr("Reboot"), callback=perform_reboot)
gui_app.push_widget(dialog)
def _power_off_prompt(self):
if ui_state.engaged:
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Power Off")))
gui_app.push_widget(alert_dialog(tr("Disengage to Power Off")))
return
dialog = ConfirmDialog(tr("Are you sure you want to power off?"), tr("Power Off"))
gui_app.set_modal_overlay(dialog, callback=self._perform_power_off)
def perform_power_off(result: DialogResult):
if not ui_state.engaged and result == DialogResult.CONFIRM:
self._params.put_bool_nonblocking("DoShutdown", True)
def _perform_power_off(self, result: int):
if not ui_state.engaged and result == DialogResult.CONFIRM:
self._params.put_bool_nonblocking("DoShutdown", True)
def _pair_device(self):
if not self._pair_device_dialog:
self._pair_device_dialog = PairingDialog()
gui_app.set_modal_overlay(self._pair_device_dialog, callback=lambda result: setattr(self, '_pair_device_dialog', None))
dialog = ConfirmDialog(tr("Are you sure you want to power off?"), tr("Power Off"), callback=perform_power_off)
gui_app.push_widget(dialog)
def _on_regulatory(self):
if not self._fcc_dialog:
self._fcc_dialog = HtmlModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html"))
gui_app.set_modal_overlay(self._fcc_dialog)
gui_app.push_widget(self._fcc_dialog)
def _on_review_training_guide(self):
if not self._training_guide:
def completed_callback():
gui_app.set_modal_overlay(None)
self._training_guide = TrainingGuide(completed_callback=completed_callback)
gui_app.set_modal_overlay(self._training_guide)
self._training_guide = TrainingGuide()
gui_app.push_widget(self._training_guide)

View File

@@ -165,12 +165,12 @@ class SoftwareLayout(Widget):
os.system("pkill -SIGHUP -f system.updated.updated")
def _on_uninstall(self):
def handle_uninstall_confirmation(result):
def handle_uninstall_confirmation(result: DialogResult):
if result == DialogResult.CONFIRM:
ui_state.params.put_bool("DoUninstall", True)
dialog = ConfirmDialog(tr("Are you sure you want to uninstall?"), tr("Uninstall"))
gui_app.set_modal_overlay(dialog, callback=handle_uninstall_confirmation)
dialog = ConfirmDialog(tr("Are you sure you want to uninstall?"), tr("Uninstall"), callback=handle_uninstall_confirmation)
gui_app.push_widget(dialog)
def _on_install_update(self):
# Trigger reboot to install update
@@ -189,9 +189,8 @@ class SoftwareLayout(Widget):
branches.insert(0, b)
current_target = ui_state.params.get("UpdaterTargetBranch") or ""
self._branch_dialog = MultiOptionDialog(tr("Select a branch"), branches, current_target)
def handle_selection(result):
def handle_selection(result: DialogResult):
# Confirmed selection
if result == DialogResult.CONFIRM and self._branch_dialog is not None and self._branch_dialog.selection:
selection = self._branch_dialog.selection
@@ -200,4 +199,5 @@ class SoftwareLayout(Widget):
os.system("pkill -SIGUSR1 -f system.updated.updated")
self._branch_dialog = None
gui_app.set_modal_overlay(self._branch_dialog, callback=handle_selection)
self._branch_dialog = MultiOptionDialog(tr("Select a branch"), branches, current_target, callback=handle_selection)
gui_app.push_widget(self._branch_dialog)

View File

@@ -214,7 +214,7 @@ class TogglesLayout(Widget):
def _handle_experimental_mode_toggle(self, state: bool):
confirmed = self._params.get_bool("ExperimentalModeConfirmed")
if state and not confirmed:
def confirm_callback(result: int):
def confirm_callback(result: DialogResult):
if result == DialogResult.CONFIRM:
self._params.put_bool("ExperimentalMode", True)
self._params.put_bool("ExperimentalModeConfirmed", True)
@@ -225,8 +225,8 @@ class TogglesLayout(Widget):
# show confirmation dialog
content = (f"<h1>{self._toggles['ExperimentalMode'].title}</h1><br>" +
f"<p>{self._toggles['ExperimentalMode'].description}</p>")
dlg = ConfirmDialog(content, tr("Enable"), rich=True)
gui_app.set_modal_overlay(dlg, callback=confirm_callback)
dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback)
gui_app.push_widget(dlg)
else:
self._update_experimental_mode_icon()
self._params.put_bool("ExperimentalMode", state)

View File

@@ -109,7 +109,7 @@ class MiciHomeLayout(Widget):
self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 54, 36)
self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 54, 36)
self._openpilot_label = MiciLabel("openpilot", font_size=96, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY)
self._openpilot_label = MiciLabel("CarrotPilot", font_size=85, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY)
self._version_label = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN)
self._large_version_label = MiciLabel("", font_size=64, color=rl.GRAY, font_weight=FontWeight.ROMAN)
self._date_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
@@ -249,7 +249,7 @@ class MiciHomeLayout(Widget):
# Offset by difference in height between slashless and slash icons to make center align match
rl.draw_texture(self._wifi_slash_txt, int(last_x), int(self._rect.y + self.rect.height - self._wifi_slash_txt.height / 2 -
(self._wifi_slash_txt.height - self._wifi_none_txt.height) / 2 - Y_CENTER),
rl.Color(255, 255, 255, 255))
rl.Color(255, 255, 255, int(255 * 0.9)))
last_x += self._wifi_slash_txt.width + ITEM_SPACING
# draw experimental icon

View File

@@ -1,5 +1,4 @@
import pyray as rl
from enum import IntEnum
import cereal.messaging as messaging
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout
@@ -15,18 +14,12 @@ from openpilot.system.ui.lib.application import gui_app
ONROAD_DELAY = 2.5 # seconds
class MainState(IntEnum):
MAIN = 0
SETTINGS = 1
class MiciMainLayout(Widget):
def __init__(self):
super().__init__()
self._pm = messaging.PubMaster(['bookmarkButton'])
self._current_mode: MainState | None = None
self._prev_onroad = False
self._prev_standstill = False
self._onroad_time_delay: float | None = None
@@ -47,40 +40,33 @@ class MiciMainLayout(Widget):
self._alerts_layout,
self._home_layout,
self._onroad_layout,
], spacing=0, pad_start=0, pad_end=0, scroll_indicator=False)
], spacing=0, pad_start=0, pad_end=0, scroll_indicator=False, edge_shadows=False)
self._scroller.set_reset_scroll_at_show(False)
self._scroller.set_enabled(lambda: self.enabled) # for nav stack
# Disable scrolling when onroad is interacting with bookmark
self._scroller.set_scrolling_enabled(lambda: not self._onroad_layout.is_swiping_left())
self._layouts = {
MainState.MAIN: self._scroller,
MainState.SETTINGS: self._settings_layout,
}
# Set callbacks
self._setup_callbacks()
# Start onboarding if terms or training not completed
gui_app.push_widget(self)
# Start onboarding if terms or training not completed, make sure to push after self
self._onboarding_window = OnboardingWindow()
if not self._onboarding_window.completed:
gui_app.set_modal_overlay(self._onboarding_window)
gui_app.push_widget(self._onboarding_window)
def _setup_callbacks(self):
self._home_layout.set_callbacks(on_settings=self._on_settings_clicked)
self._settings_layout.set_callbacks(on_close=self._on_settings_closed)
self._home_layout.set_callbacks(on_settings=lambda: gui_app.push_widget(self._settings_layout))
self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
device.add_interactive_timeout_callback(self._set_mode_for_started)
device.add_interactive_timeout_callback(self._on_interactive_timeout)
def _scroll_to(self, layout: Widget):
layout_x = int(layout.rect.x)
self._scroller.scroll_to(layout_x, smooth=True)
def _render(self, _):
# Initial show event
if self._current_mode is None:
self._set_mode(MainState.MAIN)
if not self._setup:
if self._alerts_layout.active_alerts() > 0:
self._scroller.scroll_to(self._alerts_layout.rect.x)
@@ -89,59 +75,50 @@ class MiciMainLayout(Widget):
self._setup = True
# Render
if self._current_mode == MainState.MAIN:
self._scroller.render(self._rect)
elif self._current_mode == MainState.SETTINGS:
self._settings_layout.render(self._rect)
self._scroller.render(self._rect)
self._handle_transitions()
def _set_mode(self, mode: MainState):
if mode != self._current_mode:
if self._current_mode is not None:
self._layouts[self._current_mode].hide_event()
self._layouts[mode].show_event()
self._current_mode = mode
def _handle_transitions(self):
# Don't pop if onboarding
if gui_app.get_active_widget() == self._onboarding_window:
return
if ui_state.started != self._prev_onroad:
self._prev_onroad = ui_state.started
# onroad: after delay, pop nav stack and scroll to onroad
# offroad: immediately scroll to home, but don't pop nav stack (can stay in settings)
if ui_state.started:
self._onroad_time_delay = rl.get_time()
else:
self._set_mode_for_started(True)
self._scroll_to(self._home_layout)
# delay so we show home for a bit after starting
if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY:
self._set_mode_for_started(True)
gui_app.pop_widgets_to(self)
self._scroll_to(self._onroad_layout)
self._onroad_time_delay = None
# When car leaves standstill, pop nav stack and scroll to onroad
CS = ui_state.sm["carState"]
if not CS.standstill and self._prev_standstill:
self._set_mode(MainState.MAIN)
gui_app.pop_widgets_to(self)
self._scroll_to(self._onroad_layout)
self._prev_standstill = CS.standstill
def _set_mode_for_started(self, onroad_transition: bool = False):
def _on_interactive_timeout(self):
# Don't pop if onboarding
if gui_app.get_active_widget() == self._onboarding_window:
return
if ui_state.started:
CS = ui_state.sm["carState"]
# Only go onroad if car starts or is not at a standstill
if not CS.standstill or onroad_transition:
self._set_mode(MainState.MAIN)
# Don't pop if at standstill
if not ui_state.sm["carState"].standstill:
gui_app.pop_widgets_to(self)
self._scroll_to(self._onroad_layout)
else:
# Stay in settings if car turns off while in settings
if not onroad_transition or self._current_mode != MainState.SETTINGS:
self._set_mode(MainState.MAIN)
self._scroll_to(self._home_layout)
def _on_settings_clicked(self):
self._set_mode(MainState.SETTINGS)
def _on_settings_closed(self):
self._set_mode(MainState.MAIN)
gui_app.pop_widgets_to(self)
self._scroll_to(self._home_layout)
def _on_bookmark_clicked(self):
user_bookmark = messaging.new_message('bookmarkButton')

View File

@@ -7,7 +7,7 @@ import pyray as rl
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets import Widget, NavWidget
from openpilot.system.ui.widgets.button import SmallButton, SmallCircleIconButton
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.slider import SmallSlider
@@ -42,7 +42,7 @@ class DriverCameraSetupDialog(DriverCameraDialog):
gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
rl.end_scissor_mode()
return -1
return
# Position dmoji on opposite side from driver
is_rhd = self.driver_state_renderer.is_rhd
@@ -55,7 +55,6 @@ class DriverCameraSetupDialog(DriverCameraDialog):
self._draw_face_detection(rect)
rl.end_scissor_mode()
return -1
class TrainingGuidePreDMTutorial(SetupTermsPage):
@@ -124,8 +123,11 @@ class TrainingGuideDMTutorial(Widget):
def __init__(self, continue_callback):
super().__init__()
self_ref = weakref.ref(self)
self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 28, 48))
self._back_button.set_click_callback(self._show_bad_face_page)
self._back_button.set_click_callback(lambda: self_ref() and self_ref()._show_bad_face_page())
self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 42, 42))
# Wrap the continue callback to restore settings
@@ -138,7 +140,7 @@ class TrainingGuideDMTutorial(Widget):
self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps)
self._dialog = DriverCameraSetupDialog()
self._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, self._hide_bad_face_page)
self._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, lambda: self_ref() and self_ref()._hide_bad_face_page())
self._should_show_bad_face_page = False
# Disable driver monitoring model when device times out for inactivity
@@ -364,9 +366,9 @@ class TrainingGuide(Widget):
self._completed_callback()
def _render(self, _):
rl.draw_rectangle_rec(self._rect, rl.BLACK)
if self._step < len(self._steps):
self._steps[self._step].render(self._rect)
return -1
class DeclinePage(Widget):
@@ -435,9 +437,11 @@ class TermsPage(SetupTermsPage):
))
class OnboardingWindow(Widget):
class OnboardingWindow(NavWidget):
def __init__(self):
super().__init__()
self.set_back_enabled(False)
self._accepted_terms: bool = ui_state.params.get("HasAcceptedTerms") == terms_version
self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == training_version
@@ -470,7 +474,7 @@ class OnboardingWindow(Widget):
def close(self):
ui_state.params.put_bool("IsDriverViewEnabled", False)
gui_app.set_modal_overlay(None)
gui_app.pop_widget()
def _on_terms_accepted(self):
ui_state.params.put("HasAcceptedTerms", terms_version)
@@ -487,4 +491,3 @@ class OnboardingWindow(Widget):
self._training_guide.render(self._rect)
elif self._state == OnboardingState.DECLINE:
self._decline_page.render(self._rect)
return -1

View File

@@ -1,5 +1,4 @@
import pyray as rl
from collections.abc import Callable
from openpilot.common.time_helpers import system_time_valid
from openpilot.system.ui.widgets.scroller import Scroller
@@ -13,9 +12,9 @@ from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyAction
class DeveloperLayoutMici(NavWidget):
def __init__(self, back_callback: Callable):
def __init__(self):
super().__init__()
self.set_back_callback(back_callback)
self.set_back_callback(gui_app.pop_widget)
def github_username_callback(username: str):
if username:
@@ -25,16 +24,16 @@ class DeveloperLayoutMici(NavWidget):
self._ssh_keys_btn.set_value(username)
else:
dlg = BigDialog("", ssh_keys._error_message)
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)
def ssh_keys_callback():
github_username = ui_state.params.get("GithubUsername") or ""
dlg = BigInputDialog("enter GitHub username", github_username, confirm_callback=github_username_callback)
dlg = BigInputDialog("enter GitHub username...", github_username, confirm_callback=github_username_callback)
if not system_time_valid():
dlg = BigDialog("Please connect to Wi-Fi to fetch your key", "")
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)
return
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)
txt_ssh = gui_app.texture("icons_mici/settings/developer/ssh.png", 56, 64)
github_username = ui_state.params.get("GithubUsername") or ""

View File

@@ -28,7 +28,7 @@ class MiciFccModal(NavWidget):
def __init__(self, file_path: str | None = None, text: str | None = None):
super().__init__()
self.set_back_callback(lambda: gui_app.set_modal_overlay(None))
self.set_back_callback(gui_app.pop_widget)
self._content = HtmlRenderer(file_path=file_path, text=text)
self._scroll_panel = GuiScrollPanel2(horizontal=False)
self._fcc_logo = gui_app.texture("icons_mici/settings/device/fcc_logo.png", 76, 64)
@@ -47,8 +47,6 @@ class MiciFccModal(NavWidget):
rl.draw_texture_ex(self._fcc_logo, fcc_pos, 0.0, 1.0, rl.WHITE)
return -1
def _engaged_confirmation_callback(callback: Callable, action_text: str):
if not ui_state.engaged:
@@ -74,10 +72,10 @@ def _engaged_confirmation_callback(callback: Callable, action_text: str):
dlg: BigConfirmationDialogV2 | BigDialog = BigConfirmationDialogV2(f"slide to\n{action_text.lower()}", icon, red=red,
exit_on_confirm=action_text == "reset",
confirm_callback=confirm_callback)
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)
else:
dlg = BigDialog(f"Disengage to {action_text}", "")
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)
class DeviceInfoLayoutMici(Widget):
@@ -147,7 +145,7 @@ class PairBigButton(BigButton):
dlg = BigDialog(tr("Device must be registered with the comma.ai backend to pair"), "")
else:
dlg = PairingDialog()
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)
UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond
@@ -173,7 +171,7 @@ class UpdateOpenpilotBigButton(BigButton):
def _handle_mouse_release(self, mouse_pos: MousePos):
if not system_time_valid():
dlg = BigDialog(tr("Please connect to Wi-Fi to update"), "")
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)
return
self.set_enabled(False)
@@ -268,12 +266,10 @@ class UpdateOpenpilotBigButton(BigButton):
class DeviceLayoutMici(NavWidget):
def __init__(self, back_callback: Callable):
def __init__(self):
super().__init__()
self._fcc_dialog: HtmlModal | None = None
self._driver_camera: DriverCameraDialog | None = None
self._training_guide: TrainingGuide | None = None
def power_off_callback():
ui_state.params.put_bool("DoShutdown", True)
@@ -309,11 +305,11 @@ class DeviceLayoutMici(NavWidget):
regulatory_btn.set_click_callback(self._on_regulatory)
driver_cam_btn = BigButton("driver\ncamera preview", "", "icons_mici/settings/device/cameras.png")
driver_cam_btn.set_click_callback(self._show_driver_camera)
driver_cam_btn.set_click_callback(lambda: gui_app.push_widget(DriverCameraDialog()))
driver_cam_btn.set_enabled(lambda: ui_state.is_offroad())
review_training_guide_btn = BigButton("review\ntraining guide", "", "icons_mici/settings/device/info.png")
review_training_guide_btn.set_click_callback(self._on_review_training_guide)
review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(TrainingGuide(completed_callback=gui_app.pop_widget)))
review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad())
self._scroller = Scroller([
@@ -330,7 +326,8 @@ class DeviceLayoutMici(NavWidget):
], snap_items=False)
# Set up back navigation
self.set_back_callback(back_callback)
# TODO: can this somehow be generic in widgets/__init__.py or application.py?
self.set_back_callback(gui_app.pop_widget)
# Hide power off button when onroad
ui_state.add_offroad_transition_callback(self._offroad_transition)
@@ -338,24 +335,11 @@ class DeviceLayoutMici(NavWidget):
def _on_regulatory(self):
if not self._fcc_dialog:
self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/mici_fcc.html"))
gui_app.set_modal_overlay(self._fcc_dialog)
gui_app.push_widget(self._fcc_dialog)
def _offroad_transition(self):
self._power_off_btn.set_visible(ui_state.is_offroad())
def _show_driver_camera(self):
if not self._driver_camera:
self._driver_camera = DriverCameraDialog()
gui_app.set_modal_overlay(self._driver_camera, callback=lambda result: setattr(self, '_driver_camera', None))
def _on_review_training_guide(self):
if not self._training_guide:
def completed_callback():
gui_app.set_modal_overlay(None)
self._training_guide = TrainingGuide(completed_callback=completed_callback)
gui_app.set_modal_overlay(self._training_guide, callback=lambda result: setattr(self, '_training_guide', None))
def show_event(self):
super().show_event()
self._scroller.show_event()

View File

@@ -132,9 +132,6 @@ class FirehoseLayoutBase(Widget):
y = self._draw_wrapped_text(x, y, w, tr(answer), gui_app.font(FontWeight.ROMAN), 32, self.LIGHT_GRAY)
y += 20
# return value not used by NavWidget
return -1
def _draw_wrapped_text(self, x, y, width, text, font, font_size, color):
wrapped = wrap_text(font, text, font_size, width)
for line in wrapped:
@@ -223,6 +220,6 @@ class FirehoseLayoutBase(Widget):
class FirehoseLayout(FirehoseLayoutBase, NavWidget):
BACK_TOUCH_AREA_PERCENTAGE = 0.1
def __init__(self, back_callback):
def __init__(self):
super().__init__()
self.set_back_callback(back_callback)
self.set_back_callback(gui_app.pop_widget)

View File

@@ -1,33 +1,23 @@
import pyray as rl
from enum import IntEnum
from collections.abc import Callable
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon, normalize_ssid
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import NavWidget
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType
class NetworkPanelType(IntEnum):
NONE = 0
WIFI = 1
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType, ConnectStatus, normalize_ssid
class NetworkLayoutMici(NavWidget):
def __init__(self, back_callback: Callable):
def __init__(self):
super().__init__()
self._current_panel = NetworkPanelType.WIFI
self.set_back_enabled(lambda: self._current_panel == NetworkPanelType.NONE)
self._wifi_manager = WifiManager()
self._wifi_manager.set_active(False)
self._wifi_ui = WifiUIMici(self._wifi_manager, back_callback=lambda: self._switch_to_panel(NetworkPanelType.NONE))
self._wifi_ui = WifiUIMici(self._wifi_manager)
self._wifi_manager.add_callbacks(
networks_updated=self._on_network_updated,
@@ -36,6 +26,7 @@ class NetworkLayoutMici(NavWidget):
# ******** Tethering ********
def tethering_toggle_callback(checked: bool):
self._tethering_toggle_btn.set_enabled(False)
self._tethering_password_btn.set_enabled(False)
self._network_metered_btn.set_enabled(False)
self._wifi_manager.set_tethering_active(checked)
@@ -43,13 +34,15 @@ class NetworkLayoutMici(NavWidget):
def tethering_password_callback(password: str):
if password:
self._tethering_toggle_btn.set_enabled(False)
self._tethering_password_btn.set_enabled(False)
self._wifi_manager.set_tethering_password(password)
def tethering_password_clicked():
tethering_password = self._wifi_manager.tethering_password
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
confirm_callback=tethering_password_callback)
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)
txt_tethering = gui_app.texture("icons_mici/settings/network/tethering.png", 64, 54)
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
@@ -76,7 +69,7 @@ class NetworkLayoutMici(NavWidget):
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47)
self._wifi_button = BigButton("wi-fi", "not connected", self._wifi_slash_txt, scroll=True)
self._wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui))
# ******** Advanced settings ********
# ******** Roaming toggle ********
@@ -108,7 +101,7 @@ class NetworkLayoutMici(NavWidget):
self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered)
# Set up back navigation
self.set_back_callback(back_callback)
self.set_back_callback(gui_app.pop_widget)
def _update_state(self):
super()._update_state()
@@ -120,15 +113,40 @@ class NetworkLayoutMici(NavWidget):
self._apn_btn.set_visible(show_cell_settings)
self._cellular_metered_btn.set_visible(show_cell_settings)
# Update wi-fi button with ssid and ip address
# TODO: make sure we handle hidden ssids
wifi_state = self._wifi_manager.wifi_state
display_network = next((n for n in self._wifi_manager.networks if n.ssid == wifi_state.ssid), None)
if wifi_state.status == ConnectStatus.CONNECTING:
self._wifi_button.set_text(normalize_ssid(wifi_state.ssid or "wi-fi"))
self._wifi_button.set_value("connecting...")
elif wifi_state.status == ConnectStatus.CONNECTED:
self._wifi_button.set_text(normalize_ssid(wifi_state.ssid or "wi-fi"))
self._wifi_button.set_value(self._wifi_manager.ipv4_address or "obtaining IP...")
else:
display_network = None
self._wifi_button.set_text("wi-fi")
self._wifi_button.set_value("not connected")
if display_network is not None:
strength = WifiIcon.get_strength_icon_idx(display_network.strength)
self._wifi_button.set_icon(self._wifi_full_txt if strength == 2 else self._wifi_medium_txt if strength == 1 else self._wifi_low_txt)
else:
self._wifi_button.set_icon(self._wifi_slash_txt)
def show_event(self):
super().show_event()
self._current_panel = NetworkPanelType.NONE
self._wifi_ui.show_event()
self._wifi_manager.set_active(True)
self._scroller.show_event()
# Process wifi callbacks while at any point in the nav stack
gui_app.set_nav_stack_tick(self._wifi_manager.process_callbacks)
def hide_event(self):
super().hide_event()
self._wifi_ui.hide_event()
self._wifi_manager.set_active(False)
gui_app.set_nav_stack_tick(None)
def _toggle_roaming(self, checked: bool):
self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered"))
@@ -144,8 +162,8 @@ class NetworkLayoutMici(NavWidget):
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), apn, ui_state.params.get_bool("GsmMetered"))
current_apn = ui_state.params.get("GsmApn") or ""
dlg = BigInputDialog("enter APN", current_apn, minimum_length=0, confirm_callback=update_apn)
gui_app.set_modal_overlay(dlg)
dlg = BigInputDialog("enter APN...", current_apn, minimum_length=0, confirm_callback=update_apn)
gui_app.push_widget(dlg)
def _toggle_cellular_metered(self, checked: bool):
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked)
@@ -155,26 +173,10 @@ class NetworkLayoutMici(NavWidget):
tethering_active = self._wifi_manager.is_tethering_active()
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
self._tethering_toggle_btn.set_enabled(True)
self._tethering_password_btn.set_enabled(True)
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
self._tethering_toggle_btn.set_checked(tethering_active)
# Update wi-fi button with ssid and ip address
# TODO: make sure we handle hidden ssids
connected_network = next((network for network in networks if network.is_connected), None)
self._wifi_button.set_text(normalize_ssid(connected_network.ssid) if connected_network is not None else "wi-fi")
self._wifi_button.set_value(self._wifi_manager.ipv4_address or "not connected")
if connected_network is not None:
strength = WifiIcon.get_strength_icon_idx(connected_network.strength)
if strength == 2:
strength_icon = self._wifi_full_txt
elif strength == 1:
strength_icon = self._wifi_medium_txt
else:
strength_icon = self._wifi_low_txt
self._wifi_button.set_icon(strength_icon)
else:
self._wifi_button.set_icon(self._wifi_slash_txt)
# Update network metered
self._network_metered_btn.set_value(
{
@@ -183,15 +185,5 @@ class NetworkLayoutMici(NavWidget):
MeteredType.NO: 'unmetered'
}.get(self._wifi_manager.current_network_metered, 'default'))
def _switch_to_panel(self, panel_type: NetworkPanelType):
if panel_type == NetworkPanelType.WIFI:
self._wifi_ui.show_event()
self._current_panel = panel_type
def _render(self, rect: rl.Rectangle):
self._wifi_manager.process_callbacks()
if self._current_panel == NetworkPanelType.WIFI:
self._wifi_ui.render(rect)
else:
self._scroller.render(rect)
self._scroller.render(rect)

View File

@@ -8,11 +8,7 @@ from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigInputDialog, BigDialogOptionButton, BigConfirmationDialogV2
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
from openpilot.system.ui.widgets import Widget, NavWidget
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType
def normalize_ssid(ssid: str) -> str:
return ssid.replace("", "'") # for iPhone hotspots
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, WifiState, normalize_ssid
class LoadingAnimation(Widget):
@@ -94,7 +90,7 @@ class WifiIcon(Widget):
class WifiItem(BigDialogOptionButton):
LEFT_MARGIN = 20
def __init__(self, network: Network):
def __init__(self, network: Network, wifi_state_callback: Callable[[], WifiState]):
super().__init__(network.ssid)
self.set_rect(rl.Rectangle(0, 0, gui_app.width, self.HEIGHT))
@@ -102,6 +98,7 @@ class WifiItem(BigDialogOptionButton):
self._selected_txt = gui_app.texture("icons_mici/settings/network/new/wifi_selected.png", 48, 96)
self._network = network
self._wifi_state_callback = wifi_state_callback
self._wifi_icon = WifiIcon()
self._wifi_icon.set_current_network(network)
@@ -119,7 +116,8 @@ class WifiItem(BigDialogOptionButton):
def _render(self, _):
disabled_alpha = 0.35 if not self.enabled else 1.0
if self._network.is_connected:
# connecting or connected
if self._wifi_state_callback().ssid == self._network.ssid:
selected_x = int(self._rect.x - self._selected_txt.width / 2)
selected_y = int(self._rect.y + (self._rect.height - self._selected_txt.height) / 2)
rl.draw_texture(self._selected_txt, selected_x, selected_y, rl.WHITE)
@@ -188,10 +186,9 @@ class ConnectButton(Widget):
class ForgetButton(Widget):
HORIZONTAL_MARGIN = 8
def __init__(self, forget_network: Callable, open_network_manage_page):
def __init__(self, forget_network: Callable):
super().__init__()
self._forget_network = forget_network
self._open_network_manage_page = open_network_manage_page
self._bg_txt = gui_app.texture("icons_mici/settings/network/new/forget_button.png", 100, 100)
self._bg_pressed_txt = gui_app.texture("icons_mici/settings/network/new/forget_button_pressed.png", 100, 100)
@@ -202,7 +199,7 @@ class ForgetButton(Widget):
super()._handle_mouse_release(mouse_pos)
dlg = BigConfirmationDialogV2("slide to forget", "icons_mici/settings/network/new/trash.png", red=True,
confirm_callback=self._forget_network)
gui_app.set_modal_overlay(dlg, callback=self._open_network_manage_page)
gui_app.push_widget(dlg)
def _render(self, _):
bg_txt = self._bg_pressed_txt if self.is_pressed else self._bg_txt
@@ -214,15 +211,16 @@ class ForgetButton(Widget):
class NetworkInfoPage(NavWidget):
def __init__(self, wifi_manager, connect_callback: Callable, forget_callback: Callable, open_network_manage_page: Callable):
def __init__(self, wifi_manager, connect_callback: Callable, forget_callback: Callable,
connecting_callback: Callable[[], str | None], connected_callback: Callable[[], str | None]):
super().__init__()
self._wifi_manager = wifi_manager
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
self._wifi_icon = WifiIcon()
self._forget_btn = ForgetButton(lambda: forget_callback(self._network.ssid) if self._network is not None else None,
open_network_manage_page)
self._forget_btn = ForgetButton(lambda: forget_callback(self._network.ssid) if self._network is not None else None)
self._forget_btn.set_enabled(lambda: self.enabled) # for stack
self._connect_btn = ConnectButton()
self._connect_btn.set_click_callback(lambda: connect_callback(self._network.ssid) if self._network is not None else None)
@@ -231,11 +229,12 @@ class NetworkInfoPage(NavWidget):
self._subtitle = UnifiedLabel("", 36, FontWeight.ROMAN, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
self.set_back_callback(lambda: gui_app.set_modal_overlay(None))
self.set_back_callback(gui_app.pop_widget)
# State
self._network: Network | None = None
self._connecting: Callable[[], str | None] | None = None
self._connecting_callback = connecting_callback
self._connected_callback = connected_callback
def show_event(self):
super().show_event()
@@ -249,21 +248,23 @@ class NetworkInfoPage(NavWidget):
break
else:
# network disappeared, close page
gui_app.set_modal_overlay(None)
# TODO: pop_widgets_to, to close potentially open keyboard too
if gui_app.get_active_widget() == self:
gui_app.pop_widget()
def _update_state(self):
super()._update_state()
# Modal overlays stop main UI rendering, so we need to call here
# TODO: remove? only left for potential compatibility with setup/updater
self._wifi_manager.process_callbacks()
if self._network is None:
return
self._connect_btn.set_full(not self._network.is_saved and not self._is_connecting)
self._connect_btn.set_full(not self._wifi_manager.is_connection_saved(self._network.ssid) and not self._is_connecting)
if self._is_connecting:
self._connect_btn.set_label("connecting...")
self._connect_btn.set_enabled(False)
elif self._network.is_connected:
elif self._is_connected:
self._connect_btn.set_label("connected")
self._connect_btn.set_enabled(False)
elif self._network.security_type == SecurityType.UNSUPPORTED:
@@ -271,7 +272,7 @@ class NetworkInfoPage(NavWidget):
self._connect_btn.set_enabled(False)
else: # saved or unknown
self._connect_btn.set_label("connect")
self._connect_btn.set_enabled(True)
self._connect_btn.set_enabled(self.enabled)
self._title.set_text(normalize_ssid(self._network.ssid))
if self._network.security_type == SecurityType.OPEN:
@@ -285,16 +286,20 @@ class NetworkInfoPage(NavWidget):
self._network = network
self._wifi_icon.set_current_network(network)
def set_connecting(self, is_connecting: Callable[[], str | None]):
self._connecting = is_connecting
@property
def _is_connecting(self):
if self._connecting is None or self._network is None:
if self._network is None:
return False
is_connecting = self._connecting() == self._network.ssid
is_connecting = self._connecting_callback() == self._network.ssid
return is_connecting
@property
def _is_connected(self):
if self._network is None:
return False
is_connected = self._connected_callback() == self._network.ssid
return is_connected
def _render(self, _):
self._wifi_icon.render(rl.Rectangle(
self._rect.x + 32,
@@ -332,55 +337,37 @@ class NetworkInfoPage(NavWidget):
self._forget_btn.rect.height,
))
return -1
class WifiUIMici(BigMultiOptionDialog):
def __init__(self, wifi_manager: WifiManager, back_callback: Callable):
def __init__(self, wifi_manager: WifiManager):
super().__init__([], None)
# Set up back navigation
self.set_back_callback(back_callback)
self.set_back_callback(gui_app.pop_widget)
self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, self._forget_network, self._open_network_manage_page)
self._network_info_page.set_connecting(lambda: self._connecting)
self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, wifi_manager.forget_connection,
lambda: wifi_manager.connecting_to_ssid, lambda: wifi_manager.connected_ssid)
self._loading_animation = LoadingAnimation()
self._wifi_manager = wifi_manager
self._connecting: str | None = None
self._networks: dict[str, Network] = {}
self._wifi_manager.add_callbacks(
need_auth=self._on_need_auth,
activated=self._on_activated,
forgotten=self._on_forgotten,
networks_updated=self._on_network_updated,
disconnected=self._on_disconnected,
)
def show_event(self):
# Call super to prepare scroller; selection scroll is handled dynamically
# Clear scroller items and update from latest scan results
super().show_event()
self._wifi_manager.set_active(True)
self._scroller._items.clear()
self._update_buttons()
def hide_event(self):
super().hide_event()
self._wifi_manager.set_active(False)
# clear scroller items to remove old networks on next show
self._scroller._items.clear()
def _open_network_manage_page(self, result=None):
self._network_info_page.update_networks(self._networks)
gui_app.set_modal_overlay(self._network_info_page)
def _forget_network(self, ssid: str):
network = self._networks.get(ssid)
if network is None:
cloudlog.warning(f"Trying to forget unknown network: {ssid}")
return
self._wifi_manager.forget_connection(network.ssid)
self._scroller.hide_event()
def _on_network_updated(self, networks: list[Network]):
self._networks = {network.ssid: network for network in networks}
@@ -396,11 +383,11 @@ class WifiUIMici(BigMultiOptionDialog):
# Update network on existing button
self._scroller._items[network_button_idx].set_current_network(network)
else:
network_button = WifiItem(network)
network_button = WifiItem(network, lambda: self._wifi_manager.wifi_state)
self._scroller.add_widget(network_button)
# Move connected network to the start
connected_btn_idx = next((i for i, btn in enumerate(self._scroller._items) if btn._network.is_connected), None)
# Move connecting/connected network to the start
connected_btn_idx = next((i for i, btn in enumerate(self._scroller._items) if self._wifi_manager.wifi_state.ssid == btn._network.ssid), None)
if connected_btn_idx is not None and connected_btn_idx > 0:
self._scroller._items.insert(0, self._scroller._items.pop(connected_btn_idx))
self._scroller._layout() # fixes selected style single frame stutter
@@ -412,17 +399,15 @@ class WifiUIMici(BigMultiOptionDialog):
btn.set_network_missing(True)
def _connect_with_password(self, ssid: str, password: str):
if password:
self._connecting = ssid
self._wifi_manager.connect_to_network(ssid, password)
self._update_buttons()
self._wifi_manager.connect_to_network(ssid, password)
self._update_buttons()
def _on_option_selected(self, option: str):
super()._on_option_selected(option)
if option in self._networks:
self._network_info_page.set_current_network(self._networks[option])
self._open_network_manage_page()
gui_app.push_widget(self._network_info_page)
def _connect_to_network(self, ssid: str):
network = self._networks.get(ssid)
@@ -430,12 +415,10 @@ class WifiUIMici(BigMultiOptionDialog):
cloudlog.warning(f"Trying to connect to unknown network: {ssid}")
return
if network.is_saved:
self._connecting = network.ssid
if self._wifi_manager.is_connection_saved(network.ssid):
self._wifi_manager.activate_connection(network.ssid)
self._update_buttons()
elif network.security_type == SecurityType.OPEN:
self._connecting = network.ssid
self._wifi_manager.connect_to_network(network.ssid, "")
self._update_buttons()
else:
@@ -445,24 +428,7 @@ class WifiUIMici(BigMultiOptionDialog):
hint = "wrong password..." if incorrect_password else "enter password..."
dlg = BigInputDialog(hint, "", minimum_length=8,
confirm_callback=lambda _password: self._connect_with_password(ssid, _password))
def on_close(result=None):
gui_app.set_modal_overlay_tick(None)
self._open_network_manage_page(result)
# Process wifi callbacks while the keyboard is shown so forgotten clears connecting state
gui_app.set_modal_overlay_tick(self._wifi_manager.process_callbacks)
gui_app.set_modal_overlay(dlg, on_close)
def _on_activated(self):
self._connecting = None
def _on_forgotten(self, ssid):
if self._connecting == ssid:
self._connecting = None
def _on_disconnected(self):
self._connecting = None
gui_app.push_widget(dlg)
def _render(self, _):
super()._render(_)

View File

@@ -1,7 +1,4 @@
import pyray as rl
from dataclasses import dataclass
from enum import IntEnum
from collections.abc import Callable
from openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller import Scroller
@@ -12,22 +9,7 @@ from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.widgets import Widget, NavWidget
class PanelType(IntEnum):
TOGGLES = 0
NETWORK = 1
DEVICE = 2
DEVELOPER = 3
USER_MANUAL = 4
FIREHOSE = 5
@dataclass
class PanelInfo:
name: str
instance: Widget
from openpilot.system.ui.widgets import NavWidget
class SettingsBigButton(BigButton):
@@ -39,19 +21,26 @@ class SettingsLayout(NavWidget):
def __init__(self):
super().__init__()
self._params = Params()
self._current_panel = None # PanelType.DEVICE
toggles_panel = TogglesLayoutMici()
toggles_btn = SettingsBigButton("toggles", "", "icons_mici/settings.png")
toggles_btn.set_click_callback(lambda: self._set_current_panel(PanelType.TOGGLES))
network_btn = SettingsBigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56))
network_btn.set_click_callback(lambda: self._set_current_panel(PanelType.NETWORK))
device_btn = SettingsBigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60))
device_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVICE))
developer_btn = SettingsBigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60))
developer_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVELOPER))
toggles_btn.set_click_callback(lambda: gui_app.push_widget(toggles_panel))
network_panel = NetworkLayoutMici()
network_btn = SettingsBigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56))
network_btn.set_click_callback(lambda: gui_app.push_widget(network_panel))
device_panel = DeviceLayoutMici()
device_btn = SettingsBigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60))
device_btn.set_click_callback(lambda: gui_app.push_widget(device_panel))
developer_panel = DeveloperLayoutMici()
developer_btn = SettingsBigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60))
developer_btn.set_click_callback(lambda: gui_app.push_widget(developer_panel))
firehose_panel = FirehoseLayout()
firehose_btn = SettingsBigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62))
firehose_btn.set_click_callback(lambda: self._set_current_panel(PanelType.FIREHOSE))
firehose_btn.set_click_callback(lambda: gui_app.push_widget(firehose_panel))
self._scroller = Scroller([
toggles_btn,
@@ -64,55 +53,17 @@ class SettingsLayout(NavWidget):
], snap_items=False)
# Set up back navigation
self.set_back_callback(self.close_settings)
self.set_back_enabled(lambda: self._current_panel is None)
self._panels = {
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayoutMici(back_callback=lambda: self._set_current_panel(None))),
PanelType.NETWORK: PanelInfo("Network", NetworkLayoutMici(back_callback=lambda: self._set_current_panel(None))),
PanelType.DEVICE: PanelInfo("Device", DeviceLayoutMici(back_callback=lambda: self._set_current_panel(None))),
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayoutMici(back_callback=lambda: self._set_current_panel(None))),
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout(back_callback=lambda: self._set_current_panel(None))),
}
self.set_back_callback(gui_app.pop_widget)
self._font_medium = gui_app.font(FontWeight.MEDIUM)
# Callbacks
self._close_callback: Callable | None = None
def show_event(self):
super().show_event()
self._set_current_panel(None)
self._scroller.show_event()
if self._current_panel is not None:
self._panels[self._current_panel].instance.show_event()
def hide_event(self):
super().hide_event()
if self._current_panel is not None:
self._panels[self._current_panel].instance.hide_event()
def set_callbacks(self, on_close: Callable):
self._close_callback = on_close
self._scroller.hide_event()
def _render(self, rect: rl.Rectangle):
if self._current_panel is not None:
self._draw_current_panel()
else:
self._scroller.render(rect)
def _draw_current_panel(self):
panel = self._panels[self._current_panel]
panel.instance.render(self._rect)
def _set_current_panel(self, panel_type: PanelType | None):
if panel_type != self._current_panel:
if self._current_panel is not None:
self._panels[self._current_panel].instance.hide_event()
self._current_panel = panel_type
if self._current_panel is not None:
self._panels[self._current_panel].instance.show_event()
def close_settings(self):
if self._close_callback:
self._close_callback()
self._scroller.render(rect)

View File

@@ -1,5 +1,4 @@
import pyray as rl
from collections.abc import Callable
from cereal import log
from openpilot.system.ui.widgets.scroller import Scroller
@@ -13,9 +12,9 @@ PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants
class TogglesLayoutMici(NavWidget):
def __init__(self, back_callback: Callable):
def __init__(self):
super().__init__()
self.set_back_callback(back_callback)
self.set_back_callback(gui_app.pop_widget)
self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"])
self._experimental_btn = BigParamControl("experimental mode", "ExperimentalMode")

View File

@@ -174,7 +174,13 @@ class AlertRenderer(Widget):
icon_margin_y = 5
elif event_name == 'laneChange':
icon_side = self._last_icon_side
CS = ui_state.sm['carState']
if CS.leftBlinker:
icon_side = IconSide.left
elif CS.rightBlinker:
icon_side = IconSide.right
else:
icon_side = self._last_icon_side
txt_icon = self._txt_turn_signal_left if self._last_icon_side == 'left' else self._txt_turn_signal_right
icon_margin_x = 2
icon_margin_y = 5

View File

@@ -19,6 +19,8 @@ from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCamera
from openpilot.common.transformations.orientation import rot_from_euler
from enum import IntEnum
from openpilot.selfdrive.ui.mici.onroad.traffic_light import TrafficLight
OpState = log.SelfdriveState.OpenpilotState
CALIBRATED = log.LiveCalibrationData.Status.calibrated
ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD
@@ -159,6 +161,7 @@ class AugmentedRoadView(CameraView):
self._alert_renderer = AlertRenderer()
self._driver_state_renderer = DriverStateRenderer()
self._confidence_ball = ConfidenceBall()
self._traffic_light = TrafficLight()
self._offroad_label = UnifiedLabel("start the car to\nuse openpilot", 54, FontWeight.DISPLAY,
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
@@ -237,9 +240,9 @@ class AugmentedRoadView(CameraView):
self._hud_renderer.set_wheel_critical_icon(alert_to_render is not None and not not_animating_out and
alert_to_render.visual_alert == car.CarControl.HUDControl.VisualAlert.steerRequired)
# TODO: have alert renderer draw offroad mici label below
self._hud_renderer.render(self._content_rect)
if ui_state.started:
self._alert_renderer.render(self._content_rect)
self._hud_renderer.render(self._content_rect)
# Draw fake rounded border
rl.draw_rectangle_rounded_lines_ex(self._content_rect, 0.2 * 1.02, 10, 50, rl.BLACK)
@@ -249,7 +252,9 @@ class AugmentedRoadView(CameraView):
# Custom UI extension point - add custom overlays here
# Use self._content_rect for positioning within camera bounds
self._confidence_ball.render(self.rect)
self._traffic_light.render(self.rect)
if not self._traffic_light.is_visible():
self._confidence_ball.render(self.rect)
self._bookmark_icon.render(self.rect)
@@ -363,7 +368,7 @@ class AugmentedRoadView(CameraView):
if __name__ == "__main__":
gui_app.init_window("OnRoad Camera View")
road_camera_view = AugmentedRoadView(ROAD_CAM)
road_camera_view = AugmentedRoadView(lambda: None, stream_type=ROAD_CAM)
print("***press space to switch camera view***")
try:
for _ in gui_app.render():

View File

@@ -34,8 +34,8 @@ class DriverCameraDialog(NavWidget):
self._pm: messaging.PubMaster | None = None
if not no_escape:
# TODO: this can grow unbounded, should be given some thought
device.add_interactive_timeout_callback(lambda: gui_app.set_modal_overlay(None))
self.set_back_callback(lambda: gui_app.set_modal_overlay(None))
device.add_interactive_timeout_callback(gui_app.pop_widget)
self.set_back_callback(gui_app.pop_widget)
self.set_back_enabled(not no_escape)
# Load eye icons
@@ -87,7 +87,7 @@ class DriverCameraDialog(NavWidget):
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
rl.end_scissor_mode()
self._publish_alert_sound(None)
return -1
return
driver_data = self._draw_face_detection(rect)
if driver_data is not None:
@@ -105,7 +105,7 @@ class DriverCameraDialog(NavWidget):
self._render_dm_alerts(rect)
rl.end_scissor_mode()
return -1
return
def _publish_alert_sound(self, dm_state):
"""Publish selfdriveState with only alertSound field set"""
@@ -235,9 +235,9 @@ if __name__ == "__main__":
gui_app.init_window("Driver Camera View (mici)")
driver_camera_view = DriverCameraDialog()
gui_app.push_widget(driver_camera_view)
try:
for _ in gui_app.render():
ui_state.update()
driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
finally:
driver_camera_view.close()

View File

@@ -19,6 +19,63 @@ CRUISE_DISABLED_CHAR = ''
SET_SPEED_PERSISTENCE = 2.5 # seconds
@dataclass(frozen=True)
class SetSpeedOverrideState:
active: bool
speed_kph: float
label: str
speed_color_mode: int # 0: white, 1: green, 2: orange
force_persist: bool
class SetSpeedOverride:
def compute(self, sm, set_speed_kph: float) -> SetSpeedOverrideState:
# 1) eco (highest)
cruise_target = None
try:
cruise_target = float(sm['longitudinalPlan'].cruiseTarget)
except Exception:
cruise_target = None
if cruise_target is not None and cruise_target > (set_speed_kph + 0.5):
return SetSpeedOverrideState(
active=True,
speed_kph=cruise_target,
label="eco",
speed_color_mode=1,
force_persist=True, # eco 조건 유지되는 동안 계속 표시
)
# 2) apply_speed (desiredSpeed/source)
desired_speed = None
desired_source = ""
try:
desired_speed = float(sm['carrotMan'].desiredSpeed)
desired_source = str(sm['carrotMan'].desiredSource or "")
except Exception:
desired_speed = None
desired_source = ""
if desired_speed is not None and desired_speed > 0.0 and desired_speed < set_speed_kph:
label = desired_source.strip() or "apply"
label = label[:8] # 너무 길면 UI 깨짐 방지 (원하면 길이 조절)
return SetSpeedOverrideState(
active=True,
speed_kph=desired_speed,
label=label,
speed_color_mode=2,
force_persist=True, # 조건 유지되는 동안 계속 표시
)
# 3) default
return SetSpeedOverrideState(
active=False,
speed_kph=set_speed_kph,
label=tr("MAX"),
speed_color_mode=0,
force_persist=False,
)
@dataclass(frozen=True)
class FontSizes:
@@ -126,6 +183,8 @@ class HudRenderer(Widget):
self._wheel_y_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps)
self._set_speed_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
self._set_speed_override = SetSpeedOverride()
def set_wheel_critical_icon(self, critical: bool):
"""Set the wheel icon to critical or normal state."""
@@ -175,31 +234,27 @@ class HudRenderer(Widget):
if ui_state.sm['controlsState'].lateralControlState.which() != 'angleState':
self._torque_bar.render(rect)
if self.is_cruise_set:
self._draw_set_speed(rect)
#if self.is_cruise_set:
self._draw_set_speed(rect)
self._draw_steering_wheel(rect)
self._draw_driving_mode_text(rect)
def _draw_steering_wheel(self, rect: rl.Rectangle) -> None:
wheel_txt = self._txt_wheel_critical if self._show_wheel_critical else self._txt_wheel
if self._show_wheel_critical:
self._wheel_alpha_filter.update(255)
self._wheel_y_filter.update(0)
else:
#if ui_state.status == UIStatus.DISENGAGED:
if not ui_state.lat_active:
self._wheel_alpha_filter.update(0)
self._wheel_y_filter.update(wheel_txt.height / 2)
else:
self._wheel_alpha_filter.update(255 * 0.9)
self._wheel_y_filter.update(0)
# Always visible (no hide). We keep filters but drive them to stable values.
self._wheel_alpha_filter.update(255 * 0.95)
self._wheel_y_filter.update(0)
# pos
# pos (bottom-left)
pos_x = int(rect.x + 21 + wheel_txt.width / 2)
pos_y = int(rect.y + rect.height - 14 - wheel_txt.height / 2 + self._wheel_y_filter.x)
# rotation
rotation = -ui_state.sm['carState'].steeringAngleDeg
# Turn intent still OK
turn_intent_margin = 25
self._turn_intent.render(rl.Rectangle(
pos_x - wheel_txt.width / 2 - turn_intent_margin,
@@ -212,58 +267,222 @@ class HudRenderer(Widget):
dest_rect = rl.Rectangle(pos_x, pos_y, wheel_txt.width, wheel_txt.height)
origin = (wheel_txt.width / 2, wheel_txt.height / 2)
# color and draw
color = rl.Color(255, 255, 255, int(self._wheel_alpha_filter.x))
rl.draw_texture_pro(wheel_txt, src_rect, dest_rect, origin, rotation, color)
# Color: green if lat_active else gray
if ui_state.lat_active:
wheel_color = rl.Color(0, 255, 0, int(self._wheel_alpha_filter.x)) # green
else:
wheel_color = rl.Color(160, 160, 160, int(self._wheel_alpha_filter.x)) # gray
rl.draw_texture_pro(wheel_txt, src_rect, dest_rect, origin, rotation, wheel_color)
if self._show_wheel_critical:
# Draw exclamation point icon
EXCLAMATION_POINT_SPACING = 10
exclamation_pos_x = pos_x - self._txt_exclamation_point.width / 2 + wheel_txt.width / 2 + EXCLAMATION_POINT_SPACING
exclamation_pos_y = pos_y - self._txt_exclamation_point.height / 2
rl.draw_texture(self._txt_exclamation_point, int(exclamation_pos_x), int(exclamation_pos_y), rl.WHITE)
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
"""Draw the MAX speed indicator box."""
alpha = self._set_speed_alpha_filter.update(0 < rl.get_time() - self._set_speed_changed_time < SET_SPEED_PERSISTENCE and
self._can_draw_top_icons and self._engaged)
if alpha < 1e-2:
return
ov = self._set_speed_override.compute(ui_state.sm, float(self.set_speed))
x = rect.x
y = rect.y
# draw drop shadow
circle_radius = 162 // 2
rl.draw_circle_gradient(int(x + circle_radius), int(y + circle_radius), circle_radius,
rl.Color(0, 0, 0, int(255 / 2 * alpha)), rl.BLANK)
circle_d = 162
circle_radius = circle_d // 2
set_speed_color = rl.Color(255, 255, 255, int(255 * 0.9 * alpha))
max_color = rl.Color(255, 255, 255, int(255 * 0.9 * alpha))
set_speed = self.set_speed
if self.is_cruise_set and not ui_state.is_metric:
set_speed *= KM_TO_MILE
set_speed_text = CRUISE_DISABLED_CHAR if not self.is_cruise_set else str(round(set_speed))
rl.draw_text_ex(
self._font_display,
set_speed_text,
rl.Vector2(x + 13 + 4, y + 3 - 8 - 3 + 4),
FONT_SIZES.set_speed,
0,
set_speed_color,
# 배경(원형)은 기존대로
rl.draw_circle_gradient(
int(x + circle_radius), int(y + circle_radius), circle_radius,
rl.Color(0, 0, 0, 120), rl.BLANK
)
max_text = tr("MAX")
# ============================================================
# 1) 현재 주행속도: 항상 표시 (3자리 슬롯, 오른쪽 정렬)
# ============================================================
big_font = FONT_SIZES.set_speed
top_y = int(y + 3 - 8 - 3 + 4)
left_margin = 10
gap = 10
# 실제 화면(컨텐츠 영역) 오른쪽 끝을 사용
screen_right = int(rect.x + rect.width - 12) # 오른쪽 여백 12
inner_left = int(x + left_margin)
# 3자리 슬롯 폭
slot_w = int(measure_text_cached(self._font_display, "888", big_font).x)
# 슬롯은 왼쪽에 고정
slot_left = inner_left
slot_right = slot_left + slot_w
# 숫자는 슬롯 안에서 오른쪽 정렬
cur_speed_int = int(round(self.speed))
cur_digits = str(cur_speed_int)
cur_digits_w = measure_text_cached(self._font_display, cur_digits, big_font).x
cur_x = int(slot_right - cur_digits_w)
rl.draw_text_ex(
self._font_display,
cur_digits,
rl.Vector2(cur_x, top_y),
big_font,
0,
rl.WHITE,
)
# ============================================================
# 2) set_speed 블록: engaged일 때만 표시
# -> "3" 오른쪽(= slot_right + gap)에서 시작
# ============================================================
show_set_block = self._engaged and self.is_cruise_set and self._can_draw_top_icons
if not show_set_block:
return
# set_speed 값
set_speed = ov.speed_kph if ov.active else self.set_speed
if not ui_state.is_metric:
set_speed *= KM_TO_MILE
set_speed_text = str(int(round(set_speed)))
# label
#max_text = (ov.label if ov.active else tr("MAX")) or "MAX"
max_text = ov.label if ov.active else "cruise"
max_text = max_text[:6]
# 색상(기존 로직)
if ov.speed_color_mode == 1: # eco
set_speed_color = rl.Color(0, 255, 0, 230)
elif ov.speed_color_mode == 2: # apply
set_speed_color = rl.Color(255, 165, 0, 230)
else: # default
set_speed_color = rl.Color(255, 255, 255, 230)
max_color = rl.Color(255, 255, 255, 230)
# 폰트
label_font = max(22, int(FONT_SIZES.max_speed * 0.85))
speed_font = max(48, int(FONT_SIZES.set_speed * 0.62))
label_size = measure_text_cached(self._font_semi_bold, max_text, label_font)
spd_size = measure_text_cached(self._font_display, set_speed_text, speed_font)
block_w = int(max(label_size.x, spd_size.x))
block_h = label_size.y + 2 + spd_size.y
# 블록은 무조건 속도 오른쪽부터 시작
block_left = int(slot_right + gap)
# 화면 오른쪽을 넘어가면, 블록을 오른쪽 끝에 붙임(그래도 속도쪽으로 침범 X)
max_left = int(screen_right - block_w)
if block_left > max_left:
block_left = max_left
# 그래도 속도쪽 침범하면(아주 극단) -> 그냥 그리지 않음
if block_left <= slot_right:
return
# 세로 정렬: big_font 높이 안에 맞춤
big_size = measure_text_cached(self._font_display, "888", big_font)
block_top = top_y + (big_size.y - block_h) / 2.0
# draw
rl.draw_text_ex(
self._font_semi_bold,
max_text,
rl.Vector2(x + 25, y + FONT_SIZES.set_speed - 7 + 4),
FONT_SIZES.max_speed,
rl.Vector2(block_left + (block_w - label_size.x), block_top),
label_font,
0,
max_color,
)
rl.draw_text_ex(
self._font_display,
set_speed_text,
rl.Vector2(block_left + (block_w - spd_size.x), block_top + label_size.y + 2),
speed_font,
0,
set_speed_color,
)
def _draw_driving_mode_text(self, rect: rl.Rectangle) -> None:
if not self._engaged:
return
mode_text, mode_color = self._get_driving_mode_text_and_color()
if not mode_text:
return
wheel_txt = self._txt_wheel_critical if self._show_wheel_critical else self._txt_wheel
pos_x = int(rect.x + 21 + wheel_txt.width / 2)
pos_y = int(rect.y + rect.height - 14 - wheel_txt.height / 2 + self._wheel_y_filter.x)
mode_font = FONT_SIZES.max_speed
mode_size = measure_text_cached(self._font_semi_bold, mode_text, mode_font)
mode_x = int(pos_x + wheel_txt.width / 2 + 10)
mode_y = int(pos_y - mode_size.y / 2)
# 기존 driving_mode
rl.draw_text_ex(
self._font_semi_bold,
mode_text,
rl.Vector2(mode_x, mode_y),
mode_font,
0,
mode_color,
)
active_lane_line = bool(ui_state.sm['controlsState'].activeLaneLine)
if active_lane_line:
lm_text = "lanemode"
lm_color = self._color_mode(1, 200) # green
else:
lm_text = "laneless"
lm_color = self._color_mode(2, 200) # orange
lm_gap = 10
lm_x = int(mode_x + mode_size.x + lm_gap)
lm_y = mode_y # 같은 높이/크기
rl.draw_text_ex(
self._font_semi_bold,
lm_text,
rl.Vector2(lm_x, lm_y),
mode_font,
0,
lm_color,
)
def _color_mode(self, mode: int, alpha: int = 200) -> rl.Color:
# mode: 0 white, 1 green, 2 orange, 3 red
if mode == 1:
return rl.Color(0, 255, 0, alpha)
if mode == 2:
return rl.Color(255, 165, 0, alpha)
if mode == 3:
return rl.Color(255, 0, 0, alpha)
return rl.Color(255, 255, 255, alpha)
def _get_driving_mode_text_and_color(self) -> tuple[str, rl.Color]:
try:
mode_val = int(ui_state.sm["longitudinalPlan"].myDrivingMode)
except Exception:
return "", self._color_mode(0, 200)
if mode_val == 1: # eco
return "eco", self._color_mode(1, 200)
if mode_val == 2: # safe
return "safe", self._color_mode(2, 200)
if mode_val == 3: # normal
return "norm", self._color_mode(0, 200)
if mode_val == 4: # high
return "high", self._color_mode(3, 200)
return "", self._color_mode(0, 200)
def _draw_current_speed(self, rect: rl.Rectangle) -> None:
"""Draw the current vehicle speed and unit."""

View File

@@ -11,6 +11,7 @@ from openpilot.selfdrive.ui.mici.onroad import blend_colors
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
from openpilot.system.ui.widgets import Widget
from typing import Optional, Any
CLIP_MARGIN = 500
MIN_DRAW_DISTANCE = 10.0
@@ -46,7 +47,8 @@ class LeadVehicle:
glow: list[float] = field(default_factory=list)
chevron: list[float] = field(default_factory=list)
fill_alpha: int = 0
rect: list[tuple[float, float]] = field(default_factory=list) # 4 corners (screen space)
color: Optional[Any] = None
class ModelRenderer(Widget):
def __init__(self):
@@ -76,6 +78,8 @@ class ModelRenderer(Widget):
self._car_space_transform = np.zeros((3, 3), dtype=np.float32)
self._transform_dirty = True
self._clip_region = None
self._lead_pt_filt = [None, None]
self._exp_gradient = Gradient(
start=(0.0, 1.0), # Bottom of path
@@ -134,7 +138,7 @@ class ModelRenderer(Widget):
self._update_model(lead_one, path_x_array)
if render_lead_indicator:
self._update_leads(radar_state, path_x_array)
self._update_leads_carrot(radar_state, path_x_array)
self._transform_dirty = False
# Draw elements (hide when disengaged)
@@ -175,6 +179,58 @@ class ModelRenderer(Widget):
point = self._map_to_screen(d_rel, -y_rel, z + self._path_offset_z)
if point:
self._lead_vehicles[i] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect)
def _update_leads_carrot(self, radar_state, path_x_array):
"""Carrot: draw leadOne/leadTwo as outline rectangles (no fill)."""
# Reset
self._lead_vehicles = [LeadVehicle(), LeadVehicle()]
lead_one = radar_state.leadOne
#lead_two = radar_state.leadTwo
def _filter_pt(slot: int, pt: tuple[float, float], alpha: float = 0.2):
prev = self._lead_pt_filt[slot]
if prev is None:
self._lead_pt_filt[slot] = (float(pt[0]), float(pt[1]))
return float(pt[0]), float(pt[1])
x = prev[0] + (float(pt[0]) - prev[0]) * alpha
y = prev[1] + (float(pt[1]) - prev[1]) * alpha
self._lead_pt_filt[slot] = (x, y)
return self._lead_pt_filt[slot][0], self._lead_pt_filt[slot][1]
# -------- leadOne --------
if lead_one and lead_one.status:
d_rel, y_rel = float(lead_one.dRel), float(lead_one.yRel)
idx = self._get_path_length_idx(path_x_array, d_rel)
z = float(self._path.raw_points[idx, 2]) if idx < len(self._path.raw_points) else 0.0
pt_left = self._map_to_screen(d_rel, -y_rel - 1.2, z + self._path_offset_z)
pt_right = self._map_to_screen(d_rel, -y_rel + 1.2, z + self._path_offset_z)
if pt_left and pt_right:
path_width = min(max(pt_right[0] - pt_left[0], 60), 400);
path_x = pt_left[0] + path_width / 2
path_y = pt_left[1]
pt_x, pt_y = _filter_pt(0, (path_x, path_y), alpha=0.2)
left = float(np.clip(pt_x - path_width / 2, 0.0, self._rect.width))
right = float(np.clip(pt_x + path_width / 2, 0.0, self._rect.width))
bottom_y = float(np.clip(pt_y, 0.0, self._rect.height))
top = float(np.clip(pt_y - 0.8 * path_width, 0.0, self._rect.height))
# Color rule:
# - leadOne.radar == False => BLUE
# - leadOne.radar == True and radarTrackId in (0,1) => RED
# - else => ORANGE
if not bool(lead_one.radar):
c = rl.Color(0, 120, 255, 255) # BLUE
else:
track_id = int(getattr(lead_one, "radarTrackId", 0))
if track_id in (0, 1):
c = rl.Color(201, 34, 49, 255) # RED
else:
c = rl.Color(255, 115, 0, 255) # ORANGE
self._lead_vehicles[0] = LeadVehicle(rect = [(left, top), (right, top), (right, bottom_y), (left, bottom_y)], color = c)
else:
self._lead_pt_filt[0] = None
def _update_model(self, lead, path_x_array):
"""Update model visualization data based on model message"""
@@ -355,7 +411,7 @@ class ModelRenderer(Widget):
else:
draw_polygon(self._rect, self._path.projected_points, gradient=gradient)
def _draw_lead_indicator(self):
def _draw_lead_indicator_old(self):
# Draw lead vehicles if available
for lead in self._lead_vehicles:
if not lead.glow or not lead.chevron:
@@ -364,6 +420,22 @@ class ModelRenderer(Widget):
rl.draw_triangle_fan(lead.glow, len(lead.glow), rl.Color(218, 202, 37, 255))
rl.draw_triangle_fan(lead.chevron, len(lead.chevron), rl.Color(201, 34, 49, lead.fill_alpha))
def _draw_lead_indicator(self):
# Carrot: draw outline rectangles only (no fill)
thickness = 4.0 # 원하는 두께 (float)
for lead in self._lead_vehicles:
if not lead.rect or lead.color is None:
continue
pts = lead.rect
c = lead.color
rl.draw_line_ex(pts[0], pts[1], thickness, c)
rl.draw_line_ex(pts[1], pts[2], thickness, c)
rl.draw_line_ex(pts[2], pts[3], thickness, c)
rl.draw_line_ex(pts[3], pts[0], thickness, c)
@staticmethod
def _get_path_length_idx(pos_x_array: np.ndarray, path_height: float) -> int:
"""Get the index corresponding to the given path height"""

View File

@@ -0,0 +1,83 @@
import time
import pyray as rl
from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.widgets import Widget
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.ui.mici.onroad.confidence_ball import draw_circle_gradient
class TrafficLight(Widget):
def __init__(self):
super().__init__()
self._radius = 24
self._current_state = 0
self._green_start_time = None
# fade animation (0~1)
self._alpha_filter = FirstOrderFilter(0.0, 1.0, 1/60.0)
# --------------------------------------------------
def is_visible(self):
return self._alpha_filter.x > 0.05
# --------------------------------------------------
def _update_state(self):
state = ui_state.sm["longitudinalPlan"].trafficState
visible = False
if state == 1:
# red always visible
visible = True
self._current_state = 1
self._green_start_time = None
elif state == 2:
# green max 2 sec
if self._current_state != 2:
self._green_start_time = time.monotonic()
self._current_state = 2
if self._green_start_time and (time.monotonic() - self._green_start_time <= 2.0):
visible = True
else:
visible = False
else:
visible = False
self._current_state = 0
self._green_start_time = None
# fade target
self._alpha_filter.update(1.0 if visible else 0.0)
# --------------------------------------------------
def _render(self, _):
alpha = max(0.0, min(1.0, self._alpha_filter.x))
if alpha <= 0.01:
return
content_rect = rl.Rectangle(
self.rect.x + self.rect.width - SIDE_PANEL_WIDTH,
self.rect.y,
SIDE_PANEL_WIDTH,
self.rect.height,
)
center_x = content_rect.x + content_rect.width - self._radius
center_y = self.rect.y + self._radius
#
if self._current_state == 1:
top = rl.Color(255, 80, 80, int(255 * alpha))
bottom = rl.Color(255, 0, 0, int(255 * alpha))
else:
top = rl.Color(120, 255, 120, int(255 * alpha))
bottom = rl.Color(0, 255, 0, int(255 * alpha))
draw_circle_gradient(center_x, center_y, self._radius, top, bottom)

View File

@@ -54,7 +54,7 @@ class BigCircleButton(Widget):
def _draw_content(self, btn_y: float):
# draw icon
icon_color = rl.WHITE if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
icon_color = rl.Color(255, 255, 255, int(255 * 0.9)) if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
rl.draw_texture_ex(self._txt_icon, (self._rect.x + (self._rect.width - self._txt_icon.width) / 2 + self._icon_offset[0],
btn_y + (self._rect.height - self._txt_icon.height) / 2 + self._icon_offset[1]), 0, 1.0, icon_color)
@@ -206,7 +206,7 @@ class BigButton(Widget):
source_rec = rl.Rectangle(0, 0, self._txt_icon.width, self._txt_icon.height)
dest_rec = rl.Rectangle(x, y, self._txt_icon.width, self._txt_icon.height)
origin = rl.Vector2(self._txt_icon.width / 2, self._txt_icon.height / 2)
rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.WHITE)
rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.Color(255, 255, 255, int(255 * 0.9)))
def _render(self, _):
# draw _txt_default_bg
@@ -220,6 +220,10 @@ class BigButton(Widget):
btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2
btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
# draw black background since images are transparent
scaled_rect = rl.Rectangle(btn_x, btn_y, self._rect.width * scale, self._rect.height * scale)
rl.draw_rectangle_rounded(scaled_rect, 0.4, 7, rl.Color(0, 0, 0, int(255 * 0.5)))
self._draw_content(btn_y)
rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE)

View File

@@ -4,7 +4,7 @@ import pyray as rl
from typing import Union
from collections.abc import Callable
from typing import cast
from openpilot.system.ui.widgets import Widget, NavWidget, DialogResult
from openpilot.system.ui.widgets import Widget, NavWidget
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard
from openpilot.system.ui.lib.text_measure import measure_text_cached
@@ -23,17 +23,8 @@ PADDING = 20
class BigDialogBase(NavWidget, abc.ABC):
def __init__(self):
super().__init__()
self._ret = DialogResult.NO_ACTION
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
self.set_back_callback(lambda: setattr(self, '_ret', DialogResult.CANCEL))
def _render(self, _) -> DialogResult:
"""
Allows `gui_app.set_modal_overlay(BigDialog(...))`.
The overlay runner keeps calling until result != NO_ACTION.
"""
return self._ret
self.set_back_callback(gui_app.pop_widget)
class BigDialog(BigDialogBase):
@@ -44,7 +35,7 @@ class BigDialog(BigDialogBase):
self._title = title
self._description = description
def _render(self, _) -> DialogResult:
def _render(self, _):
super()._render(_)
# draw title
@@ -74,8 +65,6 @@ class BigDialog(BigDialogBase):
gui_label(desc_rect, desc_wrapped, 30, font_weight=FontWeight.MEDIUM,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
return self._ret
class BigConfirmationDialogV2(BigDialogBase):
def __init__(self, title: str, icon: str, red: bool = False,
@@ -91,22 +80,21 @@ class BigConfirmationDialogV2(BigDialogBase):
self._slider = RedBigSlider(title, icon_txt, confirm_callback=self._on_confirm)
else:
self._slider = BigSlider(title, icon_txt, confirm_callback=self._on_confirm)
self._slider.set_enabled(lambda: not self._swiping_away)
self._slider.set_enabled(lambda: self.enabled and not self._swiping_away) # self.enabled for nav stack
def _on_confirm(self):
if self._exit_on_confirm:
gui_app.pop_widget()
if self._confirm_callback:
self._confirm_callback()
if self._exit_on_confirm:
self._ret = DialogResult.CONFIRM
def _update_state(self):
super()._update_state()
if self._swiping_away and not self._slider.confirmed:
self._slider.reset()
def _render(self, _) -> DialogResult:
def _render(self, _):
self._slider.render(self._rect)
return self._ret
class BigInputDialog(BigDialogBase):
@@ -124,6 +112,7 @@ class BigInputDialog(BigDialogBase):
font_weight=FontWeight.MEDIUM)
self._keyboard = MiciKeyboard()
self._keyboard.set_text(default_text)
self._keyboard.set_enabled(lambda: self.enabled) # for nav stack
self._minimum_length = minimum_length
self._backspace_held_time: float | None = None
@@ -140,9 +129,10 @@ class BigInputDialog(BigDialogBase):
self._top_right_button_rect = rl.Rectangle(0, 0, 0, 0)
def confirm_callback_wrapper():
self._ret = DialogResult.CONFIRM
text = self._keyboard.text()
gui_app.pop_widget()
if confirm_callback:
confirm_callback(self._keyboard.text())
confirm_callback(text)
self._confirm_callback = confirm_callback_wrapper
def _update_state(self):
@@ -195,11 +185,13 @@ class BigInputDialog(BigDialogBase):
rl.BLACK, rl.BLANK)
# draw cursor
blink_alpha = (math.sin(rl.get_time() * 6) + 1) / 2
if text:
blink_alpha = (math.sin(rl.get_time() * 6) + 1) / 2
cursor_x = min(text_x + text_size.x + 3, text_field_rect.x + text_field_rect.width)
rl.draw_rectangle_rounded(rl.Rectangle(int(cursor_x), int(text_field_rect.y), 4, int(text_size.y)),
1, 4, rl.Color(255, 255, 255, int(255 * blink_alpha)))
else:
cursor_x = text_field_rect.x - 6
rl.draw_rectangle_rounded(rl.Rectangle(int(cursor_x), int(text_field_rect.y), 4, int(text_size.y)),
1, 4, rl.Color(255, 255, 255, int(255 * blink_alpha)))
# draw backspace icon with nice fade
self._backspace_img_alpha.update(255 * bool(text))
@@ -209,7 +201,10 @@ class BigInputDialog(BigDialogBase):
if not text and self._hint_label.text and not candidate_char:
# draw description if no text entered yet and not drawing candidate char
self._hint_label.render(text_field_rect)
hint_rect = rl.Rectangle(text_field_rect.x, text_field_rect.y,
self._rect.width - text_field_rect.x - PADDING,
text_field_rect.height)
self._hint_label.render(hint_rect)
# TODO: move to update state
# make rect take up entire area so it's easier to click
@@ -233,8 +228,6 @@ class BigInputDialog(BigDialogBase):
rl.draw_rectangle_lines_ex(self._top_right_button_rect, 1, rl.Color(0, 255, 0, 255))
rl.draw_rectangle_lines_ex(self._top_left_button_rect, 1, rl.Color(0, 255, 0, 255))
return self._ret
def _handle_mouse_press(self, mouse_pos: MousePos):
super()._handle_mouse_press(mouse_pos)
# TODO: need to track where press was so enter and back can activate on release rather than press
@@ -387,8 +380,6 @@ class BigMultiOptionDialog(BigDialogBase):
super()._render(_)
self._scroller.render(self._rect)
return self._ret
class BigDialogButton(BigButton):
def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", description: str = ""):
@@ -399,4 +390,4 @@ class BigDialogButton(BigButton):
super()._handle_mouse_release(mouse_pos)
dlg = BigDialog(self.text, self._description)
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)

View File

@@ -19,7 +19,7 @@ class PairingDialog(NavWidget):
def __init__(self):
super().__init__()
self.set_back_callback(lambda: gui_app.set_modal_overlay(None))
self.set_back_callback(gui_app.pop_widget)
self._params = Params()
self._qr_texture: rl.Texture | None = None
self._last_qr_generation = float("-inf")
@@ -72,7 +72,7 @@ class PairingDialog(NavWidget):
if ui_state.prime_state.is_paired():
self._playing_dismiss_animation = True
def _render(self, rect: rl.Rectangle) -> int:
def _render(self, rect: rl.Rectangle):
self._check_qr_refresh()
self._render_qr_code()
@@ -85,8 +85,6 @@ class PairingDialog(NavWidget):
rl.draw_texture_ex(self._txt_pair, rl.Vector2(label_x, self._rect.y + self._rect.height - self._txt_pair.height - 16),
0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.35)))
return -1
def _render_qr_code(self) -> None:
if not self._qr_texture:
error_font = gui_app.font(FontWeight.BOLD)
@@ -107,10 +105,9 @@ class PairingDialog(NavWidget):
if __name__ == "__main__":
gui_app.init_window("pairing device")
pairing = PairingDialog()
gui_app.push_widget(pairing)
try:
for _ in gui_app.render():
result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
if result != -1:
break
pass
finally:
del pairing

View File

@@ -221,6 +221,7 @@ class AugmentedRoadView(CameraView):
if __name__ == "__main__":
gui_app.init_window("OnRoad Camera View")
road_camera_view = AugmentedRoadView(ROAD_CAM)
gui_app.push_widget(road_camera_view)
print("***press space to switch camera view***")
try:
for _ in gui_app.render():
@@ -229,6 +230,5 @@ if __name__ == "__main__":
if WIDE_CAM in road_camera_view.available_streams:
stream = ROAD_CAM if road_camera_view.stream_type == WIDE_CAM else WIDE_CAM
road_camera_view.switch_stream(stream)
road_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
finally:
road_camera_view.close()

View File

@@ -14,7 +14,7 @@ class DriverCameraDialog(CameraView):
super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER)
self.driver_state_renderer = DriverStateRenderer()
# TODO: this can grow unbounded, should be given some thought
device.add_interactive_timeout_callback(lambda: gui_app.set_modal_overlay(None))
device.add_interactive_timeout_callback(gui_app.pop_widget)
ui_state.params.put_bool("IsDriverViewEnabled", True)
def hide_event(self):
@@ -24,7 +24,7 @@ class DriverCameraDialog(CameraView):
def _handle_mouse_release(self, _):
super()._handle_mouse_release(_)
gui_app.set_modal_overlay(None)
gui_app.pop_widget()
def __del__(self):
self.close()
@@ -103,9 +103,9 @@ if __name__ == "__main__":
gui_app.init_window("Driver Camera View")
driver_camera_view = DriverCameraDialog()
gui_app.push_widget(driver_camera_view)
try:
for _ in gui_app.render():
ui_state.update()
driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
finally:
driver_camera_view.close()

View File

@@ -1,9 +1,2 @@
test
test_translations
test_ui/report_1
test_ui/raylib_report
diff/*.mp4
diff/*.html
diff/.coverage
diff/htmlcov/

2
selfdrive/ui/tests/diff/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
report
.coverage

View File

@@ -2,26 +2,27 @@
import os
import sys
import subprocess
import tempfile
import webbrowser
import argparse
from pathlib import Path
from openpilot.common.basedir import BASEDIR
DIFF_OUT_DIR = Path(BASEDIR) / "selfdrive" / "ui" / "tests" / "diff" / "report"
HTML_TEMPLATE_PATH = Path(__file__).with_name("diff_template.html")
def extract_frames(video_path, output_dir):
output_pattern = str(output_dir / "frame_%04d.png")
cmd = ['ffmpeg', '-i', video_path, '-vsync', '0', output_pattern, '-y']
subprocess.run(cmd, capture_output=True, check=True)
frames = sorted(output_dir.glob("frame_*.png"))
return frames
def compare_frames(frame1_path, frame2_path):
result = subprocess.run(['cmp', '-s', frame1_path, frame2_path])
return result.returncode == 0
def extract_framehashes(video_path):
cmd = ['ffmpeg', '-i', video_path, '-map', '0:v:0', '-vsync', '0', '-f', 'framehash', '-hash', 'md5', '-']
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
hashes = []
for line in result.stdout.splitlines():
if not line or line.startswith('#'):
continue
parts = line.split(',')
if len(parts) < 4:
continue
hashes.append(parts[-1].strip())
return hashes
def create_diff_video(video1, video2, output_path):
@@ -31,38 +32,24 @@ def create_diff_video(video1, video2, output_path):
subprocess.run(cmd, capture_output=True, check=True)
def find_differences(video1, video2):
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
def find_differences(video1, video2) -> tuple[list[int], tuple[int, int]]:
print(f"Hashing frames from {video1}...")
hashes1 = extract_framehashes(video1)
print(f"Extracting frames from {video1}...")
frames1_dir = tmpdir / "frames1"
frames1_dir.mkdir()
frames1 = extract_frames(video1, frames1_dir)
print(f"Hashing frames from {video2}...")
hashes2 = extract_framehashes(video2)
print(f"Extracting frames from {video2}...")
frames2_dir = tmpdir / "frames2"
frames2_dir.mkdir()
frames2 = extract_frames(video2, frames2_dir)
print(f"Comparing {len(hashes1)} frames...")
different_frames = []
if len(frames1) != len(frames2):
print(f"WARNING: Frame count mismatch: {len(frames1)} vs {len(frames2)}")
min_frames = min(len(frames1), len(frames2))
frames1 = frames1[:min_frames]
frames2 = frames2[:min_frames]
for i, (h1, h2) in enumerate(zip(hashes1, hashes2, strict=False)):
if h1 != h2:
different_frames.append(i)
print(f"Comparing {len(frames1)} frames...")
different_frames = []
for i, (f1, f2) in enumerate(zip(frames1, frames2, strict=False)):
is_different = not compare_frames(f1, f2)
if is_different:
different_frames.append(i)
return different_frames, len(frames1)
return different_frames, (len(hashes1), len(hashes2))
def generate_html_report(video1, video2, basedir, different_frames, total_frames):
def generate_html_report(videos: tuple[str, str], basedir: str, different_frames: list[int], frame_counts: tuple[int, int], diff_video_name):
chunks = []
if different_frames:
current_chunk = [different_frames[0]]
@@ -74,71 +61,28 @@ def generate_html_report(video1, video2, basedir, different_frames, total_frames
current_chunk = [different_frames[i]]
chunks.append(current_chunk)
total_frames = max(frame_counts)
frame_delta = frame_counts[1] - frame_counts[0]
different_total = len(different_frames) + abs(frame_delta)
result_text = (
f"✅ Videos are identical! ({total_frames} frames)"
if len(different_frames) == 0
else f"❌ Found {len(different_frames)} different frames out of {total_frames} total ({(len(different_frames) / total_frames * 100):.1f}%)"
if different_total == 0
else f"❌ Found {different_total} different frames out of {total_frames} total ({different_total / total_frames * 100:.1f}%)."
+ (f" Video {'2' if frame_delta > 0 else '1'} is longer by {abs(frame_delta)} frames." if frame_delta != 0 else "")
)
html = f"""<h2>UI Diff</h2>
<table>
<tr>
<td width='33%'>
<p><strong>Video 1</strong></p>
<video id='video1' width='100%' autoplay muted loop onplay='syncVideos()'>
<source src='{os.path.join(basedir, os.path.basename(video1))}' type='video/mp4'>
Your browser does not support the video tag.
</video>
</td>
<td width='33%'>
<p><strong>Video 2</strong></p>
<video id='video2' width='100%' autoplay muted loop onplay='syncVideos()'>
<source src='{os.path.join(basedir, os.path.basename(video2))}' type='video/mp4'>
Your browser does not support the video tag.
</video>
</td>
<td width='33%'>
<p><strong>Pixel Diff</strong></p>
<video id='diffVideo' width='100%' autoplay muted loop>
<source src='{os.path.join(basedir, 'diff.mp4')}' type='video/mp4'>
Your browser does not support the video tag.
</video>
</td>
</tr>
</table>
<script>
function syncVideos() {{
const video1 = document.getElementById('video1');
const video2 = document.getElementById('video2');
const diffVideo = document.getElementById('diffVideo');
video1.currentTime = video2.currentTime = diffVideo.currentTime;
}}
video1.addEventListener('timeupdate', () => {{
if (Math.abs(video1.currentTime - video2.currentTime) > 0.1) {{
video2.currentTime = video1.currentTime;
}}
if (Math.abs(video1.currentTime - diffVideo.currentTime) > 0.1) {{
diffVideo.currentTime = video1.currentTime;
}}
}});
video2.addEventListener('timeupdate', () => {{
if (Math.abs(video2.currentTime - video1.currentTime) > 0.1) {{
video1.currentTime = video2.currentTime;
}}
if (Math.abs(video2.currentTime - diffVideo.currentTime) > 0.1) {{
diffVideo.currentTime = video2.currentTime;
}}
}});
diffVideo.addEventListener('timeupdate', () => {{
if (Math.abs(diffVideo.currentTime - video1.currentTime) > 0.1) {{
video1.currentTime = diffVideo.currentTime;
video2.currentTime = diffVideo.currentTime;
}}
}});
</script>
<hr>
<p><strong>Results:</strong> {result_text}</p>
"""
# Load HTML template and replace placeholders
html = HTML_TEMPLATE_PATH.read_text()
placeholders = {
"VIDEO1_SRC": os.path.join(basedir, os.path.basename(videos[0])),
"VIDEO2_SRC": os.path.join(basedir, os.path.basename(videos[1])),
"DIFF_SRC": os.path.join(basedir, diff_video_name),
"RESULT_TEXT": result_text,
}
for key, value in placeholders.items():
html = html.replace(f"${key}", value)
return html
@@ -152,6 +96,9 @@ def main():
args = parser.parse_args()
if not args.output.lower().endswith('.html'):
args.output += '.html'
os.makedirs(DIFF_OUT_DIR, exist_ok=True)
print("=" * 60)
@@ -162,18 +109,19 @@ def main():
print(f"Output: {args.output}")
print()
# Create diff video
diff_video_path = os.path.join(os.path.dirname(args.output), DIFF_OUT_DIR / "diff.mp4")
# Create diff video with name derived from output HTML
diff_video_name = Path(args.output).stem + '.mp4'
diff_video_path = str(DIFF_OUT_DIR / diff_video_name)
create_diff_video(args.video1, args.video2, diff_video_path)
different_frames, total_frames = find_differences(args.video1, args.video2)
different_frames, frame_counts = find_differences(args.video1, args.video2)
if different_frames is None:
sys.exit(1)
print()
print("Generating HTML report...")
html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, total_frames)
html = generate_html_report((args.video1, args.video2), args.basedir, different_frames, frame_counts, diff_video_name)
with open(DIFF_OUT_DIR / args.output, 'w') as f:
f.write(html)
@@ -183,7 +131,8 @@ def main():
print(f"Opening {args.output} in browser...")
webbrowser.open(f'file://{os.path.abspath(DIFF_OUT_DIR / args.output)}')
return 0 if len(different_frames) == 0 else 1
extra_frames = abs(frame_counts[0] - frame_counts[1])
return 0 if (len(different_frames) + extra_frames) == 0 else 1
if __name__ == "__main__":

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UI Diff Report</title>
<style>
h1, h2, h3, h4 {
margin: 0.5em 0 0.25em;
}
#videoTable {
width: 100%;
}
@media (max-width: 800px) {
#videoTable tr {
display: flex;
flex-direction: column;
}
#videoTable td {
width: 100% !important;
}
}
</style>
</head>
<body>
<h2>UI Diff</h2>
<table id='videoTable'></table>
<hr>
<p><strong>Results:</strong> $RESULT_TEXT</p>
<script>
const videoDefs = [
{ id: 'video1', title: 'Video 1', src: '$VIDEO1_SRC', sync: true },
{ id: 'video2', title: 'Video 2', src: '$VIDEO2_SRC', sync: true },
{ id: 'diffVideo', title: 'Pixel Diff', src: '$DIFF_SRC', sync: false },
];
const row = document.getElementById('videoTable').insertRow();
videoDefs.forEach(({ id, title, src, sync }) => {
const td = row.insertCell();
td.width = '33%';
td.innerHTML = `<h4>${title}</h4>
<video id='${id}' width='100%' autoplay muted ${sync ? "onplay='syncVideos()'" : ''}>
<source src='${src}' type='video/mp4'>
Your browser does not support the video tag.
</video>`;
});
const videos = videoDefs.map(({ id }) => document.getElementById(id));
const isEnded = (v) => v.ended || (Number.isFinite(v.duration) && v.currentTime >= (v.duration - 0.05));
const playAll = () => videos.forEach((v) => v.play());
function syncVideos() {
const t = Math.min(...videos.map((v) => v.currentTime));
videos.forEach((v) => { v.currentTime = t; });
playAll();
}
function handleEnded(endedVideo) {
endedVideo.pause();
if (videos.every(isEnded)) {
videos.forEach((v) => { v.currentTime = 0; });
playAll();
}
}
videos.forEach((v) => {
v.addEventListener('timeupdate', () => {
videos.forEach((other) => {
if (other !== v && !isEnded(other) && Math.abs(v.currentTime - other.currentTime) > 0.1) {
other.currentTime = v.currentTime;
if (other.paused) other.play();
}
});
});
v.addEventListener('ended', () => handleEnded(v));
});
</script>
</body>
</html>

View File

@@ -1,41 +1,21 @@
#!/usr/bin/env python3
import os
import time
import argparse
import coverage
import pyray as rl
from dataclasses import dataclass
from openpilot.selfdrive.ui.tests.diff.diff import DIFF_OUT_DIR
os.environ["RECORD"] = "1"
if "RECORD_OUTPUT" not in os.environ:
os.environ["RECORD_OUTPUT"] = "mici_ui_replay.mp4"
os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ["RECORD_OUTPUT"])
from typing import Literal
from collections.abc import Callable
from cereal.messaging import PubMaster
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.ui.tests.diff.diff import DIFF_OUT_DIR
from openpilot.system.version import terms_version, training_version
from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent
from openpilot.selfdrive.ui.ui_state import ui_state
LayoutVariant = Literal["mici", "tizi"]
FPS = 60
HEADLESS = os.getenv("WINDOWED", "0") == "1"
@dataclass
class DummyEvent:
click: bool = False
# TODO: add some kind of intensity
swipe_left: bool = False
swipe_right: bool = False
swipe_down: bool = False
SCRIPT = [
(0, DummyEvent()),
(FPS * 1, DummyEvent(click=True)),
(FPS * 2, DummyEvent(click=True)),
(FPS * 3, DummyEvent()),
]
HEADLESS = os.getenv("WINDOWED", "0") != "1"
def setup_state():
@@ -44,69 +24,66 @@ def setup_state():
params.put("CompletedTrainingVersion", training_version)
params.put("DongleId", "test123456789")
params.put("UpdaterCurrentDescription", "0.10.1 / test-branch / abc1234 / Nov 30")
return None
def inject_click(coords):
events = []
x, y = coords[0]
events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=True, left_released=False, left_down=False, t=time.monotonic()))
for x, y in coords[1:]:
events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=False, left_down=True, t=time.monotonic()))
x, y = coords[-1]
events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=True, left_down=False, t=time.monotonic()))
def run_replay(variant: LayoutVariant) -> None:
if HEADLESS:
rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN)
os.environ["OFFSCREEN"] = "1" # Run UI without FPS limit (set before importing gui_app)
with gui_app._mouse._lock:
gui_app._mouse._events.extend(events)
def handle_event(event: DummyEvent):
if event.click:
inject_click([(gui_app.width // 2, gui_app.height // 2)])
if event.swipe_left:
inject_click([(gui_app.width * 3 // 4, gui_app.height // 2),
(gui_app.width // 4, gui_app.height // 2),
(0, gui_app.height // 2)])
if event.swipe_right:
inject_click([(gui_app.width // 4, gui_app.height // 2),
(gui_app.width * 3 // 4, gui_app.height // 2),
(gui_app.width, gui_app.height // 2)])
if event.swipe_down:
inject_click([(gui_app.width // 2, gui_app.height // 4),
(gui_app.width // 2, gui_app.height * 3 // 4),
(gui_app.width // 2, gui_app.height)])
def run_replay():
setup_state()
os.makedirs(DIFF_OUT_DIR, exist_ok=True)
if not HEADLESS:
rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN)
from openpilot.selfdrive.ui.ui_state import ui_state # Import within OpenpilotPrefix context so param values are setup correctly
from openpilot.system.ui.lib.application import gui_app # Import here for accurate coverage
from openpilot.selfdrive.ui.tests.diff.replay_script import build_script
gui_app.init_window("ui diff test", fps=FPS)
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout # import here for coverage
# Dynamically import main layout based on variant
if variant == "mici":
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout as MainLayout
else:
from openpilot.selfdrive.ui.layouts.main import MainLayout
main_layout = MainLayout()
main_layout = MiciMainLayout()
main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
frame = 0
pm = PubMaster(["deviceState", "pandaStates", "driverStateV2", "selfdriveState"])
script = build_script(pm, main_layout, variant)
script_index = 0
for should_render in gui_app.render():
while script_index < len(SCRIPT) and SCRIPT[script_index][0] == frame:
_, event = SCRIPT[script_index]
handle_event(event)
send_fn: Callable | None = None
frame = 0
# Override raylib timing functions to return deterministic values based on frame count instead of real time
rl.get_frame_time = lambda: 1.0 / FPS
rl.get_time = lambda: frame / FPS
# Main loop to replay events and render frames
for _ in gui_app.render():
# Handle all events for the current frame
while script_index < len(script) and script[script_index][0] == frame:
_, event = script[script_index]
# Call setup function, if any
if event.setup:
event.setup()
# Send mouse events to the application
if event.mouse_events:
with gui_app._mouse._lock:
gui_app._mouse._events.extend(event.mouse_events)
# Update persistent send function
if event.send_fn is not None:
send_fn = event.send_fn
# Move to next script event
script_index += 1
# Keep sending cereal messages for persistent states (onroad, alerts)
if send_fn:
send_fn()
ui_state.update()
if should_render:
main_layout.render()
frame += 1
if script_index >= len(SCRIPT):
if script_index >= len(script):
break
gui_app.close()
@@ -116,13 +93,35 @@ def run_replay():
def main():
cov = coverage.coverage(source=['openpilot.selfdrive.ui.mici'])
with cov.collect():
run_replay()
cov.save()
cov.report()
cov.html_report(directory=os.path.join(DIFF_OUT_DIR, 'htmlcov'))
print("HTML report: htmlcov/index.html")
parser = argparse.ArgumentParser()
parser.add_argument('--big', action='store_true', help='Use big UI layout (tizi/tici) instead of mici layout')
args = parser.parse_args()
variant: LayoutVariant = 'tizi' if args.big else 'mici'
if args.big:
os.environ["BIG"] = "1"
os.environ["RECORD"] = "1"
os.environ["RECORD_QUALITY"] = "0" # Use CRF 0 ("lossless" encode) for deterministic output across different machines
os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ.get("RECORD_OUTPUT", f"{variant}_ui_replay.mp4"))
print(f"Running {variant} UI replay...")
with OpenpilotPrefix():
sources = ["openpilot.system.ui"]
if variant == "mici":
sources.append("openpilot.selfdrive.ui.mici")
omit = ["**/*tizi*", "**/*tici*"] # exclude files containing "tizi" or "tici"
else:
sources.extend(["openpilot.selfdrive.ui.layouts", "openpilot.selfdrive.ui.onroad", "openpilot.selfdrive.ui.widgets"])
omit = ["**/*mici*"] # exclude files containing "mici"
cov = coverage.Coverage(source=sources, omit=omit)
with cov.collect():
run_replay(variant)
cov.save()
cov.report()
directory = os.path.join(DIFF_OUT_DIR, f"htmlcov-{variant}")
cov.html_report(directory=directory)
print(f"HTML report: {directory}/index.html")
if __name__ == "__main__":

View File

@@ -0,0 +1,249 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from collections.abc import Callable
from dataclasses import dataclass
from cereal import car, log, messaging
from cereal.messaging import PubMaster
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.selfdrive.ui.tests.diff.replay import FPS, LayoutVariant
from openpilot.system.updated.updated import parse_release_notes
WAIT = int(FPS * 0.5) # Default frames to wait after events
AlertSize = log.SelfdriveState.AlertSize
AlertStatus = log.SelfdriveState.AlertStatus
BRANCH_NAME = "this-is-a-really-super-mega-ultra-max-extreme-ultimate-long-branch-name"
@dataclass
class ScriptEvent:
if TYPE_CHECKING:
# Only import for type checking to avoid excluding the application code from coverage
from openpilot.system.ui.lib.application import MouseEvent
setup: Callable | None = None # Setup function to run prior to adding mouse events
mouse_events: list[MouseEvent] | None = None # Mouse events to send to the application on this event's frame
send_fn: Callable | None = None # When set, the main loop uses this as the new persistent sender
ScriptEntry = tuple[int, ScriptEvent] # (frame, event)
class Script:
def __init__(self, fps: int) -> None:
self.fps = fps
self.frame = 0
self.entries: list[ScriptEntry] = []
def get_frame_time(self) -> float:
return self.frame / self.fps
def add(self, event: ScriptEvent, before: int = 0, after: int = 0) -> None:
"""Add event to the script, optionally with the given number of frames to wait before or after the event."""
self.frame += before
self.entries.append((self.frame, event))
self.frame += after
def end(self) -> None:
"""Add a final empty event to mark the end of the script."""
self.add(ScriptEvent()) # Without this, it will just end on the last event without waiting for any specified delay after it
def wait(self, frames: int) -> None:
"""Add a delay for the given number of frames followed by an empty event."""
self.add(ScriptEvent(), before=frames)
def setup(self, fn: Callable, wait_after: int = WAIT) -> None:
"""Add a setup function to be called immediately followed by a delay of the given number of frames."""
self.add(ScriptEvent(setup=fn), after=wait_after)
def set_send(self, fn: Callable, wait_after: int = WAIT) -> None:
"""Set a new persistent send function to be called every frame."""
self.add(ScriptEvent(send_fn=fn), after=wait_after)
# TODO: Also add more complex gestures, like swipe or drag
def click(self, x: int, y: int, wait_after: int = WAIT, wait_between: int = 2) -> None:
"""Add a click event to the script for the given position and specify frames to wait between mouse events or after the click."""
# NOTE: By default we wait a couple frames between mouse events so pressed states will be rendered
from openpilot.system.ui.lib.application import MouseEvent, MousePos
# TODO: Add support for long press (left_down=True)
mouse_down = MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=True, left_released=False, left_down=False, t=self.get_frame_time())
self.add(ScriptEvent(mouse_events=[mouse_down]), after=wait_between)
mouse_up = MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=True, left_down=False, t=self.get_frame_time())
self.add(ScriptEvent(mouse_events=[mouse_up]), after=wait_after)
# --- Setup functions ---
def put_update_params(params: Params | None = None) -> None:
if params is None:
params = Params()
params.put("UpdaterCurrentReleaseNotes", parse_release_notes(BASEDIR))
params.put("UpdaterNewReleaseNotes", parse_release_notes(BASEDIR))
params.put("UpdaterTargetBranch", BRANCH_NAME)
def setup_offroad_alerts() -> None:
put_update_params(Params())
set_offroad_alert("Offroad_TemperatureTooHigh", True, extra_text='99C')
set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text='longitudinal')
set_offroad_alert("Offroad_IsTakingSnapshot", True)
def setup_update_available() -> None:
params = Params()
params.put_bool("UpdateAvailable", True)
params.put("UpdaterNewDescription", f"0.10.2 / {BRANCH_NAME} / 0a1b2c3 / Jan 01")
put_update_params(params)
def setup_developer_params() -> None:
CP = car.CarParams()
CP.alphaLongitudinalAvailable = True
Params().put("CarParamsPersistent", CP.to_bytes())
# --- Send functions ---
def send_onroad(pm: PubMaster) -> None:
ds = messaging.new_message('deviceState')
ds.deviceState.started = True
ds.deviceState.networkType = log.DeviceState.NetworkType.wifi
ps = messaging.new_message('pandaStates', 1)
ps.pandaStates[0].pandaType = log.PandaState.PandaType.dos
ps.pandaStates[0].ignitionLine = True
pm.send('deviceState', ds)
pm.send('pandaStates', ps)
def make_network_state_setup(pm: PubMaster, network_type) -> Callable:
def _send() -> None:
ds = messaging.new_message('deviceState')
ds.deviceState.networkType = network_type
pm.send('deviceState', ds)
return _send
def make_alert_setup(pm: PubMaster, size, text1, text2, status) -> Callable:
def _send() -> None:
send_onroad(pm)
alert = messaging.new_message('selfdriveState')
ss = alert.selfdriveState
ss.alertSize = size
ss.alertText1 = text1
ss.alertText2 = text2
ss.alertStatus = status
pm.send('selfdriveState', alert)
return _send
# --- Script builders ---
def build_mici_script(pm: PubMaster, main_layout, script: Script) -> None:
"""Build the replay script for the mici layout."""
from openpilot.system.ui.lib.application import gui_app
center = (gui_app.width // 2, gui_app.height // 2)
# TODO: Explore more
script.wait(FPS)
script.click(*center, FPS) # Open settings
script.click(*center, FPS) # Open toggles
script.end()
def build_tizi_script(pm: PubMaster, main_layout, script: Script) -> None:
"""Build the replay script for the tizi layout."""
def make_home_refresh_setup(fn: Callable) -> Callable:
"""Return setup function that calls the given function to modify state and forces an immediate refresh on the home layout."""
from openpilot.selfdrive.ui.layouts.main import MainState
def setup():
fn()
main_layout._layouts[MainState.HOME].last_refresh = 0
return setup
# TODO: Better way of organizing the events
# === Homescreen ===
script.set_send(make_network_state_setup(pm, log.DeviceState.NetworkType.wifi))
# === Offroad Alerts (auto-transitions via HomeLayout refresh) ===
script.setup(make_home_refresh_setup(setup_offroad_alerts))
# === Update Available (auto-transitions via HomeLayout refresh) ===
script.setup(make_home_refresh_setup(setup_update_available))
# === Settings - Device (click sidebar settings button) ===
script.click(150, 90)
script.click(1985, 790) # reset calibration confirmation
script.click(650, 750) # cancel
# === Settings - Network ===
script.click(278, 450)
script.click(1880, 100) # advanced network settings
script.click(630, 80) # back
# === Settings - Toggles ===
script.click(278, 600)
script.click(1200, 280) # experimental mode description
# === Settings - Software ===
script.setup(put_update_params, wait_after=0)
script.click(278, 720)
# === Settings - Firehose ===
script.click(278, 845)
# === Settings - Developer (set CarParamsPersistent first) ===
script.setup(setup_developer_params, wait_after=0)
script.click(278, 950)
script.click(2000, 960) # toggle alpha long
script.click(1500, 875) # confirm
# === Keyboard modal (SSH keys button in developer panel) ===
script.click(1930, 470) # click SSH keys
script.click(1930, 115) # click cancel on keyboard
# === Close settings ===
script.click(250, 160)
# === Onroad ===
script.set_send(lambda: send_onroad(pm))
script.click(1000, 500) # click onroad to toggle sidebar
# === Onroad alerts ===
# Small alert (normal)
script.set_send(make_alert_setup(pm, AlertSize.small, "Small Alert", "This is a small alert", AlertStatus.normal))
# Medium alert (userPrompt)
script.set_send(make_alert_setup(pm, AlertSize.mid, "Medium Alert", "This is a medium alert", AlertStatus.userPrompt))
# Full alert (critical)
script.set_send(make_alert_setup(pm, AlertSize.full, "DISENGAGE IMMEDIATELY", "Driver Distracted", AlertStatus.critical))
# Full alert multiline
script.set_send(make_alert_setup(pm, AlertSize.full, "Reverse\nGear", "", AlertStatus.normal))
# Full alert long text
script.set_send(make_alert_setup(pm, AlertSize.full, "TAKE CONTROL IMMEDIATELY", "Calibration Invalid: Remount Device & Recalibrate", AlertStatus.userPrompt))
# End
script.end()
def build_script(pm: PubMaster, main_layout, variant: LayoutVariant) -> list[ScriptEntry]:
"""Build the replay script for the appropriate layout variant and return list of script entries."""
print(f"Building {variant} replay script...")
script = Script(FPS)
builder = build_tizi_script if variant == 'tizi' else build_mici_script
builder(pm, main_layout, script)
print(f"Built replay script with {len(script.entries)} events and {script.frame} frames ({script.get_frame_time():.2f} seconds)")
return script.entries

View File

@@ -83,7 +83,6 @@ if __name__ == "__main__":
gui_app.init_window("UI Profiling", fps=600)
main_layout = MiciMainLayout()
main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
print("Running...")
patch_submaster(message_chunks)
@@ -95,15 +94,13 @@ if __name__ == "__main__":
yuv_buffer_size = W * H + (W // 2) * (H // 2) * 2
yuv_data = np.random.randint(0, 256, yuv_buffer_size, dtype=np.uint8).tobytes()
with cProfile.Profile() as pr:
for should_render in gui_app.render():
for _ in gui_app.render():
if ui_state.sm.frame >= len(message_chunks):
break
if ui_state.sm.frame % 3 == 0:
eof = int((ui_state.sm.frame % 3) * 0.05 * 1e9)
vipc.send(VisionStreamType.VISION_STREAM_ROAD, yuv_data, ui_state.sm.frame % 3, eof, eof)
ui_state.update()
if should_render:
main_layout.render()
pr.dump_stats(f'{args.output}_deterministic.stats')
rl.close_window()

View File

@@ -5,7 +5,7 @@ import re
import xml.etree.ElementTree as ET
import string
import requests
from parameterized import parameterized_class
from openpilot.common.parameterized import parameterized_class
from openpilot.system.ui.lib.multilang import TRANSLATIONS_DIR, LANGUAGES_FILE
with open(str(LANGUAGES_FILE)) as f:

View File

@@ -1,36 +0,0 @@
#!/usr/bin/env python3
"""
Simple script to print mouse coordinates on Ubuntu.
Run with: python print_mouse_coords.py
Press Ctrl+C to exit.
"""
from pynput import mouse
print("Mouse coordinate printer - Press Ctrl+C to exit")
print("Click to set the top left origin")
origin: tuple[int, int] | None = None
clicks: list[tuple[int, int]] = []
def on_click(x, y, button, pressed):
global origin, clicks
if pressed: # Only on mouse down, not up
if origin is None:
origin = (x, y)
print(f"Origin set to: {x},{y}")
else:
rel_x = x - origin[0]
rel_y = y - origin[1]
clicks.append((rel_x, rel_y))
print(f"Clicks: {clicks}")
if __name__ == "__main__":
try:
# Start mouse listener
with mouse.Listener(on_click=on_click) as listener:
listener.join()
except KeyboardInterrupt:
print("\nExiting...")

View File

@@ -1,317 +0,0 @@
#!/usr/bin/env python3
import os
import sys
import shutil
import time
import pathlib
from collections import namedtuple
import pyautogui
import pywinctl
from cereal import car, log
from cereal import messaging
from cereal.messaging import PubMaster
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.test.helpers import with_processes
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.system.updated.updated import parse_release_notes
from openpilot.system.version import terms_version, training_version
AlertSize = log.SelfdriveState.AlertSize
AlertStatus = log.SelfdriveState.AlertStatus
TEST_DIR = pathlib.Path(__file__).parent
TEST_OUTPUT_DIR = TEST_DIR / "raylib_report"
SCREENSHOTS_DIR = TEST_OUTPUT_DIR / "screenshots"
UI_DELAY = 0.5
BRANCH_NAME = "this-is-a-really-super-mega-ultra-max-extreme-ultimate-long-branch-name"
VERSION = f"0.10.1 / {BRANCH_NAME} / 7864838 / Oct 03"
# Offroad alerts to test
OFFROAD_ALERTS = ['Offroad_IsTakingSnapshot']
def put_update_params(params: Params):
params.put("UpdaterCurrentReleaseNotes", parse_release_notes(BASEDIR))
params.put("UpdaterNewReleaseNotes", parse_release_notes(BASEDIR))
params.put("UpdaterTargetBranch", BRANCH_NAME)
def setup_homescreen(click, pm: PubMaster):
pass
def setup_homescreen_update_available(click, pm: PubMaster):
params = Params()
params.put_bool("UpdateAvailable", True)
put_update_params(params)
setup_offroad_alert(click, pm)
def setup_settings(click, pm: PubMaster):
click(100, 100)
def close_settings(click, pm: PubMaster):
click(240, 216)
def setup_settings_network(click, pm: PubMaster):
setup_settings(click, pm)
click(278, 450)
def setup_settings_network_advanced(click, pm: PubMaster):
setup_settings_network(click, pm)
click(1880, 100)
def setup_settings_toggles(click, pm: PubMaster):
setup_settings(click, pm)
click(278, 600)
def setup_settings_software(click, pm: PubMaster):
put_update_params(Params())
setup_settings(click, pm)
click(278, 720)
def setup_settings_software_download(click, pm: PubMaster):
params = Params()
# setup_settings_software but with "DOWNLOAD" button to test long text
params.put("UpdaterState", "idle")
params.put_bool("UpdaterFetchAvailable", True)
setup_settings_software(click, pm)
def setup_settings_software_release_notes(click, pm: PubMaster):
setup_settings_software(click, pm)
click(588, 110) # expand description for current version
def setup_settings_software_branch_switcher(click, pm: PubMaster):
setup_settings_software(click, pm)
params = Params()
params.put("UpdaterAvailableBranches", f"master,nightly,release,{BRANCH_NAME}")
params.put("GitBranch", BRANCH_NAME) # should be on top
params.put("UpdaterTargetBranch", "nightly") # should be selected
click(1984, 449)
def setup_settings_firehose(click, pm: PubMaster):
setup_settings(click, pm)
click(278, 845)
def setup_settings_developer(click, pm: PubMaster):
CP = car.CarParams()
CP.alphaLongitudinalAvailable = True # show alpha long control toggle
Params().put("CarParamsPersistent", CP.to_bytes())
setup_settings(click, pm)
click(278, 950)
def setup_keyboard(click, pm: PubMaster):
setup_settings_developer(click, pm)
click(1930, 470)
def setup_pair_device(click, pm: PubMaster):
click(1950, 800)
def setup_offroad_alert(click, pm: PubMaster):
put_update_params(Params())
set_offroad_alert("Offroad_TemperatureTooHigh", True, extra_text='99C')
set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text='longitudinal')
for alert in OFFROAD_ALERTS:
set_offroad_alert(alert, True)
setup_settings(click, pm)
close_settings(click, pm)
def setup_confirmation_dialog(click, pm: PubMaster):
setup_settings(click, pm)
click(1985, 791) # reset calibration
def setup_experimental_mode_description(click, pm: PubMaster):
setup_settings_toggles(click, pm)
click(1200, 280) # expand description for experimental mode
def setup_openpilot_long_confirmation_dialog(click, pm: PubMaster):
setup_settings_developer(click, pm)
click(2000, 960) # toggle openpilot longitudinal control
def setup_onroad(click, pm: PubMaster):
ds = messaging.new_message('deviceState')
ds.deviceState.started = True
ps = messaging.new_message('pandaStates', 1)
ps.pandaStates[0].pandaType = log.PandaState.PandaType.dos
ps.pandaStates[0].ignitionLine = True
driverState = messaging.new_message('driverStateV2')
driverState.driverStateV2.leftDriverData.faceOrientation = [0, 0, 0]
for _ in range(5):
pm.send('deviceState', ds)
pm.send('pandaStates', ps)
pm.send('driverStateV2', driverState)
ds.clear_write_flag()
ps.clear_write_flag()
driverState.clear_write_flag()
time.sleep(0.05)
def setup_onroad_sidebar(click, pm: PubMaster):
setup_onroad(click, pm)
click(100, 100) # open sidebar
def setup_onroad_alert(click, pm: PubMaster, size: log.SelfdriveState.AlertSize, text1: str, text2: str, status: log.SelfdriveState.AlertStatus):
setup_onroad(click, pm)
alert = messaging.new_message('selfdriveState')
ss = alert.selfdriveState
ss.alertSize = size
ss.alertText1 = text1
ss.alertText2 = text2
ss.alertStatus = status
for _ in range(5):
pm.send('selfdriveState', alert)
alert.clear_write_flag()
time.sleep(0.05)
def setup_onroad_small_alert(click, pm: PubMaster):
setup_onroad_alert(click, pm, AlertSize.small, "Small Alert", "This is a small alert", AlertStatus.normal)
def setup_onroad_medium_alert(click, pm: PubMaster):
setup_onroad_alert(click, pm, AlertSize.mid, "Medium Alert", "This is a medium alert", AlertStatus.userPrompt)
def setup_onroad_full_alert(click, pm: PubMaster):
setup_onroad_alert(click, pm, AlertSize.full, "DISENGAGE IMMEDIATELY", "Driver Distracted", AlertStatus.critical)
def setup_onroad_full_alert_multiline(click, pm: PubMaster):
setup_onroad_alert(click, pm, AlertSize.full, "Reverse\nGear", "", AlertStatus.normal)
def setup_onroad_full_alert_long_text(click, pm: PubMaster):
setup_onroad_alert(click, pm, AlertSize.full, "TAKE CONTROL IMMEDIATELY", "Calibration Invalid: Remount Device & Recalibrate", AlertStatus.userPrompt)
CASES = {
"homescreen": setup_homescreen,
"homescreen_paired": setup_homescreen,
"homescreen_prime": setup_homescreen,
"homescreen_update_available": setup_homescreen_update_available,
"homescreen_unifont": setup_homescreen,
"settings_device": setup_settings,
"settings_network": setup_settings_network,
"settings_network_advanced": setup_settings_network_advanced,
"settings_toggles": setup_settings_toggles,
"settings_software": setup_settings_software,
"settings_software_download": setup_settings_software_download,
"settings_software_release_notes": setup_settings_software_release_notes,
"settings_software_branch_switcher": setup_settings_software_branch_switcher,
"settings_firehose": setup_settings_firehose,
"settings_developer": setup_settings_developer,
"keyboard": setup_keyboard,
"pair_device": setup_pair_device,
"offroad_alert": setup_offroad_alert,
"confirmation_dialog": setup_confirmation_dialog,
"experimental_mode_description": setup_experimental_mode_description,
"openpilot_long_confirmation_dialog": setup_openpilot_long_confirmation_dialog,
"onroad": setup_onroad,
"onroad_sidebar": setup_onroad_sidebar,
"onroad_small_alert": setup_onroad_small_alert,
"onroad_medium_alert": setup_onroad_medium_alert,
"onroad_full_alert": setup_onroad_full_alert,
"onroad_full_alert_multiline": setup_onroad_full_alert_multiline,
"onroad_full_alert_long_text": setup_onroad_full_alert_long_text,
}
class TestUI:
def __init__(self):
os.environ["SCALE"] = os.getenv("SCALE", "1")
os.environ["BIG"] = "1"
sys.modules["mouseinfo"] = False
def setup(self):
# Seed minimal offroad state
self.pm = PubMaster(["deviceState", "pandaStates", "driverStateV2", "selfdriveState"])
ds = messaging.new_message('deviceState')
ds.deviceState.networkType = log.DeviceState.NetworkType.wifi
for _ in range(5):
self.pm.send('deviceState', ds)
ds.clear_write_flag()
time.sleep(0.05)
time.sleep(0.5)
try:
self.ui = pywinctl.getWindowsWithTitle("UI")[0]
except Exception as e:
print(f"failed to find ui window, assuming that it's in the top left (for Xvfb) {e}")
self.ui = namedtuple("bb", ["left", "top", "width", "height"])(0, 0, 2160, 1080)
def screenshot(self, name: str):
full_screenshot = pyautogui.screenshot()
cropped = full_screenshot.crop((self.ui.left, self.ui.top, self.ui.left + self.ui.width, self.ui.top + self.ui.height))
cropped.save(SCREENSHOTS_DIR / f"{name}.png")
def click(self, x: int, y: int, *args, **kwargs):
pyautogui.mouseDown(self.ui.left + x, self.ui.top + y, *args, **kwargs)
time.sleep(0.01)
pyautogui.mouseUp(self.ui.left + x, self.ui.top + y, *args, **kwargs)
@with_processes(["ui"])
def test_ui(self, name, setup_case):
self.setup()
time.sleep(UI_DELAY) # wait for UI to start
setup_case(self.click, self.pm)
self.screenshot(name)
def create_screenshots():
if TEST_OUTPUT_DIR.exists():
shutil.rmtree(TEST_OUTPUT_DIR)
SCREENSHOTS_DIR.mkdir(parents=True)
t = TestUI()
for name, setup in CASES.items():
with OpenpilotPrefix():
params = Params()
params.put("DongleId", "123456789012345")
# Set branch name
params.put("UpdaterCurrentDescription", VERSION)
params.put("UpdaterNewDescription", VERSION)
# Set terms and training version (to skip onboarding)
params.put("HasAcceptedTerms", terms_version)
params.put("CompletedTrainingVersion", training_version)
if name == "homescreen_paired":
params.put("PrimeType", 0) # NONE
elif name == "homescreen_prime":
params.put("PrimeType", 2) # LITE
elif name == "homescreen_unifont":
params.put("LanguageSetting", "zh-CHT") # Traditional Chinese
t.test_ui(name, setup)
if __name__ == "__main__":
create_screenshots()

View File

@@ -1,34 +0,0 @@
<html>
<style>
.column {
float: left;
width: 50%;
padding: 5px;
}
.row::after {
content: "";
clear: both;
display: table;
}
.image {
width: 100%;
}
</style>
{% for name, (image, ref_image) in cases.items() %}
<h1>{{name}}</h1>
<div class="row">
<div class="column">
<img class="image" src="{{ image }}" />
</div>
</div>
<br>
{% endfor %}
</html>

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