Commit f3e6d081 authored by 957dd's avatar 957dd

优化了验证

parent c4f0ffb2
No preview for this file type
......@@ -227,6 +227,9 @@ void message_2_judyverify(cJSON *body)
}
else
{
if (mqtt_verify_is_pending())
my_zlog_info("message_type=2 已拦截: Token正在等待后端验证");
else
my_zlog_warn("验证不通过");
}
}
......@@ -248,6 +251,9 @@ void message_3_judyverify(cJSON *body)
}
else
{
if (mqtt_verify_is_pending())
my_zlog_info("message_type=3 已拦截: Token正在等待后端验证");
else
my_zlog_warn("验证不通过");
}
}
......@@ -269,6 +275,9 @@ void message_4_judyverify(cJSON *body)
}
else
{
if (mqtt_verify_is_pending())
my_zlog_info("message_type=4 已拦截: Token正在等待后端验证");
else
my_zlog_warn("验证不通过");
}
}
......@@ -290,6 +299,9 @@ void message_6_steering_judyverify(cJSON *body)
}
else
{
if (mqtt_verify_is_pending())
my_zlog_info("message_type=6 已拦截: Token正在等待后端验证");
else
my_zlog_warn("验证不通过");
}
}
......@@ -317,6 +329,9 @@ void message_7_joystick_judyverify(cJSON *body)
}
else
{
if (mqtt_verify_is_pending())
my_zlog_info("message_type=7 已拦截: Token正在等待后端验证");
else
my_zlog_warn("验证不通过");
}
}
......
#include <time.h>
#include <errno.h>
#include <sys/random.h>
#include "mqtt_verify.h"
#include "mqtt_aes.h"
#include "common.h"
......@@ -23,11 +25,22 @@ static bool s_secret_key_index = true;//用于判断是否对比topic
/* topic 不同时发后端验证,失败可重试,超过 3 次不通过则判定该 topic 不通过 */
static char s_pending_verify_token[1024];
static time_t s_pending_verify_token_expiry = 0;
static time_t s_last_seen_token_expiry = 0;
static time_t s_verified_token_expiry = 0;
static char s_last_seen_token[1024]; /* 用于判断token是否变化(首次启动默认全0) */
static int s_verify_retry_count = 0;
static time_t s_verify_retry_after_time = 0; /* 允许发起重试的时间戳,0 表示无待重试 */
#define VERIFY_RETRY_MAX 3
#define VERIFY_RETRY_INTERVAL_SEC 1
#define VERIFY_RESPONSE_TIMEOUT_SEC 3
static time_t s_verify_request_sent_time = 0;
static pthread_mutex_t s_verify_state_mutex = PTHREAD_MUTEX_INITIALIZER;
static bool s_verify_pending = false;
#define VERIFY_RETIRED_TOKEN_MAX 16
static char s_retired_verify_tokens[VERIFY_RETIRED_TOKEN_MAX][1024];
static size_t s_retired_verify_token_count = 0;
static size_t s_retired_verify_token_next = 0;
/* 启动时向后端查询是否开启验证(3013/2013) */
#define VERIFY_OPEN_QUERY_MAX 3
......@@ -41,9 +54,11 @@ static time_t s_verify_open_last_send_time = 0;
int g_verify_index =0;//判断是否验证成功
pthread_mutex_t g_verify_mutex = PTHREAD_MUTEX_INITIALIZER;//线程锁
char *generate_random_id() {
static char *legacy_generate_random_id() {
char *id_buffer=malloc(11);
if (id_buffer == NULL)
return NULL;
// 1. 定义字符池:包含所有小写字母、大写字母和数字
const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const int charset_size = sizeof(charset) - 1; // 减去末尾的 '\0'
......@@ -61,13 +76,400 @@ char *generate_random_id() {
}
//发送用户获取jwt给服务端验证
int send_jwtser(const char *token){
static void verify_token_copy(char *dst, size_t dst_size, const char *src)
{
if (!dst || dst_size == 0)
return;
snprintf(dst, dst_size, "%s", src ? src : "");
}
static int generate_random_id(char out[11])
{
static const char charset[] =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
unsigned char random_bytes[10];
size_t offset = 0;
size_t i;
if (!out)
return -1;
while (offset < sizeof(random_bytes)) {
ssize_t n = getrandom(random_bytes + offset,
sizeof(random_bytes) - offset, 0);
if (n < 0 && errno == EINTR)
continue;
if (n <= 0) {
out[0] = '\0';
return -1;
}
offset += (size_t)n;
}
for (i = 0; i < sizeof(random_bytes); i++)
out[i] = charset[random_bytes[i] % (sizeof(charset) - 1)];
out[10] = '\0';
return 0;
}
/* Caller holds s_verify_state_mutex. */
static bool verify_token_is_retired(const char *token)
{
size_t i;
if (!token || token[0] == '\0')
return false;
for (i = 0; i < s_retired_verify_token_count; i++) {
if (strcmp(token, s_retired_verify_tokens[i]) == 0)
return true;
}
return false;
}
/* Caller holds s_verify_state_mutex. */
static void verify_token_retire(const char *token)
{
if (!token || token[0] == '\0' || verify_token_is_retired(token))
return;
verify_token_copy(s_retired_verify_tokens[s_retired_verify_token_next],
sizeof(s_retired_verify_tokens[0]), token);
s_retired_verify_token_next =
(s_retired_verify_token_next + 1) % VERIFY_RETIRED_TOKEN_MAX;
if (s_retired_verify_token_count < VERIFY_RETIRED_TOKEN_MAX)
s_retired_verify_token_count++;
}
static void verify_count_reset(void)
{
pthread_mutex_lock(&g_verify_mutex);
g_verify_count = 0;
pthread_mutex_unlock(&g_verify_mutex);
}
bool mqtt_verify_is_pending(void)
{
bool pending;
pthread_mutex_lock(&s_verify_state_mutex);
pending = s_verify_pending;
pthread_mutex_unlock(&s_verify_state_mutex);
return pending;
}
/* Send only the token currently stored as pending. Caller holds state mutex. */
int send_jwtser(const char *token)
{
cJSON *root = NULL;
cJSON *body = NULL;
cJSON *head = NULL;
char only_id[11] = {0};
char *payload = NULL;
int sent_count = 0;
if (!token || token[0] == '\0')
return -1;
root = cJSON_CreateObject();
body = cJSON_CreateObject();
head = cJSON_CreateObject();
if (!root || !body || !head || generate_random_id(only_id) != 0)
goto cleanup;
cJSON_AddStringToObject(body, "data", token);
cJSON_AddStringToObject(body, "only_id", only_id);
cJSON_AddNumberToObject(head, "message_type", 3006);
cJSON_AddItemToObject(root, "body", body);
body = NULL;
cJSON_AddItemToObject(root, "head", head);
head = NULL;
payload = cJSON_PrintUnformatted(root);
if (!payload)
goto cleanup;
verify_token_copy(s_only_id_middle, sizeof(s_only_id_middle), only_id);
sent_count = mqtt_publish_to_all(mqtt_topic_pure_number(), payload, 0);
if (sent_count > 0) {
s_verify_request_sent_time = time(NULL);
my_zlog_info("已发送Token后端验证请求: token=%s, only_id=%s, MQTT服务器数=%d",
token, only_id, sent_count);
} else {
s_only_id_middle[0] = '\0';
s_verify_request_sent_time = 0;
my_zlog_warn("Token后端验证请求发送失败: token=%s, 没有可用的MQTT连接",
token);
}
cleanup:
free(payload);
cJSON_Delete(body);
cJSON_Delete(head);
cJSON_Delete(root);
return sent_count > 0 ? 0 : -1;
}
int receive_jwt(cJSON *body)
{
cJSON *json_token;
cJSON *json_token_time;
const char *token;
const char *token_time;
char *endptr = NULL;
unsigned long long expiry_value;
time_t now;
bool token_changed;
bool token_verified;
bool retry_sent = false;
if (!cJSON_IsObject(body)) {
g_verify_index = 1;
my_zlog_warn("JWT验证拒绝: body不是JSON对象");
return 1;
}
json_token = cJSON_GetObjectItem(body, "token");
json_token_time = cJSON_GetObjectItem(body, "token_time");
if (!cJSON_IsString(json_token) || !cJSON_IsString(json_token_time) ||
!json_token->valuestring || !json_token_time->valuestring ||
json_token->valuestring[0] == '\0' ||
json_token_time->valuestring[0] == '\0') {
char *invalid_body = cJSON_PrintUnformatted(body);
g_verify_index = 1;
my_zlog_warn("JWT验证拒绝: token或token_time缺失/类型无效, body=%s",
invalid_body ? invalid_body : "(无法打印body)");
free(invalid_body);
return 1;
}
token = json_token->valuestring;
token_time = json_token_time->valuestring;
expiry_value = strtoull(token_time, &endptr, 10);
if (endptr == token_time || *endptr != '\0' ||
expiry_value > 0x7fffffffffffffffULL) {
g_verify_index = 1;
my_zlog_warn("JWT验证拒绝: token_time格式无效, token=%s, token_time=%s",
token, token_time);
return 1;
}
now = time(NULL);
if ((time_t)expiry_value <= now) {
g_verify_index = 1;
my_zlog_warn("JWT验证拒绝: token已过期, token=%s, token_time=%s, 当前时间=%ld",
token, token_time, (long)now);
return 1;
}
pthread_mutex_lock(&s_verify_state_mutex);
token_changed = strcmp(token, s_last_seen_token) != 0;
/*
* A new token always invalidates the previous authorization result first.
* The current control message remains blocked until the matching 2006 reply.
*/
if (token_changed) {
/*
* Once a token has been replaced, delayed control packets carrying that
* exact token must never take ownership back from the current driver.
*/
if (verify_token_is_retired(token)) {
g_verify_index = 1;
my_zlog_warn("收到已退出用户的旧Token控制消息,已拦截且不再请求后端: token=%s",
token);
pthread_mutex_unlock(&s_verify_state_mutex);
return 1;
}
verify_token_retire(s_last_seen_token);
if (s_last_seen_token[0] != '\0')
my_zlog_info("旧Token已加入退出缓存,后续迟到消息将直接拦截: token=%s",
s_last_seen_token);
verify_token_copy(s_last_seen_token, sizeof(s_last_seen_token), token);
verify_token_copy(s_pending_verify_token,
sizeof(s_pending_verify_token), token);
s_last_seen_token_expiry = (time_t)expiry_value;
s_pending_verify_token_expiry = (time_t)expiry_value;
s_only_id_middle[0] = '\0';
s_verify_request_sent_time = 0;
s_verify_retry_count = 0;
s_verify_retry_after_time = 0;
s_service_verify = false;
s_verify_pending = true;
g_verify_index = 1;
if (send_jwtser(s_pending_verify_token) != 0)
s_verify_retry_after_time = now + VERIFY_RETRY_INTERVAL_SEC;
verify_count_reset();
my_zlog_info("收到新Token并更新到期时间,正在等待后端验证: token=%s, token_time=%s",
token, token_time);
pthread_mutex_unlock(&s_verify_state_mutex);
return 0;
}
/*
* The same JWT must always carry the same expiry. This prevents a caller
* from extending an already verified token by changing token_time only.
*/
if (s_last_seen_token_expiry == 0 ||
(time_t)expiry_value != s_last_seen_token_expiry) {
g_verify_index = 1;
my_zlog_warn("同一Token携带的token_time发生变化,已拒绝: token=%s, 当前token_time=%lld, 收到token_time=%s",
token, (long long)s_last_seen_token_expiry, token_time);
pthread_mutex_unlock(&s_verify_state_mutex);
return 1;
}
token_verified = s_service_verify &&
s_secret_key[0] != '\0' &&
strcmp(token, s_secret_key) == 0 &&
s_verified_token_expiry == s_last_seen_token_expiry;
/*
* A timed-out request is retired. Its late reply will no longer match the
* current only_id, so it cannot invalidate the new driver's token.
*/
if (!token_verified && s_only_id_middle[0] != '\0' &&
s_verify_request_sent_time > 0 &&
now - s_verify_request_sent_time >= VERIFY_RESPONSE_TIMEOUT_SEC) {
my_zlog_warn("Token后端验证响应超时: token=%s, only_id=%s",
s_pending_verify_token, s_only_id_middle);
s_only_id_middle[0] = '\0';
s_verify_request_sent_time = 0;
s_verify_retry_count++;
if (s_verify_retry_count < VERIFY_RETRY_MAX)
s_verify_retry_after_time = now;
}
/* Retry only the latest pending token, never an older token. */
if (!token_verified && s_only_id_middle[0] == '\0' &&
s_verify_retry_count < VERIFY_RETRY_MAX &&
(s_verify_retry_after_time == 0 ||
now >= s_verify_retry_after_time)) {
s_verify_retry_after_time = 0;
if (send_jwtser(s_pending_verify_token) == 0)
retry_sent = true;
else
s_verify_retry_after_time = now + VERIFY_RETRY_INTERVAL_SEC;
}
if (token_verified) {
s_verify_pending = false;
g_verify_index = 0;
/* Periodically recheck the same verified token without using another token. */
if (g_verify_count > 12000 && s_only_id_middle[0] == '\0') {
verify_token_copy(s_pending_verify_token,
sizeof(s_pending_verify_token), token);
s_pending_verify_token_expiry = s_verified_token_expiry;
if (send_jwtser(s_pending_verify_token) == 0)
my_zlog_debug("已发起JWT定时重新验证");
verify_count_reset();
}
} else {
g_verify_index = 1;
if (retry_sent)
my_zlog_info("已重试最新Token的后端验证: token=%s, 次数=%d/%d",
s_pending_verify_token,
s_verify_retry_count + 1, VERIFY_RETRY_MAX);
}
pthread_mutex_unlock(&s_verify_state_mutex);
return 0;
}
int message2006_verify(cJSON *body)
{
cJSON *json_status;
cJSON *json_onlyid;
const char *status;
const char *onlyid;
bool notify_driver_switch = false;
int result = 0;
if (!cJSON_IsObject(body))
return 1;
json_status = cJSON_GetObjectItem(body, "status");
json_onlyid = cJSON_GetObjectItem(body, "only_id");
if (!cJSON_IsString(json_status) || !cJSON_IsString(json_onlyid) ||
!json_status->valuestring || !json_onlyid->valuestring) {
my_zlog_warn("Token验证回包无效,已忽略");
return 1;
}
status = json_status->valuestring;
onlyid = json_onlyid->valuestring;
pthread_mutex_lock(&s_verify_state_mutex);
my_zlog_info("收到Token后端验证回包: status=%s, only_id=%s, 当前only_id=%s, 待验证token=%s",
status, onlyid,
s_only_id_middle[0] ? s_only_id_middle : "(无)",
s_pending_verify_token[0] ? s_pending_verify_token : "(无)");
/* A late reply for an old token must not change current authorization. */
if (s_only_id_middle[0] == '\0' ||
strcmp(onlyid, s_only_id_middle) != 0) {
my_zlog_warn("收到旧Token的过期验证回包,已忽略: only_id=%s, 当前待验证token=%s",
onlyid,
s_pending_verify_token[0] ? s_pending_verify_token : "(无)");
pthread_mutex_unlock(&s_verify_state_mutex);
return 1;
}
s_only_id_middle[0] = '\0';
s_verify_request_sent_time = 0;
if (strcmp(status, "1") == 0) {
notify_driver_switch =
s_secret_key[0] != '\0' &&
strcmp(s_secret_key, s_pending_verify_token) != 0;
verify_token_copy(s_secret_key, sizeof(s_secret_key),
s_pending_verify_token);
s_verified_token_expiry = s_pending_verify_token_expiry;
s_service_verify = true;
s_verify_pending = false;
g_verify_index = 0;
s_secret_key_index = false;
s_verify_retry_count = 0;
s_verify_retry_after_time = 0;
my_zlog_info("最新Token后端验证成功: token=%s, token_time=%lld, only_id=%s",
s_secret_key, (long long)s_verified_token_expiry, onlyid);
} else {
s_service_verify = false;
s_verify_pending = true;
g_verify_index = 1;
s_verify_retry_count++;
if (s_verify_retry_count < VERIFY_RETRY_MAX) {
s_verify_retry_after_time =
time(NULL) + VERIFY_RETRY_INTERVAL_SEC;
my_zlog_notice("最新Token后端验证未通过: token=%s, status=%s, 重试次数=%d/%d",
s_pending_verify_token, status,
s_verify_retry_count, VERIFY_RETRY_MAX);
} else {
s_verify_pending = false;
my_zlog_warn("最新Token连续%d次验证未通过,已禁止控制: token=%s, status=%s",
VERIFY_RETRY_MAX, s_pending_verify_token, status);
result = 2;
}
}
pthread_mutex_unlock(&s_verify_state_mutex);
if (notify_driver_switch)
webrtcpush_notify_driver_switch();
return result;
}
static int legacy_send_jwtser(const char *token){
cJSON *root = cJSON_CreateObject();
cJSON *body = cJSON_CreateObject();
cJSON *head = cJSON_CreateObject();
char *only_Id =generate_random_id();
char *only_Id =legacy_generate_random_id();
my_zlog_debug("%s",only_Id);
......@@ -93,7 +495,7 @@ int send_jwtser(const char *token){
}
//jwt验证函数
int receive_jwt(cJSON *body) {
static int legacy_receive_jwt(cJSON *body) {
char *token=NULL;
char *token_time=NULL;
......@@ -195,7 +597,7 @@ int receive_jwt(cJSON *body) {
}
//当接收到2006判断是否验证成功
int message2006_verify(cJSON *body){
static int legacy_message2006_verify(cJSON *body){
char *status=NULL;
char *onlyid=NULL;
......@@ -448,4 +850,3 @@ void mqtt_verify_data_clear(cJSON *body){
mqtt_verify_clear_send("2");
}
}
......@@ -16,6 +16,7 @@ extern int g_verify_index;//判断是否验证成功
int receive_jwt(cJSON *body);//jwt验证
bool mqtt_verify_is_pending(void);
int message2006_verify(cJSON *body);
/*接收到是否打开验证的函数(2013 仅接受加密 body)*/
......
......@@ -9,4 +9,4 @@ file perms = 600
millisecond = "%d(%Y-%m-%d %H:%M:%S).%ms [%V] %m%n"
[rules]
my_log.* "/home/orangepi/car/master/log/log_2026-07-28.log"; millisecond
my_log.* "/home/orangepi/car/master/log/log_2026-07-31.log"; millisecond
# 香橙派车控项目交接文档
# 香橙派车控项目交接文档
> 项目:飞驰人生设备端(Car + OTA + HTTP Server)
> 工程目录:`/home/orangepi/car/master`
> 设备地址:`192.168.0.104`(当前局域网地址,可能随 DHCP 变化)
> 编制日期:2026-07-31
> 文档状态:可执行交接稿(基于源码、设备环境、Git 与运行状态实机核验)
---
## 1. 文档目的与使用边界
本文用于帮助接手人员在较短时间内完成以下工作:
- 理解项目的业务目标、代码分层、启动链路和常驻线程;
- 能够在当前 Orange Pi 3B 上编译、部署、启动、停止和回滚;
- 能部署和维护 OTAdevice、设备号 HTTP Server 及三项 systemd 服务;
- 能定位设备识别、GPIO/串口控制、MQTT、配网、音频及视频推流问题;
- 清楚当前设备的真实运行状态、已知风险和优先整改项;
- 避免把设备号、JWT、MQTT 配置、Tailscale AuthKey 等敏感信息带入公开日志或版本库。
本文记录的是 2026-07-31 对 `192.168.0.104` 的核验快照。后端地址、MQTT 节点、设备 IP、硬件编号和分支状态均可能变化,操作前应重新确认。
## 2. 一页式交接摘要
### 2.1 项目是什么
完整设备端由三个常驻组件组成:`car` 主程序负责车型控制、MQTT、配网、音视频与硬件;Go 编写的 `OTAdevice` 负责轮询版本、备份、下载、校验、替换及重启 `carstart.service`;Python `httpserver.py` 提供局域网设备号查看/录入页面。三者分别由 `carstart.service``gostart.service``httpstart.service` 管理。
### 2.2 当前实机快照
| 项目 | 核验结果 |
|---|---|
| 主机 | Orange Pi 3B,aarch64 |
| 系统 | Orange Pi 1.0.8 / Ubuntu 22.04.5 LTS |
| 内核 | 5.10.160-rockchip-rk356x |
| 当前网络 | `wlan0``192.168.0.104/24``eth0` 为 DOWN |
| 工程目录 | `/home/orangepi/car/master` |
| Git 分支 | `feature/add_aboard_tts` |
| 工程版本 | CMake `project(car VERSION 1.3.11)` |
| 最新提交 | `c4f0ffb2`(2026-07-28,修改喇叭和麦并优化) |
| 当前设备号 | `CN010200000000`,型号编码 `0102` |
| 当前推流编译开关 | `WEBRTCPUSH_USE_MPP=0`,即 Chromium 路径 |
| Car 构建产物 | `build/main`,约 3.3 MB,ARM64 ELF |
| Car 生产路径 | `/home/orangepi/car/master/main`,由人工发布或 OTA 更新 |
| OTA 仓库 | `http://git.yd-ss.com/leimingyu/OTAdevice.git`,默认分支 `master` |
| systemd unit | 三份均位于 `/etc/systemd/system/` |
| 服务状态 | `carstart``httpstart``gostart` 均为 disabled、inactive |
| 当前缺失文件 | `master/main``goproject/OTAdevice``version.conf``backup/` |
| 当前可用文件 | `build/main``httpserver.py` |
| 本次检查时主程序 | 未发现 Car、OTA 或 HTTP Server 进程 |
| 仍在运行的相关进程 | 存在 Chromium 进程,不能据此认定主程序健康 |
| 资源 | 根分区约 29 GB,已用 61%;内存约 3.8 GiB |
### 2.3 接手后的首要动作
1. 按 OTA 目录约定补齐 `master/main``goproject/OTAdevice``version.conf``backup/`
2. 校验三份 unit 后执行 `daemon-reload`,明确是否 `enable --now`,完成重启恢复验收。
3. 在架空车辆状态下验证 OTA 停服、备份、替换、重启、回滚和整机重启全链路。
4. 限制 HTTP Server 只在可信局域网使用,并尽快增加鉴权和输入校验。
5. 轮换历史日志中可能暴露过的 JWT/Tailscale 凭据,并改造日志脱敏。
6. 确认生产推流路径是 Chromium、Go 还是 C/libdatachannel。
## 3. 系统架构与数据流
### 3.1 分层结构
```text
上层 App / 后端 / OTA 平台
├─ HTTPS:拉取设备配置、语言、推流模式、音量等
├─ MQTT:控制、心跳、验证、状态上报
├─ WebRTC/信令:视频、麦克风、喊话、DataChannel
└─ UDP 18888:局域网发现、WiFi 配置、推流调试
systemd 服务管理层
├─ carstart.service → /home/orangepi/car/master/main
├─ gostart.service → /home/orangepi/car/master/goproject/OTAdevice
└─ httpstart.service → python3 /home/orangepi/car/master/httpserver.py
┌─────────┼─────────┐
▼ ▼ ▼
Car 主程序 OTA 更新器 HTTP Server :8080
app/ 业务启动与设备身份
├─ main:分阶段初始化、拉起常驻线程
├─ device_identity:设备号 → 型号
└─ device_change:WiFi/设备配置变更
modules/ 公共服务
├─ HTTP / MQTT / logger / IPC
├─ runtime_deps / go_deploy / tailscale_deploy
├─ wifi_config / device_debug
└─ webrtcpush / browser
drivers/ 硬件与车型控制
├─ gpio / softPWM / I2C
├─ devicecontrol:car、tank、ship、ptz、robot、gps
├─ sensors:音频、温度、角度、红外等
└─ selfcontrol:自控逻辑
GPIO、串口、摄像头、麦克风、喇叭、传感器、电机
```
### 3.2 主数据流
- **设备识别**:读取 `Deviceld.txt` → 提取第 3~6 位型号编码 → 映射到 `DEVICE_*` → 初始化对应硬件。
- **控制链路**:MQTT 消息 → 消息类型分发 → `device_walk_control()` 等车型接口 → `device_gpio_control()` / 串口输出。
- **配置链路**:HTTPS `device/getConfig?deviceNo=...` → MQTT 节点、视频 URL、语言、`webrtcMode` 等运行参数。
- **视频链路**:当前编译为 Chromium;代码同时保留 Go 与 C/libdatachannel 分支。
- **局域网配置**:UDP `0.0.0.0:18888` 处理设备发现、WiFi 配置和推流调试命令。
- **本地 IPC**:Unix Socket `/tmp/device_msg.sock`
- **OTA 链路**`OTAdevice` 读取设备号和 `version.conf` → 请求 OTA API → 停止 Car → 备份旧 `main` → 下载 `main.tmp` → MD5 校验 → 替换 `main` → 重启 Car → 重启系统。
- **设备号录入链路**:浏览器访问 HTTP Server `:8080` → GET `/` 查看设备号 → POST `/submit` 覆盖写入 `Deviceld.txt`
## 4. 代码目录与职责
| 路径 | 职责 | 接手时重点 |
|---|---|---|
| `CMakeLists.txt` | 顶层构建、版本、编译类型、依赖链接 | Release/Debug、静态库、链接项 |
| `app/main/` | `main()`、初始化顺序、常驻线程 | 首个阅读入口 |
| `app/device_identity/` | 设备号读取、型号映射、MQTT topic | 新增型号必查 |
| `app/device_change/` | 设备号/WiFi 变更及管理 | 配网与持久化 |
| `drivers/gpio/` | wiringPi、GPIO、softPWM、I2C | 引脚冲突和板型差异 |
| `drivers/devicecontrol/` | 各车/坦克/船/云台/机器人控制 | 速度、方向、急停 |
| `drivers/sensors/` | 音频、TTS、温度、角度、红外等 | 声卡竞争与资源文件 |
| `drivers/selfcontrol/` | 自控状态及线程 | 与人工遥控互斥 |
| `modules/mqtt/` | 连接、订阅、发布、验证、消息分发 | 多 Broker 与重连 |
| `modules/http/` | 后端配置获取 | 超时、回退默认值 |
| `modules/browser/` | Chromium 推流入口与重启 | DISPLAY/PulseAudio |
| `modules/webrtcpush/` | C 原生 WebRTC、GStreamer、MPP、音频 | 复杂度最高模块 |
| `modules/wifi_config/` | UDP 发现和 WiFi 配置 | 端口 18888 |
| `modules/device_debug/` | 推流模式/URL 调试接口 | 仅受控网络使用 |
| `modules/runtime_deps/` | 运行时检查/安装依赖 | 启动时可能执行 apt |
| `modules/go_deploy/` | 拉取/校验/运行 Go 视频程序 | TLS 校验风险 |
| `modules/logger/` | zlog 封装、日志保留 | 必须做敏感信息脱敏 |
| `third_party/` | mosquitto、libdatachannel、cJSON、zlog | 供应链和许可证 |
| `build/` | CMake 构建输出 | 不作为源码维护 |
| `main` | Car 生产二进制,`carstart.service` 与 OTA 的共同目标 | 不等同于 `build/main` |
| `goproject/OTAdevice` | Go OTA 常驻程序 | 由 `gostart.service` 启动 |
| `goproject/main.go` | OTA 源码 | 版本轮询、备份、校验、替换 |
| `httpserver.py` | 设备号录入 HTTP Server | TCP 8080、无第三方 Python 依赖 |
| `version.conf` | OTA 当前版本号 | 首次部署需创建,可为空 |
| `backup/` | OTA Car 二进制备份目录 | OTA 可自动创建 |
| `log/` | 运行日志 | 可能含敏感数据 |
| `交接文档/` | 本交接材料 | DOCX 与 Markdown |
## 5. 启动流程
`app/main/main.c` 使用阶段化初始化表。带 fatal 标记的步骤失败会直接返回错误码;非 fatal 步骤只记录警告并继续。
### 5.1 初始化顺序
1. **core**
- 初始化日志。
2. **platform_driver**
- 初始化 wiringPi;
- 初始化音频配置和音频运行环境。
3. **app**
- 读取设备号;
- 生成 MQTT topic;
- 检查并自动配置 WiFi。
4. **device_driver**
- 初始化硬件 I2C;
- 初始化射击冷却(不支持的设备允许跳过)。
5. **module_service**
- 检查/安装运行时依赖;
- 请求基础数据与 MQTT/视频配置;
- 检查/部署 Go 视频程序;
- 启动 `/tmp/device_msg.sock`
- 启动 WiFi 配置 UDP 服务。
6. **runtime**
- 拉起 8 个工作线程;
- 播放连接成功提示音;
- 主线程进入常驻 sleep。
### 5.2 工作线程
| 线程 | 主要职责 | 关注点 |
|---|---|---|
| `thread_exit_time` | 串口接收、异常退出计时、失联停车、自控计时 | 安全关键 |
| `thread_mqtt_beat` | 心跳、验证开关轮询、状态发布 | MQTT 健康 |
| `thread_open_browser` | Go/原生 C/Chromium 三选一推流 | 互斥与自动回退 |
| `thread_mqtt_reconnect` | MQTT 初始化、重试与循环 | 多节点部分失败 |
| `thread_time_calculation` | 5 ms 周期快速检查与计数器 | CPU 占用与溢出 |
| `thread_play_mp3` | 音频播放队列 | 与喊话/麦克风竞争 |
| `thread_steering_control` | 25 ms 方向盘平滑处理 | 无匹配设备时退出 |
| `thread_wifi_offline_recovery` | 断网恢复 | 当前日志显示 40 秒检测 |
## 6. 设备识别与已注册型号
设备号当前为 14 位字符串,例如 `CN010200000000`。第 3~6 位 `0102` 是型号编码。新增设备时不能只加枚举;需同步检查身份映射、设备初始化表、控制实现、GPIO/串口资源、急停逻辑和文档。
| 类别 | 编码 | 说明 |
|---|---|---|
| 车辆 | 0101 | 莽牛大车,速度上限约 150,避免顶满 |
| 车辆 | 0102 | 小闪电/莽牛小车,最大速度 200;当前实机型号 |
| 车辆 | 0103 | 挖掘机,按电池电压调速 |
| 车辆 | 0104 | 推土机,按电池电压调速 |
| 车辆 | 0105 | 人坐超大车,串口遥控方案 |
| 车辆 | 0106 | F1 双电机大板,含起步阈值与固定转向 PWM |
| 坦克 | 0201 | 废弃占位,不建议新部署 |
| 坦克 | 0202/0203/0204 | 豹2/M1A2/99A 等,按电池电压调速 |
| 坦克 | 0206 | 可喷水坦克 |
| 坦克 | 0207 | 可发射水弹坦克 |
| 船舶 | 0301/0302 | 两套独立船舶控制 |
| 云台/定位 | 0401 | 云台炮台,有限位 |
| 云台/定位 | 0403 | GPS 定位设备,串口线程 |
| 云台/定位 | 0404 | 烟花大云台等,有限位 |
| 云台/定位 | 0405 | 云台,例如射击 pin27 配置 |
| 云台/定位 | 0406 | 水枪炮台 |
| 机器人 | 0501 | 机械狗 |
| 机器人 | 0502 | 六自由度机械臂,CH340 串口总线舵机 |
> 注意:旧资料中的 `0205` 与当前代码不一致,应以代码注册表为准。
## 7. 构建环境与依赖
### 7.1 已核验工具链
- CMake 3.22.1;
- GCC 11.4.0;
- GStreamer 1.20.3;
- GLib 2.72.4;
- aarch64 Linux。
### 7.2 主要依赖
- 系统库:wiringPi、libcurl、OpenSSL、pthread、dl、uuid、math;
- GStreamer:core、app、webrtc、sdp、video、json-glib、libsoup 2.4、libjpeg;
- 仓库内依赖:Mosquitto、libdatachannel、cJSON、zlog;
- Chromium 路径:`chromium-browser`/`chromium``xdotool`
- 原生推流可用:Rockchip MPP,缺失时可回退 x264。
### 7.3 常规编译
```bash
cd /home/orangepi/car/master
cmake -S . -B build -DCAR_BUILD_TYPE=Release
cmake --build build -j2
```
调试构建:
```bash
cd /home/orangepi/car/master
cmake -S . -B build -DCAR_BUILD_TYPE=Debug
cmake --build build -j2
```
产物:
```text
/home/orangepi/car/master/build/main
```
Release 模式会定义 `LOG_PRODUCTION`,减少 debug 日志。项目虽然主体是 C,但 libdatachannel 是 C++,最终链接器被设置为 C++。
### 7.4 构建后最低检查
```bash
file build/main
ldd build/main
./build/main
```
启动测试前应先确认没有旧实例,且车辆架空/断开动力,避免误动作。首次启动可能因 `runtime_deps` 自动执行 apt 安装或摄像头 USB 重置,不应在生产高峰期盲测。
## 8. systemd 服务管理
### 8.1 三项服务与文件位置
三份 unit 均应放在 `/etc/systemd/system/`,所有者为 `root:root`、权限通常为 `0644`。它们不在 `/lib/systemd/system/`;后者通常留给软件包管理器安装的 unit。
| Unit | 管理对象 | ExecStart | WorkingDirectory |
|---|---|---|---|
| `carstart.service` | Car 主程序 | `/home/orangepi/car/master/main` | `/home/orangepi/car/master` |
| `httpstart.service` | Python 设备号 HTTP Server | `/usr/bin/python3 /home/orangepi/car/master/httpserver.py` | `/home/orangepi/car/master` |
| `gostart.service` | Go OTAdevice | `/home/orangepi/car/master/goproject/OTAdevice` | `/home/orangepi/car/master/goproject` |
当前实机三份 unit 均已加载、语法可识别,但均为 disabled、inactive;`carstart``gostart` 的 ExecStart 目标文件缺失。
### 8.2 当前 carstart.service
保存位置:`/etc/systemd/system/carstart.service`
```ini
[Unit]
Description=My Custom Service
After=network.target
[Service]
ExecStart=/home/orangepi/car/master/main
Environment=TZ=Asia/Shanghai
WorkingDirectory=/home/orangepi/car/master
Restart=always
User=root
Group=root
Environment="DISPLAY=:0"
Environment="XAUTHORITY=/home/orangepi/.Xauthority"
[Install]
WantedBy=multi-user.target
```
`DISPLAY``XAUTHORITY` 用于 Chromium 推流路径。`master/main` 是 OTA 的固定发布目标;`build/main` 只是 CMake 构建产物。人工首次部署需执行:
```bash
cd /home/orangepi/car/master
sudo install -o root -g root -m 0755 build/main main
```
### 8.3 当前 httpstart.service
保存位置:`/etc/systemd/system/httpstart.service`
```ini
[Unit]
Description=My Custom Service
After=network.target
[Service]
ExecStart=/usr/bin/python3 /home/orangepi/car/master/httpserver.py
Environment=TZ=Asia/Shanghai
WorkingDirectory=/home/orangepi/car/master
Restart=always
User=root
Group=root
[Install]
WantedBy=multi-user.target
```
该服务不需要 Python 第三方包,脚本仅使用标准库。启动后先等待 20 秒,再尝试获取本机 IP 并绑定 TCP 8080。
### 8.4 当前 gostart.service
保存位置:`/etc/systemd/system/gostart.service`
```ini
[Unit]
Description=My Custom Service
After=network.target
[Service]
ExecStart=/home/orangepi/car/master/goproject/OTAdevice
Environment=TZ=Asia/Shanghai
WorkingDirectory=/home/orangepi/car/master/goproject
Restart=always
User=root
Group=root
[Install]
WantedBy=multi-user.target
```
`OTAdevice` 内部会调用 `systemctl stop/restart carstart.service``reboot`,因此当前使用 root。程序中的 `sudo` 在 root 服务下属于冗余,但不影响理解其权限需求。
### 8.5 安装、检查与启停
复制或编辑 unit 后必须执行:
```bash
sudo chown root:root /etc/systemd/system/carstart.service \
/etc/systemd/system/httpstart.service \
/etc/systemd/system/gostart.service
sudo chmod 0644 /etc/systemd/system/carstart.service \
/etc/systemd/system/httpstart.service \
/etc/systemd/system/gostart.service
sudo systemd-analyze verify /etc/systemd/system/carstart.service \
/etc/systemd/system/httpstart.service \
/etc/systemd/system/gostart.service
sudo systemctl daemon-reload
```
启动并设置开机自启:
```bash
sudo systemctl enable --now httpstart.service
sudo systemctl enable --now carstart.service
sudo systemctl enable --now gostart.service
```
统一检查:
```bash
systemctl is-enabled carstart.service httpstart.service gostart.service
systemctl is-active carstart.service httpstart.service gostart.service
systemctl status carstart.service httpstart.service gostart.service --no-pager -l
journalctl -u carstart.service -u httpstart.service -u gostart.service -n 300 --no-pager
```
停止并取消开机自启:
```bash
sudo systemctl disable --now gostart.service
sudo systemctl disable --now httpstart.service
sudo systemctl disable --now carstart.service
```
只要修改了 unit 文件,就必须重新 `daemon-reload`;只替换二进制或 Python 脚本时不需要 reload,但需要 restart 对应服务。
### 8.6 建议的加固项
-`Description` 改成明确名称;
- 网络服务使用 `Wants=network-online.target``After=network-online.target`
- 增加 `RestartSec=5`,避免缺文件或网络故障时高频重启;
-`ExecStartPre=/usr/bin/test -x ...``test -f` 提前给出清晰错误;
- Car 保留 DISPLAY/XAUTHORITY;HTTP 与 OTA 不需要图形环境;
- 生产变更前备份 `/etc/systemd/system/*.service`
以下是 Car 的加固示例,实际启用前需在测试板验证:
```ini
[Unit]
Description=Orange Pi Car Control
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/home/orangepi/car/master
Environment=TZ=Asia/Shanghai
Environment=DISPLAY=:0
Environment=XAUTHORITY=/home/orangepi/.Xauthority
ExecStartPre=/usr/bin/test -x /home/orangepi/car/master/main
ExecStart=/home/orangepi/car/master/main
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
```
## 9. OTAdevice
### 9.1 仓库与部署结构
仓库:`http://git.yd-ss.com/leimingyu/OTAdevice.git`
默认分支:`master`
本次读取提交:`fefd6dc`(2025-08-08)
仓库内容:
```text
OTAdevice/
├── httpserver.py
├── version.conf
└── goproject/
├── main.go
├── go.mod
├── go.sum
└── OTAdevice
```
部署到板端后的目标结构:
```text
/home/orangepi/car/master/
├── main # Car 生产二进制,OTA 替换目标
├── Deviceld.txt # 14 位设备号
├── version.conf # 当前 OTA 版本
├── backup/ # main_<版本> 备份
├── httpserver.py
└── goproject/
└── OTAdevice # gostart.service 启动目标
```
### 9.2 OTA 工作流程
1. 启动后循环读取 `Deviceld.txt`,文件不存在或为空时每 10 秒重试。
2. 读取 `version.conf`;文件不存在时视为空版本。
3. POST 到 `http://47.119.190.60/api/v1/ota/latestVersion`,请求体为 `{"device_id":"<设备号>"}`
4. 比较服务端 `data.version` 与本地版本。
5. 版本不同时停止 `carstart.service`
6.`master/main` 存在,备份为 `backup/main_<旧版本>`
7. 下载到 `master/main.tmp`
8. 计算 MD5,与服务端 `data.md5` 比较。
9. 校验通过后将临时文件提升为 `master/main`,写入 `version.conf` 并增加执行权限。
10. 重启 `carstart.service`,等待 3 秒后执行整机 reboot。
11. 版本相同则等待 30 分钟再次检查;请求失败等异常路径通常较快重试。
### 9.3 构建和部署 OTA
在 Orange Pi 上从源码构建:
```bash
cd /home/orangepi/car/master/goproject
go mod download
go build -o OTAdevice .
chmod 0755 OTAdevice
```
从其他平台交叉编译 ARM64:
```bash
cd goproject
GOOS=linux GOARCH=arm64 go build -o OTAdevice .
```
首次部署应先补齐目录和文件,再启动服务:
```bash
cd /home/orangepi/car/master
mkdir -p goproject backup
touch version.conf
chmod 0644 version.conf
chmod 0755 main goproject/OTAdevice
sudo systemctl daemon-reload
sudo systemctl enable --now carstart.service gostart.service
```
### 9.4 OTA 验收与回滚
OTA 测试必须在车辆架空或断开动力的测试设备上完成。至少验证:
- 相同版本不下载;
- 新版本能停止 Car、生成备份、下载、校验、替换和启动;
- MD5 不一致会删除临时文件并恢复旧版本;
- 下载失败后 Car 能恢复;
- 重启后 `carstart``gostart` 自动恢复;
- `version.conf``main``backup/` 权限正确;
- 更新后的 Car 能通过急停、控制、MQTT、视频和音频冒烟测试。
手工回滚示例:
```bash
sudo systemctl stop gostart.service carstart.service
ls -lh /home/orangepi/car/master/backup/
sudo cp /home/orangepi/car/master/backup/main_<版本> \
/home/orangepi/car/master/main
sudo chmod 0755 /home/orangepi/car/master/main
printf '%s' '<版本>' | sudo tee /home/orangepi/car/master/version.conf
sudo systemctl start carstart.service
sudo systemctl status carstart.service --no-pager -l
```
### 9.5 OTA 已知风险
- OTA API 使用 HTTP,下载 URL 也未在客户端强制 HTTPS;
- MD5 只能发现传输损坏,不能证明更新包来自可信发布方;
- 没有数字签名、证书固定、超时、灰度或更新后健康检查;
- `promoteTempToFinal()``chmod` 的返回值未被严格处理;
- 在确认 Car 真正健康前就写版本并准备重启;
- 回滚使用 rename,会消耗掉对应备份文件;
- OTA 可停止 Car 和重启整机,必须限制仓库、接口和服务权限。
## 10. HTTP Server
### 10.1 功能与接口
实现文件:`/home/orangepi/car/master/httpserver.py`。启动后固定等待 20 秒,通过连接 `8.8.8.8:80` 推断本机 IP,再绑定 `<本机IP>:8080`
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | `/` | 显示当前 `Deviceld.txt` 内容和设备号输入表单 |
| POST | `/submit` | 覆盖写入 `/home/orangepi/car/master/Deviceld.txt` |
| 其他 | 任意 | 返回 404 |
浏览器访问示例:
```text
http://192.168.0.104:8080/
```
修改设备号后应验证 14 位格式,并重启 Car 使设备身份、MQTT topic 和后端配置重新初始化:
```bash
cat /home/orangepi/car/master/Deviceld.txt
sudo systemctl restart carstart.service
```
### 10.2 启动与排错
```bash
sudo systemctl start httpstart.service
systemctl status httpstart.service --no-pager -l
journalctl -u httpstart.service -f
ss -lntp | grep ':8080'
curl -i http://192.168.0.104:8080/
```
如果没有监听 8080:
1. 检查脚本固定的 20 秒启动等待;
2. 检查 `get_local_ip()` 是否能通过路由选择获得本机 IP;
3. 检查 8080 是否被占用;
4. 检查服务是否因绑定失败而被 `Restart=always` 反复拉起;
5. 检查 `httpserver.py``Deviceld.txt` 权限。
### 10.3 HTTP Server 风险
- 无鉴权、无 CSRF 防护,局域网内任何可访问者都可能修改设备号;
- 绑定具体 IP 而非 `0.0.0.0`,换网后需要服务重启;
- 依赖到 `8.8.8.8` 的路由来获取本机 IP,无外网时可能直接退出;
- 只读取 1024 字节并手工解析 HTTP,不适合复杂请求;
- 未做 URL 解码和 14 位设备号格式校验;
- 修改设备号会影响设备类型、MQTT topic 和后端身份,生产网络应限制 8080 访问。
## 11. 配置、接口与端口
### 11.1 本地配置
| 项目 | 位置/值 | 说明 |
|---|---|---|
| 设备号 | `Deviceld.txt` | 文件名中是小写字母 `l`,不是 `DeviceId.txt` |
| OTA 版本 | `version.conf` | 当前部署版本,由 OTA 写入 |
| OTA 备份 | `backup/main_<版本>` | 旧 Car 二进制 |
| 日志配置 | `zlog.conf` | 运行时使用/生成 |
| 推流编译开关 | `modules/webrtcpush/webrtcpush_config.h` | 当前 `WEBRTCPUSH_USE_MPP=0` |
| WiFi 配置端口 | UDP 18888 | 监听所有地址 |
| IPC | `/tmp/device_msg.sock` | Unix Socket |
### 11.2 外部服务
- 设备配置:`https://fcrs-api.yd-ss.com/device/getConfig?deviceNo=<设备号>`
- 音量状态:`https://fcrs-api.yd-ss.com/api/drive/use/status/<...>`
- 原生 WebRTC 信令主机:`signal.yd-ss.com`
- Chromium 默认页面由后端配置或代码默认值产生;
- MQTT Broker 列表由后端配置下发,程序允许多节点部分成功。
- OTA 查询:`http://47.119.190.60/api/v1/ota/latestVersion`
所有 URL、Broker 和证书策略都应由配置管理,避免继续散落在 C 宏和源码中。
### 11.3 当前监听与相关端口
| 端口/套接字 | 用途 | 备注 |
|---|---|---|
| UDP 18888 | WiFi 配置、设备发现、推流调试 | 局域网可达,应限制可信网段 |
| TCP 1883(本机回环) | 本机 MQTT | 当前只监听 127.0.0.1/::1 |
| TCP 22 | SSH | 运维入口 |
| TCP 8080 | 设备号 HTTP Server | `httpstart.service` 启动后监听 |
| TCP 5555 | 未在本次核验中确认归属 | 接手人需确认 |
| `/tmp/device_msg.sock` | 本地设备消息 IPC | 主程序启动后创建 |
## 12. 视频与音频
### 12.1 三条视频路径
1. **Chromium**:当前编译开关选择的路径;依赖图形会话、`DISPLAY=:0`、XAUTHORITY、摄像头与浏览器稳定性。
2. **Go `go_video`**:后端 `moreInfo.webrtcMode=go``/home/orangepi/go_video/go_video` 存在时启用。
3. **C/libdatachannel**`WEBRTCPUSH_USE_MPP=1` 时尝试;使用 GStreamer、MPP/x264、libdatachannel,失败可回退 Chromium。
三条路径共用 `thread_open_browser`,设计上互斥。上线前必须记录该设备最终采用哪条路径。
### 12.2 当前原生推流关键参数
- H.264 24 fps,最低目标 22 fps;
- 初始码率约 1.06 Mbps,范围 0.8~3.0 Mbps;
- H.264 Baseline@L3.1;
- RTP 最大分片 1050 字节;
- 麦克风 Opus 64 kbps、48 kHz、单声道;
- 喇叭默认 `plughw:2,0`
- 信令主机 `signal.yd-ss.com`
### 12.3 音频注意事项
- 本地 MP3/TTS、WebRTC 麦克风采集和手机喊话可能竞争声卡/PulseAudio;
- 当前 USB 声卡位于 card 2,但枚举序号可能重启后变化,建议使用稳定的 ALSA by-id 或 udev 规则;
- 日志显示系统会将 Yundea USB 声卡的输入/输出设为默认;
- MQTT 音频 `volume` 是线性增益,代码钳位到 `[0, 2]`
## 13. 日志、监控与健康检查
### 13.1 日志位置
```text
/home/orangepi/car/master/log/log_YYYY-MM-DD.log
```
最近一次已检查的启动日志表明:设备识别为 0102、HTTP 配置成功、UDP 18888 与 IPC 启动、8 个 MQTT 节点中 7 个连接成功、Chromium 页面已拉起;同时存在一个 MQTT DNS 解析失败和 Tailscale AuthKey 无效。
### 13.2 最小健康检查
```bash
systemctl status carstart.service httpstart.service gostart.service --no-pager -l
pgrep -af '/home/orangepi/car/.*/main|OTAdevice|httpserver.py|chromium|go_video'
ss -lntup
tail -n 200 /home/orangepi/car/master/log/log_$(date +%F).log
```
业务健康至少应同时满足:
- 主进程常驻且无重启循环;
- 正确识别设备型号;
- 至少一个/预期数量 MQTT 节点连接成功;
- 验证开关查询成功;
- UDP 配网服务和 IPC 启动;
- 操作指令、急停、视频和音频按当前设备配置工作;
- 日志中无持续增长的 ERROR。
- OTA 版本文件、生产 `main` 和最新备份一致;
- HTTP Server 仅在需要时对可信局域网开放。
### 13.3 建议补充监控
- systemd 重启次数和退出码;
- MQTT 活跃连接数;
- 最近心跳时间;
- 视频首帧、重连次数、实际码率、丢包/PLI;
- 音频设备是否存在;
- `/dev/video0` 是否为可采集设备;
- 磁盘、日志目录、内存和 CPU;
- 后端配置接口耗时与失败率。
- OTA 最近检查时间、当前版本、下载/校验/回滚结果;
- HTTP Server 8080 暴露范围与设备号修改审计。
## 14. 常见故障排查
### 14.1 服务启动失败
1. `systemctl status carstart.service -l`
2. 检查 `ExecStart` 文件是否存在且可执行;
3. 检查工作目录、`Deviceld.txt``zlog.conf`
4. Car 可先手工运行 `./main` 获取首个 fatal 初始化错误;
5. 检查 wiringPi、I2C、声卡、摄像头权限。
当前 unit 路径符合 OTA 设计,但实机尚未部署 `master/main``goproject/OTAdevice`。应先从已验证的 `build/main` 发布生产 `main`,再部署 OTA 二进制。
### 14.2 OTA 不更新或更新后 Car 不启动
- 检查 `gostart.service` 状态与 journal;
- 检查 `Deviceld.txt``version.conf``main``main.tmp``backup/`
- 检查 OTA API 网络、返回版本、下载 URL 和 MD5;
- 检查 `main` 是否为 ARM64、是否有执行权限、动态库是否齐全;
- 检查 OTA 是否成功停止/重启 `carstart.service`
- 更新失败时先停止 `gostart`,避免重复覆盖,再手工回滚。
### 14.3 HTTP Server 无法访问
- 等待启动脚本固定的 20 秒;
- 检查 `httpstart.service`、8080 监听和防火墙;
- 检查板子是否有到外网的路由,以便 `get_local_ip()` 得到地址;
- 检查 IP 是否变化,变化后重启服务;
- 检查浏览器与设备是否在同一可达网段。
### 14.4 设备识别失败
- 检查 `Deviceld.txt` 是否存在、是否正好 14 位、是否有不可见字符;
- 检查第 3~6 位是否在 `device_identity.c` 注册;
- 检查型号是否在 `device_init.c` 有初始化配置;
- 不要随意修改设备号,topic、后端配置和设备动作都依赖它。
### 14.5 MQTT 不连接
- 检查 DNS、网关和系统时间;
- 检查后端配置接口返回的 Broker 列表;
- 允许单个节点失败,但需要确认活跃节点数;
- 检查 1883 出站策略和 Mosquitto 返回码;
- 禁止在日志中打印完整 JWT、密码或 token。
### 14.6 Chromium 无画面
- 检查当前模式确为 Chromium;
- 检查 `/dev/video0`、浏览器进程、`DISPLAY=:0` 与 XAUTHORITY;
- 检查页面 URL 是否带正确设备号;
- 检查是否被 `pkill chromium` 后未能重启;
- 检查 PulseAudio/摄像头权限和网页端信令。
### 14.7 C 原生推流失败
- 确认 `WEBRTCPUSH_USE_MPP=1` 后重新编译;
-`gst-inspect-1.0` 检查 `webrtcbin``v4l2src``opusenc``mpph264enc`/`x264enc`
- 检查信令 DNS/TLS、摄像头格式、MPP 设备和 ALSA/Pulse;
- 观察是否自动回退 Chromium;
- 不要同时手工启动多个推流实现。
### 14.8 无声音或声卡被占用
- `aplay -l``arecord -l``pactl list short sinks/sources`
- 确认 card 2 未因重启变为其他序号;
- 检查本地播报、WebRTC 喊话、麦克风是否同时占用;
- 检查 `/home/orangepi/audio/<语言>/` 资源文件;
- 先停止车辆动作再做大音量测试。
### 14.9 WiFi 配网失败
- 检查 UDP 18888 是否监听、是否被防火墙拦截;
- 手机与设备是否在同一广播域;
- 检查配置命令的 ACK/结果返回;
- 检查设备号和显示名生成是否正常;
- 修改网络前保留 SSH 回退通道,避免失联。
## 15. 安全、可靠性与技术债
### P0:立即处理
1. **部署不完整**:unit 已安装,但 Car 生产 `main`、OTA 二进制和版本文件缺失,三项服务均未启用。
2. **日志泄露凭据**:历史日志出现完整 JWT;应立刻轮换可能受影响的凭据,删除/受控归档旧日志,并在代码中只记录摘要。
3. **硬件安全测试缺少门禁**:发布/调试前必须架空车辆或断开动力,验证急停后再落地。
4. **设备号接口无鉴权**:HTTP Server 能直接覆盖设备身份,只能部署在受控网络。
### P1:近期处理
1. `go_deploy.c` 下载时关闭 TLS 证书和主机校验,存在中间人攻击风险;
2. 运行时依赖模块可能在程序启动阶段执行 `apt-get`、重载摄像头模块或 USB reset,增加不可预测性;
3. 服务使用 root,权限过大;应评估最小能力集、设备组权限或专用用户;
4. UDP 18888 监听所有网卡,调试/配网命令需要鉴权、来源限制和重放保护;
5. Tailscale 日志显示 AuthKey 缺失或无效,应明确是否仍为生产依赖;
6. 稳定配置散落在源码宏中,修改需要重新编译。
7. OTA 使用 HTTP + MD5,缺少更新包签名和更新后健康检查。
### P2:持续改进
1. 建立版本化发布目录、可审计发布脚本和一键回滚;
2. 为设备号解析、消息分发、速度边界和急停逻辑增加单元测试;
3. 为每类硬件建立 HIL 冒烟测试;
4. 统一 Chromium、Go、C 三条推流路线的负责人和淘汰策略;
5. 增加 CI 构建、静态分析、依赖漏洞和许可证检查;
6. 补齐服务 Watchdog、健康检查和结构化指标。
## 16. 常用变更流程
### 16.1 新增设备型号
1. 分配四位型号编码并确认 14 位设备号规则;
2. 在设备身份映射中注册;
3. 在 GPIO/串口初始化表中加入资源;
4. 新增对应控制文件并接入统一控制/停止入口;
5. 校验引脚、电压、PWM 上限、方向和急停;
6. 更新 MQTT 消息适配;
7. 在架空状态完成正反转、转向、失联停车和异常恢复测试;
8. 更新设备清单和发布记录。
### 16.2 修改 MQTT 消息
1. 记录 `message_type`、请求/响应 JSON、topic 和兼容策略;
2. 对空字段、类型错误、重复消息、乱序和超时做防御;
3. 保留旧版本兼容期;
4. 日志只记录消息类型、设备号掩码和结果,不记录 token;
5. 与 App/后端联调后再合入。
### 16.3 修改推流
1. 明确目标路径;
2. 固化摄像头、编码、信令、音频输入输出基线;
3. 测试首帧、10 分钟稳定性、弱网、切网、换手机、音视频同时运行;
4. 检查资源释放和回退路径;
5. 记录关键参数与实测结果,禁止只凭“有画面”上线。
### 16.4 发布 Car 与 OTA
1. Car 在 `build/main` 完成编译和冒烟测试;
2. 记录 SHA-256 后复制为 `master/main`
3. OTA 在测试板验证 API、下载、MD5、备份、替换和回滚;
4. 补齐 `version.conf``backup/` 和文件权限;
5. 校验三份 unit,执行 `daemon-reload`
6. 先启动 HTTP/Car,再启动 OTA;
7. 验证服务自启、整机重启和更新后业务健康。
## 17. 接手验收清单
### 17.1 账号与资料
- [ ] 已获得代码仓库、后端测试环境、MQTT、设备管理和日志访问权限;
- [ ] 已确认敏感凭据交付渠道,不在本文或 Git 中保存;
- [ ] 已明确生产/测试设备号和硬件负责人;
- [ ] 已确认三个 Git remote 的用途与主仓库。
- [ ] 已获得 OTAdevice 仓库与 OTA 后端接口权限。
### 17.2 开发与构建
- [ ] 能从干净工作区完成 Release/Debug 编译;
- [ ] 能说明可执行文件、配置、日志和音频资源位置;
- [ ] 能定位设备识别、控制、MQTT、配网和推流代码;
- [ ] 已保存已知可用版本的提交号和二进制哈希。
- [ ] 能构建/部署 `goproject/OTAdevice` 并说明 `version.conf``backup/` 的作用。
### 17.3 部署与运行
- [ ] 三项 ExecStart 目标文件均存在且权限正确;
- [ ] 三份 unit 位于 `/etc/systemd/system/`,已通过 verify;
- [ ] 已决定是否启用开机自启;
- [ ] 重启后设备可自动恢复;
- [ ] 能查看服务日志并判断健康状态;
- [ ] 能在 10 分钟内完成回滚。
- [ ] 已完成一次 OTA 全流程测试和一次失败回滚测试。
### 17.4 功能与安全
- [ ] 型号识别正确;
- [ ] 前进、后退、转向与急停正确;
- [ ] 失联后按预期停车;
- [ ] MQTT 心跳、验证和控制正常;
- [ ] 视频、麦克风、喇叭与本地播报正常;
- [ ] WiFi 配网和断网恢复正常;
- [ ] 日志不再输出完整 JWT、密码或 AuthKey。
- [ ] TCP 8080 仅允许可信网络访问,设备号输入经过人工复核。
## 18. 建议阅读顺序
1. `app/main/main.c`:掌握初始化阶段;
2. `app/main/pthread_open.c`:掌握常驻线程;
3. `app/device_identity/device_identity.c`:掌握型号识别;
4. `drivers/gpio/device_init.c`:掌握资源初始化;
5. `drivers/devicecontrol/devcontrol_common.c` 和当前车型实现;
6. `modules/mqtt/`:掌握控制和状态链路;
7. `modules/http/http_config_mqtt.c`:掌握后端配置;
8. `modules/wifi_config/``modules/device_debug/`
9. `modules/browser/``modules/go_deploy/``modules/webrtcpush/`
10. OTA 仓库 `goproject/main.go`:掌握版本、备份、下载和回滚。
11. OTA 仓库 `httpserver.py`:掌握设备号录入服务。
12. `/etc/systemd/system/{carstart,httpstart,gostart}.service`
## 19. 附录:实用命令
### 环境快照
```bash
uname -a
cat /etc/os-release
ip -brief address
df -h /
free -h
```
### Git 快照
```bash
cd /home/orangepi/car/master
git branch --show-current
git status --short
git log -8 --date=short --pretty=format:'%h %ad %s'
```
### 构建
```bash
cmake -S . -B build -DCAR_BUILD_TYPE=Release
cmake --build build -j2
file build/main
sha256sum build/main
sudo install -o root -g root -m 0755 build/main main
```
### 服务与日志
```bash
systemctl cat carstart.service httpstart.service gostart.service
systemctl status carstart.service httpstart.service gostart.service --no-pager -l
systemctl is-enabled carstart.service httpstart.service gostart.service
journalctl -u carstart.service -u httpstart.service -u gostart.service -n 300 --no-pager
tail -n 200 log/log_$(date +%F).log
```
### 设备与媒体
```bash
ls -l /dev/video*
v4l2-ctl --list-devices
aplay -l
arecord -l
pactl list short sinks
pactl list short sources
```
### 网络与进程
```bash
ss -lntup
pgrep -af 'main|OTAdevice|httpserver.py|chromium|go_video'
getent hosts fcrs-api.yd-ss.com
getent hosts signal.yd-ss.com
```
---
## 20. 交接结论
项目由 Car、OTAdevice 和设备号 HTTP Server 三部分共同组成。`carstart.service` 指向 `master/main` 与 OTA 的下载目标一致,是有意的发布约定;`build/main` 仅是编译产物。三份 systemd 配置已存在于正确目录,但当前板端仍缺生产 Car/OTA 文件且未启用服务。接手人应先按本文补齐部署结构并验证三服务自启,再在安全条件下完成 OTA 成功、失败和回滚测试,同时治理 HTTP 设备号入口及凭据日志风险。
本文不包含任何密码、私钥、MQTT 凭据、JWT 或 Tailscale AuthKey。相关秘密应通过独立的安全渠道交接。
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment