feat(monitoring): 添加驾驶员分心检测灵敏度设置功能

新增 `DistractionDetectionLevel` 参数以控制驾驶员分心检测的灵敏度等级,并在 `dmonitoringd.py` 和 `helpers.py` 中实现不同等级对应的时间阈值配置。同时更新了相关逻辑以支持动态调整该参数。

fix(toyota): 支持 Toyota Wildlander PHEV 车型接入与控制

增加对 Toyota Wildlander PHEV 的指纹识别、车辆规格定义及接口适配,确保其在 TSS2 平台下的正常运行,并修正部分雷达ACC判断条件。

feat(ui): 优化 Dragonpilot 设置界面选项显示语言一致性

将 Dragonpilot 设置页面中的多个下拉选项文本进行国际化处理,统一使用翻译函数包裹,提升多语言兼容性。

chore(config): 更新 launch 脚本 API 地址并切换 shell 解释器

修改 `launch_openpilot.sh` 使用 `/usr/bin/bash` 作为解释器,并设置自定义 API 与 Athena 服务地址。

refactor(key): 实现 ECU 秘钥提取脚本并写入参数存储

创建 `key.py` 脚本用于通过 UDS 协议从 ECU 提取 SecOC 密钥,并将其保存至系统参数中供后续使用。

docs(vscode): 移除不再使用的终端配置项

清理 `.vscode/settings.json` 文件中过时的 terminal 配置内容。

feat(fonts): 新增中文字体资源文件

添加 `china.ttf` 字体文件以增强 UI 在中文环境下的渲染效果。

build(payload): 添加二进制负载文件

引入新的二进制 payload 文件用于辅助密钥提取流程。
This commit is contained in:
程序员小刘 2025-11-14 16:00:25 +08:00
parent d8075d43a0
commit 2270c6d7f1
46 changed files with 8499 additions and 2348 deletions

View File

@ -4,12 +4,6 @@
"editor.renderWhitespace": "trailing", "editor.renderWhitespace": "trailing",
"files.trimTrailingWhitespace": true, "files.trimTrailingWhitespace": true,
"terminal.integrated.defaultProfile.linux": "dragonpilot", "terminal.integrated.defaultProfile.linux": "dragonpilot",
"terminal.integrated.profiles.linux": {
"dragonpilot": {
"path": "bash",
"args": ["-c", "distrobox enter dp"]
}
},
"search.exclude": { "search.exclude": {
"**/.git": true, "**/.git": true,
"**/.venv": true, "**/.venv": true,

83
1.sh Normal file
View File

@ -0,0 +1,83 @@
#!/bin/env sh
persist_dir=/persist
target_dir=${persist_dir}/comma
# Change target dir from sunnylink to comma to make this no longer a test
# Function to remount /persist as read-only
cleanup() {
echo "Remounting ${persist_dir} as read-only..."
sudo mount -o remount,ro ${persist_dir}
}
# Function to check and backup existing keys
backup_keys() {
if [ -f "id_rsa" ] || [ -f "id_rsa.pub" ]; then
timestamp=$(date +%s)
backup_base="id_rsa_backup_$timestamp"
backup_private="$backup_base"
backup_public="${backup_base}.pub"
# Ensure we're not overwriting an existing backup
counter=0
while [ -f "$backup_private" ] || [ -f "$backup_public" ]; do
counter=$((counter + 1))
backup_private="${backup_base}_$counter"
backup_public="${backup_base}_$counter.pub"
done
# Backup the keys
cp id_rsa "$backup_private"
cp id_rsa.pub "$backup_public"
# Verify the backup
original_private_hash=$(sha256sum id_rsa | cut -d ' ' -f 1)
backup_private_hash=$(sha256sum "$backup_private" | cut -d ' ' -f 1)
original_public_hash=$(sha256sum id_rsa.pub | cut -d ' ' -f 1)
backup_public_hash=$(sha256sum "$backup_public" | cut -d ' ' -f 1)
if [ "$original_private_hash" = "$backup_private_hash" ] && [ "$original_public_hash" = "$backup_public_hash" ]; then
echo "Backup verified successfully."
# Safe to delete original keys after successful backup verification
else
echo "Backup verification failed. Aborting operation."
exit 1
fi
echo "Existing keys backed up as $backup_private and $backup_public"
fi
}
# Trap any signal that exits the script to run cleanup function
trap cleanup EXIT
# Remount /persist as read-write
sudo mount -o remount,rw ${persist_dir}
# Ensure the directory exists
mkdir -p ${target_dir}
cd ${target_dir}
# Check for and backup existing keys
#backup_keys
# Generate new keys
if ! ssh-keygen -t rsa -b 4096 -m PEM -f id_rsa -N ''; then
echo "Failed to generate new RSA keys. Exiting..."
exit 1
fi
# Convert the generated SSH public key to PEM format and store it temporarily
if ! openssl rsa -pubout -in id_rsa -out id_rsa.pub -outform PEM; then
echo "Failed to convert the public key to PEM format. Exiting..."
exit 1
fi
# Display the public key
echo "Displaying the public key:"
cat id_rsa.pub
# Cleanup will be called automatically due to trap on EXIT
#echo "Operation completed successfully. System will reboot now."
#sudo reboot

View File

@ -66,7 +66,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"IsTakingSnapshot", {CLEAR_ON_MANAGER_START, BOOL}}, {"IsTakingSnapshot", {CLEAR_ON_MANAGER_START, BOOL}},
{"IsTestedBranch", {CLEAR_ON_MANAGER_START, BOOL}}, {"IsTestedBranch", {CLEAR_ON_MANAGER_START, BOOL}},
{"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, {"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"LanguageSetting", {PERSISTENT, STRING, "en"}}, {"LanguageSetting", {PERSISTENT, STRING, "zh-CHS"}},
{"LastAthenaPingTime", {CLEAR_ON_MANAGER_START, INT}}, {"LastAthenaPingTime", {CLEAR_ON_MANAGER_START, INT}},
{"LastGPSPosition", {PERSISTENT, STRING}}, {"LastGPSPosition", {PERSISTENT, STRING}},
{"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}}, {"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}},
@ -160,4 +160,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"dp_vag_a0_sng", {PERSISTENT, BOOL, "0"}}, {"dp_vag_a0_sng", {PERSISTENT, BOOL, "0"}},
{"dp_vag_pq_steering_patch", {PERSISTENT, BOOL, "0"}}, {"dp_vag_pq_steering_patch", {PERSISTENT, BOOL, "0"}},
{"dp_vag_avoid_eps_lockout", {PERSISTENT, BOOL, "0"}}, {"dp_vag_avoid_eps_lockout", {PERSISTENT, BOOL, "0"}},
// 驾驶员监控灵敏度
{"DistractionDetectionLevel", {PERSISTENT, INT, "1"}},
}; };

View File

@ -196,7 +196,7 @@ class DragonpilotLayout(Widget):
description=lambda: tr("Display the statistics of lead car and/or radar tracking points.<br>Lead: Lead stats only<br>Radar: Radar tracking point stats only<br>All: Lead and Radar stats<br>NOTE: Radar option only works on certain vehicle models."), description=lambda: tr("Display the statistics of lead car and/or radar tracking points.<br>Lead: Lead stats only<br>Radar: Radar tracking point stats only<br>All: Lead and Radar stats<br>NOTE: Radar option only works on certain vehicle models."),
initial_index=int(self._params.get("dp_ui_lead") or 0), initial_index=int(self._params.get("dp_ui_lead") or 0),
callback=lambda val: self._params.put("dp_ui_lead", val), callback=lambda val: self._params.put("dp_ui_lead", val),
options=["Off", "Lead", "Radar", "All"] options=[tr("Off"), tr("Lead"), tr("Radar"), tr("All")]
) )
def _device_toggles(self): def _device_toggles(self):
@ -227,7 +227,7 @@ class DragonpilotLayout(Widget):
self._toggles["dp_ui_display_mode"] = text_spin_button_item( self._toggles["dp_ui_display_mode"] = text_spin_button_item(
title=lambda: tr("Display Mode"), title=lambda: tr("Display Mode"),
callback=lambda val: self._params.put("dp_ui_display_mode", val), callback=lambda val: self._params.put("dp_ui_display_mode", val),
options=["Std.", "MAIN+", "OP+", "MAIN-", "OP-"], options=[tr("Std."), tr("MAIN+"), tr("OP+"), tr("MAIN-"), tr("OP-")],
initial_index=int(self._params.get("dp_ui_display_mode") or 0), initial_index=int(self._params.get("dp_ui_display_mode") or 0),
description=lambda: tr("Std.: Stock behavior.<br>MAIN+: ACC MAIN on = Display ON.<br>OP+: OP enabled = Display ON.<br>MAIN-: ACC MAIN on = Display OFF<br>OP-: OP enabled = Display OFF."), description=lambda: tr("Std.: Stock behavior.<br>MAIN+: ACC MAIN on = Display ON.<br>OP+: OP enabled = Display ON.<br>MAIN-: ACC MAIN on = Display OFF<br>OP-: OP enabled = Display OFF."),
) )
@ -238,7 +238,7 @@ class DragonpilotLayout(Widget):
description=lambda: tr("Std.: Stock behaviour.<br>Warning: Only emits sound when there is a warning.<br>Off: Does not emit any sound at all."), description=lambda: tr("Std.: Stock behaviour.<br>Warning: Only emits sound when there is a warning.<br>Off: Does not emit any sound at all."),
initial_index=int(self._params.get("dp_dev_audible_alert_mode") or 0), initial_index=int(self._params.get("dp_dev_audible_alert_mode") or 0),
callback=lambda val: self._params.put("dp_dev_audible_alert_mode", val), callback=lambda val: self._params.put("dp_dev_audible_alert_mode", val),
options=["Std.", "Warning", "Off"], options=[tr("Std."), tr("Warning"), tr("Off")],
) )
self._toggles["dp_dev_auto_shutdown_in"] = spin_button_item( self._toggles["dp_dev_auto_shutdown_in"] = spin_button_item(

234
key.py Normal file
View File

@ -0,0 +1,234 @@
#!/usr/bin/env python3
import struct
from subprocess import check_output, CalledProcessError
from Crypto.Cipher import AES
from tqdm import tqdm
from panda import Panda
from opendbc.car.uds import UdsClient, ACCESS_TYPE, SESSION_TYPE, DATA_IDENTIFIER_TYPE, SERVICE_TYPE, ROUTINE_CONTROL_TYPE, NegativeResponseError
from opendbc.car.structs import CarParams
from opendbc.car.isotp import isotp_send
ADDR = 0x7a1
DEBUG = False
BUS = 0
SEED_KEY_SECRET = b'\xf0\x5f\x36\xb7\xd7\x8c\x03\xe2\x4a\xb4\xfa\xef\x2a\x57\xd0\x44'
# These are the key and IV used to encrypt the payload in build_payload.py
DID_201_KEY = b'\x00' * 16
DID_202_IV = b'\x00' * 16
# 车型版本
APPLICATION_VERSIONS = {
b'\x018965B4209000\x00\x00\x00\x00': b'\x01!!!!!!!!!!!!!!!!', # 2021 RAV4 Prime
b'\x018965B4233100\x00\x00\x00\x00': b'\x01!!!!!!!!!!!!!!!!', # 2023 RAV4 Prime
b'\x018965B4509100\x00\x00\x00\x00': b'\x01!!!!!!!!!!!!!!!!', # 2021 Sienna
b'\x018965B4221000\x00\x00\x00\x00': b'\x01!!!!!!!!!!!!!!!!', # 2021 WILDLANDER PHEV
}
KEY_STRUCT_SIZE = 0x20
CHECKSUM_OFFSET = 0x1d
SECOC_KEY_SIZE = 0x10
SECOC_KEY_OFFSET = 0x0c
def get_key_struct(data, key_no):
return data[key_no * KEY_STRUCT_SIZE: (key_no + 1) * KEY_STRUCT_SIZE]
def verify_checksum(key_struct):
checksum = sum(key_struct[:CHECKSUM_OFFSET])
checksum = ~checksum & 0xff
return checksum == key_struct[CHECKSUM_OFFSET]
def get_secoc_key(key_struct):
return key_struct[SECOC_KEY_OFFSET:SECOC_KEY_OFFSET + SECOC_KEY_SIZE]
if __name__ == "__main__":
try:
check_output(["pidof", "boardd"])
print("boardd 正在运行,请在运行此脚本前先关闭 openpilot已中止")
exit(1)
except CalledProcessError as e:
if e.returncode != 1: # 1 == no process found (boardd not running)
raise e
except FileNotFoundError:
pass
panda = Panda()
panda.set_safety_mode(CarParams.SafetyModel.elm327)
uds_client = UdsClient(panda, ADDR, ADDR + 8, BUS, timeout=0.1, response_pending_timeout=0.1)
print("获取应用程序版本...")
try:
app_version = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION)
print(" - 应用软件标识application", app_version)
except NegativeResponseError:
print("无法读取应用软件标识。请循环点火。")
exit(1)
if app_version not in APPLICATION_VERSIONS:
print("应用程序版本异常!", app_version)
exit(1)
# Mandatory flow of diagnostic sessions
uds_client.diagnostic_session_control(SESSION_TYPE.DEFAULT)
uds_client.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC)
uds_client.diagnostic_session_control(SESSION_TYPE.PROGRAMMING)
# Get bootloader version
uds_client.diagnostic_session_control(SESSION_TYPE.DEFAULT)
uds_client.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC)
bl_version = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION)
print(" - 应用软件识别(引导加载程序)", bl_version)
if bl_version != APPLICATION_VERSIONS[app_version]:
print("引导加载程序版本异常!", bl_version)
exit(1)
# Go back to programming session
uds_client.diagnostic_session_control(SESSION_TYPE.PROGRAMMING)
# Security Access - Request Seed
seed_payload = b"\x00" * 16
seed = uds_client.security_access(ACCESS_TYPE.REQUEST_SEED, data_record=seed_payload)
key = AES.new(SEED_KEY_SECRET, AES.MODE_ECB).decrypt(seed_payload)
key = AES.new(key, AES.MODE_ECB).encrypt(seed)
print("\n安全访问...")
print(" - SEED:", seed.hex())
print(" - KEY:", key.hex())
# Security Access - Send Key
uds_client.security_access(ACCESS_TYPE.SEND_KEY, key)
print(" - Key OK!")
print("\nPreparing to upload payload...")
# Write something to DID 203, not sure why but needed for state machine
uds_client.write_data_by_identifier(0x203, b"\x00" * 5)
# Write KEY and IV to DID 201/202, prerequisite for request download
print(" - Write data by identifier 0x201", DID_201_KEY.hex())
uds_client.write_data_by_identifier(0x201, DID_201_KEY)
print(" - Write data by identifier 0x202", DID_202_IV.hex())
uds_client.write_data_by_identifier(0x202, DID_202_IV)
# Request download to RAM
data = b"\x01" # [1] Format
data += b"\x46" # [2] 4 size bytes, 6 address bytes
data += b"\x01" # [3] memoryIdentifier
data += b"\x00" # [4]
data += struct.pack('!I', 0xfebf0000) # [5] Address
data += struct.pack('!I', 0x1000) # [9] Size
print("\nUpload payload...")
print(" - Request download")
resp = uds_client._uds_request(SERVICE_TYPE.REQUEST_DOWNLOAD, data=data)
# Upload payload
payload = open("payload.bin", "rb").read()
assert len(payload) == 0x1000
chunk_size = 0x400
for i in range(len(payload) // chunk_size):
print(f" - Transfer data {i}")
uds_client.transfer_data(i + 1, payload[i * chunk_size:(i + 1) * chunk_size])
uds_client.request_transfer_exit()
print("\nVerify payload...")
# Routine control 0x10f0
# [0] 0x31 (routine control)
# [1] 0x01 (start)
# [2] 0x10f0 (routine identifier)
# [4] 0x45 (format, 4 size bytes, 5 address bytes)
# [5] 0x0
# [6] mem addr
# [10] mem addr
data = b"\x45\x00"
data += struct.pack('!I', 0xfebf0000)
data += struct.pack('!I', 0x1000)
uds_client.routine_control(ROUTINE_CONTROL_TYPE.START, 0x10f0, data)
print(" - Routine control 0x10f0 OK!")
print("\nTrigger payload...")
# Now we trigger the payload by trying to erase
# [0] 0x31 (routine control)
# [1] 0x01 (start)
# [2] 0xff00 (routine identifier)
# [4] 0x45 (format, 4 size bytes, 5 address bytes)
# [5] 0x0
# [6] mem addr
# [10] mem addr
data = b"\x45\x00"
data += struct.pack('!I', 0xe0000)
data += struct.pack('!I', 0x8000)
# Manually send so we don't get stuck waiting for the response
# uds_client.routine_control(ROUTINE_CONTROL_TYPE.START, 0xff00, data)
erase = b"\x31\x01\xff\x00" + data
isotp_send(panda, erase, ADDR, bus=BUS)
print("\nDumping keys...")
start = 0xfebe6e34
end = 0xfebe6ff4
extracted = b""
with open(f'data_{start:08x}_{end:08x}.bin', 'wb') as f:
with tqdm(total=end-start) as pbar:
while start < end:
for addr, *_, data, bus in panda.can_recv():
if bus != BUS:
continue
if data == b"\x03\x7f\x31\x78\x00\x00\x00\x00": # Skip response pending
continue
if addr != ADDR + 8:
continue
if DEBUG:
print(f"{data.hex()}")
ptr = struct.unpack("<I", data[:4])[0]
assert (ptr >> 8) == start & 0xffffff # Check lower 24 bits of address
extracted += data[4:]
f.write(data[4:])
f.flush()
start += 4
pbar.update(4)
key_1_ok = verify_checksum(get_key_struct(extracted, 1))
key_4_ok = verify_checksum(get_key_struct(extracted, 4))
if not key_1_ok or not key_4_ok:
print("SecOC key checksum verification failed!")
exit(1)
key_1 = get_secoc_key(get_key_struct(extracted, 1))
key_4 = get_secoc_key(get_key_struct(extracted, 4))
print("\nECU_MASTER_KEY ", key_1.hex())
print("SecOC Key (KEY_4)", key_4.hex())
try:
from openpilot.common.params import Params
params = Params()
params.put("SecOCKey", key_4.hex())
print("\nSecOC key written to param successfully!")
except Exception:
print("\nFailed to write SecOCKey param")

View File

@ -1,3 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/bash
export API_HOST=https://api.konik.ai
export ATHENA_HOST=wss://athena.konik.ai
exec ./launch_chffrplus.sh exec ./launch_chffrplus.sh

View File

@ -10,6 +10,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
"TOYOTA_ALPHARD_TSS2" = "TOYOTA_SIENNA" "TOYOTA_ALPHARD_TSS2" = "TOYOTA_SIENNA"
"TOYOTA_PRIUS_V" = "TOYOTA_PRIUS" "TOYOTA_PRIUS_V" = "TOYOTA_PRIUS"
"TOYOTA_RAV4_PRIME" = "TOYOTA_RAV4_TSS2" "TOYOTA_RAV4_PRIME" = "TOYOTA_RAV4_TSS2"
"TOYOTA_WILDLANDER_PHEV" = "TOYOTA_RAV4_TSS2"
"TOYOTA_SIENNA_4TH_GEN" = "TOYOTA_RAV4_TSS2" "TOYOTA_SIENNA_4TH_GEN" = "TOYOTA_RAV4_TSS2"
"LEXUS_IS" = "LEXUS_NX" "LEXUS_IS" = "LEXUS_NX"
"LEXUS_CTH" = "LEXUS_NX" "LEXUS_CTH" = "LEXUS_NX"

View File

@ -208,6 +208,7 @@ class CarState(CarStateBase):
self.pcm_follow_distance = cp.vl["PCM_CRUISE_2"]["PCM_FOLLOW_DISTANCE"] self.pcm_follow_distance = cp.vl["PCM_CRUISE_2"]["PCM_FOLLOW_DISTANCE"]
buttonEvents = [] buttonEvents = []
prev_distance_button = self.distance_button
if self.CP.carFingerprint in TSS2_CAR: if self.CP.carFingerprint in TSS2_CAR:
# lkas button is wired to the camera # lkas button is wired to the camera
prev_lkas_button = self.lkas_button prev_lkas_button = self.lkas_button
@ -218,9 +219,8 @@ class CarState(CarStateBase):
buttonEvents.extend(create_button_events(1, 0, {1: ButtonType.lkas}) + buttonEvents.extend(create_button_events(1, 0, {1: ButtonType.lkas}) +
create_button_events(0, 1, {1: ButtonType.lkas})) create_button_events(0, 1, {1: ButtonType.lkas}))
if self.CP.carFingerprint not in (RADAR_ACC_CAR | SECOC_CAR): if self.CP.carFingerprint not in RADAR_ACC_CAR:
# distance button is wired to the ACC module (camera or radar) # distance button is wired to the ACC module (camera or radar)
prev_distance_button = self.distance_button
self.distance_button = cp_acc.vl["ACC_CONTROL"]["DISTANCE"] self.distance_button = cp_acc.vl["ACC_CONTROL"]["DISTANCE"]
buttonEvents += create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise}) buttonEvents += create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})

View File

@ -1068,6 +1068,29 @@ FW_VERSIONS = {
b'\x028646F4210100\x00\x00\x00\x008646G3305000\x00\x00\x00\x00', b'\x028646F4210100\x00\x00\x00\x008646G3305000\x00\x00\x00\x00',
], ],
}, },
CAR.TOYOTA_WILDLANDER_PHEV: {
(Ecu.engine, 0x700, None): [
b'\x01896630R57001\x00\x00\x00\x00',
],
(Ecu.abs, 0x7b0, None): [
b'\x01F152642C4000\x00\x00\x00\x00',
],
(Ecu.eps, 0x7a1, None): [
b'\x018965B4221000\x00\x00\x00\x00',
],
(Ecu.fwdCamera, 0x750, 0x6d): [
b'\x028646F0R01000\x00\x00\x00\x008646G4202000\x00\x00\x00\x00',
],
(Ecu.fwdRadar, 0x750, 0xf): [
b'\x018821F3301400\x00\x00\x00\x00',
],
(Ecu.srs, 0x780, None): [
b'\x018917F0R18000\x00\x00\x00\x00'
],
(Ecu.hybrid, 0x7d2, None): [
b'\x02899830R24000\x00\x00\x00\x00899850R05000\x00\x00\x00\x00'
],
},
CAR.TOYOTA_RAV4_TSS2: { CAR.TOYOTA_RAV4_TSS2: {
(Ecu.engine, 0x700, None): [ (Ecu.engine, 0x700, None): [
b'\x01896630R58000\x00\x00\x00\x00', b'\x01896630R58000\x00\x00\x00\x00',

View File

@ -93,7 +93,7 @@ class CarInterface(CarInterfaceBase):
# https://engage.toyota.com/static/images/toyota_safety_sense/TSS_Applicability_Chart.pdf # https://engage.toyota.com/static/images/toyota_safety_sense/TSS_Applicability_Chart.pdf
stop_and_go = candidate != CAR.TOYOTA_AVALON stop_and_go = candidate != CAR.TOYOTA_AVALON
elif candidate in (CAR.TOYOTA_RAV4_TSS2, CAR.TOYOTA_RAV4_TSS2_2022, CAR.TOYOTA_RAV4_TSS2_2023, CAR.TOYOTA_RAV4_PRIME, CAR.TOYOTA_SIENNA_4TH_GEN): elif candidate in (CAR.TOYOTA_RAV4_TSS2, CAR.TOYOTA_RAV4_TSS2_2022, CAR.TOYOTA_RAV4_TSS2_2023, CAR.TOYOTA_RAV4_PRIME, CAR.TOYOTA_SIENNA_4TH_GEN, CAR.TOYOTA_WILDLANDER_PHEV):
ret.lateralTuning.init('pid') ret.lateralTuning.init('pid')
ret.lateralTuning.pid.kiBP = [0.0] ret.lateralTuning.pid.kiBP = [0.0]
ret.lateralTuning.pid.kpBP = [0.0] ret.lateralTuning.pid.kpBP = [0.0]
@ -169,9 +169,7 @@ class CarInterface(CarInterfaceBase):
ret.openpilotLongitudinalControl = ret.enableDsu or \ ret.openpilotLongitudinalControl = ret.enableDsu or \
candidate in (TSS2_CAR - RADAR_ACC_CAR) or \ candidate in (TSS2_CAR - RADAR_ACC_CAR) or \
bool(ret.flags & ToyotaFlags.DISABLE_RADAR.value) or \ bool(ret.flags & ToyotaFlags.DISABLE_RADAR.value)
sdsu_active or \
dsu_bypass
if dp_params & structs.DPFlags.ToyotaStockLon: if dp_params & structs.DPFlags.ToyotaStockLon:
ret.openpilotLongitudinalControl = False ret.openpilotLongitudinalControl = False

View File

@ -300,6 +300,10 @@ class CAR(Platforms):
[ToyotaCommunityCarDocs("Toyota RAV4 Prime 2021-23", min_enable_speed=MIN_ACC_SPEED)], [ToyotaCommunityCarDocs("Toyota RAV4 Prime 2021-23", min_enable_speed=MIN_ACC_SPEED)],
CarSpecs(mass=4372. * CV.LB_TO_KG, wheelbase=2.68, steerRatio=16.88, tireStiffnessFactor=0.5533), CarSpecs(mass=4372. * CV.LB_TO_KG, wheelbase=2.68, steerRatio=16.88, tireStiffnessFactor=0.5533),
) )
TOYOTA_WILDLANDER_PHEV = ToyotaSecOCPlatformConfig(
[ToyotaCarDocs("Toyota Wildlander PHEV 2021-23", min_enable_speed=MIN_ACC_SPEED)],
CarSpecs(mass=4155. * CV.LB_TO_KG, wheelbase=2.69, steerRatio=16.88, tireStiffnessFactor=0.5533),
)
TOYOTA_YARIS = ToyotaSecOCPlatformConfig( TOYOTA_YARIS = ToyotaSecOCPlatformConfig(
[ToyotaCommunityCarDocs("Toyota Yaris (Non-US only) 2020, 2023", min_enable_speed=MIN_ACC_SPEED)], [ToyotaCommunityCarDocs("Toyota Yaris (Non-US only) 2020, 2023", min_enable_speed=MIN_ACC_SPEED)],
CarSpecs(mass=1170, wheelbase=2.55, steerRatio=14.80, tireStiffnessFactor=0.5533), CarSpecs(mass=1170, wheelbase=2.55, steerRatio=14.80, tireStiffnessFactor=0.5533),

BIN
payload.bin Normal file

Binary file not shown.

Binary file not shown.

View File

@ -12,7 +12,11 @@ def dmonitoringd_thread():
pm = messaging.PubMaster(['driverMonitoringState']) pm = messaging.PubMaster(['driverMonitoringState'])
sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'selfdriveState', 'modelV2'], poll='driverStateV2') sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'selfdriveState', 'modelV2'], poll='driverStateV2')
DM = DriverMonitoring(rhd_saved=params.get_bool("IsRhdDetected"), always_on=params.get_bool("AlwaysOnDM")) DM = DriverMonitoring(
rhd_saved=params.get_bool("IsRhdDetected"),
always_on=params.get_bool("AlwaysOnDM"),
distraction_detection_level=int(params.get("DistractionDetectionLevel") or 1)
)
# 20Hz <- dmonitoringmodeld # 20Hz <- dmonitoringmodeld
while True: while True:
@ -22,8 +26,10 @@ def dmonitoringd_thread():
continue continue
valid = sm.all_checks() valid = sm.all_checks()
if valid: if DM.always_on and valid:
DM.run_step(sm) DM.run_step(sm)
## 设置分心率
DM.set_distract_level_params()
# publish # publish
dat = DM.get_state_packet(valid=valid) dat = DM.get_state_packet(valid=valid)
@ -32,6 +38,7 @@ def dmonitoringd_thread():
# load live always-on toggle # load live always-on toggle
if sm['driverStateV2'].frameId % 40 == 1: if sm['driverStateV2'].frameId % 40 == 1:
DM.always_on = params.get_bool("AlwaysOnDM") DM.always_on = params.get_bool("AlwaysOnDM")
DM.distraction_detection_level = int(params.get("DistractionDetectionLevel") or 1)
# save rhd virtual toggle every 5 mins # save rhd virtual toggle every 5 mins
if (sm['driverStateV2'].frameId % 6000 == 0 and if (sm['driverStateV2'].frameId % 6000 == 0 and

View File

@ -124,7 +124,7 @@ def face_orientation_from_net(angles_desc, pos_desc, rpy_calib):
class DriverMonitoring: class DriverMonitoring:
def __init__(self, rhd_saved=False, settings=None, always_on=False): def __init__(self, rhd_saved=False, settings=None, always_on=False, distraction_detection_level=None):
if settings is None: if settings is None:
settings = DRIVER_MONITOR_SETTINGS() settings = DRIVER_MONITOR_SETTINGS()
# init policy settings # init policy settings
@ -139,6 +139,7 @@ class DriverMonitoring:
self.ee1_calibrated = False self.ee1_calibrated = False
self.always_on = always_on self.always_on = always_on
self.distraction_detection_level = distraction_detection_level
self.distracted_types = [] self.distracted_types = []
self.driver_distracted = False self.driver_distracted = False
self.driver_distraction_filter = FirstOrderFilter(0., self.settings._DISTRACTED_FILTER_TS, self.settings._DT_DMON) self.driver_distraction_filter = FirstOrderFilter(0., self.settings._DISTRACTED_FILTER_TS, self.settings._DT_DMON)
@ -415,3 +416,21 @@ class DriverMonitoring:
wrong_gear=sm['carState'].gearShifter in [car.CarState.GearShifter.reverse, car.CarState.GearShifter.park], wrong_gear=sm['carState'].gearShifter in [car.CarState.GearShifter.reverse, car.CarState.GearShifter.park],
car_speed=sm['carState'].vEgo car_speed=sm['carState'].vEgo
) )
def set_distract_level_params(self):
if self.distraction_detection_level == 0:
self.settings._DISTRACTED_TIME = 8.0
self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL = 5.0
self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 4.0
elif self.distraction_detection_level == 1:
self.settings._DISTRACTED_TIME = 11.0
self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL = 8.
self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 6.
elif self.distraction_detection_level == 2:
self.settings._DISTRACTED_TIME = 20.0
self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL = 10.0
self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 7.0
else:
self.settings._DISTRACTED_TIME = float('inf')
self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL = float('inf')
self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = float('inf')

View File

@ -148,7 +148,7 @@ EmptyAlert = Alert("" , "", AlertStatus.normal, AlertSize.none, Priority.LOWEST,
class NoEntryAlert(Alert): class NoEntryAlert(Alert):
def __init__(self, alert_text_2: str, def __init__(self, alert_text_2: str,
alert_text_1: str = "openpilot Unavailable", alert_text_1: str = "openpilot 不可用",
visual_alert: car.CarControl.HUDControl.VisualAlert=VisualAlert.none): visual_alert: car.CarControl.HUDControl.VisualAlert=VisualAlert.none):
super().__init__(alert_text_1, alert_text_2, AlertStatus.normal, super().__init__(alert_text_1, alert_text_2, AlertStatus.normal,
AlertSize.mid, Priority.LOW, visual_alert, AlertSize.mid, Priority.LOW, visual_alert,
@ -157,7 +157,7 @@ class NoEntryAlert(Alert):
class SoftDisableAlert(Alert): class SoftDisableAlert(Alert):
def __init__(self, alert_text_2: str): def __init__(self, alert_text_2: str):
super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2, super().__init__("立即接管控制", alert_text_2,
AlertStatus.userPrompt, AlertSize.full, AlertStatus.userPrompt, AlertSize.full,
Priority.MID, VisualAlert.steerRequired, Priority.MID, VisualAlert.steerRequired,
AudibleAlert.warningSoft, 2.), AudibleAlert.warningSoft, 2.),
@ -167,12 +167,12 @@ class SoftDisableAlert(Alert):
class UserSoftDisableAlert(SoftDisableAlert): class UserSoftDisableAlert(SoftDisableAlert):
def __init__(self, alert_text_2: str): def __init__(self, alert_text_2: str):
super().__init__(alert_text_2), super().__init__(alert_text_2),
self.alert_text_1 = "openpilot will disengage" self.alert_text_1 = "openpilot 将解除控制"
class ImmediateDisableAlert(Alert): class ImmediateDisableAlert(Alert):
def __init__(self, alert_text_2: str): def __init__(self, alert_text_2: str):
super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2, super().__init__("立即接管控制", alert_text_2,
AlertStatus.critical, AlertSize.full, AlertStatus.critical, AlertSize.full,
Priority.HIGHEST, VisualAlert.steerRequired, Priority.HIGHEST, VisualAlert.steerRequired,
AudibleAlert.warningImmediate, 4.), AudibleAlert.warningImmediate, 4.),
@ -194,7 +194,7 @@ class NormalPermanentAlert(Alert):
class StartupAlert(Alert): class StartupAlert(Alert):
def __init__(self, alert_text_1: str, alert_text_2: str = "Always keep hands on wheel and eyes on road", alert_status=AlertStatus.normal): def __init__(self, alert_text_1: str, alert_text_2: str = "请始终将手放在方向盘上,眼睛注视道路", alert_status=AlertStatus.normal):
super().__init__(alert_text_1, alert_text_2, super().__init__(alert_text_1, alert_text_2,
alert_status, AlertSize.mid, alert_status, AlertSize.mid,
Priority.LOWER, VisualAlert.none, AudibleAlert.none, 5.), Priority.LOWER, VisualAlert.none, AudibleAlert.none, 5.),
@ -232,25 +232,25 @@ def startup_master_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubM
if "REPLAY" in os.environ: if "REPLAY" in os.environ:
branch = "replay" branch = "replay"
return StartupAlert("WARNING: This branch is not tested", branch, alert_status=AlertStatus.userPrompt) return StartupAlert("警告:此分支未经测试", branch, alert_status=AlertStatus.userPrompt)
def below_engage_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def below_engage_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
return NoEntryAlert(f"Drive above {get_display_speed(CP.minEnableSpeed, metric)} to engage") return NoEntryAlert(f"请将时速提高至 {get_display_speed(CP.minEnableSpeed, metric)} 来启用")
def below_steer_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def below_steer_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
return Alert( return Alert(
f"Steer Assist Unavailable Below {get_display_speed(CP.minSteerSpeed, metric)}", f"转向在 {get_display_speed(CP.minSteerSpeed, metric)} 以下不可用",
"", "",
AlertStatus.userPrompt, AlertSize.small, AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.4) Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.4)
def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
first_word = 'Recalibration' if sm['liveCalibration'].calStatus == log.LiveCalibrationData.Status.recalibrating else 'Calibration' first_word = '重新校准' if sm['liveCalibration'].calStatus == log.LiveCalibrationData.Status.recalibrating else '校准'
return Alert( return Alert(
f"{first_word} in Progress: {sm['liveCalibration'].calPerc:.0f}%", f"{first_word}进行中:{sm['liveCalibration'].calPerc:.0f}%",
f"Drive Above {get_display_speed(MIN_SPEED_FILTER, metric)}", f"请将时速提高至 {get_display_speed(MIN_SPEED_FILTER, metric)} 进行校准",
AlertStatus.normal, AlertSize.mid, AlertStatus.normal, AlertSize.mid,
Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2) Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2)
@ -258,8 +258,8 @@ def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messag
def audio_feedback_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: 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) duration = FEEDBACK_MAX_DURATION - ((sm['audioFeedback'].blockNum + 1) * SAMPLE_BUFFER / SAMPLE_RATE)
return NormalPermanentAlert( return NormalPermanentAlert(
"Recording Audio Feedback", "正在录制音频反馈",
f"{round(duration)} second{'s' if round(duration) != 1 else ''} remaining. Press again to save early.", f"剩余 {round(duration)} 秒。再次按下可提前保存。",
priority=Priority.LOW) priority=Priority.LOW)
@ -267,57 +267,57 @@ def audio_feedback_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubM
def out_of_space_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def out_of_space_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
full_perc = round(100. - sm['deviceState'].freeSpacePercent) full_perc = round(100. - sm['deviceState'].freeSpacePercent)
return NormalPermanentAlert("Out of Storage", f"{full_perc}% full") return NormalPermanentAlert("存储空间不足", f"已用 {full_perc}%")
def posenet_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def posenet_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
mdl = sm['modelV2'].velocity.x[0] if len(sm['modelV2'].velocity.x) else math.nan mdl = sm['modelV2'].velocity.x[0] if len(sm['modelV2'].velocity.x) else math.nan
err = CS.vEgo - mdl err = CS.vEgo - mdl
msg = f"Speed Error: {err:.1f} m/s" msg = f"速度误差: {err:.1f} 米/秒"
return NoEntryAlert(msg, alert_text_1="Posenet Speed Invalid") return NoEntryAlert(msg, alert_text_1="Posenet速度无效")
def process_not_running_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def process_not_running_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
not_running = [p.name for p in sm['managerState'].processes if not p.running and p.shouldBeRunning] not_running = [p.name for p in sm['managerState'].processes if not p.running and p.shouldBeRunning]
msg = ', '.join(not_running) msg = ', '.join(not_running)
return NoEntryAlert(msg, alert_text_1="Process Not Running") return NoEntryAlert(msg, alert_text_1="进程未运行")
def comm_issue_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def comm_issue_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
bs = [s for s in sm.data.keys() if not sm.all_checks([s, ])] bs = [s for s in sm.data.keys() if not sm.all_checks([s, ])]
msg = ', '.join(bs[:4]) # can't fit too many on one line msg = ', '.join(bs[:4]) # can't fit too many on one line
return NoEntryAlert(msg, alert_text_1="Communication Issue Between Processes") return NoEntryAlert(msg, alert_text_1="进程间通信问题")
def camera_malfunction_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def camera_malfunction_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
all_cams = ('roadCameraState', 'driverCameraState', 'wideRoadCameraState') all_cams = ('roadCameraState', 'driverCameraState', 'wideRoadCameraState')
bad_cams = [s.replace('State', '') for s in all_cams if s in sm.data.keys() and not sm.all_checks([s, ])] bad_cams = [s.replace('State', '') for s in all_cams if s in sm.data.keys() and not sm.all_checks([s, ])]
return NormalPermanentAlert("Camera Malfunction", ', '.join(bad_cams)) return NormalPermanentAlert("摄像头故障", ', '.join(bad_cams))
def calibration_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def calibration_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
rpy = sm['liveCalibration'].rpyCalib rpy = sm['liveCalibration'].rpyCalib
yaw = math.degrees(rpy[2] if len(rpy) == 3 else math.nan) yaw = math.degrees(rpy[2] if len(rpy) == 3 else math.nan)
pitch = math.degrees(rpy[1] if len(rpy) == 3 else math.nan) pitch = math.degrees(rpy[1] if len(rpy) == 3 else math.nan)
angles = f"Remount Device (Pitch: {pitch:.1f}°, Yaw: {yaw:.1f}°)" angles = f"请重新安装设备 (俯仰角: {pitch:.1f}°, 偏航角: {yaw:.1f}°)"
return NormalPermanentAlert("Calibration Invalid", angles) return NormalPermanentAlert("校准无效", angles)
def paramsd_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: 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: if not sm['liveParameters'].angleOffsetValid:
angle_offset_deg = sm['liveParameters'].angleOffsetDeg angle_offset_deg = sm['liveParameters'].angleOffsetDeg
title = "Steering misalignment detected" title = "转向系统未对准"
text = f"Angle offset too high (Offset: {angle_offset_deg:.1f}°)" text = f"角度偏移过大 (偏移量: {angle_offset_deg:.1f}°)"
elif not sm['liveParameters'].steerRatioValid: elif not sm['liveParameters'].steerRatioValid:
steer_ratio = sm['liveParameters'].steerRatio steer_ratio = sm['liveParameters'].steerRatio
title = "Steer ratio mismatch" title = "转向传动比不匹配"
text = f"Steering rack geometry may be off (Ratio: {steer_ratio:.1f})" text = f"转向齿条几何可能不正确 (传动比: {steer_ratio:.1f})"
elif not sm['liveParameters'].stiffnessFactorValid: elif not sm['liveParameters'].stiffnessFactorValid:
stiffness_factor = sm['liveParameters'].stiffnessFactor stiffness_factor = sm['liveParameters'].stiffnessFactor
title = "Abnormal tire stiffness" title = "轮胎刚度异常"
text = f"Check tires, pressure, or alignment (Factor: {stiffness_factor:.1f})" text = f"请检查轮胎、胎压或定位 (系数: {stiffness_factor:.1f})"
else: else:
return NoEntryAlert("paramsd Temporary Error") return NoEntryAlert("paramsd 临时错误")
return NoEntryAlert(alert_text_1=title, alert_text_2=text) return NoEntryAlert(alert_text_1=title, alert_text_2=text)
@ -325,34 +325,34 @@ def overheat_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster,
cpu = max(sm['deviceState'].cpuTempC, default=0.) cpu = max(sm['deviceState'].cpuTempC, default=0.)
gpu = max(sm['deviceState'].gpuTempC, default=0.) gpu = max(sm['deviceState'].gpuTempC, default=0.)
temp = max((cpu, gpu, sm['deviceState'].memoryTempC)) temp = max((cpu, gpu, sm['deviceState'].memoryTempC))
return NormalPermanentAlert("System Overheated", f"{temp:.0f} °C") return NormalPermanentAlert("系统过热", f"{temp:.0f} °C")
def low_memory_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def low_memory_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
return NormalPermanentAlert("Low Memory", f"{sm['deviceState'].memoryUsagePercent}% used") return NormalPermanentAlert("内存不足", f"已用 {sm['deviceState'].memoryUsagePercent}%")
def high_cpu_usage_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def high_cpu_usage_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
x = max(sm['deviceState'].cpuUsagePercent, default=0.) x = max(sm['deviceState'].cpuUsagePercent, default=0.)
return NormalPermanentAlert("High CPU Usage", f"{x}% used") return NormalPermanentAlert("CPU使用率过高", f"已用 {x}%")
def modeld_lagging_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def modeld_lagging_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
return NormalPermanentAlert("Driving Model Lagging", f"{sm['modelV2'].frameDropPerc:.1f}% frames dropped") return NormalPermanentAlert("驾驶模型滞后", f"已丢帧 {sm['modelV2'].frameDropPerc:.1f}%")
def wrong_car_mode_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def wrong_car_mode_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
text = "Enable Adaptive Cruise to Engage" text = "启用自适应巡航以接合"
if CP.brand == "honda": if CP.brand == "honda":
text = "Enable Main Switch to Engage" text = "启用主开关以接合"
return NoEntryAlert(text) return NoEntryAlert(text)
def joystick_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def joystick_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
gb = sm['carControl'].actuators.accel / 4. gb = sm['carControl'].actuators.accel / 4.
steer = sm['carControl'].actuators.torque steer = sm['carControl'].actuators.torque
vals = f"Gas: {round(gb * 100.)}%, Steer: {round(steer * 100.)}%" vals = f"油门: {round(gb * 100.)}%, 转向: {round(steer * 100.)}%"
return NormalPermanentAlert("Joystick Mode", vals) return NormalPermanentAlert("操纵杆模式", vals)
def longitudinal_maneuver_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def longitudinal_maneuver_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
@ -367,18 +367,25 @@ def longitudinal_maneuver_alert(CP: car.CarParams, CS: car.CarState, sm: messagi
def personality_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: def personality_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
personality = str(personality).title() personality = str(personality).title()
return NormalPermanentAlert(f"Driving Personality: {personality}", duration=1.5) personality_en = ""
if personality == "Aggressive":
personality_en = "激进"
elif personality == "Standard":
personality_en = "标准"
elif personality == "Relaxed":
personality_en = "舒适"
return NormalPermanentAlert(f"驾驶风格: {personality_en}", 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: 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" text = "请切换原厂LKAS状态以启用"
if CP.brand == "tesla": if CP.brand == "tesla":
text = "Switch to Traffic-Aware Cruise Control to engage" text = "请切换到交通感知巡航控制以启用"
elif CP.brand == "mazda": elif CP.brand == "mazda":
text = "Enable your car's LKAS to engage" text = "请启用您的车辆LKAS以启用"
elif CP.brand == "nissan": elif CP.brand == "nissan":
text = "Disable your car's stock LKAS to engage" text = "请禁用您的车辆原厂LKAS以启用"
return NormalPermanentAlert("Invalid LKAS setting", text) return NormalPermanentAlert("无效的LKAS设置", text)
@ -392,21 +399,21 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventName.joystickDebug: { EventName.joystickDebug: {
ET.WARNING: joystick_alert, ET.WARNING: joystick_alert,
ET.PERMANENT: NormalPermanentAlert("Joystick Mode"), ET.PERMANENT: NormalPermanentAlert("操纵杆模式"),
}, },
EventName.longitudinalManeuver: { EventName.longitudinalManeuver: {
ET.WARNING: longitudinal_maneuver_alert, ET.WARNING: longitudinal_maneuver_alert,
ET.PERMANENT: NormalPermanentAlert("Longitudinal Maneuver Mode", ET.PERMANENT: NormalPermanentAlert("纵向操作模式",
"Ensure road ahead is clear"), "确保前方道路畅通"),
}, },
EventName.selfdriveInitializing: { EventName.selfdriveInitializing: {
ET.NO_ENTRY: NoEntryAlert("System Initializing"), ET.NO_ENTRY: NoEntryAlert("系统初始化中"),
}, },
EventName.startup: { EventName.startup: {
ET.PERMANENT: StartupAlert("Be ready to take over at any time") ET.PERMANENT: StartupAlert("请随时准备接管您的车辆控制权")
}, },
EventName.startupMaster: { EventName.startupMaster: {
@ -414,28 +421,28 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
}, },
EventName.startupNoControl: { EventName.startupNoControl: {
ET.PERMANENT: StartupAlert("Dashcam mode"), ET.PERMANENT: StartupAlert("仅行车记录仪模式"),
ET.NO_ENTRY: NoEntryAlert("Dashcam mode"), ET.NO_ENTRY: NoEntryAlert("仅行车记录仪模式"),
}, },
EventName.startupNoCar: { EventName.startupNoCar: {
ET.PERMANENT: StartupAlert("Dashcam mode for unsupported car"), ET.PERMANENT: StartupAlert("不支持车辆的行车记录仪模式"),
}, },
EventName.startupNoSecOcKey: { EventName.startupNoSecOcKey: {
ET.PERMANENT: NormalPermanentAlert("Dashcam Mode", ET.PERMANENT: NormalPermanentAlert("仅行车记录仪模式",
"Security Key Not Available", "安全密钥不可用",
priority=Priority.HIGH), priority=Priority.HIGH),
}, },
EventName.dashcamMode: { EventName.dashcamMode: {
ET.PERMANENT: NormalPermanentAlert("Dashcam Mode", ET.PERMANENT: NormalPermanentAlert("行车记录仪模式",
priority=Priority.LOWEST), priority=Priority.LOWEST),
}, },
EventName.invalidLkasSetting: { EventName.invalidLkasSetting: {
ET.PERMANENT: invalid_lkas_setting_alert, ET.PERMANENT: invalid_lkas_setting_alert,
ET.NO_ENTRY: NoEntryAlert("Invalid LKAS setting"), ET.NO_ENTRY: NoEntryAlert("车道保持辅助系统LKAS设置无效"),
}, },
EventName.cruiseMismatch: { EventName.cruiseMismatch: {
@ -446,40 +453,40 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
# read-only mode. This can be solved by adding your fingerprint. # read-only mode. This can be solved by adding your fingerprint.
# See https://github.com/commaai/openpilot/wiki/Fingerprinting for more information # See https://github.com/commaai/openpilot/wiki/Fingerprinting for more information
EventName.carUnrecognized: { EventName.carUnrecognized: {
ET.PERMANENT: NormalPermanentAlert("Dashcam Mode", ET.PERMANENT: NormalPermanentAlert("行车记录仪模式",
"Car Unrecognized", "车辆未识别",
priority=Priority.LOWEST), priority=Priority.LOWEST),
}, },
EventName.aeb: { EventName.aeb: {
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"BRAKE!", "刹车!",
"Emergency Braking: Risk of Collision", "紧急制动:可能发生碰撞",
AlertStatus.critical, AlertSize.full, AlertStatus.critical, AlertSize.full,
Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.none, 2.), Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.none, 2.),
ET.NO_ENTRY: NoEntryAlert("AEB: Risk of Collision"), ET.NO_ENTRY: NoEntryAlert("AEB:可能发生碰撞"),
}, },
EventName.stockAeb: { EventName.stockAeb: {
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"BRAKE!", "刹车!",
"Stock AEB: Risk of Collision", "原厂AEB可能发生碰撞",
AlertStatus.critical, AlertSize.full, AlertStatus.critical, AlertSize.full,
Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.none, 2.), Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.none, 2.),
ET.NO_ENTRY: NoEntryAlert("Stock AEB: Risk of Collision"), ET.NO_ENTRY: NoEntryAlert("原厂AEB可能发生碰撞"),
}, },
EventName.fcw: { EventName.fcw: {
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"BRAKE!", "刹车!",
"Risk of Collision", "可能发生碰撞",
AlertStatus.critical, AlertSize.full, AlertStatus.critical, AlertSize.full,
Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.warningSoft, 2.), Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.warningSoft, 2.),
}, },
EventName.ldw: { EventName.ldw: {
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"Lane Departure Detected", "监测偏离车道",
"", "",
AlertStatus.userPrompt, AlertSize.small, AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.ldw, AudibleAlert.prompt, 3.), Priority.LOW, VisualAlert.ldw, AudibleAlert.prompt, 3.),
@ -489,7 +496,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventName.steerTempUnavailableSilent: { EventName.steerTempUnavailableSilent: {
ET.WARNING: Alert( ET.WARNING: Alert(
"Steering Assist Temporarily Unavailable", "转向暂时不可用",
"", "",
AlertStatus.userPrompt, AlertSize.small, AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.prompt, 1.8), Priority.LOW, VisualAlert.steerRequired, AudibleAlert.prompt, 1.8),
@ -497,7 +504,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventName.preDriverDistracted: { EventName.preDriverDistracted: {
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"Pay Attention", "请注意",
"", "",
AlertStatus.normal, AlertSize.small, AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), Priority.LOW, VisualAlert.none, AudibleAlert.none, .1),
@ -505,23 +512,23 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventName.promptDriverDistracted: { EventName.promptDriverDistracted: {
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"Pay Attention", "请注意",
"Driver Distracted", "驾驶员分心",
AlertStatus.userPrompt, AlertSize.mid, AlertStatus.userPrompt, AlertSize.mid,
Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, .1), Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, .1),
}, },
EventName.driverDistracted: { EventName.driverDistracted: {
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"DISENGAGE IMMEDIATELY", "立即解除控制",
"Driver Distracted", "驾驶员分心",
AlertStatus.critical, AlertSize.full, AlertStatus.critical, AlertSize.full,
Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.warningImmediate, .1), Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.warningImmediate, .1),
}, },
EventName.preDriverUnresponsive: { EventName.preDriverUnresponsive: {
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"Touch Steering Wheel: No Face Detected", "触摸方向盘:未检测到面部",
"", "",
AlertStatus.normal, AlertSize.small, AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.none, .1), Priority.LOW, VisualAlert.steerRequired, AudibleAlert.none, .1),
@ -529,31 +536,31 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventName.promptDriverUnresponsive: { EventName.promptDriverUnresponsive: {
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"Touch Steering Wheel", "触摸方向盘",
"Driver Unresponsive", "驾驶员无响应",
AlertStatus.userPrompt, AlertSize.mid, AlertStatus.userPrompt, AlertSize.mid,
Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, .1), Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, .1),
}, },
EventName.driverUnresponsive: { EventName.driverUnresponsive: {
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"DISENGAGE IMMEDIATELY", "立即解除控制",
"Driver Unresponsive", "驾驶员无响应",
AlertStatus.critical, AlertSize.full, AlertStatus.critical, AlertSize.full,
Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.warningImmediate, .1), Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.warningImmediate, .1),
}, },
EventName.manualRestart: { EventName.manualRestart: {
ET.WARNING: Alert( ET.WARNING: Alert(
"TAKE CONTROL", "接管控制",
"Resume Driving Manually", "请手动继续驾驶",
AlertStatus.userPrompt, AlertSize.mid, AlertStatus.userPrompt, AlertSize.mid,
Priority.LOW, VisualAlert.none, AudibleAlert.none, .2), Priority.LOW, VisualAlert.none, AudibleAlert.none, .2),
}, },
EventName.resumeRequired: { EventName.resumeRequired: {
ET.WARNING: Alert( ET.WARNING: Alert(
"Press Resume to Exit Standstill", "按恢复键以解除停止状态",
"", "",
AlertStatus.userPrompt, AlertSize.small, AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, .2), Priority.LOW, VisualAlert.none, AudibleAlert.none, .2),
@ -565,7 +572,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventName.preLaneChangeLeft: { EventName.preLaneChangeLeft: {
ET.WARNING: Alert( ET.WARNING: Alert(
"Steer Left to Start Lane Change Once Safe", "请确认安全后进行左转变道",
"", "",
AlertStatus.normal, AlertSize.small, AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), Priority.LOW, VisualAlert.none, AudibleAlert.none, .1),
@ -573,7 +580,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventName.preLaneChangeRight: { EventName.preLaneChangeRight: {
ET.WARNING: Alert( ET.WARNING: Alert(
"Steer Right to Start Lane Change Once Safe", "请确认安全后进行右转变道",
"", "",
AlertStatus.normal, AlertSize.small, AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), Priority.LOW, VisualAlert.none, AudibleAlert.none, .1),
@ -581,7 +588,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventName.laneChangeBlocked: { EventName.laneChangeBlocked: {
ET.WARNING: Alert( ET.WARNING: Alert(
"Car Detected in Blindspot", "盲点检测到车辆",
"", "",
AlertStatus.userPrompt, AlertSize.small, AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, .1), Priority.LOW, VisualAlert.none, AudibleAlert.prompt, .1),
@ -589,7 +596,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventName.laneChange: { EventName.laneChange: {
ET.WARNING: Alert( ET.WARNING: Alert(
"Changing Lanes", "正在变道",
"", "",
AlertStatus.normal, AlertSize.small, AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), Priority.LOW, VisualAlert.none, AudibleAlert.none, .1),
@ -597,41 +604,41 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventName.steerSaturated: { EventName.steerSaturated: {
ET.WARNING: Alert( ET.WARNING: Alert(
"Take Control", "请接管控制",
"Turn Exceeds Steering Limit", "转向超出限制",
AlertStatus.userPrompt, AlertSize.mid, AlertStatus.userPrompt, AlertSize.mid,
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 2.), Priority.LOW, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 2.),
}, },
# Thrown when the fan is driven at >50% but is not rotating # Thrown when the fan is driven at >50% but is not rotating
EventName.fanMalfunction: { EventName.fanMalfunction: {
ET.PERMANENT: NormalPermanentAlert("Fan Malfunction", "Likely Hardware Issue"), ET.PERMANENT: NormalPermanentAlert("风扇故障", "可能是硬件问题"),
}, },
# Camera is not outputting frames # Camera is not outputting frames
EventName.cameraMalfunction: { EventName.cameraMalfunction: {
ET.PERMANENT: camera_malfunction_alert, ET.PERMANENT: camera_malfunction_alert,
ET.SOFT_DISABLE: soft_disable_alert("Camera Malfunction"), ET.SOFT_DISABLE: soft_disable_alert("摄像头故障"),
ET.NO_ENTRY: NoEntryAlert("Camera Malfunction: Reboot Your Device"), ET.NO_ENTRY: NoEntryAlert("摄像头故障:请重启设备"),
}, },
# Camera framerate too low # Camera framerate too low
EventName.cameraFrameRate: { EventName.cameraFrameRate: {
ET.PERMANENT: NormalPermanentAlert("Camera Frame Rate Low", "Reboot your Device"), ET.PERMANENT: NormalPermanentAlert("摄像头帧率低", "请重启设备"),
ET.SOFT_DISABLE: soft_disable_alert("Camera Frame Rate Low"), ET.SOFT_DISABLE: soft_disable_alert("摄像头帧率低"),
ET.NO_ENTRY: NoEntryAlert("Camera Frame Rate Low: Reboot Your Device"), ET.NO_ENTRY: NoEntryAlert("摄像头帧率低:请重启设备"),
}, },
# Unused # Unused
EventName.locationdTemporaryError: { EventName.locationdTemporaryError: {
ET.NO_ENTRY: NoEntryAlert("locationd Temporary Error"), ET.NO_ENTRY: NoEntryAlert("locationd临时错误"),
ET.SOFT_DISABLE: soft_disable_alert("locationd Temporary Error"), ET.SOFT_DISABLE: soft_disable_alert("locationd临时错误"),
}, },
EventName.locationdPermanentError: { EventName.locationdPermanentError: {
ET.NO_ENTRY: NoEntryAlert("locationd Permanent Error"), ET.NO_ENTRY: NoEntryAlert("locationd永久错误"),
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("locationd Permanent Error"), ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("locationd永久错误"),
ET.PERMANENT: NormalPermanentAlert("locationd Permanent Error"), ET.PERMANENT: NormalPermanentAlert("locationd永久错误"),
}, },
# openpilot tries to learn certain parameters about your car by observing # openpilot tries to learn certain parameters about your car by observing
@ -644,13 +651,13 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
# bad alignment or bad sensor data. If this happens consistently consider creating an issue on GitHub # bad alignment or bad sensor data. If this happens consistently consider creating an issue on GitHub
EventName.paramsdTemporaryError: { EventName.paramsdTemporaryError: {
ET.NO_ENTRY: paramsd_invalid_alert, ET.NO_ENTRY: paramsd_invalid_alert,
ET.SOFT_DISABLE: soft_disable_alert("paramsd Temporary Error"), ET.SOFT_DISABLE: soft_disable_alert("paramsd 临时错误"),
}, },
EventName.paramsdPermanentError: { EventName.paramsdPermanentError: {
ET.NO_ENTRY: NoEntryAlert("paramsd Permanent Error"), ET.NO_ENTRY: NoEntryAlert("paramsd永久错误"),
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("paramsd Permanent Error"), ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("paramsd永久错误"),
ET.PERMANENT: NormalPermanentAlert("paramsd Permanent Error"), ET.PERMANENT: NormalPermanentAlert("paramsd永久错误"),
}, },
# ********** events that affect controls state transitions ********** # ********** events that affect controls state transitions **********
@ -669,12 +676,12 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventName.buttonCancel: { EventName.buttonCancel: {
ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage),
ET.NO_ENTRY: NoEntryAlert("Cancel Pressed"), ET.NO_ENTRY: NoEntryAlert("取消按钮被按下"),
}, },
EventName.brakeHold: { EventName.brakeHold: {
ET.WARNING: Alert( ET.WARNING: Alert(
"Press Resume to Exit Brake Hold", "按恢复键以解除制动保持",
"", "",
AlertStatus.userPrompt, AlertSize.small, AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, .2), Priority.LOW, VisualAlert.none, AudibleAlert.none, .2),
@ -682,23 +689,23 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventName.parkBrake: { EventName.parkBrake: {
ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage),
ET.NO_ENTRY: NoEntryAlert("Parking Brake Engaged"), ET.NO_ENTRY: NoEntryAlert("停车制动已启用"),
}, },
EventName.pedalPressed: { EventName.pedalPressed: {
ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage),
ET.NO_ENTRY: NoEntryAlert("Pedal Pressed", ET.NO_ENTRY: NoEntryAlert("踏板被按下",
visual_alert=VisualAlert.brakePressed), visual_alert=VisualAlert.brakePressed),
}, },
EventName.steerDisengage: { EventName.steerDisengage: {
ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage),
ET.NO_ENTRY: NoEntryAlert("Steering Pressed"), ET.NO_ENTRY: NoEntryAlert("方向盘被转动"),
}, },
EventName.preEnableStandstill: { EventName.preEnableStandstill: {
ET.PRE_ENABLE: Alert( ET.PRE_ENABLE: Alert(
"Release Brake to Engage", "释放制动以启用",
"", "",
AlertStatus.normal, AlertSize.small, AlertStatus.normal, AlertSize.small,
Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .1, creation_delay=1.), Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .1, creation_delay=1.),
@ -726,27 +733,27 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
}, },
EventName.resumeBlocked: { EventName.resumeBlocked: {
ET.NO_ENTRY: NoEntryAlert("Press Set to Engage"), ET.NO_ENTRY: NoEntryAlert("请按设定键以启用"),
}, },
EventName.wrongCruiseMode: { EventName.wrongCruiseMode: {
ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage),
ET.NO_ENTRY: NoEntryAlert("Adaptive Cruise Disabled"), ET.NO_ENTRY: NoEntryAlert("自适应巡航已禁用"),
}, },
EventName.steerTempUnavailable: { EventName.steerTempUnavailable: {
ET.SOFT_DISABLE: soft_disable_alert("Steering Assist Temporarily Unavailable"), ET.SOFT_DISABLE: soft_disable_alert("转向暂时不可用"),
ET.NO_ENTRY: NoEntryAlert("Steering Temporarily Unavailable"), ET.NO_ENTRY: NoEntryAlert("转向暂时不可用"),
}, },
EventName.steerTimeLimit: { EventName.steerTimeLimit: {
ET.SOFT_DISABLE: soft_disable_alert("Vehicle Steering Time Limit"), ET.SOFT_DISABLE: soft_disable_alert("车辆转向时间限制"),
ET.NO_ENTRY: NoEntryAlert("Vehicle Steering Time Limit"), ET.NO_ENTRY: NoEntryAlert("车辆转向时间限制"),
}, },
EventName.outOfSpace: { EventName.outOfSpace: {
ET.PERMANENT: out_of_space_alert, ET.PERMANENT: out_of_space_alert,
ET.NO_ENTRY: NoEntryAlert("Out of Storage"), ET.NO_ENTRY: NoEntryAlert("出库"),
}, },
EventName.belowEngageSpeed: { EventName.belowEngageSpeed: {
@ -755,35 +762,35 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventName.sensorDataInvalid: { EventName.sensorDataInvalid: {
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"Sensor Data Invalid", "传感器数据无效",
"Possible Hardware Issue", "可能是硬件问题",
AlertStatus.normal, AlertSize.mid, AlertStatus.normal, AlertSize.mid,
Priority.LOWER, VisualAlert.none, AudibleAlert.none, .2, creation_delay=1.), Priority.LOWER, VisualAlert.none, AudibleAlert.none, .2, creation_delay=1.),
ET.NO_ENTRY: NoEntryAlert("Sensor Data Invalid"), ET.NO_ENTRY: NoEntryAlert("传感器数据无效"),
ET.SOFT_DISABLE: soft_disable_alert("Sensor Data Invalid"), ET.SOFT_DISABLE: soft_disable_alert("传感器数据无效"),
}, },
EventName.noGps: { EventName.noGps: {
}, },
EventName.tooDistracted: { EventName.tooDistracted: {
ET.NO_ENTRY: NoEntryAlert("Distraction Level Too High"), ET.NO_ENTRY: NoEntryAlert("注意力分散程度过高"),
}, },
EventName.excessiveActuation: { EventName.excessiveActuation: {
ET.SOFT_DISABLE: soft_disable_alert("Excessive Actuation"), ET.SOFT_DISABLE: soft_disable_alert("过度操作"),
ET.NO_ENTRY: NoEntryAlert("Excessive Actuation"), ET.NO_ENTRY: NoEntryAlert("过度操作"),
}, },
EventName.overheat: { EventName.overheat: {
ET.PERMANENT: overheat_alert, ET.PERMANENT: overheat_alert,
ET.SOFT_DISABLE: soft_disable_alert("System Overheated"), ET.SOFT_DISABLE: soft_disable_alert("系统过热"),
ET.NO_ENTRY: NoEntryAlert("System Overheated"), ET.NO_ENTRY: NoEntryAlert("系统过热"),
}, },
EventName.wrongGear: { EventName.wrongGear: {
ET.SOFT_DISABLE: user_soft_disable_alert("Gear not D"), ET.SOFT_DISABLE: user_soft_disable_alert("挡位不在D挡"),
ET.NO_ENTRY: NoEntryAlert("Gear not D"), ET.NO_ENTRY: NoEntryAlert("挡位不在D挡"),
}, },
# This alert is thrown when the calibration angles are outside of the acceptable range. # This alert is thrown when the calibration angles are outside of the acceptable range.
@ -793,40 +800,40 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
# See https://comma.ai/setup for more information # See https://comma.ai/setup for more information
EventName.calibrationInvalid: { EventName.calibrationInvalid: {
ET.PERMANENT: calibration_invalid_alert, ET.PERMANENT: calibration_invalid_alert,
ET.SOFT_DISABLE: soft_disable_alert("Calibration Invalid: Remount Device & Recalibrate"), ET.SOFT_DISABLE: soft_disable_alert("校准无效:重新安装设备并重新校准"),
ET.NO_ENTRY: NoEntryAlert("Calibration Invalid: Remount Device & Recalibrate"), ET.NO_ENTRY: NoEntryAlert("校准无效:重新安装设备并重新校准"),
}, },
EventName.calibrationIncomplete: { EventName.calibrationIncomplete: {
ET.PERMANENT: calibration_incomplete_alert, ET.PERMANENT: calibration_incomplete_alert,
ET.SOFT_DISABLE: soft_disable_alert("Calibration Incomplete"), ET.SOFT_DISABLE: soft_disable_alert("校准未完成"),
ET.NO_ENTRY: NoEntryAlert("Calibration in Progress"), ET.NO_ENTRY: NoEntryAlert("校准进行中"),
}, },
EventName.calibrationRecalibrating: { EventName.calibrationRecalibrating: {
ET.PERMANENT: calibration_incomplete_alert, ET.PERMANENT: calibration_incomplete_alert,
ET.SOFT_DISABLE: soft_disable_alert("Device Remount Detected: Recalibrating"), ET.SOFT_DISABLE: soft_disable_alert("设备重新安装检测到:重新校准中"),
ET.NO_ENTRY: NoEntryAlert("Remount Detected: Recalibrating"), ET.NO_ENTRY: NoEntryAlert("设备重新安装检测到:重新校准中"),
}, },
EventName.doorOpen: { EventName.doorOpen: {
ET.SOFT_DISABLE: user_soft_disable_alert("Door Open"), ET.SOFT_DISABLE: user_soft_disable_alert("车门开启"),
ET.NO_ENTRY: NoEntryAlert("Door Open"), ET.NO_ENTRY: NoEntryAlert("车门开启"),
}, },
EventName.seatbeltNotLatched: { EventName.seatbeltNotLatched: {
ET.SOFT_DISABLE: user_soft_disable_alert("Seatbelt Unlatched"), ET.SOFT_DISABLE: user_soft_disable_alert("安全带未系"),
ET.NO_ENTRY: NoEntryAlert("Seatbelt Unlatched"), ET.NO_ENTRY: NoEntryAlert("安全带未系"),
}, },
EventName.espDisabled: { EventName.espDisabled: {
ET.SOFT_DISABLE: soft_disable_alert("Electronic Stability Control Disabled"), ET.SOFT_DISABLE: soft_disable_alert("电子稳定控制系统已禁用"),
ET.NO_ENTRY: NoEntryAlert("Electronic Stability Control Disabled"), ET.NO_ENTRY: NoEntryAlert("电子稳定控制系统已禁用"),
}, },
EventName.lowBattery: { EventName.lowBattery: {
ET.SOFT_DISABLE: soft_disable_alert("Low Battery"), ET.SOFT_DISABLE: soft_disable_alert("电池电量低"),
ET.NO_ENTRY: NoEntryAlert("Low Battery"), ET.NO_ENTRY: NoEntryAlert("电池电量低"),
}, },
# Different openpilot services communicate between each other at a certain # Different openpilot services communicate between each other at a certain
@ -834,41 +841,41 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
# is thrown. This can mean a service crashed, did not broadcast a message for # is thrown. This can mean a service crashed, did not broadcast a message for
# ten times the regular interval, or the average interval is more than 10% too high. # ten times the regular interval, or the average interval is more than 10% too high.
EventName.commIssue: { EventName.commIssue: {
ET.SOFT_DISABLE: soft_disable_alert("Communication Issue Between Processes"), ET.SOFT_DISABLE: soft_disable_alert("进程间通信问题"),
ET.NO_ENTRY: comm_issue_alert, ET.NO_ENTRY: comm_issue_alert,
}, },
EventName.commIssueAvgFreq: { EventName.commIssueAvgFreq: {
ET.SOFT_DISABLE: soft_disable_alert("Low Communication Rate Between Processes"), ET.SOFT_DISABLE: soft_disable_alert("进程间通信速率低"),
ET.NO_ENTRY: NoEntryAlert("Low Communication Rate Between Processes"), ET.NO_ENTRY: NoEntryAlert("进程间通信速率低"),
}, },
EventName.selfdrivedLagging: { EventName.selfdrivedLagging: {
ET.SOFT_DISABLE: soft_disable_alert("System Lagging"), ET.SOFT_DISABLE: soft_disable_alert("系统滞后"),
ET.NO_ENTRY: NoEntryAlert("Selfdrive Process Lagging: Reboot Your Device"), ET.NO_ENTRY: NoEntryAlert("自驾车进程滞后:请重启设备"),
}, },
# Thrown when manager detects a service exited unexpectedly while driving # Thrown when manager detects a service exited unexpectedly while driving
EventName.processNotRunning: { EventName.processNotRunning: {
ET.NO_ENTRY: process_not_running_alert, ET.NO_ENTRY: process_not_running_alert,
ET.SOFT_DISABLE: soft_disable_alert("Process Not Running"), ET.SOFT_DISABLE: soft_disable_alert("进程未运行"),
}, },
EventName.radarFault: { EventName.radarFault: {
ET.SOFT_DISABLE: soft_disable_alert("Radar Error: Restart the Car"), ET.SOFT_DISABLE: soft_disable_alert("雷达错误:请重启车辆"),
ET.NO_ENTRY: NoEntryAlert("Radar Error: Restart the Car"), ET.NO_ENTRY: NoEntryAlert("雷达错误:请重启车辆"),
}, },
EventName.radarTempUnavailable: { EventName.radarTempUnavailable: {
ET.SOFT_DISABLE: soft_disable_alert("Radar Temporarily Unavailable"), ET.SOFT_DISABLE: soft_disable_alert("雷达暂时不可用"),
ET.NO_ENTRY: NoEntryAlert("Radar Temporarily Unavailable"), ET.NO_ENTRY: NoEntryAlert("雷达暂时不可用"),
}, },
# Every frame from the camera should be processed by the model. If modeld # Every frame from the camera should be processed by the model. If modeld
# is not processing frames fast enough they have to be dropped. This alert is # is not processing frames fast enough they have to be dropped. This alert is
# thrown when over 20% of frames are dropped. # thrown when over 20% of frames are dropped.
EventName.modeldLagging: { EventName.modeldLagging: {
ET.SOFT_DISABLE: soft_disable_alert("Driving Model Lagging"), ET.SOFT_DISABLE: soft_disable_alert("驾驶模型滞后"),
ET.NO_ENTRY: NoEntryAlert("Driving Model Lagging"), ET.NO_ENTRY: NoEntryAlert("驾驶模型滞后"),
ET.PERMANENT: modeld_lagging_alert, ET.PERMANENT: modeld_lagging_alert,
}, },
@ -878,45 +885,45 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
# usually means the model has trouble understanding the scene. This is used # usually means the model has trouble understanding the scene. This is used
# as a heuristic to warn the driver. # as a heuristic to warn the driver.
EventName.posenetInvalid: { EventName.posenetInvalid: {
ET.SOFT_DISABLE: soft_disable_alert("Posenet Speed Invalid"), ET.SOFT_DISABLE: soft_disable_alert("Posenet速度无效"),
ET.NO_ENTRY: posenet_invalid_alert, ET.NO_ENTRY: posenet_invalid_alert,
}, },
# When the localizer detects an acceleration of more than 40 m/s^2 (~4G) we # When the localizer detects an acceleration of more than 40 m/s^2 (~4G) we
# alert the driver the device might have fallen from the windshield. # alert the driver the device might have fallen from the windshield.
EventName.deviceFalling: { EventName.deviceFalling: {
ET.SOFT_DISABLE: soft_disable_alert("Device Fell Off Mount"), ET.SOFT_DISABLE: soft_disable_alert("设备从支架掉落"),
ET.NO_ENTRY: NoEntryAlert("Device Fell Off Mount"), ET.NO_ENTRY: NoEntryAlert("设备从支架掉落"),
}, },
EventName.lowMemory: { EventName.lowMemory: {
ET.SOFT_DISABLE: soft_disable_alert("Low Memory: Reboot Your Device"), ET.SOFT_DISABLE: soft_disable_alert("内存不足:请重启设备"),
ET.PERMANENT: low_memory_alert, ET.PERMANENT: low_memory_alert,
ET.NO_ENTRY: NoEntryAlert("Low Memory: Reboot Your Device"), ET.NO_ENTRY: NoEntryAlert("内存不足:请重启设备"),
}, },
EventName.accFaulted: { EventName.accFaulted: {
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Cruise Fault: Restart the Car"), ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("巡航故障:请重启车辆"),
ET.PERMANENT: NormalPermanentAlert("Cruise Fault: Restart the car to engage"), ET.PERMANENT: NormalPermanentAlert("巡航故障:重启车辆以启用"),
ET.NO_ENTRY: NoEntryAlert("Cruise Fault: Restart the Car"), ET.NO_ENTRY: NoEntryAlert("巡航故障:请重启车辆"),
}, },
EventName.espActive: { EventName.espActive: {
ET.SOFT_DISABLE: soft_disable_alert("Electronic Stability Control Active"), ET.SOFT_DISABLE: soft_disable_alert("电子稳定控制系统激活中"),
ET.NO_ENTRY: NoEntryAlert("Electronic Stability Control Active"), ET.NO_ENTRY: NoEntryAlert("电子稳定控制系统激活中"),
}, },
EventName.controlsMismatch: { EventName.controlsMismatch: {
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Controls Mismatch"), ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("控制不匹配"),
ET.NO_ENTRY: NoEntryAlert("Controls Mismatch"), ET.NO_ENTRY: NoEntryAlert("控制不匹配"),
}, },
# Sometimes the USB stack on the device can get into a bad state # Sometimes the USB stack on the device can get into a bad state
# causing the connection to the panda to be lost # causing the connection to the panda to be lost
EventName.usbError: { EventName.usbError: {
ET.SOFT_DISABLE: soft_disable_alert("USB Error: Reboot Your Device"), ET.SOFT_DISABLE: soft_disable_alert("USB错误:请重启设备"),
ET.PERMANENT: NormalPermanentAlert("USB Error: Reboot Your Device"), ET.PERMANENT: NormalPermanentAlert("USB错误:请重启设备"),
ET.NO_ENTRY: NoEntryAlert("USB Error: Reboot Your Device"), ET.NO_ENTRY: NoEntryAlert("USB错误:请重启设备"),
}, },
# This alert can be thrown for the following reasons: # This alert can be thrown for the following reasons:
@ -924,45 +931,45 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
# - CAN data is received, but some message are not received at the right frequency # - CAN data is received, but some message are not received at the right frequency
# If you're not writing a new car port, this is usually cause by faulty wiring # If you're not writing a new car port, this is usually cause by faulty wiring
EventName.canError: { EventName.canError: {
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("CAN Error"), ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("CAN总线错误"),
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"CAN Error: Check Connections", "CAN总线错误:请检查连接",
"", "",
AlertStatus.normal, AlertSize.small, AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, 1., creation_delay=1.), Priority.LOW, VisualAlert.none, AudibleAlert.none, 1., creation_delay=1.),
ET.NO_ENTRY: NoEntryAlert("CAN Error: Check Connections"), ET.NO_ENTRY: NoEntryAlert("CAN总线错误:请检查连接"),
}, },
EventName.canBusMissing: { EventName.canBusMissing: {
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("CAN Bus Disconnected"), ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("CAN总线断开连接"),
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"CAN Bus Disconnected: Likely Faulty Cable", "CAN总线断开连接:可能电缆故障",
"", "",
AlertStatus.normal, AlertSize.small, AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, 1., creation_delay=1.), Priority.LOW, VisualAlert.none, AudibleAlert.none, 1., creation_delay=1.),
ET.NO_ENTRY: NoEntryAlert("CAN Bus Disconnected: Check Connections"), ET.NO_ENTRY: NoEntryAlert("CAN总线断开连接:请检查连接"),
}, },
EventName.steerUnavailable: { EventName.steerUnavailable: {
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("LKAS Fault: Restart the Car"), ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("LKAS故障:请重启车辆"),
ET.PERMANENT: NormalPermanentAlert("LKAS Fault: Restart the car to engage"), ET.PERMANENT: NormalPermanentAlert("LKAS故障:重启车辆以启用"),
ET.NO_ENTRY: NoEntryAlert("LKAS Fault: Restart the Car"), ET.NO_ENTRY: NoEntryAlert("LKAS故障:请重启车辆"),
}, },
EventName.reverseGear: { EventName.reverseGear: {
ET.PERMANENT: Alert( ET.PERMANENT: Alert(
"Reverse\nGear", "倒车中",
"", "",
AlertStatus.normal, AlertSize.full, AlertStatus.normal, AlertSize.full,
Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5), Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5),
ET.USER_DISABLE: ImmediateDisableAlert("Reverse Gear"), ET.USER_DISABLE: ImmediateDisableAlert("倒档"),
ET.NO_ENTRY: NoEntryAlert("Reverse Gear"), ET.NO_ENTRY: NoEntryAlert("倒档"),
}, },
# On cars that use stock ACC the car can decide to cancel ACC for various reasons. # On cars that use stock ACC the car can decide to cancel ACC for various reasons.
# When this happens we can no long control the car so the user needs to be warned immediately. # When this happens we can no long control the car so the user needs to be warned immediately.
EventName.cruiseDisabled: { EventName.cruiseDisabled: {
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Cruise Is Off"), ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("巡航已关闭"),
}, },
# When the relay in the harness box opens the CAN bus between the LKAS camera # When the relay in the harness box opens the CAN bus between the LKAS camera
@ -970,15 +977,15 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
# are received on the car side this usually means the relay hasn't opened correctly # are received on the car side this usually means the relay hasn't opened correctly
# and this alert is thrown. # and this alert is thrown.
EventName.relayMalfunction: { EventName.relayMalfunction: {
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Harness Relay Malfunction"), ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("线束继电器故障"),
ET.PERMANENT: NormalPermanentAlert("Harness Relay Malfunction", "Check Hardware"), ET.PERMANENT: NormalPermanentAlert("线束继电器故障", "检查硬件"),
ET.NO_ENTRY: NoEntryAlert("Harness Relay Malfunction"), ET.NO_ENTRY: NoEntryAlert("线束继电器故障"),
}, },
EventName.speedTooLow: { EventName.speedTooLow: {
ET.IMMEDIATE_DISABLE: Alert( ET.IMMEDIATE_DISABLE: Alert(
"openpilot Canceled", "openpilot已取消",
"Speed too low", "速度过低",
AlertStatus.normal, AlertSize.mid, AlertStatus.normal, AlertSize.mid,
Priority.HIGH, VisualAlert.none, AudibleAlert.disengage, 3.), Priority.HIGH, VisualAlert.none, AudibleAlert.disengage, 3.),
}, },
@ -986,17 +993,17 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
# When the car is driving faster than most cars in the training data, the model outputs can be unpredictable. # When the car is driving faster than most cars in the training data, the model outputs can be unpredictable.
EventName.speedTooHigh: { EventName.speedTooHigh: {
ET.WARNING: Alert( ET.WARNING: Alert(
"Speed Too High", "速度过高",
"Model uncertain at this speed", "在此速度下模型不稳定",
AlertStatus.userPrompt, AlertSize.mid, AlertStatus.userPrompt, AlertSize.mid,
Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 4.), Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 4.),
ET.NO_ENTRY: NoEntryAlert("Slow down to engage"), ET.NO_ENTRY: NoEntryAlert("减速以进行接合"),
}, },
EventName.vehicleSensorsInvalid: { EventName.vehicleSensorsInvalid: {
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Vehicle Sensors Invalid"), ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("车辆传感器无效"),
ET.PERMANENT: NormalPermanentAlert("Vehicle Sensors Calibrating", "Drive to Calibrate"), ET.PERMANENT: NormalPermanentAlert("车辆传感器校准中", "行驶以校准"),
ET.NO_ENTRY: NoEntryAlert("Vehicle Sensors Calibrating"), ET.NO_ENTRY: NoEntryAlert("车辆传感器校准中"),
}, },
EventName.personalityChanged: { EventName.personalityChanged: {
@ -1004,7 +1011,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
}, },
EventName.userBookmark: { EventName.userBookmark: {
ET.PERMANENT: NormalPermanentAlert("Bookmark Saved", duration=1.5), ET.PERMANENT: NormalPermanentAlert("书签已保存", duration=1.5),
}, },
EventName.audioFeedback: { EventName.audioFeedback: {

View File

@ -43,7 +43,7 @@ class DeveloperLayout(Widget):
# Build items and keep references for callbacks/state updates # Build items and keep references for callbacks/state updates
self._adb_toggle = toggle_item( self._adb_toggle = toggle_item(
lambda: tr("Enable ADB"), lambda: tr("Enable ADB"),
description=lambda: DESCRIPTIONS["enable_adb"], description=lambda: tr(DESCRIPTIONS["enable_adb"]),
initial_state=self._params.get_bool("AdbEnabled"), initial_state=self._params.get_bool("AdbEnabled"),
callback=self._on_enable_adb, callback=self._on_enable_adb,
enabled=ui_state.is_offroad, enabled=ui_state.is_offroad,
@ -56,7 +56,7 @@ class DeveloperLayout(Widget):
initial_state=self._params.get_bool("SshEnabled"), initial_state=self._params.get_bool("SshEnabled"),
callback=self._on_enable_ssh, callback=self._on_enable_ssh,
) )
self._ssh_keys = ssh_key_item(lambda: tr("SSH Keys"), description=lambda: DESCRIPTIONS["ssh_key"]) self._ssh_keys = ssh_key_item(lambda: tr("SSH Keys"), description=lambda: tr(DESCRIPTIONS["ssh_key"]))
self._joystick_toggle = toggle_item( self._joystick_toggle = toggle_item(
lambda: tr("Joystick Debug Mode"), lambda: tr("Joystick Debug Mode"),
@ -75,7 +75,7 @@ class DeveloperLayout(Widget):
self._alpha_long_toggle = toggle_item( self._alpha_long_toggle = toggle_item(
lambda: tr("openpilot Longitudinal Control (Alpha)"), lambda: tr("openpilot Longitudinal Control (Alpha)"),
description=lambda: DESCRIPTIONS["alpha_longitudinal"], description=lambda: tr(DESCRIPTIONS["alpha_longitudinal"]),
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"), initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
callback=self._on_alpha_long_enabled, callback=self._on_alpha_long_enabled,
enabled=lambda: not ui_state.engaged, enabled=lambda: not ui_state.engaged,

View File

@ -1,4 +1,5 @@
import os import os
import re
import math import math
import json import json
@ -19,6 +20,7 @@ from openpilot.system.ui.widgets.html_render import HtmlModal
from openpilot.system.ui.widgets.list_view import text_item, button_item, dual_button_item from openpilot.system.ui.widgets.list_view import text_item, button_item, dual_button_item
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.keyboard import Keyboard
# Description constants # Description constants
DESCRIPTIONS = { DESCRIPTIONS = {
@ -27,6 +29,8 @@ DESCRIPTIONS = {
'reset_calibration': tr_noop("openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."), 'reset_calibration': tr_noop("openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."),
'review_guide': tr_noop("Review the rules, features, and limitations of openpilot"), 'review_guide': tr_noop("Review the rules, features, and limitations of openpilot"),
} }
DATA_PARAMS_D_SECOCKEY_PATH = "/data/params/d/SecOCKey"
CACHE_PARAMS_SECOCKEY_PATH = "/cache/params/SecOCKey"
class DeviceLayout(Widget): class DeviceLayout(Widget):
@ -46,6 +50,9 @@ class DeviceLayout(Widget):
self._dp_vehicle_selector_make_dialog: MultiOptionDialog | None = None self._dp_vehicle_selector_make_dialog: MultiOptionDialog | None = None
self._dp_vehicle_selector_model_dialog: MultiOptionDialog | None = None self._dp_vehicle_selector_model_dialog: MultiOptionDialog | None = None
self._keyboard = Keyboard(max_text_size=64, min_text_size=8, show_password_toggle=True)
self._secoc_key = self._params.get("SecOCKey") or self._read_key_from_files()
items = self._initialize_items() items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0) self._scroller = Scroller(items, line_separator=True, spacing=0)
@ -71,6 +78,8 @@ class DeviceLayout(Widget):
text_item(lambda: tr("Dongle ID"), self._params.get("DongleId") or (lambda: tr("N/A"))), text_item(lambda: tr("Dongle ID"), self._params.get("DongleId") or (lambda: tr("N/A"))),
text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))), text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))),
self._pair_device_btn, self._pair_device_btn,
button_item(lambda: tr("SecOCKey Install"), lambda: tr("INSTALL"), lambda: self._secoc_key, callback=self._install_secockey, enabled=ui_state.is_offroad),
self._reset_calib_btn,
button_item(lambda: tr("Driver Camera"), lambda: tr("PREVIEW"), lambda: tr(DESCRIPTIONS['driver_camera']), button_item(lambda: tr("Driver Camera"), lambda: tr("PREVIEW"), lambda: tr(DESCRIPTIONS['driver_camera']),
callback=self._show_driver_camera, enabled=ui_state.is_offroad and "LITE" not in os.environ), callback=self._show_driver_camera, enabled=ui_state.is_offroad and "LITE" not in os.environ),
self._reset_calib_btn, self._reset_calib_btn,
@ -101,7 +110,7 @@ class DeviceLayout(Widget):
self._select_language_dialog = None self._select_language_dialog = None
self._select_language_dialog = MultiOptionDialog(tr("Select a language"), multilang.languages, multilang.codes[multilang.language], self._select_language_dialog = MultiOptionDialog(tr("Select a language"), multilang.languages, multilang.codes[multilang.language],
option_font_weight=FontWeight.UNIFONT) option_font_weight=FontWeight.CHINA)
gui_app.set_modal_overlay(self._select_language_dialog, callback=handle_language_selection) gui_app.set_modal_overlay(self._select_language_dialog, callback=handle_language_selection)
def _show_driver_camera(self): def _show_driver_camera(self):
@ -299,3 +308,87 @@ class DeviceLayout(Widget):
dialog = ConfirmDialog(tr("Are you sure you want to switch?"), tr("CONFIRM")) dialog = ConfirmDialog(tr("Are you sure you want to switch?"), tr("CONFIRM"))
gui_app.set_modal_overlay(dialog, callback=on_off_road) gui_app.set_modal_overlay(dialog, callback=on_off_road)
@staticmethod
def _is_key_valid(key: str) -> bool:
"""Checks if the key is a valid 32-character lowercase hexadecimal string."""
if not isinstance(key, str):
return False
if len(key) != 32:
return False
pattern = r"^[0-9a-f]{32}$"
return bool(re.match(pattern, key))
@staticmethod
def _write_key_to_file(file_path: str, key: str) -> None:
"""Writes the key to the specified file path."""
try:
with open(file_path, "w") as f:
f.write(key)
except Exception as e:
print(f"Error writing key to file {file_path}: {e}")
def _read_key_from_file(self, file_path: str) -> str | None:
if not os.path.exists(file_path):
return None
try:
with open(file_path, "r") as f:
key = f.read().strip()
if self._is_key_valid(key):
return key
else:
# Key is invalid, delete the file
try:
os.remove(file_path)
print(f"Deleted invalid key file: {file_path} which contained {key}")
except Exception as e:
print(f"Error deleting invalid key file {file_path}: {e}")
return None
except Exception as e:
print(f"Error reading key file {file_path}: {e}")
return None # Return None on any error
def _read_key_from_files(self) -> str | None:
"""Reads the key from the appropriate file(s) based on the AGNOS environment."""
data_params_d_secockey = self._read_key_from_file(DATA_PARAMS_D_SECOCKEY_PATH)
cache_params_secockey = self._read_key_from_file(CACHE_PARAMS_SECOCKEY_PATH)
existing_key = cache_params_secockey or data_params_d_secockey
if not existing_key:
return None
# Write the existing key to missing files
if data_params_d_secockey != existing_key:
self._write_key_to_file(DATA_PARAMS_D_SECOCKEY_PATH, existing_key)
if cache_params_secockey != existing_key:
self._write_key_to_file(CACHE_PARAMS_SECOCKEY_PATH, existing_key)
return existing_key
def _install_secockey(self):
def enter_secoc_key(result):
key = self._keyboard.text
if key == "":
gui_app.set_modal_overlay(alert_dialog(tr("Key cannot be empty.")))
return False
if len(key) != 32:
gui_app.set_modal_overlay(alert_dialog(tr("Key must be exactly 32 characters long. Current length: {} characters.").format(len(key))))
return False
if not self._is_key_valid(key):
gui_app.set_modal_overlay(alert_dialog(tr("Invalid key format. Key must contain only hexadecimal characters (0-9, a-f).")))
return False
self._secoc_key = key
self._params.put("SecOCKey", key)
self._write_key_to_file(DATA_PARAMS_D_SECOCKEY_PATH, key)
self._write_key_to_file(CACHE_PARAMS_SECOCKEY_PATH, key)
gui_app.set_modal_overlay(alert_dialog(tr("Success!\nRestart comma to have openpilot use the key")))
self._keyboard.reset(min_text_size=0)
self._keyboard.set_text(self._secoc_key or "")
self._keyboard.set_title(tr("Enter your Car Security Key"), tr("Archived key: \"{}\"").format(self._secoc_key))
gui_app.set_modal_overlay(self._keyboard, enter_secoc_key)

View File

@ -4,7 +4,6 @@ from enum import IntEnum
from collections.abc import Callable from collections.abc import Callable
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
@ -65,9 +64,8 @@ class SettingsLayout(Widget):
PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUI(wifi_manager)), PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUI(wifi_manager)),
PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout()), PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout()),
PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout()), PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout()),
PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout()),
PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout()), PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout()),
PanelType.DRAGONPILOT: PanelInfo("dp", DragonpilotLayout()), PanelType.DRAGONPILOT: PanelInfo(tr_noop("Dp"), DragonpilotLayout()),
} }
self._font_medium = gui_app.font(FontWeight.MEDIUM) self._font_medium = gui_app.font(FontWeight.MEDIUM)

View File

@ -68,7 +68,7 @@ class SoftwareLayout(Widget):
self._version_item, self._version_item,
self._download_btn, self._download_btn,
self._install_btn, self._install_btn,
# self._branch_btn, # rick - disable this for now self._branch_btn,
button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall), button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall),
], line_separator=True, spacing=0) ], line_separator=True, spacing=0)

View File

@ -28,7 +28,14 @@ DESCRIPTIONS = {
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " + "Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
"without a turn signal activated while driving over 31 mph (50 km/h)." "without a turn signal activated while driving over 31 mph (50 km/h)."
), ),
"AlwaysOnDM": tr_noop("Enable driver monitoring even when openpilot is not engaged."), "AlwaysOnDM": tr_noop("The driver monitoring system can be toggled on/off, but long-term activation is recommended"),
"DistractionDetectionLevel": tr_noop(
"Set how sensitive the driver distraction detection should be. " +
"Strict: Very sensitive, warns on minor distractions. " +
"Moderate: Balanced between sensitivity and false positives. " +
"Lenient: Only alerts on clear distractions. " +
"Off: Disable Driver Distraction Detection and Control."
),
'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."), 'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."),
"IsMetric": tr_noop("Display speed in km/h instead of mph."), "IsMetric": tr_noop("Display speed in km/h instead of mph."),
"RecordAudio": tr_noop("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."), "RecordAudio": tr_noop("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."),
@ -109,7 +116,7 @@ class TogglesLayout(Widget):
self._long_personality_setting = multiple_button_item( self._long_personality_setting = multiple_button_item(
lambda: tr("Driving Personality"), lambda: tr("Driving Personality"),
DESCRIPTIONS["LongitudinalPersonality"], tr(DESCRIPTIONS["LongitudinalPersonality"]),
buttons=[lambda: tr("Aggressive"), lambda: tr("Standard"), lambda: tr("Relaxed")], buttons=[lambda: tr("Aggressive"), lambda: tr("Standard"), lambda: tr("Relaxed")],
button_width=255, button_width=255,
callback=self._set_longitudinal_personality, callback=self._set_longitudinal_personality,
@ -117,6 +124,16 @@ class TogglesLayout(Widget):
icon="speed_limit.png" icon="speed_limit.png"
) )
self._distraction_detection_level = multiple_button_item(
lambda: tr("Distraction Detection Level"),
tr(DESCRIPTIONS["DistractionDetectionLevel"]),
buttons=[lambda: tr("Strict"), lambda: tr("Moderate"), lambda: tr("Lenient")],
button_width=255,
callback=self._set_distraction_detection_level,
selected_index=self._params.get("DistractionDetectionLevel", return_default=True),
icon="monitoring.png"
)
self._toggles = {} self._toggles = {}
self._locked_toggles = set() self._locked_toggles = set()
@ -156,6 +173,12 @@ class TogglesLayout(Widget):
if param == "DisengageOnAccelerator": if param == "DisengageOnAccelerator":
self._toggles["LongitudinalPersonality"] = self._long_personality_setting self._toggles["LongitudinalPersonality"] = self._long_personality_setting
if param == "AlwaysOnDM":
# 根据AlwaysOnDM状态动态显示/隐藏分心检测级别
self._toggles["DistractionDetectionLevel"] = self._distraction_detection_level
# 初始设置可见性
self._update_distraction_detection_visibility()
self._update_experimental_mode_icon() self._update_experimental_mode_icon()
self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0) self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0)
@ -228,6 +251,13 @@ class TogglesLayout(Widget):
def _render(self, rect): def _render(self, rect):
self._scroller.render(rect) self._scroller.render(rect)
def _update_distraction_detection_visibility(self):
"""根据AlwaysOnDM状态更新分心检测级别的可见性"""
always_on_dm_enabled = self._params.get_bool("AlwaysOnDM")
if "DistractionDetectionLevel" in self._toggles:
# 设置分心检测级别的可见性
self._toggles["DistractionDetectionLevel"].set_visible(always_on_dm_enabled)
def _update_experimental_mode_icon(self): def _update_experimental_mode_icon(self):
icon = "experimental.png" if self._toggles["ExperimentalMode"].action_item.get_state() else "experimental_white.png" icon = "experimental.png" if self._toggles["ExperimentalMode"].action_item.get_state() else "experimental_white.png"
self._toggles["ExperimentalMode"].set_icon(icon) self._toggles["ExperimentalMode"].set_icon(icon)
@ -261,5 +291,12 @@ class TogglesLayout(Widget):
if self._toggle_defs[param][3]: if self._toggle_defs[param][3]:
self._params.put_bool("OnroadCycleRequested", True) self._params.put_bool("OnroadCycleRequested", True)
# 如果切换的是AlwaysOnDM更新分心检测级别的可见性
if param == "AlwaysOnDM":
self._update_distraction_detection_visibility()
def _set_longitudinal_personality(self, button_index: int): def _set_longitudinal_personality(self, button_index: int):
self._params.put("LongitudinalPersonality", button_index) self._params.put("LongitudinalPersonality", button_index)
def _set_distraction_detection_level(self, button_index: int):
self._params.put("DistractionDetectionLevel", button_index)

View File

@ -68,9 +68,9 @@ class Sidebar(Widget):
self._net_type = NETWORK_TYPES.get(NetworkType.none) self._net_type = NETWORK_TYPES.get(NetworkType.none)
self._net_strength = 0 self._net_strength = 0
self._temp_status = MetricData(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD) self._temp_status = MetricData(tr_noop("TEMP"), "38°C", Colors.GOOD)
self._cpu_status = MetricData(tr_noop("CPU"), "10%", Colors.GOOD)
self._panda_status = MetricData(tr_noop("VEHICLE"), tr_noop("ONLINE"), Colors.GOOD) self._panda_status = MetricData(tr_noop("VEHICLE"), tr_noop("ONLINE"), Colors.GOOD)
self._connect_status = MetricData(tr_noop("CONNECT"), tr_noop("OFFLINE"), Colors.WARNING)
self._recording_audio = False self._recording_audio = False
self._home_img = gui_app.texture("images/button_home.png", HOME_BTN.width, HOME_BTN.height) self._home_img = gui_app.texture("images/button_home.png", HOME_BTN.width, HOME_BTN.height)
@ -110,8 +110,7 @@ class Sidebar(Widget):
self._recording_audio = ui_state.recording_audio self._recording_audio = ui_state.recording_audio
self._update_network_status(device_state) self._update_network_status(device_state)
self._update_temperature_status(device_state) self._update_temperature_status(device_state)
if not ui_state.dp_dev_disable_connect: self._update_cpu_status(device_state)
self._update_connection_status(device_state)
self._update_panda_status() self._update_panda_status()
def _update_network_status(self, device_state): def _update_network_status(self, device_state):
@ -121,26 +120,28 @@ class Sidebar(Widget):
def _update_temperature_status(self, device_state): def _update_temperature_status(self, device_state):
thermal_status = device_state.thermalStatus thermal_status = device_state.thermalStatus
max_temp = device_state.maxTempC
if thermal_status == ThermalStatus.green: if thermal_status == ThermalStatus.green:
self._temp_status.update(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD) self._temp_status.update(tr_noop("TEMP"), f"{max_temp:.1f}°C", Colors.GOOD)
elif thermal_status == ThermalStatus.yellow: elif thermal_status == ThermalStatus.yellow:
self._temp_status.update(tr_noop("TEMP"), tr_noop("OK"), Colors.WARNING) self._temp_status.update(tr_noop("TEMP"), f"{max_temp:.1f}°C", Colors.WARNING)
else: else:
self._temp_status.update(tr_noop("TEMP"), tr_noop("HIGH"), Colors.DANGER) self._temp_status.update(tr_noop("TEMP"), f"{max_temp:.1f}°C", Colors.DANGER)
def _update_connection_status(self, device_state): def _update_cpu_status(self, device_state):
last_ping = device_state.lastAthenaPingTime cpu_temp = max(device_state.cpuTempC, default=0.)
if last_ping == 0:
self._connect_status.update(tr_noop("CONNECT"), tr_noop("OFFLINE"), Colors.WARNING) if cpu_temp >= 85:
elif time.monotonic_ns() - last_ping < 80_000_000_000: # 80 seconds in nanoseconds self._cpu_status.update(tr_noop("CPU"), f"{cpu_temp:.1f}%", Colors.DANGER)
self._connect_status.update(tr_noop("CONNECT"), tr_noop("ONLINE"), Colors.GOOD) elif cpu_temp >= 65:
self._cpu_status.update(tr_noop("CPU"), f"{cpu_temp:.1f}%", Colors.WARNING)
else: else:
self._connect_status.update(tr_noop("CONNECT"), tr_noop("ERROR"), Colors.DANGER) self._cpu_status.update(tr_noop("CPU"), f"{cpu_temp:.1f}%", Colors.GOOD)
def _update_panda_status(self): def _update_panda_status(self):
if ui_state.panda_type == log.PandaState.PandaType.unknown: if ui_state.panda_type == log.PandaState.PandaType.unknown:
self._panda_status.update(tr_noop("NO"), tr_noop("PANDA"), Colors.DANGER) self._panda_status.update(tr_noop("PANDA"), tr_noop("NO"), Colors.DANGER)
else: else:
self._panda_status.update(tr_noop("VEHICLE"), tr_noop("ONLINE"), Colors.GOOD) self._panda_status.update(tr_noop("VEHICLE"), tr_noop("ONLINE"), Colors.GOOD)
@ -201,9 +202,7 @@ class Sidebar(Widget):
rl.draw_text_ex(self._font_regular, tr(self._net_type), text_pos, FONT_SIZE, 0, Colors.WHITE) rl.draw_text_ex(self._font_regular, tr(self._net_type), text_pos, FONT_SIZE, 0, Colors.WHITE)
def _draw_metrics(self, rect: rl.Rectangle): def _draw_metrics(self, rect: rl.Rectangle):
metrics = [(self._temp_status, 338), (self._panda_status, 496)] metrics = [(self._temp_status, 338), (self._cpu_status, 496), (self._panda_status, 654)]
if not ui_state.dp_dev_disable_connect:
metrics.append((self._connect_status, 654))
for metric, y_offset in metrics: for metric, y_offset in metrics:
self._draw_metric(rect, metric, rect.y + y_offset) self._draw_metric(rect, metric, rect.y + y_offset)

View File

@ -136,7 +136,7 @@ class AugmentedRoadView(CameraView):
pass pass
def _draw_border(self, rect: rl.Rectangle): def _draw_border(self, rect: rl.Rectangle):
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, rl.BLACK)
border_roundness = 0.12 border_roundness = 0.12
border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED]) border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
if ui_state.dp_alka_active and ui_state.status == UIStatus.DISENGAGED: if ui_state.dp_alka_active and ui_state.status == UIStatus.DISENGAGED:
@ -153,19 +153,6 @@ class AugmentedRoadView(CameraView):
if self._dp_indicator_show_right: if self._dp_indicator_show_right:
rl.draw_rectangle(int(rect.x + rect.width-UI_BORDER_SIZE), indicator_y, UI_BORDER_SIZE, indicator_height, self._dp_indicator_color_right) rl.draw_rectangle(int(rect.x + rect.width-UI_BORDER_SIZE), indicator_y, UI_BORDER_SIZE, indicator_height, self._dp_indicator_color_right)
# black bg around colored border
black_bg_thickness = UI_BORDER_SIZE
black_bg_rect = rl.Rectangle(
border_rect.x - UI_BORDER_SIZE,
border_rect.y - UI_BORDER_SIZE,
border_rect.width + 2 * UI_BORDER_SIZE,
border_rect.height + 2 * UI_BORDER_SIZE,
)
edge_offset = (black_bg_rect.height - border_rect.height) / 2 # distance between rect edges
roundness_out = (border_roundness * border_rect.height + 2 * edge_offset) / max(1.0, black_bg_rect.height)
rl.draw_rectangle_rounded_lines_ex(black_bg_rect, roundness_out, 10, black_bg_thickness, rl.BLACK)
rl.end_scissor_mode()
def _switch_stream_if_needed(self, sm): def _switch_stream_if_needed(self, sm):
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams: if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
v_ego = sm['carState'].vEgo v_ego = sm['carState'].vEgo

View File

@ -114,6 +114,7 @@ class CameraView(Widget):
# which drains the VisionIpcClient SubSocket for us. Re-connecting is not enough # which drains the VisionIpcClient SubSocket for us. Re-connecting is not enough
# and only clears internal buffers, not the message queue. # and only clears internal buffers, not the message queue.
self.frame = None self.frame = None
self.available_streams.clear()
if self.client: if self.client:
del self.client del self.client
self.client = VisionIpcClient(self._name, self._stream_type, conflate=True) self.client = VisionIpcClient(self._name, self._stream_type, conflate=True)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import argparse
import json import json
import os import os
import pathlib import pathlib
import xml.etree.ElementTree as ET import re
from typing import cast from typing import cast
import requests import requests
@ -12,7 +12,7 @@ import requests
TRANSLATIONS_DIR = pathlib.Path(__file__).resolve().parent TRANSLATIONS_DIR = pathlib.Path(__file__).resolve().parent
TRANSLATIONS_LANGUAGES = TRANSLATIONS_DIR / "languages.json" TRANSLATIONS_LANGUAGES = TRANSLATIONS_DIR / "languages.json"
OPENAI_MODEL = "gpt-4" OPENAI_MODEL = "deepseek-chat"
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
OPENAI_PROMPT = "You are a professional translator from English to {language} (ISO 639 language code). " + \ OPENAI_PROMPT = "You are a professional translator from English to {language} (ISO 639 language code). " + \
"The following sentence or word is in the GUI of a software called openpilot, translate it accordingly." "The following sentence or word is in the GUI of a software called openpilot, translate it accordingly."
@ -25,8 +25,8 @@ def get_language_files(languages: list[str] = None) -> dict[str, pathlib.Path]:
language_dict = json.load(fp) language_dict = json.load(fp)
for filename in language_dict.values(): for filename in language_dict.values():
path = TRANSLATIONS_DIR / f"{filename}.ts" path = TRANSLATIONS_DIR / f"app_{filename}.po"
language = path.stem language = filename
if languages is None or language in languages: if languages is None or language in languages:
files[language] = path files[language] = path
@ -36,7 +36,7 @@ def get_language_files(languages: list[str] = None) -> dict[str, pathlib.Path]:
def translate_phrase(text: str, language: str) -> str: def translate_phrase(text: str, language: str) -> str:
response = requests.post( response = requests.post(
"https://api.openai.com/v1/chat/completions", "https://api.deepseek.com/chat/completions",
json={ json={
"model": OPENAI_MODEL, "model": OPENAI_MODEL,
"messages": [ "messages": [
@ -68,39 +68,343 @@ def translate_phrase(text: str, language: str) -> str:
def translate_file(path: pathlib.Path, language: str, all_: bool) -> None: def translate_file(path: pathlib.Path, language: str, all_: bool) -> None:
tree = ET.parse(path) # Read the PO file
with path.open("r", encoding="utf-8") as fp:
lines = fp.readlines()
root = tree.getroot() # Process each line to find translation entries
i = 0
while i < len(lines):
line = lines[i].strip()
for context in root.findall("./context"): # Look for msgid line
name = context.find("name") if line.startswith('msgid'):
if name is None: # Check for empty msgid (header) - this is the start of multi-line msgid
raise ValueError("name not found") if line == 'msgid ""':
# This is a multi-line msgid entry
msgid_text = ""
j = i + 1 # Start from the next line
print(f"Context: {name.text}") # Collect all the quoted lines that form the msgid
while j < len(lines) and lines[j].strip().startswith('"'):
msgid_text += lines[j].strip().strip('"')
j += 1
for message in context.findall("./message"): # Skip header entry (empty msgid)
source = message.find("source") if not msgid_text:
translation = message.find("translation") i = j
continue
if source is None or translation is None: # Look for the corresponding msgstr or msgid_plural
raise ValueError("source or translation not found") k = j
has_plural = False
while k < len(lines) and not lines[k].strip().startswith('msgstr'):
if lines[k].strip().startswith('msgid_plural'):
has_plural = True
k += 1
if not all_ and translation.attrib.get("type") != "unfinished": if k < len(lines):
# Handle plural forms
if has_plural:
# This is a plural entry, need to handle msgstr[0], msgstr[1], etc.
msgstr_texts = []
m = k
# Find all msgstr[n] entries
while m < len(lines) and lines[m].strip().startswith('msgstr['):
msgstr_match = re.match(r'msgstr\[(\d+)\]\s*"(.*)"', lines[m].strip())
if msgstr_match:
msgstr_texts.append(msgstr_match.group(2))
m += 1
# Check if we should translate this entry
should_translate = False
if all_:
should_translate = True
else:
# Only translate if all msgstr entries are empty or contain only whitespace
should_translate = all(not text.strip() for text in msgstr_texts)
if should_translate:
# Translate both singular and plural forms
singular_text = msgid_text
# Find the plural form
plural_text = ""
p = j
while p < k and not lines[p].strip().startswith('msgid_plural'):
p += 1
if p < k:
# Extract plural text
plural_match = re.match(r'msgid_plural\s*"(.*)"', lines[p].strip())
if plural_match:
plural_text = plural_match.group(1)
# Translate both forms
singular_translation = translate_phrase(singular_text, language)
plural_translation = translate_phrase(plural_text, language)
print(f"Translating plural entry:")
print(f"Singular: {singular_text}")
print(f"Plural: {plural_text}")
print(f"Singular translation: {singular_translation}")
print(f"Plural translation: {plural_translation}")
print("-" * 50)
# Update msgstr[0] and msgstr[1]
m = k
idx = 0
while m < len(lines) and lines[m].strip().startswith('msgstr['):
if idx == 0:
lines[m] = f'msgstr[0] "{singular_translation}"\n'
elif idx == 1:
lines[m] = f'msgstr[1] "{plural_translation}"\n'
idx += 1
m += 1
i = m
continue
else:
i = k + len(msgstr_texts)
continue
else:
# Extract msgstr text (handle multi-line msgstr)
msgstr_text = ""
m = k
# Check if this is a multi-line msgstr
if lines[m].strip() == 'msgstr ""':
# Multi-line msgstr - collect all quoted lines
m += 1
while m < len(lines) and lines[m].strip().startswith('"'):
msgstr_text += lines[m].strip().strip('"')
m += 1
else:
# Single-line msgstr
msgstr_match = re.match(r'msgstr\s+"(.+)"', lines[m].strip())
if msgstr_match:
msgstr_text = msgstr_match.group(1)
# Check if we should translate this entry
should_translate = False
if all_:
should_translate = True
else:
# Only translate if msgstr is empty or contains only whitespace
if not msgstr_text.strip():
should_translate = True
if should_translate:
# Translate the phrase
llm_translation = translate_phrase(msgid_text, language)
print(f"Translating entry:")
print(f"Source: {msgid_text}")
print(f"LLM translation: {llm_translation}")
print("-" * 50)
# Update the msgstr line
if lines[k].strip() == 'msgstr ""':
# Multi-line msgstr - replace with multi-line format
# Remove existing msgstr lines
m = k + 1
while m < len(lines) and lines[m].strip().startswith('"'):
lines[m] = ""
m += 1
# Add new multi-line msgstr
lines[k] = 'msgstr ""\n'
# Split translation into lines of reasonable length
translation_lines = []
current_line = ""
for word in llm_translation.split():
if len(current_line + word) > 60: # Reasonable line length
translation_lines.append(f'"{current_line}"\n')
current_line = word
else:
if current_line:
current_line += " " + word
else:
current_line = word
if current_line:
translation_lines.append(f'"{current_line}"\n')
# Insert the translation lines
for idx, trans_line in enumerate(translation_lines):
lines.insert(k + 1 + idx, trans_line)
else:
# Single-line msgstr - replace it
lines[k] = f'msgstr "{llm_translation}"\n'
i = k + 1
continue
else:
i = k + 1
continue
i = j
continue continue
llm_translation = translate_phrase(cast(str, source.text), language) # Single-line msgid
msgid_match = re.match(r'msgid\s+"(.+)"', line)
if msgid_match:
msgid_text = msgid_match.group(1)
print(f"Source: {source.text}\n" + # Skip header entry (empty msgid)
f"Current translation: {translation.text}\n" + if not msgid_text:
f"LLM translation: {llm_translation}") i += 1
continue
translation.text = llm_translation # Look for the corresponding msgstr or msgid_plural
j = i + 1
has_plural = False
while j < len(lines) and not lines[j].strip().startswith('msgstr'):
if lines[j].strip().startswith('msgid_plural'):
has_plural = True
j += 1
if j < len(lines):
# Handle plural forms
if has_plural:
# This is a plural entry, need to handle msgstr[0], msgstr[1], etc.
msgstr_texts = []
m = j
# Find all msgstr[n] entries
while m < len(lines) and lines[m].strip().startswith('msgstr['):
msgstr_match = re.match(r'msgstr\[(\d+)\]\s*"(.*)"', lines[m].strip())
if msgstr_match:
msgstr_texts.append(msgstr_match.group(2))
m += 1
# Check if we should translate this entry
should_translate = False
if all_:
should_translate = True
else:
# Only translate if all msgstr entries are empty or contain only whitespace
should_translate = all(not text.strip() for text in msgstr_texts)
if should_translate:
# Translate both singular and plural forms
singular_text = msgid_text
# Find plural form
plural_text = ""
p = i + 1
while p < j and not lines[p].strip().startswith('msgid_plural'):
p += 1
if p < j:
# Extract plural text
plural_match = re.match(r'msgid_plural\s*"(.*)"', lines[p].strip())
if plural_match:
plural_text = plural_match.group(1)
# Translate both forms
singular_translation = translate_phrase(singular_text, language)
plural_translation = translate_phrase(plural_text, language)
print(f"Translating plural entry:")
print(f"Singular: {singular_text}")
print(f"Plural: {plural_text}")
print(f"Singular translation: {singular_translation}")
print(f"Plural translation: {plural_translation}")
print("-" * 50)
# Update msgstr[0] and msgstr[1]
m = j
idx = 0
while m < len(lines) and lines[m].strip().startswith('msgstr['):
if idx == 0:
lines[m] = f'msgstr[0] "{singular_translation}"\n'
elif idx == 1:
lines[m] = f'msgstr[1] "{plural_translation}"\n'
idx += 1
m += 1
i = m
continue
else:
i = j + len(msgstr_texts)
continue
else:
# Extract msgstr text (handle multi-line msgstr)
msgstr_text = ""
m = j
# Check if this is a multi-line msgstr
if lines[m].strip() == 'msgstr ""':
# Multi-line msgstr - collect all quoted lines
m += 1
while m < len(lines) and lines[m].strip().startswith('"'):
msgstr_text += lines[m].strip().strip('"')
m += 1
else:
# Single-line msgstr
msgstr_match = re.match(r'msgstr\s+"(.+)"', lines[m].strip())
if msgstr_match:
msgstr_text = msgstr_match.group(1)
# Check if we should translate this entry
should_translate = False
if all_:
should_translate = True
else:
# Only translate if msgstr is empty or contains only whitespace
if not msgstr_text.strip():
should_translate = True
if should_translate:
# Translate the phrase
llm_translation = translate_phrase(msgid_text, language)
print(f"Translating entry:")
print(f"Source: {msgid_text}")
print(f"LLM translation: {llm_translation}")
print("-" * 50)
# Update the msgstr line
if lines[j].strip() == 'msgstr ""':
# Multi-line msgstr - replace with multi-line format
# Remove existing msgstr lines
m = j + 1
while m < len(lines) and lines[m].strip().startswith('"'):
lines[m] = ""
m += 1
# Add new multi-line msgstr
lines[j] = 'msgstr ""\n'
# Split translation into lines of reasonable length
translation_lines = []
current_line = ""
for word in llm_translation.split():
if len(current_line + word) > 60: # Reasonable line length
translation_lines.append(f'"{current_line}"\n')
current_line = word
else:
if current_line:
current_line += " " + word
else:
current_line = word
if current_line:
translation_lines.append(f'"{current_line}"\n')
# Insert the translation lines
for idx, trans_line in enumerate(translation_lines):
lines.insert(j + 1 + idx, trans_line)
else:
# Single-line msgstr - replace it with single-line format
lines[j] = f'msgstr "{llm_translation}"\n'
i = j + 1
continue
else:
i = j + 1
continue
i += 1
# Write the updated PO file back with original formatting preserved
with path.open("w", encoding="utf-8") as fp: with path.open("w", encoding="utf-8") as fp:
fp.write('<?xml version="1.0" encoding="utf-8"?>\n' + fp.writelines(lines)
'<!DOCTYPE TS>\n' +
ET.tostring(root, encoding="utf-8").decode())
def main(): def main():

View File

@ -27,8 +27,8 @@ class SetupWidget(Widget):
def _render(self, rect: rl.Rectangle): def _render(self, rect: rl.Rectangle):
if not ui_state.prime_state.is_paired(): if not ui_state.prime_state.is_paired():
self._render_registration(rect) self._render_registration(rect)
else: # else:
self._render_firehose_prompt(rect) # self._render_firehose_prompt(rect)
def _render_registration(self, rect: rl.Rectangle): def _render_registration(self, rect: rl.Rectangle):
"""Render registration prompt.""" """Render registration prompt."""

View File

@ -52,11 +52,14 @@ class FontWeight(StrEnum):
EXTRA_BOLD = "Inter-ExtraBold.ttf" EXTRA_BOLD = "Inter-ExtraBold.ttf"
BLACK = "Inter-Black.ttf" BLACK = "Inter-Black.ttf"
UNIFONT = "unifont.otf" UNIFONT = "unifont.otf"
CHINA = "china.ttf"
def font_fallback(font: rl.Font) -> rl.Font: def font_fallback(font: rl.Font) -> rl.Font:
"""Fall back to unifont for languages that require it.""" """Fall back to unifont for languages that require it."""
if multilang.requires_unifont(): if multilang.requires_china():
return gui_app.font(FontWeight.CHINA)
elif multilang.requires_unifont():
return gui_app.font(FontWeight.UNIFONT) return gui_app.font(FontWeight.UNIFONT)
return font return font
@ -140,7 +143,12 @@ class GuiApplication:
self._fonts: dict[FontWeight, rl.Font] = {} self._fonts: dict[FontWeight, rl.Font] = {}
self._width = width self._width = width
self._height = height self._height = height
self._scale = SCALE
if PC and os.getenv("SCALE") is None:
self._scale = self._calculate_auto_scale()
else:
self._scale = SCALE
self._scaled_width = int(self._width * self._scale) self._scaled_width = int(self._width * self._scale)
self._scaled_height = int(self._height * self._scale) self._scaled_height = int(self._height * self._scale)
self._render_texture: rl.RenderTexture | None = None self._render_texture: rl.RenderTexture | None = None
@ -460,5 +468,17 @@ class GuiApplication:
cloudlog.error(f"FPS dropped critically below {fps}. Shutting down UI.") cloudlog.error(f"FPS dropped critically below {fps}. Shutting down UI.")
os._exit(1) os._exit(1)
def _calculate_auto_scale(self) -> float:
# Create temporary window to query monitor info
rl.init_window(1, 1, "")
w, h = rl.get_monitor_width(0), rl.get_monitor_height(0)
rl.close_window()
if w == 0 or h == 0 or (w >= self._width and h >= self._height):
return 1.0
# Apply 0.95 factor for window decorations/taskbar margin
return max(0.3, min(w / self._width, h / self._height) * 0.95)
gui_app = GuiApplication(2160, 1080) gui_app = GuiApplication(2160, 1080)

View File

@ -6,6 +6,7 @@ import pyray as rl
from openpilot.system.ui.lib.application import FONT_DIR from openpilot.system.ui.lib.application import FONT_DIR
_emoji_font: ImageFont.FreeTypeFont | None = None
_cache: dict[str, rl.Texture] = {} _cache: dict[str, rl.Texture] = {}
EMOJI_REGEX = re.compile( EMOJI_REGEX = re.compile(
@ -32,6 +33,13 @@ EMOJI_REGEX = re.compile(
flags=re.UNICODE flags=re.UNICODE
) )
def _load_emoji_font() -> ImageFont.FreeTypeFont | None:
global _emoji_font
if _emoji_font is None:
_emoji_font = ImageFont.truetype(str(FONT_DIR.joinpath("NotoColorEmoji.ttf")), 109)
return _emoji_font
def find_emoji(text): def find_emoji(text):
return [(m.start(), m.end(), m.group()) for m in EMOJI_REGEX.finditer(text)] return [(m.start(), m.end(), m.group()) for m in EMOJI_REGEX.finditer(text)]
@ -39,8 +47,7 @@ def emoji_tex(emoji):
if emoji not in _cache: if emoji not in _cache:
img = Image.new("RGBA", (128, 128), (0, 0, 0, 0)) img = Image.new("RGBA", (128, 128), (0, 0, 0, 0))
draw = ImageDraw.Draw(img) draw = ImageDraw.Draw(img)
font = ImageFont.truetype(FONT_DIR.joinpath("NotoColorEmoji.ttf"), 109) draw.text((0, 0), emoji, font=_load_emoji_font(), embedded_color=True)
draw.text((0, 0), emoji, font=font, embedded_color=True)
with io.BytesIO() as buffer: with io.BytesIO() as buffer:
img.save(buffer, format="PNG") img.save(buffer, format="PNG")
l = buffer.tell() l = buffer.tell()

View File

@ -17,6 +17,9 @@ LANGUAGES_FILE = os.path.join(TRANSLATIONS_DIR, "languages.json")
UNIFONT_LANGUAGES = [ UNIFONT_LANGUAGES = [
"ar", "ar",
"th", "th",
]
CHINA_LANGUAGES = [
"zh-CHT", "zh-CHT",
"zh-CHS", "zh-CHS",
"ko", "ko",
@ -37,6 +40,10 @@ class Multilang:
def language(self) -> str: def language(self) -> str:
return self._language return self._language
def requires_china(self) -> bool:
"""Certain languages require china to render their glyphs."""
return self._language in CHINA_LANGUAGES
def requires_unifont(self) -> bool: def requires_unifont(self) -> bool:
"""Certain languages require unifont to render their glyphs.""" """Certain languages require unifont to render their glyphs."""
return self._language in UNIFONT_LANGUAGES return self._language in UNIFONT_LANGUAGES

View File

@ -26,7 +26,7 @@ def clamp(value, min_value, max_value):
class Spinner(Widget): class Spinner(Widget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._comma_texture = gui_app.texture("../../dragonpilot/selfdrive/assets/images/spinner_comma.png", TEXTURE_SIZE, TEXTURE_SIZE) self._comma_texture = gui_app.texture("images/spinner_comma.png", TEXTURE_SIZE, TEXTURE_SIZE)
self._spinner_texture = gui_app.texture("images/spinner_track.png", TEXTURE_SIZE, TEXTURE_SIZE, alpha_premultiply=True) self._spinner_texture = gui_app.texture("images/spinner_track.png", TEXTURE_SIZE, TEXTURE_SIZE, alpha_premultiply=True)
self._rotation = 0.0 self._rotation = 0.0
self._progress: int | None = None self._progress: int | None = None

View File

@ -52,7 +52,7 @@ class HtmlElement:
font_weight: FontWeight font_weight: FontWeight
margin_top: int margin_top: int
margin_bottom: int margin_bottom: int
line_height: float = 0.9 # matches Qt visually, unsure why not default 1.2 line_height: float = 1.3 # matches Qt visually, unsure why not default 1.2
indent_level: int = 0 indent_level: int = 0

View File

@ -137,7 +137,7 @@ class Keyboard(Widget):
def _render(self, rect: rl.Rectangle): def _render(self, rect: rl.Rectangle):
rect = rl.Rectangle(rect.x + CONTENT_MARGIN, rect.y + CONTENT_MARGIN, rect.width - 2 * CONTENT_MARGIN, rect.height - 2 * CONTENT_MARGIN) rect = rl.Rectangle(rect.x + CONTENT_MARGIN, rect.y + CONTENT_MARGIN, rect.width - 2 * CONTENT_MARGIN, rect.height - 2 * CONTENT_MARGIN)
self._title.render(rl.Rectangle(rect.x, rect.y, rect.width, 95)) self._title.render(rl.Rectangle(rect.x, rect.y - 20, rect.width, 95))
self._sub_title.render(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60)) self._sub_title.render(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60))
self._cancel_button.render(rl.Rectangle(rect.x + rect.width - 386, rect.y, 386, 125)) self._cancel_button.render(rl.Rectangle(rect.x + rect.width - 386, rect.y, 386, 125))

View File

@ -336,6 +336,8 @@ class ListItem(Widget):
# do callback first in case receiver changes description # do callback first in case receiver changes description
if self.description_visible and self.description_opened_callback is not None: if self.description_visible and self.description_opened_callback is not None:
self.description_opened_callback() self.description_opened_callback()
# Call _update_state to catch any description changes
self._update_state()
content_width = int(self._rect.width - ITEM_PADDING * 2) content_width = int(self._rect.width - ITEM_PADDING * 2)
self._rect.height = self.get_item_height(self._font, content_width) self._rect.height = self.get_item_height(self._font, content_width)