diff --git a/.gitignore b/.gitignore index 8b2f94e..9955a91 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,12 @@ $RECYCLE.BIN/ *.orig *.rej +# ----------------------------------------------------------------------------- +# Release 交付产物(由 tools/package_release.py 生成,勿提交) +# ----------------------------------------------------------------------------- +release/ +Release_Project_*/ + # ----------------------------------------------------------------------------- # 注意:以下内容请保留上传,不要忽略 # - cpu/br28/liba/*.a SDK 预编译库 diff --git a/AC701N.cbp b/AC701N.cbp index cacb40e..32008f5 100644 --- a/AC701N.cbp +++ b/AC701N.cbp @@ -257,6 +257,7 @@ + @@ -777,29 +778,13 @@ - - - - - - - - - - - - + + diff --git a/Makefile b/Makefile index a0af6b6..88b7db4 100644 --- a/Makefile +++ b/Makefile @@ -245,6 +245,11 @@ INCLUDES := \ -Iinclude_lib/media/aispeech/enc/include \ -Icpu/br28/audio_hearing \ -Iinclude_lib/media/cvp \ + -Iapps/usr_le_code \ + -Iapps/usr_le_code/thirdpart \ + -Iapps/usr_le_code/thirdpart/mjson \ + -Iapps/usr_jb_proto/Protocol \ + -Iapps/usr_jb_proto/Utils \ -I$(SYS_INC_DIR) \ @@ -479,6 +484,7 @@ c_SRC_FILES := \ apps/earphone/bt_emitter.c \ apps/earphone/bt_tws.c \ apps/earphone/default_event_handler.c \ + apps/earphone/dual_update_demo.c \ apps/earphone/earphone.c \ apps/earphone/eartch_event_deal.c \ apps/earphone/font/fontinit.c \ @@ -608,6 +614,24 @@ c_SRC_FILES := \ cpu/br28/tws_audio.c \ cpu/br28/uart_dev.c \ cpu/br28/umidigi_chargestore.c \ + apps/usr_jb_proto/Protocol/jb_product.c \ + apps/usr_jb_proto/Protocol/jb_protocol.c \ + apps/usr_jb_proto/Utils/jb_common.c \ + apps/usr_jb_proto/Utils/jb_ringbuffer.c \ + apps/usr_jb_proto/usr_jb_main.c \ + + +# usr_le_code 静态库(协议编解码 + 广播/收发) +USR_LE_LIB := apps/usr_le_code/lib/libusr_le_code.a +USR_LE_SRC_FILES := \ + apps/usr_le_code/ble_proto.c \ + apps/usr_le_code/bleproto.c \ + apps/usr_le_code/bleproto_packer.c \ + apps/usr_le_code/thirdpart/mjson/mjson.c \ + apps/usr_le_code/usr_le_adv.c \ + apps/usr_le_code/usr_le_recieve.c + +USR_LE_OBJS := $(addprefix $(BUILD_DIR)/, $(USR_LE_SRC_FILES:%.c=%.c.o)) # 需要编译的 .S 文件 @@ -711,6 +735,7 @@ LFLAGS := \ cpu/br28/liba/libllns.a \ cpu/br28/liba/libkwscommon.a \ apps/common/third_party_profile/tuya_protocol/sdk/lib/libtuya_lib.a \ + apps/usr_le_code/lib/libusr_le_code.a \ cpu/br28/liba/lib_icsd_adt.a \ cpu/br28/liba/lib_diafx.a \ cpu/br28/liba/libFFT_pi32v2_OnChip.a \ @@ -769,16 +794,25 @@ LINK_AT ?= 1 # 表示下面的不是一个文件的名字,无论是否存在 all, clean, pre_build 这样的文件 # 还是要执行命令 # see: https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html -.PHONY: all clean pre_build +.PHONY: all clean pre_build lib_usr_le # 不要使用 make 预设置的规则 # see: https://www.gnu.org/software/make/manual/html_node/Suffix-Rules.html .SUFFIXES: -all: pre_build $(OUT_ELF) +all: pre_build $(USR_LE_LIB) $(OUT_ELF) $(info +POST-BUILD) $(QUITE) $(RUN_POST_SCRIPT) sdk +# 单独编译 usr_le_code 静态库: make lib_usr_le +lib_usr_le: $(USR_LE_LIB) + +$(USR_LE_LIB): $(USR_LE_OBJS) + $(info +AR $@) + $(shell $(MKDIR) $(@D)) + $(QUITE) $(RM) $@ + $(QUITE) $(AR) rcs $@ $(USR_LE_OBJS) + pre_build: $(info +PRE-BUILD) $(QUITE) $(CC) $(CFLAGS) $(DEFINES) $(INCLUDES) -D__LD__ -E -P cpu/br28/sdk_used_list.c -o cpu/br28/sdk_used_list.used @@ -794,13 +828,13 @@ clean: ifeq ($(LINK_AT), 1) -$(OUT_ELF): $(OBJS) +$(OUT_ELF): $(OBJS) $(USR_LE_LIB) $(info +LINK $@) $(shell $(MKDIR) $(@D)) $(file >$(OBJ_FILE), $(OBJS)) $(QUITE) $(LD) -o $(OUT_ELF) @$(OBJ_FILE) $(LFLAGS) $(LIBPATHS) $(LIBS) else -$(OUT_ELF): $(OBJS) +$(OUT_ELF): $(OBJS) $(USR_LE_LIB) $(info +LINK $@) $(shell $(MKDIR) $(@D)) $(QUITE) $(LD) -o $(OUT_ELF) $(OBJS) $(LFLAGS) $(LIBPATHS) $(LIBS) diff --git a/apps/common/third_party_profile/jieli/JL_rcsp/bt_trans_data/le_rcsp_adv_module.c b/apps/common/third_party_profile/jieli/JL_rcsp/bt_trans_data/le_rcsp_adv_module.c index 2625d32..5354771 100644 --- a/apps/common/third_party_profile/jieli/JL_rcsp/bt_trans_data/le_rcsp_adv_module.c +++ b/apps/common/third_party_profile/jieli/JL_rcsp/bt_trans_data/le_rcsp_adv_module.c @@ -57,9 +57,6 @@ #include "chgbox_box.h" #endif -extern void usr_ble_adv_updata(); - - extern void usr_le_data_recieve(u8* data,u16 len); extern void usr_ble_adv_init(); #if 1 @@ -934,7 +931,6 @@ void ble_trans_module_enable(u8 flag) ble_module_enable(flag); } -extern void usr_ble_adv_init(); void bt_ble_init(void) { log_info("***** ble_init******\n"); diff --git a/apps/earphone/include/app_main.h b/apps/earphone/include/app_main.h index 5ede37e..c7e3c30 100644 --- a/apps/earphone/include/app_main.h +++ b/apps/earphone/include/app_main.h @@ -1,7 +1,5 @@ #ifndef APP_MAIN_H #define APP_MAIN_H -#include "bleproto.h" -#include "bleproto_api.h" typedef struct _USR_VAR { u8 usr_start_write_flash ; @@ -131,7 +129,6 @@ typedef struct _BT_USER_COMM_VAR { extern APP_VAR app_var; extern USR_VAR usr_var; extern BT_USER_PRIV_VAR bt_user_priv_var; -extern bleproto_authsetup_req_t usr_auth_data; diff --git a/apps/earphone/key_event_deal.c b/apps/earphone/key_event_deal.c index c6b867a..7b5015c 100644 --- a/apps/earphone/key_event_deal.c +++ b/apps/earphone/key_event_deal.c @@ -34,8 +34,6 @@ /* #define LOG_DUMP_ENABLE */ #define LOG_CLI_ENABLE #include "debug.h" -#include "bleproto.h" -#include "bleproto_api.h" #define POWER_OFF_CNT 10 @@ -46,7 +44,6 @@ extern bool get_tws_sibling_connect_state(void); extern int bt_get_low_latency_mode(); extern void bt_set_low_latency_mode(int enable); -extern void usr_ble_adv_init(); void audio_aec_pitch_change_ctrl(); void audio_surround_voice_ctrl(); extern void start_streamer_test(void); diff --git a/apps/usr_jb_proto/Protocol/jb_protocol.h b/apps/usr_jb_proto/Protocol/jb_protocol.h index 5f3cbe8..e009008 100644 --- a/apps/usr_jb_proto/Protocol/jb_protocol.h +++ b/apps/usr_jb_proto/Protocol/jb_protocol.h @@ -34,10 +34,13 @@ #define REPORT_DEBOUNCE 2000 /* 变化上报防抖时间(ms) */ #define REPORT_PERIOD 600000 /* 定时全量上报周期(ms) */ -/* Device identity: change these values for the target product. */ +/* Device identity: change these values for the target product. + * JB_CATEGORY_CODE: 必须 4 字符 ASCII,供 BLE 广播 prodcode 使用 + * JB_PRODUCT_CODE : 必须 6 字符 ASCII,供 BLE 广播 pcode 使用 + */ #define JB_PROTOCOL_VERSION 0x0100 /* V1.0 */ -#define JB_CATEGORY_CODE "CBXX" -#define JB_PRODUCT_CODE "CBXX" +#define JB_CATEGORY_CODE "CBC0" +#define JB_PRODUCT_CODE "PICBC0" #define JB_DEVICE_SN 0x00 #define JB_DEVICE_CAPABILITY 0ULL diff --git a/apps/usr_jb_proto/usr_jb_main.c b/apps/usr_jb_proto/usr_jb_main.c index 803435b..ebf80b1 100644 --- a/apps/usr_jb_proto/usr_jb_main.c +++ b/apps/usr_jb_proto/usr_jb_main.c @@ -10,6 +10,18 @@ #include "jb_product.h" #include "jb_ringbuffer.h" #include "jb_common.h" +#include "usr_le_product.h" + +/* 向 usr_le_code 库提供产品身份(换产品只改 jb_protocol.h 宏即可) */ +const uint8_t g_usr_le_prodcode[USR_LE_PRODCODE_LEN] = { + JB_CATEGORY_CODE[0], JB_CATEGORY_CODE[1], + JB_CATEGORY_CODE[2], JB_CATEGORY_CODE[3] +}; +const uint8_t g_usr_le_pcode[USR_LE_PCODE_LEN] = { + JB_PRODUCT_CODE[0], JB_PRODUCT_CODE[1], JB_PRODUCT_CODE[2], + JB_PRODUCT_CODE[3], JB_PRODUCT_CODE[4], JB_PRODUCT_CODE[5] +}; + void usr_timer_1_ms(); static int jb_timer_id=0; diff --git a/apps/usr_le_code/build_lib.bat b/apps/usr_le_code/build_lib.bat new file mode 100644 index 0000000..7fc5849 --- /dev/null +++ b/apps/usr_le_code/build_lib.bat @@ -0,0 +1,50 @@ +@echo off +chcp 65001 >nul +REM Rebuild apps/usr_le_code/lib/libusr_le_code.a +REM Double-click this bat; window will pause so you can read the result. + +cd /d "%~dp0..\.." +if errorlevel 1 ( + echo [FAIL] cannot cd to project root + echo SCRIPT dir: %~dp0 + goto :END +) + +echo ==== build libusr_le_code.a ==== +echo Project root: %CD% +echo. + +SET PATH=%CD%\tools\utils;C:\JL\pi32\bin;%PATH% + +where make >nul 2>&1 +if errorlevel 1 ( + echo [FAIL] make.exe not found + echo Check: %CD%\tools\utils\make.exe + goto :END +) + +where clang.exe >nul 2>&1 +if errorlevel 1 ( + echo [FAIL] clang.exe not found + echo Check: C:\JL\pi32\bin\clang.exe + goto :END +) + +make lib_usr_le -j %NUMBER_OF_PROCESSORS% +if errorlevel 1 ( + echo. + echo [FAIL] build libusr_le_code.a + goto :END +) + +echo. +if exist "apps\usr_le_code\lib\libusr_le_code.a" ( + echo [OK] apps\usr_le_code\lib\libusr_le_code.a + dir "apps\usr_le_code\lib\libusr_le_code.a" +) else ( + echo [FAIL] lib file not found after build +) + +:END +echo. +pause diff --git a/apps/usr_le_code/lib/libusr_le_code.a b/apps/usr_le_code/lib/libusr_le_code.a new file mode 100644 index 0000000..5509907 Binary files /dev/null and b/apps/usr_le_code/lib/libusr_le_code.a differ diff --git a/apps/usr_le_code/usr_le_adv.c b/apps/usr_le_code/usr_le_adv.c index 0fadd9e..befd16e 100644 --- a/apps/usr_le_code/usr_le_adv.c +++ b/apps/usr_le_code/usr_le_adv.c @@ -14,8 +14,7 @@ #include "vm.h" #include "bleproto.h" #include "bleproto_api.h" - -#include "bleproto.h" +#include "usr_le_product.h" #define LOG_TAG_CONST APP #define LOG_TAG "[APP]" #define LOG_ERROR_ENABLE @@ -28,6 +27,9 @@ extern u8 muti_adv_data[31];//={0}; extern u8 muti_adv_data_len; +/* 定义在 usr_le_recieve.c,库内共用,不对外导出 */ +extern bleproto_authsetup_req_t usr_auth_data; + extern u8 usr_get_full(); extern u16 usr_get_conn_service(); extern int muti_make_set_adv_data(u8* data,u8 data_len); @@ -95,9 +97,14 @@ void usr_ble_adv_init() { int8_t txpower = 0; uint8_t manucode[BLEPROTO_ADV_TLV_LENGTH_MANUCODE] = { 0 }; - uint8_t prodcode[BLEPROTO_ADV_TLV_LENGTH_PRODCODE] = {'C', 'B', 'C', '0'};///CBP0 - uint8_t gmac[6]={0}; - uint8_t pcode[6]={'P','I','C','B','C','0'}; + uint8_t prodcode[BLEPROTO_ADV_TLV_LENGTH_PRODCODE]; + uint8_t gmac[6] = {0}; + uint8_t pcode[BLE_PROTO_ADV_LENGTH_PCODE]; + + /* 产品身份由应用层 g_usr_le_* 提供(见 usr_jb_main.c) */ + memcpy(prodcode, g_usr_le_prodcode, BLEPROTO_ADV_TLV_LENGTH_PRODCODE); + memcpy(pcode, g_usr_le_pcode, BLE_PROTO_ADV_LENGTH_PCODE); + usr_set_adv_interval(80); if(usr_var.usr_ota_succ_flag)return; //muti_adv_data @@ -214,29 +221,51 @@ void usr_soft_off() void usr_ble_adv_updata_OTA() { //printf("usr_ble_adv_updata\n"); - if(1) + ble_trans_module_enable(0); + usr_var.usr_adv_flag=0; + + extern u8 usr_mac_addr[6]; + extern int le_controller_set_mac(void *addr); + usr_mac_addr[0]=usr_mac_addr[0]+1; + le_controller_set_mac(usr_mac_addr); //修改BLE地址 + muti_make_set_adv_data_updata(); + printf("###muti_adv_data = "); + printf_buf(muti_adv_data,muti_adv_data_len); + ble_trans_module_enable(1); +} + +void usr_ota_process() +{ + static u8 cnt = 0; + if (cnt == 0) { - ble_trans_module_enable(0); - usr_var.usr_adv_flag=0; + ble_multi_trans_disconnect(); + } - #if 1 - extern u8 usr_mac_addr[6]; - //u8 ble_mac[6]; - extern int le_controller_set_mac(void *addr); - //extern void lib_make_ble_address(u8 * ble_address, u8 * edr_address); - //bt_update_mac_addr(mac); - //lmp_hci_write_local_address(mac); - //bt_update_testbox_addr(mac); - //lib_make_ble_address(ble_mac, mac); - usr_mac_addr[0]=usr_mac_addr[0]+1; - le_controller_set_mac(usr_mac_addr); //修改BLE地址 + if (usr_get_conn_service() == 0) + { + cnt++; + if (cnt == 2) + { + usr_ble_adv_updata_OTA(); + } + if (cnt >= 60) + { + cnt = 0; + cpu_reset(); + } + } + else + { + cnt = 1; + } +} - #endif - - - muti_make_set_adv_data_updata(); - printf("###muti_adv_data = "); - printf_buf(muti_adv_data,muti_adv_data_len); - ble_trans_module_enable(1); +void usr_app_ota_init() +{ + usr_var.usr_goto_updata = 1; + if (usr_var.ota_timer == 0) + { + usr_var.ota_timer = sys_timer_add(NULL, usr_ota_process, 1000); } } diff --git a/apps/usr_le_code/usr_le_api.h b/apps/usr_le_code/usr_le_api.h new file mode 100644 index 0000000..5e5b20f --- /dev/null +++ b/apps/usr_le_code/usr_le_api.h @@ -0,0 +1,66 @@ +/****************************************************************************** + * @file usr_le_api.h + * @brief usr_le_code 静态库对外 API(广播 / 收发 / 配网) + * @author cyWu <1917507415@qq.com> + * @date 2026.08.03 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.03, cyWu, 首次发布 + ******************************************************************************/ + +#ifndef __USR_LE_API_H__ +#define __USR_LE_API_H__ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief 初始化 BLE 广播数据(绑定广播或重连广播) + * @note 根据配对状态与 usr_goto_pair 选择 bind/reconn 广播,并启动 500ms 周期更新 + */ +void usr_ble_adv_init(void); + +/** + * @brief BLE 协议栈接收数据入口 + * @param data 接收缓冲区 + * @param len 本次接收长度 + * @note 组包后解析 bleproto,并分发 DEVICEINFO/AUTH/RUNCMD/OTA 等服务 + */ +void usr_le_data_recieve(uint8_t *data, uint16_t len); + +/** + * @brief 进入配对/配网模式 + * @note 断开当前连接、清除配对信息(不复位),重新发绑定广播 + */ +void usr_goto_pair_mode(void); + +/** + * @brief 清除配对信息但不复位 + */ +void usr_clear_pair_info_noreset(void); + +/** + * @brief 清除配对信息并延时复位 + */ +void usr_clear_pair_info(void); + +/** + * @brief 获取当前是否已配对 + * @return 1=已配对,0=未配对 + */ +uint8_t usr_get_pair_flag(void); + +/** + * @brief 启动应用侧 OTA 流程 + * @note 置位 usr_goto_updata,并创建 1s 周期定时器驱动断连与 OTA 广播切换 + */ +void usr_app_ota_init(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __USR_LE_API_H__ */ diff --git a/apps/usr_le_code/usr_le_product.h b/apps/usr_le_code/usr_le_product.h new file mode 100644 index 0000000..90bcda4 --- /dev/null +++ b/apps/usr_le_code/usr_le_product.h @@ -0,0 +1,46 @@ +/****************************************************************************** + * @file usr_le_product.h + * @brief usr_le_code 产品身份配置接口(由应用层提供符号,库内不写死) + * @author cyWu <1917507415@qq.com> + * @date 2026.08.03 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.03, cyWu, 首次发布 + ******************************************************************************/ + +#ifndef __USR_LE_PRODUCT_H__ +#define __USR_LE_PRODUCT_H__ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** 与 bleproto_packer.h 中 BLEPROTO_ADV_TLV_LENGTH_PRODCODE 一致 */ +#ifndef USR_LE_PRODCODE_LEN +#define USR_LE_PRODCODE_LEN 4 +#endif + +/** 与 bleproto_packer.h 中 BLE_PROTO_ADV_LENGTH_PCODE 一致 */ +#ifndef USR_LE_PCODE_LEN +#define USR_LE_PCODE_LEN 6 +#endif + +/** + * @brief 产品类别码,对应 BLE 广播 prodcode(固定 4 字节 ASCII) + * @note 应用层用 JB_CATEGORY_CODE 填充,见 usr_jb_main.c + */ +extern const uint8_t g_usr_le_prodcode[USR_LE_PRODCODE_LEN]; + +/** + * @brief 平台产品码,对应 BLE 广播 pcode(固定 6 字节 ASCII) + * @note 应用层用 JB_PRODUCT_CODE 填充,见 usr_jb_main.c + */ +extern const uint8_t g_usr_le_pcode[USR_LE_PCODE_LEN]; + +#ifdef __cplusplus +} +#endif + +#endif /* __USR_LE_PRODUCT_H__ */ diff --git a/apps/usr_le_code/usr_le_recieve.c b/apps/usr_le_code/usr_le_recieve.c index e489924..5a28e16 100644 --- a/apps/usr_le_code/usr_le_recieve.c +++ b/apps/usr_le_code/usr_le_recieve.c @@ -16,7 +16,6 @@ #include "bleproto.h" #include "bleproto_api.h" #include "ble_proto.h" -#include "jb_protocol.h" #define LOG_TAG_CONST APP #define LOG_TAG "[APP]" #define LOG_ERROR_ENABLE @@ -37,7 +36,6 @@ static u8 ota_source_data[FIRM_PACK_LEN * 2] = {0}; extern u16 usr_get_conn_service(); extern void ble_multi_trans_disconnect(void); -extern void usr_ble_adv_updata_OTA(); extern void usr_ble_send_data(u8 *data, u16 len); /// BLE发送接口,MTU 244 extern void usr_write_pair_info(); extern void usr_clear_pair_info(); @@ -45,6 +43,8 @@ extern void app_audio_volume_set(u8 value); extern int dual_ota_app_data_deal(u32 msg, u8 *buf, u32 len); extern int uart_tr_send_data(u8 *data, u32 len); extern void usr_intterupt_close(); +extern void usr_jb_le_recieve_data(u8 *data, u16 len); +extern int8_t jbReportAckCheck(u16 cmd, u8 gateway_int_cnt, int32_t gateway_int0); void usr_ota_deal_task(); void usr_data_ana(bleproto_appdata_desc_t *desc); @@ -429,10 +429,9 @@ void usr_ota_deal_task() /* 最近一次收到 RUNCMD_V2 请求的 msgid,应答时回填 */ static u8 s_last_runcmd_msgid = 0; -extern void usr_jb_le_recieve_data(u8 *data, u16 len); void usr_run_cmd_code_v2(ble_proto_packet_t *runcmd_par, u8 usr_msg_id) { - u8 jb_frame[MAX_PACKAGE_LEN]; + u8 jb_frame[128]; /* 与 jb_protocol.h MAX_PACKAGE_LEN 一致 */ u16 copy_len; if (!runcmd_par || !runcmd_par->bytes_data || runcmd_par->device_bytes_len == 0) diff --git a/tools/package_release.py b/tools/package_release.py new file mode 100644 index 0000000..deb0ce3 --- /dev/null +++ b/tools/package_release.py @@ -0,0 +1,634 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +############################################################################### +# @file package_release.py +# @brief 根据当前开发工程自动生成客户交付 Release 工程 +# @author cyWu <1917507415@qq.com> +# @date 2026.08.04 +# @version V1.0.0 +# @history +# - V1.0.0, 2026.08.04, cyWu, 首次发布 +# - V1.0.1, 2026.08.04, cyWu, 产物统一输出到 release/ 目录 +# +# 使用方式(在项目根或任意目录): +# python tools/package_release.py +# +# 约束: +# - 仅使用 Python 标准库 +# - 所有删除 / 过滤 / 复制只作用于 Release 副本 +# - 绝不修改开发工程业务源码、Makefile、build_lib.bat、CodeBlocks 工程 +############################################################################### + +from __future__ import annotations + +import fnmatch +import os +import re +import shutil +import sys +import zipfile +from datetime import datetime +from pathlib import Path +from typing import Dict, Iterable, List, Set + + +# ============================================================================= +# 可配置区域(集中管理,扩展模块只改这里) +# ============================================================================= + +# 版本文件(相对项目根) +VERSION_FILE = "version.txt" + +# Release 输出根目录(相对项目根,产物全部放这里) +RELEASE_OUTPUT_DIR = "release" + +# Release 命名规则 +RELEASE_DIR_PREFIX = "Release_Project_V" +RELEASE_ZIP_SUFFIX = ".zip" +README_NAME = "Release_Project_README.md" + +# 工程展示名称(写入 README) +PROJECT_DISPLAY_NAME = "AC701N Earphone SDK" +CHIP_DISPLAY_NAME = "AC7016C(BR28)" + +# 复制时跳过的目录名(任意层级) +SKIP_DIR_NAMES: Set[str] = { + ".git", + ".vscode", + ".idea", + "objs", + "obj", + "__pycache__", + RELEASE_OUTPUT_DIR, # 不把已有交付产物拷进新包 +} + +# 复制时跳过的根目录文件(仅项目根下,不进客户包) +SKIP_ROOT_FILES: Set[str] = { + ".gitignore", + "version.txt", +} + +# 复制时跳过的文件通配(任意层级,按文件名匹配) +SKIP_FILE_PATTERNS: List[str] = [ + "*.o", + "*.d", + "*.dep", + "*.obj", + "*.bak", + "*.log", + "*.tmp", + "*.temp", + "*.swp", + "*.swo", + "*.orig", + "*.rej", + "*.pyc", + "*.layout", + "*.depend", + "Thumbs.db", + ".DS_Store", +] + +# 复制时跳过的路径前缀(相对项目根,防自复制嵌套) +SKIP_PATH_PREFIXES: List[str] = [ + RELEASE_OUTPUT_DIR, + "Release_Project_V", # 兼容旧版曾放在项目根的产物 +] + +# --------------------------------------------------------------------------- +# 需要封装交付的模块配置 +# --------------------------------------------------------------------------- +# public_headers : 保留的公开头文件(仅文件名,相对模块根或子目录中同名匹配) +# remove_patterns : 删除的源码通配(递归) +# remove_internal_headers : True 时删除非 public_headers 的所有 .h/.hpp +# prune_empty_dirs : 清理后删除空目录 +# remove_extra : 额外删除的相对路径(相对模块根) +# keep_extra : 额外强制保留的相对路径(文件或目录,相对模块根) +# --------------------------------------------------------------------------- +MODULE_CONFIG: Dict[str, dict] = { + "apps/usr_le_code": { + "public_headers": [ + "usr_le_api.h", + "usr_le_product.h", + ], + "remove_patterns": [ + "*.c", + "*.cpp", + "*.cc", + ], + "remove_internal_headers": True, + "prune_empty_dirs": True, + "remove_extra": [ + "build_lib.bat", + ], + "keep_extra": [ + "lib/", + ], + }, +} + + +# ============================================================================= +# 路径管理 +# ============================================================================= + +def get_project_root() -> Path: + """ + @brief 由脚本位置推导项目根目录(tools/ 的上一级) + @return 项目根 Path + """ + return Path(__file__).resolve().parent.parent + + +def read_version(project_root: Path) -> str: + """ + @brief 读取 version.txt + @param project_root 项目根 + @return 版本号字符串,例如 "1.0.0" + """ + version_path = project_root / VERSION_FILE + if not version_path.is_file(): + raise FileNotFoundError(f"version file not found: {version_path}") + + version = version_path.read_text(encoding="utf-8").strip() + if not version: + raise ValueError(f"version file is empty: {version_path}") + + # 简单校验:不允许路径分隔符,避免生成非法目录名 + if any(ch in version for ch in ("/", "\\", "..")): + raise ValueError(f"invalid version string: {version!r}") + + return version + + +def get_release_paths(project_root: Path, version: str) -> dict: + """ + @brief 统一生成 Release 相关路径(均位于 release/ 下) + @param project_root 项目根 + @param version 版本号 + @return 含 output_dir / dir / zip / readme 的字典 + """ + dir_name = f"{RELEASE_DIR_PREFIX}{version}" + output_dir = project_root / RELEASE_OUTPUT_DIR + release_dir = output_dir / dir_name + release_zip = output_dir / f"{dir_name}{RELEASE_ZIP_SUFFIX}" + readme_path = release_dir / README_NAME + return { + "dir_name": dir_name, + "output_dir": output_dir, + "release_dir": release_dir, + "release_zip": release_zip, + "readme_path": readme_path, + } + + +# ============================================================================= +# 复制过滤 +# ============================================================================= + +def _match_any(name: str, patterns: Iterable[str]) -> bool: + """ + @brief 判断文件名是否匹配任一通配符 + """ + return any(fnmatch.fnmatch(name, pat) for pat in patterns) + + +def should_skip_path(rel_path: Path) -> bool: + """ + @brief 判断相对项目根的路径在复制时是否应跳过 + @param rel_path 相对路径 + @return True=跳过 + """ + parts = rel_path.parts + if not parts: + return False + + # 跳过项目根下指定文件(如 .gitignore / version.txt) + if len(parts) == 1 and parts[0] in SKIP_ROOT_FILES: + return True + + # 跳过指定目录名(任意层级) + for part in parts: + if part in SKIP_DIR_NAMES: + return True + + # 跳过 Release 产物路径(防嵌套) + rel_posix = rel_path.as_posix() + for prefix in SKIP_PATH_PREFIXES: + if rel_posix == prefix.rstrip("/") or rel_posix.startswith(prefix): + return True + + # 跳过匹配的文件名 + if _match_any(rel_path.name, SKIP_FILE_PATTERNS): + return True + + # 跳过已有 zip 包(根目录下) + if len(parts) == 1 and rel_path.name.endswith(RELEASE_ZIP_SUFFIX): + if rel_path.name.startswith(RELEASE_DIR_PREFIX): + return True + + return False + + +def copy_project(project_root: Path, release_dir: Path) -> int: + """ + @brief 过滤复制整工程到 Release 目录 + @param project_root 开发工程根 + @param release_dir 目标 Release 目录 + @return 复制的文件数量 + """ + copied = 0 + + for root, dirs, files in os.walk(project_root): + root_path = Path(root) + rel_root = root_path.relative_to(project_root) + + # 就地过滤子目录,避免继续向下遍历 + keep_dirs: List[str] = [] + for d in dirs: + candidate = rel_root / d if str(rel_root) != "." else Path(d) + if should_skip_path(candidate): + continue + keep_dirs.append(d) + dirs[:] = keep_dirs + + # 目标目录 + dst_root = release_dir if str(rel_root) == "." else release_dir / rel_root + dst_root.mkdir(parents=True, exist_ok=True) + + for name in files: + rel_file = rel_root / name if str(rel_root) != "." else Path(name) + if should_skip_path(rel_file): + continue + + src = root_path / name + dst = dst_root / name + shutil.copy2(src, dst) + copied += 1 + + return copied + + +# ============================================================================= +# 模块清理(仅作用于 Release 副本) +# ============================================================================= + +def _is_under_keep_extra(rel_posix: str, keep_extra: List[str]) -> bool: + """ + @brief 判断模块内相对路径是否属于 keep_extra 保护范围 + """ + for keep in keep_extra: + keep_norm = keep.replace("\\", "/").rstrip("/") + if not keep_norm: + continue + # 目录保护:keep 以 / 结尾或配置为目录前缀 + if rel_posix == keep_norm or rel_posix.startswith(keep_norm + "/"): + return True + # 兼容配置写成 "lib/" 的情况已在上方处理 + if keep.endswith("/") and (rel_posix == keep_norm or rel_posix.startswith(keep_norm + "/")): + return True + return False + + +def _prune_empty_dirs(module_dir: Path) -> int: + """ + @brief 自底向上删除空目录 + @return 删除的空目录数量 + """ + removed = 0 + # 深度优先:按路径长度降序 + all_dirs = sorted( + (p for p in module_dir.rglob("*") if p.is_dir()), + key=lambda p: len(p.parts), + reverse=True, + ) + for d in all_dirs: + try: + if not any(d.iterdir()): + d.rmdir() + removed += 1 + except OSError: + pass + return removed + + +def clean_module(release_dir: Path, module_rel: str, cfg: dict) -> dict: + """ + @brief 按配置清理单个封装模块(仅 Release 内) + @param release_dir Release 根目录 + @param module_rel 模块相对路径,如 apps/usr_le_code + @param cfg MODULE_CONFIG 中的单项配置 + @return 统计信息字典 + """ + module_dir = release_dir / module_rel + stats = { + "module": module_rel, + "removed_sources": 0, + "removed_headers": 0, + "removed_extra": 0, + "pruned_dirs": 0, + "missing": False, + } + + if not module_dir.is_dir(): + stats["missing"] = True + print(f"[WARN] module not found in Release: {module_rel}") + return stats + + public_headers: Set[str] = set(cfg.get("public_headers", [])) + remove_patterns: List[str] = list(cfg.get("remove_patterns", [])) + remove_internal_headers: bool = bool(cfg.get("remove_internal_headers", True)) + prune_empty_dirs: bool = bool(cfg.get("prune_empty_dirs", True)) + remove_extra: List[str] = list(cfg.get("remove_extra", [])) + keep_extra: List[str] = list(cfg.get("keep_extra", [])) + + # 1) 删除匹配 remove_patterns 的源文件 + for path in list(module_dir.rglob("*")): + if not path.is_file(): + continue + rel = path.relative_to(module_dir).as_posix() + if _is_under_keep_extra(rel, keep_extra): + continue + if _match_any(path.name, remove_patterns): + path.unlink() + stats["removed_sources"] += 1 + + # 2) 删除非公开头文件 + if remove_internal_headers: + header_patterns = ["*.h", "*.hpp"] + for path in list(module_dir.rglob("*")): + if not path.is_file(): + continue + if not _match_any(path.name, header_patterns): + continue + rel = path.relative_to(module_dir).as_posix() + if _is_under_keep_extra(rel, keep_extra): + continue + # 公开头文件按“文件名”匹配,便于配置只写文件名 + if path.name in public_headers: + continue + path.unlink() + stats["removed_headers"] += 1 + + # 3) 删除额外指定文件/目录 + for extra in remove_extra: + target = module_dir / extra + if not target.exists(): + continue + if target.is_dir(): + shutil.rmtree(target) + else: + target.unlink() + stats["removed_extra"] += 1 + + # 4) 清理空目录 + if prune_empty_dirs: + stats["pruned_dirs"] = _prune_empty_dirs(module_dir) + + return stats + + +def clean_all_modules(release_dir: Path) -> List[dict]: + """ + @brief 遍历 MODULE_CONFIG 清理所有模块 + """ + results = [] + for module_rel, cfg in MODULE_CONFIG.items(): + print(f"[INFO] cleaning module: {module_rel}") + results.append(clean_module(release_dir, module_rel, cfg)) + return results + + +def sanitize_release_makefile(release_dir: Path) -> None: + """ + @brief 调整 Release 副本中的 Makefile:仅链接预编译 libusr_le_code.a + 开发工程 Makefile 不动;客户包不得再尝试用已删除源码重建库 + @param release_dir Release 根目录 + """ + makefile = release_dir / "Makefile" + if not makefile.is_file(): + print("[WARN] Makefile not found in Release, skip sanitize") + return + + text = makefile.read_text(encoding="utf-8") + + # 1) 去掉源码列表与 OBJS,仅保留预编译库路径 + text2, n1 = re.subn( + r"# usr_le_code 静态库[^\n]*\n" + r"USR_LE_LIB := apps/usr_le_code/lib/libusr_le_code\.a\n" + r"USR_LE_SRC_FILES := \\\n" + r"(?:[ \t]+apps/usr_le_code/[^\n]+\n)+" + r"\n" + r"USR_LE_OBJS :=[^\n]+\n", + "# usr_le_code 预编译静态库(Release 不附带协议源码,禁止本地重建)\n" + "USR_LE_LIB := apps/usr_le_code/lib/libusr_le_code.a\n" + "\n", + text, + count=1, + ) + if n1 == 0: + raise RuntimeError("sanitize Makefile failed: USR_LE_SRC_FILES block not found") + + # 2) .PHONY 去掉 lib_usr_le + text2, n2 = re.subn( + r"\.PHONY:\s*all clean pre_build lib_usr_le\b", + ".PHONY: all clean pre_build", + text2, + count=1, + ) + if n2 == 0: + # 兼容顺序变化,尽量剥离 lib_usr_le + text2 = re.sub(r"\s+lib_usr_le\b", "", text2, count=1) + + # 3) 删除 lib_usr_le 目标与 $(USR_LE_LIB): $(USR_LE_OBJS) 重建规则 + # 保留 all / OUT_ELF 对 $(USR_LE_LIB) 的依赖:库文件已存在即可链接 + text2, n3 = re.subn( + r"\n# 单独编译 usr_le_code 静态库:[^\n]*\n" + r"lib_usr_le:[^\n]*\n" + r"\n" + r"\$\(USR_LE_LIB\): \$\(USR_LE_OBJS\)\n" + r"(?:[ \t]+[^\n]+\n)+", + "\n", + text2, + count=1, + ) + if n3 == 0: + raise RuntimeError("sanitize Makefile failed: lib_usr_le rebuild rule not found") + + makefile.write_text(text2, encoding="utf-8") + print("[INFO] sanitized Release Makefile (prebuilt libusr_le_code.a only)") + + +# ============================================================================= +# README / ZIP +# ============================================================================= + +def write_readme(readme_path: Path, version: str, module_stats: List[dict]) -> None: + """ + @brief 生成 Release_Project_README.md + @param readme_path README 路径 + @param version 版本号 + @param module_stats 模块清理统计(用于目录说明) + """ + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + lines: List[str] = [ + f"# {PROJECT_DISPLAY_NAME}", + "", + "## 基本信息", + "", + f"- 工程名称:{PROJECT_DISPLAY_NAME}", + f"- 版本号:V{version}", + f"- 生成时间:{now}", + f"- 适用芯片:{CHIP_DISPLAY_NAME}", + "", + "## 目录说明", + "", + "```", + f"Release_Project_V{version}/", + "├── apps/", + "│ ├── common/ # SDK 公共模块", + "│ ├── earphone/ # 耳机应用", + "│ ├── usr_jb_proto/ # 应用协议层", + "│ └── usr_le_code/ # BLE 协议库(库文件 + 公开头文件)", + "├── cpu/ # 芯片相关代码与工具", + "├── include_lib/ # SDK 头文件", + "├── tools/ # 工程工具", + "├── Makefile", + "├── AC701N.cbp", + f"└── {README_NAME}", + "```", + "", + "### 封装模块公开接口", + "", + ] + + for module_rel, cfg in MODULE_CONFIG.items(): + public_headers = cfg.get("public_headers", []) + lines.append(f"**{module_rel}**") + lines.append("") + lines.append(f"- 静态库:`{module_rel}/lib/`") + lines.append("- 公开头文件:") + for h in public_headers: + lines.append(f" - `{module_rel}/{h}`") + lines.append("") + + lines.append("---") + lines.append("") + lines.append("*本文件由 tools/package_release.py 自动生成。*") + lines.append("") + + readme_path.write_text("\n".join(lines), encoding="utf-8") + + +def make_zip(release_dir: Path, release_zip: Path) -> None: + """ + @brief 将 Release 目录压缩为 zip(与目录同级) + @param release_dir Release 目录 + @param release_zip 目标 zip 路径 + """ + if release_zip.exists(): + release_zip.unlink() + + with zipfile.ZipFile(release_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for path in release_dir.rglob("*"): + if path.is_file(): + arcname = path.relative_to(release_dir.parent) + zf.write(path, arcname.as_posix()) + + +# ============================================================================= +# 主流程 +# ============================================================================= + +def remove_old_release(release_dir: Path, release_zip: Path) -> None: + """ + @brief 删除已有同名 Release 目录与 zip + """ + if release_dir.exists(): + print(f"[INFO] remove old release dir: {release_dir.name}") + shutil.rmtree(release_dir) + + if release_zip.exists(): + print(f"[INFO] remove old release zip: {release_zip.name}") + release_zip.unlink() + + +def main() -> int: + """ + @brief Release 打包主入口 + @return 进程退出码,0=成功 + """ + try: + project_root = get_project_root() + version = read_version(project_root) + paths = get_release_paths(project_root, version) + + release_dir: Path = paths["release_dir"] + release_zip: Path = paths["release_zip"] + readme_path: Path = paths["readme_path"] + dir_name: str = paths["dir_name"] + + print("=" * 60) + print(" AC701N Release Package Tool") + print("=" * 60) + output_dir: Path = paths["output_dir"] + + print(f"[INFO] project root : {project_root}") + print(f"[INFO] version : {version}") + print(f"[INFO] output dir : {RELEASE_OUTPUT_DIR}/") + print(f"[INFO] release dir : {RELEASE_OUTPUT_DIR}/{dir_name}") + print() + + # 确保 release/ 输出目录存在 + output_dir.mkdir(parents=True, exist_ok=True) + + # ① 删除旧产物 + remove_old_release(release_dir, release_zip) + + # ② 过滤复制 + print("[INFO] copying project ...") + copied = copy_project(project_root, release_dir) + print(f"[INFO] copied files : {copied}") + + # ③ 模块清理(仅 Release 副本) + print("[INFO] cleaning encapsulated modules ...") + module_stats = clean_all_modules(release_dir) + for st in module_stats: + if st["missing"]: + continue + print( + f" - {st['module']}: " + f"src={st['removed_sources']}, " + f"hdr={st['removed_headers']}, " + f"extra={st['removed_extra']}, " + f"empty_dirs={st['pruned_dirs']}" + ) + + # ④ Release Makefile:禁止用已删除源码重建库(开发工程 Makefile 不动) + print("[INFO] sanitizing Release Makefile ...") + sanitize_release_makefile(release_dir) + + # ⑤ README + print("[INFO] writing README ...") + write_readme(readme_path, version, module_stats) + + # ⑥ ZIP + print("[INFO] creating zip ...") + make_zip(release_dir, release_zip) + + print() + print("=" * 60) + print("[OK] Release package generated successfully") + print(f" dir : {release_dir}") + print(f" zip : {release_zip}") + print("=" * 60) + return 0 + + except Exception as exc: + print(f"[FAIL] {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +1.0.0