From 267bb7317212e3bb22a8b9092ded85b338184d27 Mon Sep 17 00:00:00 2001 From: kevin Date: Sun, 5 Apr 2026 02:06:09 +0800 Subject: [PATCH] Signed-off-by: kevin --- .workbuddy/expert-history.json | 28 ----- .workbuddy/memory/2026-03-29.md | 196 -------------------------------- .workbuddy/memory/2026-03-30.md | 42 ------- .workbuddy/memory/MEMORY.md | 140 ----------------------- 4 files changed, 406 deletions(-) delete mode 100644 .workbuddy/expert-history.json delete mode 100644 .workbuddy/memory/2026-03-29.md delete mode 100644 .workbuddy/memory/2026-03-30.md delete mode 100644 .workbuddy/memory/MEMORY.md diff --git a/.workbuddy/expert-history.json b/.workbuddy/expert-history.json deleted file mode 100644 index 5bbd6c1..0000000 --- a/.workbuddy/expert-history.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "version": 2, - "sessions": { - "e341ab1f344d4f54946c2ba835bc7aa3": [ - { - "expertId": "EmbeddedFirmwareEngineer", - "name": "Owen", - "profession": "嵌入式固件工程师", - "avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/EmbeddedFirmwareEngineer/EmbeddedFirmwareEngineer.png", - "promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/EmbeddedFirmwareEngineer/EmbeddedFirmwareEngineer_zh.md", - "usedAt": 1774782976490, - "industryId": "all" - } - ], - "9ab75f98b70b43029fd6c3bae3934dea": [ - { - "expertId": "EmbeddedFirmwareEngineer", - "name": "Owen", - "profession": "嵌入式固件工程师", - "avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/EmbeddedFirmwareEngineer/EmbeddedFirmwareEngineer.png", - "promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/EmbeddedFirmwareEngineer/EmbeddedFirmwareEngineer_zh.md", - "usedAt": 1774843083830, - "industryId": "all" - } - ] - }, - "lastUpdated": 1774866488669 -} \ No newline at end of file diff --git a/.workbuddy/memory/2026-03-29.md b/.workbuddy/memory/2026-03-29.md deleted file mode 100644 index 7fd714f..0000000 --- a/.workbuddy/memory/2026-03-29.md +++ /dev/null @@ -1,196 +0,0 @@ -# 2026-03-29 工作日志 - -## 项目初始扫描 -- 首次读取 firmware-2.7.15.567b8ea 代码结构 -- 确认项目为 Meshtastic 官方固件的自定义 fork -- 发现 4 个自研板卡变体:esp32c3_moonshine / moonshine(带ADC/USB CDC)/ moonshine_mv(全功能)/ moonshine_travelers -- 详细记录写入 MEMORY.md - -## TCA9535PWR 矩阵键盘驱动实现 -- 目标板卡:esp32c3_moonshine_travelers -- 新建驱动文件: - - `src/input/TCA9535ButtonThread.h` - 驱动类声明,含寄存器宏、extern 指针 - - `src/input/TCA9535ButtonThread.cpp` - 4×4 矩阵扫描实现 -- 修改 `variants/esp32c3/diy/esp32c3_moonshine_travelers/variant.h`: - - 添加 `HAS_TCA9535_BUTTON` 宏 - - 添加 `TCA9535_KEY_MAP`(4×4 矩阵行优先映射) - - `TCA9535_INT_PIN=5`(GPIO5,低电平有效,下降沿触发) -- 修改 `src/main.cpp`:include TCA9535ButtonThread.h,在 setupModules() 后初始化线程 -- I²C 地址:0x20(A0=A1=A2=0),与 SH1106 屏幕共用 Wire(SDA=0, SCL=1) -- 矩阵接法:P0.0~P0.3 行输出,P0.4~P0.7 列输入,逐行拉低扫描,50µs 行间延时 - -## LTO 链接错误修复 -- 编译报 `undefined reference to TCA9535ButtonThread::*`(LTO -flto 导致) -- 原因:.h/.cpp 中的 `#if defined(HAS_TCA9535_BUTTON)` 守卫导致在部分编译单元中符号被丢弃 -- 修复:去掉 .h/.cpp 中的条件守卫,让类定义和实现始终编译 - - main.cpp 中的实例化和初始化仍由 `#ifdef HAS_TCA9535_BUTTON` 控制 - - extern 指针声明保留在 `#ifdef` 中,非目标板卡不会引用 - -## RadioLib LoRa RST 通过 TCA9535 P1.4 控制 -- 自定义 HAL 子类 `TCA9535GpioHal` 拦截虚拟引脚 200,转发到 I²C -- `tca9535LoraReset(bool high)` 静态函数,read-modify-write P1.4 - -## 电源管理(P1.2 + P1.3) -- P1.2 = POWER_EN 输出,高电平有效,驱动 MOS 管维持供电 -- P1.3 = POWER_BOOT 输入,低电平有效(按键按下接地) -- 开机流程:物理按键 → MOS 导通 → ESP32 得电 → init() 等 P1.3 持续按住 2 秒 → POWER_EN 拉高 - - 未按够 2 秒松开 → 不拉高 POWER_EN → 断电 -- 关机流程:运行中 P1.3 持续按住 2 秒 → POWER_EN 拉低 → 断电 -- 电源状态机:BOOT_PENDING → RUNNING → SHUTDOWN_PENDING -- init() 中 P1 config = 0xEB(P1.2=输出, P1.3=输入, P1.4=输出) -- 新增 `tca9535ReadPowerBoot()` 静态函数,读取 P1.3 输入状态 -- 新增 `tca9535PowerEn(bool on)` 静态函数,read-modify-write P1.2 - -## GPS 启用(GP-02 模块) -- 问题:`tca9535GpsEn(false)` 在 TCA9535 init 中关闭了 GPS 电源,而 GPS::setup()->probe() 时 powerState=GPS_OFF,writePinEN(true) 只在 GPS_ACTIVE 时才调用 → probe 时模块没电 -- 修复:在 main.cpp 的 `createGps()` + enablePin 桥接之后,立即 `tca9535GpsEn(true)` + `delay(1000)` 给 GP-02 冷启动上电时间 -- GP-02 接线:VCC→3.3V, GND→GND, TX→GPIO20(ESP RX), RX→GPIO21(ESP TX),EN/RST/PPS/VRTC 悬空即可 -- 编译验证通过(esp32c3_moonshine_travelers SUCCESS) - -## GPS 修复:P1.6/P1.7 默认高电平 + RX/TX 交换 -- 用户要求 GPS_RST(P1.6) 和 GPS_EN(P1.7) 通电后直接设为高电平 - - TCA9535ButtonThread.cpp init() 中改为 `tca9535GpsEn(true)` 和 `tca9535GpsReset(true)` - - 移除 main.cpp 中的 delay workaround -- GPS 仍然检测不到 → 交换 GPS_RX_PIN(21→20) 和 GPS_TX_PIN(20→21) -- 更新 CHANGELOG.md,提交 311232c,push - -## 九宫格键盘映射 -- 添加 4×4 矩阵中的数字键:key0-2=1-3, key4-6=4-6, key8-10=7-9, key12=*, key13=0, key14=# -- 方向键保留:key3=UP, key7=DOWN, key11=LEFT, key15=RIGHT -- 新增 `TCA9535_KEY_CHAR_MAP` 数组,`dispatchEvent()` 传 `kbchar` 参数 - -## ⚠️ TCA9535 MATRIXKEY 崩溃修复 + 键盘输入修复 -- 崩溃现象:按下矩阵键后立即 Load access fault at 0x40058766 -- 根因:`TCA9535_KEY_CHAR_MAP` 传的是 ASCII 字符(如 '0'=0x30, '9'=0x39),但 `CannedMessageModule` 把 `kbchar` 当 1-based 索引用(`currentMessageIndex = event->kbchar - 1`)。ASCII '0'=48 → index=47 → 数组越界 → 野指针 → 崩溃 -- 参考 RAK14004 实现:`kbI2cBase.cpp` 中 `PrintDataBuf = aCount*4 + bCount + 1` 传的是 1-16 的索引 -- 第一版修复(错误方向):把 TCA9535_KEY_CHAR_MAP 改为传索引 1-12 - - 问题:这样矩阵键只能选 canned message,无法进入文本输入 -- 正确修复: - 1. `TCA9535_KEY_CHAR_MAP` 恢复为 ASCII 字符('0'-'9', '*', '#') - 2. `CannedMessageModule::handleInputEvent()` 修改 MATRIXKEY 拦截逻辑: - - 如果 `kbchar` 是可打印 ASCII(32-126):不拦截,让事件 fall through 到正常输入路径 - → INACTIVE 状态进入 FREETEXT,FREETEXT 状态追加字符 - - 如果 `kbchar` 是 1-based 索引(如 RAK14004):继续走 canned message 选择路径 - - 加边界检查 `idx < 0 || idx >= messagesCount` 防御性忽略 - -## P1.1 充电检测 (CHARGE_DET) -- P1.1 = CHARGE_DET 输入,高电平=正在充电 -- P1 config 从 0x8B 改为 0x8D(P1.1 配置为输入) -- variant.h 新增 `TCA9535_CHARGE_DET_PIN (1u << 1)` -- TCA9535ButtonThread.h 新增 `tca9535ReadChargeDet()` 静态内联函数 -- TCA9535ButtonThread.cpp 的 runOnce() 每 2 秒轮询 P1.1,更新全局 `tca9535IsCharging` -- Power.cpp 的 `AnalogBatteryLevel::isCharging()` 读取 `tca9535IsCharging`(`#ifdef TCA9535_CHARGE_DET_PIN`) -- Power.cpp include 条件从 `TCA9535_LORA_RST_VIRTUAL_PIN` 改为 `HAS_TCA9535_BUTTON` - -## 快捷回复 ↔ 九宫格输入导航 -- INACTIVE:UP/DOWN 进入快捷回复列表 -- ACTIVE(列表):LEFT/RIGHT 进入九宫格 FREETEXT -- FREETEXT:LEFT/RIGHT 回列表,保留输入文字 -- `*` = 退格,`#` = 切换输入模式 -- `isUpEvent()`/`isDownEvent()` 移除 ACTIVE 状态对 LEFT/RIGHT 的映射 - -## T9 Multi-tap 输入法实现 -- 三种模式:abc(小写) / ABC(大写) / 123(数字),按 `#` 循环切换 -- 数字键 2-9 multi-tap 选字母,800ms 超时自动确认 -- 数字键 0 = 空格,数字键 1 = 标点循环(`. , ! ?`) -- 屏幕左下角显示当前输入模式标签 -- 光标位置实时预览 multi-tap 字符 - -## ⚠️ T9 commitMultiTap 递归崩溃修复 -- 崩溃:`commitMultiTap()` 内调用 `runOnce()` → `runOnce()` 检测超时又调用 `commitMultiTap()` → 无限递归 → 栈溢出 → Store access fault -- 反汇编确认 MEPC=0x40383d3e(systimer 中间件),S11=0x42066020 → `CannedMessageModule::runOnce()` -- 修复:添加 `committingMultiTap` 重入保护标志,`runOnce()` 中检查 `!committingMultiTap` 再调用 `commitMultiTap()` - -## ⚠️ T9 payload 残留 + 显示修复 -- 连续按两次同一按键回到主页面:疑似 payload 未清零导致 runOnce 非预期行为 + showMultiTapPreview 无用 String 分配导致堆碎片 -- 修复: - 1. 所有 `runOnce()` 调用后立即 `payload = 0`(`*` 退格、可打印字符、INPUT_BROKER_BACK 退格、commitMultiTap) - 2. `runOnce()` FREETEXT 分支末尾加 `payload = 0` 作为最后防线 - 3. 移除 `showMultiTapPreview()` 中无用 `previewText` String 分配(预览字符由 drawFrame 渲染) -- 输入法模式标签(abc/ABC/123)从左上角移到屏幕右下角(TEXT_ALIGN_RIGHT) - -## ⚠️ 任意按两个按键回到主页面 — REGENERATE_FRAMESET 重复触发 focus 丢失 -- **根因**:`commitMultiTap()` → `runOnce()` → `notifyObservers(REGENERATE_FRAMESET)` → `Screen::setFrames(FOCUS_MODULE)` 消费了 `requestFocus()` → `_requestingFocus=false` - 然后同一调用链中后续的 `notifyObservers(REGENERATE_FRAMESET)` → `setFrames(FOCUS_MODULE)` → `focusedModule=255`(默认值)→ `ui->switchToFrame(255)` → 跳到主页面 -- **关键机制**:`MeshModule::isRequestingFocus()` 是一次性的——调用一次就清零 `_requestingFocus`。`setFrames(FOCUS_MODULE)` 中 `focusedModule` 默认 255,没有 focus request 就跳第一帧 -- 修复(4处): - 1. `commitMultiTap()` 末尾移除多余的 `notifyObservers(REGENERATE_FRAMESET)` + `forceDisplay()`(`runOnce()` 已触发) - 2. `showMultiTapPreview()` 改用 `REDRAW_ONLY` 代替 `REGENERATE_FRAMESET` - 3. `#` 键(切换输入模式)`commitMultiTap()` 后改用 `REDRAW_ONLY` - 4. `runOnce()` 中 multi-tap 超时自动 `commitMultiTap()` 后直接 return,跳过后续 `notifyObservers` - 5. LEFT/RIGHT 返回 ACTIVE 列表时补加 `requestFocus()`(因为 `commitMultiTap` 已消费前一个 focus) -- `commitMultiTap()` 改为返回 `bool` 表示是否实际提交了字符 -- 编译验证通过(SUCCESS 33s) - -## ⚠️ T9 输入法逻辑修正 — 光标+大小写+数字模式 -- **问题1:光标不跳转**:`drawFrame` 中 preview 字符插入到 `displayText` 但 `displayCursor` 没有 +1,导致光标 `_` 画在 preview 字符之前而不是之后。修复:有 preview 时 `displayCursor = cursor + 1` -- **问题2:大小写模式下出现数字**:旧 `t9Map` 每个 key 的 index 0 是数字本身,在 UPPER/LOWER 模式下 multiTapIndex=0 时显示的是数字 -- **问题3:0=空格、1=标点、数字只在数字模式输入**:用户要求的行为 -- **修复**: - 1. `t9Map` 重命名为 `t9LetterMap`,去掉 index 0 的数字,每个 key 只包含字母模式下的字符 - - 0: `{" ", nullptr}`(空格) - - 1: `{".", ",", "!", "?", nullptr}`(标点) - - 2-9: 纯字母 - 2. DIGIT 模式下数字键直接输出数字,不走 multi-tap 循环(`payload = '0' + key; runOnce()`) - 3. UPPER/LOWER 模式下使用 `t9LetterMap` + multi-tap 循环,应用大小写转换 - 4. `commitMultiTap()` 简化:不再有 index 0 是数字的特殊情况 - 5. `drawFrame` preview 逻辑同步简化 + 光标 +1 -- 编译验证通过(SUCCESS 36s) - -## UTF8 常用中文字库 -- **需求**:ESP32-C3 + SH1106 OLED 上显示中文文本 -- **方案**:12×12 点阵位图字库,XBM 格式(row-major, MSB first) -- **生成工具**:`tools/gen_chinese_font.mjs`(Node.js + @napi-rs/canvas) - - 字体源:Windows msyh.ttc(微软雅黑) - - 字符集:U+4E00-U+74FF(CJK 常用汉字)+ 中文标点 + 全角数字/字母 = 10067 字形 - - 每字形 2 bytes codepoint + 24 bytes bitmap = 26 bytes -- **输出文件**:`src/graphics/fonts/ChineseFont12x12.h` - - 数据数组:`cfont12_data[]`(PROGMEM,按 codepoint 升序排列) - - 查找函数:`cfont12_find(cp)` — 二分搜索,O(log n) - - 渲染函数:`cfont12_draw(display, x, y, cp)` — 用 setPixel 逐像素绘制 - - UTF-8 解码:`cfont12_utf8(p, &len)` — 返回 codepoint 和字节长度 - - 混合绘制:`cfont12_drawStr(display, x, y, s)` — ASCII 用 drawString,CJK 用 cfont12 -- **集成点**:`CannedMessageModule.cpp` drawFrame FREETEXT 部分 - - text token 渲染:逐字符判断是否 CJK,CJK 用 cfont12_draw,ASCII 用 drawString - - word wrap 宽度计算:CJK 字符宽度 = CFONT_W(12),ASCII 用 getStringWidth -- **Flash 占用**:+256 KB(data 段),剩余 ~1.59 MB(4MB flash) -- 编译验证通过(SUCCESS 51s) - -## ⚠️ 分区表修复 — 固件超出 app 分区无法启动 -- **现象**:烧录后 ESP32-C3 反复重启,卡在 ROM bootloader,无用户程序日志 -- **根因**:256KB 中文字库使固件从 ~2.31MB 增长到 ~2.57MB,超出原 app 分区 0x250000 (2.375MB) -- **修复**:调整 `partition-table.csv` - - app: 0x250000 → 0x2C0000 (2.75MB) - - flashApp (OTA): 0x0A0000 → 0x030000 (192KB) - - spiffs: 0x100000 不变 (1MB) -- 编译验证通过(SUCCESS 34s) - -## ⚠️ 中文字形像素错乱修复(完整历程) -- **现象**:中文字形隐约可见但局部像素缺失/多余 → 改 alpha 后变全白方块 → 修复后字体太粗 -- **根因**:字库生成判断像素的逻辑有误 - - 第一版用 R 通道 `< 128`(白色背景 + 黑色前景):抗锯齿边缘灰度 ≈128 在阈值边界,不稳定 - - 第二版用 alpha `> 40`(透明背景 + 黑色前景):alpha 40 太低,抗锯齿边缘全被纳入 → 字体太粗 -- **最终修复**:透明背景 + 黑色前景,alpha 阈值从 40 提高到 **128** - - 透明区域 alpha=0 → 不标记 - - 黑色字形 alpha=255 → 标记 - - 抗锯齿边缘 alpha<128 → 不标记(去掉毛边,字形更锐利) -- ⚠️ 关键:`imageSmoothingEnabled` 对 canvas fillText 无效,不能用它关抗锯齿 -- **Unicode 覆盖扩展**:U+4E00-U+74FF(10067字) → U+4E00-U+9FFF(21075字) - - 修复"鬼"(U+9B3C)、"界"(U+754C)等常用字缺失 - - 覆盖 GB2312 全集 6763 字 + CJK 统一汉字扩展 - - Flash: 535KB, 固件总大小 2.65MB / 2.75MB (92%) -- **问题**:收到的消息包含中文时无法正常显示 -- **根因**:`MessageRenderer.cpp` 的 `generateLines()` 逐字节处理 UTF-8,CJK 三字节字符被拆散;`drawStringWithEmotes()` 用 `drawString()` 渲染,不支持 CJK -- **修复**(3处改动): - 1. 添加辅助函数 `measureMixedWidth()`(混合宽度计算)和 `drawMixedString()`(混合渲染) - 2. `generateLines()` 改为 UTF-8 感知的逐字符循环:CJK 字符作为独立 word 处理,宽度用 `measureMixedWidth()` - 3. `drawStringWithEmotes()` 文本段渲染改用 `drawMixedString()` - 4. `calculateLineHeights()` 检测 CJK 字符,行高取 max(FONT_HEIGHT_SMALL, CFONT_H) -- 编译通过,flash 仅增 ~1KB(固件 2.57MB < app 分区 2.75MB) - -## ⚠️ 开机确认逻辑导致无法启动 -- **现象**:`TCA9535: Boot not confirmed, shutting down` → deep sleep → 开不了机 -- **根因**:main.cpp 中 Wire.begin() 后的"开机确认窗口"要求用户在 3 秒内持续按住按键 2 秒,否则主动关机 - - 这违背了物理开机设计:按下按键 → MOS 得电 → ESP32 启动 → 用户很快松手 - - 3 秒窗口内几乎不可能持续按住 2 秒 -- **修复**:删除开机确认窗口代码,只保留 `tca9535PowerEn(true)` 立即锁住供电 diff --git a/.workbuddy/memory/2026-03-30.md b/.workbuddy/memory/2026-03-30.md deleted file mode 100644 index d2a3853..0000000 --- a/.workbuddy/memory/2026-03-30.md +++ /dev/null @@ -1,42 +0,0 @@ -# 2026-03-30 日志 - -## 编译环境搭建(esp32c3_moonshine_travelers) - -- Python 3.14.0 已预装(C:\Python314) -- 安装 PlatformIO 6.1.19:`pip install platformio` -- PlatformIO Scripts 路径:`C:\Users\wuwen\AppData\Roaming\Python\Python314\Scripts` -- 已写入用户 PATH 永久生效(无需每次手动加) -- 平台工具链 `espressif32@6.11.0`、`framework-arduinoespressif32`、`toolchain-riscv32-esp` 已存在(之前工程留下) -- 固件目录:`code/firmware-2.7.15.567b8ea`,编译命令:`pio run -e esp32c3_moonshine_travelers` - -## 遇到问题:Windows MAX_PATH 限制 - -- SparkFun ICM-20948 库解压时路径超过 260 字符 → `PackageException` -- 根因:`examples/Arduino/Example10_DMP_FastMultipleSensors/` 路径过长 -- 修复方案:需要管理员权限执行以下命令启用 Windows 长路径支持: - ```powershell - Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 - ``` -- 后续:用户反映编译成功(Windows 长路径问题已自行解决) - -## 调试:节点不发公钥/节点名字对端看不到 - -- **根本原因**:设备 Flash 中残留 `owner.is_licensed = true`(HamConfig 曾通过 App 下发并持久化) -- **公钥不发**:`NodeInfoModule::allocReply()` 中 `is_licensed==true` 时主动清零公钥字段(Ham 法规要求) -- **名字对端不可见**:`handleReceivedProtobuf()` 检查 `p.is_licensed != owner.is_licensed` 时直接丢包 -- **解决**:擦除 Flash NVS 分区(`pio run -t erase`)后重烧,或通过 App 关闭 Licensed 开关 - -## 修复:切换 role 后 NodeInfo/公钥仍不广播(CLIENT_HIDDEN 遗留问题) - -### Bug 1:owner.role 不随 config.device.role 同步 -- **位置**:`AdminModule.cpp` `handleSetConfig()` device_tag 分支 -- **原因**:改 role 时只更新 `config.device.role`,`owner.role`(广播字段)从不更新 -- **修复**:在 `installRoleDefaults()` 调用后加 `owner.role = config.device.role;` - -### Bug 2:从 CLIENT_HIDDEN 切回 CLIENT 后广播间隔不恢复 -- **位置**:`NodeDB.cpp` `installRoleDefaults()` -- **原因**:CLIENT_HIDDEN 把 `node_info_broadcast_secs` 设为 `INT32_MAX`,切回 CLIENT 时没有对应的重置分支 -- **修复**:新增 `CLIENT` / `CLIENT_MUTE` 分支,恢复 `default_node_info_broadcast_secs` 等默认值 - -### 额外确认 -- **公钥不生成的前置条件**:`config.lora.region == UNSET` 时 NodeDB::init() 跳过 PKI keygen;需先通过 App 设置 region 才会生成密钥对 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md deleted file mode 100644 index 5e34216..0000000 --- a/.workbuddy/memory/MEMORY.md +++ /dev/null @@ -1,140 +0,0 @@ -# 项目长期记忆 - -## 项目概述 -- **项目**:LoRa Meshtastic 固件开发 -- **主代码目录**:`code/meshtastic_firmware`(基于 Meshtastic 官方最新 develop 分支) -- **旧代码目录**:`code/firmware-2.7.15.567b8ea`(基于 Meshtastic 官方固件 v2.7.15,已废弃) -- **构建系统**:PlatformIO + ESP-IDF (Arduino framework for ESP32) - -## 硬件平台 & 自定义板卡 -项目有多个自研板卡(均为 ESP32-C3 + LoRa 模块组合): - -### esp32c3_moonshine(基础版) -- MCU:ESP32-C3(esp32-c3-devkitm-1) -- LoRa:E220-400M30S(LLCC68/SX1262/SX1268 三选一探测) -- 无屏幕、无 GPS -- SPI: SCK=10, MISO=6, MOSI=7, CS=8, RST=5, DIO1=3, BUSY=4 -- LED_POWER=12,DIO3 TCXO 1.8V,TCXO_OPTIONAL -- Flash: 4MB, dio 模式, 80MHz - -### esp32c3_moonshine(firmware 版本,variants 中) -- 同上基础版,增加: - - 支持 SX126X_RXEN=2(RX 使能 GPIO) - - 支持 E22_400M33S(SX126X_MAX_POWER=22, TX_GAIN=0) - - BATTERY_PIN=1, ADC_MULTIPLIER=2.0f - - USB CDC 启动:ARDUINO_USB_MODE=1, ARDUINO_USB_CDC_ON_BOOT=1 - -### esp32c3_moonshine_mv(带屏幕+GPS+多按键版) -- MCU:ESP32-C3 -- LoRa:RA-01SC-P(SETTING_MAX_POWER=29, TX_GAIN=26, SX126X_MAX_POWER=3) -- 屏幕:SSD1306(I2C: SDA=0, SCL=1) -- GPS:GPS_RX=21, GPS_TX=20, GPS_POWER_TOGGLE, PIN_GPS_EN=12 -- 多按键:PCF8574 IO 扩展器(地址 0x27,INT=9),映射 SELECT/UP/DOWN/LEFT/RIGHT/CANCEL -- NeoPixel:GPIO13, 1颗 NEO_GRB -- BATTERY_PIN=2 - -### esp32c3_moonshine_travelers(旅行者版) -- MCU:ESP32-C3 -- LoRa:RA-01SC-P(SETTING_MAX_POWER=3, TX_GAIN=0, SX126X_MAX_POWER=3) -- 屏幕:SH1106(I2C: SDA=0, SCL=1) -- GPS:GPS_RX=21, GPS_TX=20, GPS_RST=P1.6(TCA9535), GPS_EN=P1.7(TCA9535) - - GPS EN 通过 `GpioTca9535GpsEnPin` + `GpioUnaryTransformer` 桥接 `gps->enablePin` - - GPS RST 由 `tca9535GpsReset()` 控制,init 中释放(高电平) -- BATTERY_PIN=2 -- **GPIO9**:短按=SELECT,长按=无功能(`BUTTON_DISABLE_LONG_PRESS`) -- **TCA9535PWR 4×4 矩阵键盘**:与屏幕共用 I2C,A0=A1=A2=0,地址 0x20 - - 驱动:`src/input/TCA9535ButtonThread.h/.cpp` - - 矩阵接法:P0.0~P0.3 行输出(ROW0~ROW3),P0.4~P0.7 列输入(COL0~COL3) - - 扫描方式:逐行拉低输出,读列检测低电平,50µs 行间延时 - - 中断引脚:TCA9535_INT_PIN=5(GPIO5,低电平有效,下降沿触发) - - 映射(variant.h):key0-2=索引1-3, key4-6=索引4-6, key8-10=索引7-9, key12=索引10(*), key13=索引11(0), key14=索引12(#);key3=UP, key7=DOWN, key11=LEFT, key15=RIGHT - - **kbchar 字段语义**: - - RAK14004 传 1-based 索引(kbchar=1..16),走 canned message 选择路径 - - TCA9535 传 ASCII 字符('0'-'9','*','#'),走文本输入路径(INACTIVE→FREETEXT) - - `CannedMessageModule` 的 MATRIXKEY 拦截:可打印 ASCII 不拦截(fall through),索引才拦截 - - 不能传 ASCII 给索引路径!ASCII '0'=48 → index=47 → 数组越界 → 崩溃 - - SELECT 由 GPIO9 短按处理,CANCEL 由 POWER_BOOT 短按处理 - - **P1.2 = POWER_EN**:高电平有效,驱动 MOS 管维持供电 - - `tca9535PowerEn(bool on)` 静态函数,read-modify-write P1.2 - - **P1.3 = POWER_BOOT**:输入,低电平有效(按键按下接地) - - 开机:物理按键 → MOS → ESP32 得电 → **main.cpp Wire.begin() 后立即调用 `tca9535PowerEn(true)` 锁住供电** - - ⚠️ 曾经的设计错误:在 `tca9535ButtonThread::init()` 里等 P1.3 按住 2s 再拉高 POWER_EN,但 setup() 需要数秒初始化,用户早已松手导致 MOS 断电。已修复为在 Wire.begin() 后立即上锁。 - - 短按:派发 INPUT_BROKER_CANCEL - - 长按 2s:派发 INPUT_BROKER_SHUTDOWN(走系统关机流程 → Power::shutdown() → POWER_EN 拉低) - - `tca9535ReadPowerBoot()` 静态函数读取 P1.3 状态 - - **P1.4 = LoRa RST**:通过 TCA9535GpioHal 自定义 HAL 拦截虚拟引脚 200 转发到 I²C - - `tca9535LoraReset(bool high)` 静态函数,read-modify-write P1.4 - - `LORA_RESET = TCA9535_LORA_RST_VIRTUAL_PIN(200)`,RadioLib 的 `findChip()`/`reset()` 全链路走 I²C - - **P1.5 = 状态指示灯**:输出,低电平点亮 - - `tca9535StatusLed(bool on)` 静态函数,read-modify-write P1.5 - - init() 中配置为输出,默认熄灭(高电平) - - 在 TCA9535ButtonThread::runOnce() 中独立驱动,500ms 亮 + 500ms 灭 = 1 秒闪烁 - - 不再跟随 LED_PIN(已从 GpioSplitter 中移除) - - **P1.0 = 键盘背光**:输出,高电平点亮 - - `TCA9535_BIT_P10` 宏 = `(1u << 0)` - - `tca9535Backlight(bool on)` 静态函数,read-modify-write P1.0 - - init() 中默认熄灭(低电平) - - runOnce() 中驱动:按键按下时点亮,刷新 5 秒计时;5 秒无操作自动熄灭 - - **P1.1 = CHARGE_DET**:输入,高电平=正在充电 - - `TCA9535_CHARGE_DET_PIN` 宏 = `(1u << 1)`,在 variant.h 中定义 - - `tca9535ReadChargeDet()` 静态内联函数,读 P1.1 输入寄存器 - - `tca9535IsCharging` 全局 volatile bool,由 runOnce() 每 2 秒轮询更新 - - `Power.cpp` 的 `AnalogBatteryLevel::isCharging()` 和 `isVbusIn()` 均读取此变量 - - P1 config = 0x0A(P1.1=输入 CHARGE_DET, P1.3=输入 POWER_BOOT, 其余输出) - - ⚠️ 踩坑1:加 CHARGE_DET 后曾将 config 从 0x8B 改为 0x8D,导致 P1.2(POWER_EN) 被误配成输入(高阻),MOS 失控断电。正确值 0x0A - - ⚠️ 踩坑2:TP4057 充电芯片电压反串导致 P1.1 在未充电时仍读高电平,充电检测误报。需硬件修改解决。 -- **关机路径**:所有关机触发最终走 Power::shutdown() → tca9535PowerEn(false) → doDeepSleep() - -## 代码架构要点 -- **踩坑记录**:TCA9535 矩阵扫描 cols 计算中 `~` 运算符对 uint8_t 会整数提升为 int,导致高 4 位被污染。修复:`((~(p0In & 0xF0)) >> 4) & 0x0F`。见 scanMatrix()。 -- **踩坑记录**:`owner.role` 只在 `NodeDB::NodeDB()` 构造时同步一次(`owner.role = config.device.role`),`AdminModule::handleSetConfig()` 改 role 时不更新 `owner.role`,导致广播出去的 NodeInfo 里 role 仍是旧值。已修复:在 `AdminModule.cpp` device_tag 分支末尾加 `owner.role = config.device.role;`(2026-03-30) -- **踩坑记录**:`NodeDB::installRoleDefaults(CLIENT_HIDDEN)` 把 `node_info_broadcast_secs = INT32_MAX`,但 `installRoleDefaults(CLIENT)` 没有对应的恢复分支,切回 CLIENT 后 NodeInfo 永远不广播。已修复:新增 CLIENT/CLIENT_MUTE 分支恢复 `default_node_info_broadcast_secs`(2026-03-30) -- **踩坑记录**:`is_licensed = true` 时 `NodeInfoModule::allocReply()` 主动清零公钥,且接收端 `handleReceivedProtobuf()` 因 `is_licensed` mismatch 直接丢包。Flash 残留旧 HamConfig 时会触发此问题,需擦 Flash 或 App 关闭 Licensed 开关。 -- 路由层:FloodingRouter → ReliableRouter → NextHopRouter -- 无线接口:RadioLib 抽象层(SX126x/SX128x/LR11x0/RF95) -- 模块系统:`src/modules/` 下各功能模块(TextMessage, Position, Telemetry, etc.) -- Protobuf 消息定义:`src/mesh/generated/` + `protobufs/` -- 关键全局变量:`router`, `service`, `screen`, `gps`, `rIf` - -## 自定义修改记录(readme.md) -- 增加 CN 频段定义:`RDEF(CN, 470.0f, 510.0f, 100, 0, SETTING_MAX_POWER, true, false, false)` -- SETTING_MAX_POWER 使用宏保护:`#ifndef SETTING_MAX_POWER / #define SETTING_MAX_POWER 3` - -## 构建注意事项 -- `platformio.ini` 中 lib_deps 均已固定 commit hash,符合生产规范 -- 排除了大量 RadioLib 不用的协议(AX25/LoRaWAN/APRS 等)以节省 flash -- MAX_THREADS=40 -- 默认 env:tbeam(需要切换到 esp32c3_moonshine 系列时要指定 env) -- **Flash 占用**(esp32c3_moonshine_travelers):text≈1.92MB, data≈0.63MB, 总≈2.57MB / 4MB -- **移植记录**(2026-03-30): - - 将 esp32c3_moonshine_travelers 变体从 firmware-2.7.15 移植到官方最新 develop 分支 - - 修复 `tca9535IsCharging` 未声明错误:在 Power.cpp 添加 extern 声明 -- **分区表**(partition-table.csv):app=0x2C0000(2.75MB), OTA=0x030000(192KB), spiffs=0x100000(1MB) -- **中文 12×12 字库**:`src/graphics/fonts/ChineseFont12x12.h`,21075 字形,535KB flash - -## 🔧 硬件更换计划 - -### 问题描述 -- **症状**:不发送公钥、不发送节点名字信息(最新固件仍存在) -- **潜在原因**:LLCC68 规格书不支持 SF11 125kHz 模式,导致 LoRa 通信异常 - -### 解决方案 -1. **方案A**:更换为带温补晶振(TCXO)的 LoRa 模块 - - 确保晶振精度满足 SF11 125kHz 模式要求 - -2. **方案B**:更换为 E22-30S 系列模块 - - E22-900M30S 或 E22-400M30S - - 该系列支持更宽的工作模式 - -### 待确认事项 -- [ ] 确认 RA-01SC-P 是否为 LLCC68 芯片 -- [ ] 测试更换 TCXO 版模块后的表现 -- [ ] 测试 E22-30S 系列的兼容性 - - 生成工具:`tools/gen_chinese_font.mjs`(需 npm install @napi-rs/canvas) - - 覆盖:U+4E00-U+9FFF(CJK 统一汉字全集,含 GB2312 6763 字) - - 像素判定:透明背景 + 黑色前景,alpha > 128 阈值(去掉抗锯齿毛边,字形锐利) - - ⚠️ 阈值 40 太低→字体膨胀变粗;128 合适→清晰锐利 - - ⚠️ `imageSmoothingEnabled` 对 canvas fillText 无效 - - 渲染函数:`cfont12_find()` / `cfont12_draw()` / `cfont12_utf8()` / `cfont12_drawStr()` - - 集成点:CannedMessageModule drawFrame FREETEXT 部分(混合 ASCII+CJK 渲染) - - MessageRenderer(收到消息显示)也已支持:generateLines + drawStringWithEmotes + calculateLineHeights 均改为 CJK 感知