From 3ced7709c65a39494f5684e99111854a5bcbd8c7 Mon Sep 17 00:00:00 2001 From: Xiaoxia Date: Tue, 4 Nov 2025 05:03:40 +0800 Subject: [PATCH] Update to v1.9.4 (#1374) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Corrected the inverted touch screen parameter configuration of lichuang_S3_dev, which caused touch offset. (#1209) * ci: support multiple variants per board (#1036) * fix release.py * OTTO 左右腿反了 (#1239) * Change the button array to ADC buttons as in the board for esp32s3-korv2 (#1256) * Change the button array to ADC buttons as in the board for esp32s3-korv2 * Add MuteVol function to control audio volume * Optimize AdcBatteryMonitor to work without charge detection pin. (#1276) Co-authored-by: Yuv Zhao * 修复charging_pin为NC充电时Battery Level不更新的问题 (#1316) Co-authored-by: Yuv Zhao * Bump to 1.9.4 --------- Co-authored-by: ZhouShaoYuan Co-authored-by: laride <198868291+laride@users.noreply.github.com> Co-authored-by: Toby Co-authored-by: masc2008 Co-authored-by: konglingboy Co-authored-by: Yuv Zhao --- .github/workflows/build.yml | 49 +-- CMakeLists.txt | 2 +- main/boards/common/adc_battery_monitor.cc | 75 +++-- .../esp32s3_korvo2_v3_board.cc | 111 +++++++ .../boards/lichuang-dev/lichuang_dev_board.cc | 4 +- main/boards/otto-robot/otto_movements.cc | 2 +- main/idf_component.yml | 4 +- scripts/release.py | 290 ++++++++++++------ 8 files changed, 398 insertions(+), 139 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f7f365ed..d9659192 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,10 +14,10 @@ permissions: jobs: prepare: - name: Determine boards to build + name: Determine variants to build runs-on: ubuntu-latest outputs: - boards: ${{ steps.select.outputs.boards }} + variants: ${{ steps.select.outputs.variants }} steps: - name: Checkout uses: actions/checkout@v4 @@ -28,30 +28,30 @@ jobs: run: sudo apt-get update && sudo apt-get install -y jq - id: list - name: Get all board list + name: Get all variant list run: | - echo "all_boards=$(python scripts/release.py --list-boards --json)" >> $GITHUB_OUTPUT + echo "all_variants=$(python scripts/release.py --list-boards --json)" >> $GITHUB_OUTPUT - id: select - name: Select boards based on changes + name: Select variants based on changes env: - ALL_BOARDS: ${{ steps.list.outputs.all_boards }} + ALL_VARIANTS: ${{ steps.list.outputs.all_variants }} run: | EVENT_NAME="${{ github.event_name }}" - # For push to main branch, build all boards + # push 到 main 分支,编译全部变体 if [[ "$EVENT_NAME" == "push" ]]; then - echo "boards=$ALL_BOARDS" >> $GITHUB_OUTPUT + echo "variants=$ALL_VARIANTS" >> $GITHUB_OUTPUT exit 0 fi - # For pull_request + # pull_request 场景 BASE_SHA="${{ github.event.pull_request.base.sha }}" HEAD_SHA="${{ github.event.pull_request.head.sha }}" echo "Base: $BASE_SHA, Head: $HEAD_SHA" CHANGED=$(git diff --name-only $BASE_SHA $HEAD_SHA || true) - echo "Changed files:\n$CHANGED" + echo -e "Changed files:\n$CHANGED" NEED_ALL=0 declare -A AFFECTED @@ -60,6 +60,10 @@ jobs: NEED_ALL=1 fi + if [[ "$file" == main/boards/common/* ]]; then + NEED_ALL=1 + fi + if [[ "$file" == main/boards/* ]]; then board=$(echo "$file" | cut -d '/' -f3) AFFECTED[$board]=1 @@ -67,24 +71,25 @@ jobs: done <<< "$CHANGED" if [[ "$NEED_ALL" -eq 1 ]]; then - echo "boards=$ALL_BOARDS" >> $GITHUB_OUTPUT + echo "variants=$ALL_VARIANTS" >> $GITHUB_OUTPUT else if [[ ${#AFFECTED[@]} -eq 0 ]]; then - echo "boards=[]" >> $GITHUB_OUTPUT + echo "variants=[]" >> $GITHUB_OUTPUT else - JSON=$(printf '%s\n' "${!AFFECTED[@]}" | sort -u | jq -R -s -c 'split("\n")[:-1]') - echo "boards=$JSON" >> $GITHUB_OUTPUT + BOARDS_JSON=$(printf '%s\n' "${!AFFECTED[@]}" | sort -u | jq -R -s -c 'split("\n")[:-1]') + FILTERED=$(echo "$ALL_VARIANTS" | jq -c --argjson boards "$BOARDS_JSON" 'map(select(.board as $b | $boards | index($b)))') + echo "variants=$FILTERED" >> $GITHUB_OUTPUT fi fi build: - name: Build ${{ matrix.board }} + name: Build ${{ matrix.name }} needs: prepare - if: ${{ needs.prepare.outputs.boards != '[]' }} + if: ${{ needs.prepare.outputs.variants != '[]' }} strategy: - fail-fast: false # 单个 board 失败不影响其它 board + fail-fast: false # 单个变体失败不影响其它变体 matrix: - board: ${{ fromJson(needs.prepare.outputs.boards) }} + include: ${{ fromJson(needs.prepare.outputs.variants) }} runs-on: ubuntu-latest container: image: espressif/idf:release-v5.4 @@ -92,15 +97,15 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Build current board + - name: Build current variant shell: bash run: | source $IDF_PATH/export.sh - python scripts/release.py ${{ matrix.board }} + python scripts/release.py ${{ matrix.board }} --name ${{ matrix.name }} - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: xiaozhi_${{ matrix.board }}_${{ github.sha }}.bin + name: xiaozhi_${{ matrix.name }}_${{ github.sha }}.bin path: build/merged-binary.bin - if-no-files-found: error \ No newline at end of file + if-no-files-found: error diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f925aef..5e049ba3 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ # CMakeLists in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.16) -set(PROJECT_VER "1.9.2") +set(PROJECT_VER "1.9.4") # Add this line to disable the specific warning add_compile_options(-Wno-missing-field-initializers) diff --git a/main/boards/common/adc_battery_monitor.cc b/main/boards/common/adc_battery_monitor.cc index 9edf11cb..c34da68c 100644 --- a/main/boards/common/adc_battery_monitor.cc +++ b/main/boards/common/adc_battery_monitor.cc @@ -3,32 +3,42 @@ AdcBatteryMonitor::AdcBatteryMonitor(adc_unit_t adc_unit, adc_channel_t adc_channel, float upper_resistor, float lower_resistor, gpio_num_t charging_pin) : charging_pin_(charging_pin) { - // Initialize charging pin - gpio_config_t gpio_cfg = { - .pin_bit_mask = 1ULL << charging_pin, - .mode = GPIO_MODE_INPUT, - .pull_up_en = GPIO_PULLUP_DISABLE, - .pull_down_en = GPIO_PULLDOWN_DISABLE, - .intr_type = GPIO_INTR_DISABLE, - }; - ESP_ERROR_CHECK(gpio_config(&gpio_cfg)); + // Initialize charging pin (only if it's not NC) + if (charging_pin_ != GPIO_NUM_NC) { + gpio_config_t gpio_cfg = { + .pin_bit_mask = 1ULL << charging_pin, + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + ESP_ERROR_CHECK(gpio_config(&gpio_cfg)); + } // Initialize ADC battery estimation adc_battery_estimation_t adc_cfg = { .internal = { .adc_unit = adc_unit, - .adc_bitwidth = ADC_BITWIDTH_12, + .adc_bitwidth = ADC_BITWIDTH_DEFAULT, .adc_atten = ADC_ATTEN_DB_12, }, .adc_channel = adc_channel, .upper_resistor = upper_resistor, .lower_resistor = lower_resistor }; - adc_cfg.charging_detect_cb = [](void *user_data) -> bool { - AdcBatteryMonitor *self = (AdcBatteryMonitor *)user_data; - return gpio_get_level(self->charging_pin_) == 1; - }; - adc_cfg.charging_detect_user_data = this; + + // 在ADC配置部分进行条件设置 + if (charging_pin_ != GPIO_NUM_NC) { + adc_cfg.charging_detect_cb = [](void *user_data) -> bool { + AdcBatteryMonitor *self = (AdcBatteryMonitor *)user_data; + return gpio_get_level(self->charging_pin_) == 1; + }; + adc_cfg.charging_detect_user_data = this; + } else { + // 不设置回调,让adc_battery_estimation库使用软件估算 + adc_cfg.charging_detect_cb = nullptr; + adc_cfg.charging_detect_user_data = nullptr; + } adc_battery_estimation_handle_ = adc_battery_estimation_create(&adc_cfg); // Initialize timer @@ -48,12 +58,29 @@ AdcBatteryMonitor::~AdcBatteryMonitor() { if (adc_battery_estimation_handle_) { ESP_ERROR_CHECK(adc_battery_estimation_destroy(adc_battery_estimation_handle_)); } + + if (timer_handle_) { + esp_timer_stop(timer_handle_); + esp_timer_delete(timer_handle_); + } } bool AdcBatteryMonitor::IsCharging() { - bool is_charging = false; - ESP_ERROR_CHECK(adc_battery_estimation_get_charging_state(adc_battery_estimation_handle_, &is_charging)); - return is_charging; + // 优先使用adc_battery_estimation库的功能 + if (adc_battery_estimation_handle_ != nullptr) { + bool is_charging = false; + esp_err_t err = adc_battery_estimation_get_charging_state(adc_battery_estimation_handle_, &is_charging); + if (err == ESP_OK) { + return is_charging; + } + } + + // 回退到GPIO读取或返回默认值 + if (charging_pin_ != GPIO_NUM_NC) { + return gpio_get_level(charging_pin_) == 1; + } + + return false; } bool AdcBatteryMonitor::IsDischarging() { @@ -61,9 +88,17 @@ bool AdcBatteryMonitor::IsDischarging() { } uint8_t AdcBatteryMonitor::GetBatteryLevel() { + // 如果句柄无效,返回默认值 + if (adc_battery_estimation_handle_ == nullptr) { + return 100; + } + float capacity = 0; - ESP_ERROR_CHECK(adc_battery_estimation_get_capacity(adc_battery_estimation_handle_, &capacity)); - return capacity; + esp_err_t err = adc_battery_estimation_get_capacity(adc_battery_estimation_handle_, &capacity); + if (err != ESP_OK) { + return 100; // 出错时返回默认值 + } + return (uint8_t)capacity; } void AdcBatteryMonitor::OnChargingStatusChanged(std::function callback) { diff --git a/main/boards/esp32s3-korvo2-v3/esp32s3_korvo2_v3_board.cc b/main/boards/esp32s3-korvo2-v3/esp32s3_korvo2_v3_board.cc index d9b852c6..35656b30 100644 --- a/main/boards/esp32s3-korvo2-v3/esp32s3_korvo2_v3_board.cc +++ b/main/boards/esp32s3-korvo2-v3/esp32s3_korvo2_v3_board.cc @@ -5,6 +5,7 @@ #include "button.h" #include "config.h" #include "i2c_device.h" +#include "assets/lang_config.h" #include #include @@ -16,6 +17,16 @@ #include "esp32_camera.h" #define TAG "esp32s3_korvo2_v3" +/* ADC Buttons */ +typedef enum { + BSP_ADC_BUTTON_REC, + BSP_ADC_BUTTON_VOL_MUTE, + BSP_ADC_BUTTON_PLAY, + BSP_ADC_BUTTON_SET, + BSP_ADC_BUTTON_VOL_DOWN, + BSP_ADC_BUTTON_VOL_UP, + BSP_ADC_BUTTON_NUM +} bsp_adc_button_t; LV_FONT_DECLARE(font_puhui_20_4); LV_FONT_DECLARE(font_awesome_20_4); @@ -45,6 +56,10 @@ static const ili9341_lcd_init_cmd_t vendor_specific_init[] = { class Esp32S3Korvo2V3Board : public WifiBoard { private: Button boot_button_; + Button* adc_button_[BSP_ADC_BUTTON_NUM]; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + adc_oneshot_unit_handle_t bsp_adc_handle = NULL; +#endif i2c_master_bus_handle_t i2c_bus_; LcdDisplay* display_; esp_io_expander_handle_t io_expander_ = NULL; @@ -131,7 +146,103 @@ private: ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO)); } + void ChangeVol(int val) { + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() + val; + if (volume > 100) { + volume = 100; + } + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + } + + void MuteVol() { + auto codec = GetAudioCodec(); + auto volume = codec->output_volume(); + if (volume > 1) { + volume = 0; + } else { + volume = 50; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + } + void InitializeButtons() { + button_adc_config_t adc_cfg = {}; + adc_cfg.adc_channel = ADC_CHANNEL_4; // ADC1 channel 0 is GPIO5 +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + const adc_oneshot_unit_init_cfg_t init_config1 = { + .unit_id = ADC_UNIT_1, + }; + adc_oneshot_new_unit(&init_config1, &bsp_adc_handle); + adc_cfg.adc_handle = &bsp_adc_handle; +#endif + adc_cfg.button_index = BSP_ADC_BUTTON_REC; + adc_cfg.min = 2310; // middle is 2410mV + adc_cfg.max = 2510; + adc_button_[0] = new AdcButton(adc_cfg); + + adc_cfg.button_index = BSP_ADC_BUTTON_VOL_MUTE; + adc_cfg.min = 1880; // middle is 1980mV + adc_cfg.max = 2080; + adc_button_[1] = new AdcButton(adc_cfg); + + adc_cfg.button_index = BSP_ADC_BUTTON_PLAY; + adc_cfg.min = 1550; // middle is 1650mV + adc_cfg.max = 1750; + adc_button_[2] = new AdcButton(adc_cfg); + + adc_cfg.button_index = BSP_ADC_BUTTON_SET; + adc_cfg.min = 1015; // middle is 1115mV + adc_cfg.max = 1215; + adc_button_[3] = new AdcButton(adc_cfg); + + adc_cfg.button_index = BSP_ADC_BUTTON_VOL_DOWN; + adc_cfg.min = 720; // middle is 820mV + adc_cfg.max = 920; + adc_button_[4] = new AdcButton(adc_cfg); + + adc_cfg.button_index = BSP_ADC_BUTTON_VOL_UP; + adc_cfg.min = 280; // middle is 380mV + adc_cfg.max = 480; + adc_button_[5] = new AdcButton(adc_cfg); + + auto volume_up_button = adc_button_[BSP_ADC_BUTTON_VOL_UP]; + volume_up_button->OnClick([this]() {ChangeVol(10);}); + volume_up_button->OnLongPress([this]() { + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + }); + + auto volume_down_button = adc_button_[BSP_ADC_BUTTON_VOL_DOWN]; + volume_down_button->OnClick([this]() {ChangeVol(-10);}); + volume_down_button->OnLongPress([this]() { + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + }); + + auto volume_mute_button = adc_button_[BSP_ADC_BUTTON_VOL_MUTE]; + volume_mute_button->OnClick([this]() {MuteVol();}); + + auto play_button = adc_button_[BSP_ADC_BUTTON_PLAY]; + play_button->OnClick([this]() { + ESP_LOGI(TAG, " TODO %s:%d\n", __func__, __LINE__); + }); + + auto set_button = adc_button_[BSP_ADC_BUTTON_SET]; + set_button->OnClick([this]() { + ESP_LOGI(TAG, "TODO %s:%d\n", __func__, __LINE__); + }); + + auto rec_button = adc_button_[BSP_ADC_BUTTON_REC]; + rec_button->OnClick([this]() { + ESP_LOGI(TAG, "TODO %s:%d\n", __func__, __LINE__); + }); + boot_button_.OnClick([this]() {}); boot_button_.OnClick([this]() { auto& app = Application::GetInstance(); if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { diff --git a/main/boards/lichuang-dev/lichuang_dev_board.cc b/main/boards/lichuang-dev/lichuang_dev_board.cc index 9b682d6b..43ed63b2 100644 --- a/main/boards/lichuang-dev/lichuang_dev_board.cc +++ b/main/boards/lichuang-dev/lichuang_dev_board.cc @@ -173,8 +173,8 @@ private: { esp_lcd_touch_handle_t tp; esp_lcd_touch_config_t tp_cfg = { - .x_max = DISPLAY_WIDTH, - .y_max = DISPLAY_HEIGHT, + .x_max = DISPLAY_HEIGHT, + .y_max = DISPLAY_WIDTH, .rst_gpio_num = GPIO_NUM_NC, // Shared with LCD reset .int_gpio_num = GPIO_NUM_NC, .levels = { diff --git a/main/boards/otto-robot/otto_movements.cc b/main/boards/otto-robot/otto_movements.cc index 5f3315d3..5dee127c 100644 --- a/main/boards/otto-robot/otto_movements.cc +++ b/main/boards/otto-robot/otto_movements.cc @@ -389,7 +389,7 @@ void Otto::ShakeLeg(int steps, int period, int dir) { int homes[SERVO_COUNT] = {90, 90, 90, 90, HAND_HOME_POSITION, 180 - HAND_HOME_POSITION}; // Changes in the parameters if left leg is chosen - if (dir == -1) { + if (dir == 1) { shake_leg1[2] = 180 - 35; shake_leg1[3] = 180 - 58; shake_leg2[2] = 180 - 120; diff --git a/main/idf_component.yml b/main/idf_component.yml index ddc06909..d583bbb4 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -13,9 +13,9 @@ dependencies: espressif/esp_io_expander_tca9554: ==2.0.0 espressif/esp_lcd_panel_io_additions: ^1.0.1 78/esp_lcd_nv3023: ~1.0.0 - 78/esp-wifi-connect: ~2.5.2 + 78/esp-wifi-connect: ~2.6.1 78/esp-opus-encoder: ~2.4.1 - 78/esp-ml307: ~3.3.5 + 78/esp-ml307: ~3.3.7 78/xiaozhi-fonts: ~1.5.2 espressif/led_strip: ~3.0.1 espressif/esp_codec_dev: ~1.4.0 diff --git a/scripts/release.py b/scripts/release.py index fdceedf6..93d83d03 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -3,107 +3,186 @@ import os import json import zipfile import argparse +from pathlib import Path +from typing import Optional -# 切换到项目根目录 -os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +# Switch to project root directory +os.chdir(Path(__file__).resolve().parent.parent) -def get_board_type(): - with open("build/compile_commands.json") as f: +################################################################################ +# Common utility functions +################################################################################ + +def get_board_type_from_compile_commands() -> Optional[str]: + """Parse the current compiled BOARD_TYPE from build/compile_commands.json""" + compile_file = Path("build/compile_commands.json") + if not compile_file.exists(): + return None + with compile_file.open() as f: data = json.load(f) - for item in data: - if not item["file"].endswith("main.cc"): - continue - command = item["command"] - # extract -DBOARD_TYPE=xxx - board_type = command.split("-DBOARD_TYPE=\\\"")[1].split("\\\"")[0].strip() - return board_type + for item in data: + if not item["file"].endswith("main.cc"): + continue + cmd = item["command"] + if "-DBOARD_TYPE=\\\"" in cmd: + return cmd.split("-DBOARD_TYPE=\\\"")[1].split("\\\"")[0].strip() return None -def get_project_version(): - with open("CMakeLists.txt") as f: + +def get_project_version() -> Optional[str]: + """Read set(PROJECT_VER "x.y.z") from root CMakeLists.txt""" + with Path("CMakeLists.txt").open() as f: for line in f: if line.startswith("set(PROJECT_VER"): - return line.split("\"")[1].split("\"")[0].strip() + return line.split("\"")[1] return None -def merge_bin(): + +def merge_bin() -> None: if os.system("idf.py merge-bin") != 0: - print("merge bin failed") + print("merge-bin failed", file=sys.stderr) sys.exit(1) -def zip_bin(board_type, project_version): - if not os.path.exists("releases"): - os.makedirs("releases") - output_path = f"releases/v{project_version}_{board_type}.zip" - if os.path.exists(output_path): - os.remove(output_path) - with zipfile.ZipFile(output_path, 'w', compression=zipfile.ZIP_DEFLATED) as zipf: + +def zip_bin(name: str, version: str) -> None: + """Zip build/merged-binary.bin to releases/v{version}_{name}.zip""" + out_dir = Path("releases") + out_dir.mkdir(exist_ok=True) + output_path = out_dir / f"v{version}_{name}.zip" + + if output_path.exists(): + output_path.unlink() + + with zipfile.ZipFile(output_path, "w", compression=zipfile.ZIP_DEFLATED) as zipf: zipf.write("build/merged-binary.bin", arcname="merged-binary.bin") print(f"zip bin to {output_path} done") - -def release_current(): - merge_bin() - board_type = get_board_type() - print("board type:", board_type) - project_version = get_project_version() - print("project version:", project_version) - zip_bin(board_type, project_version) +################################################################################ +# board / variant related functions +################################################################################ -def get_all_board_types(): - board_configs = {} - with open("main/CMakeLists.txt", encoding='utf-8') as f: - lines = f.readlines() - for i, line in enumerate(lines): - # 查找 if(CONFIG_BOARD_TYPE_*) 行 - if "if(CONFIG_BOARD_TYPE_" in line: - config_name = line.strip().split("if(")[1].split(")")[0] - # 查找下一行的 set(BOARD_TYPE "xxx") - next_line = lines[i + 1].strip() +_BOARDS_DIR = Path("main/boards") + + +def _collect_variants(config_filename: str = "config.json") -> list[dict[str, str]]: + """Traverse all boards under main/boards, collect variant information. + + Return example: + [{"board": "bread-compact-ml307", "name": "bread-compact-ml307"}, ...] + """ + variants: list[dict[str, str]] = [] + for board_path in _BOARDS_DIR.iterdir(): + if not board_path.is_dir(): + continue + if board_path.name == "common": + continue + cfg_path = board_path / config_filename + if not cfg_path.exists(): + print(f"[WARN] {cfg_path} does not exist, skip", file=sys.stderr) + continue + try: + with cfg_path.open() as f: + cfg = json.load(f) + for build in cfg.get("builds", []): + variants.append({"board": board_path.name, "name": build["name"]}) + except Exception as e: + print(f"[ERROR] 解析 {cfg_path} 失败: {e}", file=sys.stderr) + return variants + + +def _parse_board_config_map() -> dict[str, str]: + """Build the mapping of CONFIG_BOARD_TYPE_xxx and board_type from main/CMakeLists.txt""" + cmake_file = Path("main/CMakeLists.txt") + mapping: dict[str, str] = {} + lines = cmake_file.read_text(encoding="utf-8").splitlines() + for idx, line in enumerate(lines): + if "if(CONFIG_BOARD_TYPE_" in line: + config_name = line.strip().split("if(")[1].split(")")[0] + if idx + 1 < len(lines): + next_line = lines[idx + 1].strip() if next_line.startswith("set(BOARD_TYPE"): board_type = next_line.split('"')[1] - board_configs[config_name] = board_type - return board_configs + mapping[config_name] = board_type + return mapping -def release(board_type, board_config, config_filename="config.json"): - config_path = f"main/boards/{board_type}/{config_filename}" - if not os.path.exists(config_path): - print(f"跳过 {board_type} 因为 {config_filename} 不存在") + +def _find_board_config(board_type: str) -> Optional[str]: + """Find the corresponding CONFIG_BOARD_TYPE_xxx for the given board_type""" + for config, b_type in _parse_board_config_map().items(): + if b_type == board_type: + return config + return None + +################################################################################ +# Check board_type in CMakeLists +################################################################################ + +def _board_type_exists(board_type: str) -> bool: + cmake_file = Path("main/CMakeLists.txt") + pattern = f'set(BOARD_TYPE "{board_type}")' + return pattern in cmake_file.read_text(encoding="utf-8") + +################################################################################ +# Compile implementation +################################################################################ + +def release(board_type: str, config_filename: str = "config.json", *, filter_name: Optional[str] = None) -> None: + """Compile and package all/specified variants of the specified board_type + + Args: + board_type: directory name under main/boards + config_filename: config.json name (default: config.json) + filter_name: if specified, only compile the build["name"] that matches + """ + cfg_path = _BOARDS_DIR / board_type / config_filename + if not cfg_path.exists(): + print(f"[WARN] {cfg_path} 不存在,跳过 {board_type}") return - # Print Project Version project_version = get_project_version() - print(f"Project Version: {project_version}", config_path) + print(f"Project Version: {project_version} ({cfg_path})") + + with cfg_path.open() as f: + cfg = json.load(f) + target = cfg["target"] + + builds = cfg.get("builds", []) + if filter_name: + builds = [b for b in builds if b["name"] == filter_name] + if not builds: + print(f"[ERROR] 未在 {board_type} 的 {config_filename} 中找到变体 {filter_name}", file=sys.stderr) + sys.exit(1) - with open(config_path, "r") as f: - config = json.load(f) - target = config["target"] - builds = config["builds"] - for build in builds: name = build["name"] if not name.startswith(board_type): - raise ValueError(f"name {name} 必须以 {board_type} 开头") - output_path = f"releases/v{project_version}_{name}.zip" - if os.path.exists(output_path): - print(f"跳过 {board_type} 因为 {output_path} 已存在") + raise ValueError(f"build.name {name} 必须以 {board_type} 开头") + + output_path = Path("releases") / f"v{project_version}_{name}.zip" + if output_path.exists(): + print(f"跳过 {name} 因为 {output_path} 已存在") continue - sdkconfig_append = [f"{board_config}=y"] - for append in build.get("sdkconfig_append", []): - sdkconfig_append.append(append) + # Process sdkconfig_append + board_type_config = _find_board_config(board_type) + sdkconfig_append = [f"{board_type_config}=y"] + sdkconfig_append.extend(build.get("sdkconfig_append", [])) + + print("-" * 80) print(f"name: {name}") print(f"target: {target}") - for append in sdkconfig_append: - print(f"sdkconfig_append: {append}") - # unset IDF_TARGET + for item in sdkconfig_append: + print(f"sdkconfig_append: {item}") + os.environ.pop("IDF_TARGET", None) + # Call set-target if os.system(f"idf.py set-target {target}") != 0: - print("set-target failed") + print("set-target failed", file=sys.stderr) sys.exit(1) + # Append sdkconfig - with open("sdkconfig", "a") as f: + with Path("sdkconfig").open("a") as f: f.write("\n") for append in sdkconfig_append: f.write(f"{append}\n") @@ -111,43 +190,72 @@ def release(board_type, board_config, config_filename="config.json"): if os.system(f"idf.py -DBOARD_NAME={name} build") != 0: print("build failed") sys.exit(1) - # Call merge-bin - if os.system("idf.py merge-bin") != 0: - print("merge-bin failed") - sys.exit(1) - # Zip bin + + # merge-bin + merge_bin() + + # Zip zip_bin(name, project_version) - print("-" * 80) + +################################################################################ +# CLI entry +################################################################################ if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("board", nargs="?", default=None, help="板子类型或 all") parser.add_argument("-c", "--config", default="config.json", help="指定 config 文件名,默认 config.json") - parser.add_argument("--list-boards", action="store_true", help="列出所有支持的 board 列表") + parser.add_argument("--list-boards", action="store_true", help="列出所有支持的 board 及变体列表") parser.add_argument("--json", action="store_true", help="配合 --list-boards,JSON 格式输出") + parser.add_argument("--name", help="指定变体名称,仅编译匹配的变体") + args = parser.parse_args() + # List mode if args.list_boards: - board_configs = get_all_board_types() - boards = list(board_configs.values()) + variants = _collect_variants(config_filename=args.config) if args.json: - print(json.dumps(boards)) + print(json.dumps(variants)) else: - for board in boards: - print(board) + for v in variants: + print(f"{v['board']}: {v['name']}") sys.exit(0) - if args.board: - board_configs = get_all_board_types() - found = False - for board_config, board_type in board_configs.items(): - if args.board == 'all' or board_type == args.board: - release(board_type, board_config, config_filename=args.config) - found = True - if not found: - print(f"未找到板子类型: {args.board}") - print("可用的板子类型:") - for board_type in board_configs.values(): - print(f" {board_type}") + # Current directory firmware packaging mode + if args.board is None: + merge_bin() + curr_board_type = get_board_type_from_compile_commands() + if curr_board_type is None: + print("未能从 compile_commands.json 解析 board_type", file=sys.stderr) + sys.exit(1) + project_ver = get_project_version() + zip_bin(curr_board_type, project_ver) + sys.exit(0) + + # Compile mode + board_type_input: str = args.board + name_filter: str | None = args.name + + # Check board_type in CMakeLists + if board_type_input != "all" and not _board_type_exists(board_type_input): + print(f"[ERROR] main/CMakeLists.txt 中未找到 board_type {board_type_input}", file=sys.stderr) + sys.exit(1) + + variants_all = _collect_variants(config_filename=args.config) + + # Filter board_type list + target_board_types: set[str] + if board_type_input == "all": + target_board_types = {v["board"] for v in variants_all} else: - release_current() + target_board_types = {board_type_input} + + for bt in sorted(target_board_types): + if not _board_type_exists(bt): + print(f"[ERROR] main/CMakeLists.txt 中未找到 board_type {bt}", file=sys.stderr) + sys.exit(1) + cfg_path = _BOARDS_DIR / bt / args.config + if bt == board_type_input and not cfg_path.exists(): + print(f"开发板 {bt} 未定义 {args.config} 配置文件,跳过") + sys.exit(0) + release(bt, config_filename=args.config, filter_name=name_filter if bt == board_type_input else None)