先确定这篇要完成什么
这是一份完整工程,不是只给两个回调。firmware/保留小智v2.4.0 / 5540258abcbfa62518d09959308200be1c5b1b2b的1590个来源文件,并明确包含本站I2C字段顺序/功放16k两处明确兼容补丁、四处原源文件改动和四个新头文件。原样该板型在5.5.5没有编译通过;本篇不把补丁版叫原样上游成功。UPSTREAM.json、COMPATIBILITY.json、AUDIO-COMPATIBILITY.json、两份patch和A08-OVERLAY.json分别记录来源与差异。
用途是把“模型说开灯”拆成可检查的参数、主任务调度、GPIO数字回读和最终状态。仅控制GPIO18外接实验LED,不能套作门锁、加热器或安全执行器。没有实板、RF、云账号、对话或光学结果;主机使用明确的HAL/网络适配器,空观察表保持未填。
参考:[1] S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB[2] ICS43434 DS-000069 v1.2:电气/边沿/24bit/启动[4] MAX98357A/MAX98357B Rev7:供电/采样率/SD/GAIN/BTL
1. 固定完整组合
仅覆盖官方ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N16R8:16MB Quad Flash、8MB Octal PSRAM、80MHz,SDK5.5.5。这是新的N16R8组合,A01/A02原板是N8R8。核模组后缀和板修订,不把16M商品标题当证明。本包选择bread-compact-wifi、SSD1306 128×32、中文,禁用WakeNet/音频处理器/AEC;普通对话仍依赖所选网络后端。
所有排针须已可靠焊接,断USB和外供后接线。主板由UART桥USB供电;原生USB19/20留空,日志/下载用UART0桥43/44。Octal PSRAM相关35/36/37不用。
本版BUILTIN_LED_GPIO48用于外接RGB,与v1.1板载GPIO38不同,本实验不接48。原版LampController也写18,本包已移除其include/实例,只由新策略拥有18;不用两个工具同时争同一脚。无电压/电流/亮度实测。先关功放外供和SD,确认LED实验后才按需进行语音。停止时先关SD与功放外供,再拔主板USB,避免无供电GPIO被外部电路偏置。
输入是16kHz左槽32bit、24有效位,源码右移12并限16bit;输出已由原版24k改为16kHz左槽32bit,软件音量平方缩放。不是A01的转换算法。默认音量70/NVS会保存,非正音量启动会回10;软件音量不是复位静音保证。MAX98357A BTL和8Ω/1W额定不代表实测功率。保持物理SD关闭检查,低音量开始,发热/削波/压降立刻停。本例无AEC,不上传或自动播放现实音频。
本组合为什么必须把原版输出24k改为16k
MAX98357A Rev7 p6 DIGITAL AUDIO INTERFACE保证的LRCLK区间为7.6–8.4、15.2–16.8、30.4–50.4、83.8–100.8kHz。24k不在保证范围;原上游AUDIO_OUTPUT_SAMPLE_RATE=24000不能用来证明本站所选PID3006合规。第二份显式板局部补丁只把config.h此宏改16000,输入16k和所有引脚不变。两套I2S仍独立,不因频率相同就短接时钟。32bit×2slot标称BCLK=1.024MHz,物理时钟未测。
固定AudioService::SetDecodeSampleRate按到来的Opus packet sample_rate打开decoder。若服务端送24k,则以锁定esp_audio_effects1.3.0~1配置src=24000/dst=16000、mono16bit、complexity2;decode后通过get_max_out_sample_num分配容量、process实际输出数量缩放PCM,再入播放队列。组件头声明输入/输出rate为4000或11025倍数,24k和16k均符合。TX codec本身仍配置16k,不直接将服务端24k设成MAX98357A LRCLK。实际组件已链接;主机桩只查完整方法参数/分支,不执行Xtensa DSP,不证明音质或时钟。可在本机执行 python3 tools/check_rate_path.py --output /tmp/a07-rate-contract(输出目录须尚不存在;A08可换目录名),它从下载包生产源提取完整方法和转换条件块,验证24→16配置、容量/实际输出数、重复配置复用、同频旁路和空handle局限;编译器静态断言同时核所选LRCLK落在厂家保证区间。
上游重采样open失败只打印日志,没有自动静音保证;get_max_out_sample_num/process返回值也没有在本版全面处理。看到“Failed to create output resampler”或“Failed to create Opus decoder”、异常语速/音调或内存错误时先关闭物理SD启用(开关断开VIN、由10k下拉静音),再停止功放外供,检查供电/PSRAM/堆、decoder采样率与完整版本,恢复自己的归档或RESET,不继续把错误当正常转换。此处保留并准确说明原实现,未改SDK/managed算法或暗称故障已被自动安全处理。
原24k作者包和全部原产品留在历史,编译通过未曾证明器件频率适配;当前结果只认16k修正版的三阶段/工具版新构建。
| 器件 | 完整接线 |
|---|---|
| PID6049 / ICS-43434 I2S麦克风 | 主板3V3→3V,GND/SEL→GND,GPIO4→WS,5→BCLK,6←DOUT |
| PID3006 / MAX98357A单声道功放 | GPIO7→DIN、15→BCLK、16→LRC;独立稳压3.3V额定≥1A→VIN,外供GND与主板共地,正端不接主板3V3/5V;GAIN→功放VIN选6dB |
| PID1313 / C2464-001,8Ω/1W | 两线只接功放OUT+/OUT−,任何一端都不接GND、耳机地或接地示波器夹 |
| 物理静音 | SD→10kΩ→GND;SD经1kΩ和低压SPST开关接功放VIN,断开为关闭、闭合为左声道。先保持断开并测SD电压 |
| PID4440 / 自动复位revB,128×32 SSD1306 | VIN→主板3V3,GND→GND,SDA41、SCL42,RST留空;0x3C、400kHz,非PID326 128×64或旧需复位的PID931 |
| 按钮 | 板载BOOT GPIO0;GPIO47按钮到GND并外10kΩ上拉3V3;39/40可各接同样按钮作音量减/增 |
| 本篇实验LED | GPIO18→1kΩ→Kingbright WP7113ID阳极,阴极→GND;不省电阻 |
参考:[1] S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB[2] ICS43434 DS-000069 v1.2:电气/边沿/24bit/启动[3] PID6049固定PCB:3V、SEL与47k数据下拉[4] MAX98357A/MAX98357B Rev7:供电/采样率/SD/GAIN/BTL[5] PID3006固定单声道PCB:VIN/SD1M/BTL端子[6] PID1313扬声器2017-02-14厂家规格:8Ω额定1W[7] PID6049产品页:具体型号与停产边界[13] ESP32-S3-WROOM-1/1U v1.8:N16R8规格表[14] PID4440固定revB PCB:上拉/电平转换/自动复位/128×32[15] PID4440具体产品:128×32、自动复位[18] 固定AudioService:decoder采样率与输出重采样路径[19] 实际esp_audio_effects1.3.0~1来源:rate converter格式/采样率/API约束
2. 用准确API注册工具
板级InitializeTools()先建立GpioLed和LedPolicy,再向McpServer::AddTool(McpTool*)注册:
固定上游PropertyList会丢弃未知字段并把JSON数转换为int;因此本包给McpTool增加通用的可选raw-validator hook,在该转换之前检查已解析的arguments。核心不include具体板文件,其他工具不安装hook,其原行为不变。两个新增工具拒绝缺字段、重复字段、错误类型、大小写变体、未知字段和小数/越界id。规范化arguments序列化≤64字节,不是整网络包≤64字节,也不是替换cJSON的严格原始JSON验证/鉴权层。cJSON字符串键的NUL等限制仍按上游处理。
回调由固定DoToolCall通过Application::Schedule()交主任务。策略是单所有者,不能从ISR/别的任务直接调用。固件不会调用模型或猜测物理成功;网络回包可能丢失,执行与收到回答不是同一时刻。
- self.lab_led.set:power必须JSON布尔;request_id必须有限整数0..2147483647。
- self.lab_led.get_state:无参数或空对象,不接受其他字段。
参考:[8] 固定设备侧MCP:参数转换、主任务调度与结果[9] 固定McpTool API/PropertyList与错误路径[16] 固定Application:parsed mcp payload派发
3. 幂等、故障和查询的边界
request_id由请求端分配:新命令用新id,重试用同id同power。只缓存本次启动最近16个成功本地应用id,无NVS、无永久去重。相同id同power返回duplicate且不再写GPIO;同id不同power报CONFLICT且不改变输出。ON(id1)→OFF(id2)后迟到重试ON(id1),返回duplicate但查询仍是当前OFF,不重放旧ON。淘汰或RESET后旧id可以再应用;不能称exactly-once。32bit随机boot_id用于区分观察轮次,不是认证/全球唯一标识。
上电先LOW预置,再配置INPUT_OUTPUT、关闭上下拉,以便读数字pad。写后回读不符、初始化或写失败会锁定fault,并尽力写LOW;尽力LOW不证明实体已熄灭。gpio_get_level()无ESP_ERR返回,本例生产读数为0/1,主机抽象IO的read失败是注入故障。查询可以发现pad与最后确认值不同,但不清fault。修复接线/负载后RESET;不接成负载控制器。
结果中last_confirmed_power为最后一次成功本地写/读的历史值;digital_pad_level为本次数字采样或null;last_applied_request_id记录最后本地应用的id。fault后尽力LOW可能使历史值与当前pad不同。feedback固定digital_gpio_only_not_optical:LED断路、反接或损坏仍可能读到正确pad。MCP成功返回不是光学、云端收件或设备动作证明。set回调的最终snapshot发现fault会抛异常返回JSON-RPC error;get_state查询仍正常返回含fault=true的状态JSON,set动作失败不能只把一个false字段塞进正常result冒充JSON-RPC错误。
参考:[8] 固定设备侧MCP:参数转换、主任务调度与结果[9] 固定McpTool API/PropertyList与错误路径[11] IDF5.5.5 GPIO:INPUT_OUTPUT回读与错误返回
4. 构建、保存和刷写
激活已安装的ESP-IDF5.5.5环境,核idf.py --version。v2.4.0的manifest最低>=5.5.2不是每板兼容承诺;v2.5.x最低>=6.0.1,不能换tag后复用本锁。完整dependencies.lock随包,managed组件按其锁和许可解析,不下载另一个SDK。
工具先运行上游lang_header目标,避免原版生成头依赖顺序问题,再真实build。保存local-products.json、日志及其中列出的原镜像/配置/锁/地址表,不只留哈希。manifest/flash要求当前输入、产品和成功构建记录一致,修改后必须重建。Flash16MB、Octal PSRAM、V2分区与0x800000 assets区必须匹配,不能把app.bin统一写0x0。
先保持SD断开和外供关闭,用桥接USB,核自己端口:
按本版配网流程关联自己后端;无账号和设备不能把示例请求当真实成功记录。默认OTA保留,可能替换自编固件;工具消失/日志变版时核OTA和镜像,用自己的完整归档恢复。设备侧MCP与外部MCP服务接入不同,本例不要求填写远程工具URL。
python tools/verify_overlay.py
python tools/project.py configure
python tools/project.py build
python tools/project.py manifest
python tools/project.py flash --port /dev/cu.YOUR_BRIDGE_PORT
python tools/project.py monitor --port /dev/cu.YOUR_BRIDGE_PORT参考:[8] 固定设备侧MCP:参数转换、主任务调度与结果[9] 固定McpTool API/PropertyList与错误路径[17] 固定SDK兼容字段依据
5. 逐项测试,别只说“帮我开灯”
若所选后端有设备MCP调试入口,使用它实际下发标准tools/list及tools/call,保存请求和返回;没有该入口时先读下方本机夹具,再用语音测试,不能虚构云UI。以下是mcp payload示例,不是直接贴到普通聊天框的命令,也不是自动连接云服务的脚本。
再说“把实验灯打开”“查询实验灯状态”,对照真实工具调用、参数、UART、数字值和光学观察。模型选错工具、改id或没有调用时是上游链路问题,不能因为口头回答“已开”就填成功。本包没有执行这些物理/云端步骤。
- 列表应有两个新工具和准确schema;上游self.lamp旧工具不应再由本板注册。
- 发送ON/100,查UART SET outcome及PAD,再查状态并实际看LED。三者分别记录,不能互相代填。
- 重试ON/100,expect duplicate;发OFF/101;再重试ON/100,应仍保持当前OFF。发OFF/100应CONFLICT。
- 单独尝试power:"true"、request_id=1.5、-1、2147483648、未知字段、query带参数,应JSON-RPC error且无新GPIO动作。模型未必会产生这些输入,须调试请求或主机夹具执行。
- 断电后拔LED支路、再上电执行:pad可能仍正确但光不亮,证明数字反馈边界。不要带电短接GPIO到电源/地来注入故障。主机的write/read/stuck fault不当作真实引脚短路测试。
- RESET后boot/cache重新建立,旧id可能再执行;断网/回包丢失时先查询,保持相同id重试,不生成新id盲目重复动作。
{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"self.lab_led.set","arguments":{"power":true,"request_id":100}}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"self.lab_led.get_state","arguments":{}}}参考:[8] 固定设备侧MCP:参数转换、主任务调度与结果[9] 固定McpTool API/PropertyList与错误路径[10] 固定bread board:原LampController所有权[11] IDF5.5.5 GPIO:INPUT_OUTPUT回读与错误返回
6. 本机同源检查
策略12组、生产GpioLed helper 5组;MCP工具从当前源提取10个完整方法,实际生产header/binding/policy+Espressif cJSON1.7.19~2(上游版本1.7.19),FIFO Application调度/复制JSON输出与FakeLedIo是明确适配器。覆盖列表、真正parsed-payload重载与字符串重载、严格参数、排队前无IO、去重/迟到/冲突、错误返回及最终snapshot故障。不是网络传输或整个Application回调复现,不证明RF/RTOS排程/电压/光学。cJSON原文件不改;Apple SDK的deprecated sprintf警告保留,只在vendor C编译取消该项Werror,本站C++仍Werror。首次vendor警告失败和恢复原日志均保留。
clang++ -std=c++17 -Wall -Wextra -Werror -fsanitize=address,undefined tests/policy.cpp -o /tmp/a08-policy
/tmp/a08-policy
clang++ -std=c++17 -Wall -Wextra -Werror -fsanitize=address,undefined -Itests/gpio-stubs tests/gpio.cpp -o /tmp/a08-gpio
/tmp/a08-gpio
python3 tests/run_mcp_host.py --output /tmp/a08-mcp-UNUSED-DIRECTORY参考:[8] 固定设备侧MCP:参数转换、主任务调度与结果[9] 固定McpTool API/PropertyList与错误路径[11] IDF5.5.5 GPIO:INPUT_OUTPUT回读与错误返回[12] 实际Registry cJSON来源SBOM commit:1.7.19~2
7. 恢复与许可
无工具先核编译选板、overlay验证、当前镜像/OTA和列表,不先改云端口令。参数报错按schema修请求;CONFLICT换新命令id而非改旧id;GPIO fault修电路后RESET;无声回A01/A02分离输入输出,再核外供/GAIN/SD/槽和原版音量。下载失败用桥USB、数据线/端口,按住BOOT点RESET进入下载。所有改线均断电。
hardware-observations.csv仅表头,保留实际请求/响应、boot/id、UART、pad和可见光、镜像SHA、恢复记录。上游MIT与本包MIT说明保留;managed组件/cJSON/字体/模型各自原许可列THIRD_PARTY,不能一概改成MIT。来源核查与软件测试不替代硬件验收。
参考:[1] S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB[4] MAX98357A/MAX98357B Rev7:供电/采样率/SD/GAIN/BTL[8] 固定设备侧MCP:参数转换、主任务调度与结果[9] 固定McpTool API/PropertyList与错误路径[11] IDF5.5.5 GPIO:INPUT_OUTPUT回读与错误返回
本次实际完成的检查与仍空白的物理记录
软件结果来自实际命令、当前源码与原始产物。失败和恢复均保留,四个验证维度分别展示;下表没有把主机夹具替代实体实验。
| 检查项 | 实际结果 | 边界 |
|---|---|---|
| 固件 | 补丁版完整增量构建Exit0,输入稳定 | 当前16k xiaozhi.bin2633680B;28产品与配置/日志SHA留存,旧24k产品另留历史 |
| 同源协议 / GPIO适配 | 12策略+5生产GPIO helper组;完整10个MCP方法的12+1组均ASan/UBSan通过 | GPIO/网络使用明确桩,非RF/电压/光学实验 |
| 真实文件 | 镜像校验/hash有效,ELF包含新工具名,vendor逐字等实际Registry组件 | 不证明设备端收到调用或LED已亮 |
| 实体 / 云对话 | 未执行,空CSV | 没有账号关联、工具调用或光学成功记录 |
参考:[8] 固定设备侧MCP:参数转换、主任务调度与结果[9] 固定McpTool API/PropertyList与错误路径
完整生产LED策略与GPIO适配
以下逐字对应下载包当前文件;完整依赖、构建说明、许可和空物理记录一起提供。通用MCP核心两个改动文件也完整保留在firmware/main,A08-OVERLAY.json列出原/现SHA;这里的板级绑定不会让核心依赖具体板。
// ---- firmware/main/boards/bread-compact-wifi/audited_led_policy.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
namespace a08 {
enum class Result { APPLIED, DUPLICATE, CONFLICT, FAULT };
struct Snapshot {
uint32_t boot_id;
bool fault, last_confirmed_power, pad_known, pad_level, has_last_request;
uint32_t last_request;
};
// Single owner: Xiaozhi Application main-task scheduled callbacks only.
// Io implements initialize(), write(bool), read(bool&); all return success.
template <class Io>
class LedPolicy {
public:
LedPolicy(Io& io, uint32_t boot_id) : io_(io), boot_id_(boot_id) {
initialized_ = io_.initialize();
if (!initialized_ || !io_.write(false) || !sample() || pad_)
latch_fault();
}
Result set(uint32_t request_id, bool power) {
if (!verify())
return Result::FAULT;
for (const auto& entry : history_) {
if (entry.used && entry.id == request_id)
return entry.power == power ? Result::DUPLICATE : Result::CONFLICT;
}
if (!io_.write(power) || !sample() || pad_ != power) {
latch_fault();
return Result::FAULT;
}
confirmed_ = power;
last_id_ = request_id;
has_last_ = true;
history_[next_] = {true, request_id, power};
next_ = (next_ + 1) % history_.size();
return Result::APPLIED;
}
Snapshot snapshot() {
if (!fault_)
verify();
else
sample();
return {boot_id_, fault_, confirmed_, known_, pad_, has_last_, last_id_};
}
private:
struct Entry {
bool used = false;
uint32_t id = 0;
bool power = false;
};
bool sample() {
bool level = false;
known_ = initialized_ && io_.read(level);
if (known_)
pad_ = level;
return known_;
}
bool verify() {
if (fault_)
return false;
if (!sample() || pad_ != confirmed_) {
latch_fault();
return false;
}
return true;
}
void latch_fault() {
fault_ = true;
known_ = false;
// Best effort, not a claim that the pin or the optical LED became low/off.
if (initialized_ && io_.write(false))
sample();
}
Io& io_;
uint32_t boot_id_, last_id_ = 0;
bool initialized_ = false, fault_ = false, confirmed_ = false, known_ = false, pad_ = false,
has_last_ = false;
std::array<Entry, 16> history_{};
std::size_t next_ = 0;
};
} // namespace a08
// ---- firmware/main/boards/bread-compact-wifi/audited_led_gpio.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <driver/gpio.h>
namespace a08 {
class GpioLed {
public:
explicit GpioLed(gpio_num_t pin) : pin_(pin) {}
bool initialize() {
// Preload output latch low before enabling the output. INPUT_OUTPUT keeps
// the input buffer enabled for digital pad feedback; this is not optics.
if (gpio_set_level(pin_, 0) != ESP_OK)
return false;
gpio_config_t c = {};
c.pin_bit_mask = 1ULL << pin_;
c.mode = GPIO_MODE_INPUT_OUTPUT;
c.pull_up_en = GPIO_PULLUP_DISABLE;
c.pull_down_en = GPIO_PULLDOWN_DISABLE;
c.intr_type = GPIO_INTR_DISABLE;
configured_ = gpio_config(&c) == ESP_OK;
return configured_;
}
bool write(bool on) { return configured_ && gpio_set_level(pin_, on ? 1 : 0) == ESP_OK; }
bool read(bool& level) {
if (!configured_)
return false;
level = gpio_get_level(pin_) != 0;
return true;
}
private:
gpio_num_t pin_;
bool configured_ = false;
};
} // namespace a08参考:[8] 固定设备侧MCP:参数转换、主任务调度与结果[9] 固定McpTool API/PropertyList与错误路径
完整参数校验与MCP注册
以下逐字对应下载包当前文件;完整依赖、构建说明、许可和空物理记录一起提供。通用MCP核心两个改动文件也完整保留在firmware/main,A08-OVERLAY.json列出原/现SHA;这里的板级绑定不会让核心依赖具体板。
// ---- firmware/main/boards/bread-compact-wifi/audited_led_wire.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <cJSON.h>
#include <cmath>
#include <cstring>
namespace a08 {
inline bool own_tool(const char* name) {
return std::strcmp(name, "self.lab_led.set") == 0 ||
std::strcmp(name, "self.lab_led.get_state") == 0;
}
// Called before the fixed upstream PropertyList decoder discards unknown keys
// and converts JSON numbers to int. This is a parsed-arguments contract, not an
// authentication layer, general JSON parser, or a replacement for cJSON.
inline const char* validate_arguments(const char* name, const cJSON* args) {
const bool set = std::strcmp(name, "self.lab_led.set") == 0;
if (!args)
return set ? "A08 arguments required" : nullptr;
if (!cJSON_IsObject(args))
return "A08 arguments must be object";
bool power_seen = false, id_seen = false;
unsigned count = 0;
for (const cJSON* p = args->child; p; p = p->next) {
if (++count > 2 || !p->string)
return "A08 too many arguments";
if (!set)
return "A08 query takes no arguments";
if (std::strcmp(p->string, "power") == 0) {
if (power_seen || !cJSON_IsBool(p))
return "A08 power must be one boolean";
power_seen = true;
} else if (std::strcmp(p->string, "request_id") == 0) {
if (id_seen || !cJSON_IsNumber(p) || !std::isfinite(p->valuedouble) ||
p->valuedouble < 0 || p->valuedouble > 2147483647.0 ||
p->valuedouble != static_cast<double>(p->valueint))
return "A08 request_id must be one integer in range";
id_seen = true;
} else
return "A08 unknown argument";
}
if (set && (!power_seen || !id_seen))
return "A08 missing argument";
char* canonical = cJSON_PrintUnformatted(args);
if (!canonical)
return "A08 serialization allocation failed";
const size_t bytes = std::strlen(canonical);
cJSON_free(canonical);
if (bytes > 64)
return "A08 canonical arguments exceed64 bytes";
return nullptr;
}
} // namespace a08
// ---- firmware/main/boards/bread-compact-wifi/audited_led_mcp.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <esp_log.h>
#include "audited_led_policy.h"
#include "audited_led_wire.h"
#include "mcp_server.h"
namespace a08 {
inline cJSON* snapshot_json(const Snapshot& s) {
cJSON* j = cJSON_CreateObject();
if (!j)
throw std::runtime_error("A08 allocation failed");
cJSON_AddNumberToObject(j, "boot_id", s.boot_id);
cJSON_AddBoolToObject(j, "fault", s.fault);
cJSON_AddBoolToObject(j, "last_confirmed_power", s.last_confirmed_power);
if (s.pad_known)
cJSON_AddBoolToObject(j, "digital_pad_level", s.pad_level);
else
cJSON_AddNullToObject(j, "digital_pad_level");
if (s.has_last_request)
cJSON_AddNumberToObject(j, "last_applied_request_id", s.last_request);
else
cJSON_AddNullToObject(j, "last_applied_request_id");
cJSON_AddStringToObject(j, "feedback", "digital_gpio_only_not_optical");
cJSON_AddNumberToObject(j, "dedup_entries", 16);
return j;
}
template <class Io>
void register_tools(McpServer& server, LedPolicy<Io>& policy) {
auto* set_tool = new McpTool(
"self.lab_led.set",
"Set only the external laboratory LED. power is boolean. request_id is an integer "
"0..2147483647. Use a new id for a new command; retry with the same id and power. Cache "
"holds last16 successful ids for this boot only. Never use this example for a safety "
"actuator.",
PropertyList({Property("power", kPropertyTypeBoolean),
Property("request_id", kPropertyTypeInteger, 0, 2147483647)}),
[&policy](const PropertyList& args) -> ReturnValue {
const bool power = args["power"].template value<bool>();
const auto id = static_cast<uint32_t>(args["request_id"].template value<int>());
const Result result = policy.set(id, power);
ESP_LOGI("a08", "SET request=%lu requested=%d outcome=%s",
static_cast<unsigned long>(id), power,
result == Result::APPLIED ? "APPLIED"
: result == Result::DUPLICATE ? "DUPLICATE"
: result == Result::CONFLICT ? "CONFLICT"
: "FAULT");
if (result == Result::CONFLICT)
throw std::runtime_error("A08 request id conflict");
if (result == Result::FAULT)
throw std::runtime_error("A08 GPIO fault; repair and RESET");
const Snapshot state = policy.snapshot();
ESP_LOGI("a08", "PAD boot=%lu confirmed=%d known=%d value=%d fault=%d",
static_cast<unsigned long>(state.boot_id), state.last_confirmed_power,
state.pad_known, state.pad_level, state.fault);
if (state.fault)
throw std::runtime_error("A08 GPIO fault; repair and RESET");
cJSON* j = snapshot_json(state);
cJSON_AddNumberToObject(j, "request_id", id);
cJSON_AddBoolToObject(j, "requested_power", power);
cJSON_AddBoolToObject(j, "duplicate", result == Result::DUPLICATE);
return j;
});
set_tool->set_validator(
[](const cJSON* args) { return validate_arguments("self.lab_led.set", args); });
server.AddTool(set_tool);
auto* query_tool = new McpTool(
"self.lab_led.get_state",
"Query the laboratory LED digital pad and latched fault. No arguments. Digital feedback "
"does not prove visible light. A fault needs repair and RESET; a query does not clear it.",
PropertyList(), [&policy](const PropertyList&) -> ReturnValue {
const Snapshot state = policy.snapshot();
ESP_LOGI("a08", "QUERY boot=%lu confirmed=%d known=%d value=%d fault=%d",
static_cast<unsigned long>(state.boot_id), state.last_confirmed_power,
state.pad_known, state.pad_level, state.fault);
return snapshot_json(state);
});
query_tool->set_validator(
[](const cJSON* args) { return validate_arguments("self.lab_led.get_state", args); });
server.AddTool(query_tool);
}
} // namespace a08参考:[8] 固定设备侧MCP:参数转换、主任务调度与结果[9] 固定McpTool API/PropertyList与错误路径
完整本板注册与初始化
以下逐字对应下载包当前文件;完整依赖、构建说明、许可和空物理记录一起提供。通用MCP核心两个改动文件也完整保留在firmware/main,A08-OVERLAY.json列出原/现SHA;这里的板级绑定不会让核心依赖具体板。
// ---- firmware/main/boards/bread-compact-wifi/compact_wifi_board.cc ----
#include <esp_random.h>
#include "application.h"
#include "assets/lang_config.h"
#include "audited_led_gpio.h"
#include "audited_led_mcp.h"
#include "button.h"
#include "codecs/no_audio_codec.h"
#include "config.h"
#include "display/oled_display.h"
#include "led/single_led.h"
#include "mcp_server.h"
#include "system_reset.h"
#include "wifi_board.h"
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_panel_vendor.h>
#ifdef SH1106
#include <esp_lcd_panel_sh1106.h>
#endif
#define TAG "CompactWifiBoard"
class CompactWifiBoard : public WifiBoard {
private:
i2c_master_bus_handle_t display_i2c_bus_;
esp_lcd_panel_io_handle_t panel_io_ = nullptr;
esp_lcd_panel_handle_t panel_ = nullptr;
Display* display_ = nullptr;
Button boot_button_;
Button touch_button_;
Button volume_up_button_;
Button volume_down_button_;
void InitializeDisplayI2c() {
i2c_master_bus_config_t bus_config = {
.i2c_port = (i2c_port_t)0,
.sda_io_num = DISPLAY_SDA_PIN,
.scl_io_num = DISPLAY_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = {
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &display_i2c_bus_));
}
void InitializeSsd1306Display() {
// SSD1306 config
esp_lcd_panel_io_i2c_config_t io_config = {
.dev_addr = 0x3C,
.on_color_trans_done = nullptr,
.user_ctx = nullptr,
.control_phase_bytes = 1,
.dc_bit_offset = 6,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
.flags =
{
.dc_low_on_data = 0,
.disable_control_phase = 0,
},
.scl_speed_hz = 400 * 1000,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(display_i2c_bus_, &io_config, &panel_io_));
ESP_LOGI(TAG, "Install SSD1306 driver");
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = GPIO_NUM_NC;
panel_config.bits_per_pixel = 1;
esp_lcd_panel_ssd1306_config_t ssd1306_config = {
.height = static_cast<uint8_t>(DISPLAY_HEIGHT),
};
panel_config.vendor_config = &ssd1306_config;
#ifdef SH1106
ESP_ERROR_CHECK(esp_lcd_new_panel_sh1106(panel_io_, &panel_config, &panel_));
#else
ESP_ERROR_CHECK(esp_lcd_new_panel_ssd1306(panel_io_, &panel_config, &panel_));
#endif
ESP_LOGI(TAG, "SSD1306 driver installed");
// Reset the display
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_));
if (esp_lcd_panel_init(panel_) != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize display");
display_ = new NoDisplay();
return;
}
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_, false));
// Set the display to on
ESP_LOGI(TAG, "Turning display on");
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true));
display_ = new OledDisplay(panel_io_, panel_, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});
touch_button_.OnPressDown([this]() {
Application::GetInstance().StartListening();
});
touch_button_.OnPressUp([this]() {
Application::GetInstance().StopListening();
});
volume_up_button_.OnClick([this]() {
auto codec = GetAudioCodec();
auto volume = codec->output_volume() + 10;
if (volume > 100) {
volume = 100;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume));
});
volume_up_button_.OnLongPress([this]() {
GetAudioCodec()->SetOutputVolume(100);
GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME);
});
volume_down_button_.OnClick([this]() {
auto codec = GetAudioCodec();
auto volume = codec->output_volume() - 10;
if (volume < 0) {
volume = 0;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume));
});
volume_down_button_.OnLongPress([this]() {
GetAudioCodec()->SetOutputVolume(0);
GetDisplay()->ShowNotification(Lang::Strings::MUTED);
});
}
// 物联网初始化,逐步迁移到 MCP 协议
void InitializeTools() {
static a08::GpioLed led_io(LAMP_GPIO);
static a08::LedPolicy<a08::GpioLed> led(led_io, esp_random());
static const bool registered = []() {
a08::register_tools(McpServer::GetInstance(), led);
return true;
}();
(void)registered;
}
public:
CompactWifiBoard() :
boot_button_(BOOT_BUTTON_GPIO),
touch_button_(TOUCH_BUTTON_GPIO),
volume_up_button_(VOLUME_UP_BUTTON_GPIO),
volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) {
InitializeDisplayI2c();
InitializeSsd1306Display();
InitializeButtons();
InitializeTools();
}
virtual Led* GetLed() override {
static SingleLed led(BUILTIN_LED_GPIO);
return &led;
}
virtual AudioCodec* GetAudioCodec() override {
#ifdef AUDIO_I2S_METHOD_SIMPLEX
static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN);
#else
static NoAudioCodecDuplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN);
#endif
return &audio_codec;
}
virtual Display* GetDisplay() override {
return display_;
}
};
DECLARE_BOARD(CompactWifiBoard);参考:[8] 固定设备侧MCP:参数转换、主任务调度与结果[9] 固定McpTool API/PropertyList与错误路径
资料来源与版本说明
固定上游、实际SDK/managed和厂家具体电路章节已核;主机/构建记录不替代指定硬件、HA/云端、光学或声音实测。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。
- S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB
- ICS43434 DS-000069 v1.2:电气/边沿/24bit/启动
- PID6049固定PCB:3V、SEL与47k数据下拉
- MAX98357A/MAX98357B Rev7:供电/采样率/SD/GAIN/BTL
- PID3006固定单声道PCB:VIN/SD1M/BTL端子
- PID1313扬声器2017-02-14厂家规格:8Ω额定1W
- PID6049产品页:具体型号与停产边界
- 固定设备侧MCP:参数转换、主任务调度与结果
- 固定McpTool API/PropertyList与错误路径
- 固定bread board:原LampController所有权
- IDF5.5.5 GPIO:INPUT_OUTPUT回读与错误返回
- 实际Registry cJSON来源SBOM commit:1.7.19~2
- ESP32-S3-WROOM-1/1U v1.8:N16R8规格表
- PID4440固定revB PCB:上拉/电平转换/自动复位/128×32
- PID4440具体产品:128×32、自动复位
- 固定Application:parsed mcp payload派发
- 固定SDK兼容字段依据
- 固定AudioService:decoder采样率与输出重采样路径
- 实际esp_audio_effects1.3.0~1来源:rate converter格式/采样率/API约束
