From 12f4fb81b827fcc2e0ab127c3fc7873abfeb9f16 Mon Sep 17 00:00:00 2001 From: Bairan Zhang Date: Tue, 10 Feb 2026 17:11:06 +0800 Subject: [PATCH] first commit --- .clangd | 2 + .devcontainer/Dockerfile | 13 + .devcontainer/devcontainer.json | 21 + .gitignore | 78 ++++ .vscode/c_cpp_properties.json | 23 ++ .vscode/launch.json | 15 + .vscode/settings.json | 21 + AGENTS.md | 63 +++ APP_BLE打印控制实现说明.md | 395 +++++++++++++++++++ BLE_PAIRING_PARAMETERS.md | 147 +++++++ CMakeLists.txt | 8 + Communication_Protocol.md | 179 +++++++++ ESP32_ESP-IDF移植实施文档.md | 459 ++++++++++++++++++++++ README.md | 381 ++++++++++++++++++ dependencies.lock | 20 + main/CMakeLists.txt | 5 + main/Kconfig.projbuild | 42 ++ main/idf_component.yml | 2 + main/include/ble_client.h | 18 + main/include/common.h | 37 ++ main/include/uart_bridge.h | 15 + main/main.c | 53 +++ main/src/ble_client.c | 665 ++++++++++++++++++++++++++++++++ main/src/uart_bridge.c | 281 ++++++++++++++ sdkconfig.defaults | 6 + sdkconfig.defaults.esp32 | 1 + sdkconfig.defaults.esp32c3 | 1 + sdkconfig.defaults.esp32c5 | 2 + sdkconfig.defaults.esp32c6 | 1 + sdkconfig.defaults.esp32c61 | 1 + sdkconfig.defaults.esp32h2 | 1 + sdkconfig.defaults.esp32s3 | 2 + 32 files changed, 2958 insertions(+) create mode 100644 .clangd create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .gitignore create mode 100644 .vscode/c_cpp_properties.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 AGENTS.md create mode 100644 APP_BLE打印控制实现说明.md create mode 100644 BLE_PAIRING_PARAMETERS.md create mode 100755 CMakeLists.txt create mode 100644 Communication_Protocol.md create mode 100644 ESP32_ESP-IDF移植实施文档.md create mode 100755 README.md create mode 100644 dependencies.lock create mode 100755 main/CMakeLists.txt create mode 100755 main/Kconfig.projbuild create mode 100755 main/idf_component.yml create mode 100644 main/include/ble_client.h create mode 100755 main/include/common.h create mode 100644 main/include/uart_bridge.h create mode 100755 main/main.c create mode 100644 main/src/ble_client.c create mode 100644 main/src/uart_bridge.c create mode 100755 sdkconfig.defaults create mode 100755 sdkconfig.defaults.esp32 create mode 100755 sdkconfig.defaults.esp32c3 create mode 100755 sdkconfig.defaults.esp32c5 create mode 100755 sdkconfig.defaults.esp32c6 create mode 100755 sdkconfig.defaults.esp32c61 create mode 100755 sdkconfig.defaults.esp32h2 create mode 100755 sdkconfig.defaults.esp32s3 diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..437f255 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Remove: [-f*, -m*] diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..dafb8ad --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,13 @@ +ARG DOCKER_TAG=latest +FROM espressif/idf:${DOCKER_TAG} + +ENV LC_ALL=C.UTF-8 +ENV LANG=C.UTF-8 + +RUN apt-get update -y && apt-get install udev -y + +RUN echo "source /opt/esp/idf/export.sh > /dev/null 2>&1" >> ~/.bashrc + +ENTRYPOINT [ "/opt/esp/entrypoint.sh" ] + +CMD ["/bin/bash", "-c"] \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b801786 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,21 @@ +{ + "name": "ESP-IDF QEMU", + "build": { + "dockerfile": "Dockerfile" + }, + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.defaultProfile.linux": "bash", + "idf.espIdfPath": "/opt/esp/idf", + "idf.toolsPath": "/opt/esp", + "idf.gitPath": "/usr/bin/git" + }, + "extensions": [ + "espressif.esp-idf-extension", + "espressif.esp-idf-web" + ] + } + }, + "runArgs": ["--privileged"] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7805557 --- /dev/null +++ b/.gitignore @@ -0,0 +1,78 @@ +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Directory metadata +.directory + +# Temporary files +*~ +*.swp +*.swo +*.bak +*.tmp + +# Log files +*.log + +# Build artifacts and directories +**/build/ +build/ +*.o +*.a +*.out +*.exe # For any host-side utilities compiled on Windows + +# ESP-IDF specific build outputs +*.bin +*.elf +*.map +flasher_args.json # Generated in build directory +sdkconfig.old +sdkconfig + +# ESP-IDF dependencies +# For older versions or manual component management +/components/.idf/ +**/components/.idf/ +# For modern ESP-IDF component manager +managed_components/ +# If ESP-IDF tools are installed/referenced locally to the project +.espressif/ + +# CMake generated files +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +install_manifest.txt +CTestTestfile.cmake + +# Python environment files +*.pyc +*.pyo +*.pyd +__pycache__/ +*.egg-info/ +dist/ + +# Virtual environment folders +venv/ +.venv/ +env/ + +# Language Servers +.clangd/ +.ccls-cache/ +compile_commands.json + +# Windows specific +Thumbs.db +ehthumbs.db +Desktop.ini + +# User-specific configuration files +*.user +*.workspace # General workspace files, can be from various tools +*.suo # Visual Studio Solution User Options +*.sln.docstates # Visual Studio diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..b7d20b2 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,23 @@ +{ + "configurations": [ + { + "name": "ESP-IDF", + "compilerPath": "${config:idf.toolsPath}/tools/xtensa-esp-elf/esp-14.2.0_20251107/xtensa-esp-elf/bin/xtensa-esp32-elf-gcc", + "compileCommands": "${config:idf.buildPath}/compile_commands.json", + "includePath": [ + "${config:idf.espIdfPath}/components/**", + "${config:idf.espIdfPathWin}/components/**", + "${workspaceFolder}/**" + ], + "browse": { + "path": [ + "${config:idf.espIdfPath}/components", + "${config:idf.espIdfPathWin}/components", + "${workspaceFolder}" + ], + "limitSymbolsToIncludedHeaders": true + } + } + ], + "version": 4 +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2511a38 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "gdbtarget", + "request": "attach", + "name": "Eclipse CDT GDB Adapter" + }, + { + "type": "espidf", + "name": "Launch", + "request": "launch" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3a5f257 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "C_Cpp.intelliSenseEngine": "default", + "idf.espIdfPath": "/home/bairan_ubuntu/esp/v5.5.2/esp-idf", + "idf.pythonInstallPath": "/usr/bin/python3", + "idf.openOcdConfigs": [ + "board/esp32s3-builtin.cfg" + ], + "idf.port": "/dev/ttyACM0", + "idf.toolsPath": "/home/bairan_ubuntu/.espressif", + "idf.customExtraVars": { + "IDF_TARGET": "esp32s3" + }, + "clangd.path": "/home/bairan_ubuntu/.espressif/tools/esp-clang/esp-19.1.2_20250312/esp-clang/bin/clangd", + "clangd.arguments": [ + "--background-index", + "--query-driver=**", + "--compile-commands-dir=/home/bairan_ubuntu/project/Alpha_AI_Printer/Software/BLE-Printer/build" + ], + "idf.flashType": "UART", + "idf.currentSetup": "/home/bairan_ubuntu/esp/v5.5.2/esp-idf" +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7c9b5c0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,63 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- Root `CMakeLists.txt` defines the ESP-IDF project (`project(BLE-Printer)`). +- `main/` is the primary component. + - `main/main.c` contains the `app_main` entry point. + - `main/src/` holds module implementations (`gap.c`, `gatt_svc.c`, `heart_rate_mock.c`, `led.c`). + - `main/include/` provides public headers mirroring module names. +- `sdkconfig.defaults*` files store per-target defaults (e.g., `sdkconfig.defaults.esp32c3`). +- `.vscode/` and `.devcontainer/` include editor/devcontainer settings. + +## Build, Flash, and Development Commands +- `idf.py set-target esp32` (or `esp32c3`, `esp32c6`, etc.) selects the chip target. +- `idf.py build` builds the firmware (requires an ESP-IDF environment). +- `idf.py -p /dev/ttyACM0 flash monitor` builds, flashes, and opens the serial monitor (`Ctrl-]` to exit). +- Ensure `IDF_PATH` is set via the ESP-IDF export script before running `idf.py`. + +## Coding Style & Naming Conventions +- Language: C with ESP-IDF conventions. +- Indentation: 4 spaces; braces stay on the same line as control statements and function declarations. +- Naming: lower_snake for files and functions (e.g., `gatt_svc_init`, `heart_rate_task`). +- Headers live in `main/include/` and match module names. +- Prefer `static` for file-local helpers; use `ESP_LOGx` macros for logging. + +## Testing Guidelines +- No automated test suite is present in this repository. +- Manual verification: flash to hardware and validate GATT behavior with a BLE client (for example, nRF Connect) and serial logs. + +## Commit & Pull Request Guidelines +- This directory does not contain Git history, so no established commit format is visible. +- Suggested format: `type(scope): summary` (for example, `feat(gatt): add battery service`). +- PRs should include target chip, test steps (`idf.py …` commands), and relevant serial logs or BLE client screenshots. + +## BLE Project Context & Configuration + +**Goal**: +The ESP32 (running ESP-IDF) must act as a **BLE Central (GATT Client)** to replace the Android App described in the documentation. It needs to connect to a specific BLE Printer. + +**Reference Document**: +The paramaters please strickly refer to `BLE_PAIRING_PARAMETERS.md`, focusing on **Section 9 and Section 10**. + +## Document Relationship and Locations +- `./APP_BLE打印控制实现说明.md`: Implementation guide for the BLE thermal printer host-side Android APP. +- `./ESP32_ESP-IDF移植实施文档.md`: ESP32 (ESP-IDF) migration implementation document derived from the Android APP solution. + + + +## Configuration Tips +- Avoid committing local `sdkconfig`; prefer `sdkconfig.defaults*` for shared defaults per target. + +## 协议帧格式 ## +- Refer to `Communication_Protocol.md` diff --git a/APP_BLE打印控制实现说明.md b/APP_BLE打印控制实现说明.md new file mode 100644 index 0000000..fbb6b81 --- /dev/null +++ b/APP_BLE打印控制实现说明.md @@ -0,0 +1,395 @@ +# lyfPrinter APP BLE 打印控制实现说明 + +本文档基于当前项目源码,说明 Android 上位机如何通过 BLE 控制热敏打印机完成打印。 + +适用代码目录:`app/src/main/java/com/example/electronicScale` + +--- + +## 1. 总体架构 + +APP 的控制链路可分为 4 层: + +1. 页面层(各类预览页) + - 负责生成待打印位图(图片/文本/二维码/小票/标签)。 +2. 业务协议层(`BlueHandler`) + - 负责打印协议封装、状态解析、分包发送、打印参数下发。 +3. BLE 传输层(`ECBLE`) + - 负责扫描、连接、订阅通知、写特征、MTU 协商。 +4. 打印机固件 + - 解析自定义指令,执行打印、走纸、标签定位、OTA 等动作。 + +核心类: +- `MainActivity`:权限、扫描、连接入口。 +- `ECBLE`:BLE 扫描/连接/读写底层。 +- `BlueHandler`:打印协议与数据处理核心。 +- `PrinterControlActivity`:打印机状态面板与功能入口页。 +- 各 `*PreviewActivity`:生成位图并调用统一打印入口。 + +--- + +## 2. 连接与初始化流程 + +### 2.1 权限与系统开关 + +`MainActivity` 在进入时先做权限检查,再打开蓝牙: + +- 位置权限:`ACCESS_FINE_LOCATION`、`ACCESS_COARSE_LOCATION` +- Android 12+ 蓝牙权限:`BLUETOOTH_SCAN`、`BLUETOOTH_ADVERTISE`、`BLUETOOTH_CONNECT` + +参考: +- `MainActivity.java:264` +- `AndroidManifest.xml` + +`ECBLE.openBluetoothAdapter()` 会继续检查: +- 是否支持蓝牙 +- 蓝牙开关是否开启 +- 定位开关是否开启(GPS 或 Network) + +参考: +- `ECBLE.java:61` + +--- + +### 2.2 扫描与设备过滤 + +扫描使用 `BluetoothAdapter.startLeScan()`,回调设备名与 MAC。 + +APP 仅展示设备名为 `lyfPrinter`(忽略大小写、去空格)的设备。 + +参考: +- `ECBLE.java:132` +- `MainActivity.java:238` +- `MainActivity.java:244` + +--- + +### 2.3 建链与 GATT 特征绑定 + +用户点击设备后,调用 `ECBLE.createBLEConnection()`。 + +连接策略: +- 失败自动重试,最多 5 次(初次 + 4 次重试)。 +- 每次重试间隔约 300ms。 + +参考: +- `MainActivity.java:176` +- `ECBLE.java:336` + +服务发现后,绑定两个关键特征: +- Notify:`0000fff1-0000-1000-8000-00805f9b34fb` +- Write:`0000fff2-0000-1000-8000-00805f9b34fb` + +并执行: +- 开启通知(`setCharacteristicNotification + CCCD`) +- 请求 MTU = 247 +- 写入类型 `WRITE_TYPE_NO_RESPONSE` + +参考: +- `ECBLE.java:180` +- `ECBLE.java:181` +- `ECBLE.java:219` +- `ECBLE.java:291` +- `ECBLE.java:303` +- `ECBLE.java:378` + +--- + +## 3. BlueHandler:统一打印控制核心 + +`BlueHandler` 是单例,负责: + +1. 注册 BLE 回调并解析回包。 +2. 定时查询打印机状态(每 5 秒)。 +3. 校验打印条件(缺纸/低电/高温)。 +4. 将位图转为热敏点阵数据。 +5. 按打印协议分帧发送并处理进度。 + +参考: +- `BlueHandler.java:70` +- `BlueHandler.java:91` +- `BlueHandler.java:232` +- `BlueHandler.java:296` +- `BlueHandler.java:438` + +--- + +## 4. 自定义通信协议(打印指令) + +### 4.1 帧格式 + +常规帧由 `printer_sendParam()` 组包: + +1. `slaveAddr`(1字节,默认 `0x01`) +2. `function`(1字节) +3. `lengthH`(1字节) +4. `lengthL`(1字节) +5. `payload`(可变) +6. `checkSum`(可选,1字节) + +校验规则(若启用): +- 从地址到 payload 所有字节求和,取低 8 位。 + +参考: +- `BlueHandler.java:590` + +--- + +### 4.2 指令码 + +打印控制相关: + +| 指令 | 功能 | +|---|---| +| `0x00` | 电源开关(`PRINT_POWER_CMD`) | +| `0x01` | 获取状态(`PRINT_GET_STATUS_CMD`) | +| `0x02` | 走纸距离(`PRINT_SET_DISTANCE_CMD`) | +| `0x03` | 打印参数(`PRINT_SET_PARAM_CMD`) | +| `0x04` | 发送打印数据(`PRINT_SEND_DATA_CMD`) | +| `0x05` | 标签间隙定位(`PRINT_GAP_MOVE_CMD`) | +| `0x06` | 查询标签偏移(`PRINT_GET_LABEL_OFFSET_CMD`) | +| `0x07` | 设置标签偏移(`PRINT_SET_LABEL_OFFSET_CMD`) | + +OTA 相关: + +| 指令 | 功能 | +|---|---| +| `0xA0` | 跳转 Boot | +| `0xA1` | 擦页 | +| `0xA2` | 写固件数据 | +| `0xA3` | 跳转 App | +| `0xA4` | 查询版本 | + +参考: +- `BlueHandler.java:49` +- `BlueHandler.java:59` + +--- + +### 4.3 回包解析 + +`BlueHandler.anasysBlueData()` 解析 `hexBuf[1]`(功能码): + +- `0x01`(状态) + - `hexBuf[4]`:纸张状态 + - `hexBuf[5]`:电量 + - `hexBuf[6]`:温度符号(`0x2D` 表示负) + - `hexBuf[7..8]`:温度数值(0.1℃) +- `0x04`(打印数据应答) + - 将 `printerIsIdle = 0`,允许发送下一包 +- `0x05`(标签定位完成) + - 将 `printerPaperPosition = 1` + +参考: +- `BlueHandler.java:296` + +--- + +## 5. 从页面到打印的通用流程 + +无论是图片、文字、二维码、小票、标签,最终都走统一入口: + +`mBlueHandler.printImageToPrinter(bitmap, density)` + +参考: +- `ImagePreviewActivity.java:369` +- `TextPreviewActivity.java:352` +- `QrPreviewMainActivity.java:367` +- `receiptPreviewActivity.java:412` +- `labelPapaerPreviewActivity.java:927` +- `factoryTestPreviewActivity.java:308` + +通用步骤: + +1. 页面先调用 `printrt_errorDetect()` 做可打印校验。 +2. 各页面把预览内容渲染成 384 宽位图。 +3. 调用 `printImageToPrinter()` 进入统一发送流程。 + +--- + +## 6. 位图到热敏数据的转换 + +`BlueHandler.printImageToPrinter()` 内部流程: + +1. `convertBitmapToThermalData(bitmap, 384)` +2. `scaleBitmapToPrinterWidth()`:缩放到 384 像素宽 +3. `convertToBlackWhite()`:阈值二值化(灰度 < 128 记黑) +4. `convertBitmapToRawData()`:按行取模,8 像素打包 1 字节(MSB first) +5. 得到连续点阵字节流后进行分包发送 + +参考: +- `BlueHandler.java:438` +- `BlueHandler.java:379` +- `BlueHandler.java:401` +- `DataConvertTool.java:117` +- `DataConvertTool.java:128` + +--- + +## 7. 分包发送与打印执行 + +### 7.1 分包策略 + +APP 设置单帧上限为 244 字节,其中: +- 协议头 4 字节 +- 数据体最多 240 字节 + +这样每帧可发送 5 行(384点宽 => 48字节/行,5行=240字节)。 + +参考: +- `BlueHandler.java:442` +- `BlueHandler.java:476` + +--- + +### 7.2 打印前参数下发 + +实际发送前会执行: + +1. 开启打印电源:`printer_setVhSwitch(true)`(`0x00`) +2. 设置打印参数:`printer_setPrintParam(1, 2, hotTime)` +3. `hotTime` 来自浓度映射: + - 较淡=1000 + - 中等=1500 + - 较浓=2000 + - 最深=3000 + +参考: +- `BlueHandler.java:505` +- `BlueHandler.java:509` +- `BlueHandler.java:450` + +--- + +### 7.3 数据发送节奏 + +发送循环中每个分包都走 `PRINT_SEND_DATA_CMD(0x04)`,但 `isSendCheck=false`(打印数据帧不附加校验字节)。 + +流控机制: +- 发送后置 `printerIsIdle = 0x04` +- 收到 `0x04` 应答时在解析函数中将其清零 +- 第 1 包后只短延时 +- 从第 2 包开始等待空闲再发下一包(超时 30s) + +参考: +- `BlueHandler.java:520` +- `BlueHandler.java:334` +- `BlueHandler.java:551` + +--- + +### 7.4 收尾动作 + +发送结束后统一执行: + +1. 关闭打印电源:`printer_setVhSwitch(false)` +2. 走纸留白:`printer_setMoveDistance(12.5f)`(约 12.5mm) +3. 恢复状态轮询 +4. 弹窗提示“打印完成” + +参考: +- `BlueHandler.java:566` +- `BlueHandler.java:574` + +--- + +## 8. 各打印页面的实现差异 + +### 8.1 图片打印(`ImagePreviewActivity`) + +- 用户选图后预处理成 384 宽位图。 +- 预览和打印使用误差扩散(Floyd-Steinberg/Atkinson)提升灰阶视觉效果。 +- 最终把二值结果送入统一打印入口。 + +参考: +- `ImagePreviewActivity.java:184` +- `ImagePreviewActivity.java:319` +- `ImagePreviewActivity.java:354` + +### 8.2 文本打印(`TextPreviewActivity`) + +- 把 `TextView` 按 384 固定宽重新测量布局。 +- 绘制到白底 Bitmap 后打印。 + +参考: +- `TextPreviewActivity.java:328` +- `TextPreviewActivity.java:356` + +### 8.3 二维码打印(`QrPreviewMainActivity`) + +- 先生成二维码,再按打印宽度 384 等比缩放后打印。 + +参考: +- `QrPreviewMainActivity.java:340` +- `QrPreviewMainActivity.java:367` + +### 8.4 小票与工厂测试页 + +- 都是将固定模板文本绘制为 Bitmap,再统一打印。 + +参考: +- `receiptPreviewActivity.java:387` +- `factoryTestPreviewActivity.java:286` + +### 8.5 标签纸打印(`labelPapaerPreviewActivity`) + +标签场景在打印前多了“找缝 + 偏移”动作: + +1. 查询/设置标签偏移(`0x06` / `0x07`)。 +2. 打印前发送 `0x05` 做 gap 定位,等待 `printerPaperPosition` 标志。 +3. 再走纸到 `currentOffset` 位置后开始打印。 + +参考: +- `labelPapaerPreviewActivity.java:117` +- `labelPapaerPreviewActivity.java:234` +- `labelPapaerPreviewActivity.java:886` +- `labelPapaerPreviewActivity.java:909` + +--- + +## 9. 打印状态与防呆 + +状态评估来自 `detectJundge()`: + +- 缺纸 -> 禁止打印 +- 电量 `<= 40%` -> 禁止打印 +- 温度 `>= 60℃` -> 禁止打印 + +页面点击打印时会先调用 `printrt_errorDetect()`,失败就弹窗并中止。 + +参考: +- `DataConvertTool.java:12` +- `BlueHandler.java:787` + +--- + +## 10. 一次打印的时序(简化) + +```text +用户点击打印 + -> PreviewActivity 生成 Bitmap(384宽) + -> BlueHandler.printrt_errorDetect() + -> BlueHandler.printImageToPrinter() + -> 位图转点阵字节流 + -> printer_setVhSwitch(true) + -> printer_setPrintParam(...) + -> 循环发送 0x04 数据帧 + -> 等待 0x04 应答解锁下一帧 + -> printer_setVhSwitch(false) + -> printer_setMoveDistance(12.5) + -> 弹窗: 打印完成 +``` + +--- + +## 11. 代码实现中的关键注意点 + +1. `ECBLE` 与 `BlueHandler` 的回调是静态单实例风格,多个页面共享同一通道,页面切换时需正确注册/移除监听器。 +2. 打印分包等待采用忙等循环(无休眠),高负载时可能有 CPU 占用峰值。 +3. 打印数据帧(`0x04`)当前不带校验字节,链路异常时主要依赖 BLE 层可靠性与应答超时控制。 +4. 扫描采用 `startLeScan`(旧 API),如后续适配新系统可迁移到 `BluetoothLeScanner`。 +5. 状态解析里 `connectState` 目前写死为 `1`(`BlueHandler.java:305`),如果后续需要严格依赖设备回包连接位,建议改为按协议字段解析。 + +--- + +如果需要,我可以继续补一份《协议字段示例帧文档》(把 `0x00~0x07` 每个命令的数据字段按字节位展开,并给出示例十六进制报文)。 diff --git a/BLE_PAIRING_PARAMETERS.md b/BLE_PAIRING_PARAMETERS.md new file mode 100644 index 0000000..808f0c3 --- /dev/null +++ b/BLE_PAIRING_PARAMETERS.md @@ -0,0 +1,147 @@ +# BLE 配对关键参数说明 + +本文档面向通用 BLE 场景,概述配对(Pairing)过程中最关键的参数与含义。配对由 SMP(Security Manager Protocol)执行,结果是双方生成并分发密钥,进而建立加密链路和(可选)长期绑定(Bonding)。文末附上本项目“已配对/可连接从机”的关键参数,便于其他客户端(如 ESP-IDF/ESP32)复用连接。 + +## 1) 角色与流程层级 + +- 角色 + - Central(中心/主机)与 Peripheral(外设/从机)均可发起配对,但在 SMP 中仍以“发起方/响应方”区分。 + - GATT Client/Server 是应用层角色;SMP 只关心链路安全,不直接关心 GATT 读写。 +- 安全版本 + - Legacy Pairing(蓝牙 4.0+) + - LE Secure Connections(LESC,蓝牙 4.2+,更强的密钥协商) + +## 2) 配对方法(由 IO 能力与安全需求决定) + +- Just Works:无交互,防 MITM 能力弱。 +- Passkey Entry:一端显示 6 位码,另一端输入。 +- Numeric Comparison:双方显示 6 位码,用户确认一致。 +- OOB(Out of Band):通过 NFC/二维码等带外信道交换信息,安全性高。 + +决定因素:设备 IO 能力 + 是否要求 MITM 防护 + 是否使用 LESC。 + +## 3) 配对请求/响应的关键字段(SMP Pairing Request/Response) + +这些字段决定配对方式、密钥类型和安全等级,是“配对关键参数”的核心。 + +- IO Capability + - DisplayOnly, DisplayYesNo, KeyboardOnly, NoInputNoOutput, KeyboardDisplay + - 影响是否可用 Passkey / Numeric Comparison +- OOB Data Flag + - 指示是否有 OOB 数据可用于配对 +- AuthReq(Authentication Requirements) + - Bonding(是否保存长期密钥) + - MITM(是否要求防中间人) + - SC(是否要求 LE Secure Connections) + - Keypress(是否支持按键通知) +- Max Encryption Key Size + - 7~16 字节,通常为 16 +- Key Distribution + - EncKey(LTK 等加密相关) + - IdKey(IRK/Identity Address) + - Sign(CSRK,用于数据签名) + - LinkKey(用于 BR/EDR 互通,LE 通常不用) + +## 4) 关键密钥与作用 + +- LTK(Long Term Key) + - 用于后续重连时的链路加密 +- IRK(Identity Resolving Key) + - 用于解析可解析私有地址(RPA),实现隐私保护 +- CSRK(Connection Signature Resolving Key) + - 用于数据签名,较少使用 +- STK(Short Term Key) + - Legacy Pairing 中临时加密用(非长期) + +## 5) 地址与隐私相关参数 + +- 地址类型 + - Public Address / Random Address + - Random 又分 Static / Non-Resolvable / Resolvable(RPA) +- 隐私开关 + - 使用 RPA + IRK 可隐藏真实地址,提升隐私与安全 + +## 6) 安全等级(常见分级) + +- LE Security Mode 1 + - Level 1:无加密 + - Level 2:加密(Legacy) + - Level 3:加密 + MITM(Legacy) + - Level 4:加密 + MITM(LESC) +- LE Security Mode 2 + - Level 1:数据签名(无加密) + - Level 2:数据签名 + MITM + +## 7) 连接参数(与配对间接相关) + +这类参数不属于配对协议,但会影响配对体验与稳定性: + +- Connection Interval(连接间隔) +- Slave Latency(从机延迟) +- Supervision Timeout(监督超时) + +注:若连接不稳定或超时过短,可能导致配对失败或断连。 + +## 8) 实际排查时常关注的“关键参数清单” + +- 是否启用 LESC(SC 位) +- 是否要求 MITM(AuthReq) +- IO Capability 是否允许所需配对方式 +- Key Size 是否为 16 +- Key Distribution 是否双方一致 +- 是否使用 RPA/IRK(隐私) +- 是否允许 Bonding(长期记忆配对) + +--- + +## 9) 本项目 BLE 从机的关键参数(供其他客户端复用) + +以下信息基于当前 Android 代码实现,目标是让其他设备/程序连接到**同一台 BLE 从机**(打印机)。相关代码位置: +- `app/src/main/java/com/example/electronicScale/MainActivity.java` +- `app/src/main/java/com/example/electronicScale/ECBLE.java` +- `app/src/main/java/com/example/electronicScale/BlueHandler.java` + +### 9.1 角色与连接方式 +- Android App 作为 Central / GATT Client +- BLE 从机为打印机(由广播名过滤判断) + +### 9.2 设备发现与识别参数 +- 广播设备名:`lyfPrinter`(忽略大小写、忽略首尾空格) +- 设备地址:扫描得到的 MAC 地址,连接时会去掉冒号作为内部 id +- 扫描方式:使用 `BluetoothAdapter.startLeScan()`(Legacy 扫描) + +### 9.3 GATT 关键参数 +- Notify 特征 UUID:`0000fff1-0000-1000-8000-00805f9b34fb` +- Write 特征 UUID:`0000fff2-0000-1000-8000-00805f9b34fb` +- 服务 UUID:代码未硬编码服务 UUID,靠服务发现后遍历特征匹配 +- 通知开启方式:`setCharacteristicNotification(true)` + 写 CCCD(0x2902)=0x0001 +- 写入方式:`WRITE_TYPE_NO_RESPONSE` +- MTU:连接后请求 `247` + +### 9.4 数据编码/格式 +- 默认字符集:GBK(`ECBLE.setChineseTypeGBK()`) +- 写入支持 Hex 字符串转字节:`writeBLECharacteristicValue(data, true)` +- 实际指令帧为自定义协议,参考 `BlueHandler` 中的命令/打包逻辑 + +### 9.5 安全/配对相关 +- 代码未调用 `createBond()` 或设置 PIN/LESC +- 未显式要求加密或 MITM +- 推测从机允许**无配对/Just Works**直连 + - 若固件强制安全配对,需要补充:IO Capability、AuthReq、Key Size、Key Distribution 等 + +### 9.6 连接限制提示 +- 多数 BLE 从机**同时只允许一个 Central 连接** +- 若 Android App 已连接,ESP32 可能无法同时连接(除非从机支持多连接) + +## 10) ESP-IDF 侧接入要点(简版清单) + +- 扫描广播,优先匹配设备名 `lyfPrinter`(或直接使用已知 MAC) +- 连接后进行服务发现,查找 `fff1/fff2` 特征 +- 启用 `fff1` 通知(写 CCCD = 0x0001) +- 向 `fff2` 以 Write Without Response 方式写数据 +- 协商 MTU 为 247 +- 按自定义协议发送帧(参考 `BlueHandler`/`DataConvertTool`) + +--- + +如果你需要,我可以把 **ESP-IDF 的具体 GATT 客户端代码框架**按这些参数整理出来,或者补齐“自定义指令协议”的字段与示例帧。 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100755 index 0000000..d646475 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +# "Trim" the build. Include the minimal set of components, main, and anything it depends on. +idf_build_set_property(MINIMAL_BUILD ON) +project(BLE-Printer) diff --git a/Communication_Protocol.md b/Communication_Protocol.md new file mode 100644 index 0000000..08b6159 --- /dev/null +++ b/Communication_Protocol.md @@ -0,0 +1,179 @@ +# lyfPrinter 热敏打印机通讯协议(整理版,基于文档 V1.1) + +> 来源:*lyfPrinter 热敏打印机协议文档*(V1.1,2026-01-03:增加标签纸打印指令) + +--- + +## 1. 帧格式(命令/响应一致) + +一帧数据由以下字段顺序组成: + +- **设备地址**(1B) +- **功能码**(1B) +- **数据长度**(2B) +- **数据区**(NB) +- **和校验**(1B) + +> 文档定义:命令格式=设备地址(1B)+功能码(1B)+数据长度(2B)+数据(NB)+和校验(1B)。 + +### 1.1 和校验(Checksum) + +**和校验 = 设备地址 + 功能码 + 数据长度(2B) + 数据区(NB) 的总和取低 8 位** + +也就是: + +```text +checksum = (addr + func + len_hi + len_lo + sum(data_bytes)) & 0xFF +``` + +### 1.2 十六进制写法 vs 实际发送 + +文档示例里常写成带空格的十六进制,如: + +```text +01 00 00 01 01 03 +``` + +这只是**便于阅读**。实际发送时需要发**原始字节流**(bytes),等价于: + +```text +0x01 0x00 0x00 0x01 0x01 0x03 +``` + +--- + +## 2. 指令列表 + +> 说明:下面“send/recv”都是“帧格式”中的 **数据区**含义(其它字段按帧格式固定存在)。 +> 文档中设备地址示例均为 `0x01`。 + +--- + +### 2.1 电源开关控制(0x00) + +**send 数据区(1B)** +- `0x00`:关 +- `0x01`:开 + +**recv 数据区(1B)** +- `0x00`:失败 +- `0x01`:成功 + +**示例** +- 打开电源:`01 00 00 01 01 03` +- 关闭电源:`01 00 00 01 00 02` + +--- + +### 2.2 获取设备状态(0x01) + +**send 数据区**:无(长度为 0) + +**recv 数据区** +- 纸张状态(1B):`0x00` 无纸 / `0x01` 有纸 +- 电池电量(1B):0~100 +- 打印头温度(3B): + - 第 1 字节为符号位:`0x2B` 正号 / `0x2D` 负号 + - 保留 1 位小数(范围 -20℃~80℃) + - *注:文档未进一步明确后 2 字节的编码方式,需结合实际设备/固件实现确认。* + +**示例** +- 获取设备状态:`01 01 00 00 02` + +--- + +### 2.3 设置走纸距离(0x02) + +**send 数据区** +- 走纸方向(1B):`0x2B` 正向 / `0x2D` 反向 +- 走纸距离(2B):单位 mm + +**recv 数据区(1B)** +- `0x00`:失败 +- `0x01`:成功 + +**示例** +- 设置走纸 5mm:`01 02 00 03 2B 00 05 36` + +--- + +### 2.4 设置打印参数(0x03) + +> 包含:打印速度(切换 STB 同时加热数量)、打印浓度(调整加热时间) + +**send 数据区** +- 加热模式(1B,等级 1~4) + - `0x01`:分 6 次加热 + - `0x02`:分 3 次加热 + - `0x03`:分 2 次加热 + - `0x04`:分 1 次加热(功率太大暂不支持) +- 电机运行速度(1B):单位 ms,范围 1~10ms +- 打印浓度/加热时间(2B):单位 us,建议范围 1000~2500(不推荐超过 2500us 长时间加热) + - 1000:较淡 + - 1500:中等 + - 2000:浓 + - 2500:最深 + +**recv 数据区(1B)** +- `0x00`:失败 +- `0x01`:成功 + +**示例** +- 模式 1、速度 1ms、加热 2000us:`01 03 00 04 01 01 07 D0 E0` + +--- + +### 2.5 发送打印数据(0x04) + +**send 数据区** +- 打印数据(NB) + - 一次最多 **240 字节** + - **一行 48 字节**,因此一次最多 **5 行** + +**recv 数据区(1B)** +- `0x00`:失败 +- `0x01`:成功(打印机已完成当前数据打印,可接收下一帧) + +> 实际打印流程通常是:发送一帧 0x04 → 等待 recv 处理状态=0x01 → 再发下一帧。 + +--- + +### 2.6 标签纸定位走纸(0x05)【V1.1 增加】 + +**send 数据区(1B)**:固定 `0x01` +**recv 数据区(1B)**:固定 `0x01` + +**示例** +- 执行标签纸走纸定位:`01 05 00 01 01 08` + +--- + +### 2.7 查询标签纸偏移(0x06)【掉电保存】 + +**send 数据区(1B)**:固定 `0x01` + +**recv 数据区(1B)** +- 标签纸偏移值:单位 mm,1B,保留 1 位小数,最大 25.4mm +- *从“12.8mm → 0x80”的示例可推断:偏移字节 ≈ 偏移(mm)×10(0.1mm 为 1 LSB)* + +**示例** +- 查询标签纸偏移:`01 06 00 01 01 09` + +--- + +### 2.8 设置标签纸偏移(0x07)【掉电保存】 + +**send 数据区(1B)** +- 标签纸偏移值:单位 mm,1B,保留 1 位小数,最大 25.4mm + +**recv 数据区(1B)**:固定 `0x01` + +**示例** +- 设置标签纸偏移 12.8mm:`01 07 00 01 80 89` + +--- + +## 3. 安全警告(文档原意) + +- 电源开关不要长时间打开;打印结束后及时关断,否则可能导致打印头损坏。 + diff --git a/ESP32_ESP-IDF移植实施文档.md b/ESP32_ESP-IDF移植实施文档.md new file mode 100644 index 0000000..f67cc57 --- /dev/null +++ b/ESP32_ESP-IDF移植实施文档.md @@ -0,0 +1,459 @@ +# lyfPrinter 上位机 APP 迁移到 ESP32(ESP-IDF) 实施文档 + +本文档基于当前 Android 上位机 APP 实现(`MainActivity + ECBLE + BlueHandler + 各预览页`),给出一套可落地的 ESP32/ESP-IDF 迁移方案。 + +适用目标: +- 让 ESP32 作为 BLE Central 连接 `lyfPrinter` 热敏打印机 +- 复用现有私有打印协议(`0x00~0x07`、`0xA0~0xA4`) +- 在 ESP32 上实现打印、状态监控、标签纸定位、OTA(打印机固件)能力 + +--- + +## 1. 现有 APP 能力基线(迁移输入) + +Android 端关键行为: + +1. 扫描过滤设备名 `lyfPrinter` +2. 连接 BLE 外设并绑定特征: + - Notify: `0000fff1-0000-1000-8000-00805f9b34fb` + - Write: `0000fff2-0000-1000-8000-00805f9b34fb` +3. 请求 `MTU=247` +4. 向写特征发送私有协议帧,接收 Notify 回包并解析状态 +5. 打印流程: + - 输入内容转 384 点宽位图 + - 二值化 + - 行取模(8 像素打包 1 字节) + - 按 240 字节数据体分包(整帧 244 字节) + - 按应答节奏发送完毕 +6. 打印前后控制: + - 开启/关闭 VH 电源 + - 设置热参数(浓度映射) + - 结束后走纸留白 + +可参考: +- `APP_BLE打印控制实现说明.md` +- `BLE_PAIRING_PARAMETERS.md` + +--- + +## 2. 迁移目标定义 + +建议按两级目标推进: + +### 2.1 M1(必做,先跑通) + +1. 扫描/连接/重连 +2. 状态查询与解析(纸张、电量、温度) +3. 基础打印(接收已编码点阵数据后发送) +4. 标签纸 gap 定位与偏移读写 + +### 2.2 M2(增强) + +1. ESP32 端文本渲染与位图编码 +2. 图片/二维码本地生成与打印 +3. 打印机 OTA 完整流程 +4. UI 层(按键、串口命令、Web、LCD+LVGL 任一) + +--- + +## 3. ESP-IDF 技术选型建议 + +推荐: +- ESP-IDF `v5.1+` 或 `v5.2+` +- BLE Host:`NimBLE`(内存占用更低,Central 场景稳定) + +备选: +- Bluedroid 也可实现,但本项目建议优先 NimBLE。 + +编译配置建议(`menuconfig`): + +1. 启用 BLE(NimBLE) +2. 增大 GATT MTU 到 `247` +3. 提升 BT controller/host 内存预算 +4. 打开 NVS(存储设备地址、标签偏移、最近配置) + +--- + +## 4. Android 到 ESP32 模块映射 + +| Android 类 | 迁移后模块 | 说明 | +|---|---|---| +| `MainActivity` | `printer_ble_scan.c` + `app_cli.c` | 扫描、设备选择、连接触发 | +| `ECBLE` | `printer_ble_client.c` | GATT 连接、服务发现、通知订阅、写特征 | +| `BlueHandler` | `printer_proto.c` + `printer_engine.c` | 协议封装/解析、状态机、分包与流控 | +| `DataConvertTool` | `image_raster.c` | 二值化、384宽缩放、行取模 | +| 各 PreviewActivity | `content_renderer_*.c` | 文本/二维码/模板渲染 | +| `otaUpdateActivity` | `printer_ota.c` | `A0~A4` 升级流程 | + +建议目录: + +```text +components/ + printer_ble/ + include/printer_ble_client.h + printer_ble_client.c + printer_proto/ + include/printer_proto.h + printer_proto.c + printer_engine/ + include/printer_engine.h + printer_engine.c + image_raster/ + include/image_raster.h + image_raster.c + printer_ota/ + include/printer_ota.h + printer_ota.c +main/ + app_main.c + app_cli.c + app_config.c +``` + +--- + +## 5. BLE 迁移要点(与 Android 行为对齐) + +### 5.1 扫描与过滤 + +逻辑对齐 Android: +- 仅处理设备名 `lyfPrinter` +- 保存 MAC、RSSI、最后发现时间 +- 支持“按 MAC 直连”模式(量产更稳) + +### 5.2 建链流程 + +1. `scan -> connect` +2. `discover service/characteristics` +3. 找到 `FFF1/FFF2` +4. 对 `FFF1` 写 CCCD `0x0001` 开启 Notify +5. 交换 MTU(目标 247) + +### 5.3 写入策略 + +与 Android 保持一致: +- Write Without Response +- 发送内容使用十六进制帧字节序列 + +### 5.4 断线策略 + +建议状态机: +- `DISCONNECTED` +- `SCANNING` +- `CONNECTING` +- `DISCOVERING` +- `READY` +- `PRINTING` + +断线后: +- 延时 300ms 重连 +- 最多重试 4 次(与 Android 一致) +- 失败后回 `SCANNING` + +--- + +## 6. 私有协议迁移(核心) + +### 6.1 指令定义 + +```c +// 打印 +#define CMD_POWER 0x00 +#define CMD_GET_STATUS 0x01 +#define CMD_SET_DISTANCE 0x02 +#define CMD_SET_PARAM 0x03 +#define CMD_SEND_DATA 0x04 +#define CMD_GAP_MOVE 0x05 +#define CMD_GET_LABEL_OFFSET 0x06 +#define CMD_SET_LABEL_OFFSET 0x07 + +// OTA +#define CMD_BOOT_JUMP_BOOT 0xA0 +#define CMD_BOOT_ERASE_PAGE 0xA1 +#define CMD_BOOT_WRITE_DATA 0xA2 +#define CMD_BOOT_JUMP_APP 0xA3 +#define CMD_BOOT_GET_VERSION 0xA4 +``` + +### 6.2 帧结构 + +常规帧: + +```text +[addr:1][func:1][lenH:1][lenL:1][payload:len][checksum:optional] +``` + +校验和规则: +- 从 `addr` 到 payload 最后一个字节累加,取低 8 位 + +与 Android 对齐点: +- 控制类命令一般带校验 +- `CMD_SEND_DATA(0x04)` 数据帧可不带校验(当前 Android 逻辑) + +### 6.3 回包解析 + +按 `func` 分发处理: + +1. `0x01`:设备状态 +2. `0x04`:数据发送 ACK,用于释放“发送下一包”锁 +3. `0x05`:标签定位完成 +4. `0x06`:标签偏移读取结果 +5. `0xA1~0xA4`:OTA流程响应 + +--- + +## 7. 打印引擎迁移设计 + +### 7.1 打印参数映射 + +浓度映射(与 Android 对齐): +- `较淡 -> 1000` +- `中等 -> 1500` +- `较浓 -> 2000` +- `最深 -> 3000` + +打印前序列: + +1. `CMD_POWER` 开 +2. `CMD_SET_PARAM(hot_mode, move_time, hot_time)` +3. 分包发送 `CMD_SEND_DATA` + +打印后序列: + +1. `CMD_POWER` 关 +2. `CMD_SET_DISTANCE(12.5mm)` 留白走纸 + +### 7.2 分包规则 + +与 Android 同步: +- 单帧上限 244 bytes +- 协议头 4 bytes +- 数据体 240 bytes(384 点宽 -> 48 bytes/行 -> 每包 5 行) + +流控策略: +- 发出数据包后等待 `0x04` ACK +- 首包后可短延时一次 +- 后续每包 ACK 驱动 +- ACK 超时建议 `2~5s`,整任务超时 `30s` + +### 7.3 打印状态门限 + +建议保持一致: +- 缺纸:禁止打印 +- 电量 `<=40%`:禁止打印 +- 温度 `>=60℃`:禁止打印 + +--- + +## 8. 图像处理迁移策略(重点) + +ESP32 内存有限,不建议一次性处理大图。推荐 3 种模式: + +### 8.1 模式 A(推荐首版) + +外部(PC/手机/云)先生成“384宽 + 二值化 + 行取模”数据,ESP32 仅负责协议发送。 + +优点: +- 开发最快 +- ESP32 负载最低 + +### 8.2 模式 B(文本优先) + +ESP32 只做文本/模板渲染,不做复杂图片解码。 + +优点: +- 资源可控 +- 适合收据、标签、测试页 + +### 8.3 模式 C(全本地) + +ESP32 本地做图片解码+缩放+二值化+取模。需外接 PSRAM,建议 ESP32-S3。 + +--- + +## 9. FreeRTOS 任务模型建议 + +建议拆成 4 个任务 + 2 个队列: + +1. `ble_task` + - 处理 GAP/GATT 事件 +2. `proto_task` + - 帧收发、ACK 管理、回包解析 +3. `print_task` + - 打印流程状态机(预热、分包、收尾) +4. `cmd_task` + - 外部命令入口(串口/Web/UI) + +队列: +- `q_cmd`:业务命令(print/status/ota) +- `q_evt`:BLE/协议事件(connected/ack/timeout) + +同步对象: +- `EventGroup`:`READY`、`ACK_04`、`GAP_OK`、`OTA_ACK` +- `Mutex`:写特征互斥 + +--- + +## 10. 关键 C 接口建议 + +```c +// BLE +esp_err_t printer_ble_start_scan(void); +esp_err_t printer_ble_connect_by_name(const char *name); +esp_err_t printer_ble_write(const uint8_t *data, size_t len, bool no_rsp); + +// 协议 +size_t printer_proto_build_frame(uint8_t addr, uint8_t cmd, + const uint8_t *payload, uint16_t len, + bool with_checksum, uint8_t *out); +void printer_proto_handle_notify(const uint8_t *data, size_t len); + +// 打印引擎 +esp_err_t printer_engine_print_raw(const uint8_t *raw, size_t len, int hot_time); +esp_err_t printer_engine_get_status(void); +esp_err_t printer_engine_gap_move(void); +esp_err_t printer_engine_set_label_offset(uint8_t value); + +// OTA +esp_err_t printer_ota_run(const uint8_t *fw, size_t len); +``` + +--- + +## 11. OTA 迁移(针对打印机固件升级) + +流程对齐 Android: + +1. `A0` 跳 Boot +2. `A1` 擦页(按 1024 bytes 估页数) +3. `A2` 写入(单包 236 bytes 数据) +4. `A3` 跳回 App +5. `A4` 读版本确认 + +建议: +- 每步都有超时和重试(每步 3 次) +- 断电恢复策略:记录 last packet index + +--- + +## 12. 分阶段实施计划 + +### Phase 1(3~5天) + +1. BLE 扫描连接 + 订阅通知 + MTU +2. 实现 `GET_STATUS` +3. 实现 `POWER / SET_PARAM / SEND_DATA / SET_DISTANCE` +4. 用固定测试页 raw 数据打印 + +验收: +- 可稳定打印 50 次无死锁 + +### Phase 2(3~7天) + +1. 文本模板渲染 +2. 标签定位/偏移 +3. 设备参数保存(NVS) + +验收: +- 标签纸对齐稳定 + +### Phase 3(3~7天) + +1. OTA 全流程 +2. 回归测试与异常恢复 + +验收: +- 升级成功率 > 99% + +--- + +## 13. 测试清单 + +基础通信: + +1. 扫描到 `lyfPrinter` +2. 连接后找到 `FFF1/FFF2` +3. Notify 有数据 +4. MTU 协商到 247 + +协议一致性: + +1. 每条命令帧结构正确 +2. 校验和与 Android 一致 +3. `0x04` ACK 能正确驱动分包 + +打印质量: + +1. 浓度 4 档效果一致 +2. 长图连续打印无丢行 +3. 结束留白距离稳定 + +异常场景: + +1. 打印中断线后可恢复 +2. 缺纸/低电/高温阻断生效 +3. OTA 超时重试有效 + +--- + +## 14. 常见问题与规避 + +1. `write without response` 太快导致对端缓存溢出 +规避:严格按 `0x04 ACK` 节奏发送。 + +2. 连接成功但收不到 Notify +规避:确认 CCCD 写成功且未被后续重连覆盖。 + +3. 打印偏移不稳定 +规避:先 `GAP_MOVE`,再按偏移走纸,偏移值写入 NVS。 + +4. ESP32 内存不足 +规避:首版采用“外部预编码 raw 数据”,不在设备端做重图像处理。 + +--- + +## 15. 最小可用迁移路径(建议) + +如果你希望最快落地,按下面最小路径: + +1. 先只做 BLE + 协议 + raw 发送 +2. 用串口命令输入 `print_raw/status/gap/offset` +3. 打通后再逐步补 UI 和本地渲染 + +这样可以最短时间复用 Android 的核心协议能力,并把主要风险收敛在 BLE 链路与分包流控上。 + +--- + +## 16. 附:协议发送伪代码 + +```c +void do_print(const uint8_t *raw, size_t raw_len, int hot_time) { + send_cmd_power(true); // 0x00 + send_cmd_set_param(1, 2, hot_time); // 0x03 + + const size_t chunk = 240; + size_t off = 0; + int idx = 0; + + while (off < raw_len) { + size_t n = (raw_len - off > chunk) ? chunk : (raw_len - off); + send_cmd_data(raw + off, n); // 0x04 + + if (idx > 0) { + if (!wait_ack_04(3000)) { + // 超时处理:重发或中止 + break; + } + } else { + vTaskDelay(pdMS_TO_TICKS(10)); + } + + off += n; + idx++; + } + + send_cmd_power(false); // 0x00 + send_cmd_set_distance(12.5f); // 0x02 +} +``` + diff --git a/README.md b/README.md new file mode 100755 index 0000000..9be317a --- /dev/null +++ b/README.md @@ -0,0 +1,381 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | + +# NimBLE GATT Server Example + +## Overview + +This example is extended from NimBLE Connection Example, and further introduces + +1. How to implement a GATT server using GATT services table +2. How to handle characteristic access requests + 1. Write access demonstrated by LED control + 2. Read and indicate access demonstrated by heart rate measurement(mocked) + +To test this demo, install *nRF Connect for Mobile* on your phone. + +Please refer to [BLE Introduction](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/ble/get-started/ble-introduction.html#:~:text=%E4%BE%8B%E7%A8%8B%E5%AE%9E%E8%B7%B5) +for detailed example introduction and code explanation. + +## Try It Yourself + +### Set Target + +Before project configuration and build, be sure to set the correct chip target using: + +``` shell +idf.py set-target +``` + +For example, if you're using ESP32, then input + +``` Shell +idf.py set-target esp32 +``` + +### Build and Flash + +Run the following command to build, flash and monitor the project. + +``` Shell +idf.py -p flash monitor +``` + +For example, if the corresponding serial port is `/dev/ttyACM0`, then it goes + +``` Shell +idf.py -p /dev/ttyACM0 flash monitor +``` + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF to build projects. + +## Code Explained + +### Overview + +1. Initialization + 1. Initialize LED, NVS flash, NimBLE host stack, GAP service + 2. Initialize GATT service and add services to registration queue + 3. Configure NimBLE host stack and start NimBLE host task thread, GATT services will be registered automatically when NimBLE host stack started + 4. Start heart rate update task thread +2. Wait for NimBLE host stack to sync with BLE controller, and start advertising; wait for connection event to come +3. After connection established, wait for GATT characteristics access events to come + 1. On write LED event, turn on or off the LED accordingly + 2. On read heart rate event, send out current heart rate measurement value + 3. On indicate heart rate event, enable heart rate indication + +### Entry Point + +In this example, we call GATT `gatt_svr_init` function to initialize GATT server in `app_main` before NimBLE host configuration. This is a custom function defined in `gatt_svc.c`, and basically we just call GATT service initialization API and add services to registration queue. + +And there's another code added in `nimble_host_config_init`, which is + +``` C +static void nimble_host_config_init(void) { + ... + + ble_hs_cfg.gatts_register_cb = gatt_svr_register_cb; + + ... +} +``` + +That is GATT server register callback function. In this case it will only print out some registration information when services, characteristics or descriptors are registered. + +Then, after NimBLE host task thread is created, we'll create another task defined in `heart_rate_task` to update heart rate measurement mock value and send indication if enabled. + +### GAP Service Updates + +`gap_event_handler` is updated with LED control removed, and more event handling branches, when compared to NimBLE Connection Example, including + +- `BLE_GAP_EVENT_ADV_COMPLETE` - Advertising complete event +- `BLE_GAP_EVENT_NOTIFY_TX` - Notificate event +- `BLE_GAP_EVENT_SUBSCRIBE` - Subscribe event +- `BLE_GAP_EVENT_MTU` - MTU update event + +`BLE_GAP_EVENT_ADV_COMPLETE` and `BLE_GAP_EVENT_MTU` events are actually not involved in this example, but we still put them down there for reference. `BLE_GAP_EVENT_NOTIFY_TX` and `BLE_GAP_EVENT_SUBSCRIBE` events will be discussed in the next section. + +### GATT Services Table + +GATT services are defined in `ble_gatt_svc_def` struct array, with a variable name `gatt_svr_svcs` in this demo. We'll call it as the GATT services table in the following content. + +``` C +/* Heart rate service */ +static const ble_uuid16_t heart_rate_svc_uuid = BLE_UUID16_INIT(0x180D); + +static uint8_t heart_rate_chr_val[2] = {0}; +static uint16_t heart_rate_chr_val_handle; +static const ble_uuid16_t heart_rate_chr_uuid = BLE_UUID16_INIT(0x2A37); + +static uint16_t heart_rate_chr_conn_handle = 0; +static bool heart_rate_chr_conn_handle_inited = false; +static bool heart_rate_ind_status = false; + +/* Automation IO service */ +static const ble_uuid16_t auto_io_svc_uuid = BLE_UUID16_INIT(0x1815); +static uint16_t led_chr_val_handle; +static const ble_uuid128_t led_chr_uuid = + BLE_UUID128_INIT(0x23, 0xd1, 0xbc, 0xea, 0x5f, 0x78, 0x23, 0x15, 0xde, 0xef, + 0x12, 0x12, 0x25, 0x15, 0x00, 0x00); + +/* GATT services table */ +static const struct ble_gatt_svc_def gatt_svr_svcs[] = { + /* Heart rate service */ + {.type = BLE_GATT_SVC_TYPE_PRIMARY, + .uuid = &heart_rate_svc_uuid.u, + .characteristics = + (struct ble_gatt_chr_def[]){ + {/* Heart rate characteristic */ + .uuid = &heart_rate_chr_uuid.u, + .access_cb = heart_rate_chr_access, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_INDICATE, + .val_handle = &heart_rate_chr_val_handle}, + { + 0, /* No more characteristics in this service. */ + }}}, + + /* Automation IO service */ + { + .type = BLE_GATT_SVC_TYPE_PRIMARY, + .uuid = &auto_io_svc_uuid.u, + .characteristics = + (struct ble_gatt_chr_def[]){/* LED characteristic */ + {.uuid = &led_chr_uuid.u, + .access_cb = led_chr_access, + .flags = BLE_GATT_CHR_F_WRITE, + .val_handle = &led_chr_val_handle}, + {0}}, + }, + + { + 0, /* No more services. */ + }, +}; +``` + +In this table, there are two GATT primary services defined + +- Heart rate service with a UUID of `0x180D` +- Automation IO service with a UUID of `0x1815` + +#### Automation IO Service + +Under automation IO service, there's a LED characteristic, with a vendor-specific UUID and write only permission. + +The characteristic is binded with `led_chr_access` callback function, in which the write access event is captured. The LED will be turned on or off according to the write value, quite straight-forward. + +``` C +static int led_chr_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) { + /* Local variables */ + int rc; + + /* Handle access events */ + /* Note: LED characteristic is write only */ + switch (ctxt->op) { + + /* Write characteristic event */ + case BLE_GATT_ACCESS_OP_WRITE_CHR: + /* Verify connection handle */ + if (conn_handle != BLE_HS_CONN_HANDLE_NONE) { + ESP_LOGI(TAG, "characteristic write; conn_handle=%d attr_handle=%d", + conn_handle, attr_handle); + } else { + ESP_LOGI(TAG, + "characteristic write by nimble stack; attr_handle=%d", + attr_handle); + } + + /* Verify attribute handle */ + if (attr_handle == led_chr_val_handle) { + /* Verify access buffer length */ + if (ctxt->om->om_len == 1) { + /* Turn the LED on or off according to the operation bit */ + if (ctxt->om->om_data[0]) { + led_on(); + ESP_LOGI(TAG, "led turned on!"); + } else { + led_off(); + ESP_LOGI(TAG, "led turned off!"); + } + } else { + goto error; + } + return rc; + } + goto error; + + /* Unknown event */ + default: + goto error; + } + +error: + ESP_LOGE(TAG, + "unexpected access operation to led characteristic, opcode: %d", + ctxt->op); + return BLE_ATT_ERR_UNLIKELY; +} +``` + +#### Heart Rate Service + +Under heart rate service, there's a heart rate measurement characteristic, with a UUID of `0x2A37` and read + indicate access permission. + +The characteristic is binded with `heart_rate_chr_access` callback function, in which the read access event is captured. It should be mentioned that in SIG definition, heart rate measurement is a multi-byte data structure, with the first byte indicating the flags + +- Bit 0: Heart rate value type + - 0: Heart rate value is `uint8_t` type + - 1: Heart rate value is `uint16_t` type +- Bit 1: Sensor contact status +- Bit 2: Sensor contact supported +- Bit 3: Energy expended status +- Bit 4: RR-interval status +- Bit 5-7: Reserved + +and the rest of bytes are data fields. In this case, we use `uint8_t` type and disable other features, making the characteristic value a 2-byte array. So when characteristic read event arrives, we'll get the latest heart rate value and send it back to peer device. + +``` C +static int heart_rate_chr_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) { + /* Local variables */ + int rc; + + /* Handle access events */ + /* Note: Heart rate characteristic is read only */ + switch (ctxt->op) { + + /* Read characteristic event */ + case BLE_GATT_ACCESS_OP_READ_CHR: + /* Verify connection handle */ + if (conn_handle != BLE_HS_CONN_HANDLE_NONE) { + ESP_LOGI(TAG, "characteristic read; conn_handle=%d attr_handle=%d", + conn_handle, attr_handle); + } else { + ESP_LOGI(TAG, "characteristic read by nimble stack; attr_handle=%d", + attr_handle); + } + + /* Verify attribute handle */ + if (attr_handle == heart_rate_chr_val_handle) { + /* Update access buffer value */ + heart_rate_chr_val[1] = get_heart_rate(); + rc = os_mbuf_append(ctxt->om, &heart_rate_chr_val, + sizeof(heart_rate_chr_val)); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + goto error; + + /* Unknown event */ + default: + goto error; + } + +error: + ESP_LOGE( + TAG, + "unexpected access operation to heart rate characteristic, opcode: %d", + ctxt->op); + return BLE_ATT_ERR_UNLIKELY; +} +``` + +Indicate access, however, is a bit more complicated. As mentioned in *GAP Service Updates*, we'll handle another 2 events namely `BLE_GAP_EVENT_NOTIFY_TX` and `BLE_GAP_EVENT_SUBSCRIBE` in `gap_event_handler`. In this case, if peer device wants to enable heart rate measurement indication, it will send a subscribe request to the local device, and the request will be captured as a subscribe event in `gap_event_handler`. But from the perspective of software layering, the event should be handled in GATT server, so we just pass the event to GATT server by calling `gatt_svr_subscribe_cb`, as demonstrated in the demo + +``` C +static int gap_event_handler(struct ble_gap_event *event, void *arg) { + ... + + /* Subscribe event */ + case BLE_GAP_EVENT_SUBSCRIBE: + /* Print subscription info to log */ + ESP_LOGI(TAG, + "subscribe event; conn_handle=%d attr_handle=%d " + "reason=%d prevn=%d curn=%d previ=%d curi=%d", + event->subscribe.conn_handle, event->subscribe.attr_handle, + event->subscribe.reason, event->subscribe.prev_notify, + event->subscribe.cur_notify, event->subscribe.prev_indicate, + event->subscribe.cur_indicate); + + /* GATT subscribe event callback */ + gatt_svr_subscribe_cb(event); + return rc; + + ... +} +``` + +Then we'll check connection handle and attribute handle, if the attribute handle matches `heart_rate_chr_val_chandle`, `heart_rate_chr_conn_handle` and `heart_rate_ind_status` will be updated together. + +``` C +void gatt_svr_subscribe_cb(struct ble_gap_event *event) { + /* Check connection handle */ + if (event->subscribe.conn_handle != BLE_HS_CONN_HANDLE_NONE) { + ESP_LOGI(TAG, "subscribe event; conn_handle=%d attr_handle=%d", + event->subscribe.conn_handle, event->subscribe.attr_handle); + } else { + ESP_LOGI(TAG, "subscribe by nimble stack; attr_handle=%d", + event->subscribe.attr_handle); + } + + /* Check attribute handle */ + if (event->subscribe.attr_handle == heart_rate_chr_val_handle) { + /* Update heart rate subscription status */ + heart_rate_chr_conn_handle = event->subscribe.conn_handle; + heart_rate_chr_conn_handle_inited = true; + heart_rate_ind_status = event->subscribe.cur_indicate; + } +} +``` + +Notice that heart rate measurement incation is handled in `heart_rate_task` by calling `send_heart_rate_indication` function periodically. Actually, this function will check heart rate indication status and send indication accordingly. In this way, heart rate indication is implemented. + +``` C +void send_heart_rate_indication(void) { + if (heart_rate_ind_status && heart_rate_chr_conn_handle_inited) { + ble_gatts_indicate(heart_rate_chr_conn_handle, + heart_rate_chr_val_handle); + ESP_LOGI(TAG, "heart rate indication sent!"); + } +} + +static void heart_rate_task(void *param) { + /* Task entry log */ + ESP_LOGI(TAG, "heart rate task has been started!"); + + /* Loop forever */ + while (1) { + /* Update heart rate value every 1 second */ + update_heart_rate(); + ESP_LOGI(TAG, "heart rate updated to %d", get_heart_rate()); + + /* Send heart rate indication if enabled */ + send_heart_rate_indication(); + + /* Sleep */ + vTaskDelay(HEART_RATE_TASK_PERIOD); + } + + /* Clean up at exit */ + vTaskDelete(NULL); +} +``` + +## Observation + +If everything goes well, you should be able to see 4 services when connected to ESP32, including + +- Generic Access +- Generic Attribute +- Heart Rate +- Automation IO + +Click on Automation IO Service, you should be able to see LED characteristic. Click on upload button, you should be able to write `ON` or `OFF` value. Send it to the device, LED will be turned on or off following your instruction. + +Click on Heart Rate Service, you should be able to see Heart Rate Measurement characteristic. Click on download button, you should be able to see the latest heart rate measurement mock value, and it should be consistent with what is shown on serial output. Click on subscribe button, you should be able to see the heart rate measurement mock value updated every second. + +## Troubleshooting + +For any technical queries, please file an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon. diff --git a/dependencies.lock b/dependencies.lock new file mode 100644 index 0000000..96f9a35 --- /dev/null +++ b/dependencies.lock @@ -0,0 +1,20 @@ +dependencies: + espressif/led_strip: + component_hash: 28c6509a727ef74925b372ed404772aeedf11cce10b78c3f69b3c66799095e2d + dependencies: + - name: idf + require: private + version: '>=4.4' + source: + registry_url: https://components.espressif.com/ + type: service + version: 2.5.5 + idf: + source: + type: idf + version: 5.5.2 +direct_dependencies: +- espressif/led_strip +manifest_hash: a9af7824fb34850fbe175d5384052634b3c00880abb2d3a7937e666d07603998 +target: esp32s3 +version: 2.0.0 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100755 index 0000000..757bc63 --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,5 @@ +file(GLOB_RECURSE srcs "main.c" "src/*.c") + +idf_component_register(SRCS "${srcs}" + PRIV_REQUIRES bt nvs_flash esp_driver_gpio esp_driver_uart + INCLUDE_DIRS "./include") diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100755 index 0000000..0035dc3 --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1,42 @@ +menu "Example Configuration" + + orsource "$IDF_PATH/examples/common_components/env_caps/$IDF_TARGET/Kconfig.env_caps" + + choice BLINK_LED + prompt "Blink LED type" + default BLINK_LED_GPIO + help + Select the LED type. A normal level controlled LED or an addressable LED strip. + The default selection is based on the Espressif DevKit boards. + You can change the default selection according to your board. + + config BLINK_LED_GPIO + bool "GPIO" + config BLINK_LED_STRIP + bool "LED strip" + endchoice + + choice BLINK_LED_STRIP_BACKEND + depends on BLINK_LED_STRIP + prompt "LED strip backend peripheral" + default BLINK_LED_STRIP_BACKEND_RMT if SOC_RMT_SUPPORTED + default BLINK_LED_STRIP_BACKEND_SPI + help + Select the backend peripheral to drive the LED strip. + + config BLINK_LED_STRIP_BACKEND_RMT + depends on SOC_RMT_SUPPORTED + bool "RMT" + config BLINK_LED_STRIP_BACKEND_SPI + bool "SPI" + endchoice + + config BLINK_GPIO + int "Blink GPIO number" + range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX + default 8 + help + GPIO number (IOxx) to blink on and off the LED. + Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used to blink. + +endmenu diff --git a/main/idf_component.yml b/main/idf_component.yml new file mode 100755 index 0000000..8723a2e --- /dev/null +++ b/main/idf_component.yml @@ -0,0 +1,2 @@ +dependencies: + espressif/led_strip: "^2.4.1" diff --git a/main/include/ble_client.h b/main/include/ble_client.h new file mode 100644 index 0000000..c46cfe7 --- /dev/null +++ b/main/include/ble_client.h @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#ifndef BLE_CLIENT_H +#define BLE_CLIENT_H + +#include +#include +#include + +void ble_client_init(void); +bool ble_client_is_ready(void); +uint16_t ble_client_max_payload(void); +int ble_client_send(const uint8_t *data, size_t len); + +#endif // BLE_CLIENT_H diff --git a/main/include/common.h b/main/include/common.h new file mode 100755 index 0000000..09bf09f --- /dev/null +++ b/main/include/common.h @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#ifndef COMMON_H +#define COMMON_H + +/* Includes */ +/* STD APIs */ +#include +#include +#include +#include + +/* ESP APIs */ +#include "esp_log.h" +#include "nvs_flash.h" +#include "sdkconfig.h" + +/* FreeRTOS APIs */ +#include +#include + +/* NimBLE stack APIs */ +#include "host/ble_hs.h" +#include "host/ble_uuid.h" +#include "host/util/util.h" +#include "nimble/ble.h" +#include "nimble/nimble_port.h" +#include "nimble/nimble_port_freertos.h" + +/* Defines */ +#define TAG "BLE_UART_BRIDGE" +#define DEVICE_NAME "ESP32-BLE-UART" + +#endif // COMMON_H diff --git a/main/include/uart_bridge.h b/main/include/uart_bridge.h new file mode 100644 index 0000000..6cda699 --- /dev/null +++ b/main/include/uart_bridge.h @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#ifndef UART_BRIDGE_H +#define UART_BRIDGE_H + +#include +#include + +void uart_bridge_init(void); +void uart_bridge_write(const uint8_t *data, size_t len); + +#endif // UART_BRIDGE_H diff --git a/main/main.c b/main/main.c new file mode 100755 index 0000000..2bce968 --- /dev/null +++ b/main/main.c @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +/* Includes */ +#include "ble_client.h" +#include "common.h" +#include "uart_bridge.h" + +/* Private function declarations */ +static void nimble_host_task(void *param); + +/* Private functions */ +static void nimble_host_task(void *param) { + ESP_LOGI(TAG, "nimble host task started"); + + /* This function won't return until nimble_port_stop() is executed */ + nimble_port_run(); + + /* Clean up at exit */ + vTaskDelete(NULL); +} + +void app_main(void) { + esp_err_t ret; + + /* NVS flash initialization (required by BLE stack) */ + ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || + ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + if (ret != ESP_OK) { + ESP_LOGE(TAG, "failed to initialize nvs flash, error code: %d", ret); + return; + } + + /* NimBLE stack initialization */ + ret = nimble_port_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "failed to initialize nimble stack, error code: %d", + ret); + return; + } + + uart_bridge_init(); + ble_client_init(); + + /* Start NimBLE host task thread and return */ + xTaskCreate(nimble_host_task, "NimBLE Host", 4 * 1024, NULL, 5, NULL); +} diff --git a/main/src/ble_client.c b/main/src/ble_client.c new file mode 100644 index 0000000..7502716 --- /dev/null +++ b/main/src/ble_client.c @@ -0,0 +1,665 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +/* Includes */ +#include "ble_client.h" +#include "common.h" +#include "uart_bridge.h" + +#include +#include + +#include "host/ble_att.h" +#include "host/ble_gatt.h" +#include "host/ble_hs_adv.h" +#include "os/os_mbuf.h" +#include "services/gap/ble_svc_gap.h" + +/* Library function declarations */ +void ble_store_config_init(void); + +/* Defines */ +#define TARGET_DEVICE_NAME "lyfPrinter" +#define BLE_DESIRED_MTU 247 +#define MAX_SERVICES 16 +#define MAX_CHRS 32 +#define CONNECT_TIMEOUT_MS 30000 +#define RX_FRAME_BUF_SIZE 512 + +/* Private variables */ +static uint8_t own_addr_type; +static ble_addr_t target_addr; +static bool target_addr_valid; +static bool connecting; +static bool connected; +static uint16_t conn_handle = BLE_HS_CONN_HANDLE_NONE; + +static struct ble_gatt_svc svc_list[MAX_SERVICES]; +static size_t svc_count; +static size_t svc_index; + +static struct ble_gatt_chr chr_list[MAX_CHRS]; +static size_t chr_count; + +static uint16_t notify_val_handle; +static uint16_t notify_end_handle; +static uint16_t write_val_handle; +static uint16_t cccd_handle; +static uint16_t negotiated_mtu = BLE_ATT_MTU_DFLT; +static uint8_t rx_frame_buf[RX_FRAME_BUF_SIZE]; +static size_t rx_frame_len; +static bool auto_status_query_pending = true; + +/* Private function declarations */ +static void start_scan(void); +static void connect_target(void); +static void reset_gatt_state(void); +static void discover_chrs_next_service(void); +static void discover_cccd(void); +static bool adv_name_matches(const struct ble_hs_adv_fields *fields); +static void ble_client_on_reset(int reason); +static void ble_client_on_sync(void); +static void handle_notify_bytes(const uint8_t *data, size_t len); +static void process_frame(const uint8_t *frame, size_t len); +static void send_status_query_if_ready(void); +static uint8_t calc_checksum(const uint8_t *data, size_t len); +static void uart_write_hex_line(const uint8_t *data, size_t len, + const char *prefix); + +static int gap_event_handler(struct ble_gap_event *event, void *arg); +static int svc_disc_cb(uint16_t conn_handle, const struct ble_gatt_error *error, + const struct ble_gatt_svc *service, void *arg); +static int chr_disc_cb(uint16_t conn_handle, const struct ble_gatt_error *error, + const struct ble_gatt_chr *chr, void *arg); +static int dsc_disc_cb(uint16_t conn_handle, const struct ble_gatt_error *error, + uint16_t chr_val_handle, + const struct ble_gatt_dsc *dsc, void *arg); +static int mtu_exchange_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, uint16_t mtu, + void *arg); +static int cccd_write_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, void *arg); + +/* Private functions */ +static void reset_gatt_state(void) { + svc_count = 0; + svc_index = 0; + chr_count = 0; + notify_val_handle = 0; + notify_end_handle = 0; + write_val_handle = 0; + cccd_handle = 0; + negotiated_mtu = BLE_ATT_MTU_DFLT; + rx_frame_len = 0; + auto_status_query_pending = true; +} + +static bool adv_name_matches(const struct ble_hs_adv_fields *fields) { + if (fields->name == NULL || fields->name_len == 0) { + return false; + } + + char name_buf[32]; + size_t copy_len = fields->name_len; + if (copy_len >= sizeof(name_buf)) { + copy_len = sizeof(name_buf) - 1; + } + + memcpy(name_buf, fields->name, copy_len); + name_buf[copy_len] = '\0'; + + char *start = name_buf; + while (*start && isspace((unsigned char)*start)) { + start++; + } + + char *end = start + strlen(start); + while (end > start && isspace((unsigned char)*(end - 1))) { + end--; + } + *end = '\0'; + + if (*start == '\0') { + return false; + } + + return strcasecmp(start, TARGET_DEVICE_NAME) == 0; +} + +static void start_scan(void) { + int rc; + struct ble_gap_disc_params params = {0}; + + rc = ble_hs_id_infer_auto(0, &own_addr_type); + if (rc != 0) { + ESP_LOGE(TAG, "failed to infer address type; rc=%d", rc); + return; + } + + params.filter_duplicates = 1; + params.passive = 0; + params.itvl = 0; + params.window = 0; + params.filter_policy = 0; + params.limited = 0; + + rc = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, + gap_event_handler, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "failed to start scan; rc=%d", rc); + return; + } + + ESP_LOGI(TAG, "scanning for %s", TARGET_DEVICE_NAME); +} + +static void connect_target(void) { + int rc; + + if (!target_addr_valid || connecting || connected) { + return; + } + + rc = ble_gap_connect(own_addr_type, &target_addr, CONNECT_TIMEOUT_MS, NULL, + gap_event_handler, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "failed to initiate connection; rc=%d", rc); + target_addr_valid = false; + start_scan(); + return; + } + + connecting = true; + ESP_LOGI(TAG, "connecting to target device..."); +} + +static void discover_chrs_next_service(void) { + int rc; + + if (!connected || conn_handle == BLE_HS_CONN_HANDLE_NONE) { + return; + } + + if (svc_index >= svc_count) { + discover_cccd(); + return; + } + + chr_count = 0; + const struct ble_gatt_svc *svc = &svc_list[svc_index]; + rc = ble_gattc_disc_all_chrs(conn_handle, svc->start_handle, + svc->end_handle, chr_disc_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "failed to discover characteristics; rc=%d", rc); + svc_index++; + discover_chrs_next_service(); + } +} + +static void discover_cccd(void) { + int rc; + + if (notify_val_handle == 0 || notify_end_handle == 0) { + ESP_LOGW(TAG, "notify characteristic not found"); + return; + } + + rc = ble_gattc_disc_all_dscs(conn_handle, notify_val_handle, + notify_end_handle, dsc_disc_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "failed to discover descriptors; rc=%d", rc); + } +} + +static int svc_disc_cb(uint16_t handle, const struct ble_gatt_error *error, + const struct ble_gatt_svc *service, void *arg) { + (void)handle; + (void)arg; + if (error->status == 0) { + if (svc_count < MAX_SERVICES) { + svc_list[svc_count++] = *service; + } else { + ESP_LOGW(TAG, "service list full; ignoring extra services"); + } + return 0; + } + + if (error->status == BLE_HS_EDONE) { + svc_index = 0; + discover_chrs_next_service(); + return 0; + } + + ESP_LOGE(TAG, "service discovery failed; status=%d", error->status); + return error->status; +} + +static void process_chr_list(uint16_t svc_end_handle) { + static const ble_uuid16_t notify_uuid = BLE_UUID16_INIT(0xFFF1); + static const ble_uuid16_t write_uuid = BLE_UUID16_INIT(0xFFF2); + + for (size_t i = 0; i < chr_count; i++) { + const struct ble_gatt_chr *chr = &chr_list[i]; + + if (ble_uuid_cmp(&chr->uuid.u, ¬ify_uuid.u) == 0) { + notify_val_handle = chr->val_handle; + if (i + 1 < chr_count) { + notify_end_handle = chr_list[i + 1].def_handle - 1; + } else { + notify_end_handle = svc_end_handle; + } + ESP_LOGI(TAG, "found notify characteristic; handle=%d", + notify_val_handle); + } + + if (ble_uuid_cmp(&chr->uuid.u, &write_uuid.u) == 0) { + write_val_handle = chr->val_handle; + ESP_LOGI(TAG, "found write characteristic; handle=%d", + write_val_handle); + } + } +} + +static int chr_disc_cb(uint16_t handle, const struct ble_gatt_error *error, + const struct ble_gatt_chr *chr, void *arg) { + (void)handle; + (void)arg; + if (error->status == 0) { + if (chr_count < MAX_CHRS) { + chr_list[chr_count++] = *chr; + } else { + ESP_LOGW(TAG, "characteristic list full; ignoring extra entries"); + } + return 0; + } + + if (error->status == BLE_HS_EDONE) { + if (svc_index < svc_count) { + process_chr_list(svc_list[svc_index].end_handle); + svc_index++; + } + discover_chrs_next_service(); + return 0; + } + + ESP_LOGE(TAG, "characteristic discovery failed; status=%d", error->status); + return error->status; +} + +static int dsc_disc_cb(uint16_t handle, const struct ble_gatt_error *error, + uint16_t chr_val_handle, + const struct ble_gatt_dsc *dsc, void *arg) { + (void)handle; + (void)chr_val_handle; + (void)arg; + if (error->status == 0) { + if (ble_uuid_cmp(&dsc->uuid.u, + BLE_UUID16_DECLARE(BLE_GATT_DSC_CLT_CFG_UUID16)) == + 0) { + cccd_handle = dsc->handle; + ESP_LOGI(TAG, "found CCCD; handle=%d", cccd_handle); + } + return 0; + } + + if (error->status == BLE_HS_EDONE) { + if (cccd_handle == 0) { + ESP_LOGW(TAG, "CCCD not found; notifications disabled"); + return 0; + } + + uint8_t enable_notify[2] = {0x01, 0x00}; + int rc = ble_gattc_write_flat(conn_handle, cccd_handle, enable_notify, + sizeof(enable_notify), cccd_write_cb, + NULL); + if (rc != 0) { + ESP_LOGE(TAG, "failed to write CCCD; rc=%d", rc); + } + return 0; + } + + ESP_LOGE(TAG, "descriptor discovery failed; status=%d", error->status); + return error->status; +} + +static int mtu_exchange_cb(uint16_t handle, const struct ble_gatt_error *error, + uint16_t mtu, void *arg) { + (void)handle; + (void)arg; + if (error->status == 0) { + negotiated_mtu = mtu; + ESP_LOGI(TAG, "mtu exchanged; mtu=%d", mtu); + return 0; + } + + ESP_LOGW(TAG, "mtu exchange failed; status=%d", error->status); + return error->status; +} + +static int cccd_write_cb(uint16_t handle, const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, void *arg) { + (void)handle; + (void)attr; + (void)arg; + if (error->status == 0) { + ESP_LOGI(TAG, "notifications enabled"); + send_status_query_if_ready(); + return 0; + } + + ESP_LOGE(TAG, "failed to enable notifications; status=%d", error->status); + return error->status; +} + +static uint8_t calc_checksum(const uint8_t *data, size_t len) { + uint16_t sum = 0; + for (size_t i = 0; i < len; i++) { + sum += data[i]; + } + return (uint8_t)(sum & 0xFF); +} + +static void uart_write_hex_line(const uint8_t *data, size_t len, + const char *prefix) { + char buf[64]; + if (prefix != NULL) { + uart_bridge_write((const uint8_t *)prefix, strlen(prefix)); + } + + for (size_t i = 0; i < len; i++) { + int n = snprintf(buf, sizeof(buf), "%02X%s", data[i], + (i + 1 == len) ? "" : " "); + uart_bridge_write((const uint8_t *)buf, (size_t)n); + } + uart_bridge_write((const uint8_t *)"\r\n", 2); +} + +static void process_frame(const uint8_t *frame, size_t len) { + if (len < 5) { + return; + } + + uint16_t data_len = (uint16_t)((frame[2] << 8) | frame[3]); + if (data_len + 5 != len) { + return; + } + + uart_write_hex_line(frame, len, "RX: "); + + if (frame[1] == 0x01 && data_len >= 5) { + uint8_t paper = frame[4]; + uint8_t battery = frame[5]; + uint8_t temp_sign = frame[6]; + uint16_t temp_raw = (uint16_t)((frame[7] << 8) | frame[8]); + char msg[96]; + int n = snprintf(msg, sizeof(msg), + "STATUS: paper=%s battery=%u%% temp_sign=0x%02X " + "temp_raw=0x%04X\r\n", + paper == 0x01 ? "present" : "absent", battery, + temp_sign, temp_raw); + uart_bridge_write((const uint8_t *)msg, (size_t)n); + } else if (data_len == 1) { + char msg[64]; + int n = snprintf(msg, sizeof(msg), + "ACK: func=0x%02X result=0x%02X\r\n", frame[1], + frame[4]); + uart_bridge_write((const uint8_t *)msg, (size_t)n); + } +} + +static void handle_notify_bytes(const uint8_t *data, size_t len) { + size_t offset = 0; + + while (offset < len) { + size_t space = RX_FRAME_BUF_SIZE - rx_frame_len; + size_t copy_len = len - offset; + if (copy_len > space) { + copy_len = space; + } + + memcpy(&rx_frame_buf[rx_frame_len], &data[offset], copy_len); + rx_frame_len += copy_len; + offset += copy_len; + + while (rx_frame_len >= 5) { + uint16_t data_len = + (uint16_t)((rx_frame_buf[2] << 8) | rx_frame_buf[3]); + size_t total_len = (size_t)data_len + 5; + + if (total_len > RX_FRAME_BUF_SIZE) { + memmove(rx_frame_buf, rx_frame_buf + 1, rx_frame_len - 1); + rx_frame_len -= 1; + continue; + } + + if (rx_frame_len < total_len) { + break; + } + + uint8_t checksum = calc_checksum(rx_frame_buf, total_len - 1); + if (checksum != rx_frame_buf[total_len - 1]) { + memmove(rx_frame_buf, rx_frame_buf + 1, rx_frame_len - 1); + rx_frame_len -= 1; + continue; + } + + process_frame(rx_frame_buf, total_len); + memmove(rx_frame_buf, rx_frame_buf + total_len, + rx_frame_len - total_len); + rx_frame_len -= total_len; + } + + if (rx_frame_len == RX_FRAME_BUF_SIZE) { + rx_frame_len = 0; + } + } +} + +static void send_status_query_if_ready(void) { + if (!auto_status_query_pending || !ble_client_is_ready()) { + return; + } + + uint8_t frame[5] = {0x01, 0x01, 0x00, 0x00, 0x02}; + int rc = ble_client_send(frame, sizeof(frame)); + if (rc == 0) { + auto_status_query_pending = false; + ESP_LOGI(TAG, "status query sent"); + uart_write_hex_line(frame, sizeof(frame), "TX: "); + } else { + ESP_LOGW(TAG, "status query failed; rc=%d", rc); + } +} + +static int gap_event_handler(struct ble_gap_event *event, void *arg) { + struct ble_hs_adv_fields fields; + + switch (event->type) { + case BLE_GAP_EVENT_DISC: { + if (connecting || connected) { + return 0; + } + + int rc = ble_hs_adv_parse_fields(&fields, event->disc.data, + event->disc.length_data); + if (rc == 0 && adv_name_matches(&fields)) { + target_addr = event->disc.addr; + target_addr_valid = true; + ESP_LOGI(TAG, "target found; stopping scan"); +#if !(MYNEWT_VAL(BLE_HOST_ALLOW_CONNECT_WITH_SCAN)) + rc = ble_gap_disc_cancel(); + if (rc != 0 && rc != BLE_HS_EALREADY) { + ESP_LOGW(TAG, "scan cancel failed; rc=%d", rc); + } +#endif + connect_target(); + } + return 0; + } + + case BLE_GAP_EVENT_DISC_COMPLETE: + ESP_LOGI(TAG, "scan complete; reason=%d", + event->disc_complete.reason); + if (!connecting && !connected) { + if (target_addr_valid) { + connect_target(); + } else { + start_scan(); + } + } + return 0; + + case BLE_GAP_EVENT_CONNECT: + connecting = false; + if (event->connect.status == 0) { + connected = true; + conn_handle = event->connect.conn_handle; + ESP_LOGI(TAG, "connected; conn_handle=%d", conn_handle); + + reset_gatt_state(); + ble_att_set_preferred_mtu(BLE_DESIRED_MTU); + ble_gattc_exchange_mtu(conn_handle, mtu_exchange_cb, NULL); + ble_gattc_disc_all_svcs(conn_handle, svc_disc_cb, NULL); + } else { + ESP_LOGW(TAG, "connection failed; status=%d", + event->connect.status); + connected = false; + conn_handle = BLE_HS_CONN_HANDLE_NONE; + start_scan(); + } + return 0; + + case BLE_GAP_EVENT_DISCONNECT: + ESP_LOGW(TAG, "disconnected; reason=%d", event->disconnect.reason); + connecting = false; + connected = false; + conn_handle = BLE_HS_CONN_HANDLE_NONE; + target_addr_valid = false; + reset_gatt_state(); + start_scan(); + return 0; + + case BLE_GAP_EVENT_NOTIFY_RX: { + uint16_t total = OS_MBUF_PKTLEN(event->notify_rx.om); + uint16_t offset = 0; + uint8_t buf[128]; + + while (offset < total) { + uint16_t chunk = total - offset; + if (chunk > sizeof(buf)) { + chunk = sizeof(buf); + } + os_mbuf_copydata(event->notify_rx.om, offset, chunk, buf); + handle_notify_bytes(buf, chunk); + offset += chunk; + } + return 0; + } + + case BLE_GAP_EVENT_MTU: + negotiated_mtu = event->mtu.value; + ESP_LOGI(TAG, "mtu updated; mtu=%d", negotiated_mtu); + return 0; + + case BLE_GAP_EVENT_NOTIFY_TX: + if ((event->notify_tx.status != 0) && + (event->notify_tx.status != BLE_HS_EDONE)) { + ESP_LOGW(TAG, "notify tx error; status=%d", event->notify_tx.status); + } + return 0; + + default: + return 0; + } +} + +static void ble_client_on_reset(int reason) { + ESP_LOGE(TAG, "nimble reset; reason=%d", reason); +} + +static void ble_client_on_sync(void) { + int rc = ble_hs_util_ensure_addr(0); + if (rc != 0) { + ESP_LOGE(TAG, "failed to ensure addr; rc=%d", rc); + return; + } + + target_addr_valid = false; + connecting = false; + connected = false; + conn_handle = BLE_HS_CONN_HANDLE_NONE; + reset_gatt_state(); + start_scan(); +} + +/* Public functions */ +bool ble_client_is_ready(void) { + return connected && (write_val_handle != 0); +} + +uint16_t ble_client_max_payload(void) { + uint16_t mtu = negotiated_mtu; + if (mtu < BLE_ATT_MTU_DFLT) { + mtu = BLE_ATT_MTU_DFLT; + } + if (mtu <= 3) { + return 0; + } + return (uint16_t)(mtu - 3); +} + +int ble_client_send(const uint8_t *data, size_t len) { + if (data == NULL || len == 0) { + return 0; + } + if (!connected || write_val_handle == 0) { + return BLE_HS_EINVAL; + } + + uint16_t max_payload = ble_client_max_payload(); + if (max_payload == 0) { + max_payload = BLE_ATT_MTU_DFLT - 3; + } + + size_t offset = 0; + while (offset < len) { + uint16_t chunk = (uint16_t)(len - offset); + if (chunk > max_payload) { + chunk = max_payload; + } + + int rc = ble_gattc_write_no_rsp_flat(conn_handle, write_val_handle, + data + offset, chunk); + if (rc != 0) { + ESP_LOGE(TAG, "write failed; rc=%d", rc); + return rc; + } + + offset += chunk; + } + + return 0; +} + +void ble_client_init(void) { + ble_hs_cfg.reset_cb = ble_client_on_reset; + ble_hs_cfg.sync_cb = ble_client_on_sync; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; + ble_hs_cfg.sm_io_cap = BLE_HS_IO_NO_INPUT_OUTPUT; + ble_hs_cfg.sm_bonding = 0; + ble_hs_cfg.sm_mitm = 0; + ble_hs_cfg.sm_sc = 0; + +#if CONFIG_BT_NIMBLE_GAP_SERVICE + ble_svc_gap_init(); + int rc = ble_svc_gap_device_name_set(DEVICE_NAME); + if (rc != 0) { + ESP_LOGW(TAG, "failed to set device name; rc=%d", rc); + } +#endif + + ble_store_config_init(); +} diff --git a/main/src/uart_bridge.c b/main/src/uart_bridge.c new file mode 100644 index 0000000..190cfbe --- /dev/null +++ b/main/src/uart_bridge.c @@ -0,0 +1,281 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +/* Includes */ +#include "uart_bridge.h" +#include "ble_client.h" +#include "common.h" + +#include "driver/uart.h" + +#include + +/* Defines */ +#define UART_PORT UART_NUM_0 +#define UART_BAUD_RATE 115200 +#define UART_RX_BUF_SIZE 2048 +#define UART_TX_BUF_SIZE 2048 +#define UART_READ_TIMEOUT_MS 20 +#define UART_TASK_STACK 4096 +#define UART_TASK_PRIORITY 5 +#define UART_LINE_BUF_SIZE 512 +#define UART_ECHO_INPUT 1 +#define UART_MAX_FRAME_BYTES 512 + +/* Private function declarations */ +static void uart_rx_task(void *param); +static bool parse_hex_line(const uint8_t *line_buf, size_t line_len, + uint8_t *out, size_t out_max, size_t *out_len); +static bool build_frame(const uint8_t *input, size_t input_len, + uint8_t *out, size_t out_max, size_t *out_len); +static uint8_t calc_checksum(const uint8_t *data, size_t len); +static int hex_val(uint8_t ch); + +static int hex_val(uint8_t ch) { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } + if (ch >= 'a' && ch <= 'f') { + return 10 + (ch - 'a'); + } + if (ch >= 'A' && ch <= 'F') { + return 10 + (ch - 'A'); + } + return -1; +} + +static uint8_t calc_checksum(const uint8_t *data, size_t len) { + uint16_t sum = 0; + for (size_t i = 0; i < len; i++) { + sum += data[i]; + } + return (uint8_t)(sum & 0xFF); +} + +static bool parse_hex_line(const uint8_t *line_buf, size_t line_len, + uint8_t *out, size_t out_max, size_t *out_len) { + size_t i = 0; + size_t count = 0; + + while (i < line_len) { + while (i < line_len && + (line_buf[i] == ' ' || line_buf[i] == '\t' || + line_buf[i] == ',')) { + i++; + } + + if (i >= line_len) { + break; + } + + if (line_buf[i] == '#') { + break; + } + + if (i + 1 < line_len && line_buf[i] == '0' && + (line_buf[i + 1] == 'x' || line_buf[i + 1] == 'X')) { + i += 2; + } + + int digit = hex_val(line_buf[i]); + if (digit < 0) { + return false; + } + + uint8_t value = 0; + int digits = 0; + while (i < line_len) { + digit = hex_val(line_buf[i]); + if (digit < 0) { + break; + } + value = (uint8_t)((value << 4) | (uint8_t)digit); + digits++; + if (digits > 2) { + return false; + } + i++; + } + + if (digits == 0) { + return false; + } + + if (count >= out_max) { + return false; + } + out[count++] = value; + } + + *out_len = count; + return count > 0; +} + +static bool build_frame(const uint8_t *input, size_t input_len, + uint8_t *out, size_t out_max, size_t *out_len) { + if (input_len < 2) { + return false; + } + + if (input_len >= 5) { + uint16_t declared_len = (uint16_t)((input[2] << 8) | input[3]); + size_t expected = (size_t)declared_len + 5; + if (expected == input_len) { + uint8_t checksum = calc_checksum(input, input_len - 1); + if (checksum != input[input_len - 1]) { + ESP_LOGW(TAG, "checksum mismatch; expected 0x%02X got 0x%02X", + checksum, input[input_len - 1]); + return false; + } + if (input_len > out_max) { + return false; + } + memcpy(out, input, input_len); + *out_len = input_len; + return true; + } + } + + size_t data_len = input_len - 2; + size_t total_len = data_len + 5; + if (total_len > out_max) { + return false; + } + + out[0] = input[0]; + out[1] = input[1]; + out[2] = (uint8_t)((data_len >> 8) & 0xFF); + out[3] = (uint8_t)(data_len & 0xFF); + if (data_len > 0) { + memcpy(&out[4], &input[2], data_len); + } + out[4 + data_len] = calc_checksum(out, 4 + data_len); + *out_len = total_len; + return true; +} + +/* Private functions */ +static void uart_rx_task(void *param) { + uint8_t buf[256]; + uint8_t line_buf[UART_LINE_BUF_SIZE]; + uint8_t frame_buf[UART_MAX_FRAME_BYTES]; + uint8_t parsed_buf[UART_MAX_FRAME_BYTES]; + size_t line_len = 0; + bool skip_lf = false; + TickType_t last_log = 0; + + while (1) { + int len = uart_read_bytes(UART_PORT, buf, sizeof(buf), + pdMS_TO_TICKS(UART_READ_TIMEOUT_MS)); + if (len > 0) { + for (int i = 0; i < len; i++) { + uint8_t ch = buf[i]; + + if (skip_lf) { + skip_lf = false; + if (ch == '\n') { + continue; + } + } + + if (ch == '\r' || ch == '\n') { +#if UART_ECHO_INPUT + uart_write_bytes(UART_PORT, "\r\n", 2); +#endif + if (line_len > 0) { + size_t parsed_len = 0; + if (!parse_hex_line(line_buf, line_len, parsed_buf, + sizeof(parsed_buf), &parsed_len)) { + ESP_LOGW(TAG, "invalid hex input; ignored"); + } else { + size_t frame_len = 0; + if (!build_frame(parsed_buf, parsed_len, frame_buf, + sizeof(frame_buf), &frame_len)) { + ESP_LOGW(TAG, "failed to build frame"); + } else { + int rc = ble_client_send(frame_buf, frame_len); + if (rc != 0) { + TickType_t now = xTaskGetTickCount(); + if (now - last_log > + pdMS_TO_TICKS(1000)) { + ESP_LOGW(TAG, + "ble not ready; dropping " + "uart line (%d)", + rc); + last_log = now; + } + } + } + } + } + line_len = 0; + if (ch == '\r') { + skip_lf = true; + } + continue; + } + +#if UART_ECHO_INPUT + if (ch == '\b' || ch == 0x7F) { + if (line_len > 0) { + line_len--; + uart_write_bytes(UART_PORT, "\b \b", 3); + } + continue; + } +#else + if (ch == '\b' || ch == 0x7F) { + if (line_len > 0) { + line_len--; + } + continue; + } +#endif + +#if UART_ECHO_INPUT + uart_write_bytes(UART_PORT, (const char *)&ch, 1); +#endif + if (line_len < sizeof(line_buf)) { + line_buf[line_len++] = ch; + } else { + line_len = 0; + TickType_t now = xTaskGetTickCount(); + if (now - last_log > pdMS_TO_TICKS(1000)) { + ESP_LOGW(TAG, "uart line too long; dropped"); + last_log = now; + } + } + } + } + } +} + +/* Public functions */ +void uart_bridge_init(void) { + uart_config_t config = { + .baud_rate = UART_BAUD_RATE, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_DEFAULT, + }; + + ESP_ERROR_CHECK(uart_driver_install(UART_PORT, UART_RX_BUF_SIZE, + UART_TX_BUF_SIZE, 0, NULL, 0)); + ESP_ERROR_CHECK(uart_param_config(UART_PORT, &config)); + + ESP_LOGI(TAG, + "UART expects hex bytes per line. Example: 01 00 00 01 01 03"); + xTaskCreate(uart_rx_task, "uart_rx", UART_TASK_STACK, NULL, + UART_TASK_PRIORITY, NULL); +} + +void uart_bridge_write(const uint8_t *data, size_t len) { + if (data == NULL || len == 0) { + return; + } + uart_write_bytes(UART_PORT, (const char *)data, len); +} diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100755 index 0000000..551f506 --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,6 @@ +CONFIG_BT_ENABLED=y +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_50_FEATURE_SUPPORT=n + +CONFIG_BLINK_LED_GPIO=y +CONFIG_BLINK_GPIO=8 diff --git a/sdkconfig.defaults.esp32 b/sdkconfig.defaults.esp32 new file mode 100755 index 0000000..263ec93 --- /dev/null +++ b/sdkconfig.defaults.esp32 @@ -0,0 +1 @@ +CONFIG_BLINK_GPIO=5 diff --git a/sdkconfig.defaults.esp32c3 b/sdkconfig.defaults.esp32c3 new file mode 100755 index 0000000..da04430 --- /dev/null +++ b/sdkconfig.defaults.esp32c3 @@ -0,0 +1 @@ +CONFIG_BLINK_LED_STRIP=y diff --git a/sdkconfig.defaults.esp32c5 b/sdkconfig.defaults.esp32c5 new file mode 100755 index 0000000..faee357 --- /dev/null +++ b/sdkconfig.defaults.esp32c5 @@ -0,0 +1,2 @@ +CONFIG_BLINK_GPIO=27 +CONFIG_BLINK_LED_STRIP=y diff --git a/sdkconfig.defaults.esp32c6 b/sdkconfig.defaults.esp32c6 new file mode 100755 index 0000000..da04430 --- /dev/null +++ b/sdkconfig.defaults.esp32c6 @@ -0,0 +1 @@ +CONFIG_BLINK_LED_STRIP=y diff --git a/sdkconfig.defaults.esp32c61 b/sdkconfig.defaults.esp32c61 new file mode 100755 index 0000000..da04430 --- /dev/null +++ b/sdkconfig.defaults.esp32c61 @@ -0,0 +1 @@ +CONFIG_BLINK_LED_STRIP=y diff --git a/sdkconfig.defaults.esp32h2 b/sdkconfig.defaults.esp32h2 new file mode 100755 index 0000000..da04430 --- /dev/null +++ b/sdkconfig.defaults.esp32h2 @@ -0,0 +1 @@ +CONFIG_BLINK_LED_STRIP=y diff --git a/sdkconfig.defaults.esp32s3 b/sdkconfig.defaults.esp32s3 new file mode 100755 index 0000000..5ac9d30 --- /dev/null +++ b/sdkconfig.defaults.esp32s3 @@ -0,0 +1,2 @@ +CONFIG_BLINK_LED_STRIP=y +CONFIG_BLINK_GPIO=48