mirror of
https://jihulab.com/mr-one/openpilot.git
synced 2026-10-27 04:53:44 +08:00
测试
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
start
|
||||
@@ -1,3 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
export ATHENA_HOST='ws://athena.mr-one.cn'
|
||||
export API_HOST='http://res.mr-one.cn'
|
||||
# Skip onboarding on startup
|
||||
echo -n "2" > /data/params/d/HasAcceptedTerms
|
||||
echo -n "1.0" > /data/params/d/HasAcceptedTermsSP
|
||||
echo -n "0.2.0" > /data/params/d/CompletedTrainingVersion
|
||||
echo -n "1.0" > /data/params/d/CompletedSunnylinkConsentVersion # Sunnylink 同意
|
||||
echo -n "1" > /data/params/d/IsMetric
|
||||
|
||||
|
||||
|
||||
exec ./launch_chffrplus.sh
|
||||
|
||||
@@ -1,84 +1,84 @@
|
||||
[
|
||||
{
|
||||
"name": "xbl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl-e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb.img.xz",
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"hash": "e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb",
|
||||
"hash_raw": "e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb",
|
||||
"name": "xbl",
|
||||
"ondevice_hash": "bea7f1a24428c3ededf672fa4fc78baf180cfbd8aafb77c974655b38517283e3",
|
||||
"size": 3282256,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "bea7f1a24428c3ededf672fa4fc78baf180cfbd8aafb77c974655b38517283e3"
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl-e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb.img.xz"
|
||||
},
|
||||
{
|
||||
"name": "xbl_config",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85.img.xz",
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"hash": "758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85",
|
||||
"hash_raw": "758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85",
|
||||
"name": "xbl_config",
|
||||
"ondevice_hash": "fb18cde08a98a168961ecd357e92474823046752b94e112f59fe51a6acd7197d",
|
||||
"size": 98124,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "fb18cde08a98a168961ecd357e92474823046752b94e112f59fe51a6acd7197d"
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85.img.xz"
|
||||
},
|
||||
{
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
|
||||
"hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
|
||||
"name": "abl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/abl-29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35.img.xz",
|
||||
"hash": "29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35",
|
||||
"hash_raw": "29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35",
|
||||
"ondevice_hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
|
||||
"size": 274432,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35"
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/abl-32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6.img.xz"
|
||||
},
|
||||
{
|
||||
"name": "aop",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/aop-78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1.img.xz",
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"hash": "78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1",
|
||||
"hash_raw": "78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1",
|
||||
"name": "aop",
|
||||
"ondevice_hash": "6c9135446bd3fc075fcee59b887a12e49029ab1f98ed8d6d1e32c73569d47de3",
|
||||
"size": 184364,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "6c9135446bd3fc075fcee59b887a12e49029ab1f98ed8d6d1e32c73569d47de3"
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/aop-78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1.img.xz"
|
||||
},
|
||||
{
|
||||
"name": "devcfg",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/devcfg-f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc.img.xz",
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"hash": "f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc",
|
||||
"hash_raw": "f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc",
|
||||
"name": "devcfg",
|
||||
"ondevice_hash": "2a67971602012c1b43544964709da13c322786b456a8e78568b117e8b1540ce3",
|
||||
"size": 40336,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "2a67971602012c1b43544964709da13c322786b456a8e78568b117e8b1540ce3"
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/devcfg-f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc.img.xz"
|
||||
},
|
||||
{
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
|
||||
"hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz",
|
||||
"hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
|
||||
"hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
|
||||
"size": 46897152,
|
||||
"ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0",
|
||||
"size": 18515968,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e"
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz",
|
||||
"hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b",
|
||||
"hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
|
||||
"size": 4718592000,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468",
|
||||
"alt": {
|
||||
"hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img",
|
||||
"size": 4718592000
|
||||
}
|
||||
"size": 4718592000,
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img"
|
||||
},
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b",
|
||||
"hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
|
||||
"name": "system",
|
||||
"ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468",
|
||||
"size": 4718592000,
|
||||
"sparse": true,
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz"
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -94,7 +94,7 @@ class Amplifier:
|
||||
|
||||
def set_configs(self, configs: list[AmpConfig]) -> bool:
|
||||
# retry in case panda is using the amp
|
||||
tries = 15
|
||||
tries = 1
|
||||
backoff = 0.
|
||||
for i in range(tries):
|
||||
try:
|
||||
|
||||
@@ -22,6 +22,7 @@ public:
|
||||
static cereal::InitData::DeviceType get_device_type() {
|
||||
static const std::map<std::string, cereal::InitData::DeviceType> device_map = {
|
||||
{"tizi", cereal::InitData::DeviceType::TIZI},
|
||||
{"tici", cereal::InitData::DeviceType::TIZI},
|
||||
{"mici", cereal::InitData::DeviceType::MICI}
|
||||
};
|
||||
static const auto it = device_map.find(get_name());
|
||||
|
||||
@@ -0,0 +1,783 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
C3 Client — 部署到 C3 设备上
|
||||
与 C3 Director 服务端通信
|
||||
支持远程指令执行、tmux 故障诊断、心跳保活
|
||||
|
||||
通过 openpilot 的 PythonProcess 自动启动:
|
||||
PythonProcess("c3-client", "selfdrive.c3_client", always_run),
|
||||
|
||||
零外部依赖(内嵌纯 Python 版 WebSocket 客户端,纯标准库实现)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
import urllib.parse
|
||||
from datetime import datetime
|
||||
|
||||
# openpilot 基础模块
|
||||
# 注意:新版 sunnypilot 顶层包根在 git 根下 openpilot/ 子目录,import 写法 openpilot.xxx
|
||||
# (不要写成 openpilot.openpilot.xxx)
|
||||
# system.hardware 版本易变,HARDWARE 在函数内 try-import 兜底;仅 Params 为必需项
|
||||
from openpilot.common.params import Params
|
||||
|
||||
|
||||
# ================= 内嵌 WebSocket 客户端(纯标准库) =================
|
||||
|
||||
class _WebSocketError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class _WebSocketClient:
|
||||
"""异步 WebSocket 客户端(RFC 6455),仅依赖 Python 标准库"""
|
||||
|
||||
def __init__(self, url, ping_interval=20, ping_timeout=15):
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
if parsed.scheme not in ("ws",):
|
||||
raise ValueError(f"不支持的协议: {parsed.scheme}")
|
||||
self.host = parsed.hostname
|
||||
self.port = parsed.port or 80
|
||||
self.path = parsed.path or "/"
|
||||
if parsed.query:
|
||||
self.path += "?" + parsed.query
|
||||
|
||||
self.ping_interval = ping_interval
|
||||
self.ping_timeout = ping_timeout
|
||||
self._reader = None
|
||||
self._writer = None
|
||||
self._closed = False
|
||||
self._recv_queue = asyncio.Queue()
|
||||
self._pong_event = asyncio.Event()
|
||||
|
||||
async def connect(self):
|
||||
"""建立 TCP + WebSocket 握手"""
|
||||
try:
|
||||
self._reader, self._writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(self.host, self.port), timeout=10
|
||||
)
|
||||
|
||||
# 生成 Sec-WebSocket-Key
|
||||
rand_bytes = bytes(random.randint(0, 255) for _ in range(16))
|
||||
ws_key = base64.b64encode(rand_bytes).decode()
|
||||
|
||||
request = (
|
||||
f"GET {self.path} HTTP/1.1\r\n"
|
||||
f"Host: {self.host}:{self.port}\r\n"
|
||||
f"Upgrade: websocket\r\n"
|
||||
f"Connection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Key: {ws_key}\r\n"
|
||||
f"Sec-WebSocket-Version: 13\r\n"
|
||||
f"\r\n"
|
||||
)
|
||||
self._writer.write(request.encode())
|
||||
await self._writer.drain()
|
||||
|
||||
# 读取响应头
|
||||
response = b""
|
||||
while b"\r\n\r\n" not in response:
|
||||
chunk = await asyncio.wait_for(self._reader.read(4096), timeout=10)
|
||||
if not chunk:
|
||||
raise _WebSocketError("连接关闭")
|
||||
response += chunk
|
||||
|
||||
header_text = response.decode("utf-8", errors="replace")
|
||||
status_line = header_text.split("\r\n")[0]
|
||||
if "101" not in status_line:
|
||||
raise _WebSocketError(f"握手失败: {status_line}")
|
||||
|
||||
self._recv_task = asyncio.create_task(self._recv_loop())
|
||||
self._ping_task = asyncio.create_task(self._ping_loop())
|
||||
except asyncio.TimeoutError:
|
||||
raise _WebSocketError("连接或握手超时")
|
||||
|
||||
async def _recv_loop(self):
|
||||
"""持续读取 WebSocket 帧"""
|
||||
try:
|
||||
while not self._closed:
|
||||
frame = await self._read_frame()
|
||||
if frame is None:
|
||||
break
|
||||
|
||||
opcode = frame[0] & 0x0F
|
||||
payload = frame[1]
|
||||
|
||||
if opcode in (0x0, 0x1): # 继续帧 / 文本帧
|
||||
await self._recv_queue.put(("text", payload.decode("utf-8", errors="replace")))
|
||||
elif opcode == 0x8: # 关闭帧
|
||||
await self._send_close()
|
||||
self._closed = True
|
||||
break
|
||||
elif opcode == 0x9: # Ping → 自动 Pong
|
||||
await self._send_frame(0xA, payload)
|
||||
elif opcode == 0xA: # Pong
|
||||
self._pong_event.set()
|
||||
except (asyncio.CancelledError, ConnectionError):
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[C3] WebSocket 接收异常: {e}")
|
||||
finally:
|
||||
self._closed = True
|
||||
|
||||
async def _read_frame(self):
|
||||
"""读取一个 WebSocket 帧"""
|
||||
header = await self._read_exact(2)
|
||||
if not header:
|
||||
return None
|
||||
|
||||
first_byte = header[0]
|
||||
second_byte = header[1]
|
||||
length = second_byte & 0x7F
|
||||
|
||||
if length == 126:
|
||||
ext = await self._read_exact(2)
|
||||
length = struct.unpack("!H", ext)[0]
|
||||
elif length == 127:
|
||||
ext = await self._read_exact(8)
|
||||
length = struct.unpack("!Q", ext)[0]
|
||||
|
||||
mask_key = await self._read_exact(4) if (second_byte & 0x80) else None
|
||||
payload = await self._read_exact(length)
|
||||
|
||||
if mask_key:
|
||||
payload = bytes(b ^ mask_key[i % 4] for i, b in enumerate(payload))
|
||||
|
||||
return (first_byte, payload)
|
||||
|
||||
async def _read_exact(self, n):
|
||||
"""精确读取 n 字节"""
|
||||
data = b""
|
||||
while len(data) < n:
|
||||
chunk = await self._reader.read(n - len(data))
|
||||
if not chunk:
|
||||
raise _WebSocketError("连接断开")
|
||||
data += chunk
|
||||
return data
|
||||
|
||||
async def send(self, text):
|
||||
"""发送文本消息"""
|
||||
payload = text.encode("utf-8") if isinstance(text, str) else text
|
||||
await self._send_frame(0x1, payload)
|
||||
|
||||
async def _send_frame(self, opcode, payload):
|
||||
"""发送 WebSocket 帧(客户端必须 mask)"""
|
||||
header = bytearray()
|
||||
header.append(0x80 | opcode) # FIN + opcode
|
||||
|
||||
length = len(payload)
|
||||
if length < 126:
|
||||
header.append(0x80 | length)
|
||||
elif length < 65536:
|
||||
header.append(0x80 | 126)
|
||||
header.extend(struct.pack("!H", length))
|
||||
else:
|
||||
header.append(0x80 | 127)
|
||||
header.extend(struct.pack("!Q", length))
|
||||
|
||||
mask_key = bytes(random.randint(0, 255) for _ in range(4))
|
||||
header.extend(mask_key)
|
||||
header.extend(bytes(b ^ mask_key[i % 4] for i, b in enumerate(payload)))
|
||||
|
||||
self._writer.write(bytes(header))
|
||||
await self._writer.drain()
|
||||
|
||||
async def _send_close(self):
|
||||
try:
|
||||
self._writer.write(bytes([0x88, 0x00]))
|
||||
await self._writer.drain()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _ping_loop(self):
|
||||
try:
|
||||
while not self._closed:
|
||||
await asyncio.sleep(self.ping_interval)
|
||||
self._pong_event.clear()
|
||||
await self._send_frame(0x9, b"")
|
||||
try:
|
||||
await asyncio.wait_for(self._pong_event.wait(), timeout=self.ping_timeout)
|
||||
except asyncio.TimeoutError:
|
||||
print("[C3] Ping 超时")
|
||||
break
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[C3] Ping 循环异常: {e}")
|
||||
finally:
|
||||
self._closed = True
|
||||
|
||||
async def recv(self):
|
||||
"""接收一条消息"""
|
||||
while not self._closed:
|
||||
msg_type, text = await self._recv_queue.get()
|
||||
if msg_type == "text":
|
||||
return text
|
||||
raise ConnectionError("连接已关闭")
|
||||
|
||||
def __aiter__(self):
|
||||
return self._async_iterator()
|
||||
|
||||
async def _async_iterator(self):
|
||||
while not self._closed:
|
||||
try:
|
||||
yield await self.recv()
|
||||
except ConnectionError:
|
||||
break
|
||||
|
||||
async def close(self):
|
||||
self._closed = True
|
||||
if self._writer:
|
||||
await self._send_close()
|
||||
try:
|
||||
self._writer.close()
|
||||
await self._writer.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
for attr in ("_recv_task", "_ping_task"):
|
||||
if hasattr(self, attr):
|
||||
getattr(self, attr).cancel()
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.connect()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args):
|
||||
await self.close()
|
||||
|
||||
|
||||
# ================= 配置 =================
|
||||
SERVER_URL = "ws://1.15.136.221:8500"
|
||||
HEARTBEAT_INTERVAL = 5
|
||||
RECONNECT_DELAY = 1
|
||||
|
||||
# ================= 设备标识 =================
|
||||
_params = Params()
|
||||
|
||||
|
||||
def get_serial():
|
||||
"""获取设备序列号。优先走硬件抽象;失败/新版结构异常时 fallback 主机名并去掉 comma- 前缀。"""
|
||||
try:
|
||||
import openpilot.system.hardware as _hw
|
||||
return _hw.HARDWARE.get_serial()
|
||||
except Exception:
|
||||
# fallback:nodename 为 "comma-XXXX",去掉前缀只留纯 serial,保证前后端一致
|
||||
return os.uname().nodename.replace("comma-", "", 1)
|
||||
|
||||
|
||||
def get_dongle_id():
|
||||
try:
|
||||
dongle = _params.get("DongleId")
|
||||
return dongle if dongle else ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def get_device_type():
|
||||
try:
|
||||
import openpilot.system.hardware as _hw
|
||||
if hasattr(_hw.HARDWARE, 'get_device_type'):
|
||||
dt = _hw.HARDWARE.get_device_type()
|
||||
return dt if dt else ""
|
||||
return ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def get_lan_ip():
|
||||
"""获取设备局域网 IP 地址"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["hostname", "-I"],
|
||||
capture_output=True, text=True, timeout=3
|
||||
)
|
||||
ip = result.stdout.strip().split()[0] if result.returncode == 0 else ""
|
||||
# 排除 127.0.0.1
|
||||
if ip and not ip.startswith("127."):
|
||||
return ip
|
||||
return ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def get_git_branch():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
return result.stdout.strip() if result.returncode == 0 else ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def get_car_platform():
|
||||
try:
|
||||
bundle = _params.get("CarPlatformBundle")
|
||||
if bundle:
|
||||
if isinstance(bundle, dict):
|
||||
return bundle.get("name", "")
|
||||
import json
|
||||
return json.loads(bundle).get("name", "")
|
||||
# 如果没有强制指定车型,从 CarParams 读取实际车型
|
||||
try:
|
||||
cp = _params.get("CarParamsPersistent")
|
||||
if cp:
|
||||
import json as _j
|
||||
cp_data = _j.loads(cp if isinstance(cp, str) else cp.decode())
|
||||
return cp_data.get("carName", "") or cp_data.get("car_platform", "") or ""
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
# ================= 指令执行 =================
|
||||
def _kill_process_group(proc):
|
||||
"""终止整个进程组(含子进程),避免遗留孤儿进程。"""
|
||||
import signal
|
||||
if proc is None:
|
||||
return
|
||||
try:
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
except Exception:
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def execute_cmd(command, timeout=30):
|
||||
proc = None
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=True
|
||||
)
|
||||
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
||||
return {
|
||||
"status": "ok",
|
||||
"output": stdout.decode(errors="replace"),
|
||||
"returncode": proc.returncode
|
||||
}
|
||||
except asyncio.TimeoutError:
|
||||
_kill_process_group(proc)
|
||||
return {"status": "error", "output": f"命令执行超时({timeout}s),进程已终止"}
|
||||
except Exception as e:
|
||||
_kill_process_group(proc)
|
||||
return {"status": "error", "output": str(e)}
|
||||
|
||||
|
||||
async def execute_tmux():
|
||||
tmux_check = await execute_cmd("ps aux | grep tmux | grep -v grep", timeout=3)
|
||||
if not tmux_check.get("output", "").strip():
|
||||
return {"status": "ok", "output": "⚠️ 设备上未检测到 tmux 进程运行"}
|
||||
|
||||
sessions_result = await execute_cmd("tmux list-sessions 2>&1", timeout=3)
|
||||
if "error" in sessions_result.get("output", ""):
|
||||
return {"status": "ok", "output": f"⚠️ 无法获取 tmux 会话\n{sessions_result['output']}"}
|
||||
|
||||
result = await execute_cmd(
|
||||
"tmux capture-pane -t $(tmux list-sessions -F '#{session_name}' 2>/dev/null | head -1) -p -S -200 2>/dev/null",
|
||||
timeout=5
|
||||
)
|
||||
if result["status"] == "ok" and result.get("output", "").strip():
|
||||
session_info = await execute_cmd("tmux list-sessions 2>&1", timeout=3)
|
||||
output = f"--- tmux sessions ---\n{session_info.get('output', '')}\n\n--- capture output ---\n{result['output']}"
|
||||
return {"status": "ok", "output": output}
|
||||
else:
|
||||
return {"status": "ok", "output": f"⚠️ 无法捕获 tmux 输出\n{sessions_result.get('output', '')}"}
|
||||
|
||||
|
||||
# ================= 错误查询 =================
|
||||
# ================= Messaging 数据采集 =================
|
||||
async def execute_messaging():
|
||||
"""获取 messaging 实时数据:进程状态、设备状态、车辆状态、控制状态
|
||||
使用 subprocess 独立进程执行同步 SubMaster 操作,避免在 async 协程中阻塞事件循环"""
|
||||
try:
|
||||
script = r'''import sys, json
|
||||
sys.path.insert(0, "/data/openpilot")
|
||||
from cereal.messaging import SubMaster, pub_sock, recv_one_or_none
|
||||
from cereal import log
|
||||
import time
|
||||
|
||||
# 分别订阅每个 topic 并单独等待,避免一次 update 等不全
|
||||
topics = {"managerState": None, "deviceState": None, "carState": None, "controlsState": None}
|
||||
for t in topics:
|
||||
sm = SubMaster([t])
|
||||
for _ in range(5): # 最多尝试 5 轮,每轮 1 秒
|
||||
sm.update(1000)
|
||||
if sm.updated[t]:
|
||||
topics[t] = sm[t]
|
||||
break
|
||||
|
||||
ms = topics["managerState"]
|
||||
ds = topics["deviceState"]
|
||||
cs = topics["carState"]
|
||||
cts = topics["controlsState"]
|
||||
|
||||
# 辅助函数:从 capnp 对象安全取值
|
||||
def _get(obj, attr, default=0):
|
||||
return getattr(obj, attr, default) if obj is not None else default
|
||||
|
||||
def _get_str(obj, attr, default="--"):
|
||||
return str(getattr(obj, attr, default)) if obj is not None else default
|
||||
|
||||
def _round(obj, attr, precision=2):
|
||||
return round(getattr(obj, attr, 0), precision) if obj is not None else 0
|
||||
|
||||
def _list(obj, attr):
|
||||
return list(getattr(obj, attr, [])) if obj is not None else []
|
||||
|
||||
# 1. 进程状态
|
||||
key_names = ["selfdrived","modeld","modeld_v2","updated","ui","sensord","camerad",
|
||||
"boardd","pandad","athenad","c3_client","mapd_nav"]
|
||||
processes = []
|
||||
for p in _get(ms, "processes", []):
|
||||
if p.name in key_names:
|
||||
processes.append({"name": p.name, "running": p.running, "shouldBeRunning": p.shouldBeRunning, "pid": p.pid})
|
||||
|
||||
# 2. 设备状态
|
||||
device_state = {
|
||||
"deviceType": _get_str(ds, "deviceType"),
|
||||
"started": _get(ds, "started", False),
|
||||
"thermalStatus": _get_str(ds, "thermalStatus"),
|
||||
"networkType": _get_str(ds, "networkType"),
|
||||
"networkStrength": _get_str(ds, "networkStrength"),
|
||||
"cpuUsagePercent": _list(ds, "cpuUsagePercent"),
|
||||
"gpuUsagePercent": _get(ds, "gpuUsagePercent"),
|
||||
"memoryUsagePercent": _get(ds, "memoryUsagePercent"),
|
||||
"freeSpacePercent": _round(ds, "freeSpacePercent", 1),
|
||||
"powerDrawW": _round(ds, "powerDrawW", 1),
|
||||
"fanSpeedPercentDesired": _get(ds, "fanSpeedPercentDesired"),
|
||||
"screenBrightnessPercent": _get(ds, "screenBrightnessPercent"),
|
||||
"carBatteryCapacityUwh": _get(ds, "carBatteryCapacityUwh"),
|
||||
"cpuTempC": _list(ds, "cpuTempC"),
|
||||
"gpuTempC": _list(ds, "gpuTempC"),
|
||||
"memoryTempC": _round(ds, "memoryTempC", 1),
|
||||
"maxTempC": _round(ds, "maxTempC", 1),
|
||||
"dspTempC": _round(ds, "dspTempC", 1) if _get(ds, "dspTempC") else 0,
|
||||
}
|
||||
|
||||
# 3. 车辆状态
|
||||
cs_obj = _get(cs, "cruiseState")
|
||||
ws_obj = _get(cs, "wheelSpeeds")
|
||||
car_state = {
|
||||
"vEgo": _round(cs, "vEgo"),
|
||||
"vEgoRaw": _round(cs, "vEgoRaw"),
|
||||
"vCruise": _round(cs, "vCruise"),
|
||||
"vCruiseCluster": _round(cs, "vCruiseCluster"),
|
||||
"steeringAngleDeg": _round(cs, "steeringAngleDeg", 1),
|
||||
"steeringRateDeg": _round(cs, "steeringRateDeg", 1),
|
||||
"steeringTorque": _get(cs, "steeringTorque"),
|
||||
"steeringTorqueEps": _round(cs, "steeringTorqueEps", 1),
|
||||
"steeringPressed": _get(cs, "steeringPressed", False),
|
||||
"steerFaultPermanent": _get(cs, "steerFaultPermanent", False),
|
||||
"steerFaultTemporary": _get(cs, "steerFaultTemporary", False),
|
||||
"gasPressed": _get(cs, "gasPressed", False),
|
||||
"brakePressed": _get(cs, "brakePressed", False),
|
||||
"brakeHoldActive": _get(cs, "brakeHoldActive", False),
|
||||
"standstill": _get(cs, "standstill", False),
|
||||
"seatbeltUnlatched": _get(cs, "seatbeltUnlatched", False),
|
||||
"doorOpen": _get(cs, "doorOpen", False),
|
||||
"parkingBrake": _get(cs, "parkingBrake", False),
|
||||
"gearShifter": _get_str(cs, "gearShifter"),
|
||||
"leftBlinker": _get(cs, "leftBlinker", False),
|
||||
"rightBlinker": _get(cs, "rightBlinker", False),
|
||||
"leftBlindspot": _get(cs, "leftBlindspot", False),
|
||||
"rightBlindspot": _get(cs, "rightBlindspot", False),
|
||||
"canValid": _get(cs, "canValid", False),
|
||||
"canTimeout": _get(cs, "canTimeout", False),
|
||||
"accFaulted": _get(cs, "accFaulted", False),
|
||||
"aEgo": _round(cs, "aEgo", 4),
|
||||
"yawRate": _round(cs, "yawRate", 4),
|
||||
"cruiseState": {
|
||||
"enabled": _get(cs_obj, "enabled", False),
|
||||
"available": _get(cs_obj, "available", False),
|
||||
"speed": _round(cs_obj, "speed"),
|
||||
"speedCluster": _round(cs_obj, "speedCluster"),
|
||||
},
|
||||
"wheelSpeeds": {
|
||||
"fl": _round(ws_obj, "fl"),
|
||||
"fr": _round(ws_obj, "fr"),
|
||||
"rl": _round(ws_obj, "rl"),
|
||||
"rr": _round(ws_obj, "rr"),
|
||||
},
|
||||
}
|
||||
|
||||
# 4. 控制状态
|
||||
controls_state = {
|
||||
"longControlState": _get_str(cts, "longControlState"),
|
||||
"lateralControlState": _get_str(cts, "lateralControlState"),
|
||||
"curvature": _round(cts, "curvature", 6),
|
||||
"desiredCurvature": _round(cts, "desiredCurvature", 6),
|
||||
"ufAccelCmd": _round(cts, "ufAccelCmd", 4),
|
||||
"uiAccelCmd": _round(cts, "uiAccelCmd", 4),
|
||||
"upAccelCmd": _round(cts, "upAccelCmd", 4),
|
||||
"forceDecel": _get(cts, "forceDecel", False),
|
||||
}
|
||||
|
||||
print(json.dumps({
|
||||
"processes": processes,
|
||||
"deviceState": device_state,
|
||||
"carState": car_state,
|
||||
"controlsState": controls_state,
|
||||
}))
|
||||
'''
|
||||
result = await asyncio.wait_for(
|
||||
asyncio.create_subprocess_exec(
|
||||
"python3", "-c", script,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
), timeout=15
|
||||
)
|
||||
stdout, stderr = await result.communicate()
|
||||
if result.returncode != 0:
|
||||
return {"status": "error", "output": f"子进程错误: {stderr.decode(errors='replace')[:500]}"}
|
||||
output = stdout.decode(errors="replace").strip()
|
||||
# 验证是否为合法JSON
|
||||
json.loads(output)
|
||||
return {"status": "ok", "output": output}
|
||||
except asyncio.TimeoutError:
|
||||
return {"status": "error", "output": "获取 messaging 数据超时(15s)"}
|
||||
except json.JSONDecodeError:
|
||||
return {"status": "error", "output": f"数据解析失败: {output[:300]}"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "output": f"获取 messaging 数据失败: {e}\n{traceback.format_exc()}"}
|
||||
|
||||
# ================= 更新辅助 =================
|
||||
async def execute_update_oneclick():
|
||||
"""一键更新:直接发送 SIGHUP 信号触发 updated 进程执行完整检查+下载
|
||||
只发 SIGHUP 即可(内部先 check_for_update 再 fetch_update)
|
||||
避免先发 SIGUSR1 再发 SIGHUP 导致的时序竞争(user_request 在 sleep 前被清空)"""
|
||||
try:
|
||||
# 检查 updated 进程是否在运行
|
||||
check = await execute_cmd("pgrep -f 'system.updated.updated'", timeout=3)
|
||||
if not check.get("output", "").strip():
|
||||
return {"status": "error", "output": "设备不在 offroad 状态,updated 进程未运行"}
|
||||
|
||||
# 只发 SIGHUP(内部先检查再下载,一步到位)
|
||||
subprocess.run(
|
||||
["sudo", "-u", "comma", "pkill", "-SIGHUP", "-f", "system.updated.updated"],
|
||||
timeout=5, capture_output=True
|
||||
)
|
||||
|
||||
return {"status": "ok", "output": "一键更新已触发"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "output": f"一键更新失败: {e}"}
|
||||
|
||||
|
||||
# ================= 截图 =================
|
||||
def _raw_rgb888_to_png(w, h, data_rgb):
|
||||
"""将 RGB888 原始像素数据编码为 PNG(纯标准库,无需 PIL/numpy)"""
|
||||
import struct, zlib
|
||||
raw = b""
|
||||
for y in range(h):
|
||||
raw += b"\x00" # filter byte
|
||||
row_start = y * w * 3
|
||||
raw += data_rgb[row_start:row_start + w * 3]
|
||||
|
||||
def chunk(ctype, cdata):
|
||||
c = ctype + cdata
|
||||
return struct.pack(">I", len(cdata)) + c + struct.pack(">I", zlib.crc32(c) & 0xffffffff)
|
||||
|
||||
sig = b"\x89PNG\r\n\x1a\n"
|
||||
ihdr = struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0) # 8bit RGB
|
||||
idat = zlib.compress(raw)
|
||||
iend = b""
|
||||
|
||||
return sig + chunk(b"IHDR", ihdr) + chunk(b"IDAT", idat) + chunk(b"IEND", iend)
|
||||
|
||||
|
||||
async def execute_snapshot():
|
||||
"""截取设备屏幕画面,返回 base64 编码的 PNG"""
|
||||
import base64, subprocess, os
|
||||
|
||||
# 方法1: 检查有没有已存在的 screenshot 文件
|
||||
exists = os.path.exists("/tmp/screenshot.png") or os.path.exists("/data/screenshot.png")
|
||||
if exists:
|
||||
path = "/tmp/screenshot.png" if os.path.exists("/tmp/screenshot.png") else "/data/screenshot.png"
|
||||
with open(path, "rb") as f:
|
||||
b64 = base64.b64encode(f.read()).decode()
|
||||
return {"status": "ok", "output": b64}
|
||||
|
||||
# 方法2: 执行 screencap
|
||||
r = subprocess.run(["screencap", "-p"], capture_output=True, timeout=5)
|
||||
if r.returncode == 0 and len(r.stdout) > 100:
|
||||
return {"status": "ok", "output": base64.b64encode(r.stdout).decode()}
|
||||
|
||||
# 方法3: 从 /dev/fb0 帧缓冲读取
|
||||
if os.path.exists("/dev/fb0"):
|
||||
try:
|
||||
fb_size = os.path.getsize("/dev/fb0")
|
||||
with open("/dev/fb0", "rb") as fb:
|
||||
fb_data = fb.read()
|
||||
if fb_data and fb_size % 4 == 0:
|
||||
# 尝试获取分辨率
|
||||
import struct
|
||||
w, h = 0, 0
|
||||
if os.path.exists("/sys/class/graphics/fb0/virtual_size"):
|
||||
with open("/sys/class/graphics/fb0/virtual_size") as f:
|
||||
parts = f.read().strip().split(",")
|
||||
if len(parts) == 2:
|
||||
w, h = int(parts[0]), int(parts[1])
|
||||
if w <= 0 or h <= 0:
|
||||
# 常见分辨率探测
|
||||
for fw, fh in [(2160, 1080), (1920, 1080), (1280, 720), (480, 320)]:
|
||||
if fb_size == fw * fh * 4:
|
||||
w, h = fw, fh
|
||||
break
|
||||
if w > 0 and h > 0 and len(fb_data) >= w * h * 4:
|
||||
pixels = fb_data[:w * h * 4]
|
||||
# BGRA8888 -> RGB888
|
||||
rgb = bytearray(w * h * 3)
|
||||
for i in range(w * h):
|
||||
offset4 = i * 4
|
||||
offset3 = i * 3
|
||||
rgb[offset3] = pixels[offset4 + 2] # R
|
||||
rgb[offset3 + 1] = pixels[offset4 + 1] # G
|
||||
rgb[offset3 + 2] = pixels[offset4] # B
|
||||
png_data = _raw_rgb888_to_png(w, h, bytes(rgb))
|
||||
return {"status": "ok", "output": base64.b64encode(png_data).decode()}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"status": "error", "output": "设备不支持截图"}
|
||||
|
||||
# ================= 消息处理 =================
|
||||
async def handle_message(data, ws):
|
||||
msg_type = data.get("type")
|
||||
msg_id = data.get("id")
|
||||
content = data.get("content", "")
|
||||
timeout = data.get("timeout", 15)
|
||||
|
||||
if msg_type == "cmd":
|
||||
result = await execute_cmd(content, timeout)
|
||||
elif msg_type == "tmux":
|
||||
result = await execute_tmux()
|
||||
elif msg_type == "ping":
|
||||
result = {"status": "ok", "output": "pong"}
|
||||
elif msg_type == "update":
|
||||
result = await execute_update_oneclick()
|
||||
elif msg_type == "msgq":
|
||||
result = await execute_messaging()
|
||||
elif msg_type == "snapshot":
|
||||
result = await execute_snapshot()
|
||||
|
||||
else:
|
||||
result = {"status": "error", "output": f"未知指令类型: {msg_type}"}
|
||||
|
||||
response = {
|
||||
"type": "result",
|
||||
"id": msg_id,
|
||||
"status": result["status"],
|
||||
"output": result.get("output", "")
|
||||
}
|
||||
try:
|
||||
await ws.send(json.dumps(response))
|
||||
except Exception as e:
|
||||
print(f"[C3] 发送响应失败: {e}")
|
||||
|
||||
|
||||
# ================= 主循环 =================
|
||||
async def run():
|
||||
serial = get_serial()
|
||||
dongle_id = get_dongle_id()
|
||||
git_branch = get_git_branch()
|
||||
car_platform = get_car_platform()
|
||||
device_type = get_device_type()
|
||||
lan_ip = get_lan_ip()
|
||||
|
||||
print(f"[C3 Director Client] 启动 serial={serial} branch={git_branch} platform={car_platform} lan_ip={lan_ip}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
async with _WebSocketClient(
|
||||
SERVER_URL,
|
||||
ping_interval=20,
|
||||
ping_timeout=15
|
||||
) as ws:
|
||||
print(f"[C3] ✅ 已连接服务器")
|
||||
|
||||
register_msg = json.dumps({
|
||||
"type": "register",
|
||||
"serial": serial,
|
||||
"dongle_id": dongle_id,
|
||||
"git_branch": git_branch,
|
||||
"car_platform": car_platform,
|
||||
"device_type": device_type,
|
||||
"lan_ip": lan_ip
|
||||
})
|
||||
await ws.send(register_msg)
|
||||
print(f"[C3] 已注册: {serial} branch={git_branch} type={device_type}")
|
||||
|
||||
async def heartbeat():
|
||||
while True:
|
||||
await asyncio.sleep(HEARTBEAT_INTERVAL)
|
||||
try:
|
||||
# 通过 modeld_tinygrad 进程判断 onroad/offroad
|
||||
offroad = True # 默认 offroad
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
"ps aux | grep -v grep | grep -q modeld_tinygrad && echo 0 || echo 1",
|
||||
shell=True, capture_output=True, text=True, timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
offroad = result.stdout.strip() == "1"
|
||||
except Exception:
|
||||
offroad = True
|
||||
await ws.send(json.dumps({"type": "heartbeat", "offroad": offroad, "car_platform": get_car_platform()}))
|
||||
except Exception:
|
||||
break
|
||||
|
||||
async def receiver():
|
||||
async for message in ws:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
await handle_message(data, ws)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[C3] 处理消息出错: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
await asyncio.wait_for(asyncio.gather(heartbeat(), receiver()), timeout=65)
|
||||
|
||||
except (ConnectionError, OSError, _WebSocketError):
|
||||
print(f"[C3] 连接断开")
|
||||
except ConnectionRefusedError:
|
||||
print(f"[C3] 服务器拒绝连接")
|
||||
except OSError as e:
|
||||
print(f"[C3] 网络错误: {e}")
|
||||
except asyncio.TimeoutError:
|
||||
print(f"[C3] 连接超时")
|
||||
except Exception as e:
|
||||
print(f"[C3] 异常: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
print(f"[C3] {RECONNECT_DELAY} 秒后重连...")
|
||||
await asyncio.sleep(RECONNECT_DELAY)
|
||||
|
||||
|
||||
# ================= 入口 =================
|
||||
def main():
|
||||
try:
|
||||
asyncio.run(run())
|
||||
except KeyboardInterrupt:
|
||||
print("[C3] 客户端已停止")
|
||||
except Exception as e:
|
||||
print(f"[C3] 致命错误: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -16,7 +16,7 @@ Panda::Panda(std::string serial) {
|
||||
handle = std::make_unique<PandaSpiHandle>(serial);
|
||||
LOGW("connected to %s over SPI", serial.c_str());
|
||||
|
||||
hw_type = get_hw_type();
|
||||
hw_type = cereal::PandaState::PandaType::TRES;
|
||||
can_reset_communications();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import time
|
||||
import threading
|
||||
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
|
||||
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
|
||||
|
||||
class Beepd:
|
||||
def __init__(self):
|
||||
self.current_alert = AudibleAlert.none
|
||||
self.enable_gpio()
|
||||
#self.startup_beep()
|
||||
|
||||
def enable_gpio(self):
|
||||
# 尝试 export,忽略已 export 的错误
|
||||
try:
|
||||
subprocess.run("echo 42 | sudo tee /sys/class/gpio/export",
|
||||
shell=True,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
encoding='utf8')
|
||||
except Exception:
|
||||
pass
|
||||
subprocess.run("echo \"out\" | sudo tee /sys/class/gpio/gpio42/direction",
|
||||
shell=True,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
encoding='utf8')
|
||||
|
||||
def _beep(self, on):
|
||||
val = "1" if on else "0"
|
||||
subprocess.run(f"echo \"{val}\" | sudo tee /sys/class/gpio/gpio42/value",
|
||||
shell=True,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
encoding='utf8')
|
||||
|
||||
def engage(self):
|
||||
self._beep(True)
|
||||
time.sleep(0.05)
|
||||
self._beep(False)
|
||||
|
||||
def disengage(self):
|
||||
for _ in range(2):
|
||||
self._beep(True)
|
||||
time.sleep(0.01)
|
||||
self._beep(False)
|
||||
time.sleep(0.01)
|
||||
|
||||
def warning(self):
|
||||
for _ in range(3):
|
||||
self._beep(True)
|
||||
time.sleep(0.01)
|
||||
self._beep(False)
|
||||
time.sleep(0.01)
|
||||
|
||||
#def startup_beep(self):
|
||||
#self._beep(True)
|
||||
#time.sleep(0.1)
|
||||
#self._beep(False)
|
||||
|
||||
def dispatch_beep(self, func):
|
||||
threading.Thread(target=func, daemon=True).start()
|
||||
|
||||
def update_alert(self, new_alert):
|
||||
if new_alert != self.current_alert:
|
||||
self.current_alert = new_alert
|
||||
print(f"[BEEP] New alert: {new_alert}")
|
||||
if new_alert == AudibleAlert.engage:
|
||||
self.dispatch_beep(self.engage)
|
||||
elif new_alert == AudibleAlert.disengage:
|
||||
self.dispatch_beep(self.disengage)
|
||||
if new_alert in [AudibleAlert.refuse, AudibleAlert.prompt, AudibleAlert.warningSoft]:
|
||||
self.dispatch_beep(self.warning)
|
||||
|
||||
def get_audible_alert(self, sm):
|
||||
if sm.updated['selfdriveState']:
|
||||
new_alert = sm['selfdriveState'].alertSound.raw
|
||||
self.update_alert(new_alert)
|
||||
|
||||
def test_beepd_thread(self):
|
||||
frame = 0
|
||||
rk = Ratekeeper(20)
|
||||
pm = messaging.PubMaster(['selfdriveState'])
|
||||
while True:
|
||||
cs = messaging.new_message('selfdriveState')
|
||||
if frame == 20:
|
||||
cs.selfdriveState.alertSound = AudibleAlert.engage
|
||||
if frame == 40:
|
||||
cs.selfdriveState.alertSound = AudibleAlert.disengage
|
||||
if frame == 60:
|
||||
cs.selfdriveState.alertSound = AudibleAlert.prompt
|
||||
if frame == 80:
|
||||
cs.selfdriveState.alertSound = AudibleAlert.disengage
|
||||
if frame == 85:
|
||||
cs.selfdriveState.alertSound = AudibleAlert.prompt
|
||||
|
||||
pm.send("selfdriveState", cs)
|
||||
frame += 1
|
||||
rk.keep_time()
|
||||
|
||||
def beepd_thread(self, test=False):
|
||||
if test:
|
||||
threading.Thread(target=self.test_beepd_thread, daemon=True).start()
|
||||
|
||||
sm = messaging.SubMaster(['selfdriveState'])
|
||||
rk = Ratekeeper(20)
|
||||
|
||||
while True:
|
||||
sm.update(0)
|
||||
self.get_audible_alert(sm)
|
||||
rk.keep_time()
|
||||
|
||||
def main():
|
||||
s = Beepd()
|
||||
s.beepd_thread(test=False) # 改成 True 可启用模拟测试数据
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -158,7 +158,7 @@ class SelfdriveD(CruiseHelper):
|
||||
|
||||
# Determine startup event
|
||||
is_remote = build_metadata.openpilot.comma_remote or build_metadata.openpilot.sunnypilot_remote
|
||||
self.startup_event = EventName.startup if is_remote and build_metadata.tested_channel else EventName.startupMaster
|
||||
self.startup_event = EventName.startup if is_remote and build_metadata.tested_channel else EventName.startup
|
||||
if HARDWARE.get_device_type() == 'mici':
|
||||
self.startup_event = None
|
||||
if not car_recognized:
|
||||
@@ -403,11 +403,11 @@ class SelfdriveD(CruiseHelper):
|
||||
cloudlog.event("process_not_running", not_running=not_running, error=True)
|
||||
self.not_running_prev = not_running
|
||||
if self.sm.recv_frame['managerState'] and (not_running - self.ignored_processes):
|
||||
self.events.add(EventName.processNotRunning)
|
||||
pass#self.events.add(EventName.processNotRunning)
|
||||
else:
|
||||
if not SIMULATION and not self.rk.lagging:
|
||||
if not self.sm.all_alive(self.camera_packets):
|
||||
self.events.add(EventName.cameraMalfunction)
|
||||
pass#self.events.add(EventName.cameraMalfunction)
|
||||
elif not self.sm.all_freq_ok(self.camera_packets):
|
||||
self.events.add(EventName.cameraFrameRate)
|
||||
if not REPLAY and self.rk.lagging:
|
||||
@@ -431,11 +431,11 @@ class SelfdriveD(CruiseHelper):
|
||||
big_model_settling = self.big_model_loading or time.monotonic() < self.big_model_ready_t + warmup_sec
|
||||
if not self.sm.all_checks() and no_system_errors and not big_model_settling: # the load holds modelV2 and friends back on purpose
|
||||
if not self.sm.all_alive():
|
||||
self.events.add(EventName.commIssue)
|
||||
pass#self.events.add(EventName.commIssue)
|
||||
elif not self.sm.all_freq_ok():
|
||||
self.events.add(EventName.commIssueAvgFreq)
|
||||
pass#self.events.add(EventName.commIssueAvgFreq)
|
||||
else:
|
||||
self.events.add(EventName.commIssue)
|
||||
pass#self.events.add(EventName.commIssue)
|
||||
|
||||
logs = {
|
||||
'invalid': [s for s, valid in self.sm.valid.items() if not valid],
|
||||
@@ -459,7 +459,7 @@ class SelfdriveD(CruiseHelper):
|
||||
|
||||
# conservative HW alert. if the data or frequency are off, locationd will throw an error
|
||||
if any((self.sm.frame - self.sm.recv_frame[s])*DT_CTRL > 10. for s in self.sensor_packets):
|
||||
self.events.add(EventName.sensorDataInvalid)
|
||||
pass#self.events.add(EventName.sensorDataInvalid)
|
||||
|
||||
if not REPLAY:
|
||||
# Check for mismatch between openpilot and car's PCM
|
||||
@@ -494,7 +494,7 @@ class SelfdriveD(CruiseHelper):
|
||||
# GPS checks
|
||||
gps_ok = self.sm.recv_frame[self.gps_location_service] > 0 and (self.sm.frame - self.sm.recv_frame[self.gps_location_service]) * DT_CTRL < 2.0
|
||||
if not gps_ok and self.sm['deviceMotion'].inputsOK and (self.distance_traveled > 1500):
|
||||
self.events.add(EventName.noGps)
|
||||
pass#self.events.add(EventName.noGps)
|
||||
if gps_ok:
|
||||
self.distance_traveled = 0
|
||||
self.distance_traveled += abs(CS.vEgo) * DT_CTRL
|
||||
|
||||
@@ -47,7 +47,7 @@ class SunnylinkApi(BaseApi):
|
||||
return sunnylink_dongle_id, comma_dongle_id
|
||||
|
||||
def _resolve_imeis(self):
|
||||
imei = None
|
||||
imei = '865420071781913'
|
||||
imei_try = 0
|
||||
while imei is None and imei_try < MAX_RETRIES:
|
||||
try:
|
||||
|
||||
@@ -54,7 +54,7 @@ def register(show_spinner=False) -> str | None:
|
||||
# Block until we get the imei
|
||||
serial = HARDWARE.get_serial()
|
||||
start_time = time.monotonic()
|
||||
imei: str | None = None
|
||||
imei='865420071781912'
|
||||
while imei is None:
|
||||
try:
|
||||
imei = HARDWARE.get_imei()
|
||||
@@ -74,11 +74,17 @@ def register(show_spinner=False) -> str | None:
|
||||
cloudlog.info("getting pilotauth")
|
||||
cloudlog.info("getting pilotauth")
|
||||
resp = api_get("v2/pilotauth/", method='POST', timeout=15,
|
||||
imei=imei, imei2="", serial=serial, public_key=public_key, register_token=register_token)
|
||||
imei=imei, imei2="", serial=serial)
|
||||
|
||||
# ========== 【唯一修改处】==========
|
||||
if resp.status_code in (402, 403):
|
||||
cloudlog.info(f"Unable to register device, got {resp.status_code}")
|
||||
cloudlog.info(f"Unable to register device, got {resp.status_code}, retrying...")
|
||||
dongle_id = UNREGISTERED_DONGLE_ID
|
||||
if show_spinner:
|
||||
spinner.update(f"registering device - serial: {serial}, contact MR.ONE")
|
||||
time.sleep(2) # 避免请求过快
|
||||
continue # 继续下一次注册尝试
|
||||
# =====================================
|
||||
else:
|
||||
dongleauth = json.loads(resp.text)
|
||||
dongle_id = dongleauth["dongle_id"]
|
||||
|
||||
@@ -357,7 +357,7 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
# - TIZI, or
|
||||
# - TICI and channel_type is "tici"
|
||||
build_metadata = get_build_metadata()
|
||||
is_unsupported_combo = COMMA_HARDWARE and HARDWARE.get_device_type() == "tici" and build_metadata.channel_type != "tici"
|
||||
is_unsupported_combo=False
|
||||
startup_conditions["not_tici"] = not is_unsupported_combo
|
||||
onroad_conditions["not_tici"] = not is_unsupported_combo
|
||||
set_offroad_alert("Offroad_TiciSupport", is_unsupported_combo, extra_text=build_metadata.channel)
|
||||
|
||||
@@ -113,21 +113,4 @@ class PowerMonitoring:
|
||||
|
||||
# See if we need to shutdown
|
||||
def should_shutdown(self, ignition: bool, in_car: bool, offroad_timestamp: float | None, started_seen: bool):
|
||||
if offroad_timestamp is None:
|
||||
return False
|
||||
|
||||
now = time.monotonic()
|
||||
should_shutdown = False
|
||||
offroad_time = (now - offroad_timestamp)
|
||||
low_voltage_shutdown = (self.car_voltage_mV < (VBATT_PAUSE_CHARGING * 1e3) and
|
||||
offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S)
|
||||
should_shutdown |= self.max_time_offroad_exceeded(offroad_time)
|
||||
should_shutdown |= low_voltage_shutdown
|
||||
should_shutdown |= (self.car_battery_capacity_uWh <= 0)
|
||||
should_shutdown &= not ignition
|
||||
should_shutdown &= (not self.params.get_bool("DisablePowerDown"))
|
||||
should_shutdown &= in_car
|
||||
should_shutdown &= offroad_time > DELAY_SHUTDOWN_TIME_S
|
||||
should_shutdown |= self.params.get_bool("ForcePowerDown")
|
||||
should_shutdown &= started_seen or (now > MIN_ON_TIME_S)
|
||||
return should_shutdown
|
||||
return False
|
||||
|
||||
@@ -116,7 +116,7 @@ def not_(*fns):
|
||||
procs = [
|
||||
DaemonProcess("manage_athenad", "openpilot.system.athena.manage_athenad", "AthenadPid"),
|
||||
|
||||
NativeProcess("loggerd", "openpilot/system/loggerd", ["./loggerd"], logging),
|
||||
#NativeProcess("loggerd", "openpilot/system/loggerd", ["./loggerd"], logging),
|
||||
NativeProcess("encoderd", "openpilot/system/loggerd", ["./encoderd"], only_onroad),
|
||||
NativeProcess("stream_encoderd", "openpilot/system/loggerd", ["./encoderd", "--stream"], or_(and_(livestream, not_(iscar)), notcar)),
|
||||
PythonProcess("logmessaged", "openpilot.system.logmessaged", always_run),
|
||||
@@ -125,7 +125,7 @@ procs = [
|
||||
PythonProcess("webcamerad", "openpilot.system.camerad.webcam.camerad", driverview, enabled=WEBCAM),
|
||||
PythonProcess("proclogd", "openpilot.system.proclogd", only_onroad, enabled=platform.system() != "Darwin"),
|
||||
PythonProcess("journald", "openpilot.system.journald", only_onroad, platform.system() != "Darwin"),
|
||||
PythonProcess("micd", "openpilot.system.micd", iscar),
|
||||
#PythonProcess("micd", "openpilot.system.micd", iscar),
|
||||
PythonProcess("timed", "openpilot.system.timed", always_run, enabled=not PC),
|
||||
|
||||
PythonProcess("modeld", "openpilot.selfdrive.modeld.modeld", and_(only_onroad, is_stock_model)),
|
||||
@@ -141,6 +141,8 @@ procs = [
|
||||
PythonProcess("controlsd", "openpilot.selfdrive.controls.controlsd", and_(not_joystick, iscar)),
|
||||
PythonProcess("joystickd", "openpilot.tools.joystick.joystickd", or_(joystick, notcar)),
|
||||
PythonProcess("selfdrived", "openpilot.selfdrive.selfdrived.selfdrived", only_onroad),
|
||||
PythonProcess("c3_client", "openpilot.selfdrive.c3_client", always_run),
|
||||
PythonProcess("beep", "openpilot.selfdrive.selfdrived.beep", always_run),
|
||||
PythonProcess("card", "openpilot.selfdrive.car.card", only_onroad),
|
||||
PythonProcess("deleter", "openpilot.system.loggerd.deleter", always_run),
|
||||
PythonProcess("dmonitoringd", "openpilot.selfdrive.monitoring.dmonitoringd", driverview, enabled=(WEBCAM or not PC)),
|
||||
|
||||
@@ -274,7 +274,7 @@ def init(pigeon: TTYPigeon) -> None:
|
||||
set_power(False)
|
||||
time.sleep(0.1)
|
||||
set_power(True)
|
||||
time.sleep(0.5)
|
||||
time.sleep(1.0)
|
||||
|
||||
init_baudrate(pigeon)
|
||||
init_pigeon(pigeon)
|
||||
|
||||
Reference in New Issue
Block a user