diff --git a/README.md b/README.md index b03e745a..e685e1d4 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,6 @@ group — `manufacturer` wins whenever both are present. Forward-compatible: an IDE predating the field ignores it (shows everything); an SDK-aware IDE filters the catalogue by the active SDK. `platforms` items do **not** carry this field. - On `skills` this field is **load-bearing rather than advisory** since 2026-09-02: it is the *only* thing separating the two product lines, which used to be separated by their directory (`skills/TuyaOpen/` vs @@ -148,6 +147,13 @@ group — `manufacturer` wins whenever both are present. `scripts/validate-skills-index.py` reads the same field to decide which of its rules — the ones asserting a relationship with the `tuyaopen-cli` CLI — apply to a given item. +- **SDK branch requirements** (`sdkRequirements`) — optional array in a board + or demo **detail** file, one `{ "sdk": "tuyaopen" | "tuyaos", "branch": + "…" }` entry per SDK line. It declares an exceptional branch needed by that + resource; it does not decide catalogue visibility (that remains `sdks`). If + the detail has no entry for the active SDK, the default is that SDK's + `master` or a Release version. Keep this field out of `index.json` so + catalogue lists do not need to fetch every detail file. - **Platform pinout `functions` vs `caps`** — in a platform detail file each `pinout[]` entry splits its labels into two arrays: `functions[]` is a **controlled, selection-only** vocabulary of editor-selectable *routing* @@ -161,8 +167,10 @@ group — `manufacturer` wins whenever both are present. (PWM on `spec`) carries `routable` (default `false` = fixed pinmux, pins locked). GPIO-matrix chips (ESP32) set `routable: true` on digital ports so their `pinGroups` become *defaults* and any `GPIO`-capable pin is selectable; - an optional `candidates: [gpio,…]` constrains the routable set (e.g. LP-domain - ports). ADC/analog ports stay `routable: false`. + an optional `candidates` constrains the routable set (e.g. LP-domain ports). + It accepts individual GPIOs and inclusive ranges, such as + `candidates: [0, 2, [8, 28], [30, 40]]`. ADC/analog ports stay + `routable: false`. - **`published` gates downstream** — a platform item and a board item each carry `published` (default `true` when absent). A board's **effective** publish state is `board.published !== false` **AND** its chip platform's `published !== false`: diff --git a/boards-and-chips/esp32/espressif-esp32-s31.json b/boards-and-chips/esp32/espressif-esp32-s31.json new file mode 100644 index 00000000..0f820ec8 --- /dev/null +++ b/boards-and-chips/esp32/espressif-esp32-s31.json @@ -0,0 +1,83 @@ +{ + "schemaVersion": 1, + "memory": { + "flashBytes": 16777216, + "psramBytes": 16777216 + }, + "peripheralPatterns": {}, + "peripheralGroups": {}, + "links": { + "schematic": null, + "datasheet": { + "en": "https://documentation.espressif.com/esp32-s31-wroom-3_wroom-3u_datasheet_en.html", + "zh-CN": "https://documentation.espressif.com/esp32-s31-wroom-3_wroom-3u_datasheet_cn.html" + }, + "productPage": null + }, + "boardSymbol": "ESP32_S31", + "expansionPins": [ + { "gpio": 0, "functions": ["GPIO"] }, + { "gpio": 1, "functions": ["GPIO"] }, + { "gpio": 2, "functions": ["GPIO"] }, + { "gpio": 3, "functions": ["GPIO"] }, + { "gpio": 4, "functions": ["GPIO"] }, + { "gpio": 5, "functions": ["GPIO"] }, + { "gpio": 6, "functions": ["GPIO", "TOUCH0"] }, + { "gpio": 7, "functions": ["GPIO", "TOUCH1"] }, + { "gpio": 8, "functions": ["GPIO", "TOUCH2"] }, + { "gpio": 9, "functions": ["GPIO", "TOUCH3", "SPI2_HOLD"] }, + { "gpio": 10, "functions": ["GPIO", "TOUCH4", "SPI2_CS"] }, + { "gpio": 11, "functions": ["GPIO", "TOUCH5", "SPI2_MOSI"] }, + { "gpio": 12, "functions": ["GPIO", "TOUCH6", "SPI2_CLK"] }, + { "gpio": 13, "functions": ["GPIO", "TOUCH7", "SPI2_MISO"] }, + { "gpio": 14, "functions": ["GPIO", "TOUCH8", "SPI2_WP"] }, + { "gpio": 15, "functions": ["GPIO", "TOUCH9"] }, + { "gpio": 16, "functions": ["GPIO", "TOUCH10"] }, + { "gpio": 17, "functions": ["GPIO", "TOUCH11"] }, + { "gpio": 18, "functions": ["GPIO", "TOUCH12"] }, + { "gpio": 19, "functions": ["GPIO", "TOUCH13"] }, + { "gpio": 20, "functions": ["GPIO"] }, + { "gpio": 21, "functions": ["GPIO"] }, + { "gpio": 22, "functions": ["GPIO"] }, + { "gpio": 23, "functions": ["GPIO"] }, + { "gpio": 24, "functions": ["GPIO"] }, + { "gpio": 25, "functions": ["GPIO"] }, + { "gpio": 33, "functions": ["GPIO"] }, + { "gpio": 34, "functions": ["GPIO"] }, + { "gpio": 35, "functions": ["GPIO"] }, + { "gpio": 36, "functions": ["GPIO"] }, + { "gpio": 37, "functions": ["GPIO"] }, + { "gpio": 38, "functions": ["GPIO"] }, + { "gpio": 39, "functions": ["GPIO"] }, + { "gpio": 40, "functions": ["GPIO"] }, + { "gpio": 42, "functions": ["GPIO", "ADC1_CH0_N"] }, + { "gpio": 43, "functions": ["GPIO", "ADC1_CH0_P"] }, + { "gpio": 44, "functions": ["GPIO", "ADC1_CH1_N"] }, + { "gpio": 45, "functions": ["GPIO", "ADC1_CH1_P"] }, + { "gpio": 46, "functions": ["GPIO", "ADC1_CH2_N"] }, + { "gpio": 47, "functions": ["GPIO", "ADC1_CH2_P"] }, + { "gpio": 48, "functions": ["GPIO", "ADC1_CH3_N"] }, + { "gpio": 49, "functions": ["GPIO", "ADC1_CH3_P"] }, + { "gpio": 50, "functions": ["GPIO", "ADC2_CH0_N"] }, + { "gpio": 51, "functions": ["GPIO", "ADC2_CH0_P"] }, + { "gpio": 52, "functions": ["GPIO", "ADC2_CH1_N"] }, + { "gpio": 53, "functions": ["GPIO", "ADC2_CH1_P"] }, + { "gpio": 54, "functions": ["GPIO", "ADC2_CH2_N"] }, + { "gpio": 55, "functions": ["GPIO", "ADC2_CH2_P"] }, + { "gpio": 56, "functions": ["GPIO", "ADC2_CH3_N"] }, + { "gpio": 57, "functions": ["GPIO", "ADC2_CH3_P"] }, + { "gpio": 58, "functions": ["GPIO", "UART0_TX"] }, + { "gpio": 59, "functions": ["GPIO", "UART0_RX"] }, + { "gpio": 60, "functions": ["GPIO"] }, + { "gpio": 61, "functions": ["GPIO"] } + ], + "sdkRequirements": [ + { + "sdk": "tuyaopen", + "branch": "develop" + } + ], + "source": { + "repo": "https://github.com/tuya/TuyaOpen/tree/develop/boards/ESP32/ESP32-S31" + } +} diff --git a/boards-and-chips/esp32/espressif-esp32s31-korvo-1.json b/boards-and-chips/esp32/espressif-esp32s31-korvo-1.json new file mode 100644 index 00000000..25f5c41d --- /dev/null +++ b/boards-and-chips/esp32/espressif-esp32s31-korvo-1.json @@ -0,0 +1,418 @@ +{ + "schemaVersion": 1, + "memory": { + "flashBytes": 16777216, + "psramBytes": 16777216 + }, + "peripheralPatterns": { + "display": [ + { + "id": "display", + "name": { + "en": "4.3-inch RGB LCD", + "zh-CN": "4.3 寸 RGB LCD 显示屏" + }, + "model": "RGB LCD", + "interface": "RGB", + "width": 800, + "height": 480, + "pixelFormat": "RGB565", + "mounting": "accessory", + "pinLocked": true, + "pins": { + "RGB": [ + { + "role": "b0", + "gpio": 8 + }, + { + "role": "b1", + "gpio": 9 + }, + { + "role": "b2", + "gpio": 10 + }, + { + "role": "b3", + "gpio": 11 + }, + { + "role": "b4", + "gpio": 12 + }, + { + "role": "g0", + "gpio": 13 + }, + { + "role": "g1", + "gpio": 14 + }, + { + "role": "g2", + "gpio": 15 + }, + { + "role": "g3", + "gpio": 16 + }, + { + "role": "g4", + "gpio": 17 + }, + { + "role": "g5", + "gpio": 18 + }, + { + "role": "r0", + "gpio": 19 + }, + { + "role": "r1", + "gpio": 33 + }, + { + "role": "r2", + "gpio": 34 + }, + { + "role": "r3", + "gpio": 35 + }, + { + "role": "r4", + "gpio": 36 + }, + { + "role": "cs", + "gpio": 38 + }, + { + "role": "pclk", + "gpio": 40 + }, + { + "role": "de", + "gpio": 43 + }, + { + "role": "hsync", + "gpio": 44 + }, + { + "role": "vsync", + "gpio": 45 + } + ] + }, + "kconfig": null, + "note": { + "en": "Bundled 4.3-inch 800x480 RGB565 LCD sub-board. RGB clock is 18 MHz in the TuyaOpen board configuration. GPIO38 is LCD CS and shares the camera GM_FK / Boot Mode 0 signal.", + "zh-CN": "随板 4.3 寸 800x480 RGB565 LCD 子板。TuyaOpen 板级配置中的 RGB 时钟为 18 MHz。GPIO38 为 LCD CS,并与摄像头 GM_FK / 启动模式 0 信号复用。" + } + } + ], + "touchpad": [ + { + "id": "touchpad", + "name": { + "en": "GT1151 Capacitive Touch", + "zh-CN": "GT1151 电容触控" + }, + "model": "GT1151", + "interface": "I2C", + "mounting": "accessory", + "pinLocked": true, + "pins": { + "I2C": [ + { + "role": "scl", + "gpio": 1 + }, + { + "role": "sda", + "gpio": 0 + } + ] + }, + "kconfig": null, + "group": "i2c-bus-0", + "note": { + "en": "GT1151 touch controller shares I2C0 (SCL=GPIO1, SDA=GPIO0); its interrupt and reset lines are not used by the TuyaOpen board configuration.", + "zh-CN": "GT1151 触控控制器与其他器件共用 I2C0(SCL=GPIO1,SDA=GPIO0);TuyaOpen 板级配置未使用其中断和复位线。" + } + } + ], + "audio": [ + { + "id": "audio", + "name": { + "en": "ES8389 Audio Codec", + "zh-CN": "ES8389 音频编解码器" + }, + "model": "ES8389", + "interface": "I2S", + "mounting": "onboard", + "pinLocked": true, + "pins": { + "I2S": [ + { + "role": "mclk", + "gpio": 2 + }, + { + "role": "bck", + "gpio": 3 + }, + { + "role": "ws", + "gpio": 4 + }, + { + "role": "dout", + "gpio": 5 + }, + { + "role": "din", + "gpio": 6 + }, + { + "role": "spk_en", + "gpio": 7 + } + ], + "I2C": [ + { + "role": "scl", + "gpio": 1 + }, + { + "role": "sda", + "gpio": 0 + } + ] + }, + "kconfig": null, + "group": "i2c-bus-0", + "note": { + "en": "ES8389 codec with dual microphones and speakers. The control bus is shared I2C0; the codec uses 7-bit address 0x10. GPIO2 is routed as I2S_MCLK, while the TuyaOpen Korvo configuration runs the codec MCLK-less at 16 kHz.", + "zh-CN": "ES8389 音频编解码器,配有双麦克风和扬声器。控制总线为共享 I2C0,编解码器使用 7 位地址 0x10。GPIO2 路由为 I2S_MCLK,但 TuyaOpen 的 Korvo 配置以无 MCLK 的 16 kHz 模式运行编解码器。" + } + } + ], + "camera": [ + { + "id": "camera", + "name": { + "en": "OV3660 Camera", + "zh-CN": "OV3660 摄像头" + }, + "model": "OV3660", + "interface": "DVP", + "mounting": "onboard", + "pinLocked": true, + "pins": { + "DVP": [ + { + "role": "d0", + "gpio": 46 + }, + { + "role": "d1", + "gpio": 47 + }, + { + "role": "d2", + "gpio": 48 + }, + { + "role": "d3", + "gpio": 49 + }, + { + "role": "d4", + "gpio": 50 + }, + { + "role": "d5", + "gpio": 51 + }, + { + "role": "d6", + "gpio": 52 + }, + { + "role": "d7", + "gpio": 53 + }, + { + "role": "pclk", + "gpio": 54 + }, + { + "role": "mclk", + "gpio": 55 + }, + { + "role": "vsync", + "gpio": 56 + }, + { + "role": "hsync", + "gpio": 57 + } + ], + "I2C": [ + { + "role": "scl", + "gpio": 1 + }, + { + "role": "sda", + "gpio": 0 + } + ] + }, + "kconfig": null, + "group": "i2c-bus-0", + "note": { + "en": "OV3660 DVP camera; its SCCB control interface shares I2C0 with the audio codec and touch controller.", + "zh-CN": "OV3660 DVP 摄像头;其 SCCB 控制接口与音频编解码器、触控控制器共用 I2C0。" + } + } + ], + "led-pixel": [ + { + "id": "led-pixel", + "name": { + "en": "Addressable RGB LED", + "zh-CN": "可寻址 RGB LED" + }, + "model": "WS2812B", + "count": 1, + "interface": "RMT", + "mounting": "onboard", + "pinLocked": true, + "pins": { + "RMT": [ + { + "role": "data", + "gpio": 37 + } + ] + }, + "kconfig": null, + "note": { + "en": "Single addressable RGB status LED on GPIO37, driven through RMT by the TuyaOpen board driver.", + "zh-CN": "单颗可寻址 RGB 状态 LED 接在 GPIO37,由 TuyaOpen 板级驱动通过 RMT 控制。" + } + } + ], + "button": [ + { + "id": "button", + "name": { + "en": "Four User Buttons", + "zh-CN": "四个用户按键" + }, + "count": 4, + "interface": "ADC", + "mounting": "onboard", + "pinLocked": true, + "pins": { + "ADC": [ + { + "role": "resistor_ladder", + "gpio": 42 + } + ] + }, + "kconfig": null, + "note": { + "en": "Four buttons use a resistor ladder on GPIO42 (ADC1_CH0); the board driver decodes the individual keys.", + "zh-CN": "四个按键通过 GPIO42(ADC1_CH0)上的电阻分压网络接入,由板级驱动识别各个按键。" + } + } + ], + "sd-card": [ + { + "id": "sd-card", + "name": { + "en": "microSD Card Slot", + "zh-CN": "microSD 卡槽" + }, + "interface": "SDIO", + "mounting": "onboard", + "pinLocked": true, + "pins": { + "SDIO": [ + { + "role": "clk", + "gpio": 24 + }, + { + "role": "cmd", + "gpio": 25 + }, + { + "role": "d0", + "gpio": 20 + }, + { + "role": "d1", + "gpio": 21 + }, + { + "role": "d2", + "gpio": 22 + }, + { + "role": "d3", + "gpio": 23 + } + ] + }, + "kconfig": null, + "note": { + "en": "4-bit SDIO microSD slot. GPIO39 controls the card power/switch circuit and is a strapping pin; keep its boot-state requirements intact.", + "zh-CN": "4 位 SDIO microSD 卡槽。GPIO39 用于卡电源/开关电路且为启动配置引脚,使用时须保持其启动状态要求。" + } + } + ] + }, + "peripheralGroups": { + "i2c-bus-0": { + "name": { + "en": "Shared I2C0 Bus", + "zh-CN": "共享 I2C0 总线" + }, + "note": { + "en": "I2C0 (SCL=GPIO1, SDA=GPIO0) is shared by the ES8389 codec, GT1151 touch controller, and OV3660 camera control interface.", + "zh-CN": "I2C0(SCL=GPIO1,SDA=GPIO0)由 ES8389 编解码器、GT1151 触控控制器及 OV3660 摄像头控制接口共用。" + } + } + }, + "links": { + "schematic": "https://dl.espressif.com/schematics/esp32-s31-korvo-1-schematics.pdf", + "datasheet": { + "en": "https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s31/esp32-s31-korvo-1/user_guide.html", + "zh-CN": "https://docs.espressif.com/projects/esp-dev-kits/zh_CN/latest/esp32s31/esp32-s31-korvo-1/user_guide.html" + }, + "productPage": { + "en": "https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s31/esp32-s31-korvo-1/user_guide.html", + "zh-CN": "https://docs.espressif.com/projects/esp-dev-kits/zh_CN/latest/esp32s31/esp32-s31-korvo-1/user_guide.html" + } + }, + "boardSymbol": "ESP32S31_KORVO_1", + "sdkRequirements": [ + { + "sdk": "tuyaopen", + "branch": "develop" + } + ], + "source": { + "repo": "https://github.com/tuya/TuyaOpen/tree/develop/boards/ESP32/ESP32S31_KORVO_1" + } +} diff --git a/boards-and-chips/images/espressif-esp32-s31/espressif-esp32-s31.jpg b/boards-and-chips/images/espressif-esp32-s31/espressif-esp32-s31.jpg new file mode 100644 index 00000000..e118c3d9 Binary files /dev/null and b/boards-and-chips/images/espressif-esp32-s31/espressif-esp32-s31.jpg differ diff --git a/boards-and-chips/images/espressif-esp32s31-korvo-1/espressif-esp32s31-korvo-1.jpg b/boards-and-chips/images/espressif-esp32s31-korvo-1/espressif-esp32s31-korvo-1.jpg new file mode 100644 index 00000000..fa65f438 Binary files /dev/null and b/boards-and-chips/images/espressif-esp32s31-korvo-1/espressif-esp32s31-korvo-1.jpg differ diff --git a/boards-and-chips/index.json b/boards-and-chips/index.json index 24358982..f90a7e9a 100644 --- a/boards-and-chips/index.json +++ b/boards-and-chips/index.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "domain": "boardsAndChips", - "publishedAt": "2026-08-26T08:43:31Z", + "publishedAt": "2026-09-14T07:29:14Z", "items": [ { "id": "tuya-t5-e1", @@ -564,6 +564,75 @@ "url": "images/espressif-esp32-s3/espressif-esp32-s3.jpg" } }, + { + "id": "espressif-esp32-s31", + "name": { + "en": "ESP32-S31-WROOM-3", + "zh-CN": "ESP32-S31-WROOM-3 模组" + }, + "manufacturer": { + "en": "Espressif", + "zh-CN": "乐鑫" + }, + "platformId": "esp32", + "variantId": "esp32s31", + "summary": { + "en": "ESP32-S31-WROOM-3 module target — dual-core RISC-V with Wi-Fi 6, Bluetooth 5.4, and IEEE 802.15.4 (Zigbee / Thread). TuyaOpen's default target uses 16MB Flash and 16MB PSRAM.", + "zh-CN": "ESP32-S31-WROOM-3 模组目标 — 双核 RISC-V,支持 Wi-Fi 6、蓝牙 5.4 及 IEEE 802.15.4(Zigbee / Thread)。TuyaOpen 默认目标使用 16MB Flash 与 16MB PSRAM。" + }, + "tags": [ + "wifi", + "ble", + "zigbee", + "thread", + "ai", + "module" + ], + "detailUrl": "boards-and-chips/esp32/espressif-esp32-s31.json", + "published": true, + "sdks": [ + "tuyaopen" + ], + "image": { + "url": "images/espressif-esp32-s31/espressif-esp32-s31.jpg" + } + }, + { + "id": "espressif-esp32s31-korvo-1", + "name": { + "en": "ESP32-S31-Korvo-1", + "zh-CN": "ESP32-S31-Korvo-1 开发板" + }, + "manufacturer": { + "en": "Espressif", + "zh-CN": "乐鑫" + }, + "platformId": "esp32", + "variantId": "esp32s31", + "summary": { + "en": "ESP32-S31-Korvo-1 AIoT development board with 16MB Flash, 16MB PSRAM, ES8389 audio, 4.3-inch RGB display and touch, OV3660 camera, addressable RGB LED, buttons, and microSD.", + "zh-CN": "ESP32-S31-Korvo-1 AIoT 开发板,配备 16MB Flash、16MB PSRAM、ES8389 音频、4.3 寸 RGB 显示屏与触控、OV3660 摄像头、可寻址 RGB LED、按键及 microSD 卡槽。" + }, + "tags": [ + "wifi", + "ble", + "zigbee", + "thread", + "ai", + "audio", + "display", + "camera", + "development" + ], + "detailUrl": "boards-and-chips/esp32/espressif-esp32s31-korvo-1.json", + "published": true, + "sdks": [ + "tuyaopen" + ], + "image": { + "url": "images/espressif-esp32s31-korvo-1/espressif-esp32s31-korvo-1.jpg" + } + }, { "id": "seeed-xiao-esp32s3-sense", "name": { @@ -1921,4 +1990,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/demos/app/tuya-ai-your-chat-bot.json b/demos/app/tuya-ai-your-chat-bot.json index b7a2bafd..8538ac6c 100644 --- a/demos/app/tuya-ai-your-chat-bot.json +++ b/demos/app/tuya-ai-your-chat-bot.json @@ -308,6 +308,26 @@ ] } ] + }, + { + "board": "espressif-esp32s31-korvo-1", + "options": [ + { + "file": "ESP32S31_KORVO_1.config", + "name": { + "en": "ESP32-S31-Korvo-1", + "zh-CN": "ESP32-S31-Korvo-1 开发板" + }, + "peripherals": [ + "audio", + "display", + "touchpad", + "camera", + "led-pixel", + "button" + ] + } + ] } ], "documentation": { diff --git a/demos/example/graphics-lvgl-demo.json b/demos/example/graphics-lvgl-demo.json index 6f60c2cd..07785dcc 100644 --- a/demos/example/graphics-lvgl-demo.json +++ b/demos/example/graphics-lvgl-demo.json @@ -169,6 +169,22 @@ ] } ] + }, + { + "board": "espressif-esp32s31-korvo-1", + "options": [ + { + "file": "ESP32S31_KORVO_1.config", + "name": { + "en": "ESP32-S31-Korvo-1", + "zh-CN": "ESP32-S31-Korvo-1 开发板" + }, + "peripherals": [ + "display", + "touchpad" + ] + } + ] } ], "documentation": { diff --git a/demos/example/peripherals-audio-codecs.json b/demos/example/peripherals-audio-codecs.json index e48f6672..a9cd8f86 100644 --- a/demos/example/peripherals-audio-codecs.json +++ b/demos/example/peripherals-audio-codecs.json @@ -85,6 +85,21 @@ } } ] + }, + { + "board": "espressif-esp32s31-korvo-1", + "options": [ + { + "file": "ESP32S31_KORVO_1.config", + "name": { + "en": "ESP32-S31-Korvo-1", + "zh-CN": "ESP32-S31-Korvo-1 开发板" + }, + "peripherals": [ + "audio" + ] + } + ] } ], "drivers": [ diff --git a/demos/example/peripherals-button.json b/demos/example/peripherals-button.json index d60e2d3f..f198baed 100644 --- a/demos/example/peripherals-button.json +++ b/demos/example/peripherals-button.json @@ -121,6 +121,21 @@ } } ] + }, + { + "board": "espressif-esp32s31-korvo-1", + "options": [ + { + "file": "ESP32S31_KORVO_1.config", + "name": { + "en": "ESP32-S31-Korvo-1", + "zh-CN": "ESP32-S31-Korvo-1 开发板" + }, + "peripherals": [ + "button" + ] + } + ] } ], "drivers": [ diff --git a/demos/example/peripherals-camera-output-display.json b/demos/example/peripherals-camera-output-display.json index 47ee4849..49f5cc91 100644 --- a/demos/example/peripherals-camera-output-display.json +++ b/demos/example/peripherals-camera-output-display.json @@ -38,6 +38,22 @@ } } ] + }, + { + "board": "espressif-esp32s31-korvo-1", + "options": [ + { + "file": "ESP32S31_KORVO_1.config", + "name": { + "en": "ESP32-S31-Korvo-1", + "zh-CN": "ESP32-S31-Korvo-1 开发板" + }, + "peripherals": [ + "camera", + "display" + ] + } + ] } ], "documentation": { diff --git a/demos/example/peripherals-display-fill-color.json b/demos/example/peripherals-display-fill-color.json index 8391c6c4..27b7ebc4 100644 --- a/demos/example/peripherals-display-fill-color.json +++ b/demos/example/peripherals-display-fill-color.json @@ -105,6 +105,21 @@ } } ] + }, + { + "board": "espressif-esp32s31-korvo-1", + "options": [ + { + "file": "ESP32S31_KORVO_1.config", + "name": { + "en": "ESP32-S31-Korvo-1", + "zh-CN": "ESP32-S31-Korvo-1 开发板" + }, + "peripherals": [ + "display" + ] + } + ] } ], "drivers": [ diff --git a/demos/example/peripherals-display-picture.json b/demos/example/peripherals-display-picture.json index d655bb12..0c7ed3a7 100644 --- a/demos/example/peripherals-display-picture.json +++ b/demos/example/peripherals-display-picture.json @@ -105,6 +105,21 @@ } } ] + }, + { + "board": "espressif-esp32s31-korvo-1", + "options": [ + { + "file": "ESP32S31_KORVO_1.config", + "name": { + "en": "ESP32-S31-Korvo-1", + "zh-CN": "ESP32-S31-Korvo-1 开发板" + }, + "peripherals": [ + "display" + ] + } + ] } ], "drivers": [ diff --git a/demos/example/peripherals-led.json b/demos/example/peripherals-led.json index a92e7475..02b50b5c 100644 --- a/demos/example/peripherals-led.json +++ b/demos/example/peripherals-led.json @@ -109,6 +109,21 @@ } } ] + }, + { + "board": "espressif-esp32s31-korvo-1", + "options": [ + { + "file": "ESP32S31_KORVO_1.config", + "name": { + "en": "ESP32-S31-Korvo-1", + "zh-CN": "ESP32-S31-Korvo-1 开发板" + }, + "peripherals": [ + "led-pixel" + ] + } + ] } ], "drivers": [ diff --git a/demos/example/peripherals-tp.json b/demos/example/peripherals-tp.json index 73c215e5..bdece0e6 100644 --- a/demos/example/peripherals-tp.json +++ b/demos/example/peripherals-tp.json @@ -1,6 +1,23 @@ { "id": "peripherals-tp", "source": "https://github.com/tuya/TuyaOpen/tree/master/examples/peripherals/tp", + "configs": [ + { + "board": "espressif-esp32s31-korvo-1", + "options": [ + { + "file": "ESP32S31_KORVO_1.config", + "name": { + "en": "ESP32-S31-Korvo-1", + "zh-CN": "ESP32-S31-Korvo-1 开发板" + }, + "peripherals": [ + "touchpad" + ] + } + ] + } + ], "drivers": [ { "driver": "touch", diff --git a/demos/index.json b/demos/index.json index 3b88d046..d7d74248 100644 --- a/demos/index.json +++ b/demos/index.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "domain": "demos", - "publishedAt": "2026-08-13T00:00:00Z", + "publishedAt": "2026-09-14T07:29:14Z", "items": [ { "id": "ble-ble-central", @@ -233,7 +233,8 @@ "atk-dnesp32s3-box2-wifi", "waveshare-esp32p4c6-touch-lcd-4-3", "waveshare-esp32s3-touch-amoled-1-8", - "zectrix-t5ai-note-4" + "zectrix-t5ai-note-4", + "espressif-esp32s31-korvo-1" ], "compatibilityType": "board-specific", "detailUrl": "demos/example/graphics-lvgl-demo.json", @@ -616,7 +617,8 @@ "esp32s3-bread-compact-wifi", "tuya-t5ai-board", "tuya-t5ai-pocket", - "waveshare-esp32s3-touch-amoled-1-8" + "waveshare-esp32s3-touch-amoled-1-8", + "espressif-esp32s31-korvo-1" ], "compatibilityType": "board-specific", "detailUrl": "demos/example/peripherals-audio-codecs.json", @@ -649,7 +651,8 @@ "tuya-t5ai-evb", "tuya-t5ai-pocket", "waveshare-esp32s3-touch-amoled-1-8", - "zectrix-t5ai-note-4" + "zectrix-t5ai-note-4", + "espressif-esp32s31-korvo-1" ], "compatibilityType": "board-specific", "detailUrl": "demos/example/peripherals-button.json", @@ -675,7 +678,8 @@ "boards": [ "atk-dnesp32s3", "tuya-t5ai-board", - "tuya-t5ai-pocket" + "tuya-t5ai-pocket", + "espressif-esp32s31-korvo-1" ], "compatibilityType": "board-specific", "detailUrl": "demos/example/peripherals-camera-output-display.json", @@ -731,7 +735,8 @@ "tuya-t5ai-pocket", "waveshare-esp32p4c6-touch-lcd-4-3", "waveshare-esp32s3-touch-amoled-1-8", - "waveshare-t5ai-touch-amoled-175" + "waveshare-t5ai-touch-amoled-175", + "espressif-esp32s31-korvo-1" ], "compatibilityType": "board-specific", "detailUrl": "demos/example/peripherals-display-fill-color.json", @@ -762,7 +767,8 @@ "waveshare-esp32p4c6-touch-lcd-4-3", "waveshare-esp32s3-touch-amoled-1-8", "waveshare-t5ai-touch-amoled-175", - "zectrix-t5ai-note-4" + "zectrix-t5ai-note-4", + "espressif-esp32s31-korvo-1" ], "compatibilityType": "board-specific", "detailUrl": "demos/example/peripherals-display-picture.json", @@ -975,7 +981,8 @@ "tuya-t5ai-evb", "tuya-t5ai-pocket", "waveshare-esp32c6-dev-kit-n16", - "zectrix-t5ai-note-4" + "zectrix-t5ai-note-4", + "espressif-esp32s31-korvo-1" ], "compatibilityType": "board-specific", "detailUrl": "demos/example/peripherals-led.json", @@ -1143,7 +1150,9 @@ "tags": [ "peripheral" ], - "boards": [], + "boards": [ + "espressif-esp32s31-korvo-1" + ], "compatibilityType": "board-specific", "detailUrl": "demos/example/peripherals-tp.json", "publish": true, @@ -1542,7 +1551,8 @@ "sparkleiot-t5ai-dev", "brd2605a", "siwx917-ai-dev-kit", - "seeed-xiao-esp32s3-sense" + "seeed-xiao-esp32s3-sense", + "espressif-esp32s31-korvo-1" ], "compatibilityType": "board-specific", "detailUrl": "demos/app/tuya-ai-your-chat-bot.json", diff --git a/platforms/esp32/esp32s31.json b/platforms/esp32/esp32s31.json new file mode 100644 index 00000000..fb7423c4 --- /dev/null +++ b/platforms/esp32/esp32s31.json @@ -0,0 +1,1929 @@ +{ + "schemaVersion": 1, + "name": "Espressif ESP32-S31", + "arch": "risc-v", + "flashInterface": "qspi", + "connectivity": { + "wifi": true, + "ble": true, + "ethernet": true, + "cellular": false + }, + "memory": { + "sramBytes": 524288, + "lpSramBytes": 32768, + "romBytes": 327680, + "flashMaxBytes": 33554432, + "psramMaxBytes": 33554432, + "efuse": true + }, + "peripherals": { + "wifi": { + "spec": { + "standard": "802.11b/g/n/ax", + "bands": [ + "2.4GHz" + ], + "security": [ + "WPA", + "WPA2", + "WPA3-Personal" + ], + "modes": [ + "STA", + "AP", + "Direct" + ] + } + }, + "ble": { + "spec": { + "version": "5.4" + } + }, + "gpio": { + "enabled": true, + "count": 60, + "spec": { + "pins": [ + [ + 0, + 28 + ], + [ + 30, + 40 + ], + [ + 42, + 61 + ] + ], + "direction": [ + "TUYA_GPIO_INPUT", + "TUYA_GPIO_OUTPUT" + ], + "mode": [ + "TUYA_GPIO_PUSH_PULL", + "TUYA_GPIO_PULLUP", + "TUYA_GPIO_PULLDOWN", + "TUYA_GPIO_FLOATING", + "TUYA_GPIO_OPENDRAIN", + "TUYA_GPIO_OPENDRAIN_PULLUP" + ], + "irq": { + "supported": true, + "triggers": [ + "TUYA_GPIO_IRQ_RISE", + "TUYA_GPIO_IRQ_FALL", + "TUYA_GPIO_IRQ_RISE_FALL", + "TUYA_GPIO_IRQ_LOW", + "TUYA_GPIO_IRQ_HIGH" + ], + "pins": [ + [ + 0, + 28 + ], + [ + 30, + 40 + ], + [ + 42, + 61 + ] + ] + }, + "note": "GPIO29 and GPIO41 do not exist. GPIO0–7 are LP-capable; GPIO8–28, GPIO30–40, and GPIO42–61 are HP-only." + } + }, + "uart": { + "enabled": true, + "count": 5, + "spec": { + "baudrate": { + "min": 300, + "max": 5000000 + }, + "databits": [ + "TUYA_UART_DATA_LEN_5BIT", + "TUYA_UART_DATA_LEN_6BIT", + "TUYA_UART_DATA_LEN_7BIT", + "TUYA_UART_DATA_LEN_8BIT" + ], + "stopbits": [ + "TUYA_UART_STOP_LEN_1BIT", + "TUYA_UART_STOP_LEN_1_5BIT1", + "TUYA_UART_STOP_LEN_2BIT" + ], + "parity": [ + "TUYA_UART_PARITY_TYPE_NONE", + "TUYA_UART_PARITY_TYPE_ODD", + "TUYA_UART_PARITY_TYPE_EVEN" + ], + "flowctrl": [ + "TUYA_UART_FLOWCTRL_NONE", + "TUYA_UART_FLOWCTRL_RTSCTS" + ], + "ports": [ + { + "id": 0, + "role": "log", + "irq": { + "rx": true, + "tx": true + }, + "pinGroups": [ + { + "tx": 58, + "rx": 59 + } + ], + "routable": true, + "candidates": [ + [8, 25], + [33, 40], + [42, 61] + ], + "note": "UART0 IO-MUX defaults; GPIO58/59 are also the recommended console pins." + }, + { + "id": 1, + "role": "general", + "irq": { + "rx": true, + "tx": true + }, + "pinGroups": [ + { + "tx": 8, + "rx": 9 + } + ], + "routable": true, + "candidates": [ + [8, 25], + [33, 40], + [42, 61] + ] + }, + { + "id": 2, + "role": "general", + "irq": { + "rx": true, + "tx": true + }, + "pinGroups": [ + { + "tx": 10, + "rx": 11 + } + ], + "routable": true, + "candidates": [ + [8, 25], + [33, 40], + [42, 61] + ] + }, + { + "id": 3, + "role": "general", + "irq": { + "rx": true, + "tx": true + }, + "pinGroups": [ + { + "tx": 12, + "rx": 13 + } + ], + "routable": true, + "candidates": [ + [8, 25], + [33, 40], + [42, 61] + ] + }, + { + "id": 4, + "role": "low-power", + "lowPower": true, + "irq": { + "rx": true, + "tx": true + }, + "pinGroups": [ + { + "tx": 6, + "rx": 7, + "rts": 4, + "cts": 5 + } + ], + "routable": true, + "candidates": [ + [0, 7] + ] + } + ] + } + }, + "i2c": { + "enabled": true, + "count": 3, + "spec": { + "role": [ + "TUYA_IIC_MODE_MASTER", + "TUYA_IIC_MODE_SLAVE" + ], + "speed": [ + "TUYA_IIC_BUS_SPEED_100K", + "TUYA_IIC_BUS_SPEED_400K" + ], + "addrWidth": [ + "TUYA_IIC_ADDRESS_7BIT", + "TUYA_IIC_ADDRESS_10BIT" + ], + "ports": [ + { + "id": 0, + "type": [ + "hw", + "sw" + ], + "irq": false, + "pinGroups": [ + { + "scl": 8, + "sda": 9 + } + ], + "routable": true, + "candidates": [ + [8, 25], + [33, 40], + [42, 61] + ] + }, + { + "id": 1, + "type": [ + "hw", + "sw" + ], + "irq": false, + "pinGroups": [ + { + "scl": 10, + "sda": 11 + } + ], + "routable": true, + "candidates": [ + [8, 25], + [33, 40], + [42, 61] + ] + }, + { + "id": 2, + "type": [ + "hw" + ], + "lowPower": true, + "irq": false, + "pinGroups": [ + { + "scl": 6, + "sda": 7 + } + ], + "routable": true, + "candidates": [ + [0, 7] + ], + "note": "LP I2C is master-only; it cannot use LP UART hardware flow control concurrently." + } + ] + } + }, + "spi": { + "enabled": true, + "count": 3, + "spec": { + "role": [ + "TUYA_SPI_ROLE_MASTER", + "TUYA_SPI_ROLE_SLAVE", + "TUYA_SPI_ROLE_MASTER_SIMPLEX", + "TUYA_SPI_ROLE_SLAVE_SIMPLEX" + ], + "mode": [ + "TUYA_SPI_MODE0", + "TUYA_SPI_MODE1", + "TUYA_SPI_MODE2", + "TUYA_SPI_MODE3" + ], + "csType": [ + "TUYA_SPI_AUTO_TYPE", + "TUYA_SPI_SOFT_TYPE", + "TUYA_SPI_SOFT_ONE_WIRE_TYPE" + ], + "databits": [ + "TUYA_SPI_DATA_BIT8", + "TUYA_SPI_DATA_BIT16" + ], + "bitorder": [ + "TUYA_SPI_ORDER_MSB2LSB", + "TUYA_SPI_ORDER_LSB2MSB" + ], + "ports": [ + { + "id": 0, + "backend": "spi", + "irq": true, + "dma": true, + "freq": { + "min": 100000, + "max": 80000000 + }, + "pinGroups": [ + { + "clk": 12, + "cs": 10, + "mosi": 11, + "miso": 13 + } + ], + "routable": true, + "candidates": [ + [8, 25], + [33, 40], + [42, 61] + ], + "note": "GP-SPI2 IO-MUX defaults; octal/OPI is available in master mode." + }, + { + "id": 1, + "backend": "spi", + "irq": true, + "dma": true, + "freq": { + "min": 100000, + "max": 80000000 + }, + "pinGroups": [ + { + "clk": 15, + "cs": 16, + "mosi": 17, + "miso": 18 + } + ], + "routable": true, + "candidates": [ + [8, 25], + [33, 40], + [42, 61] + ] + }, + { + "id": 2, + "backend": "spi", + "lowPower": true, + "irq": true, + "dma": false, + "freq": { + "min": 100000, + "max": 40000000 + }, + "pinGroups": [ + { + "clk": 2, + "cs": 3, + "mosi": 4, + "miso": 5 + } + ], + "routable": true, + "candidates": [ + [0, 7] + ] + } + ] + } + }, + "pwm": { + "enabled": true, + "count": 16, + "spec": { + "routable": true, + "polarity": [ + "TUYA_PWM_NEGATIVE", + "TUYA_PWM_POSITIVE" + ], + "countMode": [ + "TUYA_PWM_CNT_UP", + "TUYA_PWM_CNT_UP_AND_DOWN" + ], + "duty": { + "min": 0, + "max": 10000 + }, + "freq": { + "min": 1, + "max": 40000000 + }, + "capture": { + "supported": false + }, + "channels": [ + { + "id": 0, + "pin": 8, + "irq": false + }, + { + "id": 1, + "pin": 9, + "irq": false + }, + { + "id": 2, + "pin": 10, + "irq": false + }, + { + "id": 3, + "pin": 11, + "irq": false + }, + { + "id": 4, + "pin": 12, + "irq": false + }, + { + "id": 5, + "pin": 13, + "irq": false + }, + { + "id": 6, + "pin": 14, + "irq": false + }, + { + "id": 7, + "pin": 15, + "irq": false + }, + { + "id": 8, + "pin": 16, + "irq": false + }, + { + "id": 9, + "pin": 17, + "irq": false + }, + { + "id": 10, + "pin": 18, + "irq": false + }, + { + "id": 11, + "pin": 19, + "irq": false + }, + { + "id": 12, + "pin": 42, + "irq": false + }, + { + "id": 13, + "pin": 43, + "irq": false + }, + { + "id": 14, + "pin": 44, + "irq": false + }, + { + "id": 15, + "pin": 45, + "irq": false + } + ] + } + }, + "adc": { + "enabled": true, + "count": 2, + "spec": { + "bits": 12, + "mode": [ + "TUYA_ADC_SINGLE", + "TUYA_ADC_CONTINUOUS" + ], + "ports": [ + { + "id": 1, + "routable": false, + "channels": [ + { + "id": 0, + "pin": 42, + "signal": "ADC1_CH0_N" + }, + { + "id": 1, + "pin": 43, + "signal": "ADC1_CH0_P" + }, + { + "id": 2, + "pin": 44, + "signal": "ADC1_CH1_N" + }, + { + "id": 3, + "pin": 45, + "signal": "ADC1_CH1_P" + }, + { + "id": 4, + "pin": 46, + "signal": "ADC1_CH2_N" + }, + { + "id": 5, + "pin": 47, + "signal": "ADC1_CH2_P" + }, + { + "id": 6, + "pin": 48, + "signal": "ADC1_CH3_N" + }, + { + "id": 7, + "pin": 49, + "signal": "ADC1_CH3_P" + } + ] + }, + { + "id": 2, + "routable": false, + "channels": [ + { + "id": 0, + "pin": 50, + "signal": "ADC2_CH0_N" + }, + { + "id": 1, + "pin": 51, + "signal": "ADC2_CH0_P" + }, + { + "id": 2, + "pin": 52, + "signal": "ADC2_CH1_N" + }, + { + "id": 3, + "pin": 53, + "signal": "ADC2_CH1_P" + }, + { + "id": 4, + "pin": 54, + "signal": "ADC2_CH2_N" + }, + { + "id": 5, + "pin": 55, + "signal": "ADC2_CH2_P" + }, + { + "id": 6, + "pin": 56, + "signal": "ADC2_CH3_N" + }, + { + "id": 7, + "pin": 57, + "signal": "ADC2_CH3_P" + } + ] + } + ] + } + }, + "timer": { + "enabled": true, + "count": 4, + "spec": { + "bits": 54, + "mode": [ + "TUYA_TIMER_MODE_ONCE", + "TUYA_TIMER_MODE_PERIOD" + ], + "ids": [ + 0, + 1, + 2, + 3 + ] + } + }, + "wdt": { + "enabled": true, + "count": 3 + }, + "rtc": { + "enabled": true, + "count": 1 + }, + "flash": { + "enabled": true, + "count": 1, + "spec": { + "interface": [ + "SPI", + "Dual SPI", + "Quad SPI", + "Octal SPI", + "QPI", + "OPI" + ], + "partitionMap": [ + { + "type": "TUYA_FLASH_TYPE_APP", + "startAddr": "0x010000", + "endAddr": "0x110000", + "size": 1048576, + "blockSize": 4096, + "desc": "Default application partition" + }, + { + "type": "TUYA_FLASH_TYPE_OTA", + "startAddr": "0x110000", + "endAddr": "0x210000", + "size": 1048576, + "blockSize": 4096, + "desc": "Default OTA application partition" + }, + { + "type": "TUYA_FLASH_TYPE_KV_DATA", + "startAddr": "0x210000", + "endAddr": "0x220000", + "size": 65536, + "blockSize": 4096, + "desc": "Default key-value data partition" + }, + { + "type": "TUYA_FLASH_TYPE_USER0", + "startAddr": "0x220000", + "endAddr": "0x230000", + "size": 65536, + "blockSize": 4096, + "desc": "Default user-data partition" + } + ], + "note": "The partition map is a 4 MB default layout. Off-package flash is required. GPIO26–28 and GPIO30–32 are SPI0/1 flash pins and should not be repurposed." + } + }, + "pinmux": { + "enabled": true, + "count": 1, + "spec": { + "remappableFuncs": { + "i2c": [ + "TUYA_IIC0_SCL", + "TUYA_IIC0_SDA", + "TUYA_IIC1_SCL", + "TUYA_IIC1_SDA" + ], + "uart": [ + "TUYA_UART0_TX", + "TUYA_UART0_RX", + "TUYA_UART1_TX", + "TUYA_UART1_RX", + "TUYA_UART2_TX", + "TUYA_UART2_RX", + "TUYA_UART3_TX", + "TUYA_UART3_RX" + ], + "spi": [ + "TUYA_SPI0_MISO", + "TUYA_SPI0_MOSI", + "TUYA_SPI0_CLK", + "TUYA_SPI0_CS", + "TUYA_SPI1_MISO", + "TUYA_SPI1_MOSI", + "TUYA_SPI1_CLK", + "TUYA_SPI1_CS" + ], + "i2s": [ + "TUYA_I2S0_SCK", + "TUYA_I2S0_WS", + "TUYA_I2S0_SDO_0", + "TUYA_I2S0_SDI_0" + ] + } + } + }, + "dma2d": { + "enabled": true, + "count": 1, + "spec": { + "formats": [ + "TUYA_FRAME_FMT_YUV422", + "TUYA_FRAME_FMT_RGB565", + "TUYA_FRAME_FMT_RGB888" + ] + } + }, + "rgb": { + "enabled": true, + "count": 1, + "spec": { + "pixelFmt": [ + "TUYA_PIXEL_FMT_RGB565", + "TUYA_PIXEL_FMT_RGB666", + "TUYA_PIXEL_FMT_RGB888" + ], + "outDataClkEdge": [ + "TUYA_RGB_DATA_IN_FALLING_EDGE", + "TUYA_RGB_DATA_IN_RISING_EDGE" + ], + "dclkFreq": { + "min": 8000000, + "max": 80000000 + }, + "ports": [ + { + "id": 0, + "routable": true, + "pinGroups": [ + { + "dclk": 40, + "de": 43, + "hsync": 44, + "vsync": 45, + "r": [ + 8, + 9, + 10, + 11, + 12 + ], + "g": [ + 13, + 14, + 15, + 16, + 17, + 18 + ], + "b": [ + 19, + 33, + 34, + 35, + 36 + ] + } + ] + } + ] + } + }, + "dvp": { + "enabled": true, + "count": 1, + "spec": { + "syncMode": [ + "TUYA_DVP_SYNC_MODE_0", + "TUYA_DVP_SYNC_MODE_1", + "TUYA_DVP_SYNC_MODE_2", + "TUYA_DVP_SYNC_MODE_3" + ], + "outputMode": [ + "TUYA_CAMERA_OUTPUT_YUV422", + "TUYA_CAMERA_OUTPUT_JPEG" + ], + "mclkFreq": { + "min": 10000000, + "max": 40000000 + }, + "ports": [ + { + "id": 0, + "routable": true, + "pinGroups": [ + { + "mclk": 55, + "pclk": 54, + "hsync": 57, + "vsync": 56, + "data": [ + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53 + ] + } + ] + } + ] + } + }, + "kws": { + "count": 1, + "spec": { + "wakeupWords": [ + "TKL_KWS_WAKEUP_NIHAO_XIAOZHI" + ] + }, + "enabled": true + }, + "vad": { + "count": 1, + "enabled": true + } + }, + "platformSymbol": "ESP32", + "pinout": [ + { + "pin": 1, + "name": "ANT", + "gpio": null, + "type": "A", + "functions": [], + "caps": [ + "ANT" + ], + "note": "RF antenna connection" + }, + { + "pin": 2, + "name": "VDDA3", + "gpio": null, + "type": "P", + "functions": [], + "caps": [ + "VDDA3" + ] + }, + { + "pin": 3, + "name": "VDDA4", + "gpio": null, + "type": "P", + "functions": [], + "caps": [ + "VDDA4" + ] + }, + { + "pin": 4, + "name": "CHIP_PU", + "gpio": null, + "type": "A", + "functions": [], + "caps": [ + "CHIP_PU" + ], + "note": "Chip enable and active-high reset" + }, + { + "pin": 5, + "name": "GPIO0", + "gpio": 0, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "XTAL_32K_N", + "LP_GPIO0" + ], + "note": "LP-capable; alternate 32 kHz crystal negative pin" + }, + { + "pin": 6, + "name": "GPIO1", + "gpio": 1, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "XTAL_32K_P", + "LP_GPIO1" + ], + "note": "LP-capable; alternate 32 kHz crystal positive pin" + }, + { + "pin": 7, + "name": "GPIO2", + "gpio": 2, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "LP_GPIO2", + "LP_UART_DTRN", + "LP_SPI_CLK" + ] + }, + { + "pin": 8, + "name": "GPIO3", + "gpio": 3, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "LP_GPIO3", + "LP_UART_DSRN", + "LP_SPI_CS" + ] + }, + { + "pin": 9, + "name": "GPIO4", + "gpio": 4, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "LP_GPIO4", + "LP_UART_RTSN", + "LP_SPI_MOSI" + ] + }, + { + "pin": 10, + "name": "GPIO5", + "gpio": 5, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "LP_GPIO5", + "LP_UART_CTSN", + "LP_SPI_MISO" + ] + }, + { + "pin": 11, + "name": "VDDPST_1", + "gpio": null, + "type": "P", + "functions": [], + "caps": [ + "VDDPST_1" + ] + }, + { + "pin": 12, + "name": "GPIO6", + "gpio": 6, + "type": "I/O", + "functions": [ + "GPIO", + "TOUCH0" + ], + "caps": [ + "LP_GPIO6", + "LP_UART_TXD", + "LP_I2C_SCL" + ] + }, + { + "pin": 13, + "name": "GPIO7", + "gpio": 7, + "type": "I/O", + "functions": [ + "GPIO", + "TOUCH1" + ], + "caps": [ + "LP_GPIO7", + "LP_UART_RXD", + "LP_I2C_SDA" + ] + }, + { + "pin": 14, + "name": "GPIO8", + "gpio": 8, + "type": "I/O", + "functions": [ + "GPIO", + "TOUCH2" + ], + "caps": [ + "EMAC_RGMII_TXD0", + "LCD_DATA0" + ] + }, + { + "pin": 15, + "name": "GPIO9", + "gpio": 9, + "type": "I/O", + "functions": [ + "GPIO", + "TOUCH3", + "SPI2_HOLD" + ], + "caps": [ + "EMAC_RGMII_TXD1", + "LCD_DATA1" + ] + }, + { + "pin": 16, + "name": "GPIO10", + "gpio": 10, + "type": "I/O", + "functions": [ + "GPIO", + "TOUCH4", + "SPI2_CS" + ], + "caps": [ + "EMAC_RGMII_TXD2", + "LCD_DATA2" + ] + }, + { + "pin": 17, + "name": "GPIO11", + "gpio": 11, + "type": "I/O", + "functions": [ + "GPIO", + "TOUCH5", + "SPI2_MOSI" + ], + "caps": [ + "EMAC_RGMII_TXD3", + "LCD_DATA3" + ] + }, + { + "pin": 18, + "name": "VREF_TOUCH", + "gpio": null, + "type": "A", + "functions": [], + "caps": [ + "VREF_TOUCH" + ] + }, + { + "pin": 19, + "name": "GPIO12", + "gpio": 12, + "type": "I/O", + "functions": [ + "GPIO", + "TOUCH6", + "SPI2_CLK" + ], + "caps": [ + "EMAC_RGMII_TXEN", + "LCD_DATA4" + ] + }, + { + "pin": 20, + "name": "GPIO13", + "gpio": 13, + "type": "I/O", + "functions": [ + "GPIO", + "TOUCH7", + "SPI2_MISO" + ], + "caps": [ + "EMAC_RMII_CLK", + "LCD_DATA5" + ] + }, + { + "pin": 21, + "name": "GPIO14", + "gpio": 14, + "type": "I/O", + "functions": [ + "GPIO", + "TOUCH8", + "SPI2_WP" + ], + "caps": [ + "EMAC_RGMII_RXCLK", + "LCD_DATA6" + ] + }, + { + "pin": 22, + "name": "GPIO15", + "gpio": 15, + "type": "I/O", + "functions": [ + "GPIO", + "TOUCH9" + ], + "caps": [ + "SPI2_IO4", + "EMAC_RGMII_RXDV", + "LCD_DATA7" + ] + }, + { + "pin": 23, + "name": "GPIO16", + "gpio": 16, + "type": "I/O", + "functions": [ + "GPIO", + "TOUCH10" + ], + "caps": [ + "SPI2_IO5", + "EMAC_RGMII_RXD3", + "LCD_DATA8" + ] + }, + { + "pin": 24, + "name": "GPIO17", + "gpio": 17, + "type": "I/O", + "functions": [ + "GPIO", + "TOUCH11" + ], + "caps": [ + "SPI2_IO6", + "EMAC_RGMII_RXD2", + "LCD_DATA9" + ] + }, + { + "pin": 25, + "name": "GPIO18", + "gpio": 18, + "type": "I/O", + "functions": [ + "GPIO", + "TOUCH12" + ], + "caps": [ + "SPI2_IO7", + "EMAC_RGMII_RXD1", + "LCD_DATA10" + ] + }, + { + "pin": 26, + "name": "GPIO19", + "gpio": 19, + "type": "I/O", + "functions": [ + "GPIO", + "TOUCH13" + ], + "caps": [ + "SPI2_DQS", + "EMAC_RGMII_RXD0", + "LCD_DATA11" + ] + }, + { + "pin": 27, + "name": "GPIO20", + "gpio": 20, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "SDIO_DATA0", + "SPI2_CLK" + ] + }, + { + "pin": 28, + "name": "GPIO21", + "gpio": 21, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "SDIO_DATA1", + "SPI2_MOSI" + ] + }, + { + "pin": 29, + "name": "GPIO22", + "gpio": 22, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "SDIO_DATA2", + "SPI2_MISO" + ] + }, + { + "pin": 30, + "name": "VDD_PSRAM_1P8_1", + "gpio": null, + "type": "P", + "functions": [], + "caps": [ + "VDD_PSRAM_1P8" + ] + }, + { + "pin": 31, + "name": "GPIO23", + "gpio": 23, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "SDIO_DATA3", + "SPI2_CS" + ] + }, + { + "pin": 32, + "name": "GPIO24", + "gpio": 24, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "SDIO_CLK", + "SPI2_HOLD" + ] + }, + { + "pin": 33, + "name": "GPIO25", + "gpio": 25, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "SDIO_CMD", + "SPI2_WP" + ] + }, + { + "pin": 34, + "name": "VDD_PSRAM_1P8_2", + "gpio": null, + "type": "P", + "functions": [], + "caps": [ + "VDD_PSRAM_1P8" + ] + }, + { + "pin": 35, + "name": "VDD_LDO_1P8", + "gpio": null, + "type": "P", + "functions": [], + "caps": [ + "VDD_LDO_1P8" + ] + }, + { + "pin": 36, + "name": "GPIO26", + "gpio": 26, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "SPI0_1_CS" + ], + "note": "Reserved for off-package flash SPI0/1; do not repurpose." + }, + { + "pin": 37, + "name": "GPIO27", + "gpio": 27, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "SPI0_1_Q" + ], + "note": "Reserved for off-package flash SPI0/1; do not repurpose." + }, + { + "pin": 38, + "name": "GPIO28", + "gpio": 28, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "SPI0_1_WP" + ], + "note": "Reserved for off-package flash SPI0/1; do not repurpose." + }, + { + "pin": 39, + "name": "VDD_SPI", + "gpio": null, + "type": "P", + "functions": [], + "caps": [ + "VDD_SPI" + ] + }, + { + "pin": 40, + "name": "GPIO30", + "gpio": 30, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "SPI0_1_HD" + ], + "note": "Reserved for off-package flash SPI0/1; do not repurpose." + }, + { + "pin": 41, + "name": "GPIO31", + "gpio": 31, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "SPI0_1_CLK" + ], + "note": "Reserved for off-package flash SPI0/1; do not repurpose." + }, + { + "pin": 42, + "name": "GPIO32", + "gpio": 32, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "SPI0_1_D" + ], + "note": "Reserved for off-package flash SPI0/1; do not repurpose." + }, + { + "pin": 43, + "name": "VCCA_VDDPST_2", + "gpio": null, + "type": "P", + "functions": [], + "caps": [ + "VCCA", + "VDDPST_2" + ] + }, + { + "pin": 44, + "name": "USB_DP", + "gpio": null, + "type": "I/O", + "functions": [], + "caps": [ + "USB2_OTG_PHY_DP" + ], + "note": "Dedicated USB 2.0 high-speed OTG D+" + }, + { + "pin": 45, + "name": "USB_DM", + "gpio": null, + "type": "I/O", + "functions": [], + "caps": [ + "USB2_OTG_PHY_DM" + ], + "note": "Dedicated USB 2.0 high-speed OTG D-" + }, + { + "pin": 46, + "name": "GPIO33", + "gpio": 33, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "USB_SERIAL_JTAG_DM", + "LCD_DATA12" + ], + "note": "Default USB Serial/JTAG D-; reconfigure before GPIO use." + }, + { + "pin": 47, + "name": "GPIO34", + "gpio": 34, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "USB_SERIAL_JTAG_DP", + "LCD_DATA13" + ], + "note": "Default USB Serial/JTAG D+; reconfigure before GPIO use." + }, + { + "pin": 48, + "name": "GPIO35", + "gpio": 35, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "REF_GMAC_CLK", + "LCD_DATA14", + "SD2_DATA0" + ] + }, + { + "pin": 49, + "name": "GPIO36", + "gpio": 36, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "LCD_DATA15", + "SD2_DATA1", + "STRAP" + ], + "note": "Strapping pin; use with caution." + }, + { + "pin": 50, + "name": "GPIO37", + "gpio": 37, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "PAD_COMP0", + "LCD_DATA16", + "SD2_DATA2", + "STRAP" + ], + "note": "Strapping pin; use with caution." + }, + { + "pin": 51, + "name": "GPIO38", + "gpio": 38, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "PAD_COMP1", + "LCD_DATA17", + "SD2_DATA3" + ] + }, + { + "pin": 52, + "name": "GPIO39", + "gpio": 39, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "PAD_COMP2", + "LCD_DATA18", + "SD2_CLK" + ] + }, + { + "pin": 53, + "name": "GPIO40", + "gpio": 40, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "PAD_COMP3", + "LCD_PCLK", + "SD2_CMD" + ] + }, + { + "pin": 54, + "name": "VDDPST_3", + "gpio": null, + "type": "P", + "functions": [], + "caps": [ + "VDDPST_3" + ] + }, + { + "pin": 55, + "name": "GPIO42", + "gpio": 42, + "type": "I/O", + "functions": [ + "GPIO", + "ADC1_CH0_N" + ], + "caps": [] + }, + { + "pin": 56, + "name": "GPIO43", + "gpio": 43, + "type": "I/O", + "functions": [ + "GPIO", + "ADC1_CH0_P" + ], + "caps": [ + "LCD_H_ENABLE" + ] + }, + { + "pin": 57, + "name": "GPIO44", + "gpio": 44, + "type": "I/O", + "functions": [ + "GPIO", + "ADC1_CH1_N" + ], + "caps": [ + "LCD_H_SYNC" + ] + }, + { + "pin": 58, + "name": "GPIO45", + "gpio": 45, + "type": "I/O", + "functions": [ + "GPIO", + "ADC1_CH1_P" + ], + "caps": [ + "LCD_V_SYNC" + ] + }, + { + "pin": 59, + "name": "GPIO46", + "gpio": 46, + "type": "I/O", + "functions": [ + "GPIO", + "ADC1_CH2_N" + ], + "caps": [ + "CAM_DATA0" + ] + }, + { + "pin": 60, + "name": "GPIO47", + "gpio": 47, + "type": "I/O", + "functions": [ + "GPIO", + "ADC1_CH2_P" + ], + "caps": [ + "CAM_DATA1" + ] + }, + { + "pin": 61, + "name": "GPIO48", + "gpio": 48, + "type": "I/O", + "functions": [ + "GPIO", + "ADC1_CH3_N" + ], + "caps": [ + "CAM_DATA2" + ] + }, + { + "pin": 62, + "name": "GPIO49", + "gpio": 49, + "type": "I/O", + "functions": [ + "GPIO", + "ADC1_CH3_P" + ], + "caps": [ + "CAM_DATA3" + ] + }, + { + "pin": 63, + "name": "VREF_ADC", + "gpio": null, + "type": "A", + "functions": [], + "caps": [ + "VREF_ADC" + ] + }, + { + "pin": 64, + "name": "VDDPST_4", + "gpio": null, + "type": "P", + "functions": [], + "caps": [ + "VDDPST_4" + ] + }, + { + "pin": 65, + "name": "GPIO50", + "gpio": 50, + "type": "I/O", + "functions": [ + "GPIO", + "ADC2_CH0_N" + ], + "caps": [ + "CAM_DATA4" + ] + }, + { + "pin": 66, + "name": "GPIO51", + "gpio": 51, + "type": "I/O", + "functions": [ + "GPIO", + "ADC2_CH0_P" + ], + "caps": [ + "CAM_DATA5" + ] + }, + { + "pin": 67, + "name": "GPIO52", + "gpio": 52, + "type": "I/O", + "functions": [ + "GPIO", + "ADC2_CH1_N" + ], + "caps": [ + "SPI2_CS", + "CAM_DATA6" + ] + }, + { + "pin": 68, + "name": "GPIO53", + "gpio": 53, + "type": "I/O", + "functions": [ + "GPIO", + "ADC2_CH1_P" + ], + "caps": [ + "SPI2_CLK", + "CAM_DATA7" + ] + }, + { + "pin": 69, + "name": "GPIO54", + "gpio": 54, + "type": "I/O", + "functions": [ + "GPIO", + "ADC2_CH2_N" + ], + "caps": [ + "MTDO", + "SPI2_MOSI", + "CAM_PCLK" + ], + "note": "Default JTAG MTDO; use with caution." + }, + { + "pin": 70, + "name": "GPIO55", + "gpio": 55, + "type": "I/O", + "functions": [ + "GPIO", + "ADC2_CH2_P" + ], + "caps": [ + "MTCK", + "SPI2_MISO", + "CAM_XCLK" + ], + "note": "Default JTAG MTCK; use with caution." + }, + { + "pin": 71, + "name": "GPIO56", + "gpio": 56, + "type": "I/O", + "functions": [ + "GPIO", + "ADC2_CH3_N" + ], + "caps": [ + "MTDI", + "SPI2_HOLD", + "CAM_VSYNC" + ], + "note": "Default JTAG MTDI; use with caution." + }, + { + "pin": 72, + "name": "GPIO57", + "gpio": 57, + "type": "I/O", + "functions": [ + "GPIO", + "ADC2_CH3_P" + ], + "caps": [ + "MTMS", + "SPI2_WP", + "CAM_HSYNC" + ], + "note": "Default JTAG MTMS; use with caution." + }, + { + "pin": 73, + "name": "GPIO58", + "gpio": 58, + "type": "I/O", + "functions": [ + "GPIO", + "UART0_TX" + ], + "caps": [ + "UART0_TXD" + ], + "note": "UART0 IO-MUX default; commonly used for console output." + }, + { + "pin": 74, + "name": "GPIO59", + "gpio": 59, + "type": "I/O", + "functions": [ + "GPIO", + "UART0_RX" + ], + "caps": [ + "UART0_RXD" + ], + "note": "UART0 IO-MUX default; commonly used for console input." + }, + { + "pin": 75, + "name": "GPIO60", + "gpio": 60, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "STRAP" + ], + "note": "Strapping pin; default weak pull-up." + }, + { + "pin": 76, + "name": "GPIO61", + "gpio": 61, + "type": "I/O", + "functions": [ + "GPIO" + ], + "caps": [ + "STRAP" + ], + "note": "Strapping pin; default weak pull-up." + }, + { + "pin": 77, + "name": "VDDA1", + "gpio": null, + "type": "P", + "functions": [], + "caps": [ + "VDDA1" + ] + }, + { + "pin": 78, + "name": "XTAL_N", + "gpio": null, + "type": "A", + "functions": [], + "caps": [ + "XTAL_N" + ], + "note": "Main crystal negative pin" + }, + { + "pin": 79, + "name": "XTAL_P", + "gpio": null, + "type": "A", + "functions": [], + "caps": [ + "XTAL_P" + ], + "note": "Main crystal positive pin" + }, + { + "pin": 80, + "name": "VDDA2", + "gpio": null, + "type": "P", + "functions": [], + "caps": [ + "VDDA2" + ] + }, + { + "pin": 81, + "name": "GND", + "gpio": null, + "type": "P", + "functions": [], + "caps": [ + "GND" + ], + "note": "Ground / exposed thermal pad" + } + ] +} diff --git a/platforms/index.json b/platforms/index.json index ca2ae78c..8ccad911 100644 --- a/platforms/index.json +++ b/platforms/index.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "domain": "platforms", - "publishedAt": "2026-09-16T08:10:00Z", + "publishedAt": "2026-09-18T09:02:56Z", "items": [ { "id": "t5ai", @@ -269,6 +269,23 @@ ], "published": true }, + { + "id": "esp32s31", + "platformId": "esp32", + "name": { + "en": "Espressif ESP32-S31", + "zh-CN": "乐鑫 ESP32-S31" + }, + "summary": { + "en": "Espressif ESP32-S31 platform — dual-core 32-bit RISC-V @320MHz with Wi-Fi 6 (2.4GHz), Bluetooth 5.4 including Classic Bluetooth, and IEEE 802.15.4 for Zigbee / Thread. 512KB HP SRAM plus 32KB LP SRAM and up to 32MB PSRAM suit connected audio, display, and camera AIoT devices.", + "zh-CN": "乐鑫 ESP32-S31 平台 — 双核 32 位 RISC-V,主频 320MHz,支持 Wi-Fi 6(2.4GHz)、蓝牙 5.4(含经典蓝牙)及 IEEE 802.15.4(Zigbee / Thread)。配备 512KB 高性能 SRAM、32KB 低功耗 SRAM,最高支持 32MB PSRAM,适用于音频、显示和摄像头类 AIoT 设备。" + }, + "detailUrl": "platforms/esp32/esp32s31.json", + "sdks": [ + "tuyaopen" + ], + "published": true + }, { "id": "linux", "platformId": "linux", diff --git a/platforms/platform-template.json b/platforms/platform-template.json index 347389c6..1be9816d 100644 --- a/platforms/platform-template.json +++ b/platforms/platform-template.json @@ -120,7 +120,7 @@ "id": 0, "role": "general", "routable": false, - "candidates": "", + "candidates": "", "irq": { "rx": false, "tx": false diff --git a/registry.json b/registry.json index 638b5a82..389b4a9e 100644 --- a/registry.json +++ b/registry.json @@ -2,21 +2,21 @@ "schemaVersion": 1, "name": "tuyaopen-ide-manifests", "description": "Top-level index. IDE fetches this first and uses the URLs below to lazy-load each domain's index.json.", - "publishedAt": "2026-09-01T03:06:00Z", + "publishedAt": "2026-09-14T07:29:14Z", "manifests": { "platforms": { "url": "platforms/index.json", - "version": "1.0.1", + "version": "1.1.0", "summary": "芯片平台与变体(T5AI、ESP32、Linux 等)" }, "boardsAndChips": { "url": "boards-and-chips/index.json", - "version": "1.0.2", + "version": "1.1.0", "summary": "开发板与芯片(官方 + 生态)" }, "demos": { "url": "demos/index.json", - "version": "1.1.0", + "version": "1.2.0", "summary": "示例工程" }, "skills": { diff --git a/tools/manifest-editor/backend/routes/boards.js b/tools/manifest-editor/backend/routes/boards.js index 7a740c71..97659bdb 100644 --- a/tools/manifest-editor/backend/routes/boards.js +++ b/tools/manifest-editor/backend/routes/boards.js @@ -6,8 +6,8 @@ import { asyncHandler } from '../middleware/error-handler.js'; const router = express.Router(); -// SDK applicability — optional array; omitted ⇒ ['tuyaopen'] (default). Returns -// a deduped subset of known SDK ids, or undefined when empty/absent (drop field). +// SDK applicability — a deduped subset of known SDK ids, or undefined when +// empty/absent (drop field). This is separate from an SDK branch requirement. const SDKS = ['tuyaopen', 'tuyaos']; function normalizeSdks(v) { if (!Array.isArray(v)) return undefined; @@ -15,6 +15,30 @@ function normalizeSdks(v) { return arr.length ? arr : undefined; } +const SAFE_BRANCH = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/; +function parseSdkRequirements(value) { + if (value === undefined) return { ok: true, value: undefined }; + if (!Array.isArray(value)) return { ok: false, error: 'sdkRequirements must be an array' }; + + const requirements = []; + const seen = new Set(); + for (const requirement of value) { + if (!requirement || typeof requirement !== 'object' || Array.isArray(requirement) || !SDKS.includes(requirement.sdk)) { + return { ok: false, error: 'Each sdkRequirements entry must name a supported SDK' }; + } + const branch = typeof requirement.branch === 'string' ? requirement.branch.trim() : ''; + if (!branch || branch.length > 256 || branch.includes('..') || !SAFE_BRANCH.test(branch)) { + return { ok: false, error: 'Each sdkRequirements branch must be a valid branch name' }; + } + if (seen.has(requirement.sdk)) { + return { ok: false, error: 'sdkRequirements may contain only one entry per SDK' }; + } + seen.add(requirement.sdk); + requirements.push({ sdk: requirement.sdk, branch }); + } + return { ok: true, value: requirements }; +} + // Resolve a board's platform reference (its `platformId` field — which holds a // platform *variant id*, e.g. "gd32vw553") to that platform's detail. The detail // lives at platforms//.json, so a plain @@ -52,7 +76,7 @@ router.get('/:id', asyncHandler(async (req, res) => { }); } - // Merge detail fields (boardSymbol, demos, peripheralPatterns, links) + // Merge detail fields (boardSymbol, peripheralPatterns, links, SDK branch requirements) const detail = await manifestLoader.loadBoardDetail(req.params.id); const merged = { ...board }; if (detail) { @@ -61,6 +85,7 @@ router.get('/:id', asyncHandler(async (req, res) => { if (detail.links) merged.links = detail.links; if (detail.peripheralPatterns) merged.peripheralPatterns = detail.peripheralPatterns; if (detail.peripheralGroups) merged.peripheralGroups = detail.peripheralGroups; + if (Array.isArray(detail.sdkRequirements)) merged.sdkRequirements = detail.sdkRequirements; if (detail.source) merged.source = detail.source; // Map links to editor field names @@ -102,6 +127,11 @@ router.post('/', asyncHandler(async (req, res) => { }); } + const sdkRequirementsResult = parseSdkRequirements(req.body.sdkRequirements); + if (!sdkRequirementsResult.ok) { + return res.status(400).json({ success: false, error: sdkRequirementsResult.error }); + } + // Create new board object. Detail files are grouped by SDK platform // (boards-and-chips//.json), mirroring platforms//. const newBoard = { @@ -151,6 +181,7 @@ router.post('/', asyncHandler(async (req, res) => { peripheralPatterns: {}, links: { schematic: null, datasheet: null, productPage: null }, boardSymbol: boardSymbol, + ...(sdkRequirementsResult.value?.length ? { sdkRequirements: sdkRequirementsResult.value } : {}), }; await manifestLoader.saveBoardDetail(id, initialDetail); @@ -198,6 +229,14 @@ router.patch('/:id', asyncHandler(async (req, res) => { } } + const sdkRequirementsResult = parseSdkRequirements(updates.sdkRequirements); + if (!sdkRequirementsResult.ok) { + return res.status(400).json({ success: false, error: sdkRequirementsResult.error }); + } + if (updates.sdkRequirements !== undefined) { + updates.sdkRequirements = sdkRequirementsResult.value; + } + // Fields that go to the index const indexFields = ['name', 'platformId', 'variantId', 'manufacturer', 'summary', 'tags', 'image', 'published']; for (const key of indexFields) { @@ -206,7 +245,7 @@ router.patch('/:id', asyncHandler(async (req, res) => { } } - // SDK applicability — empty/invalid clears it (defaults back to tuyaopen). + // SDK applicability — empty/invalid clears it. if (updates.sdks !== undefined) { const arr = normalizeSdks(updates.sdks); if (arr) board.sdks = arr; @@ -223,8 +262,8 @@ router.patch('/:id', asyncHandler(async (req, res) => { // Save index await manifestLoader.saveBoardsIndex(boards); - // Fields that go to the detail file: boardSymbol, demos, peripheralPatterns, links, source, memory - const detailFields = ['boardSymbol', 'links', 'source', 'memory']; + // Fields that go to the detail file. + const detailFields = ['boardSymbol', 'links', 'source', 'memory', 'sdkRequirements']; // Map editor link fields back to nested links object (merge with existing) const editorLinkFields = ['schematicLink', 'guideDocs', 'purchaseLink', 'threeDModelLink']; const hasEditorLinks = editorLinkFields.some(k => updates[k] !== undefined); @@ -259,6 +298,9 @@ router.patch('/:id', asyncHandler(async (req, res) => { if (updates[key] !== undefined) { if (key === 'links' && updates._mergeLinks) { detail.links = { ...(detail.links || {}), ...updates.links }; + } else if (key === 'sdkRequirements') { + if (updates.sdkRequirements.length) detail.sdkRequirements = updates.sdkRequirements; + else delete detail.sdkRequirements; } else { detail[key] = updates[key]; } diff --git a/tools/manifest-editor/backend/routes/demos.js b/tools/manifest-editor/backend/routes/demos.js index d4d2f7d7..35af3c2b 100644 --- a/tools/manifest-editor/backend/routes/demos.js +++ b/tools/manifest-editor/backend/routes/demos.js @@ -5,8 +5,8 @@ import { asyncHandler } from '../middleware/error-handler.js'; const router = express.Router(); -// SDK applicability — optional array; omitted ⇒ ['tuyaopen'] (default). Returns a -// deduped subset of known SDK ids, or undefined when empty/absent (drop field). +// SDK applicability — a deduped subset of known SDK ids, or undefined when +// empty/absent (drop field). This is separate from an SDK branch requirement. const SDKS = ['tuyaopen', 'tuyaos']; function normalizeSdks(v) { if (!Array.isArray(v)) return undefined; @@ -14,14 +14,39 @@ function normalizeSdks(v) { return arr.length ? arr : undefined; } +const SAFE_BRANCH = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/; +function parseSdkRequirements(value) { + if (value === undefined) return { ok: true, value: undefined }; + if (!Array.isArray(value)) return { ok: false, error: 'sdkRequirements must be an array' }; + + const requirements = []; + const seen = new Set(); + for (const requirement of value) { + if (!requirement || typeof requirement !== 'object' || Array.isArray(requirement) || !SDKS.includes(requirement.sdk)) { + return { ok: false, error: 'Each sdkRequirements entry must name a supported SDK' }; + } + const branch = typeof requirement.branch === 'string' ? requirement.branch.trim() : ''; + if (!branch || branch.length > 256 || branch.includes('..') || !SAFE_BRANCH.test(branch)) { + return { ok: false, error: 'Each sdkRequirements branch must be a valid branch name' }; + } + if (seen.has(requirement.sdk)) { + return { ok: false, error: 'sdkRequirements may contain only one entry per SDK' }; + } + seen.add(requirement.sdk); + requirements.push({ sdk: requirement.sdk, branch }); + } + return { ok: true, value: requirements }; +} + // Build the demo detail object. Holds the source path + build config + docs; // identity, classification and tags stay in the index. `configs` is an array // of board targets: { board, accessory?, options:[{name?,file}] }. // Returns null when there is nothing to store (caller deletes the file). -function buildDemoDetail(id, { source, cloud, configs, documentation, drivers }) { +function buildDemoDetail(id, { source, cloud, configs, documentation, drivers, sdkRequirements }) { const detail = { id }; if (typeof source === 'string' && source.trim()) detail.source = source.trim(); + if (Array.isArray(sdkRequirements) && sdkRequirements.length) detail.sdkRequirements = sdkRequirements; // Cloud demo flag: `true` when it just needs a PID, or an object with optional // PID-location overrides ({ pid: { kconfigKey?, macro?, file? } }) when the demo @@ -126,7 +151,7 @@ router.get('/:id', asyncHandler(async (req, res) => { // POST /api/demos - Create new demo router.post('/', asyncHandler(async (req, res) => { - const { id, type, name, summary, tags, boards, platforms, compatibilityType, source, cloud, configs, documentation, drivers, publish, sdks } = req.body; + const { id, type, name, summary, tags, boards, platforms, compatibilityType, source, cloud, configs, documentation, drivers, publish, sdks, sdkRequirements } = req.body; if (!id || !name?.en || !source || typeof source !== 'string') { return res.status(400).json({ @@ -151,6 +176,11 @@ router.post('/', asyncHandler(async (req, res) => { return res.status(400).json({ success: false, error: 'configs must be an array of board targets' }); } + const sdkRequirementsResult = parseSdkRequirements(sdkRequirements); + if (!sdkRequirementsResult.ok) { + return res.status(400).json({ success: false, error: sdkRequirementsResult.error }); + } + const demos = await manifestLoader.loadDemos(); if (demos.items.some(d => d.id === id)) { return res.status(409).json({ success: false, error: `Demo "${id}" already exists` }); @@ -176,7 +206,7 @@ router.post('/', asyncHandler(async (req, res) => { await manifestLoader.saveDemosIndex(demos); // Detail holds source path + build config + docs (identity/tags in the index). - const detailEntry = buildDemoDetail(id, { source, cloud, configs, documentation, drivers }); + const detailEntry = buildDemoDetail(id, { source, cloud, configs, documentation, drivers, sdkRequirements: sdkRequirementsResult.value }); if (detailEntry) await manifestLoader.saveDemoDetail(id, detailEntry, indexEntry.type); if (req.body.autoCommit !== false) { @@ -204,6 +234,14 @@ router.patch('/:id', asyncHandler(async (req, res) => { return res.status(400).json({ success: false, error: 'configs must be an array of board targets' }); } + const sdkRequirementsResult = parseSdkRequirements(updates.sdkRequirements); + if (!sdkRequirementsResult.ok) { + return res.status(400).json({ success: false, error: sdkRequirementsResult.error }); + } + if (updates.sdkRequirements !== undefined) { + updates.sdkRequirements = sdkRequirementsResult.value; + } + // Update index entry (identity / classification / tags + detailUrl) const item = demos.items[idx]; const oldType = item.type === 'app' ? 'app' : 'example'; @@ -211,7 +249,7 @@ router.patch('/:id', asyncHandler(async (req, res) => { for (const key of indexFields) { if (updates[key] !== undefined) item[key] = updates[key]; } - // SDK applicability — empty/invalid clears it (defaults back to tuyaopen). + // SDK applicability — empty/invalid clears it. if (updates.sdks !== undefined) { const arr = normalizeSdks(updates.sdks); if (arr) item.sdks = arr; @@ -239,6 +277,7 @@ router.patch('/:id', asyncHandler(async (req, res) => { configs: updates.configs !== undefined ? updates.configs : existing.configs, documentation: updates.documentation !== undefined ? updates.documentation : existing.documentation, drivers: updates.drivers !== undefined ? updates.drivers : existing.drivers, + sdkRequirements: updates.sdkRequirements !== undefined ? updates.sdkRequirements : existing.sdkRequirements, }); // A type change moves the detail file between demos/example|app/; drop the stale one. if (item.type !== oldType) await manifestLoader.deleteDemoDetail(req.params.id, oldType); diff --git a/tools/manifest-editor/frontend/js/board-editor.js b/tools/manifest-editor/frontend/js/board-editor.js index b60ec68c..bfdb5c39 100644 --- a/tools/manifest-editor/frontend/js/board-editor.js +++ b/tools/manifest-editor/frontend/js/board-editor.js @@ -47,6 +47,11 @@ function validateWebUrl(url) { export function renderBoardForm(board = null) { const isNew = !board; const title = isNew ? i18n.t('boardFormCreateTitle') : i18n.t('boardFormEditTitle'); + const sdkRequirements = Array.isArray(board?.sdkRequirements) ? board.sdkRequirements : []; + const branchFor = (sdk) => { + const requirement = sdkRequirements.find((entry) => entry?.sdk === sdk); + return typeof requirement?.branch === 'string' ? requirement.branch : ''; + }; const formHtml = `
@@ -123,6 +128,21 @@ export function renderBoardForm(board = null) { ${i18n.t('skillSdksHint')} +
+ +
+
+ + +
+
+ + +
+
+ ${i18n.t('sdkBranchRequirementsHint')} +
+
+
+
+ + +
+ + ${i18n.t('sdkBranchRequirementsHint')} + +
@@ -437,11 +457,16 @@ export async function saveDemoForm(form, demoId = null) { } const sdks = [...document.querySelectorAll('.demo-sdk-cb:checked')].map((cb) => cb.value); + const sdkRequirements = [ + { sdk: 'tuyaopen', branch: document.getElementById('demoSdkBranchTuyaopen')?.value?.trim() || '' }, + { sdk: 'tuyaos', branch: document.getElementById('demoSdkBranchTuyaos')?.value?.trim() || '' }, + ].filter((requirement) => requirement.branch); const data = { id, type, sdks, + sdkRequirements, publish: document.getElementById('demoPublish').checked, name: { en: document.getElementById('demoNameEn').value.trim(), diff --git a/tools/manifest-editor/frontend/js/i18n.js b/tools/manifest-editor/frontend/js/i18n.js index 1e11994c..ba524174 100644 --- a/tools/manifest-editor/frontend/js/i18n.js +++ b/tools/manifest-editor/frontend/js/i18n.js @@ -314,6 +314,7 @@ const TRANSLATIONS = { pfPeriAdd: 'Add peripheral', pfPeriEmpty: 'No peripherals yet — click “Add peripheral”.', pfPeriAllAdded: 'All known peripherals have been added.', + pfCandidateRanges: 'Candidate GPIO Ranges', pfSegAdd: 'segment', pfPmAddFunc: 'pin function', pfPmAddCat: 'category', @@ -371,6 +372,10 @@ const TRANSLATIONS = { skillCommaHint: 'Comma-separated.', skillSdks: 'Applies to SDK(s)', skillSdksHint: 'Leave both unchecked = TuyaOpen only (default).', + sdkBranchRequirements: 'SDK branch requirements', + sdkBranchRequirementsHint: 'Leave a branch blank to use master or a Release version for that SDK.', + sdkBranchTuyaopen: 'TuyaOpen branch', + sdkBranchTuyaos: 'TuyaOS branch', skillPayloadInfo: 'Payload (read-only)', skillSource: 'Source', skillInstallPayload: 'Install payload', @@ -887,6 +892,7 @@ const TRANSLATIONS = { pfPeriAdd: '添加外设', pfPeriEmpty: '暂无外设 —— 点击"添加外设"。', pfPeriAllAdded: '已知外设都已添加。', + pfCandidateRanges: '候选 GPIO 区间', pfSegAdd: '添加段', pfPmAddFunc: '引脚功能', pfPmAddCat: '外设种类', @@ -944,6 +950,10 @@ const TRANSLATIONS = { skillCommaHint: '用英文逗号分隔。', skillSdks: '适用 SDK', skillSdksHint: '都不勾选 = 仅适用 TuyaOpen(默认)。', + sdkBranchRequirements: 'SDK 分支要求', + sdkBranchRequirementsHint: '留空则使用该 SDK 的 master 或 Release 发布版本。', + sdkBranchTuyaopen: 'TuyaOpen 分支', + sdkBranchTuyaos: 'TuyaOS 分支', skillPayloadInfo: '载荷信息(只读)', skillSource: '来源', skillInstallPayload: '安装载荷', diff --git a/tools/manifest-editor/frontend/js/platform-editor.js b/tools/manifest-editor/frontend/js/platform-editor.js index 11d28306..75f42b18 100644 --- a/tools/manifest-editor/frontend/js/platform-editor.js +++ b/tools/manifest-editor/frontend/js/platform-editor.js @@ -217,6 +217,29 @@ function kindOf(v) { return 'string'; } +// Whether a GPIO is included by a routable port's candidate declaration. +// Candidates accept either individual GPIO numbers or inclusive [start, end] +// ranges, so [0, 2, [8, 28]] means GPIO0, GPIO2, and GPIO8 through GPIO28. +// Invalid entries are ignored instead of broadening the selectable pin set. +export function isCandidateGpio(candidates, gpio) { + if (!Array.isArray(candidates) || !Number.isInteger(gpio)) return false; + return candidates.some((candidate) => { + if (Number.isInteger(candidate)) return candidate === gpio; + if (!Array.isArray(candidate) || candidate.length !== 2) return false; + const [start, end] = candidate; + return Number.isInteger(start) && Number.isInteger(end) && start <= end && start <= gpio && gpio <= end; + }); +} + +// A candidates list is rendered as a range editor only when every entry uses +// the [start, end] form. Legacy lists of individual GPIOs keep their existing +// scalar editor, so opening them never rewrites their data shape. +export function isCandidateRangeList(candidates) { + return Array.isArray(candidates) && candidates.every( + (candidate) => Array.isArray(candidate) && candidate.length === 2, + ); +} + class StructEditor { constructor(data, { omitKeys, labels, lockStructure, enums, datalistKeys, datalistSuggest, pinout, categories, schema, flattenKeys, inlineObjects, itemTemplate } = {}) { this.data = data ?? {}; @@ -270,6 +293,7 @@ class StructEditor { } } if (this.labels && this.labels[k]) return this.labels[k]; + if (k === 'candidates') return i18n.t('pfCandidateRanges'); // A raw HAL-constant key (e.g. a pixel-format map key) shows shortened. if (typeof k === 'string' && /^T(?:UYA|KL)_[A-Z0-9]+_/.test(k)) return prettyEnum(k); return k; @@ -340,6 +364,7 @@ class StructEditor { // True when an array key at this path is a list of [start,end] ranges. _isSegments(key, path) { + if (key === 'candidates') return isCandidateRangeList(this._getByPath([...path, key])); const type = (path && path.length) ? path[0] : null; const sc = type && this.schema ? this.schema[type] : null; return !!(sc && sc.segments && sc.segments.includes(key)); @@ -469,8 +494,9 @@ class StructEditor { return r; } - // Nearest enclosing port's `candidates` (GPIO numbers a routable port may use), - // or null when unconstrained (= any GPIO-capable pin). + // Nearest enclosing port's `candidates` (individual GPIO numbers and/or + // inclusive [start, end] ranges that a routable port may use), or null when + // unconstrained (= any GPIO-capable pin). _candidatesAt(path) { let cur = this.data, c = null; for (const seg of path) { @@ -555,7 +581,7 @@ class StructEditor { if (this._routableAt(path)) { const cand = this._candidatesAt(path); const pool = Array.isArray(cand) && cand.length - ? all.filter(e => cand.includes(e.gpio)) + ? all.filter(e => isCandidateGpio(cand, e.gpio)) : all.filter(e => Array.isArray(e.functions) && e.functions.includes('GPIO')); const poolF = pool.length ? pool : all; const curEnt = poolF.find(e => String(e.gpio) === cur); diff --git a/tools/manifest-editor/test/platform-editor-candidates.test.js b/tools/manifest-editor/test/platform-editor-candidates.test.js new file mode 100644 index 00000000..f9699d0e --- /dev/null +++ b/tools/manifest-editor/test/platform-editor-candidates.test.js @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +globalThis.localStorage = { + getItem: () => null, + setItem: () => {}, +}; + +const { isCandidateGpio, isCandidateRangeList } = await import('../frontend/js/platform-editor.js'); + +test('routable candidates accept individual GPIOs and inclusive ranges', () => { + const candidates = [0, 2, [8, 28], [30, 40], [42, 61]]; + + assert.equal(isCandidateGpio(candidates, 0), true); + assert.equal(isCandidateGpio(candidates, 2), true); + assert.equal(isCandidateGpio(candidates, 8), true); + assert.equal(isCandidateGpio(candidates, 28), true); + assert.equal(isCandidateGpio(candidates, 30), true); + assert.equal(isCandidateGpio(candidates, 61), true); + assert.equal(isCandidateGpio(candidates, 1), false); + assert.equal(isCandidateGpio(candidates, 29), false); + assert.equal(isCandidateGpio(candidates, 41), false); +}); + +test('invalid candidate entries do not make extra GPIOs selectable', () => { + assert.equal(isCandidateGpio([[28, 8], [1], '2'], 8), false); + assert.equal(isCandidateGpio(null, 8), false); +}); + +test('range editor is used only for range-form candidates', () => { + assert.equal(isCandidateRangeList([[8, 25], [33, 40]]), true); + assert.equal(isCandidateRangeList([]), true); + assert.equal(isCandidateRangeList([8, 9]), false); + assert.equal(isCandidateRangeList([[8, 25], 33]), false); +}); diff --git a/tools/manifest-gen/src/commands/platform-edit.js b/tools/manifest-gen/src/commands/platform-edit.js index e506ca8d..4c569236 100644 --- a/tools/manifest-gen/src/commands/platform-edit.js +++ b/tools/manifest-gen/src/commands/platform-edit.js @@ -6,7 +6,6 @@ import { editBasicInfo, editConnectivity, editMemory, - editKconfig, editPeripherals, } from '../generators/platform/wizard.js' import { buildPlatform } from '../generators/platform/builder.js' @@ -39,7 +38,6 @@ export async function runPlatformEdit(filePath) { flashInterface: existing.flashInterface, connectivity: existing.connectivity, memory: existing.memory, - kconfigId: existing.kconfigId, selectedPeripherals: Object.keys(existing.peripherals ?? {}), peripheralConfigs: existing.peripherals ?? {}, } @@ -53,7 +51,6 @@ export async function runPlatformEdit(filePath) { flashInterface: answers.flashInterface, connectivity: answers.connectivity, memory: answers.memory, - kconfigId: answers.kconfigId, peripherals: answers.peripheralConfigs, }) @@ -65,7 +62,6 @@ export async function runPlatformEdit(filePath) { { name: '基本信息(platformId / name / arch / flashInterface)', value: 'basic' }, { name: '连接方式', value: 'connectivity' }, { name: '内存配置', value: 'memory' }, - { name: 'Kconfig ID', value: 'kconfig' }, { name: '外设配置', value: 'peripherals' }, { name: chalk.green('✔ 保存并退出'), value: 'done' }, { name: chalk.red('✗ 放弃修改'), value: 'abort' }, @@ -85,8 +81,6 @@ export async function runPlatformEdit(filePath) { await editConnectivity(answers) } else if (section === 'memory') { await editMemory(answers) - } else if (section === 'kconfig') { - await editKconfig(answers) } else if (section === 'peripherals') { await editPeripherals(answers) } @@ -114,4 +108,3 @@ export async function runPlatformEdit(filePath) { console.log(chalk.green(`\n✔ 已保存:${filePath}`)) } - diff --git a/tools/manifest-gen/src/commands/platform-show.js b/tools/manifest-gen/src/commands/platform-show.js index 79537548..23813023 100644 --- a/tools/manifest-gen/src/commands/platform-show.js +++ b/tools/manifest-gen/src/commands/platform-show.js @@ -31,5 +31,5 @@ export async function runPlatformShow(filePath) { const periList = Object.keys(data.peripherals ?? {}) console.log(` 外设 (${periList.length}):${periList.join(', ') || '无'}`) - console.log(` Schema: v${data.schemaVersion} | kconfigId: ${data.kconfigId ?? '?'}\n`) + console.log(` Schema: v${data.schemaVersion}\n`) } diff --git a/tools/manifest-gen/src/generators/platform/builder.js b/tools/manifest-gen/src/generators/platform/builder.js index 1f03d563..0f20b02a 100644 --- a/tools/manifest-gen/src/generators/platform/builder.js +++ b/tools/manifest-gen/src/generators/platform/builder.js @@ -1,7 +1,7 @@ import { peripheralModules } from '../registry.js' export function buildPlatform(answers) { - const { platformId, variantId, name, arch, flashInterface, connectivity, memory, kconfigId, selectedPeripherals, peripheralConfigs = {} } = answers + const { platformId, variantId, name, arch, flashInterface, connectivity, memory, selectedPeripherals, peripheralConfigs = {} } = answers const peripherals = {} for (const mod of peripheralModules) { @@ -20,7 +20,6 @@ export function buildPlatform(answers) { connectivity, memory, peripherals, - kconfigId, } } diff --git a/tools/manifest-gen/src/generators/platform/wizard.js b/tools/manifest-gen/src/generators/platform/wizard.js index 056d6f8d..8cb00c91 100644 --- a/tools/manifest-gen/src/generators/platform/wizard.js +++ b/tools/manifest-gen/src/generators/platform/wizard.js @@ -142,23 +142,6 @@ export async function editMemory(answers) { } } -export async function editKconfig(answers) { - while (true) { - const field = await select({ - message: 'Kconfig ID — 选择字段:', - loop: false, - choices: [ - { name: `kconfigId ${chalk.gray(answers.kconfigId ?? '—')}`, value: 'choice' }, - { name: chalk.gray('← 返回'), value: 'back' }, - ], - }) - if (field === 'back') break - if (field === 'choice') { - answers.kconfigId = await input({ message: 'kconfigId (如 T5AI):', default: answers.kconfigId }) - } - } -} - export async function editPeripherals(answers) { while (true) { const choices = peripheralModules.map(m => { @@ -313,11 +296,6 @@ export async function configureMemory(defaults = {}) { return { memory: { sramBytes: sramKB * 1024, romBytes: romKB * 1024, flashMaxBytes: flashMaxKB * 1024, psramMaxBytes: psramMaxKB * 1024, efuse } } } -export async function configureKconfig(defaults = {}) { - const kconfigId = await input({ message: 'kconfigId(平台 Kconfig 标识,如 T5AI):', default: defaults.kconfigId }) - return { kconfigId } -} - // reconfigureKeys: null = configure all selected (create mode) // string[] = only reconfigure these + newly added peripherals (edit mode) export async function configurePeripherals(defaults = {}, reconfigureKeys = null) { @@ -356,9 +334,8 @@ export async function runPlatformWizard(defaults = {}) { const { connectivity } = await configureConnectivity(defaults) console.log('\n--- 内存配置 ---') const { memory } = await configureMemory(defaults) - const { kconfigId } = await configureKconfig(defaults) console.log('\n--- 外设选择 ---') const { selectedPeripherals, peripheralConfigs } = await configurePeripherals(defaults, null) - return { ...basic, connectivity, memory, kconfigId, selectedPeripherals, peripheralConfigs } + return { ...basic, connectivity, memory, selectedPeripherals, peripheralConfigs } } diff --git a/tools/manifest-gen/src/validators/platform.js b/tools/manifest-gen/src/validators/platform.js index cc172bf3..e596d589 100644 --- a/tools/manifest-gen/src/validators/platform.js +++ b/tools/manifest-gen/src/validators/platform.js @@ -1,10 +1,19 @@ import { peripheralModules } from '../generators/registry.js' const VALID_FLASH = ['qspi', 'spi'] +const ALLOWED_TOP_LEVEL = new Set([ + 'schemaVersion', 'platformId', 'id', 'name', 'arch', 'flashInterface', + 'connectivity', 'memory', 'peripherals', 'power', 'pinout', 'platformSymbol', +]) export function validatePlatform(data) { const errors = [] + for (const key of Object.keys(data)) { + if (!ALLOWED_TOP_LEVEL.has(key)) + errors.push(`${key} — 未注册的 platform 顶层字段;请先更新公共 Schema、生成器和校验`) + } + if (data.schemaVersion !== 1) errors.push(`schemaVersion — 期望 1,实际 ${data.schemaVersion}`) @@ -28,9 +37,6 @@ export function validatePlatform(data) { if (data.connectivity == null || typeof data.connectivity !== 'object' || Array.isArray(data.connectivity)) errors.push('connectivity — 期望 object') - if (typeof data.kconfigId !== 'string' || data.kconfigId.length === 0) - errors.push('kconfigId — 期望非空 string') - if (data.peripherals && typeof data.peripherals === 'object') { for (const mod of peripheralModules) { const key = mod.meta.key diff --git a/tools/manifest-gen/test/builder.test.js b/tools/manifest-gen/test/builder.test.js index 8cb0b239..fe204d53 100644 --- a/tools/manifest-gen/test/builder.test.js +++ b/tools/manifest-gen/test/builder.test.js @@ -14,7 +14,6 @@ const minimalAnswers = { cellular: { enabled: false, enableMacro: 'ENABLE_CELLULAR' }, }, memory: { sramBytes: 1024, romBytes: 512, flashMaxBytes: 4096, psramMaxBytes: 0, efuse: false }, - kconfigId: 'TEST_PLAT', selectedPeripherals: ['gpio', 'uart'], } @@ -25,6 +24,7 @@ describe('buildPlatform()', () => { expect(result.platformId).toBe('test-plat') expect(result.id).toBe('test-v1') expect(result.arch).toBe('arm-cortex-m33') + expect(result).not.toHaveProperty('kconfigId') }) it('只包含选中的外设', () => { diff --git a/tools/manifest-gen/test/validator.test.js b/tools/manifest-gen/test/validator.test.js index 5d72b9ea..c3e87ca5 100644 --- a/tools/manifest-gen/test/validator.test.js +++ b/tools/manifest-gen/test/validator.test.js @@ -12,7 +12,6 @@ const validAnswers = { cellular: { enabled: false, enableMacro: 'ENABLE_CELLULAR' }, }, memory: { sramBytes: 655360, romBytes: 65536, flashMaxBytes: 16777216, psramMaxBytes: 16777216, efuse: true }, - kconfigId: 'T5AI', selectedPeripherals: ['gpio', 'uart'], } @@ -34,6 +33,12 @@ describe('validatePlatform()', () => { expect(validatePlatform(data).some(e => e.includes('platformId'))).toBe(true) }) + it('未注册的顶层字段时报错', () => { + const data = buildPlatform(validAnswers) + data.kconfigId = 'T5AI' + expect(validatePlatform(data).some(e => e.includes('未注册的 platform 顶层字段'))).toBe(true) + }) + it('memory.sramBytes 为 string 时报错', () => { const data = buildPlatform(validAnswers) data.memory.sramBytes = '655360'