mirror of
https://github.com/ajouatom/openpilot.git
synced 2026-06-08 02:54:45 +08:00
v4
This commit is contained in:
@@ -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
16
Jenkinsfile
vendored
@@ -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/"]]),
|
||||
])
|
||||
},
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
47
common/parameterized.py
Normal 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
|
||||
@@ -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"}},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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로 나오는듯..
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
from opendbc.car.interfaces import CarInterfaceBase
|
||||
|
||||
class CarrotCarInterface(CarInterfaceBase):
|
||||
def _carrot_init(self, CP, ret):
|
||||
return ret
|
||||
pass
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -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})", "|")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
40
selfdrive/carrot/carrot_controls.py
Normal file
40
selfdrive/carrot/carrot_controls.py
Normal 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
|
||||
@@ -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):
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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("&", "&")
|
||||
@@ -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(() => {});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
0
selfdrive/controls/lib/desire_lib/__init__.py
Normal file
0
selfdrive/controls/lib/desire_lib/__init__.py
Normal file
106
selfdrive/controls/lib/desire_lib/blinker_manager.py
Normal file
106
selfdrive/controls/lib/desire_lib/blinker_manager.py
Normal 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,
|
||||
)
|
||||
41
selfdrive/controls/lib/desire_lib/constants.py
Normal file
41
selfdrive/controls/lib/desire_lib/constants.py
Normal 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,
|
||||
}
|
||||
21
selfdrive/controls/lib/desire_lib/hysteresis.py
Normal file
21
selfdrive/controls/lib/desire_lib/hysteresis.py
Normal 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
|
||||
15
selfdrive/controls/lib/desire_lib/lane_math.py
Normal file
15
selfdrive/controls/lib/desire_lib/lane_math.py
Normal 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
|
||||
45
selfdrive/controls/lib/desire_lib/maneuver_classifier.py
Normal file
45
selfdrive/controls/lib/desire_lib/maneuver_classifier.py
Normal 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"
|
||||
156
selfdrive/controls/lib/desire_lib/side_state.py
Normal file
156
selfdrive/controls/lib/desire_lib/side_state.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
45
selfdrive/controls/tests/test_latcontrol.py
Normal file
45
selfdrive/controls/tests/test_latcontrol.py
Normal 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
|
||||
36
selfdrive/controls/tests/test_latcontrol_torque_buffer.py
Normal file
36
selfdrive/controls/tests/test_latcontrol_torque_buffer.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
70
selfdrive/controls/tests/test_torqued_lat_accel_offset.py
Normal file
70
selfdrive/controls/tests/test_torqued_lat_accel_offset.py
Normal 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)
|
||||
135
selfdrive/locationd/test/test_lagd.py
Normal file
135
selfdrive/locationd/test/test_lagd.py
Normal 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
|
||||
67
selfdrive/locationd/test/test_paramsd.py
Normal file
67
selfdrive/locationd/test/test_paramsd.py
Normal 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
|
||||
25
selfdrive/locationd/test/test_torqued.py
Normal file
25
selfdrive/locationd/test/test_torqued.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
38
selfdrive/modeld/external_pickle.py
Normal file
38
selfdrive/modeld/external_pickle.py
Normal 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]))
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(_)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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"""
|
||||
|
||||
83
selfdrive/ui/mici/onroad/traffic_light.py
Normal file
83
selfdrive/ui/mici/onroad/traffic_light.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
7
selfdrive/ui/tests/.gitignore
vendored
7
selfdrive/ui/tests/.gitignore
vendored
@@ -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
2
selfdrive/ui/tests/diff/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
report
|
||||
.coverage
|
||||
@@ -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__":
|
||||
|
||||
80
selfdrive/ui/tests/diff/diff_template.html
Normal file
80
selfdrive/ui/tests/diff/diff_template.html
Normal 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>
|
||||
@@ -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__":
|
||||
|
||||
249
selfdrive/ui/tests/diff/replay_script.py
Normal file
249
selfdrive/ui/tests/diff/replay_script.py
Normal 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
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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...")
|
||||
@@ -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()
|
||||
@@ -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
Reference in New Issue
Block a user