Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
car-controlserver
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
wenzhongjian
car-controlserver
Commits
f3e6d081
Commit
f3e6d081
authored
Jul 31, 2026
by
957dd
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
优化了验证
parent
c4f0ffb2
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
1402 additions
and
7 deletions
+1402
-7
main
build/main
+0
-0
mqtt_infor_handle.c
modules/mqtt/mqtt_infor_handle.c
+15
-0
mqtt_verify.c
modules/mqtt/mqtt_verify.c
+407
-6
mqtt_verify.h
modules/mqtt/mqtt_verify.h
+1
-0
zlog.conf
zlog.conf
+1
-1
香橙派车控项目交接文档.docx
交接文档/香橙派车控项目交接文档.docx
+0
-0
香橙派车控项目交接文档.md
交接文档/香橙派车控项目交接文档.md
+978
-0
No files found.
build/main
View file @
f3e6d081
No preview for this file type
modules/mqtt/mqtt_infor_handle.c
View file @
f3e6d081
...
...
@@ -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
(
"验证不通过"
);
}
}
...
...
modules/mqtt/mqtt_verify.c
View file @
f3e6d081
#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"
);
}
}
modules/mqtt/mqtt_verify.h
View file @
f3e6d081
...
...
@@ -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)*/
...
...
zlog.conf
View file @
f3e6d081
...
...
@@ -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
交接文档/香橙派车控项目交接文档.docx
0 → 100644
View file @
f3e6d081
File added
交接文档/香橙派车控项目交接文档.md
0 → 100644
View file @
f3e6d081
# 香橙派车控项目交接文档
# 香橙派车控项目交接文档
> 项目:飞驰人生设备端(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。相关秘密应通过独立的安全渠道交接。
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment