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:
parent
d8075d43a0
commit
2270c6d7f1
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -4,12 +4,6 @@
|
||||
"editor.renderWhitespace": "trailing",
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"terminal.integrated.defaultProfile.linux": "dragonpilot",
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"dragonpilot": {
|
||||
"path": "bash",
|
||||
"args": ["-c", "distrobox enter dp"]
|
||||
}
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.venv": true,
|
||||
|
||||
83
1.sh
Normal file
83
1.sh
Normal 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
|
||||
@ -66,7 +66,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"IsTakingSnapshot", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
{"IsTestedBranch", {CLEAR_ON_MANAGER_START, 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}},
|
||||
{"LastGPSPosition", {PERSISTENT, 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_pq_steering_patch", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_vag_avoid_eps_lockout", {PERSISTENT, BOOL, "0"}},
|
||||
|
||||
// 驾驶员监控灵敏度
|
||||
{"DistractionDetectionLevel", {PERSISTENT, INT, "1"}},
|
||||
};
|
||||
|
||||
@ -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."),
|
||||
initial_index=int(self._params.get("dp_ui_lead") or 0),
|
||||
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):
|
||||
@ -227,7 +227,7 @@ class DragonpilotLayout(Widget):
|
||||
self._toggles["dp_ui_display_mode"] = text_spin_button_item(
|
||||
title=lambda: tr("Display Mode"),
|
||||
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),
|
||||
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."),
|
||||
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),
|
||||
options=["Std.", "Warning", "Off"],
|
||||
options=[tr("Std."), tr("Warning"), tr("Off")],
|
||||
)
|
||||
|
||||
self._toggles["dp_dev_auto_shutdown_in"] = spin_button_item(
|
||||
|
||||
234
key.py
Normal file
234
key.py
Normal 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")
|
||||
@ -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
|
||||
|
||||
@ -10,6 +10,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
|
||||
"TOYOTA_ALPHARD_TSS2" = "TOYOTA_SIENNA"
|
||||
"TOYOTA_PRIUS_V" = "TOYOTA_PRIUS"
|
||||
"TOYOTA_RAV4_PRIME" = "TOYOTA_RAV4_TSS2"
|
||||
"TOYOTA_WILDLANDER_PHEV" = "TOYOTA_RAV4_TSS2"
|
||||
"TOYOTA_SIENNA_4TH_GEN" = "TOYOTA_RAV4_TSS2"
|
||||
"LEXUS_IS" = "LEXUS_NX"
|
||||
"LEXUS_CTH" = "LEXUS_NX"
|
||||
|
||||
@ -208,6 +208,7 @@ class CarState(CarStateBase):
|
||||
self.pcm_follow_distance = cp.vl["PCM_CRUISE_2"]["PCM_FOLLOW_DISTANCE"]
|
||||
|
||||
buttonEvents = []
|
||||
prev_distance_button = self.distance_button
|
||||
if self.CP.carFingerprint in TSS2_CAR:
|
||||
# lkas button is wired to the camera
|
||||
prev_lkas_button = self.lkas_button
|
||||
@ -218,9 +219,8 @@ class CarState(CarStateBase):
|
||||
buttonEvents.extend(create_button_events(1, 0, {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)
|
||||
prev_distance_button = self.distance_button
|
||||
self.distance_button = cp_acc.vl["ACC_CONTROL"]["DISTANCE"]
|
||||
|
||||
buttonEvents += create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
|
||||
|
||||
@ -1068,6 +1068,29 @@ FW_VERSIONS = {
|
||||
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: {
|
||||
(Ecu.engine, 0x700, None): [
|
||||
b'\x01896630R58000\x00\x00\x00\x00',
|
||||
|
||||
@ -93,7 +93,7 @@ class CarInterface(CarInterfaceBase):
|
||||
# https://engage.toyota.com/static/images/toyota_safety_sense/TSS_Applicability_Chart.pdf
|
||||
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.pid.kiBP = [0.0]
|
||||
ret.lateralTuning.pid.kpBP = [0.0]
|
||||
@ -169,9 +169,7 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
ret.openpilotLongitudinalControl = ret.enableDsu or \
|
||||
candidate in (TSS2_CAR - RADAR_ACC_CAR) or \
|
||||
bool(ret.flags & ToyotaFlags.DISABLE_RADAR.value) or \
|
||||
sdsu_active or \
|
||||
dsu_bypass
|
||||
bool(ret.flags & ToyotaFlags.DISABLE_RADAR.value)
|
||||
|
||||
if dp_params & structs.DPFlags.ToyotaStockLon:
|
||||
ret.openpilotLongitudinalControl = False
|
||||
|
||||
@ -300,6 +300,10 @@ class CAR(Platforms):
|
||||
[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),
|
||||
)
|
||||
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(
|
||||
[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),
|
||||
|
||||
BIN
payload.bin
Normal file
BIN
payload.bin
Normal file
Binary file not shown.
BIN
selfdrive/assets/fonts/china.ttf
Normal file
BIN
selfdrive/assets/fonts/china.ttf
Normal file
Binary file not shown.
@ -12,7 +12,11 @@ def dmonitoringd_thread():
|
||||
pm = messaging.PubMaster(['driverMonitoringState'])
|
||||
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
|
||||
while True:
|
||||
@ -22,8 +26,10 @@ def dmonitoringd_thread():
|
||||
continue
|
||||
|
||||
valid = sm.all_checks()
|
||||
if valid:
|
||||
if DM.always_on and valid:
|
||||
DM.run_step(sm)
|
||||
## 设置分心率
|
||||
DM.set_distract_level_params()
|
||||
|
||||
# publish
|
||||
dat = DM.get_state_packet(valid=valid)
|
||||
@ -32,6 +38,7 @@ def dmonitoringd_thread():
|
||||
# load live always-on toggle
|
||||
if sm['driverStateV2'].frameId % 40 == 1:
|
||||
DM.always_on = params.get_bool("AlwaysOnDM")
|
||||
DM.distraction_detection_level = int(params.get("DistractionDetectionLevel") or 1)
|
||||
|
||||
# save rhd virtual toggle every 5 mins
|
||||
if (sm['driverStateV2'].frameId % 6000 == 0 and
|
||||
|
||||
@ -124,7 +124,7 @@ def face_orientation_from_net(angles_desc, pos_desc, rpy_calib):
|
||||
|
||||
|
||||
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:
|
||||
settings = DRIVER_MONITOR_SETTINGS()
|
||||
# init policy settings
|
||||
@ -139,6 +139,7 @@ class DriverMonitoring:
|
||||
self.ee1_calibrated = False
|
||||
|
||||
self.always_on = always_on
|
||||
self.distraction_detection_level = distraction_detection_level
|
||||
self.distracted_types = []
|
||||
self.driver_distracted = False
|
||||
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],
|
||||
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')
|
||||
|
||||
@ -148,7 +148,7 @@ EmptyAlert = Alert("" , "", AlertStatus.normal, AlertSize.none, Priority.LOWEST,
|
||||
|
||||
class NoEntryAlert(Alert):
|
||||
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):
|
||||
super().__init__(alert_text_1, alert_text_2, AlertStatus.normal,
|
||||
AlertSize.mid, Priority.LOW, visual_alert,
|
||||
@ -157,7 +157,7 @@ class NoEntryAlert(Alert):
|
||||
|
||||
class SoftDisableAlert(Alert):
|
||||
def __init__(self, alert_text_2: str):
|
||||
super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2,
|
||||
super().__init__("立即接管控制", alert_text_2,
|
||||
AlertStatus.userPrompt, AlertSize.full,
|
||||
Priority.MID, VisualAlert.steerRequired,
|
||||
AudibleAlert.warningSoft, 2.),
|
||||
@ -167,12 +167,12 @@ class SoftDisableAlert(Alert):
|
||||
class UserSoftDisableAlert(SoftDisableAlert):
|
||||
def __init__(self, alert_text_2: str):
|
||||
super().__init__(alert_text_2),
|
||||
self.alert_text_1 = "openpilot will disengage"
|
||||
self.alert_text_1 = "openpilot 将解除控制"
|
||||
|
||||
|
||||
class ImmediateDisableAlert(Alert):
|
||||
def __init__(self, alert_text_2: str):
|
||||
super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2,
|
||||
super().__init__("立即接管控制", alert_text_2,
|
||||
AlertStatus.critical, AlertSize.full,
|
||||
Priority.HIGHEST, VisualAlert.steerRequired,
|
||||
AudibleAlert.warningImmediate, 4.),
|
||||
@ -194,7 +194,7 @@ class NormalPermanentAlert(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,
|
||||
alert_status, AlertSize.mid,
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
return Alert(
|
||||
f"Steer Assist Unavailable Below {get_display_speed(CP.minSteerSpeed, metric)}",
|
||||
f"转向在 {get_display_speed(CP.minSteerSpeed, metric)} 以下不可用",
|
||||
"",
|
||||
AlertStatus.userPrompt, AlertSize.small,
|
||||
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:
|
||||
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(
|
||||
f"{first_word} in Progress: {sm['liveCalibration'].calPerc:.0f}%",
|
||||
f"Drive Above {get_display_speed(MIN_SPEED_FILTER, metric)}",
|
||||
f"{first_word}进行中:{sm['liveCalibration'].calPerc:.0f}%",
|
||||
f"请将时速提高至 {get_display_speed(MIN_SPEED_FILTER, metric)} 进行校准",
|
||||
AlertStatus.normal, AlertSize.mid,
|
||||
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:
|
||||
duration = FEEDBACK_MAX_DURATION - ((sm['audioFeedback'].blockNum + 1) * SAMPLE_BUFFER / SAMPLE_RATE)
|
||||
return NormalPermanentAlert(
|
||||
"Recording Audio Feedback",
|
||||
f"{round(duration)} second{'s' if round(duration) != 1 else ''} remaining. Press again to save early.",
|
||||
"正在录制音频反馈",
|
||||
f"剩余 {round(duration)} 秒。再次按下可提前保存。",
|
||||
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:
|
||||
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:
|
||||
mdl = sm['modelV2'].velocity.x[0] if len(sm['modelV2'].velocity.x) else math.nan
|
||||
err = CS.vEgo - mdl
|
||||
msg = f"Speed Error: {err:.1f} m/s"
|
||||
return NoEntryAlert(msg, alert_text_1="Posenet Speed Invalid")
|
||||
msg = f"速度误差: {err:.1f} 米/秒"
|
||||
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:
|
||||
not_running = [p.name for p in sm['managerState'].processes if not p.running and p.shouldBeRunning]
|
||||
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:
|
||||
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
|
||||
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:
|
||||
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, ])]
|
||||
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:
|
||||
rpy = sm['liveCalibration'].rpyCalib
|
||||
yaw = math.degrees(rpy[2] 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}°)"
|
||||
return NormalPermanentAlert("Calibration Invalid", angles)
|
||||
angles = f"请重新安装设备 (俯仰角: {pitch:.1f}°, 偏航角: {yaw:.1f}°)"
|
||||
return NormalPermanentAlert("校准无效", angles)
|
||||
|
||||
|
||||
def paramsd_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
|
||||
if not sm['liveParameters'].angleOffsetValid:
|
||||
angle_offset_deg = sm['liveParameters'].angleOffsetDeg
|
||||
title = "Steering misalignment detected"
|
||||
text = f"Angle offset too high (Offset: {angle_offset_deg:.1f}°)"
|
||||
title = "转向系统未对准"
|
||||
text = f"角度偏移过大 (偏移量: {angle_offset_deg:.1f}°)"
|
||||
elif not sm['liveParameters'].steerRatioValid:
|
||||
steer_ratio = sm['liveParameters'].steerRatio
|
||||
title = "Steer ratio mismatch"
|
||||
text = f"Steering rack geometry may be off (Ratio: {steer_ratio:.1f})"
|
||||
title = "转向传动比不匹配"
|
||||
text = f"转向齿条几何可能不正确 (传动比: {steer_ratio:.1f})"
|
||||
elif not sm['liveParameters'].stiffnessFactorValid:
|
||||
stiffness_factor = sm['liveParameters'].stiffnessFactor
|
||||
title = "Abnormal tire stiffness"
|
||||
text = f"Check tires, pressure, or alignment (Factor: {stiffness_factor:.1f})"
|
||||
title = "轮胎刚度异常"
|
||||
text = f"请检查轮胎、胎压或定位 (系数: {stiffness_factor:.1f})"
|
||||
else:
|
||||
return NoEntryAlert("paramsd Temporary Error")
|
||||
return NoEntryAlert("paramsd 临时错误")
|
||||
|
||||
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.)
|
||||
gpu = max(sm['deviceState'].gpuTempC, default=0.)
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
text = "Enable Adaptive Cruise to Engage"
|
||||
text = "启用自适应巡航以接合"
|
||||
if CP.brand == "honda":
|
||||
text = "Enable Main Switch to Engage"
|
||||
text = "启用主开关以接合"
|
||||
return NoEntryAlert(text)
|
||||
|
||||
|
||||
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.
|
||||
steer = sm['carControl'].actuators.torque
|
||||
vals = f"Gas: {round(gb * 100.)}%, Steer: {round(steer * 100.)}%"
|
||||
return NormalPermanentAlert("Joystick Mode", vals)
|
||||
vals = f"油门: {round(gb * 100.)}%, 转向: {round(steer * 100.)}%"
|
||||
return NormalPermanentAlert("操纵杆模式", vals)
|
||||
|
||||
|
||||
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:
|
||||
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:
|
||||
text = "Toggle stock LKAS on or off to engage"
|
||||
text = "请切换原厂LKAS状态以启用"
|
||||
if CP.brand == "tesla":
|
||||
text = "Switch to Traffic-Aware Cruise Control to engage"
|
||||
text = "请切换到交通感知巡航控制以启用"
|
||||
elif CP.brand == "mazda":
|
||||
text = "Enable your car's LKAS to engage"
|
||||
text = "请启用您的车辆LKAS以启用"
|
||||
elif CP.brand == "nissan":
|
||||
text = "Disable your car's stock LKAS to engage"
|
||||
return NormalPermanentAlert("Invalid LKAS setting", text)
|
||||
text = "请禁用您的车辆原厂LKAS以启用"
|
||||
return NormalPermanentAlert("无效的LKAS设置", text)
|
||||
|
||||
|
||||
|
||||
@ -392,21 +399,21 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
|
||||
EventName.joystickDebug: {
|
||||
ET.WARNING: joystick_alert,
|
||||
ET.PERMANENT: NormalPermanentAlert("Joystick Mode"),
|
||||
ET.PERMANENT: NormalPermanentAlert("操纵杆模式"),
|
||||
},
|
||||
|
||||
EventName.longitudinalManeuver: {
|
||||
ET.WARNING: longitudinal_maneuver_alert,
|
||||
ET.PERMANENT: NormalPermanentAlert("Longitudinal Maneuver Mode",
|
||||
"Ensure road ahead is clear"),
|
||||
ET.PERMANENT: NormalPermanentAlert("纵向操作模式",
|
||||
"确保前方道路畅通"),
|
||||
},
|
||||
|
||||
EventName.selfdriveInitializing: {
|
||||
ET.NO_ENTRY: NoEntryAlert("System Initializing"),
|
||||
ET.NO_ENTRY: NoEntryAlert("系统初始化中"),
|
||||
},
|
||||
|
||||
EventName.startup: {
|
||||
ET.PERMANENT: StartupAlert("Be ready to take over at any time")
|
||||
ET.PERMANENT: StartupAlert("请随时准备接管您的车辆控制权")
|
||||
},
|
||||
|
||||
EventName.startupMaster: {
|
||||
@ -414,28 +421,28 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
},
|
||||
|
||||
EventName.startupNoControl: {
|
||||
ET.PERMANENT: StartupAlert("Dashcam mode"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Dashcam mode"),
|
||||
ET.PERMANENT: StartupAlert("仅行车记录仪模式"),
|
||||
ET.NO_ENTRY: NoEntryAlert("仅行车记录仪模式"),
|
||||
},
|
||||
|
||||
EventName.startupNoCar: {
|
||||
ET.PERMANENT: StartupAlert("Dashcam mode for unsupported car"),
|
||||
ET.PERMANENT: StartupAlert("不支持车辆的行车记录仪模式"),
|
||||
},
|
||||
|
||||
EventName.startupNoSecOcKey: {
|
||||
ET.PERMANENT: NormalPermanentAlert("Dashcam Mode",
|
||||
"Security Key Not Available",
|
||||
ET.PERMANENT: NormalPermanentAlert("仅行车记录仪模式",
|
||||
"安全密钥不可用",
|
||||
priority=Priority.HIGH),
|
||||
},
|
||||
|
||||
EventName.dashcamMode: {
|
||||
ET.PERMANENT: NormalPermanentAlert("Dashcam Mode",
|
||||
ET.PERMANENT: NormalPermanentAlert("行车记录仪模式",
|
||||
priority=Priority.LOWEST),
|
||||
},
|
||||
|
||||
EventName.invalidLkasSetting: {
|
||||
ET.PERMANENT: invalid_lkas_setting_alert,
|
||||
ET.NO_ENTRY: NoEntryAlert("Invalid LKAS setting"),
|
||||
ET.NO_ENTRY: NoEntryAlert("车道保持辅助系统(LKAS)设置无效"),
|
||||
},
|
||||
|
||||
EventName.cruiseMismatch: {
|
||||
@ -446,40 +453,40 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
# read-only mode. This can be solved by adding your fingerprint.
|
||||
# See https://github.com/commaai/openpilot/wiki/Fingerprinting for more information
|
||||
EventName.carUnrecognized: {
|
||||
ET.PERMANENT: NormalPermanentAlert("Dashcam Mode",
|
||||
"Car Unrecognized",
|
||||
ET.PERMANENT: NormalPermanentAlert("行车记录仪模式",
|
||||
"车辆未识别",
|
||||
priority=Priority.LOWEST),
|
||||
},
|
||||
|
||||
EventName.aeb: {
|
||||
ET.PERMANENT: Alert(
|
||||
"BRAKE!",
|
||||
"Emergency Braking: Risk of Collision",
|
||||
"刹车!",
|
||||
"紧急制动:可能发生碰撞",
|
||||
AlertStatus.critical, AlertSize.full,
|
||||
Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.none, 2.),
|
||||
ET.NO_ENTRY: NoEntryAlert("AEB: Risk of Collision"),
|
||||
ET.NO_ENTRY: NoEntryAlert("AEB:可能发生碰撞"),
|
||||
},
|
||||
|
||||
EventName.stockAeb: {
|
||||
ET.PERMANENT: Alert(
|
||||
"BRAKE!",
|
||||
"Stock AEB: Risk of Collision",
|
||||
"刹车!",
|
||||
"原厂AEB:可能发生碰撞",
|
||||
AlertStatus.critical, AlertSize.full,
|
||||
Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.none, 2.),
|
||||
ET.NO_ENTRY: NoEntryAlert("Stock AEB: Risk of Collision"),
|
||||
ET.NO_ENTRY: NoEntryAlert("原厂AEB:可能发生碰撞"),
|
||||
},
|
||||
|
||||
EventName.fcw: {
|
||||
ET.PERMANENT: Alert(
|
||||
"BRAKE!",
|
||||
"Risk of Collision",
|
||||
"刹车!",
|
||||
"可能发生碰撞",
|
||||
AlertStatus.critical, AlertSize.full,
|
||||
Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.warningSoft, 2.),
|
||||
},
|
||||
|
||||
EventName.ldw: {
|
||||
ET.PERMANENT: Alert(
|
||||
"Lane Departure Detected",
|
||||
"监测偏离车道",
|
||||
"",
|
||||
AlertStatus.userPrompt, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.ldw, AudibleAlert.prompt, 3.),
|
||||
@ -489,7 +496,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
|
||||
EventName.steerTempUnavailableSilent: {
|
||||
ET.WARNING: Alert(
|
||||
"Steering Assist Temporarily Unavailable",
|
||||
"转向暂时不可用",
|
||||
"",
|
||||
AlertStatus.userPrompt, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.prompt, 1.8),
|
||||
@ -497,7 +504,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
|
||||
EventName.preDriverDistracted: {
|
||||
ET.PERMANENT: Alert(
|
||||
"Pay Attention",
|
||||
"请注意",
|
||||
"",
|
||||
AlertStatus.normal, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1),
|
||||
@ -505,23 +512,23 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
|
||||
EventName.promptDriverDistracted: {
|
||||
ET.PERMANENT: Alert(
|
||||
"Pay Attention",
|
||||
"Driver Distracted",
|
||||
"请注意",
|
||||
"驾驶员分心",
|
||||
AlertStatus.userPrompt, AlertSize.mid,
|
||||
Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, .1),
|
||||
},
|
||||
|
||||
EventName.driverDistracted: {
|
||||
ET.PERMANENT: Alert(
|
||||
"DISENGAGE IMMEDIATELY",
|
||||
"Driver Distracted",
|
||||
"立即解除控制",
|
||||
"驾驶员分心",
|
||||
AlertStatus.critical, AlertSize.full,
|
||||
Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.warningImmediate, .1),
|
||||
},
|
||||
|
||||
EventName.preDriverUnresponsive: {
|
||||
ET.PERMANENT: Alert(
|
||||
"Touch Steering Wheel: No Face Detected",
|
||||
"触摸方向盘:未检测到面部",
|
||||
"",
|
||||
AlertStatus.normal, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.none, .1),
|
||||
@ -529,31 +536,31 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
|
||||
EventName.promptDriverUnresponsive: {
|
||||
ET.PERMANENT: Alert(
|
||||
"Touch Steering Wheel",
|
||||
"Driver Unresponsive",
|
||||
"触摸方向盘",
|
||||
"驾驶员无响应",
|
||||
AlertStatus.userPrompt, AlertSize.mid,
|
||||
Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, .1),
|
||||
},
|
||||
|
||||
EventName.driverUnresponsive: {
|
||||
ET.PERMANENT: Alert(
|
||||
"DISENGAGE IMMEDIATELY",
|
||||
"Driver Unresponsive",
|
||||
"立即解除控制",
|
||||
"驾驶员无响应",
|
||||
AlertStatus.critical, AlertSize.full,
|
||||
Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.warningImmediate, .1),
|
||||
},
|
||||
|
||||
EventName.manualRestart: {
|
||||
ET.WARNING: Alert(
|
||||
"TAKE CONTROL",
|
||||
"Resume Driving Manually",
|
||||
"接管控制",
|
||||
"请手动继续驾驶",
|
||||
AlertStatus.userPrompt, AlertSize.mid,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.none, .2),
|
||||
},
|
||||
|
||||
EventName.resumeRequired: {
|
||||
ET.WARNING: Alert(
|
||||
"Press Resume to Exit Standstill",
|
||||
"按恢复键以解除停止状态",
|
||||
"",
|
||||
AlertStatus.userPrompt, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.none, .2),
|
||||
@ -565,7 +572,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
|
||||
EventName.preLaneChangeLeft: {
|
||||
ET.WARNING: Alert(
|
||||
"Steer Left to Start Lane Change Once Safe",
|
||||
"请确认安全后进行左转变道",
|
||||
"",
|
||||
AlertStatus.normal, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1),
|
||||
@ -573,7 +580,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
|
||||
EventName.preLaneChangeRight: {
|
||||
ET.WARNING: Alert(
|
||||
"Steer Right to Start Lane Change Once Safe",
|
||||
"请确认安全后进行右转变道",
|
||||
"",
|
||||
AlertStatus.normal, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1),
|
||||
@ -581,7 +588,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
|
||||
EventName.laneChangeBlocked: {
|
||||
ET.WARNING: Alert(
|
||||
"Car Detected in Blindspot",
|
||||
"盲点检测到车辆",
|
||||
"",
|
||||
AlertStatus.userPrompt, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, .1),
|
||||
@ -589,7 +596,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
|
||||
EventName.laneChange: {
|
||||
ET.WARNING: Alert(
|
||||
"Changing Lanes",
|
||||
"正在变道",
|
||||
"",
|
||||
AlertStatus.normal, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1),
|
||||
@ -597,41 +604,41 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
|
||||
EventName.steerSaturated: {
|
||||
ET.WARNING: Alert(
|
||||
"Take Control",
|
||||
"Turn Exceeds Steering Limit",
|
||||
"请接管控制",
|
||||
"转向超出限制",
|
||||
AlertStatus.userPrompt, AlertSize.mid,
|
||||
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 2.),
|
||||
},
|
||||
|
||||
# Thrown when the fan is driven at >50% but is not rotating
|
||||
EventName.fanMalfunction: {
|
||||
ET.PERMANENT: NormalPermanentAlert("Fan Malfunction", "Likely Hardware Issue"),
|
||||
ET.PERMANENT: NormalPermanentAlert("风扇故障", "可能是硬件问题"),
|
||||
},
|
||||
|
||||
# Camera is not outputting frames
|
||||
EventName.cameraMalfunction: {
|
||||
ET.PERMANENT: camera_malfunction_alert,
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Camera Malfunction"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Camera Malfunction: Reboot Your Device"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("摄像头故障"),
|
||||
ET.NO_ENTRY: NoEntryAlert("摄像头故障:请重启设备"),
|
||||
},
|
||||
# Camera framerate too low
|
||||
EventName.cameraFrameRate: {
|
||||
ET.PERMANENT: NormalPermanentAlert("Camera Frame Rate Low", "Reboot your Device"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Camera Frame Rate Low"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Camera Frame Rate Low: Reboot Your Device"),
|
||||
ET.PERMANENT: NormalPermanentAlert("摄像头帧率低", "请重启设备"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("摄像头帧率低"),
|
||||
ET.NO_ENTRY: NoEntryAlert("摄像头帧率低:请重启设备"),
|
||||
},
|
||||
|
||||
# Unused
|
||||
|
||||
EventName.locationdTemporaryError: {
|
||||
ET.NO_ENTRY: NoEntryAlert("locationd Temporary Error"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("locationd Temporary Error"),
|
||||
ET.NO_ENTRY: NoEntryAlert("locationd临时错误"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("locationd临时错误"),
|
||||
},
|
||||
|
||||
EventName.locationdPermanentError: {
|
||||
ET.NO_ENTRY: NoEntryAlert("locationd Permanent Error"),
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("locationd Permanent Error"),
|
||||
ET.PERMANENT: NormalPermanentAlert("locationd Permanent Error"),
|
||||
ET.NO_ENTRY: NoEntryAlert("locationd永久错误"),
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("locationd永久错误"),
|
||||
ET.PERMANENT: NormalPermanentAlert("locationd永久错误"),
|
||||
},
|
||||
|
||||
# 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
|
||||
EventName.paramsdTemporaryError: {
|
||||
ET.NO_ENTRY: paramsd_invalid_alert,
|
||||
ET.SOFT_DISABLE: soft_disable_alert("paramsd Temporary Error"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("paramsd 临时错误"),
|
||||
},
|
||||
|
||||
EventName.paramsdPermanentError: {
|
||||
ET.NO_ENTRY: NoEntryAlert("paramsd Permanent Error"),
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("paramsd Permanent Error"),
|
||||
ET.PERMANENT: NormalPermanentAlert("paramsd Permanent Error"),
|
||||
ET.NO_ENTRY: NoEntryAlert("paramsd永久错误"),
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("paramsd永久错误"),
|
||||
ET.PERMANENT: NormalPermanentAlert("paramsd永久错误"),
|
||||
},
|
||||
|
||||
# ********** events that affect controls state transitions **********
|
||||
@ -669,12 +676,12 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
|
||||
EventName.buttonCancel: {
|
||||
ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage),
|
||||
ET.NO_ENTRY: NoEntryAlert("Cancel Pressed"),
|
||||
ET.NO_ENTRY: NoEntryAlert("取消按钮被按下"),
|
||||
},
|
||||
|
||||
EventName.brakeHold: {
|
||||
ET.WARNING: Alert(
|
||||
"Press Resume to Exit Brake Hold",
|
||||
"按恢复键以解除制动保持",
|
||||
"",
|
||||
AlertStatus.userPrompt, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.none, .2),
|
||||
@ -682,23 +689,23 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
|
||||
EventName.parkBrake: {
|
||||
ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage),
|
||||
ET.NO_ENTRY: NoEntryAlert("Parking Brake Engaged"),
|
||||
ET.NO_ENTRY: NoEntryAlert("停车制动已启用"),
|
||||
},
|
||||
|
||||
EventName.pedalPressed: {
|
||||
ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage),
|
||||
ET.NO_ENTRY: NoEntryAlert("Pedal Pressed",
|
||||
ET.NO_ENTRY: NoEntryAlert("踏板被按下",
|
||||
visual_alert=VisualAlert.brakePressed),
|
||||
},
|
||||
|
||||
EventName.steerDisengage: {
|
||||
ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage),
|
||||
ET.NO_ENTRY: NoEntryAlert("Steering Pressed"),
|
||||
ET.NO_ENTRY: NoEntryAlert("方向盘被转动"),
|
||||
},
|
||||
|
||||
EventName.preEnableStandstill: {
|
||||
ET.PRE_ENABLE: Alert(
|
||||
"Release Brake to Engage",
|
||||
"释放制动以启用",
|
||||
"",
|
||||
AlertStatus.normal, AlertSize.small,
|
||||
Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .1, creation_delay=1.),
|
||||
@ -726,27 +733,27 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
},
|
||||
|
||||
EventName.resumeBlocked: {
|
||||
ET.NO_ENTRY: NoEntryAlert("Press Set to Engage"),
|
||||
ET.NO_ENTRY: NoEntryAlert("请按设定键以启用"),
|
||||
},
|
||||
|
||||
EventName.wrongCruiseMode: {
|
||||
ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage),
|
||||
ET.NO_ENTRY: NoEntryAlert("Adaptive Cruise Disabled"),
|
||||
ET.NO_ENTRY: NoEntryAlert("自适应巡航已禁用"),
|
||||
},
|
||||
|
||||
EventName.steerTempUnavailable: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Steering Assist Temporarily Unavailable"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Steering Temporarily Unavailable"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("转向暂时不可用"),
|
||||
ET.NO_ENTRY: NoEntryAlert("转向暂时不可用"),
|
||||
},
|
||||
|
||||
EventName.steerTimeLimit: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Vehicle Steering Time Limit"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Vehicle Steering Time Limit"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("车辆转向时间限制"),
|
||||
ET.NO_ENTRY: NoEntryAlert("车辆转向时间限制"),
|
||||
},
|
||||
|
||||
EventName.outOfSpace: {
|
||||
ET.PERMANENT: out_of_space_alert,
|
||||
ET.NO_ENTRY: NoEntryAlert("Out of Storage"),
|
||||
ET.NO_ENTRY: NoEntryAlert("出库"),
|
||||
},
|
||||
|
||||
EventName.belowEngageSpeed: {
|
||||
@ -755,35 +762,35 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
|
||||
EventName.sensorDataInvalid: {
|
||||
ET.PERMANENT: Alert(
|
||||
"Sensor Data Invalid",
|
||||
"Possible Hardware Issue",
|
||||
"传感器数据无效",
|
||||
"可能是硬件问题",
|
||||
AlertStatus.normal, AlertSize.mid,
|
||||
Priority.LOWER, VisualAlert.none, AudibleAlert.none, .2, creation_delay=1.),
|
||||
ET.NO_ENTRY: NoEntryAlert("Sensor Data Invalid"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Sensor Data Invalid"),
|
||||
ET.NO_ENTRY: NoEntryAlert("传感器数据无效"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("传感器数据无效"),
|
||||
},
|
||||
|
||||
EventName.noGps: {
|
||||
},
|
||||
|
||||
EventName.tooDistracted: {
|
||||
ET.NO_ENTRY: NoEntryAlert("Distraction Level Too High"),
|
||||
ET.NO_ENTRY: NoEntryAlert("注意力分散程度过高"),
|
||||
},
|
||||
|
||||
EventName.excessiveActuation: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Excessive Actuation"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Excessive Actuation"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("过度操作"),
|
||||
ET.NO_ENTRY: NoEntryAlert("过度操作"),
|
||||
},
|
||||
|
||||
EventName.overheat: {
|
||||
ET.PERMANENT: overheat_alert,
|
||||
ET.SOFT_DISABLE: soft_disable_alert("System Overheated"),
|
||||
ET.NO_ENTRY: NoEntryAlert("System Overheated"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("系统过热"),
|
||||
ET.NO_ENTRY: NoEntryAlert("系统过热"),
|
||||
},
|
||||
|
||||
EventName.wrongGear: {
|
||||
ET.SOFT_DISABLE: user_soft_disable_alert("Gear not D"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Gear not D"),
|
||||
ET.SOFT_DISABLE: user_soft_disable_alert("挡位不在D挡"),
|
||||
ET.NO_ENTRY: NoEntryAlert("挡位不在D挡"),
|
||||
},
|
||||
|
||||
# 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
|
||||
EventName.calibrationInvalid: {
|
||||
ET.PERMANENT: calibration_invalid_alert,
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Calibration Invalid: Remount Device & Recalibrate"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Calibration Invalid: Remount Device & Recalibrate"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("校准无效:重新安装设备并重新校准"),
|
||||
ET.NO_ENTRY: NoEntryAlert("校准无效:重新安装设备并重新校准"),
|
||||
},
|
||||
|
||||
EventName.calibrationIncomplete: {
|
||||
ET.PERMANENT: calibration_incomplete_alert,
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Calibration Incomplete"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Calibration in Progress"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("校准未完成"),
|
||||
ET.NO_ENTRY: NoEntryAlert("校准进行中"),
|
||||
},
|
||||
|
||||
EventName.calibrationRecalibrating: {
|
||||
ET.PERMANENT: calibration_incomplete_alert,
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Device Remount Detected: Recalibrating"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Remount Detected: Recalibrating"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("设备重新安装检测到:重新校准中"),
|
||||
ET.NO_ENTRY: NoEntryAlert("设备重新安装检测到:重新校准中"),
|
||||
},
|
||||
|
||||
EventName.doorOpen: {
|
||||
ET.SOFT_DISABLE: user_soft_disable_alert("Door Open"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Door Open"),
|
||||
ET.SOFT_DISABLE: user_soft_disable_alert("车门开启"),
|
||||
ET.NO_ENTRY: NoEntryAlert("车门开启"),
|
||||
},
|
||||
|
||||
EventName.seatbeltNotLatched: {
|
||||
ET.SOFT_DISABLE: user_soft_disable_alert("Seatbelt Unlatched"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Seatbelt Unlatched"),
|
||||
ET.SOFT_DISABLE: user_soft_disable_alert("安全带未系"),
|
||||
ET.NO_ENTRY: NoEntryAlert("安全带未系"),
|
||||
},
|
||||
|
||||
EventName.espDisabled: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Electronic Stability Control Disabled"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Electronic Stability Control Disabled"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("电子稳定控制系统已禁用"),
|
||||
ET.NO_ENTRY: NoEntryAlert("电子稳定控制系统已禁用"),
|
||||
},
|
||||
|
||||
EventName.lowBattery: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Low Battery"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Low Battery"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("电池电量低"),
|
||||
ET.NO_ENTRY: NoEntryAlert("电池电量低"),
|
||||
},
|
||||
|
||||
# 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
|
||||
# ten times the regular interval, or the average interval is more than 10% too high.
|
||||
EventName.commIssue: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Communication Issue Between Processes"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("进程间通信问题"),
|
||||
ET.NO_ENTRY: comm_issue_alert,
|
||||
},
|
||||
EventName.commIssueAvgFreq: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Low Communication Rate Between Processes"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Low Communication Rate Between Processes"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("进程间通信速率低"),
|
||||
ET.NO_ENTRY: NoEntryAlert("进程间通信速率低"),
|
||||
},
|
||||
|
||||
EventName.selfdrivedLagging: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("System Lagging"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Selfdrive Process Lagging: Reboot Your Device"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("系统滞后"),
|
||||
ET.NO_ENTRY: NoEntryAlert("自驾车进程滞后:请重启设备"),
|
||||
},
|
||||
|
||||
# Thrown when manager detects a service exited unexpectedly while driving
|
||||
EventName.processNotRunning: {
|
||||
ET.NO_ENTRY: process_not_running_alert,
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Process Not Running"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("进程未运行"),
|
||||
},
|
||||
|
||||
EventName.radarFault: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Radar Error: Restart the Car"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Radar Error: Restart the Car"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("雷达错误:请重启车辆"),
|
||||
ET.NO_ENTRY: NoEntryAlert("雷达错误:请重启车辆"),
|
||||
},
|
||||
|
||||
EventName.radarTempUnavailable: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Radar Temporarily Unavailable"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Radar Temporarily Unavailable"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("雷达暂时不可用"),
|
||||
ET.NO_ENTRY: NoEntryAlert("雷达暂时不可用"),
|
||||
},
|
||||
|
||||
# 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
|
||||
# thrown when over 20% of frames are dropped.
|
||||
EventName.modeldLagging: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Driving Model Lagging"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Driving Model Lagging"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("驾驶模型滞后"),
|
||||
ET.NO_ENTRY: NoEntryAlert("驾驶模型滞后"),
|
||||
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
|
||||
# as a heuristic to warn the driver.
|
||||
EventName.posenetInvalid: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Posenet Speed Invalid"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Posenet速度无效"),
|
||||
ET.NO_ENTRY: posenet_invalid_alert,
|
||||
},
|
||||
|
||||
# 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.
|
||||
EventName.deviceFalling: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Device Fell Off Mount"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Device Fell Off Mount"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("设备从支架掉落"),
|
||||
ET.NO_ENTRY: NoEntryAlert("设备从支架掉落"),
|
||||
},
|
||||
|
||||
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.NO_ENTRY: NoEntryAlert("Low Memory: Reboot Your Device"),
|
||||
ET.NO_ENTRY: NoEntryAlert("内存不足:请重启设备"),
|
||||
},
|
||||
|
||||
EventName.accFaulted: {
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Cruise Fault: Restart the Car"),
|
||||
ET.PERMANENT: NormalPermanentAlert("Cruise Fault: Restart the car to engage"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Cruise Fault: Restart the Car"),
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("巡航故障:请重启车辆"),
|
||||
ET.PERMANENT: NormalPermanentAlert("巡航故障:重启车辆以启用"),
|
||||
ET.NO_ENTRY: NoEntryAlert("巡航故障:请重启车辆"),
|
||||
},
|
||||
|
||||
EventName.espActive: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Electronic Stability Control Active"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Electronic Stability Control Active"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("电子稳定控制系统激活中"),
|
||||
ET.NO_ENTRY: NoEntryAlert("电子稳定控制系统激活中"),
|
||||
},
|
||||
|
||||
EventName.controlsMismatch: {
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Controls Mismatch"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Controls Mismatch"),
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("控制不匹配"),
|
||||
ET.NO_ENTRY: NoEntryAlert("控制不匹配"),
|
||||
},
|
||||
|
||||
# Sometimes the USB stack on the device can get into a bad state
|
||||
# causing the connection to the panda to be lost
|
||||
EventName.usbError: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("USB Error: Reboot Your Device"),
|
||||
ET.PERMANENT: NormalPermanentAlert("USB Error: Reboot Your Device"),
|
||||
ET.NO_ENTRY: NoEntryAlert("USB Error: Reboot Your Device"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("USB错误:请重启设备"),
|
||||
ET.PERMANENT: NormalPermanentAlert("USB错误:请重启设备"),
|
||||
ET.NO_ENTRY: NoEntryAlert("USB错误:请重启设备"),
|
||||
},
|
||||
|
||||
# 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
|
||||
# If you're not writing a new car port, this is usually cause by faulty wiring
|
||||
EventName.canError: {
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("CAN Error"),
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("CAN总线错误"),
|
||||
ET.PERMANENT: Alert(
|
||||
"CAN Error: Check Connections",
|
||||
"CAN总线错误:请检查连接",
|
||||
"",
|
||||
AlertStatus.normal, AlertSize.small,
|
||||
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: {
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("CAN Bus Disconnected"),
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("CAN总线断开连接"),
|
||||
ET.PERMANENT: Alert(
|
||||
"CAN Bus Disconnected: Likely Faulty Cable",
|
||||
"CAN总线断开连接:可能电缆故障",
|
||||
"",
|
||||
AlertStatus.normal, AlertSize.small,
|
||||
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: {
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("LKAS Fault: Restart the Car"),
|
||||
ET.PERMANENT: NormalPermanentAlert("LKAS Fault: Restart the car to engage"),
|
||||
ET.NO_ENTRY: NoEntryAlert("LKAS Fault: Restart the Car"),
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("LKAS故障:请重启车辆"),
|
||||
ET.PERMANENT: NormalPermanentAlert("LKAS故障:重启车辆以启用"),
|
||||
ET.NO_ENTRY: NoEntryAlert("LKAS故障:请重启车辆"),
|
||||
},
|
||||
|
||||
EventName.reverseGear: {
|
||||
ET.PERMANENT: Alert(
|
||||
"Reverse\nGear",
|
||||
"倒车中",
|
||||
"",
|
||||
AlertStatus.normal, AlertSize.full,
|
||||
Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5),
|
||||
ET.USER_DISABLE: ImmediateDisableAlert("Reverse Gear"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Reverse Gear"),
|
||||
ET.USER_DISABLE: ImmediateDisableAlert("倒档"),
|
||||
ET.NO_ENTRY: NoEntryAlert("倒档"),
|
||||
},
|
||||
|
||||
# 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.
|
||||
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
|
||||
@ -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
|
||||
# and this alert is thrown.
|
||||
EventName.relayMalfunction: {
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Harness Relay Malfunction"),
|
||||
ET.PERMANENT: NormalPermanentAlert("Harness Relay Malfunction", "Check Hardware"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Harness Relay Malfunction"),
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("线束继电器故障"),
|
||||
ET.PERMANENT: NormalPermanentAlert("线束继电器故障", "检查硬件"),
|
||||
ET.NO_ENTRY: NoEntryAlert("线束继电器故障"),
|
||||
},
|
||||
|
||||
EventName.speedTooLow: {
|
||||
ET.IMMEDIATE_DISABLE: Alert(
|
||||
"openpilot Canceled",
|
||||
"Speed too low",
|
||||
"openpilot已取消",
|
||||
"速度过低",
|
||||
AlertStatus.normal, AlertSize.mid,
|
||||
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.
|
||||
EventName.speedTooHigh: {
|
||||
ET.WARNING: Alert(
|
||||
"Speed Too High",
|
||||
"Model uncertain at this speed",
|
||||
"速度过高",
|
||||
"在此速度下模型不稳定",
|
||||
AlertStatus.userPrompt, AlertSize.mid,
|
||||
Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 4.),
|
||||
ET.NO_ENTRY: NoEntryAlert("Slow down to engage"),
|
||||
ET.NO_ENTRY: NoEntryAlert("减速以进行接合"),
|
||||
},
|
||||
|
||||
EventName.vehicleSensorsInvalid: {
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Vehicle Sensors Invalid"),
|
||||
ET.PERMANENT: NormalPermanentAlert("Vehicle Sensors Calibrating", "Drive to Calibrate"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Vehicle Sensors Calibrating"),
|
||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("车辆传感器无效"),
|
||||
ET.PERMANENT: NormalPermanentAlert("车辆传感器校准中", "行驶以校准"),
|
||||
ET.NO_ENTRY: NoEntryAlert("车辆传感器校准中"),
|
||||
},
|
||||
|
||||
EventName.personalityChanged: {
|
||||
@ -1004,7 +1011,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
},
|
||||
|
||||
EventName.userBookmark: {
|
||||
ET.PERMANENT: NormalPermanentAlert("Bookmark Saved", duration=1.5),
|
||||
ET.PERMANENT: NormalPermanentAlert("书签已保存", duration=1.5),
|
||||
},
|
||||
|
||||
EventName.audioFeedback: {
|
||||
|
||||
@ -43,7 +43,7 @@ class DeveloperLayout(Widget):
|
||||
# Build items and keep references for callbacks/state updates
|
||||
self._adb_toggle = toggle_item(
|
||||
lambda: tr("Enable ADB"),
|
||||
description=lambda: DESCRIPTIONS["enable_adb"],
|
||||
description=lambda: tr(DESCRIPTIONS["enable_adb"]),
|
||||
initial_state=self._params.get_bool("AdbEnabled"),
|
||||
callback=self._on_enable_adb,
|
||||
enabled=ui_state.is_offroad,
|
||||
@ -56,7 +56,7 @@ class DeveloperLayout(Widget):
|
||||
initial_state=self._params.get_bool("SshEnabled"),
|
||||
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(
|
||||
lambda: tr("Joystick Debug Mode"),
|
||||
@ -75,7 +75,7 @@ class DeveloperLayout(Widget):
|
||||
|
||||
self._alpha_long_toggle = toggle_item(
|
||||
lambda: tr("openpilot Longitudinal Control (Alpha)"),
|
||||
description=lambda: DESCRIPTIONS["alpha_longitudinal"],
|
||||
description=lambda: tr(DESCRIPTIONS["alpha_longitudinal"]),
|
||||
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
|
||||
callback=self._on_alpha_long_enabled,
|
||||
enabled=lambda: not ui_state.engaged,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import os
|
||||
import re
|
||||
import math
|
||||
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.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
|
||||
# Description constants
|
||||
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."),
|
||||
'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):
|
||||
@ -46,6 +50,9 @@ class DeviceLayout(Widget):
|
||||
self._dp_vehicle_selector_make_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()
|
||||
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("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))),
|
||||
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']),
|
||||
callback=self._show_driver_camera, enabled=ui_state.is_offroad and "LITE" not in os.environ),
|
||||
self._reset_calib_btn,
|
||||
@ -101,7 +110,7 @@ class DeviceLayout(Widget):
|
||||
self._select_language_dialog = None
|
||||
|
||||
self._select_language_dialog = MultiOptionDialog(tr("Select a language"), multilang.languages, multilang.codes[multilang.language],
|
||||
option_font_weight=FontWeight.UNIFONT)
|
||||
option_font_weight=FontWeight.CHINA)
|
||||
gui_app.set_modal_overlay(self._select_language_dialog, callback=handle_language_selection)
|
||||
|
||||
def _show_driver_camera(self):
|
||||
@ -299,3 +308,87 @@ class DeviceLayout(Widget):
|
||||
dialog = ConfirmDialog(tr("Are you sure you want to switch?"), tr("CONFIRM"))
|
||||
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)
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
|
||||
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.toggles import TogglesLayout
|
||||
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.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout()),
|
||||
PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout()),
|
||||
PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout()),
|
||||
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)
|
||||
|
||||
@ -68,7 +68,7 @@ class SoftwareLayout(Widget):
|
||||
self._version_item,
|
||||
self._download_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),
|
||||
], line_separator=True, spacing=0)
|
||||
|
||||
|
||||
@ -28,7 +28,14 @@ DESCRIPTIONS = {
|
||||
"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)."
|
||||
),
|
||||
"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."),
|
||||
"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."),
|
||||
@ -109,7 +116,7 @@ class TogglesLayout(Widget):
|
||||
|
||||
self._long_personality_setting = multiple_button_item(
|
||||
lambda: tr("Driving Personality"),
|
||||
DESCRIPTIONS["LongitudinalPersonality"],
|
||||
tr(DESCRIPTIONS["LongitudinalPersonality"]),
|
||||
buttons=[lambda: tr("Aggressive"), lambda: tr("Standard"), lambda: tr("Relaxed")],
|
||||
button_width=255,
|
||||
callback=self._set_longitudinal_personality,
|
||||
@ -117,6 +124,16 @@ class TogglesLayout(Widget):
|
||||
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._locked_toggles = set()
|
||||
|
||||
@ -156,6 +173,12 @@ class TogglesLayout(Widget):
|
||||
if param == "DisengageOnAccelerator":
|
||||
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._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0)
|
||||
|
||||
@ -228,6 +251,13 @@ class TogglesLayout(Widget):
|
||||
def _render(self, 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):
|
||||
icon = "experimental.png" if self._toggles["ExperimentalMode"].action_item.get_state() else "experimental_white.png"
|
||||
self._toggles["ExperimentalMode"].set_icon(icon)
|
||||
@ -261,5 +291,12 @@ class TogglesLayout(Widget):
|
||||
if self._toggle_defs[param][3]:
|
||||
self._params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
# 如果切换的是AlwaysOnDM,更新分心检测级别的可见性
|
||||
if param == "AlwaysOnDM":
|
||||
self._update_distraction_detection_visibility()
|
||||
|
||||
def _set_longitudinal_personality(self, button_index: int):
|
||||
self._params.put("LongitudinalPersonality", button_index)
|
||||
|
||||
def _set_distraction_detection_level(self, button_index: int):
|
||||
self._params.put("DistractionDetectionLevel", button_index)
|
||||
|
||||
@ -68,9 +68,9 @@ class Sidebar(Widget):
|
||||
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
||||
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._connect_status = MetricData(tr_noop("CONNECT"), tr_noop("OFFLINE"), Colors.WARNING)
|
||||
self._recording_audio = False
|
||||
|
||||
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._update_network_status(device_state)
|
||||
self._update_temperature_status(device_state)
|
||||
if not ui_state.dp_dev_disable_connect:
|
||||
self._update_connection_status(device_state)
|
||||
self._update_cpu_status(device_state)
|
||||
self._update_panda_status()
|
||||
|
||||
def _update_network_status(self, device_state):
|
||||
@ -121,26 +120,28 @@ class Sidebar(Widget):
|
||||
|
||||
def _update_temperature_status(self, device_state):
|
||||
thermal_status = device_state.thermalStatus
|
||||
max_temp = device_state.maxTempC
|
||||
|
||||
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:
|
||||
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:
|
||||
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):
|
||||
last_ping = device_state.lastAthenaPingTime
|
||||
if last_ping == 0:
|
||||
self._connect_status.update(tr_noop("CONNECT"), tr_noop("OFFLINE"), Colors.WARNING)
|
||||
elif time.monotonic_ns() - last_ping < 80_000_000_000: # 80 seconds in nanoseconds
|
||||
self._connect_status.update(tr_noop("CONNECT"), tr_noop("ONLINE"), Colors.GOOD)
|
||||
def _update_cpu_status(self, device_state):
|
||||
cpu_temp = max(device_state.cpuTempC, default=0.)
|
||||
|
||||
if cpu_temp >= 85:
|
||||
self._cpu_status.update(tr_noop("CPU"), f"{cpu_temp:.1f}%", Colors.DANGER)
|
||||
elif cpu_temp >= 65:
|
||||
self._cpu_status.update(tr_noop("CPU"), f"{cpu_temp:.1f}%", Colors.WARNING)
|
||||
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):
|
||||
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:
|
||||
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)
|
||||
|
||||
def _draw_metrics(self, rect: rl.Rectangle):
|
||||
metrics = [(self._temp_status, 338), (self._panda_status, 496)]
|
||||
if not ui_state.dp_dev_disable_connect:
|
||||
metrics.append((self._connect_status, 654))
|
||||
metrics = [(self._temp_status, 338), (self._cpu_status, 496), (self._panda_status, 654)]
|
||||
|
||||
for metric, y_offset in metrics:
|
||||
self._draw_metric(rect, metric, rect.y + y_offset)
|
||||
|
||||
@ -136,7 +136,7 @@ class AugmentedRoadView(CameraView):
|
||||
pass
|
||||
|
||||
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_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[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:
|
||||
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):
|
||||
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
||||
v_ego = sm['carState'].vEgo
|
||||
|
||||
@ -114,6 +114,7 @@ class CameraView(Widget):
|
||||
# which drains the VisionIpcClient SubSocket for us. Re-connecting is not enough
|
||||
# and only clears internal buffers, not the message queue.
|
||||
self.frame = None
|
||||
self.available_streams.clear()
|
||||
if self.client:
|
||||
del self.client
|
||||
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
@ -4,7 +4,7 @@ import argparse
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import xml.etree.ElementTree as ET
|
||||
import re
|
||||
from typing import cast
|
||||
|
||||
import requests
|
||||
@ -12,7 +12,7 @@ import requests
|
||||
TRANSLATIONS_DIR = pathlib.Path(__file__).resolve().parent
|
||||
TRANSLATIONS_LANGUAGES = TRANSLATIONS_DIR / "languages.json"
|
||||
|
||||
OPENAI_MODEL = "gpt-4"
|
||||
OPENAI_MODEL = "deepseek-chat"
|
||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
|
||||
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."
|
||||
@ -25,8 +25,8 @@ def get_language_files(languages: list[str] = None) -> dict[str, pathlib.Path]:
|
||||
language_dict = json.load(fp)
|
||||
|
||||
for filename in language_dict.values():
|
||||
path = TRANSLATIONS_DIR / f"{filename}.ts"
|
||||
language = path.stem
|
||||
path = TRANSLATIONS_DIR / f"app_{filename}.po"
|
||||
language = filename
|
||||
|
||||
if languages is None or language in languages:
|
||||
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:
|
||||
response = requests.post(
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
"https://api.deepseek.com/chat/completions",
|
||||
json={
|
||||
"model": OPENAI_MODEL,
|
||||
"messages": [
|
||||
@ -68,39 +68,343 @@ def translate_phrase(text: str, language: str) -> str:
|
||||
|
||||
|
||||
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"):
|
||||
name = context.find("name")
|
||||
if name is None:
|
||||
raise ValueError("name not found")
|
||||
# Look for msgid line
|
||||
if line.startswith('msgid'):
|
||||
# Check for empty msgid (header) - this is the start of multi-line msgid
|
||||
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"):
|
||||
source = message.find("source")
|
||||
translation = message.find("translation")
|
||||
# Skip header entry (empty msgid)
|
||||
if not msgid_text:
|
||||
i = j
|
||||
continue
|
||||
|
||||
if source is None or translation is None:
|
||||
raise ValueError("source or translation not found")
|
||||
# Look for the corresponding msgstr or msgid_plural
|
||||
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
|
||||
|
||||
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" +
|
||||
f"Current translation: {translation.text}\n" +
|
||||
f"LLM translation: {llm_translation}")
|
||||
# Skip header entry (empty msgid)
|
||||
if not msgid_text:
|
||||
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:
|
||||
fp.write('<?xml version="1.0" encoding="utf-8"?>\n' +
|
||||
'<!DOCTYPE TS>\n' +
|
||||
ET.tostring(root, encoding="utf-8").decode())
|
||||
fp.writelines(lines)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@ -27,8 +27,8 @@ class SetupWidget(Widget):
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if not ui_state.prime_state.is_paired():
|
||||
self._render_registration(rect)
|
||||
else:
|
||||
self._render_firehose_prompt(rect)
|
||||
# else:
|
||||
# self._render_firehose_prompt(rect)
|
||||
|
||||
def _render_registration(self, rect: rl.Rectangle):
|
||||
"""Render registration prompt."""
|
||||
|
||||
@ -52,11 +52,14 @@ class FontWeight(StrEnum):
|
||||
EXTRA_BOLD = "Inter-ExtraBold.ttf"
|
||||
BLACK = "Inter-Black.ttf"
|
||||
UNIFONT = "unifont.otf"
|
||||
CHINA = "china.ttf"
|
||||
|
||||
|
||||
def font_fallback(font: rl.Font) -> rl.Font:
|
||||
"""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 font
|
||||
|
||||
@ -140,7 +143,12 @@ class GuiApplication:
|
||||
self._fonts: dict[FontWeight, rl.Font] = {}
|
||||
self._width = width
|
||||
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_height = int(self._height * self._scale)
|
||||
self._render_texture: rl.RenderTexture | None = None
|
||||
@ -460,5 +468,17 @@ class GuiApplication:
|
||||
cloudlog.error(f"FPS dropped critically below {fps}. Shutting down UI.")
|
||||
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)
|
||||
|
||||
@ -6,6 +6,7 @@ import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import FONT_DIR
|
||||
|
||||
_emoji_font: ImageFont.FreeTypeFont | None = None
|
||||
_cache: dict[str, rl.Texture] = {}
|
||||
|
||||
EMOJI_REGEX = re.compile(
|
||||
@ -32,6 +33,13 @@ EMOJI_REGEX = re.compile(
|
||||
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):
|
||||
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:
|
||||
img = Image.new("RGBA", (128, 128), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
font = ImageFont.truetype(FONT_DIR.joinpath("NotoColorEmoji.ttf"), 109)
|
||||
draw.text((0, 0), emoji, font=font, embedded_color=True)
|
||||
draw.text((0, 0), emoji, font=_load_emoji_font(), embedded_color=True)
|
||||
with io.BytesIO() as buffer:
|
||||
img.save(buffer, format="PNG")
|
||||
l = buffer.tell()
|
||||
|
||||
@ -17,6 +17,9 @@ LANGUAGES_FILE = os.path.join(TRANSLATIONS_DIR, "languages.json")
|
||||
UNIFONT_LANGUAGES = [
|
||||
"ar",
|
||||
"th",
|
||||
]
|
||||
|
||||
CHINA_LANGUAGES = [
|
||||
"zh-CHT",
|
||||
"zh-CHS",
|
||||
"ko",
|
||||
@ -37,6 +40,10 @@ class Multilang:
|
||||
def language(self) -> str:
|
||||
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:
|
||||
"""Certain languages require unifont to render their glyphs."""
|
||||
return self._language in UNIFONT_LANGUAGES
|
||||
|
||||
@ -26,7 +26,7 @@ def clamp(value, min_value, max_value):
|
||||
class Spinner(Widget):
|
||||
def __init__(self):
|
||||
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._rotation = 0.0
|
||||
self._progress: int | None = None
|
||||
|
||||
@ -52,7 +52,7 @@ class HtmlElement:
|
||||
font_weight: FontWeight
|
||||
margin_top: 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
|
||||
|
||||
|
||||
|
||||
@ -137,7 +137,7 @@ class Keyboard(Widget):
|
||||
|
||||
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)
|
||||
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._cancel_button.render(rl.Rectangle(rect.x + rect.width - 386, rect.y, 386, 125))
|
||||
|
||||
|
||||
@ -336,6 +336,8 @@ class ListItem(Widget):
|
||||
# do callback first in case receiver changes description
|
||||
if self.description_visible and self.description_opened_callback is not None:
|
||||
self.description_opened_callback()
|
||||
# Call _update_state to catch any description changes
|
||||
self._update_state()
|
||||
|
||||
content_width = int(self._rect.width - ITEM_PADDING * 2)
|
||||
self._rect.height = self.get_item_height(self._font, content_width)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user