diff --git a/.gitignore b/.gitignore
index 242061c..65311da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
PY32F040开发框架/*
-.vscode/
\ No newline at end of file
+.vscode/
+模组功能分期清单.md
+.clang-format
\ No newline at end of file
diff --git a/Doc/MCU方案接入协议通讯文档.md b/Doc/MCU方案接入协议通讯文档.md
index 049929e..d755b37 100644
--- a/Doc/MCU方案接入协议通讯文档.md
+++ b/Doc/MCU方案接入协议通讯文档.md
@@ -1,4 +1,4 @@
-# MCU方案接入协议通讯文档
+# MCU方案接入协议通讯文档
# 修订记录
diff --git a/Doc/SOC方案接入通信协议文档.md b/Doc/SOC方案接入通信协议文档.md
new file mode 100644
index 0000000..4d3bf1a
--- /dev/null
+++ b/Doc/SOC方案接入通信协议文档.md
@@ -0,0 +1,335 @@
+# SOC方案接入通信协议文档
+
+# 修订记录
+
+|文档版本|协议版本|日期|编写人|修订类型|修订内容|
+|---|---|---|---|---|---|
+|V1\.0\.0|V1\.0|2026\-07\-08|麦田|新建|首次发布SOC方案接入协议通讯文档|
+
+---
+
+# 协议说明
+
+- 数据采用**大端模式**(Big Endian)传输。
+
+- 数据帧格式: **包头 \+ 包长度 \+ 命令 \+ 序列号 \+ 数据 \+ 校验和**。
+
+- 包头固定为 `0x55AA`。
+
+- 包长度表示 **命令开始至校验和结束** 的数据长度。
+
+- 命令字(Command)用于区分不同功能。
+
+- 序列号(Seq)由请求方递增生成,ACK需原样返回,用于匹配请求与响应。
+
+- 默认所有命令采用 **一问一答机制**,发送方发送命令后,接收方必须返回对应 ACK。
+
+- ACK 数据包包含 Result 字段,用于返回执行结果。
+
+- 超时未收到 ACK 时,发送方可进行重试,最大重试次数建议为 3 次。
+
+- 数据点标志(Bitmap)由 DP 配置文档自动生成,一个 Bit 对应一个 DP。
+
+- 数据点值按照 Bitmap 中 Bit 置位顺序依次排列。
+
+- 读取命令通过 Bitmap 指定需要读取的 DP,Bitmap 全 1 表示读取全部 DP。
+
+- BLE 模组主要负责数据透传、蓝牙连接及状态管理,业务数据由 MCU 处理。
+
+- 校验方式:累加和校验(Sum Check),计算范围为 **包长度字段至校验和前一个字节**。
+
+## Result 定义:
+
+|值|含义|
+|---|---|
+|0x00|成功|
+|0x01|参数错误|
+|0x02|设备忙|
+|0x03|不支持该操作|
+|0x04|校验错误|
+|0x05|执行失败|
+
+### OTA 专用错误码
+
+|值|含义|
+|---|---|
+|0x20|无需升级(版本一致)|
+|0x21|OTA 版本低于当前版本|
+|0x22|Flash 空间不足|
+|0x23|OTA 状态错误|
+|0x24|OTA文件检验失败|
+|0x25|升级失败|
+
+# 基本通讯协议
+
+## 设备上报版本信息
+
+> 设备绑定上网关之后,主动上报固件版本到服务器,此命令通过网关的应答格式判断是否发送成功。。
+>
+>
+
+### 设备 =\> 网关
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|0x55AA|
+|2|包长度|2|len\(命令\.\.\.校验和\)|
+|3|命令|1|0x41|
+|4|序列号|1|与请求的序列号一致|
+|5|Result|1|执行结果(见 Result 定义)|
+|6|BLE Version|3|固件版本号,格式:主版本号\(1Byte\)\+次版本号\(1Byte\)\+修订版本号\(1Byte\),显示格式为:`Vx.x.x`|
+|7|MCU Version|3|固件版本号,格式:主版本号\(1Byte\)\+次版本号\(1Byte\)\+修订版本号\(1Byte\),显示格式为:`Vx.x.x`,如MCU不存在则填0|
+|8|Hardware Version|3|硬件版本号,格式:硬件主版本号\(1Byte\)\+硬件次版本号\(1Byte\)\+硬件修订版本号\(1Byte\),显示格式:`Vx.x.x`|
+|9|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+#### Version版本格式说明
+
+|字节|字段名称|内容说明|
+|---|---|---|
+|Byte0|主版本号|产品架构、通信协议等重大变化时递增|
+|Byte1|次版本号|新增功能或功能优化时递增|
+|Byte2|修订版本号|Bug修复、参数调整、性能优化时递增|
+
+#### 示例
+
+|MCU Version|版本号|
+|---|---|
+|0x01 0x02 0x05|V1\.2\.5|
+|0x05 0x02 0x21|V5\.2\.33|
+
+### 网关 =\> 设备
+
+通过网关的应答格式判断是否发送成功
+
+## APP控制设备
+
+### APP =\> 设备
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|0x55AA|
+|2|包长度|2|len\(命令\.\.\.校验和\)|
+|3|命令|1|0x03(下发控制)|
+|4|序列号|1|APP递增生成,ACK原样回传|
+|5|数据点标志|x(\(DP数量\+7\)/8)|一个字节可代表8个数据点|
+|6|数据点值|x|数据值|
+|7|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+### 设备 =\> APP
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|0x55AA|
+|2|包长度|2|len\(命令\.\.\.校验和\)|
+|3|命令|1|0x04(应答下发控制)|
+|4|序列号|1|与请求的序列号一致|
+|5|Result|1|执行结果(见 Result 定义)|
+|6|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+---
+
+## APP读取设备
+
+### APP =\> 设备
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|0x55AA|
+|2|包长度|2|len\(命令\.\.\.校验和\)|
+|3|命令|1|0x05(下发读取)|
+|4|序列号|1|APP递增生成,ACK原样回传|
+|5|数据点标志|x(\(DP数量\+7\)/8)|指定需要读取的 DP,Bitmap 全 1 表示读取全部 DP|
+|6|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+### 设备=\> APP
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|0x55AA|
+|2|包长度|2|len\(命令\.\.\.校验和\)|
+|3|命令|1|0x06(应答读取)|
+|4|序列号|1|与请求的序列号一致|
+|5|数据点标志|x(\(DP数量\+7\)/8)|返回的数据点标志|
+|6|数据点值|x|数据值|
+|7|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+---
+
+## 设备主动上报数据
+
+此命令直接上报到服务器,通过网关的应答格式判断是否发送成功。
+
+### 设备 =\> APP/IPC
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|0x55AA|
+|2|包长度|2|len\(命令\.\.\.校验和\)|
+|3|命令|1|0x07(主动上报)|
+|4|序列号|1|设备递增生成|
+|5|数据点标志|x(\(DP数量\+7\)/8)|一个字节可代表8个数据点|
+|6|数据点值|x|数据值|
+|7|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+### APP/IPC =\> 设备
+
+通过网关的应答格式判断是否发送成功
+
+---
+
+# 扩展通讯协议
+
+## APP向设备推送OTA升级
+
+> **功能说明:**
+APP 将与设备升级相关的信息发送给设备,设备根据这些信息判断是否允许OTA升级,并返回升级准备结果。
+
+### APP =\> 设备
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|固定 `0x55AA`|
+|2|包长度|2|len\(命令\.\.\.校验和\)|
+|3|命令|1|0x30|
+|4|序列号|1|数据包序列号|
+|5|固件类型(Type)|1|0x01:MCU
0x02:BLE|
+|6|固件版本(Firmware Version)|3|OTA 固件版本号|
+|7|固件长度(Payload Length)|4|OTA Bin 数据长度|
+|8|固件 CRC32(Payload CRC32)|4|OTA Bin 数据 CRC32 校验值|
+|9|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+### 设备 =\> APP
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|固定 `0x55AA`|
+|2|包长度|2|`len(命令...校验和)`|
+|3|命令|1|0x31|
+|4|序列号|1|与请求保持一致|
+|5|Result|1|执行结果(见 Result OTA 专用错误码定义)|
+|6|Max Data Length|2|设备 单次允许接收的最大 Payload 长度(Byte)|
+|7|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+---
+
+## APP向设备推送OTA数据
+
+> **功能说明:**
+APP 根据 设备 返回的 **Max Data Length** 对 OTA Bin 进行分片,并按顺序发送给设备。
+设备 每接收一包数据后立即返回应答,APP 收到成功应答后继续发送下一包。
+
+### APP =\> 设备
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|固定 `0x55AA`|
+|2|包长度|2|`len(命令...校验和)`|
+|3|命令|1|0x32|
+|4|序列号|1|数据包序列号|
+|5|分片序号|2|当前数据包的分片序号,分片序号从1开始计算|
+|6|总分片数|2||
+|7|Payload Data|x|OTA Bin 数据,长度 ≤ Max Data Length|
+|8|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+### 设备 =\> APP
+
+> **功能说明:**
+设备 成功接收并处理当前 OTA 数据包后返回 Result。
+APP收到成功应答后继续发送下一包 OTA 数据;若返回失败,则停止本次 OTA 升级。
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|固定 `0x55AA`|
+|2|包长度|2|`len(命令...校验和)`|
+|3|命令|1|0x33|
+|4|序列号|1|与请求保持一致|
+|5|Result|1|执行结果(见 Result 定义)|
+|6|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+## 设备通知APP模组OTA升级完成
+
+> **功能说明:
+** 设备完成OTA升级后,主动通知APP当前升级结果及固件版本信息。
+
+### 设备 =\> APP
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|固定 `0x55AA`|
+|2|包长度|2|`len(命令...校验和)`|
+|3|命令|1|0x34|
+|4|序列号|1|数据包序列号|
+|5|类型(Type)|1|0x01:MCU
0x02:BLE|
+|6|OTA Result|1|OTA结果|
+|7|Firmware Version|3|当前固件版本|
+|8|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+### APP =\> 设备
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|固定 `0x55AA`|
+|2|包长度|2|`len(命令...校验和)`|
+|3|命令|1|0x35|
+|4|序列号|1|与请求保持一致|
+|5|Result|1|执行结果(见 Result 定义)|
+|6|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+---
+
+## APP向设备通知OTA停止
+
+> **功能说明:**
+APP 主动通知设备终止当前 OTA 升级流程。设备收到后立即停止 OTA 数据接收,释放 OTA 相关资源,并返回处理结果。
+
+### APP =\> 设备
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|固定 `0x55AA`|
+|2|包长度|2|`len(命令...校验和)`|
+|3|命令|1|0x36|
+|4|序列号|1|数据包序列号|
+|5|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+### 设备 =\> APP
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|固定 `0x55AA`|
+|2|包长度|2|`len(命令...校验和)`|
+|3|命令|1|0x37|
+|4|序列号|1|与请求保持一致|
+|5|Result|1|执行结果(见 Result 定义)|
+|6|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+---
+
+## 设备向APP通知OTA停止
+
+> **功能说明:**
+设备在 OTA 过程中因内部异常无法继续升级时,主动通知 APP 停止当前 OTA。APP 收到通知后立即停止发送 OTA 数据,并返回应答。
+
+### 设备 =\> APP
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|固定 `0x55AA`|
+|2|包长度|2|`len(命令...校验和)`|
+|3|命令|1|0x38|
+|4|序列号|1|数据包序列号|
+|5|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+### APP =\> 设备
+
+|序号|字段名称|字节长度|内容说明|
+|---|---|---|---|
+|1|包头|2|固定 `0x55AA`|
+|2|包长度|2|`len(命令...校验和)`|
+|3|命令|1|0x39|
+|4|序列号|1|与通知保持一致|
+|5|Result|1|`0x00`:已收到并停止 OTA|
+|6|校验和|1|和校验(包长度\.\.\.校验和前一个字节)|
+
+
+
diff --git a/JBao_JL7016_MOD_FW/AC701N.cbp b/JBao_JL7016_MOD_FW/AC701N.cbp
index 90dc304..2863999 100644
--- a/JBao_JL7016_MOD_FW/AC701N.cbp
+++ b/JBao_JL7016_MOD_FW/AC701N.cbp
@@ -177,6 +177,7 @@
+
@@ -759,43 +760,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -821,6 +785,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1271,10 +1285,6 @@
-
-
-
-
-
+
diff --git a/JBao_JL7016_MOD_FW/Makefile b/JBao_JL7016_MOD_FW/Makefile
index 9aebae2..ffcd9bd 100644
--- a/JBao_JL7016_MOD_FW/Makefile
+++ b/JBao_JL7016_MOD_FW/Makefile
@@ -251,6 +251,7 @@ INCLUDES := \
-Iapps/usr_mcu_bridge \
-Iapps/usr_mcu_bridge/protocol \
-Iapps/usr_mcu_bridge/business \
+ -Iapps/usr_periph \
-I$(SYS_INC_DIR) \
@@ -624,6 +625,10 @@ c_SRC_FILES := \
apps/usr_mcu_bridge/business/mcu_pairing.c \
apps/usr_mcu_bridge/business/mcu_link_status.c \
apps/usr_mcu_bridge/business/mcu_business.c \
+ apps/usr_mcu_bridge/business/mcu_mgmt.c \
+ apps/usr_mcu_bridge/business/mcu_time.c \
+ apps/usr_mcu_bridge/business/mcu_version.c \
+ apps/usr_periph/usr_rtc.c \
apps/usr_le_code/ble_proto.c \
apps/usr_le_code/bleproto.c \
apps/usr_le_code/bleproto_packer.c \
diff --git a/JBao_JL7016_MOD_FW/apps/common/third_party_profile/jieli/JL_rcsp/bt_trans_data/le_rcsp_adv_module.c b/JBao_JL7016_MOD_FW/apps/common/third_party_profile/jieli/JL_rcsp/bt_trans_data/le_rcsp_adv_module.c
index e94d728..bf37547 100644
--- a/JBao_JL7016_MOD_FW/apps/common/third_party_profile/jieli/JL_rcsp/bt_trans_data/le_rcsp_adv_module.c
+++ b/JBao_JL7016_MOD_FW/apps/common/third_party_profile/jieli/JL_rcsp/bt_trans_data/le_rcsp_adv_module.c
@@ -526,6 +526,11 @@ static int att_write_callback(hci_con_handle_t connection_handle, uint16_t att_h
void usr_ble_send_data(u8* data,u16 len)
{
+ /* CCC 未写时 ATT 发数会失败(常见 app_send_fail:-93),等主机使能通知再发 */
+ if (get_ble_work_state() != BLE_ST_NOTIFY_IDICATE) {
+ log_info("ble send skip, wait CCC");
+ return;
+ }
if (app_send_user_data_check(len))
{
app_send_user_data(ATT_CHARACTERISTIC_15f1e601_a277_43fc_a484_dd39ef8a9100_01_VALUE_HANDLE, data, len, ATT_OP_AUTO_READ_CCC);
@@ -538,7 +543,6 @@ void usr_ble_send_data(u8* data,u16 len)
// printf("%c",data[i]);
// }
// printf("\n");
-
}
static int app_send_user_data(u16 handle, u8 *data, u16 len, u8 handle_type)
{
@@ -961,6 +965,15 @@ u16 usr_get_conn_service()
{
return con_handle;
}
+
+/**
+ * @brief 主机是否已写 CCC,允许 GATT notify/indicate
+ * @return 1=可发数(BLE_ST_NOTIFY_IDICATE),0=还不行
+ */
+u8 usr_ble_is_notify_ready(void)
+{
+ return (get_ble_work_state() == BLE_ST_NOTIFY_IDICATE) ? 1 : 0;
+}
void ble_app_disconnect(void)
{
ble_disconnect(NULL);
diff --git a/JBao_JL7016_MOD_FW/apps/earphone/app_main.c b/JBao_JL7016_MOD_FW/apps/earphone/app_main.c
index 5bbca73..0082712 100644
--- a/JBao_JL7016_MOD_FW/apps/earphone/app_main.c
+++ b/JBao_JL7016_MOD_FW/apps/earphone/app_main.c
@@ -17,6 +17,7 @@
#include "dev_manager/dev_manager.h"
#include "update_loader_download.h"
#include "mcu_bridge.h"
+#include "usr_rtc.h"
#ifndef CONFIG_MEDIA_NEW_ENABLE
#ifndef CONFIG_MEDIA_DEVELOP_ENABLE
@@ -292,6 +293,7 @@ void app_main()
}
app_var_init();
+ usr_rtc_init(); /* 打开硬件 RTC,后续对时/0x1D 都走这里 */
mcu_bridge_init();
#if TCFG_MC_BIAS_AUTO_ADJUST
diff --git a/JBao_JL7016_MOD_FW/apps/earphone/board/br28/board_jl701n_demo.c b/JBao_JL7016_MOD_FW/apps/earphone/board/br28/board_jl701n_demo.c
index 6d682a1..51529d6 100644
--- a/JBao_JL7016_MOD_FW/apps/earphone/board/br28/board_jl701n_demo.c
+++ b/JBao_JL7016_MOD_FW/apps/earphone/board/br28/board_jl701n_demo.c
@@ -586,9 +586,9 @@ GSENSOR_PLATFORM_DATA_END();
-#if 1
+#if TCFG_APP_RTC_EN
-#define RTC_CLK_RES_SEL CLK_SEL_32K //rtc时钟源选择:CLK_SEL_32K/CLK_SEL_12M/CLK_SEL_24M
+#define RTC_CLK_RES_SEL CLK_SEL_LRC // 开发板无 32K,用 LRC;量产模组有晶振时改 CLK_SEL_32K
const struct sys_time def_sys_time = { //初始一下当前时间
.year = 2020,
@@ -615,7 +615,7 @@ RTC_DEV_PLATFORM_DATA_BEGIN(rtc_data)
.cbfun = NULL, //闹钟中断的回调函数,用户自行定义
/* .cbfun = alarm_isr_user_cbfun, */
RTC_DEV_PLATFORM_DATA_END()
-#endif // 0
+#endif /* TCFG_APP_RTC_EN */
@@ -632,7 +632,9 @@ REGISTER_DEVICES(device_table) = {
#if TCFG_OTG_MODE
{ "otg", &usb_dev_ops, (void *)&otg_data},
#endif
+#if TCFG_APP_RTC_EN
{ "rtc", &rtc_dev_ops , (void *)&rtc_data},
+#endif
};
/************************** imu sensor ****************************/
diff --git a/JBao_JL7016_MOD_FW/apps/earphone/include/app_main.h b/JBao_JL7016_MOD_FW/apps/earphone/include/app_main.h
index f7998ef..6eee49e 100644
--- a/JBao_JL7016_MOD_FW/apps/earphone/include/app_main.h
+++ b/JBao_JL7016_MOD_FW/apps/earphone/include/app_main.h
@@ -1,6 +1,8 @@
#ifndef APP_MAIN_H
#define APP_MAIN_H
+#include "generic/typedef.h" /* u8/u16/u32 等杰理类型,本头必须自包含 */
+
typedef struct _USR_VAR {
u8 usr_start_write_flash ;
u8 usr_plan_cnt ;
@@ -37,12 +39,10 @@ typedef struct _USR_VAR {
u32 usr_data_index ;
u32 usr_allow_to_flash ;
u32 usr_ota_data_size ;
- u32 usr_cureent_utc ;
int usr_double_ota_timer_id ;
int ota_timer ;
int usr_ota_data_id ;
- int usr_timezoneOffset ;
} USR_VAR;
diff --git a/JBao_JL7016_MOD_FW/apps/usr_le_code/bleproto_packer.h b/JBao_JL7016_MOD_FW/apps/usr_le_code/bleproto_packer.h
index 92df46f..9e4ce41 100644
--- a/JBao_JL7016_MOD_FW/apps/usr_le_code/bleproto_packer.h
+++ b/JBao_JL7016_MOD_FW/apps/usr_le_code/bleproto_packer.h
@@ -381,7 +381,7 @@ typedef struct bleproto_ycmd {
// E_BLEPROTO_SERVICE_ID_DEVICEINFO
//////////////////////////////////////////////////////////////////////
typedef struct bleproto_deviceinfo_req {
- uint64_t t; ///< utcms时间戳
+ uint64_t t; ///< Unix 时间戳,单位秒。网关从子设备连上起计时,每 12 小时同步一次
int gmtoff; ///< 时区偏移,单位秒,例如东八区(8x60x60=28800秒)
} bleproto_deviceinfo_req_t;
diff --git a/JBao_JL7016_MOD_FW/apps/usr_le_code/usr_le_adv.c b/JBao_JL7016_MOD_FW/apps/usr_le_code/usr_le_adv.c
index fc54e89..e0d81d6 100644
--- a/JBao_JL7016_MOD_FW/apps/usr_le_code/usr_le_adv.c
+++ b/JBao_JL7016_MOD_FW/apps/usr_le_code/usr_le_adv.c
@@ -99,7 +99,7 @@ void usr_write_pair_info()
*/
void usr_clear_pair_info()
{
- printf("usr_clear_pair_info\n");
+ printf("usr_clear_pair_info_reset\n");
memset(&usr_auth_data,0x00,sizeof(usr_auth_data));
usr_auth_data.paired_flag=0; /* 回到未配网 */
syscfg_write(CFG_USER_PAIR_INFO, (u8 *)&usr_auth_data, sizeof(usr_auth_data));
@@ -112,7 +112,7 @@ void usr_clear_pair_info()
*/
void usr_clear_pair_info_noreset()
{
- printf("usr_clear_pair_info2\n");
+ printf("usr_clear_pair_info_noreset\n");
memset(&usr_auth_data,0x00,sizeof(usr_auth_data));
usr_auth_data.paired_flag=0; /* 回到未配网,后续由 usr_goto_pair 决定是否绑广播 */
syscfg_write(CFG_USER_PAIR_INFO, (u8 *)&usr_auth_data, sizeof(usr_auth_data));
@@ -138,6 +138,18 @@ void usr_goto_pair_mode()
usr_ble_adv_init();
}
+/**
+ * @brief 断开 BLE 并停止广播(模组复位/重启前)
+ */
+void usr_ble_disconnect_and_stop_adv(void)
+{
+ printf("usr_ble_disconnect_and_stop_adv\n");
+ ble_multi_trans_disconnect();
+ usr_var.usr_goto_pair = 0;
+ usr_var.usr_adv_flag = 0;
+ ble_trans_module_enable(0);
+}
+
/**
* @brief 按当前标志刷新广播策略(不一定开广播)
* @note 见文件头「三个标志」说明;未拿身份 / 未配网时保持不广播
@@ -199,14 +211,6 @@ void usr_ble_adv_init()
if(le_timer_handle==0)le_timer_handle = sys_timer_add(NULL, usr_ble_adv_updata, 500);
}
-/**
- * @brief 获取是否已配网
- * @return 1=已配网(paired_flag),0=未配网
- */
-/**
- * @brief 获取是否已配网
- * @return 1=已配网(paired_flag),0=未配网
- */
/**
* @brief 获取是否已配网
* @return paired_flag:1=已配网,0=未配网
@@ -239,8 +243,9 @@ void usr_ble_adv_updata()
static u16 usr_goto_slow_adv=0;
if(usr_var.usr_adv_flag)
{
+ /* usr_adv_flag=1:把 muti_adv_data 刷进协议栈并开广播,用完即清 */
ble_trans_module_enable(0);
- usr_var.usr_adv_flag=0; /* 一次性触发,用完即清 */
+ usr_var.usr_adv_flag=0;
muti_make_set_adv_data(muti_adv_data,muti_adv_data_len);
printf("###muti_adv_data = ");
printf_buf(muti_adv_data,muti_adv_data_len);
@@ -249,10 +254,12 @@ void usr_ble_adv_updata()
if(usr_get_conn_service()==0)
{
+ /* 未连接:回连快广播(80)持续约 60s(500ms×120)后切慢广播 */
if(usr_get_adv_interval()==80)
{
if((usr_var.usr_goto_pair==0)&&(usr_auth_data.paired_flag))
{
+ /* 已配网回连,且不是绑广播、不是 OTA 成功态,才开始计时 */
if(usr_var.usr_ota_succ_flag==0)
{
usr_goto_slow_adv++;
@@ -262,23 +269,24 @@ void usr_ble_adv_updata()
{
usr_goto_slow_adv=0;
usr_set_adv_interval(80*10);
- usr_var.usr_adv_flag=1;
+ usr_var.usr_adv_flag=1; /* 下一拍刷新成慢间隔 */
}
}
}
else
{
- usr_goto_slow_adv=0;
+ usr_goto_slow_adv=0; /* 已经是慢广播,计数清掉 */
}
}
else
{
- usr_goto_slow_adv=0;
+ usr_goto_slow_adv=0; /* 已连接,不需要慢广播计时 */
}
if(usr_get_ota_status()==0)
{
+ /* 不在 OTA:未连接则退出 OTA 残留(ota_succ 时不退) */
if(usr_get_conn_service()==0)
{
if(usr_var.usr_ota_succ_flag==0)usr_ota_exit();
@@ -312,7 +320,7 @@ void usr_ota_process()
static u8 cnt = 0;
if (cnt == 0)
{
- ble_multi_trans_disconnect();
+ ble_multi_trans_disconnect(); /* 第一次:先断开,好换 OTA 广播地址 */
}
if (usr_get_conn_service() == 0)
@@ -320,17 +328,17 @@ void usr_ota_process()
cnt++;
if (cnt == 2)
{
- usr_ble_adv_updata_OTA();
+ usr_ble_adv_updata_OTA(); /* 断连后第二拍:改 MAC 并发 OTA 广播 */
}
if (cnt >= 60)
{
cnt = 0;
- cpu_reset();
+ cpu_reset(); /* 约 60s 仍没连上 OTA,复位退出 */
}
}
else
{
- cnt = 1;
+ cnt = 1; /* 已连上:保持断开过的状态,不再累加复位计时 */
}
}
diff --git a/JBao_JL7016_MOD_FW/apps/usr_le_code/usr_le_api.h b/JBao_JL7016_MOD_FW/apps/usr_le_code/usr_le_api.h
index 71e3b9c..7264919 100644
--- a/JBao_JL7016_MOD_FW/apps/usr_le_code/usr_le_api.h
+++ b/JBao_JL7016_MOD_FW/apps/usr_le_code/usr_le_api.h
@@ -2,11 +2,12 @@
* @file usr_le_api.h
* @brief usr_le_code 对外 API(广播 / 收发 / 配网 / MCU 帧转发)
* @author cyWu <1917507415@qq.com>
- * @date 2026.08.06
- * @version V1.1.0
+ * @date 2026.08.13
+ * @version V1.2.0
* @history
* - V1.0.0, 2026.08.03, cyWu, 首次发布
* - V1.1.0, 2026.08.06, cyWu, 增加 MCU 帧 BLE 发送接口,解耦模组桥
+ * - V1.2.0, 2026.08.13, cyWu, P1:复位前断连停广播;0x41 走 cmd100 上报
******************************************************************************/
#ifndef __USR_LE_API_H__
@@ -39,9 +40,10 @@ void usr_le_data_recieve(uint8_t *data, uint16_t len);
* @brief 将 MCU 协议 55AA 帧封装为 bleproto 后经 GATT 发送
* @param data 完整 55AA 帧
* @param len 帧长度
- * @note 0x07/0x34/0x38 走 REPORTCMD_V2 主动上报;其余走 RUNCMD_V2 应答
+ * @note 0x07/0x34/0x38/0x41 走 REPORTCMD_V2 主动上报;其余走 RUNCMD_V2 应答
+ * @note 主机未写 CCC 时不会发出,返回 0
*/
-void usr_le_send_mcu_frame(uint8_t *data, uint16_t len);
+uint8_t usr_le_send_mcu_frame(uint8_t *data, uint16_t len);
/**
* @brief 进入配对/配网模式
@@ -49,6 +51,11 @@ void usr_le_send_mcu_frame(uint8_t *data, uint16_t len);
*/
void usr_goto_pair_mode(void);
+/**
+ * @brief 断开 BLE 连接并停止广播(复位/重启前调用)
+ */
+void usr_ble_disconnect_and_stop_adv(void);
+
/**
* @brief 清除配对信息但不复位(paired_flag→0)
*/
@@ -65,6 +72,12 @@ void usr_clear_pair_info(void);
*/
uint8_t usr_get_pair_flag(void);
+/**
+ * @brief GATT 是否已允许发数(主机已写 CCC)
+ * @return 1=可 notify/indicate,0=刚连上还不能发
+ */
+uint8_t usr_ble_is_notify_ready(void);
+
/**
* @brief 启动应用侧 OTA 流程
*/
diff --git a/JBao_JL7016_MOD_FW/apps/usr_le_code/usr_le_recieve.c b/JBao_JL7016_MOD_FW/apps/usr_le_code/usr_le_recieve.c
index 1afebb2..247283d 100644
--- a/JBao_JL7016_MOD_FW/apps/usr_le_code/usr_le_recieve.c
+++ b/JBao_JL7016_MOD_FW/apps/usr_le_code/usr_le_recieve.c
@@ -17,6 +17,7 @@
#include "bleproto_api.h"
#include "ble_proto.h"
#include "mcu_frame.h"
+#include "usr_le_api.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP]"
#define LOG_ERROR_ENABLE
@@ -50,6 +51,7 @@ extern void mcu_bridge_on_app_frame(const uint8_t *data, uint16_t len);
extern void mcu_bridge_on_gateway_report_ack(uint16_t cmd,
uint8_t gateway_int_cnt,
int32_t gateway_int0);
+extern void mcu_bridge_on_network_time(uint64_t unix_s, int32_t gmtoff_s);
void usr_ota_deal_task();
void usr_data_ana(bleproto_appdata_desc_t *desc);
@@ -113,6 +115,12 @@ void usr_data_ana(bleproto_appdata_desc_t *desc)
case E_BLEPROTO_SERVICE_ID_DEVICEINFO:
+ {
+ bleproto_deviceinfo_req_t *req = &desc->datast.deviceinfo_req;
+ /* 网关从子设备连上开始计时,每 12 小时经 DEVICEINFO 同步一次时间戳+时区 */
+ mcu_bridge_on_network_time(req->t, req->gmtoff);
+ }
+
s_encode_desc.header.version = 0;
s_encode_desc.header.datafmt = BLEPROTO_APPDATA_DATAFMT_JSON;
s_encode_desc.header.msgtype = BLEPROTO_APPDATA_MSGTYPE_RSP;
@@ -479,11 +487,12 @@ void usr_run_cmd_code_v2(ble_proto_packet_t *runcmd_par, u8 usr_msg_id)
* @brief 将 MCU 协议帧封装为 bleproto V2 后通过 BLE 发送
* @param data 55AA 完整帧
* @param len 帧长度
- * @note 主动上报(0x07/0x34/0x38) → REPORTCMD_V2;其余 → RUNCMD_V2 应答
+ * @note 主动上报(0x07/0x34/0x38/0x41) → REPORTCMD_V2;其余 → RUNCMD_V2 应答
* @note 轻量路径:栈上仅 ble_proto_packet_t + 小 v2 缓冲,
* 不走 bleproto_appdata_desc_t / bleproto_appdata_encode(约 2.3KB×2)
+ * @return 1=已交给协议栈,0=参数非法/超长/CCC 未就绪/组包失败
*/
-void usr_le_send_mcu_frame(u8 *data, u16 len)
+uint8_t usr_le_send_mcu_frame(u8 *data, u16 len)
{
ble_proto_packet_t pkt;
bleproto_appdata_header_t hdr;
@@ -497,16 +506,26 @@ void usr_le_send_mcu_frame(u8 *data, u16 len)
if (!data || len < 5)
{
printf("usr_le_send_mcu_frame invalid param, len=%d\n", len);
- return;
+ return 0;
}
if (len > MCU_FRAME_MAX_LEN)
{
printf("usr_le_send_mcu_frame frame too long %u\n", len);
- return;
+ return 0;
+ }
+ /* 已配网/已连接不等于能发:主机写完 CCC 才允许 notify */
+ if (!usr_ble_is_notify_ready())
+ {
+ printf("usr_le_send_mcu_frame wait CCC cmd=0x%02X\n", data[4]);
+ return 0;
}
mcu_cmd = data[4];
- is_active_send = (mcu_cmd == 0x07) || (mcu_cmd == 0x34) || (mcu_cmd == 0x38);
+ /* 主动上报走 REPORTCMD_V2(cmd100),应答走 RUNCMD_V2(cmd110) */
+ is_active_send = (mcu_cmd == MCU_CMD_REPORT) ||
+ (mcu_cmd == MCU_CMD_OTA_COMPLETE) ||
+ (mcu_cmd == MCU_CMD_OTA_STOP_MCU) ||
+ (mcu_cmd == MCU_CMD_GET_MCU_INFO_ACK);
memset(&pkt, 0, sizeof(pkt));
memset(&hdr, 0, sizeof(hdr));
@@ -517,6 +536,7 @@ void usr_le_send_mcu_frame(u8 *data, u16 len)
if (is_active_send)
{
+ /* 设备→网关主动上报,等网关 ACK,不带 runcmd msgid */
hdr.msgtype = BLEPROTO_APPDATA_MSGTYPE_REQ;
hdr.msgid = 0;
hdr.serviceid = E_BLEPROTO_SERVICE_ID_REPORTCMD_V2;
@@ -526,6 +546,7 @@ void usr_le_send_mcu_frame(u8 *data, u16 len)
}
else
{
+ /* APP 下发的应答:msgid 必须对上最近一次 RUNCMD,网关才认 */
hdr.msgtype = BLEPROTO_APPDATA_MSGTYPE_RSP;
hdr.msgid = s_last_runcmd_msgid;
hdr.serviceid = E_BLEPROTO_SERVICE_ID_RUNCMD_V2;
@@ -543,16 +564,18 @@ void usr_le_send_mcu_frame(u8 *data, u16 len)
if (plen <= 0)
{
printf("usr_le_send_mcu_frame pack fail %d\n", plen);
- return;
+ return 0;
}
elen = bleproto_appdata_wrap_raw(txbuf, sizeof(txbuf), &hdr, v2_buf, (uint16_t)plen);
printf("usr_le_send_mcu_frame cmd=0x%02X active=%d encode_len=%d msgid=%d\n",
mcu_cmd, is_active_send, elen, hdr.msgid);
- if (elen > 0)
+ if (elen <= 0)
{
- usr_ble_send_data(txbuf, elen);
- printf("send data:");
- printf_buf(txbuf, elen);
+ return 0;
}
+ usr_ble_send_data(txbuf, elen);
+ printf("send data:");
+ printf_buf(txbuf, elen);
+ return 1;
}
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_business.c b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_business.c
index c2797d7..73e05c4 100644
--- a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_business.c
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_business.c
@@ -2,22 +2,26 @@
* @file mcu_business.c
* @brief 业务门面实现
* @author cyWu <1917507415@qq.com>
- * @date 2026.08.12
- * @version V1.0.0
+ * @date 2026.08.13
+ * @version V1.1.0
* @history
* - V1.0.0, 2026.08.12, cyWu, 取代原 mcu_link,编排三个业务子模块
+ * - V1.1.0, 2026.08.13, cyWu, 编排 P1 管理/对时/版本
******************************************************************************/
#include "system/includes.h"
#include "app_main.h"
#include "mcu_business.h"
-#include "mcu_identity.h"
-#include "mcu_pairing.h"
-#include "mcu_link_status.h"
#include "mcu_cmd.h"
+#include "mcu_identity.h"
+#include "mcu_link_status.h"
+#include "mcu_mgmt.h"
+#include "mcu_pairing.h"
+#include "mcu_time.h"
+#include "mcu_version.h"
-#define LOG_TAG_CONST APP
-#define LOG_TAG "[MCU_BIZ]"
+#define LOG_TAG_CONST APP
+#define LOG_TAG "[MCU_BIZ]"
#define LOG_ERROR_ENABLE
#define LOG_INFO_ENABLE
#include "debug.h"
@@ -32,6 +36,9 @@ void mcu_business_init(void)
mcu_identity_init();
mcu_pairing_init();
mcu_link_status_init();
+ mcu_mgmt_init();
+ mcu_time_init();
+ mcu_version_init();
usr_var.usr_goto_pair = 0;
usr_var.usr_adv_flag = 0;
@@ -45,6 +52,9 @@ void mcu_business_periodic(void)
mcu_identity_periodic();
mcu_link_status_periodic();
mcu_pairing_periodic();
+ mcu_mgmt_periodic();
+ mcu_time_periodic();
+ mcu_version_periodic();
}
/**
@@ -92,8 +102,10 @@ void mcu_business_on_paired(void)
{
usr_var.usr_goto_pair = 0;
mcu_pairing_stop();
- mcu_link_status_set_phase(MCU_WS_CONFIGURED);
- log_info("paired -> CONFIGURED");
+ /* 三元组已写入 VM,但 GATT 未必可发。退出 PAIRING,
+ * CONFIGURED 交给 mcu_link_status_periodic:连接+CCC 才置位。 */
+ mcu_link_status_set_phase(MCU_WS_RECONNECTING);
+ log_info("paired, wait link up");
}
/**
@@ -103,7 +115,76 @@ void mcu_business_on_paired(void)
*/
void mcu_business_on_pair_cleared(void)
{
- if (mcu_link_status_get_phase() != MCU_WS_PAIRING) {
+ /* 正在走 0x10 配网:清配对表是配网流程的一部分,过程态保持 PAIRING。
+ * 其它情况(单纯清空配对)才回到未配网空闲。 */
+ if (mcu_link_status_get_phase() != MCU_WS_PAIRING)
+ {
mcu_link_status_set_phase(0);
}
}
+
+/**
+ * @brief 恢复出厂
+ */
+void mcu_business_on_factory_reset(void)
+{
+ mcu_mgmt_on_factory_reset();
+}
+
+/**
+ * @brief 模组重启
+ */
+void mcu_business_on_mod_reboot(void)
+{
+ mcu_mgmt_on_mod_reboot();
+}
+
+/**
+ * @brief 0x1B
+ * @param frame 完整帧
+ * @param len 长度
+ */
+void mcu_business_on_mcu_reboot_ack(const uint8_t *frame, uint16_t len)
+{
+ mcu_mgmt_on_mcu_reboot_ack(frame, len);
+}
+
+/**
+ * @brief 组 0x1D
+ * @param payload 输出
+ * @param plen 长度
+ */
+void mcu_business_build_time_ack(uint8_t *payload, uint16_t *plen)
+{
+ mcu_time_build_ack(payload, plen);
+}
+
+/**
+ * @brief 网络时间
+ * @param unix_s UTC Unix 时间戳(秒)
+ * @param gmtoff_s 时区偏移
+ */
+void mcu_business_on_network_time(uint64_t unix_s, int32_t gmtoff_s)
+{
+ mcu_time_set_from_gateway(unix_s, gmtoff_s);
+}
+
+/**
+ * @brief UART 0x41
+ * @param frame 完整帧
+ * @param len 长度
+ */
+void mcu_business_on_mcu_info_ack(const uint8_t *frame, uint16_t len)
+{
+ mcu_version_on_mcu_info_ack(frame, len);
+}
+
+/**
+ * @brief 网关 ACK
+ * @param success 成功
+ * @param mcu_report_consumed MCU 0x07 已消费
+ */
+void mcu_business_on_gateway_ack(uint8_t success, uint8_t mcu_report_consumed)
+{
+ mcu_version_on_gateway_ack(success, mcu_report_consumed);
+}
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_business.h b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_business.h
index cad777f..4f10464 100644
--- a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_business.h
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_business.h
@@ -1,12 +1,13 @@
/******************************************************************************
* @file mcu_business.h
- * @brief 业务门面:协调身份 / 配网 / 连接状态三个业务子模块
+ * @brief 业务门面:协调身份 / 配网 / 连接 / 管理 / 对时 / 版本
* @author cyWu <1917507415@qq.com>
- * @date 2026.08.12
- * @version V1.0.0
+ * @date 2026.08.13
+ * @version V1.1.0
* @history
* - V1.0.0, 2026.08.12, cyWu, 取代原 mcu_link,供协议层(mcu_protocol)
* 单点调用;协议层不直接依赖 identity/pairing/link_status 三个子模块
+ * - V1.1.0, 2026.08.13, cyWu, 接入 P1 管理/对时/版本子模块
******************************************************************************/
#ifndef __MCU_BUSINESS_H__
@@ -20,16 +21,19 @@
/* mcu_identity.c —— 身份获取 */
/* mcu_pairing.c —— 配网请求 / 超时 */
/* mcu_link_status.c —— 连接状态位图(Bit0/1/2) */
+/* mcu_mgmt.c —— 出厂复位 / 重启 / 通知 MCU 重启 */
+/* mcu_time.c —— 对时(写 RTC / 0x1D 读回) */
+/* mcu_version.c —— UART 0x40/0x41 与 SOC 0x41 上报 */
/* 须在协议任务(mcu_protocol_task)单一上下文中调用。 */
/*============================================================================*/
/**
- * @brief 初始化业务层(身份 + 配网 + 连接状态)
+ * @brief 初始化业务层
*/
void mcu_business_init(void);
/**
- * @brief 周期处理:依次驱动身份 / 连接状态 / 配网三个子业务
+ * @brief 周期处理:依次驱动各子业务
*/
void mcu_business_periodic(void);
@@ -69,4 +73,50 @@ void mcu_business_on_paired(void);
*/
void mcu_business_on_pair_cleared(void);
+/**
+ * @brief MCU 请求恢复出厂(协议层已 ACK)
+ */
+void mcu_business_on_factory_reset(void);
+
+/**
+ * @brief MCU 请求模组重启(协议层已 ACK)
+ */
+void mcu_business_on_mod_reboot(void);
+
+/**
+ * @brief 处理 UART 0x1B
+ * @param frame 完整帧
+ * @param len 长度
+ */
+void mcu_business_on_mcu_reboot_ack(const uint8_t *frame, uint16_t len);
+
+/**
+ * @brief 组 0x1D 载荷
+ * @param payload 输出缓冲
+ * @param plen 输出长度
+ */
+void mcu_business_build_time_ack(uint8_t *payload, uint16_t *plen);
+
+/**
+ * @brief 写入网关网络时间
+ * @param unix_s UTC Unix 时间戳(秒)
+ * @param gmtoff_s 时区偏移秒
+ * @note 网关从子设备连上开始计时,每 12 小时同步一次
+ */
+void mcu_business_on_network_time(uint64_t unix_s, int32_t gmtoff_s);
+
+/**
+ * @brief 处理 UART 0x41(仅本地解析)
+ * @param frame 完整帧
+ * @param len 长度
+ */
+void mcu_business_on_mcu_info_ack(const uint8_t *frame, uint16_t len);
+
+/**
+ * @brief 网关 REPORTCMD_V2 应答
+ * @param success 1=成功
+ * @param mcu_report_consumed 1=已按 MCU 0x07 回了 0x08
+ */
+void mcu_business_on_gateway_ack(uint8_t success, uint8_t mcu_report_consumed);
+
#endif /* __MCU_BUSINESS_H__ */
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_identity.c b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_identity.c
index 30dda8b..c89e2b6 100644
--- a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_identity.c
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_identity.c
@@ -10,14 +10,14 @@
#include "system/includes.h"
#include "mcu_identity.h"
-#include "mcu_protocol.h"
#include "mcu_cmd.h"
+#include "mcu_protocol.h"
#include "mcu_util.h"
#include "usr_le_api.h"
#include "usr_le_product.h"
-#define LOG_TAG_CONST APP
-#define LOG_TAG "[MCU_ID]"
+#define LOG_TAG_CONST APP
+#define LOG_TAG "[MCU_ID]"
#define LOG_ERROR_ENABLE
#define LOG_DEBUG_ENABLE
#define LOG_INFO_ENABLE
@@ -27,8 +27,8 @@
/* 私有宏 */
/*============================================================================*/
-#define MCU_IDENTITY_RETRY_MS (1000u)
-#define MCU_IDENTITY_PAYLOAD_MIN (2 + 8 + 8 + 16 + 8) /* Ver+Cat+Prod+SN+Cap */
+#define MCU_IDENTITY_RETRY_MS (1000u)
+#define MCU_IDENTITY_PAYLOAD_MIN (2 + 8 + 8 + 16 + 8) /* Ver+Cat+Prod+SN+Cap */
/*============================================================================*/
/* 公有变量:运行时可改写的产品身份(供 BLE 广播使用) */
@@ -42,8 +42,8 @@ uint8_t g_usr_le_pcode[USR_LE_PCODE_LEN];
/*============================================================================*/
typedef struct {
- uint8_t ready;
- uint32_t last_tx_ms;
+ uint8_t ready; /* 1=已解析 0x02,广播可用产品码 */
+ uint32_t last_tx_ms; /* 最近一次发 0x01 的时刻 */
} McuIdentity_t;
static McuIdentity_t s_identity;
@@ -69,17 +69,22 @@ void mcu_identity_periodic(void)
{
uint32_t now;
- if (s_identity.ready) {
+ /* 已经拿到身份,不再发 0x01 */
+ if (s_identity.ready)
+ {
return;
}
now = mcu_util_get_ms();
- if ((now - s_identity.last_tx_ms) < MCU_IDENTITY_RETRY_MS) {
+ /* 未就绪则每 1s 问一次 MCU,无限轮询,没有次数上限 */
+ if ((now - s_identity.last_tx_ms) < MCU_IDENTITY_RETRY_MS)
+ {
return;
}
s_identity.last_tx_ms = now;
- if (mcu_protocol_send_cmd(MCU_CMD_GET_IDENTITY, NULL, 0) > 0) {
+ if (mcu_protocol_send_cmd(MCU_CMD_GET_IDENTITY, NULL, 0) > 0)
+ {
log_debug("identity req");
}
}
@@ -103,14 +108,19 @@ void mcu_identity_on_ack(const uint8_t *frame, uint16_t len)
{
int i;
- if (!frame || len < 7) {
+ if (!frame || len < 7)
+ {
return;
}
- if (frame[6] != MCU_RESULT_OK) {
+ /* frame[6]=Result,MCU 拒绝则继续 periodic 里每秒重问 */
+ if (frame[6] != MCU_RESULT_OK)
+ {
log_error("identity ack result=0x%02X", frame[6]);
return;
}
- if (len < (7 + MCU_IDENTITY_PAYLOAD_MIN + 1)) {
+ /* 7 字节头 + 身份载荷 + 1 字节校验 */
+ if (len < (7 + MCU_IDENTITY_PAYLOAD_MIN + 1))
+ {
log_error("identity ack too short %u", len);
return;
}
@@ -123,12 +133,14 @@ void mcu_identity_on_ack(const uint8_t *frame, uint16_t len)
s_identity.ready = 1;
printf("identity ok prodcode:");
- for (i = 0; i < USR_LE_PRODCODE_LEN; i++) {
+ for (i = 0; i < USR_LE_PRODCODE_LEN; i++)
+ {
printf("%c", g_usr_le_prodcode[i]);
}
printf("\n");
printf("identity ok pcode:");
- for (i = 0; i < USR_LE_PCODE_LEN; i++) {
+ for (i = 0; i < USR_LE_PCODE_LEN; i++)
+ {
printf("%c", g_usr_le_pcode[i]);
}
printf("\n");
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_link_status.c b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_link_status.c
index b086ed1..2315fa6 100644
--- a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_link_status.c
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_link_status.c
@@ -11,12 +11,12 @@
#include "system/includes.h"
#include "app_main.h"
#include "mcu_link_status.h"
-#include "mcu_identity.h"
#include "mcu_cmd.h"
+#include "mcu_identity.h"
#include "usr_le_api.h"
-#define LOG_TAG_CONST APP
-#define LOG_TAG "[MCU_LNK]"
+#define LOG_TAG_CONST APP
+#define LOG_TAG "[MCU_LNK]"
#define LOG_ERROR_ENABLE
#define LOG_DEBUG_ENABLE
#define LOG_INFO_ENABLE
@@ -33,8 +33,8 @@ extern u16 usr_get_conn_service(void);
/*============================================================================*/
typedef struct {
- uint8_t work_status;
- uint8_t last_ble_conn;
+ uint8_t work_status; /* 0x14 用的整字节:Bit0/1/2 过程态 + Bit6 OTA */
+ uint8_t last_ble_conn; /* 上一拍 BLE 是否已连接,用来检测边沿 */
} McuLinkStatus_t;
static McuLinkStatus_t s_status;
@@ -76,7 +76,8 @@ uint8_t mcu_link_status_get_phase(void)
void mcu_link_status_set_phase(uint8_t phase)
{
s_status.work_status &= (uint8_t)~MCU_WS_LINK_MASK;
- if (phase & MCU_WS_LINK_MASK) {
+ if (phase & MCU_WS_LINK_MASK)
+ {
s_status.work_status |= (uint8_t)(phase & MCU_WS_LINK_MASK);
}
}
@@ -87,9 +88,12 @@ void mcu_link_status_set_phase(uint8_t phase)
*/
void mcu_link_status_set_ota(uint8_t on)
{
- if (on) {
+ if (on)
+ {
s_status.work_status |= MCU_WS_OTA_UPGRADING;
- } else {
+ }
+ else
+ {
s_status.work_status &= (uint8_t)~MCU_WS_OTA_UPGRADING;
}
}
@@ -97,33 +101,46 @@ void mcu_link_status_set_ota(uint8_t on)
/**
* @brief 周期处理:轮询 BLE 连接,维护 Bit0/1/2,已配网断连时开回连广播
* @note - 配网中(phase==PAIRING)由配网业务维护,此处不覆盖
- * - 已配网:已连接→CONFIGURED,未连接→RECONNECTING
+ * - VM 已绑定:链路可发数(连接+CCC)→CONFIGURED,否则→RECONNECTING
* - Bit3~5 预留固定 0,不在此处更新
*/
void mcu_link_status_periodic(void)
{
- uint8_t conn = (usr_get_conn_service() != 0) ? 1 : 0;
- uint8_t paired = usr_get_pair_flag();
+ uint8_t conn = (usr_get_conn_service() != 0) ? 1 : 0; /* 1=BLE 已连接 */
+ uint8_t paired = usr_get_pair_flag(); /* 1=VM 已绑定三元组 */
+ uint8_t link_up;
- if (conn != s_status.last_ble_conn) {
+ /* 只在连接状态发生变化时处理广播,避免每拍都重开广播 */
+ if (conn != s_status.last_ble_conn)
+ {
s_status.last_ble_conn = conn;
- if (!conn && paired) {
- /* 已配网断连:发回连广播(非绑定广播) */
+ if (!conn && paired)
+ {
+ /* 已绑定后断连:发回连广播(usr_goto_pair=0 → 非绑定广播) */
usr_var.usr_goto_pair = 0;
- if (mcu_identity_is_ready()) {
+ if (mcu_identity_is_ready())
+ {
usr_ble_adv_init();
}
}
}
- if (mcu_link_status_get_phase() == MCU_WS_PAIRING) {
- return; /* 配网流程中不切换到回连/已配网 */
+ /* 配网中由 pairing 模块维护过程态,这里不要改成回连/已配网 */
+ if (mcu_link_status_get_phase() == MCU_WS_PAIRING)
+ {
+ return;
}
- if (paired) {
- mcu_link_status_set_phase(conn ? MCU_WS_CONFIGURED : MCU_WS_RECONNECTING);
- } else {
- mcu_link_status_set_phase(0); /* 未配网空闲 */
+ /* VM 已绑定 ≠ 已配网可用。MCU 看到 CONFIGURED 才会上报 0x07,
+ * 必须等 GATT CCC 写完,整条链路才能发到网关。 */
+ link_up = (uint8_t)(paired && conn && usr_ble_is_notify_ready());
+ if (paired)
+ {
+ mcu_link_status_set_phase(link_up ? MCU_WS_CONFIGURED : MCU_WS_RECONNECTING);
+ }
+ else
+ {
+ mcu_link_status_set_phase(0); /* 未绑定空闲,Bit0/1/2 全 0 */
}
}
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_link_status.h b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_link_status.h
index a0be37c..26c319b 100644
--- a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_link_status.h
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_link_status.h
@@ -19,8 +19,9 @@
void mcu_link_status_init(void);
/**
- * @brief 周期处理:轮询 BLE 连接,维护 Bit0/1/2,已配网断连时触发回连广播
- * @note 配网中(phase==PAIRING)时不会被本函数覆盖
+ * @brief 周期处理:轮询 BLE 连接/CCC,维护 Bit0/1/2,已绑定断连时触发回连广播
+ * @note 配网中(phase==PAIRING)时不会被本函数覆盖;
+ * CONFIGURED 要求 VM 已绑定 + BLE 已连接 + CCC 已写
*/
void mcu_link_status_periodic(void);
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_mgmt.c b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_mgmt.c
new file mode 100644
index 0000000..7cb785c
--- /dev/null
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_mgmt.c
@@ -0,0 +1,198 @@
+/******************************************************************************
+ * @file mcu_mgmt.c
+ * @brief 模组管理业务实现
+ * @author cyWu <1917507415@qq.com>
+ * @date 2026.08.13
+ * @version V1.0.0
+ * @history
+ * - V1.0.0, 2026.08.13, cyWu, 首次发布
+ ******************************************************************************/
+
+#include "system/includes.h"
+#include "asm/cpu.h"
+#include "mcu_mgmt.h"
+#include "mcu_cmd.h"
+#include "mcu_protocol.h"
+#include "mcu_util.h"
+#include "usr_le_api.h"
+#include "usr_rtc.h"
+
+#define LOG_TAG_CONST APP
+#define LOG_TAG "[MCU_MGMT]"
+#define LOG_ERROR_ENABLE
+#define LOG_DEBUG_ENABLE
+#define LOG_INFO_ENABLE
+#include "debug.h"
+
+/*============================================================================*/
+/* 私有宏 */
+/*============================================================================*/
+
+#define MCU_MGMT_RESET_DELAY_MS (600u)
+#define MCU_MGMT_ACK_WAIT_MS (500u)
+#define MCU_MGMT_MAX_RETRY (3u)
+
+typedef enum
+{
+ MCU_MGMT_RESET_NONE = 0, /* 无待执行复位 */
+ MCU_MGMT_RESET_FACTORY, /* 恢复出厂后复位 */
+ MCU_MGMT_RESET_REBOOT /* 普通重启 */
+} McuMgmtResetKind_t;
+
+/*============================================================================*/
+/* 私有类型/变量 */
+/*============================================================================*/
+
+typedef struct {
+ McuMgmtResetKind_t reset_kind; /* 延迟复位类型,NONE 表示没有 */
+ uint32_t reset_due_ms; /* 到点执行 cpu_reset */
+ uint8_t notify_waiting; /* 1=已发 0x1A,等 MCU 0x1B */
+ uint8_t notify_retry; /* 0x1A 已发送次数(含首次) */
+ uint32_t notify_tx_ms; /* 最近一次 0x1A 发出时刻 */
+} McuMgmt_t;
+
+static McuMgmt_t s_mgmt;
+
+/*============================================================================*/
+/* 私有函数 */
+/*============================================================================*/
+
+/**
+ * @brief 启动延迟复位
+ * @param kind 复位类型
+ */
+static void mcu_mgmt_arm_reset(McuMgmtResetKind_t kind)
+{
+ s_mgmt.reset_kind = kind;
+ s_mgmt.reset_due_ms = mcu_util_get_ms() + MCU_MGMT_RESET_DELAY_MS;
+}
+
+/*============================================================================*/
+/* 公有函数 */
+/*============================================================================*/
+
+/**
+ * @brief 初始化模组管理业务
+ */
+void mcu_mgmt_init(void)
+{
+ memset(&s_mgmt, 0, sizeof(s_mgmt));
+}
+
+/**
+ * @brief 周期处理:0x1A 重试、延迟复位
+ */
+void mcu_mgmt_periodic(void)
+{
+ uint32_t now = mcu_util_get_ms();
+
+ /* 已安排延迟复位:到点 cpu_reset,期间不再发 0x1A */
+ if (s_mgmt.reset_kind != MCU_MGMT_RESET_NONE)
+ {
+ if ((int32_t)(now - s_mgmt.reset_due_ms) >= 0)
+ {
+ log_info("cpu_reset kind=%u", (unsigned)s_mgmt.reset_kind);
+ cpu_reset();
+ }
+ return;
+ }
+
+ /* 没在等 0x1B,无事可做 */
+ if (!s_mgmt.notify_waiting)
+ {
+ return;
+ }
+ /* 500ms 窗口内继续等 MCU ACK */
+ if ((now - s_mgmt.notify_tx_ms) <= MCU_MGMT_ACK_WAIT_MS)
+ {
+ return;
+ }
+ if (s_mgmt.notify_retry >= MCU_MGMT_MAX_RETRY)
+ {
+ log_error("notify MCU reboot give up");
+ s_mgmt.notify_waiting = 0;
+ return;
+ }
+ /* UART 被心跳/0x14 占用则下周期再试 */
+ if (mcu_protocol_is_ble_pending())
+ {
+ return;
+ }
+ if (mcu_protocol_send_cmd_wait_ack(MCU_CMD_MCU_REBOOT, NULL, 0,
+ MCU_CMD_MCU_REBOOT_ACK) > 0)
+ {
+ s_mgmt.notify_retry++;
+ s_mgmt.notify_tx_ms = now;
+ log_debug("notify MCU reboot retry %u", s_mgmt.notify_retry);
+ }
+}
+
+/**
+ * @brief 恢复出厂:清配对后延迟复位
+ */
+void mcu_mgmt_on_factory_reset(void)
+{
+ log_info("factory reset");
+ usr_ble_disconnect_and_stop_adv();
+ usr_clear_pair_info_noreset();
+ usr_rtc_clear_tz(); /* 时区是上次配网对时留下的,出厂一并清掉 */
+ mcu_mgmt_arm_reset(MCU_MGMT_RESET_FACTORY);
+}
+
+/**
+ * @brief 模组重启:保留配对后延迟复位
+ */
+void mcu_mgmt_on_mod_reboot(void)
+{
+ log_info("mod reboot");
+ usr_ble_disconnect_and_stop_adv();
+ mcu_mgmt_arm_reset(MCU_MGMT_RESET_REBOOT);
+}
+
+/**
+ * @brief 发送 0x1A 通知 MCU 重启
+ */
+void mcu_mgmt_notify_mcu_reboot(void)
+{
+ /* 已经在等 0x1B,或马上要复位,不要重复发 0x1A */
+ if (s_mgmt.notify_waiting || s_mgmt.reset_kind != MCU_MGMT_RESET_NONE)
+ {
+ log_error("notify MCU reboot busy");
+ return;
+ }
+ if (mcu_protocol_is_ble_pending())
+ {
+ log_error("notify MCU reboot uart busy");
+ return;
+ }
+ if (mcu_protocol_send_cmd_wait_ack(MCU_CMD_MCU_REBOOT, NULL, 0,
+ MCU_CMD_MCU_REBOOT_ACK) <= 0)
+ {
+ log_error("notify MCU reboot tx fail");
+ return;
+ }
+ s_mgmt.notify_waiting = 1;
+ s_mgmt.notify_retry = 1;
+ s_mgmt.notify_tx_ms = mcu_util_get_ms();
+ log_info("notify MCU reboot tx");
+}
+
+/**
+ * @brief 处理 0x1B
+ * @param frame 完整帧
+ * @param len 长度
+ */
+void mcu_mgmt_on_mcu_reboot_ack(const uint8_t *frame, uint16_t len)
+{
+ /* 不是自己发出去的 0x1A 对应的应答,忽略 */
+ if (!s_mgmt.notify_waiting)
+ {
+ return;
+ }
+ s_mgmt.notify_waiting = 0;
+ if (!frame || len < 7)
+ {
+ return;
+ }
+ log_info("MCU reboot ack result=0x%02X", frame[6]);
+}
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_mgmt.h b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_mgmt.h
new file mode 100644
index 0000000..497f3a7
--- /dev/null
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_mgmt.h
@@ -0,0 +1,52 @@
+/******************************************************************************
+ * @file mcu_mgmt.h
+ * @brief 模组管理业务:出厂复位 / 模组重启 / 通知 MCU 重启
+ * @author cyWu <1917507415@qq.com>
+ * @date 2026.08.13
+ * @version V1.0.0
+ * @history
+ * - V1.0.0, 2026.08.13, cyWu, 首次发布(P1:0x16/0x18/0x1A)
+ ******************************************************************************/
+
+#ifndef __MCU_MGMT_H__
+#define __MCU_MGMT_H__
+
+#include
+
+/**
+ * @brief 初始化模组管理业务
+ */
+void mcu_mgmt_init(void);
+
+/**
+ * @brief 周期处理:0x1A 重试、延迟复位倒计时
+ * @note 仅在协议任务上下文调用
+ */
+void mcu_mgmt_periodic(void);
+
+/**
+ * @brief MCU 请求恢复出厂(协议层已回 0x17 OK)
+ * @note 清配对、清时区 → 断 BLE/停广播 → 600ms 后复位;起来后不广播
+ */
+void mcu_mgmt_on_factory_reset(void);
+
+/**
+ * @brief MCU 请求模组重启(协议层已回 0x19 OK)
+ * @note 保留配对 → 断 BLE/停广播 → 600ms 后复位
+ */
+void mcu_mgmt_on_mod_reboot(void);
+
+/**
+ * @brief 主动通知 MCU 重启(发 0x1A,等 0x1B)
+ * @note P1 无自动触发,供后续 OTA 等调用;busy 时忽略
+ */
+void mcu_mgmt_notify_mcu_reboot(void);
+
+/**
+ * @brief 处理 MCU 对 0x1A 的应答 0x1B
+ * @param frame 完整帧
+ * @param len 长度
+ */
+void mcu_mgmt_on_mcu_reboot_ack(const uint8_t *frame, uint16_t len);
+
+#endif /* __MCU_MGMT_H__ */
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_pairing.c b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_pairing.c
index 6a6bae9..d4ff011 100644
--- a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_pairing.c
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_pairing.c
@@ -83,14 +83,17 @@ void mcu_pairing_periodic(void)
uint32_t now;
uint32_t timeout_ms;
+ /* 当前不是配网过程态,不管超时 */
if (mcu_link_status_get_phase() != MCU_WS_PAIRING) {
return;
}
+ /* timeout_s=0 表示 MCU 没给超时,一直等到配上或被 stop */
if (s_pairing.timeout_s == 0) {
return;
}
+ /* 已经配上:切态由 mcu_business_on_paired 做,这里别抢 */
if (usr_get_pair_flag()) {
- return; /* 已配网成功,交由 mcu_business_on_paired 处理切态 */
+ return;
}
now = mcu_util_get_ms();
@@ -99,6 +102,7 @@ void mcu_pairing_periodic(void)
return;
}
+ /* 超时仍未配上:停绑定广播,过程态回到未配网空闲 */
log_info("pairing timeout, stop adv");
usr_var.usr_goto_pair = 0;
s_pairing.timeout_s = 0;
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_time.c b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_time.c
new file mode 100644
index 0000000..a58722d
--- /dev/null
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_time.c
@@ -0,0 +1,107 @@
+/******************************************************************************
+ * @file mcu_time.c
+ * @brief 对时业务:网关时间写入 RTC,0x1D 从 RTC 读回
+ * @author cyWu <1917507415@qq.com>
+ * @date 2026.08.14
+ * @version V1.1.0
+ * @history
+ * - V1.0.0, 2026.08.13, cyWu, 首次发布(RAM 缓存)
+ * - V1.1.0, 2026.08.14, cyWu, 改为写硬件 RTC,周期读回 UTC
+ ******************************************************************************/
+
+#include "system/includes.h"
+#include "mcu_time.h"
+#include "mcu_cmd.h"
+#include "mcu_util.h"
+#include "usr_rtc.h"
+
+#define LOG_TAG_CONST APP
+#define LOG_TAG "[MCU_TIME]"
+#define LOG_ERROR_ENABLE
+#define LOG_DEBUG_ENABLE
+#define LOG_INFO_ENABLE
+#include "debug.h"
+
+/*============================================================================*/
+/* 公有函数 */
+/*============================================================================*/
+
+/**
+ * @brief 初始化对时业务(RTC 已由 app_main 打开则直接返回 OK)
+ */
+void mcu_time_init(void)
+{
+ if (usr_rtc_init() != USR_RTC_OK)
+ {
+ log_error("rtc not ready");
+ }
+}
+
+/**
+ * @brief 周期读 RTC,刷新 UTC 给业务
+ */
+void mcu_time_periodic(void)
+{
+ usr_rtc_run();
+}
+
+/**
+ * @brief 网关 DEVICEINFO 对时:UTC + 时区写入 RTC
+ * @param unix_s UTC Unix 秒
+ * @param gmtoff_s 时区偏移秒
+ * @note 网关从子设备连上开始计时,每 12 小时同步一次
+ */
+void mcu_time_set_from_gateway(uint64_t unix_s, int32_t gmtoff_s)
+{
+ UsrRtc_Ret_t ret;
+
+ ret = usr_rtc_set_from_unix(unix_s, gmtoff_s);
+ if (ret != USR_RTC_OK)
+ {
+ log_debug("set rtc fail ret=%d", (int)ret);
+ }
+}
+
+/**
+ * @brief 组 0x1D 载荷:本地日历 + UTC
+ * @param payload 输出
+ * @param plen 输出长度
+ */
+void mcu_time_build_ack(uint8_t *payload, uint16_t *plen)
+{
+ struct sys_time t;
+ uint32_t unix_utc;
+
+ if (!payload || !plen)
+ {
+ return;
+ }
+
+ payload[0] = MCU_RESULT_OK;
+ /* 年份不在 2025~2098 或还没有时区:载荷后面全 0 */
+ if (!usr_rtc_is_valid() || (usr_rtc_read(&t) != USR_RTC_OK))
+ {
+ memset(&payload[1], 0, 12);
+ *plen = MCU_TIME_ACK_LEN;
+ return;
+ }
+
+ unix_utc = usr_rtc_get_unix_utc();
+ mcu_util_write_u16_be(&payload[1], t.year);
+ payload[3] = t.month;
+ payload[4] = t.day;
+ payload[5] = t.hour;
+ payload[6] = t.min;
+ payload[7] = t.sec;
+ mcu_util_write_u32_be(&payload[8], unix_utc);
+ *plen = MCU_TIME_ACK_LEN;
+}
+
+/**
+ * @brief 是否已有可用 RTC 时间
+ * @return 1/0
+ */
+uint8_t mcu_time_is_valid(void)
+{
+ return usr_rtc_is_valid();
+}
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_time.h b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_time.h
new file mode 100644
index 0000000..8346a63
--- /dev/null
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_time.h
@@ -0,0 +1,53 @@
+/******************************************************************************
+ * @file mcu_time.h
+ * @brief 对时业务:网关时间写入 RTC,应答 MCU 0x1C/0x1D
+ * @author cyWu <1917507415@qq.com>
+ * @date 2026.08.14
+ * @version V1.1.0
+ * @history
+ * - V1.0.0, 2026.08.13, cyWu, 首次发布(P1:0x1C/0x1D)
+ * - V1.1.0, 2026.08.14, cyWu, 对接 usr_rtc 硬件 RTC
+ ******************************************************************************/
+
+#ifndef __MCU_TIME_H__
+#define __MCU_TIME_H__
+
+#include
+
+/** 0x1D 载荷长度:Result(1)+年(2)+月日时分秒(5)+Unix(4) */
+#define MCU_TIME_ACK_LEN (13u)
+
+/**
+ * @brief 初始化对时业务(确保 RTC 已打开)
+ */
+void mcu_time_init(void);
+
+/**
+ * @brief 周期读 RTC,刷新 UTC
+ */
+void mcu_time_periodic(void);
+
+/**
+ * @brief 写入网关 DEVICEINFO 下发的网络时间
+ * @param unix_s UTC Unix 时间戳(秒);0 视为无效
+ * @param gmtoff_s 时区偏移(秒),如东八区 28800
+ * @note 网关从子设备连上开始计时,每 12 小时同步一次;
+ * 模组收到后转为本地日历 write_sys_time,时区存 VM
+ */
+void mcu_time_set_from_gateway(uint64_t unix_s, int32_t gmtoff_s);
+
+/**
+ * @brief 组 0x1D 载荷(Result 起,不含帧头/校验)
+ * @param payload 输出缓冲,至少 MCU_TIME_ACK_LEN
+ * @param plen 输出载荷长度
+ * @note 无有效时间时 Result=OK,日历与 Unix 全 0
+ */
+void mcu_time_build_ack(uint8_t *payload, uint16_t *plen);
+
+/**
+ * @brief RTC 时间是否可用(年份合法且已有时区)
+ * @return 1=有效,0=无
+ */
+uint8_t mcu_time_is_valid(void);
+
+#endif /* __MCU_TIME_H__ */
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_version.c b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_version.c
new file mode 100644
index 0000000..37a45ab
--- /dev/null
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_version.c
@@ -0,0 +1,331 @@
+/******************************************************************************
+ * @file mcu_version.c
+ * @brief 版本业务实现:拉 MCU 信息并按 SOC 0x41 上报网关
+ * @author cyWu <1917507415@qq.com>
+ * @date 2026.08.13
+ * @version V1.0.0
+ * @history
+ * - V1.0.0, 2026.08.13, cyWu, 首次发布
+ ******************************************************************************/
+
+#include "system/includes.h"
+#include "mcu_version.h"
+#include "mcu_cmd.h"
+#include "mcu_frame.h"
+#include "mcu_protocol.h"
+#include "mcu_util.h"
+#include "usr_le_api.h"
+
+#define LOG_TAG_CONST APP
+#define LOG_TAG "[MCU_VER]"
+#define LOG_ERROR_ENABLE
+#define LOG_DEBUG_ENABLE
+#define LOG_INFO_ENABLE
+#include "debug.h"
+
+/*============================================================================*/
+/* 外部符号 */
+/*============================================================================*/
+
+extern u16 usr_get_conn_service(void);
+
+/*============================================================================*/
+/* 私有宏 */
+/*============================================================================*/
+
+#define MCU_VER_ACK_WAIT_MS (500u) /* UART 0x40 等 MCU 应答 */
+#define MCU_VER_REPORT_WAIT_MS (2000u) /* SOC 0x41 走网关,等 ACK 需更长 */
+#define MCU_VER_MAX_RETRY (3u)
+#define MCU_VER_UART_ACK_MIN_LEN (14u) /* 55AA+Len+Cmd+Sn+Result+MCU3+HW3+Sum */
+#define MCU_VER_SOC_PAYLOAD_LEN (10u) /* Result+BLE3+MCU3+HW3 */
+
+typedef enum
+{
+ MCU_VER_STATE_IDLE = 0, /* 空闲:无进行中的拉取/上报 */
+ MCU_VER_STATE_FETCH, /* 正在 UART 发 0x40,等 MCU 回 0x41 */
+ MCU_VER_STATE_REPORT /* 正在 BLE 发 SOC 0x41,等网关 ACK */
+} McuVerState_t;
+
+/*============================================================================*/
+/* 私有类型/变量 */
+/*============================================================================*/
+
+typedef struct
+{
+ McuVerState_t state; /* 当前阶段 */
+ uint8_t cached; /* 1=已从 MCU 拿到版本,RAM 里有 mcu_ver/hw_ver */
+ uint8_t reported; /* 1=SOC 0x41 已被网关成功 ACK,本连接不再报 */
+ uint8_t last_conn; /* 上一拍 BLE 是否已连接,仅作记录 */
+ uint8_t give_up; /* 1=本轮 3 次已用尽,等断连后再试,避免连着打 */
+ uint8_t fetch_retry; /* 0x40 已发送次数(含首次) */
+ uint8_t report_retry; /* SOC 0x41 已发送次数(含首次) */
+ uint8_t report_sn; /* SOC 0x41 序列号,每次上报递增 */
+ uint32_t last_tx_ms; /* 最近一次 0x40 或 SOC 0x41 发出时刻 */
+ uint8_t mcu_ver[3]; /* MCU 软件版本 Vx.x.x */
+ uint8_t hw_ver[3]; /* 硬件版本 Vx.x.x */
+} McuVersion_t;
+
+static McuVersion_t s_ver;
+
+/*============================================================================*/
+/* 私有函数 */
+/*============================================================================*/
+
+/**
+ * @brief 发送 UART 0x40
+ * @return 1=已发出,0=忙或失败
+ */
+static uint8_t mcu_version_tx_fetch(void)
+{
+ /* UART 正在等别的命令 ACK(心跳/0x14 等),先不抢 */
+ if (mcu_protocol_is_ble_pending())
+ {
+ return 0;
+ }
+ /* 发 0x40,期望 MCU 回 0x41 */
+ if (mcu_protocol_send_cmd_wait_ack(MCU_CMD_GET_MCU_INFO, NULL, 0,
+ MCU_CMD_GET_MCU_INFO_ACK) <= 0)
+ {
+ return 0;
+ }
+ s_ver.last_tx_ms = mcu_util_get_ms();
+ s_ver.fetch_retry++;
+ log_debug("0x40 fetch retry=%u", s_ver.fetch_retry);
+ return 1;
+}
+
+/**
+ * @brief 组 SOC 0x41 并走 cmd 100 上报
+ * @return 1=已发出,0=失败
+ */
+static uint8_t mcu_version_tx_report(void)
+{
+ uint8_t payload[MCU_VER_SOC_PAYLOAD_LEN];
+ uint8_t frame[MCU_FRAME_MAX_LEN];
+ int32_t total;
+
+ /* MCU 0x07 正在等网关 ACK,通道被占用,版本上报让路 */
+ if (mcu_protocol_is_mcu_report_waiting())
+ {
+ return 0;
+ }
+ /* 刚连上、主机还没写 CCC:发了也会 app_send_fail,不占重试次数 */
+ if (!usr_ble_is_notify_ready())
+ {
+ log_debug("SOC 0x41 wait CCC");
+ return 0;
+ }
+
+ payload[0] = MCU_RESULT_OK;
+ payload[1] = MCU_BLE_FW_VER_MAJOR;
+ payload[2] = MCU_BLE_FW_VER_MINOR;
+ payload[3] = MCU_BLE_FW_VER_PATCH;
+ memcpy(&payload[4], s_ver.mcu_ver, 3);
+ memcpy(&payload[7], s_ver.hw_ver, 3);
+
+ s_ver.report_sn++;
+ total = mcu_frame_pack(frame, sizeof(frame), MCU_CMD_GET_MCU_INFO_ACK,
+ s_ver.report_sn, payload, MCU_VER_SOC_PAYLOAD_LEN);
+ if (total <= 0)
+ {
+ return 0;
+ }
+
+ if (usr_le_send_mcu_frame(frame, (uint16_t)total) == 0)
+ {
+ return 0;
+ }
+ s_ver.last_tx_ms = mcu_util_get_ms();
+ s_ver.report_retry++;
+ log_info("SOC 0x41 report sn=%u retry=%u ble=%u.%u.%u mcu=%u.%u.%u hw=%u.%u.%u",
+ s_ver.report_sn, s_ver.report_retry,
+ payload[1], payload[2], payload[3],
+ s_ver.mcu_ver[0], s_ver.mcu_ver[1], s_ver.mcu_ver[2],
+ s_ver.hw_ver[0], s_ver.hw_ver[1], s_ver.hw_ver[2]);
+ return 1;
+}
+
+/**
+ * @brief 进入拉取或上报(缓存空则拉,有缓存未上报则报)
+ */
+static void mcu_version_start_session(void)
+{
+ s_ver.fetch_retry = 0;
+ s_ver.report_retry = 0;
+ if (!s_ver.cached)
+ {
+ /* 还没有 MCU 版本:先 UART 拉 0x40 */
+ s_ver.state = MCU_VER_STATE_FETCH;
+ (void)mcu_version_tx_fetch();
+ return;
+ }
+ if (!s_ver.reported)
+ {
+ /* 已有缓存但网关还没 ACK:直接报 SOC 0x41 */
+ s_ver.state = MCU_VER_STATE_REPORT;
+ (void)mcu_version_tx_report();
+ return;
+ }
+ /* 已拉过也已报过,保持空闲 */
+ s_ver.state = MCU_VER_STATE_IDLE;
+}
+
+/*============================================================================*/
+/* 公有函数 */
+/*============================================================================*/
+
+/**
+ * @brief 初始化版本业务
+ */
+void mcu_version_init(void)
+{
+ memset(&s_ver, 0, sizeof(s_ver));
+}
+
+/**
+ * @brief 周期:已配网且已连接时拉版本/上报;超时重试
+ * @note 流程:BLE 已连 + 已配网 → 拉 MCU 0x40 → 报网关 SOC 0x41 → 等 ACK
+ */
+void mcu_version_periodic(void)
+{
+ uint8_t conn = (usr_get_conn_service() != 0) ? 1 : 0; /* 1=BLE 已连接 */
+ uint8_t paired = usr_get_pair_flag(); /* 1=已配网/已绑定 */
+ uint32_t now = mcu_util_get_ms();
+
+ s_ver.last_conn = conn;
+
+ /* BLE 断了:清 give_up,下次连上允许再试;进行中的拉取/上报也停掉 */
+ if (!conn)
+ {
+ s_ver.give_up = 0;
+ if (s_ver.state != MCU_VER_STATE_IDLE)
+ {
+ s_ver.state = MCU_VER_STATE_IDLE;
+ }
+ return;
+ }
+
+ /* 已连接但未配网:版本要报给网关,没绑定就不上报。
+ * give_up=1:本轮 3 次已失败,连着重打没用,等下次断连再清标志。
+ * CCC 未就绪:UART 可以先拉 0x40,但 SOC 0x41 等主机使能通知后再发。 */
+ if (!paired || s_ver.give_up)
+ {
+ return;
+ }
+
+ /* 空闲且还有事没做完:没缓存就去拉,有缓存没上报就去报 */
+ if (s_ver.state == MCU_VER_STATE_IDLE)
+ {
+ if (!s_ver.cached || !s_ver.reported)
+ {
+ mcu_version_start_session();
+ }
+ return;
+ }
+
+ /* 等 MCU 回 UART 0x41:500ms 窗口内不重发;满 3 次则本轮放弃 */
+ if (s_ver.state == MCU_VER_STATE_FETCH)
+ {
+ if ((s_ver.fetch_retry > 0) &&
+ ((now - s_ver.last_tx_ms) <= MCU_VER_ACK_WAIT_MS))
+ {
+ return;
+ }
+ if (s_ver.fetch_retry >= MCU_VER_MAX_RETRY)
+ {
+ log_error("0x40 give up, wait next connect");
+ s_ver.state = MCU_VER_STATE_IDLE;
+ s_ver.give_up = 1;
+ return;
+ }
+ (void)mcu_version_tx_fetch();
+ return;
+ }
+
+ /* 等网关 ACK:2s 窗口内不重发(走网关比 UART 慢);满 3 次则本轮放弃 */
+ if (s_ver.state == MCU_VER_STATE_REPORT)
+ {
+ if ((s_ver.report_retry > 0) &&
+ ((now - s_ver.last_tx_ms) <= MCU_VER_REPORT_WAIT_MS))
+ {
+ return;
+ }
+ if (s_ver.report_retry >= MCU_VER_MAX_RETRY)
+ {
+ log_error("SOC 0x41 give up, wait next connect");
+ s_ver.state = MCU_VER_STATE_IDLE;
+ s_ver.give_up = 1;
+ return;
+ }
+ (void)mcu_version_tx_report();
+ }
+}
+
+/**
+ * @brief 处理 UART 0x41
+ * @param frame 完整帧
+ * @param len 长度
+ */
+void mcu_version_on_mcu_info_ack(const uint8_t *frame, uint16_t len)
+{
+ if (!frame || len < MCU_VER_UART_ACK_MIN_LEN)
+ {
+ log_error("0x41 too short %u", len);
+ return;
+ }
+ /* frame[6] 是 Result,非 0 表示 MCU 拒绝,不缓存、不上报 */
+ if (frame[6] != MCU_RESULT_OK)
+ {
+ log_error("0x41 result=0x%02X", frame[6]);
+ return;
+ }
+
+ memcpy(s_ver.mcu_ver, &frame[7], 3);
+ memcpy(s_ver.hw_ver, &frame[10], 3);
+ s_ver.cached = 1;
+ s_ver.reported = 0; /* 新版本尚未报给网关 */
+ s_ver.report_retry = 0;
+ s_ver.state = MCU_VER_STATE_REPORT;
+ log_info("MCU ver %u.%u.%u hw %u.%u.%u",
+ s_ver.mcu_ver[0], s_ver.mcu_ver[1], s_ver.mcu_ver[2],
+ s_ver.hw_ver[0], s_ver.hw_ver[1], s_ver.hw_ver[2]);
+ (void)mcu_version_tx_report();
+}
+
+/**
+ * @brief 网关 ACK:MCU 0x07 优先消费,版本上报绝不回 MCU 0x08
+ * @param success 成功
+ * @param mcu_report_consumed 已被 MCU 上报占用
+ */
+void mcu_version_on_gateway_ack(uint8_t success, uint8_t mcu_report_consumed)
+{
+ /* 当前不是在等版本上报的 ACK,忽略 */
+ if (s_ver.state != MCU_VER_STATE_REPORT)
+ {
+ return;
+ }
+ /* 这条 ACK 已经给 MCU 0x07 回过 0x08 了,不是给版本的,版本继续等 */
+ if (mcu_report_consumed)
+ {
+ log_debug("gateway ack used by MCU 0x07, version waits");
+ return;
+ }
+ /* 网关明确失败:不置 reported,周期函数超时后会重发 SOC 0x41 */
+ if (!success)
+ {
+ log_error("SOC 0x41 gateway ack fail");
+ return;
+ }
+ s_ver.reported = 1;
+ s_ver.state = MCU_VER_STATE_IDLE;
+ log_info("SOC 0x41 report ok");
+}
+
+/**
+ * @brief 是否已缓存 MCU 版本
+ * @return 1/0
+ */
+uint8_t mcu_version_is_cached(void)
+{
+ return s_ver.cached;
+}
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_version.h b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_version.h
new file mode 100644
index 0000000..a89b462
--- /dev/null
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/business/mcu_version.h
@@ -0,0 +1,52 @@
+/******************************************************************************
+ * @file mcu_version.h
+ * @brief 版本业务:UART 0x40/0x41 拉 MCU 信息,SOC 0x41 上报网关
+ * @author cyWu <1917507415@qq.com>
+ * @date 2026.08.13
+ * @version V1.0.0
+ * @history
+ * - V1.0.0, 2026.08.13, cyWu, 首次发布(P1:0x40/0x41 + SOC 0x41)
+ ******************************************************************************/
+
+#ifndef __MCU_VERSION_H__
+#define __MCU_VERSION_H__
+
+#include
+
+/** BLE 模组软件版本 V1.0.0(三字节,对应显示 Vx.x.x) */
+#define MCU_BLE_FW_VER_MAJOR (1u)
+#define MCU_BLE_FW_VER_MINOR (0u)
+#define MCU_BLE_FW_VER_PATCH (0u)
+
+/**
+ * @brief 初始化版本业务(清空 RAM 缓存)
+ */
+void mcu_version_init(void);
+
+/**
+ * @brief 周期处理:已配网且已连接时拉 MCU 版本,成功后上报网关
+ * @note 仅在协议任务上下文调用
+ */
+void mcu_version_periodic(void);
+
+/**
+ * @brief 处理 UART 0x41(MCU 信息应答),只本地解析,不转发网关
+ * @param frame 完整帧
+ * @param len 长度
+ */
+void mcu_version_on_mcu_info_ack(const uint8_t *frame, uint16_t len);
+
+/**
+ * @brief 网关对 REPORTCMD_V2 的应答
+ * @param success 1=网关成功 ACK
+ * @param mcu_report_consumed 1=该 ACK 已按 MCU 0x07 消费(回了 0x08)
+ */
+void mcu_version_on_gateway_ack(uint8_t success, uint8_t mcu_report_consumed);
+
+/**
+ * @brief MCU 版本缓存是否就绪
+ * @return 1=已缓存,0=无
+ */
+uint8_t mcu_version_is_cached(void);
+
+#endif /* __MCU_VERSION_H__ */
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/mcu_bridge.c b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/mcu_bridge.c
index 3facdc4..18c5e06 100644
--- a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/mcu_bridge.c
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/mcu_bridge.c
@@ -2,8 +2,8 @@
* @file mcu_bridge.c
* @brief BLE+MCU 接入模块对外总入口实现(组合门面,仅转调)
* @author cyWu <1917507415@qq.com>
- * @date 2026.08.12
- * @version V2.0.0
+ * @date 2026.08.13
+ * @version V2.1.0
* @history
* - V1.0.0, 2026.08.06, cyWu, 首次发布
* - V1.1.0, 2026.08.06, cyWu, 改为事件队列 + 独立任务驱动
@@ -12,6 +12,7 @@
* - V1.4.0, 2026.08.12, cyWu, 工作状态 Bit0/1/2 互斥
* - V2.0.0, 2026.08.12, cyWu, 拆分为 protocol/ + business/ 分层,
* 本文件收敛为纯组合门面,不再持有任何状态
+ * - V2.1.0, 2026.08.13, cyWu, P1 增加网络时间入口
******************************************************************************/
#include "mcu_bridge.h"
@@ -84,3 +85,11 @@ void mcu_bridge_on_gateway_report_ack(uint16_t cmd,
{
mcu_protocol_notify_gateway_ack(cmd, gateway_int_cnt, gateway_int0);
}
+
+/**
+ * @brief 网关网络时间:经协议层事件队列串行化
+ */
+void mcu_bridge_on_network_time(uint64_t unix_s, int32_t gmtoff_s)
+{
+ mcu_protocol_notify_network_time(unix_s, gmtoff_s);
+}
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/mcu_bridge.h b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/mcu_bridge.h
index 7993176..af7793c 100644
--- a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/mcu_bridge.h
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/mcu_bridge.h
@@ -2,8 +2,8 @@
* @file mcu_bridge.h
* @brief BLE+MCU 接入模块对外总入口(组合门面)
* @author cyWu <1917507415@qq.com>
- * @date 2026.08.12
- * @version V2.0.0
+ * @date 2026.08.13
+ * @version V2.1.0
* @history
* - V1.0.0, 2026.08.06, cyWu, 首次发布
* - V1.1.0, 2026.08.06, cyWu, 内部改为事件队列 + 独立任务驱动
@@ -11,6 +11,7 @@
* - V2.0.0, 2026.08.12, cyWu, 整体重构为 protocol/ + business/ 分层:
* 本文件不再包含任何实现逻辑,只做“组合根”,对外 API 保持不变,
* 外部调用方(app_main.c / usr_le_adv.c / usr_le_recieve.c 等)无需改动
+ * - V2.1.0, 2026.08.13, cyWu, P1 增加网络时间入口;业务子模块扩到管理/对时/版本
******************************************************************************/
#ifndef __MCU_BRIDGE_H__
@@ -34,7 +35,10 @@
/* ├── mcu_identity.c/h 身份业务(0x01/0x02) */
/* ├── mcu_pairing.c/h 配网业务(0x10、超时) */
/* ├── mcu_link_status.c/h 连接状态业务(Bit0/1/2/6 位图) */
-/* └── mcu_business.c/h 业务门面:协调以上三者,供协议层调用 */
+/* ├── mcu_mgmt.c/h 模组管理(0x16/0x18/0x1A) */
+/* ├── mcu_time.c/h 对时(0x1C/0x1D,写入 usr_rtc) */
+/* ├── mcu_version.c/h MCU 信息 + SOC 0x41 上报 */
+/* └── mcu_business.c/h 业务门面:协调子模块,供协议层调用 */
/* */
/* 依赖方向(单向,禁止反向 include): */
/* mcu_bridge → protocol → business(子模块) → business(门面) */
@@ -110,4 +114,12 @@ void mcu_bridge_on_gateway_report_ack(uint16_t cmd,
uint8_t gateway_int_cnt,
int32_t gateway_int0);
+/**
+ * @brief 网关 DEVICEINFO 中的网络时间(BLE 上下文可调,内部入队)
+ * @param unix_s UTC Unix 时间戳(秒)
+ * @param gmtoff_s 时区偏移(秒)
+ * @note 网关从子设备连上开始计时,每 12 小时同步一次
+ */
+void mcu_bridge_on_network_time(uint64_t unix_s, int32_t gmtoff_s);
+
#endif /* __MCU_BRIDGE_H__ */
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/protocol/mcu_cmd.h b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/protocol/mcu_cmd.h
index 3c4f07f..0e05f7a 100644
--- a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/protocol/mcu_cmd.h
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/protocol/mcu_cmd.h
@@ -24,8 +24,8 @@
/*============================================================================*/
#define MCU_WS_PAIRING (1u << 0) /**< 配网中(绑定广播) */
-#define MCU_WS_CONFIGURED (1u << 1) /**< 已配网正常态(已连接/稳定工作) */
-#define MCU_WS_RECONNECTING (1u << 2) /**< 断线重连中(回连广播) */
+#define MCU_WS_CONFIGURED (1u << 1) /**< 已配网且链路可发数(已连接+CCC) */
+#define MCU_WS_RECONNECTING (1u << 2) /**< 已绑定但链路未通(断连,或已连未写 CCC) */
#define MCU_WS_CLOUD_ONLINE (1u << 3) /**< 已连接到云,预留:暂固定 0 */
#define MCU_WS_APP_ONLINE (1u << 4) /**< APP 在线,预留:暂固定 0 */
#define MCU_WS_APP_DIRECT (1u << 5) /**< APP 直连,预留:暂固定 0 */
@@ -60,7 +60,7 @@ typedef enum {
MCU_CMD_WORK_STATUS = 0x14,
MCU_CMD_WORK_STATUS_ACK = 0x15,
- /* 模组管理(后续扩展) */
+ /* 模组管理 */
MCU_CMD_RESET = 0x16,
MCU_CMD_RESET_ACK = 0x17,
MCU_CMD_REBOOT = 0x18,
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/protocol/mcu_protocol.c b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/protocol/mcu_protocol.c
index 3d4c086..1e4a89b 100644
--- a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/protocol/mcu_protocol.c
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/protocol/mcu_protocol.c
@@ -2,27 +2,28 @@
* @file mcu_protocol.c
* @brief MCU 接入协议引擎实现
* @author cyWu <1917507415@qq.com>
- * @date 2026.08.12
- * @version V1.1.0
+ * @date 2026.08.13
+ * @version V1.2.0
* @history
* - V1.0.0, 2026.08.12, cyWu, 从 mcu_bridge.c 拆出(历史版本见原文件):
* 事件队列 + 独立任务驱动、LP target、心跳/透传重传均原样保留
* - V1.1.0, 2026.08.12, cyWu, 0x07 上报改为不缓存、不超时重发:
* MCU 自身已有超时重发机制,模组侧只记 SN 转发+等 ACK 回 0x08,
* 去掉 report_pending 整包缓存与 mcu_protocol_check_report_pending()
+ * - V1.2.0, 2026.08.13, cyWu, P1 管理命令分发、pending 发送、网络时间入队
******************************************************************************/
#include "system/includes.h"
#include "asm/power/power_api.h"
#include "mcu_protocol.h"
-#include "mcu_uart.h"
+#include "mcu_business.h"
#include "mcu_frame.h"
+#include "mcu_uart.h"
#include "mcu_util.h"
#include "usr_le_api.h"
-#include "mcu_business.h"
#define LOG_TAG_CONST APP
-#define LOG_TAG "[MCU_PROTO]"
+#define LOG_TAG "[MCU_PROTO]"
#define LOG_ERROR_ENABLE
#define LOG_DEBUG_ENABLE
#define LOG_INFO_ENABLE
@@ -34,16 +35,16 @@
/* 私有宏 */
/*============================================================================*/
-#define MCU_HEARTBEAT_IDLE_MS (55000u)
+#define MCU_HEARTBEAT_IDLE_MS (55000u)
#define MCU_HEARTBEAT_MAX_RETRY (3u)
-#define MCU_UART_ACK_WAIT_MS (500u) /* 等 MCU UART ACK 超时(心跳/透传/0x14 等) */
-#define MCU_APP_ACK_MAX_RETRY (3u)
+#define MCU_UART_ACK_WAIT_MS (500u) /* 等 MCU UART ACK 超时(心跳/透传/0x14 等) */
+#define MCU_APP_ACK_MAX_RETRY (3u)
#define MCU_PROTOCOL_TICK_MS (200u)
/* 杰理 os_sem_pend 超时单位是 OS tick,OS_TICKS_PER_SEC=100 → 1 tick = 10ms */
-#define MCU_PROTOCOL_TICK_OS ((MCU_PROTOCOL_TICK_MS + 9u) / 10u)
-#define MCU_EVT_QUEUE_DEPTH (8u)
-#define MCU_PROTOCOL_TASK_PRIO (31u)
+#define MCU_PROTOCOL_TICK_OS ((MCU_PROTOCOL_TICK_MS + 9u) / 10u)
+#define MCU_EVT_QUEUE_DEPTH (8u)
+#define MCU_PROTOCOL_TASK_PRIO (31u)
#define MCU_PROTOCOL_TASK_STACK (2048u)
/*============================================================================*/
@@ -52,12 +53,12 @@
typedef struct
{
- uint8_t active;
- uint8_t sn;
- uint8_t expect_ack;
- uint8_t retry;
- uint32_t send_time;
- uint8_t buf[MCU_FRAME_MAX_LEN];
+ uint8_t active; /* 1=正在等 MCU UART ACK */
+ uint8_t sn; /* 发出去的 SN,用来对 ACK */
+ uint8_t expect_ack; /* 期望的 ACK 命令字,如 0x04/0x13 */
+ uint8_t retry; /* 已重发次数(不含首次) */
+ uint32_t send_time; /* 最近一次发出时刻 */
+ uint8_t buf[MCU_FRAME_MAX_LEN]; /* 仅 app_pending 缓存整包,便于重发 */
uint16_t len;
} McuPending_t;
@@ -67,7 +68,8 @@ typedef enum
MCU_EVT_APP_FRAME,
MCU_EVT_GATEWAY_ACK,
MCU_EVT_BLE_PAIRED,
- MCU_EVT_BLE_PAIR_CLEARED
+ MCU_EVT_BLE_PAIR_CLEARED,
+ MCU_EVT_NET_TIME
} McuEvtType_t;
typedef struct
@@ -78,22 +80,24 @@ typedef struct
uint16_t gw_cmd;
uint8_t gw_int_cnt;
int32_t gw_int0;
+ uint64_t unix_s;
+ int32_t gmtoff_s;
} McuEvt_t;
typedef struct
{
uint8_t initialized;
- uint8_t sn;
- uint8_t last_pushed_status;
- uint8_t hb_fail_cnt;
- uint8_t hb_waiting;
- uint8_t mcu_comm_alarm;
- uint32_t last_mcu_rx_ms;
- uint32_t last_hb_tx_ms;
- McuPending_t app_pending; /* APP→MCU 透传等待 */
- McuPending_t ble_pending; /* BLE 主动请求等待(心跳/状态等) */
- uint8_t report_waiting; /* 1=已转网关,等 ACK 后再回 MCU 0x08 */
- uint8_t report_sn; /* 等待回 0x08 的 SN */
+ uint8_t sn; /* 模组侧发出帧的 SN 发生器 */
+ uint8_t last_pushed_status; /* 上次已推给 MCU 的 0x14 状态,0xFF=强制再推 */
+ uint8_t hb_fail_cnt; /* 心跳连续失败次数 */
+ uint8_t hb_waiting; /* 1=已发 0x12,正在等 0x13 */
+ uint8_t mcu_comm_alarm; /* 1=心跳连失败 3 次,MCU 通信告警 */
+ uint32_t last_mcu_rx_ms; /* 最近一次收到 MCU 任意合法帧 */
+ uint32_t last_hb_tx_ms; /* 最近一次发出心跳 */
+ McuPending_t app_pending; /* APP 0x03/0x05 透传,等 MCU 0x04/0x06 */
+ McuPending_t ble_pending; /* 模组主动 UART(心跳/0x14/0x40/0x1A),一次只能占一个 */
+ uint8_t report_waiting; /* 1=MCU 0x07 已转网关,等 ACK 后再回 0x08 */
+ uint8_t report_sn; /* 等待回 0x08 的那条 0x07 的 SN */
} McuProtocol_t;
typedef struct
@@ -125,6 +129,7 @@ static int32_t mcu_protocol_send_frame(uint8_t cmd, uint8_t sn,
static void mcu_protocol_send_ack(uint8_t cmd, uint8_t sn, uint8_t result);
static void mcu_protocol_handle_frame(const uint8_t *frame, uint16_t len);
static void mcu_protocol_handle_pair_cfg(const uint8_t *frame, uint16_t len);
+static void mcu_protocol_handle_get_time(const uint8_t *frame, uint16_t len);
static void mcu_protocol_handle_report(const uint8_t *frame, uint16_t len);
static void mcu_protocol_handle_app_frame(const uint8_t *data, uint16_t len);
static void mcu_protocol_handle_gateway_ack(uint16_t cmd, uint8_t gateway_int_cnt,
@@ -218,6 +223,9 @@ static void mcu_protocol_dispatch_event(const McuEvt_t *evt)
case MCU_EVT_BLE_PAIR_CLEARED:
mcu_business_on_pair_cleared();
break;
+ case MCU_EVT_NET_TIME:
+ mcu_business_on_network_time(evt->unix_s, evt->gmtoff_s);
+ break;
default:
log_error("unknown evt type=%d", evt->type);
break;
@@ -315,6 +323,59 @@ int32_t mcu_protocol_send_cmd(uint8_t cmd, const uint8_t *payload, uint16_t plen
return mcu_protocol_send_frame(cmd, mcu_protocol_next_sn(), payload, plen);
}
+/**
+ * @brief 发送并占用 ble_pending
+ * @param cmd 命令
+ * @param payload 载荷
+ * @param plen 长度
+ * @param expect_ack 期望 ACK
+ * @return 帧长;<0 忙或失败
+ */
+int32_t mcu_protocol_send_cmd_wait_ack(uint8_t cmd, const uint8_t *payload,
+ uint16_t plen, uint8_t expect_ack)
+{
+ uint8_t sn;
+ int32_t total;
+
+ /* UART 一次只等一条 ACK,已被心跳/0x14/0x40 等占用则拒绝 */
+ if (s_proto.ble_pending.active)
+ {
+ return -1;
+ }
+
+ sn = mcu_protocol_next_sn();
+ total = mcu_protocol_send_frame(cmd, sn, payload, plen);
+ if (total <= 0)
+ {
+ return -1;
+ }
+
+ s_proto.ble_pending.active = 1;
+ s_proto.ble_pending.sn = sn;
+ s_proto.ble_pending.expect_ack = expect_ack;
+ s_proto.ble_pending.send_time = mcu_util_get_ms();
+ s_proto.ble_pending.retry = 0;
+ return total;
+}
+
+/**
+ * @brief ble_pending 是否占用
+ * @return 1/0
+ */
+uint8_t mcu_protocol_is_ble_pending(void)
+{
+ return s_proto.ble_pending.active ? 1 : 0;
+}
+
+/**
+ * @brief MCU 0x07 是否在等网关 ACK
+ * @return 1/0
+ */
+uint8_t mcu_protocol_is_mcu_report_waiting(void)
+{
+ return s_proto.report_waiting ? 1 : 0;
+}
+
/**
* @brief 发送仅含 Result 的 ACK 帧
* @param cmd ACK 命令
@@ -357,11 +418,13 @@ static void mcu_protocol_handle_pair_cfg(const uint8_t *frame, uint16_t len)
mode = frame[6];
timeout_s = mcu_util_read_u16_be(&frame[7]);
+ /* 协议只认 mode=1/2,其它直接回参数错误 */
if (mode != 1 && mode != 2)
{
mcu_protocol_send_ack(MCU_CMD_PAIR_CFG_ACK, sn, MCU_RESULT_PARAM_ERR);
return;
}
+ /* 还没拿到 MCU 身份,广播都发不了,回忙让 MCU 稍后再试 */
if (!mcu_business_is_identity_ready())
{
mcu_protocol_send_ack(MCU_CMD_PAIR_CFG_ACK, sn, MCU_RESULT_BUSY);
@@ -372,6 +435,32 @@ static void mcu_protocol_handle_pair_cfg(const uint8_t *frame, uint16_t len)
mcu_business_on_pair_cfg(mode, timeout_s);
}
+/**
+ * @brief 处理 0x1C:组 0x1D(时间由 business 填充)
+ * @param frame 完整帧
+ * @param len 长度
+ */
+static void mcu_protocol_handle_get_time(const uint8_t *frame, uint16_t len)
+{
+ uint8_t sn;
+ uint8_t payload[16];
+ uint16_t plen = 0;
+
+ (void)len;
+ if (!frame)
+ {
+ return;
+ }
+ sn = frame[5];
+ mcu_business_build_time_ack(payload, &plen);
+ if (plen == 0)
+ {
+ plen = 1;
+ payload[0] = MCU_RESULT_OK;
+ }
+ mcu_protocol_send_frame(MCU_CMD_GET_TIME_ACK, sn, payload, plen);
+}
+
/**
* @brief 处理 MCU 上报 0x07:转网关,等网关 ACK 后再回 0x08
* @param frame 完整帧
@@ -383,9 +472,20 @@ static void mcu_protocol_handle_report(const uint8_t *frame, uint16_t len)
{
uint8_t sn = frame[5];
+ /* 已配网也会在 CCC 之前连上;这时发出去会失败,不置 waiting,让 MCU 自己重发 0x07 */
+ if (!usr_ble_is_notify_ready())
+ {
+ log_debug("MCU 0x07 wait CCC sn=%u, MCU will retry", sn);
+ return;
+ }
+
+ if (usr_le_send_mcu_frame((u8 *)frame, len) == 0)
+ {
+ log_error("MCU 0x07 send fail sn=%u", sn);
+ return;
+ }
s_proto.report_sn = sn;
s_proto.report_waiting = 1;
- usr_le_send_mcu_frame((u8 *)frame, len);
log_debug("MCU report sn=%u -> gateway, wait ACK", sn);
}
@@ -404,6 +504,7 @@ static void mcu_protocol_handle_frame(const uint8_t *frame, uint16_t len)
return;
}
+ /* 收到任意合法 MCU 帧:MCU 还活着,清心跳失败与告警 */
s_proto.last_mcu_rx_ms = mcu_util_get_ms();
s_proto.hb_fail_cnt = 0;
s_proto.hb_waiting = 0;
@@ -413,6 +514,7 @@ static void mcu_protocol_handle_frame(const uint8_t *frame, uint16_t len)
sn = frame[5];
log_debug("MCU RX cmd=0x%02X sn=%u len=%u", cmd, sn, len);
+ /* APP 透传在等这条 ACK(命令+SN 都对上):转给 APP,不再往下分发 */
if (s_proto.app_pending.active &&
(cmd == s_proto.app_pending.expect_ack) &&
(sn == s_proto.app_pending.sn))
@@ -422,6 +524,7 @@ static void mcu_protocol_handle_frame(const uint8_t *frame, uint16_t len)
return;
}
+ /* 模组主动 UART 在等这条 ACK:先释放 pending,再落到下面 switch */
if (s_proto.ble_pending.active &&
(cmd == s_proto.ble_pending.expect_ack) &&
(sn == s_proto.ble_pending.sn))
@@ -432,7 +535,7 @@ static void mcu_protocol_handle_frame(const uint8_t *frame, uint16_t len)
s_proto.hb_waiting = 0;
s_proto.hb_fail_cnt = 0;
}
- return;
+ /* 0x41/0x1B 等其余 ACK 继续走 switch,交给 business */
}
switch (cmd)
@@ -444,20 +547,37 @@ static void mcu_protocol_handle_frame(const uint8_t *frame, uint16_t len)
mcu_protocol_handle_pair_cfg(frame, len);
break;
case MCU_CMD_REPORT:
- mcu_protocol_handle_report(frame, len);
+ mcu_protocol_handle_report(frame, len); /* 转网关,等 ACK 再回 0x08 */
break;
case MCU_CMD_CONTROL_ACK:
case MCU_CMD_READ_ACK:
+ /* 没匹配上 app_pending(SN 不对或没在等)仍转给 APP,避免 MCU 应答丢掉 */
usr_le_send_mcu_frame((u8 *)frame, len);
break;
+ case MCU_CMD_RESET:
+ mcu_protocol_send_ack(MCU_CMD_RESET_ACK, sn, MCU_RESULT_OK);
+ mcu_business_on_factory_reset();
+ break;
+ case MCU_CMD_REBOOT:
+ mcu_protocol_send_ack(MCU_CMD_REBOOT_ACK, sn, MCU_RESULT_OK);
+ mcu_business_on_mod_reboot();
+ break;
+ case MCU_CMD_GET_TIME:
+ mcu_protocol_handle_get_time(frame, len);
+ break;
+ case MCU_CMD_GET_MCU_INFO_ACK:
+ mcu_business_on_mcu_info_ack(frame, len); /* 只本地解析,不原样转发网关 */
+ break;
+ case MCU_CMD_MCU_REBOOT_ACK:
+ mcu_business_on_mcu_reboot_ack(frame, len);
+ break;
case MCU_CMD_HEARTBEAT_ACK:
case MCU_CMD_WORK_STATUS_ACK:
- break;
+ break; /* pending 已在上面释放,无需再处理载荷 */
default:
- if (cmd == MCU_CMD_RESET || cmd == MCU_CMD_REBOOT ||
- cmd == MCU_CMD_GET_TIME || cmd == MCU_CMD_FACTORY ||
- cmd == MCU_CMD_PHOTO || cmd == MCU_CMD_RECORD ||
- cmd == MCU_CMD_AUDIO)
+ /* 工厂/拍照/录音/音频:协议有命令字但模组未实现,回不支持 */
+ if (cmd == MCU_CMD_FACTORY || cmd == MCU_CMD_PHOTO ||
+ cmd == MCU_CMD_RECORD || cmd == MCU_CMD_AUDIO)
{
mcu_protocol_send_ack((uint8_t)(cmd + 1), sn, MCU_RESULT_NOT_SUPPORT);
}
@@ -479,6 +599,8 @@ static void mcu_protocol_handle_app_frame(const uint8_t *data, uint16_t len)
cmd = data[4];
sn = data[5];
+ /* 0x03/0x05 必须等 MCU ACK 再回 APP,所以进 pending 可超时重发。
+ * 其它 APP 帧直接透传到 UART,模组不管应答。 */
if (cmd == MCU_CMD_CONTROL)
{
expect_ack = MCU_CMD_CONTROL_ACK;
@@ -493,6 +615,7 @@ static void mcu_protocol_handle_app_frame(const uint8_t *data, uint16_t len)
return;
}
+ /* 上一条 0x03/0x05 还没回,丢掉新的,避免 SN 对乱 */
if (s_proto.app_pending.active)
{
log_error("app frame busy, drop cmd=0x%02X", cmd);
@@ -523,23 +646,29 @@ static void mcu_protocol_handle_gateway_ack(uint16_t cmd,
int32_t gateway_int0)
{
uint8_t sn;
+ uint8_t success;
+ uint8_t mcu_consumed = 0;
- if (!s_proto.report_waiting)
- {
- return;
- }
+ /* 网关成功 ACK 约定:cmd=0、只有 1 个 int、且 int0=0 */
+ success = (cmd == 0 && gateway_int_cnt == 1 && gateway_int0 == 0) ? 1 : 0;
- if (cmd == 0 && gateway_int_cnt == 1 && gateway_int0 == 0)
+ /* MCU 0x07 优先:成功才回 0x08;失败不回,让 MCU 自己超时重发 0x07 */
+ if (s_proto.report_waiting && success)
{
sn = s_proto.report_sn;
mcu_protocol_send_ack(MCU_CMD_REPORT_ACK, sn, MCU_RESULT_OK);
s_proto.report_waiting = 0;
+ mcu_consumed = 1;
log_info("gateway report ack ok, MCU 0x08 sn=%u", sn);
- return;
+ }
+ else if (s_proto.report_waiting)
+ {
+ log_error("gateway report ack fail cmd=%u cnt=%u int0=%ld",
+ (unsigned)cmd, (unsigned)gateway_int_cnt, (long)gateway_int0);
}
- log_error("gateway report ack fail cmd=%u cnt=%u int0=%ld",
- (unsigned)cmd, (unsigned)gateway_int_cnt, (long)gateway_int0);
+ /* 再交给版本上报:若已被 0x07 占用,版本侧会继续等,不会误当成自己的 ACK */
+ mcu_business_on_gateway_ack(success, mcu_consumed);
}
/*============================================================================*/
@@ -556,13 +685,21 @@ static void mcu_protocol_try_heartbeat(void)
uint8_t buf[MCU_FRAME_MAX_LEN];
int32_t total;
+ /* 身份都还没通,先别发心跳 */
if (!mcu_business_is_identity_ready())
{
return;
}
+ /* ble_pending 被 0x14/0x40 等占用时不插心跳;自己等 0x13 时除外 */
+ if (s_proto.ble_pending.active && !s_proto.hb_waiting)
+ {
+ return;
+ }
+
if (s_proto.hb_waiting)
{
+ /* 500ms 内还在等 0x13,不重发 */
if ((now - s_proto.last_hb_tx_ms) <= MCU_UART_ACK_WAIT_MS)
{
return;
@@ -571,6 +708,7 @@ static void mcu_protocol_try_heartbeat(void)
s_proto.ble_pending.active = 0;
s_proto.hb_fail_cnt++;
log_error("heartbeat timeout fail=%u", s_proto.hb_fail_cnt);
+ /* 连续 3 次没回:置告警,并刷新 last_rx 避免立刻再打心跳 */
if (s_proto.hb_fail_cnt >= MCU_HEARTBEAT_MAX_RETRY)
{
s_proto.mcu_comm_alarm = 1;
@@ -581,6 +719,7 @@ static void mcu_protocol_try_heartbeat(void)
}
else if ((now - s_proto.last_mcu_rx_ms) < MCU_HEARTBEAT_IDLE_MS)
{
+ /* 55s 内 MCU 有过任何合法帧,视为还活着,不发心跳 */
return;
}
@@ -623,10 +762,12 @@ static void mcu_protocol_try_work_status(void)
}
ws = mcu_business_get_work_status();
+ /* 状态没变就不再推 0x14;0xFF 表示强制重推 */
if (ws == s_proto.last_pushed_status)
{
return;
}
+ /* UART 正在等别的 ACK,让下个周期再推 */
if (s_proto.ble_pending.active)
{
return;
@@ -671,11 +812,13 @@ static void mcu_protocol_check_app_pending(void)
}
now = mcu_util_get_ms();
+ /* 500ms 窗口内继续等 MCU ACK */
if ((now - s_proto.app_pending.send_time) <= MCU_UART_ACK_WAIT_MS)
{
return;
}
+ /* 重发 3 次仍无 ACK:主动给 APP 回 FAIL,避免 APP 一直干等 */
if (s_proto.app_pending.retry >= MCU_APP_ACK_MAX_RETRY)
{
log_error("app pending give up cmd expect=0x%02X, notify APP FAIL",
@@ -709,6 +852,7 @@ static void mcu_protocol_check_ble_pending(void)
{
return;
}
+ /* 心跳超时由 try_heartbeat 自己处理,这里不管 */
if (s_proto.ble_pending.expect_ack == MCU_CMD_HEARTBEAT_ACK)
{
return;
@@ -717,6 +861,7 @@ static void mcu_protocol_check_ble_pending(void)
now = mcu_util_get_ms();
if ((now - s_proto.ble_pending.send_time) > MCU_UART_ACK_WAIT_MS)
{
+ /* 0x14 超时:清 pending,并把 last 置 0xFF,下周期会再推一次 */
if (s_proto.ble_pending.expect_ack == MCU_CMD_WORK_STATUS_ACK)
{
log_error("work_status ack timeout, will retry");
@@ -879,3 +1024,19 @@ void mcu_protocol_notify_gateway_ack(uint16_t cmd,
evt.gw_int0 = gateway_int0;
mcu_protocol_post_event(&evt);
}
+
+/**
+ * @brief 网络时间入队
+ * @param unix_s UTC Unix 时间戳(秒)
+ * @param gmtoff_s 时区偏移秒
+ */
+void mcu_protocol_notify_network_time(uint64_t unix_s, int32_t gmtoff_s)
+{
+ McuEvt_t evt;
+
+ memset(&evt, 0, sizeof(evt));
+ evt.type = MCU_EVT_NET_TIME;
+ evt.unix_s = unix_s;
+ evt.gmtoff_s = gmtoff_s;
+ mcu_protocol_post_event(&evt);
+}
diff --git a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/protocol/mcu_protocol.h b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/protocol/mcu_protocol.h
index 0d44c8e..4fae81f 100644
--- a/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/protocol/mcu_protocol.h
+++ b/JBao_JL7016_MOD_FW/apps/usr_mcu_bridge/protocol/mcu_protocol.h
@@ -2,11 +2,12 @@
* @file mcu_protocol.h
* @brief MCU 接入协议引擎:事件队列 / 任务 / 命令分发 / ACK / 透传重传 / 心跳
* @author cyWu <1917507415@qq.com>
- * @date 2026.08.12
- * @version V1.0.0
+ * @date 2026.08.13
+ * @version V1.1.0
* @history
* - V1.0.0, 2026.08.12, cyWu, 从 mcu_bridge.c 拆出,仅保留协议机制,
* 不含任何业务判断(身份/配网/连接状态见 ../business)
+ * - V1.1.0, 2026.08.13, cyWu, P1:管理命令分发、pending 发送、网关时间入队
******************************************************************************/
#ifndef __MCU_PROTOCOL_H__
@@ -62,6 +63,13 @@ void mcu_protocol_notify_gateway_ack(uint16_t cmd,
uint8_t gateway_int_cnt,
int32_t gateway_int0);
+/**
+ * @brief 网关 DEVICEINFO 网络时间(入队,BLE 上下文可调)
+ * @param unix_s UTC Unix 时间戳(秒)
+ * @param gmtoff_s 时区偏移(秒)
+ */
+void mcu_protocol_notify_network_time(uint64_t unix_s, int32_t gmtoff_s);
+
/*============================================================================*/
/* 供 business 层调用的协议发送接口 */
/*============================================================================*/
@@ -75,6 +83,29 @@ void mcu_protocol_notify_gateway_ack(uint16_t cmd,
*/
int32_t mcu_protocol_send_cmd(uint8_t cmd, const uint8_t *payload, uint16_t plen);
+/**
+ * @brief 发送并占用 ble_pending,等待 expect_ack
+ * @param cmd 命令字
+ * @param payload 载荷,可为 NULL
+ * @param plen 载荷长度
+ * @param expect_ack 期望的 MCU ACK 命令
+ * @return 帧长;<0 忙或失败
+ */
+int32_t mcu_protocol_send_cmd_wait_ack(uint8_t cmd, const uint8_t *payload,
+ uint16_t plen, uint8_t expect_ack);
+
+/**
+ * @brief BLE 主动 UART 请求是否正在等 ACK(心跳/0x14/0x40 等)
+ * @return 1=忙,0=空闲
+ */
+uint8_t mcu_protocol_is_ble_pending(void);
+
+/**
+ * @brief MCU 0x07 是否已转发、正在等网关 ACK
+ * @return 1=是,0=否
+ */
+uint8_t mcu_protocol_is_mcu_report_waiting(void);
+
/**
* @brief 强制下一轮重新推送 0x14(业务状态变化后调用)
*/
diff --git a/JBao_JL7016_MOD_FW/apps/usr_periph/usr_rtc.c b/JBao_JL7016_MOD_FW/apps/usr_periph/usr_rtc.c
new file mode 100644
index 0000000..5fef500
--- /dev/null
+++ b/JBao_JL7016_MOD_FW/apps/usr_periph/usr_rtc.c
@@ -0,0 +1,540 @@
+/******************************************************************************
+ * @file usr_rtc.c
+ * @brief 板级 RTC 外设实现(BR28 硬件 rtc_dev_ops + 32K)
+ * @author cyWu <1917507415@qq.com>
+ * @date 2026.08.14
+ * @version V1.0.0
+ * @history
+ * - V1.0.0, 2026.08.14, cyWu, 首次发布
+ ******************************************************************************/
+
+#include "system/includes.h"
+#include "app_config.h"
+#include "usr_rtc.h"
+#include "asm/rtc.h"
+
+#define LOG_TAG_CONST APP
+#define LOG_TAG "[USR_RTC]"
+#define LOG_ERROR_ENABLE
+#define LOG_DEBUG_ENABLE
+#define LOG_INFO_ENABLE
+#include "debug.h"
+
+/*============================================================================*/
+/* 私有宏 */
+/*============================================================================*/
+
+#define USR_RTC_TZ_MAGIC (0x52544331u) /**< 'RTC1',校验 VM 时区 */
+#define USR_RTC_LOG_PERIOD_MS (60000u) /**< 周期日志间隔 */
+
+/*============================================================================*/
+/* 私有类型 */
+/*============================================================================*/
+
+typedef struct {
+ uint32_t magic;
+ int32_t tz_s;
+} UsrRtcTzVm_t;
+
+typedef struct {
+ uint8_t inited;
+ uint8_t tz_valid;
+ uint8_t time_valid;
+ int32_t tz_s;
+ uint32_t unix_utc;
+ uint32_t last_log_ms;
+ void *dev;
+} UsrRtc_t;
+
+/*============================================================================*/
+/* 私有变量 */
+/*============================================================================*/
+
+static UsrRtc_t s_rtc;
+
+/*============================================================================*/
+/* 私有函数声明 */
+/*============================================================================*/
+
+static uint8_t usr_rtc_is_leap(uint16_t year);
+static uint8_t usr_rtc_days_in_month(uint16_t year, uint8_t month);
+static uint8_t usr_rtc_year_ok(uint16_t year);
+static void usr_rtc_unix_to_calendar(uint32_t unix_utc, int32_t tz_s,
+ struct sys_time *t);
+static uint32_t usr_rtc_calendar_to_utc(const struct sys_time *t, int32_t tz_s);
+static void usr_rtc_load_tz(void);
+static void usr_rtc_save_tz(int32_t tz_s);
+static void usr_rtc_refresh_cache(void);
+
+/*============================================================================*/
+/* 公有函数 */
+/*============================================================================*/
+
+/**
+ * @brief 打开 rtc 设备并加载时区
+ * @return UsrRtc_Ret_t
+ */
+UsrRtc_Ret_t usr_rtc_init(void)
+{
+ struct sys_time t;
+
+ if (s_rtc.inited)
+ {
+ return USR_RTC_OK;
+ }
+
+#if TCFG_APP_RTC_EN
+ s_rtc.dev = dev_open("rtc", NULL);
+ if (!s_rtc.dev)
+ {
+ log_error("open rtc fail");
+ return USR_RTC_ERR_NOT_INIT;
+ }
+#else
+ log_error("TCFG_APP_RTC_EN=0");
+ return USR_RTC_ERR_NOT_INIT;
+#endif
+
+ usr_rtc_load_tz();
+ s_rtc.inited = 1;
+
+ memset(&t, 0, sizeof(t));
+ read_sys_time(&t);
+ log_info("rtc open %u-%02u-%02u %02u:%02u:%02u tz=%ld tz_ok=%u",
+ t.year, t.month, t.day, t.hour, t.min, t.sec,
+ (long)s_rtc.tz_s, s_rtc.tz_valid);
+
+ usr_rtc_refresh_cache();
+ return USR_RTC_OK;
+}
+
+/**
+ * @brief 周期读 RTC,刷新 UTC
+ */
+void usr_rtc_run(void)
+{
+ uint32_t now;
+
+ if (!s_rtc.inited)
+ {
+ return;
+ }
+
+ usr_rtc_refresh_cache();
+
+ now = timer_get_ms();
+ if ((now - s_rtc.last_log_ms) >= USR_RTC_LOG_PERIOD_MS)
+ {
+ struct sys_time t;
+ s_rtc.last_log_ms = now;
+ if (usr_rtc_read(&t) == USR_RTC_OK)
+ {
+ log_info("rtc %u-%02u-%02u %02u:%02u:%02u utc=%u valid=%u",
+ t.year, t.month, t.day, t.hour, t.min, t.sec,
+ s_rtc.unix_utc, s_rtc.time_valid);
+ }
+ }
+}
+
+/**
+ * @brief 获取对时状态
+ * @return UsrRtc_Ret_t
+ */
+UsrRtc_Ret_t usr_rtc_get_status(void)
+{
+ if (!s_rtc.inited)
+ {
+ return USR_RTC_ERR_NOT_INIT;
+ }
+ return s_rtc.time_valid ? USR_RTC_OK : USR_RTC_ERR_INVALID;
+}
+
+/**
+ * @brief 读硬件日历
+ * @param t 输出
+ * @return UsrRtc_Ret_t
+ */
+UsrRtc_Ret_t usr_rtc_read(struct sys_time *t)
+{
+ if (!t)
+ {
+ return USR_RTC_ERR_PARAM;
+ }
+ if (!s_rtc.inited)
+ {
+ return USR_RTC_ERR_NOT_INIT;
+ }
+ read_sys_time(t);
+ return USR_RTC_OK;
+}
+
+/**
+ * @brief 写硬件日历
+ * @param t 输入
+ * @return UsrRtc_Ret_t
+ */
+UsrRtc_Ret_t usr_rtc_write(const struct sys_time *t)
+{
+ struct sys_time tmp;
+
+ if (!t)
+ {
+ return USR_RTC_ERR_PARAM;
+ }
+ if (!s_rtc.inited)
+ {
+ return USR_RTC_ERR_NOT_INIT;
+ }
+ if (!usr_rtc_year_ok(t->year) || (t->month < 1) || (t->month > 12) ||
+ (t->day < 1) || (t->hour > 23) || (t->min > 59) || (t->sec > 59))
+ {
+ return USR_RTC_ERR_PARAM;
+ }
+ if (t->day > usr_rtc_days_in_month(t->year, t->month))
+ {
+ return USR_RTC_ERR_PARAM;
+ }
+
+ tmp = *t;
+ write_sys_time(&tmp);
+ usr_rtc_refresh_cache();
+ return USR_RTC_OK;
+}
+
+/**
+ * @brief UTC 秒 + 时区 → 本地日历写入 RTC
+ * @param unix_utc UTC 秒
+ * @param tz_s 时区偏移秒
+ * @return UsrRtc_Ret_t
+ */
+UsrRtc_Ret_t usr_rtc_set_from_unix(uint64_t unix_utc, int32_t tz_s)
+{
+ struct sys_time t;
+ UsrRtc_Ret_t ret;
+
+ if (!s_rtc.inited)
+ {
+ return USR_RTC_ERR_NOT_INIT;
+ }
+ if (unix_utc == 0)
+ {
+ log_debug("unix=0, ignore");
+ return USR_RTC_ERR_PARAM;
+ }
+ /* 网关采样到模组写入大约慢 1s,在 UTC 上补偿,跨分跨时由日历换算进位 */
+ unix_utc += USR_RTC_SYNC_COMPENSATE_S;
+ if ((tz_s > USR_RTC_TZ_ABS_MAX_S) || (tz_s < -USR_RTC_TZ_ABS_MAX_S))
+ {
+ log_error("tz out of range %ld", (long)tz_s);
+ return USR_RTC_ERR_PARAM;
+ }
+ if (unix_utc > 0xFFFFFFFFull)
+ {
+ log_error("unix too large");
+ return USR_RTC_ERR_PARAM;
+ }
+
+ usr_rtc_unix_to_calendar((uint32_t)unix_utc, tz_s, &t);
+ if (!usr_rtc_year_ok(t.year))
+ {
+ log_error("year %u not in %u~%u, wait sync",
+ t.year, USR_RTC_YEAR_MIN, USR_RTC_YEAR_MAX);
+ return USR_RTC_ERR_INVALID;
+ }
+
+ ret = usr_rtc_write(&t);
+ if (ret != USR_RTC_OK)
+ {
+ return ret;
+ }
+
+ usr_rtc_save_tz(tz_s);
+ usr_rtc_refresh_cache();
+ log_info("set rtc %u-%02u-%02u %02u:%02u:%02u utc=%u tz=%ld",
+ t.year, t.month, t.day, t.hour, t.min, t.sec,
+ s_rtc.unix_utc, (long)tz_s);
+ return USR_RTC_OK;
+}
+
+/**
+ * @brief 时间是否可用
+ * @return 1/0
+ */
+uint8_t usr_rtc_is_valid(void)
+{
+ return (s_rtc.inited && s_rtc.time_valid) ? 1 : 0;
+}
+
+/**
+ * @brief 当前 UTC 秒
+ * @return Unix 秒,无效为 0
+ */
+uint32_t usr_rtc_get_unix_utc(void)
+{
+ return s_rtc.time_valid ? s_rtc.unix_utc : 0;
+}
+
+/**
+ * @brief 当前时区偏移
+ * @return 秒
+ */
+int32_t usr_rtc_get_timezone(void)
+{
+ return s_rtc.tz_valid ? s_rtc.tz_s : 0;
+}
+
+/**
+ * @brief 恢复出厂:清 VM 时区和 RAM 对时状态
+ */
+void usr_rtc_clear_tz(void)
+{
+ UsrRtcTzVm_t vm;
+
+ s_rtc.tz_valid = 0;
+ s_rtc.tz_s = 0;
+ s_rtc.time_valid = 0;
+ s_rtc.unix_utc = 0;
+
+ memset(&vm, 0, sizeof(vm));
+ syscfg_write(CFG_USER_TIM_FLAG, (u8 *)&vm, sizeof(vm));
+ log_info("tz cleared");
+}
+
+/**
+ * @brief 计算星期
+ * @param t 日历
+ * @return 0=周日 … 6=周六
+ */
+uint8_t usr_rtc_get_weekday(const struct sys_time *t)
+{
+ static const uint8_t sakamoto[12] = {
+ 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
+ uint16_t y;
+
+ if (!t || (t->month < 1) || (t->month > 12) || (t->day < 1))
+ {
+ return 0xFF;
+ }
+
+ y = t->year;
+ if (t->month < 3)
+ {
+ y--;
+ }
+ return (uint8_t)((y + y / 4u - y / 100u + y / 400u +
+ sakamoto[t->month - 1] + t->day) % 7u);
+}
+
+/*============================================================================*/
+/* 私有函数 */
+/*============================================================================*/
+
+/**
+ * @brief 是否闰年
+ * @param year 公历年
+ * @return 1/0
+ */
+static uint8_t usr_rtc_is_leap(uint16_t year)
+{
+ if ((year % 400u) == 0)
+ {
+ return 1;
+ }
+ if ((year % 100u) == 0)
+ {
+ return 0;
+ }
+ return ((year % 4u) == 0) ? 1 : 0;
+}
+
+/**
+ * @brief 某月天数
+ * @param year 年
+ * @param month 月 1~12
+ * @return 天数,非法月返回 0
+ */
+static uint8_t usr_rtc_days_in_month(uint16_t year, uint8_t month)
+{
+ static const uint8_t days[12] = {
+ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+
+ if ((month < 1) || (month > 12))
+ {
+ return 0;
+ }
+ if ((month == 2) && usr_rtc_is_leap(year))
+ {
+ return 29;
+ }
+ return days[month - 1];
+}
+
+/**
+ * @brief 年份是否在合法窗口
+ * @param year 年
+ * @return 1/0
+ */
+static uint8_t usr_rtc_year_ok(uint16_t year)
+{
+ return ((year >= USR_RTC_YEAR_MIN) && (year <= USR_RTC_YEAR_MAX)) ? 1 : 0;
+}
+
+/**
+ * @brief UTC 秒 + 时区 → 本地日历
+ * @param unix_utc UTC 秒
+ * @param tz_s 时区偏移
+ * @param t 输出
+ */
+static void usr_rtc_unix_to_calendar(uint32_t unix_utc, int32_t tz_s,
+ struct sys_time *t)
+{
+ int64_t local;
+ uint32_t days;
+ uint32_t tod;
+ uint16_t y;
+ uint8_t m;
+ uint8_t dim;
+
+ memset(t, 0, sizeof(*t));
+ local = (int64_t)unix_utc + (int64_t)tz_s;
+ if (local < 0)
+ {
+ local = 0;
+ }
+
+ tod = (uint32_t)(local % 86400);
+ days = (uint32_t)(local / 86400);
+ t->sec = (uint8_t)(tod % 60u);
+ t->min = (uint8_t)((tod / 60u) % 60u);
+ t->hour = (uint8_t)(tod / 3600u);
+
+ y = 1970;
+ while (1)
+ {
+ uint32_t diy = usr_rtc_is_leap(y) ? 366u : 365u;
+ if (days < diy)
+ {
+ break;
+ }
+ days -= diy;
+ y++;
+ if (y > 4095u)
+ {
+ break;
+ }
+ }
+ t->year = y;
+
+ for (m = 1; m <= 12u; m++)
+ {
+ dim = usr_rtc_days_in_month(y, m);
+ if (days < dim)
+ {
+ break;
+ }
+ days -= dim;
+ }
+ t->month = m;
+ t->day = (uint8_t)(days + 1u);
+}
+
+/**
+ * @brief 本地日历 − 时区 → UTC 秒
+ * @param t 本地日历
+ * @param tz_s 时区偏移
+ * @return UTC Unix 秒
+ */
+static uint32_t usr_rtc_calendar_to_utc(const struct sys_time *t, int32_t tz_s)
+{
+ uint32_t days = 0;
+ uint16_t y;
+ uint8_t m;
+ int64_t local_s;
+ int64_t utc;
+
+ for (y = 1970; y < t->year; y++)
+ {
+ days += usr_rtc_is_leap(y) ? 366u : 365u;
+ }
+ for (m = 1; m < t->month; m++)
+ {
+ days += usr_rtc_days_in_month(t->year, m);
+ }
+ days += (uint32_t)(t->day - 1u);
+
+ local_s = (int64_t)days * 86400 +
+ (int64_t)t->hour * 3600 +
+ (int64_t)t->min * 60 +
+ (int64_t)t->sec;
+ utc = local_s - (int64_t)tz_s;
+ if (utc < 0)
+ {
+ utc = 0;
+ }
+ return (uint32_t)utc;
+}
+
+/**
+ * @brief 从 VM 加载时区(CFG_USER_TIM_FLAG)
+ */
+static void usr_rtc_load_tz(void)
+{
+ UsrRtcTzVm_t vm;
+ int ret;
+
+ s_rtc.tz_valid = 0;
+ s_rtc.tz_s = 0;
+ memset(&vm, 0, sizeof(vm));
+ ret = syscfg_read(CFG_USER_TIM_FLAG, (u8 *)&vm, sizeof(vm));
+ if ((ret == (int)sizeof(vm)) && (vm.magic == USR_RTC_TZ_MAGIC))
+ {
+ if ((vm.tz_s <= USR_RTC_TZ_ABS_MAX_S) && (vm.tz_s >= -USR_RTC_TZ_ABS_MAX_S))
+ {
+ s_rtc.tz_s = vm.tz_s;
+ s_rtc.tz_valid = 1;
+ log_info("tz from vm %ld", (long)s_rtc.tz_s);
+ return;
+ }
+ }
+ log_debug("tz vm empty");
+}
+
+/**
+ * @brief 时区写入 VM
+ * @param tz_s 时区偏移秒
+ */
+static void usr_rtc_save_tz(int32_t tz_s)
+{
+ UsrRtcTzVm_t vm;
+
+ s_rtc.tz_s = tz_s;
+ s_rtc.tz_valid = 1;
+ vm.magic = USR_RTC_TZ_MAGIC;
+ vm.tz_s = tz_s;
+ syscfg_write(CFG_USER_TIM_FLAG, (u8 *)&vm, sizeof(vm));
+}
+
+/**
+ * @brief 读 RTC,年份+时区都合法才刷新 UTC
+ */
+static void usr_rtc_refresh_cache(void)
+{
+ struct sys_time t;
+
+ s_rtc.time_valid = 0;
+ s_rtc.unix_utc = 0;
+ if (!s_rtc.inited || !s_rtc.tz_valid)
+ {
+ return;
+ }
+
+ memset(&t, 0, sizeof(t));
+ read_sys_time(&t);
+ if (!usr_rtc_year_ok(t.year))
+ {
+ return;
+ }
+
+ s_rtc.unix_utc = usr_rtc_calendar_to_utc(&t, s_rtc.tz_s);
+ s_rtc.time_valid = 1;
+}
diff --git a/JBao_JL7016_MOD_FW/apps/usr_periph/usr_rtc.h b/JBao_JL7016_MOD_FW/apps/usr_periph/usr_rtc.h
new file mode 100644
index 0000000..c60c241
--- /dev/null
+++ b/JBao_JL7016_MOD_FW/apps/usr_periph/usr_rtc.h
@@ -0,0 +1,117 @@
+/******************************************************************************
+ * @file usr_rtc.h
+ * @brief 板级 RTC 外设:打开 "rtc" 设备,读写系统时间,UTC↔本地日历
+ * @author cyWu <1917507415@qq.com>
+ * @date 2026.08.14
+ * @version V1.0.0
+ * @history
+ * - V1.0.0, 2026.08.14, cyWu, 首次发布(硬件 RTC,不对闹钟)
+ ******************************************************************************/
+
+#ifndef __USR_RTC_H__
+#define __USR_RTC_H__
+
+#include
+#include "system/sys_time.h"
+
+/*============================================================================*/
+/* 宏定义 */
+/*============================================================================*/
+
+#define USR_RTC_YEAR_MIN (2026u) /**< 合法年份下限 */
+#define USR_RTC_YEAR_MAX (2098u) /**< 合法年份上限 */
+#define USR_RTC_TZ_ABS_MAX_S (57600) /**< 时区偏移绝对值上限(±16h) */
+#define USR_RTC_SYNC_COMPENSATE_S (1u) /**< 网关对时延迟补偿,写入 RTC 前加的秒数 */
+
+/*============================================================================*/
+/* 错误码 */
+/*============================================================================*/
+
+typedef enum {
+ USR_RTC_OK = 0,
+ USR_RTC_ERR_PARAM,
+ USR_RTC_ERR_BUSY,
+ USR_RTC_ERR_TIMEOUT,
+ USR_RTC_ERR_NOT_INIT,
+ USR_RTC_ERR_INVALID, /**< 年份非法,等待 App 对时 */
+ USR_RTC_ERR_UNKNOWN
+} UsrRtc_Ret_t;
+
+/*============================================================================*/
+/* 外部接口 */
+/*============================================================================*/
+
+/**
+ * @brief 打开 "rtc" 设备,加载时区,读一次硬件时间
+ * @return UsrRtc_Ret_t
+ * @note 可重复调用;板级需已 REGISTER_DEVICES 注册 rtc
+ */
+UsrRtc_Ret_t usr_rtc_init(void);
+
+/**
+ * @brief 周期读 RTC,刷新 UTC 缓存(给业务用)
+ */
+void usr_rtc_run(void);
+
+/**
+ * @brief 获取对时状态
+ * @return USR_RTC_OK=时间合法,ERR_INVALID=等待对时,ERR_NOT_INIT=未打开
+ */
+UsrRtc_Ret_t usr_rtc_get_status(void);
+
+/**
+ * @brief 读硬件日历(本地时区)
+ * @param t 输出
+ * @return UsrRtc_Ret_t
+ */
+UsrRtc_Ret_t usr_rtc_read(struct sys_time *t);
+
+/**
+ * @brief 写硬件日历(本地时区)
+ * @param t 输入
+ * @return UsrRtc_Ret_t
+ */
+UsrRtc_Ret_t usr_rtc_write(const struct sys_time *t);
+
+/**
+ * @brief App/网关下发 UTC 秒 + 时区偏移,写入 RTC
+ * @param unix_utc UTC Unix 秒;0 视为无效
+ * @param tz_s 时区偏移秒(东八区 28800)
+ * @return UsrRtc_Ret_t
+ * @note 网关从子设备连上开始计时,每 12 小时经 DEVICEINFO 同步一次;
+ * timestamp + tz → 本地年月日时分秒 → write_sys_time;tz 写入 VM
+ */
+UsrRtc_Ret_t usr_rtc_set_from_unix(uint64_t unix_utc, int32_t tz_s);
+
+/**
+ * @brief 年份合法且已有时区
+ * @return 1/0
+ */
+uint8_t usr_rtc_is_valid(void);
+
+/**
+ * @brief 当前 UTC Unix 秒(日历换算后再减时区)
+ * @return 无效时返回 0
+ */
+uint32_t usr_rtc_get_unix_utc(void);
+
+/**
+ * @brief 当前时区偏移秒
+ * @return 未保存过时返回 0
+ */
+int32_t usr_rtc_get_timezone(void);
+
+/**
+ * @brief 恢复出厂:清 VM 时区和 RAM 对时状态
+ * @note 硬件日历不改;下次需网关重新对时。0x1D 在此之后回全 0
+ */
+void usr_rtc_clear_tz(void);
+
+/**
+ * @brief 由日历计算星期
+ * @param t 本地日历
+ * @return 0=周日 … 6=周六;参数非法返回 0xFF
+ */
+uint8_t usr_rtc_get_weekday(const struct sys_time *t);
+
+#endif /* __USR_RTC_H__ */