Add CYD BLE foundation with NimBLE Nordic UART.
Reusable ble_core (+ optional LVGL ble_ui), flashable firmware/cyd, aligned with wifitest as a base feature. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
*.pyc
|
||||
__pycache__/
|
||||
109
README.md
Normal file
109
README.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# CYD BLE 基础
|
||||
|
||||
面向 **ESP32-2432S028(CYD)** 的可复用 BLE 外设模块。显示/触控沿用 [tftest](../tftest) 的 LVGL9 + SoftSPI XPT2046 方案;工程约定见 [CYD_PROJECT_GUIDE.md](../CYD_PROJECT_GUIDE.md)。
|
||||
|
||||
## 为什么这样设计
|
||||
|
||||
| 方案 | 优点 | 缺点 | 本项目 |
|
||||
|------|------|------|--------|
|
||||
| **NimBLE + Nordic UART (NUS)** | 省内存;手机 App 生态成熟;Espressif / Arduino 主流 | 需理解 GATT | **主路径** |
|
||||
| Bluedroid(Arduino 自带 BLE) | API 常见 | RAM/Flash 占用大,和 WiFi/LVGL 难挤 | 未采用 |
|
||||
| Espressif BLE Provisioning | 官方配网、可加密 | 要专用 App;本仓 WiFi 已有屏上配网 | 后续可选扩展 |
|
||||
| Improv WiFi over BLE | Web Bluetooth 友好 | 偏配网场景 | 后续可选 |
|
||||
|
||||
对标 [wifitest](../wifitest):`ble_core` 无 UI 依赖,可拷到任意工程;`ble_ui` 为屏上演示。
|
||||
|
||||
## 手机怎么连
|
||||
|
||||
1. 安装 **nRF Connect**(Nordic)或 **Serial Bluetooth Terminal** / LightBlue。
|
||||
2. 扫描广播名 **`CYD-BLE`**,连接。
|
||||
3. 找到 Nordic UART Service:
|
||||
- RX `6E400002-…`:手机 → 板子(Write)
|
||||
- TX `6E400003-…`:板子 → 手机(Notify,先 Enable)
|
||||
4. 默认 **Echo ON**:手机发什么,板子原样 Notify 回来;屏上 **Ping** 会发 `Hello from CYD`。
|
||||
|
||||
## 刷写(无需每次编译)
|
||||
|
||||
预编译包在 [`firmware/cyd/`](firmware/cyd/):
|
||||
|
||||
```powershell
|
||||
cd firmware\cyd
|
||||
.\flash.ps1
|
||||
# 或
|
||||
.\flash.ps1 -Port COM3
|
||||
```
|
||||
|
||||
重新打包:
|
||||
|
||||
```powershell
|
||||
.\firmware\export_from_build.ps1
|
||||
.\firmware\export_from_build.ps1 -SkipBuild
|
||||
```
|
||||
|
||||
源码直接烧录:`pio run -e cyd -t upload`
|
||||
串口监视:`pio device monitor -b 460800`
|
||||
|
||||
## 接到其它项目
|
||||
|
||||
只需带走这些文件(无业务耦合):
|
||||
|
||||
```
|
||||
include/ble_core.h # 核心 API(无 LVGL)
|
||||
src/ble_core.cpp
|
||||
include/ble_ui.h # 可选:屏上 UI
|
||||
src/ble_ui.cpp
|
||||
```
|
||||
|
||||
`platformio.ini` 增加:
|
||||
|
||||
```ini
|
||||
lib_deps =
|
||||
h2zero/NimBLE-Arduino@^2.3.2
|
||||
```
|
||||
|
||||
有屏时的最小接入:
|
||||
|
||||
```cpp
|
||||
ble_core_begin(); // 默认广播 CYD-BLE
|
||||
ble_ui_create();
|
||||
|
||||
void loop() {
|
||||
ble_core_loop();
|
||||
ble_ui_tick();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
无屏 / 只要管道:
|
||||
|
||||
```cpp
|
||||
ble_core_begin("MyDevice");
|
||||
ble_core_set_rx_cb([](const uint8_t *data, size_t len, void *) {
|
||||
ble_core_send(data, len); // 或自己的协议
|
||||
}, nullptr);
|
||||
// loop 里 ble_core_loop();
|
||||
```
|
||||
|
||||
宏可覆盖(`build_flags`):
|
||||
|
||||
- `BLE_CORE_DEVICE_NAME` — 默认 `CYD-BLE`
|
||||
- `BLE_CORE_RX_QUEUE` / `BLE_CORE_RX_MAX` — 收包队列
|
||||
|
||||
## 模块划分
|
||||
|
||||
- `touch.*` / `pins.h` — CYD 硬件(与 tftest 同校准)
|
||||
- `ble_core.*` — NimBLE、NUS、广播、收发、断线重广播
|
||||
- `ble_ui.*` — LVGL 状态页 + Adv / Echo / Ping
|
||||
- `main.cpp` — 演示入口
|
||||
|
||||
## 与 WiFi 的关系
|
||||
|
||||
当前演示里 `ble_core_begin()` 会 `WiFi.mode(WIFI_OFF)`,把射频留给 BLE。后续作为基础功能与 WiFi 同开时,参考 Espressif `bleprph_wifi_coex` 与 NimBLE 共存配置,再去掉强制关 WiFi。
|
||||
|
||||
## 分区
|
||||
|
||||
使用 `min_spiffs.csv`(约 1.9MB APP),避免默认 1.2MB 在 LVGL+NimBLE 下顶满,并为后续 WiFi 共存留余量。
|
||||
|
||||
## 硬件
|
||||
|
||||
CYD ESP32-2432S028R:ILI9341 240×320,横屏 `LV_DISPLAY_ROTATION_270`(逻辑 320×240)。板载 BLE 4.2,无需外接模块。
|
||||
13
copy_lv_conf.py
Normal file
13
copy_lv_conf.py
Normal file
@@ -0,0 +1,13 @@
|
||||
Import("env") # noqa: F821
|
||||
from pathlib import Path
|
||||
|
||||
src = Path(env["PROJECT_DIR"]) / "include" / "lv_conf.h" # noqa: F821
|
||||
dst_dir = Path(env["PROJECT_LIBDEPS_DIR"]) / env["PIOENV"] # noqa: F821
|
||||
dst_dir.mkdir(parents=True, exist_ok=True)
|
||||
dst = dst_dir / "lv_conf.h"
|
||||
if src.exists():
|
||||
dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
lvgl = dst_dir / "lvgl" / "lv_conf.h"
|
||||
if (dst_dir / "lvgl").exists():
|
||||
lvgl.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
print(f"copy_lv_conf: synced -> {dst}")
|
||||
49
firmware/README.md
Normal file
49
firmware/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# 预编译固件(可随时刷写)
|
||||
|
||||
当前归档目录:[`cyd/`](cyd/)
|
||||
|
||||
对应板子:**CYD ESP32-2432S028R**(ILI9341)
|
||||
功能:**BLE 外设**(NimBLE + Nordic UART + 屏上状态)
|
||||
版本信息见 [`cyd/VERSION.txt`](cyd/VERSION.txt)。
|
||||
|
||||
也可在 [`../../cydweb`](../../cydweb)(CYD Flasher)里通过浏览器串口刷写本目录固件。
|
||||
|
||||
## 直接刷写(无需重新编译)
|
||||
|
||||
1. USB 连接板子
|
||||
2. 执行:
|
||||
|
||||
```powershell
|
||||
cd firmware\cyd
|
||||
.\flash.ps1
|
||||
# 或指定端口
|
||||
.\flash.ps1 -Port COM3
|
||||
```
|
||||
|
||||
也可运行 `flash.bat`。
|
||||
|
||||
依赖其一即可:
|
||||
|
||||
- 已安装 PlatformIO(`pio`,自带 esptool)
|
||||
- 或 `pip install esptool`
|
||||
|
||||
## 手动 esptool
|
||||
|
||||
```bash
|
||||
esptool.py --chip esp32 --port COM3 --baud 921600 write_flash -z ^
|
||||
--flash_mode dio --flash_freq 40m --flash_size 4MB ^
|
||||
0x1000 bootloader.bin ^
|
||||
0x8000 partitions.bin ^
|
||||
0xe000 boot_app0.bin ^
|
||||
0x10000 firmware.bin
|
||||
```
|
||||
|
||||
## 从源码重新打包进本目录
|
||||
|
||||
在仓库根目录:
|
||||
|
||||
```powershell
|
||||
.\firmware\export_from_build.ps1
|
||||
# 仅复制、不编译:
|
||||
.\firmware\export_from_build.ps1 -SkipBuild
|
||||
```
|
||||
16
firmware/cyd/VERSION.txt
Normal file
16
firmware/cyd/VERSION.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
name: CYD BLE (cyd)
|
||||
git: unknown
|
||||
date: 2026-10-23
|
||||
board: ESP32-2432S028R (ILI9341)
|
||||
features: NimBLE Nordic UART, LVGL status UI, SoftSPI touch
|
||||
flash_size: 4MB
|
||||
offsets:
|
||||
0x1000 bootloader.bin
|
||||
0x8000 partitions.bin
|
||||
0xe000 boot_app0.bin
|
||||
0x10000 firmware.bin
|
||||
sha256:
|
||||
bootloader.bin 3D234A7471F67B013686DABD4DEE7C1FA915C9928463616A94BC9297ACF1ABF8
|
||||
boot_app0.bin F94C5D786A7A8FAB06AC5D10E33BF37711A6697636DC037559EA19CC410A17F0
|
||||
firmware.bin BE8534B98EDF84B1147E04B0A3D56372A877957D34BB245458B511E8E3D66EF0
|
||||
partitions.bin 0A8B5720E7B77FF11F1462458C3A509DEE79224E5279898F26D6A2E3AE0517B7
|
||||
BIN
firmware/cyd/boot_app0.bin
Normal file
BIN
firmware/cyd/boot_app0.bin
Normal file
Binary file not shown.
BIN
firmware/cyd/bootloader.bin
Normal file
BIN
firmware/cyd/bootloader.bin
Normal file
Binary file not shown.
BIN
firmware/cyd/firmware.bin
Normal file
BIN
firmware/cyd/firmware.bin
Normal file
Binary file not shown.
5
firmware/cyd/flash.bat
Normal file
5
firmware/cyd/flash.bat
Normal file
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0flash.ps1" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
98
firmware/cyd/flash.ps1
Normal file
98
firmware/cyd/flash.ps1
Normal file
@@ -0,0 +1,98 @@
|
||||
# Flash archived CYD BLE firmware (no rebuild needed)
|
||||
# Usage:
|
||||
# .\flash.ps1
|
||||
# .\flash.ps1 -Port COM5
|
||||
# .\flash.ps1 -Port COM5 -Baud 460800
|
||||
|
||||
param(
|
||||
[string]$Port = "",
|
||||
[int]$Baud = 921600
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
Set-Location $here
|
||||
|
||||
function Find-Esptool {
|
||||
$pio = Get-Command pio -ErrorAction SilentlyContinue
|
||||
if (-not $pio) {
|
||||
$pioPath = Join-Path $env:USERPROFILE "AppData\Local\Programs\Python\Python312\Scripts\pio.exe"
|
||||
if (Test-Path $pioPath) {
|
||||
$env:Path = "$(Split-Path $pioPath);$env:Path"
|
||||
$pio = Get-Command pio -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
if ($pio) {
|
||||
$pkg = Join-Path $env:USERPROFILE ".platformio\packages\tool-esptoolpy\esptool.py"
|
||||
if (Test-Path $pkg) { return @{ Kind = "py"; Path = $pkg } }
|
||||
}
|
||||
$py = Get-Command python -ErrorAction SilentlyContinue
|
||||
if ($py) {
|
||||
try {
|
||||
& python -c "import esptool" 2>$null
|
||||
if ($LASTEXITCODE -eq 0) { return @{ Kind = "mod"; Path = "esptool" } }
|
||||
} catch {}
|
||||
}
|
||||
$esp = Get-Command esptool.py -ErrorAction SilentlyContinue
|
||||
if ($esp) { return @{ Kind = "py"; Path = $esp.Source } }
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not $Port) {
|
||||
$ports = @()
|
||||
try {
|
||||
Add-Type -AssemblyName System.IO.Ports -ErrorAction SilentlyContinue | Out-Null
|
||||
$ports = [System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object
|
||||
} catch {}
|
||||
if (-not $ports -or $ports.Count -eq 0) {
|
||||
$ports = Get-CimInstance Win32_SerialPort -ErrorAction SilentlyContinue |
|
||||
Select-Object -ExpandProperty DeviceID | Sort-Object
|
||||
}
|
||||
if (-not $ports -or $ports.Count -eq 0) {
|
||||
Write-Error "No serial ports found. Plug in the CYD USB cable, then retry with -Port COMx"
|
||||
}
|
||||
if ($ports.Count -eq 1) {
|
||||
$Port = $ports[0]
|
||||
} else {
|
||||
Write-Host "Available ports: $($ports -join ', ')"
|
||||
$Port = $ports[-1]
|
||||
Write-Host "Using last port: $Port (override with -Port)"
|
||||
}
|
||||
}
|
||||
|
||||
$tool = Find-Esptool
|
||||
if (-not $tool) {
|
||||
Write-Error "esptool not found. Install PlatformIO (`pio`) or: pip install esptool"
|
||||
}
|
||||
|
||||
$needed = @("bootloader.bin", "partitions.bin", "boot_app0.bin", "firmware.bin")
|
||||
foreach ($f in $needed) {
|
||||
if (-not (Test-Path (Join-Path $here $f))) {
|
||||
Write-Error "Missing $f — run ..\export_from_build.ps1 from repo root first"
|
||||
}
|
||||
}
|
||||
|
||||
$argsCommon = @(
|
||||
"--chip", "esp32",
|
||||
"--port", $Port,
|
||||
"--baud", "$Baud",
|
||||
"--before", "default_reset",
|
||||
"--after", "hard_reset",
|
||||
"write_flash", "-z",
|
||||
"--flash_mode", "dio",
|
||||
"--flash_freq", "40m",
|
||||
"--flash_size", "4MB",
|
||||
"0x1000", "bootloader.bin",
|
||||
"0x8000", "partitions.bin",
|
||||
"0xe000", "boot_app0.bin",
|
||||
"0x10000", "firmware.bin"
|
||||
)
|
||||
|
||||
Write-Host "Flashing $here -> $Port @ $Baud"
|
||||
if ($tool.Kind -eq "mod") {
|
||||
& python -m esptool @argsCommon
|
||||
} else {
|
||||
& python $tool.Path @argsCommon
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
Write-Host "Done. Open serial monitor at 460800 if needed."
|
||||
7
firmware/cyd/meta.json
Normal file
7
firmware/cyd/meta.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"boardId": "esp32-2432s028",
|
||||
"name": "CYD BLE",
|
||||
"summary": "NimBLE Nordic UART peripheral with LVGL status UI",
|
||||
"description": "Advertise as CYD-BLE, Nordic UART Service for phone apps (nRF Connect / Serial Bluetooth Terminal). Echo, Ping, advertise control on touchscreen.",
|
||||
"tags": ["ble", "nimble", "nus", "uart", "touch"]
|
||||
}
|
||||
BIN
firmware/cyd/partitions.bin
Normal file
BIN
firmware/cyd/partitions.bin
Normal file
Binary file not shown.
57
firmware/export_from_build.ps1
Normal file
57
firmware/export_from_build.ps1
Normal file
@@ -0,0 +1,57 @@
|
||||
# Rebuild (optional) and refresh firmware/cyd from PlatformIO build output.
|
||||
# Usage:
|
||||
# .\firmware\export_from_build.ps1
|
||||
# .\firmware\export_from_build.ps1 -SkipBuild
|
||||
|
||||
param([switch]$SkipBuild)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||
Set-Location $root
|
||||
|
||||
$env:Path = "C:\Users\dsx2016\AppData\Local\Programs\Python\Python312\Scripts;" + $env:Path
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
pio run -e cyd
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
$src = Join-Path $root ".pio\build\cyd"
|
||||
$dst = Join-Path $root "firmware\cyd"
|
||||
New-Item -ItemType Directory -Force -Path $dst | Out-Null
|
||||
|
||||
foreach ($f in @("bootloader.bin", "partitions.bin", "firmware.bin")) {
|
||||
$p = Join-Path $src $f
|
||||
if (-not (Test-Path $p)) { Write-Error "Missing build artifact: $p" }
|
||||
Copy-Item $p $dst -Force
|
||||
}
|
||||
|
||||
$bootApp = Join-Path $env:USERPROFILE ".platformio\packages\framework-arduinoespressif32\tools\partitions\boot_app0.bin"
|
||||
if (-not (Test-Path $bootApp)) {
|
||||
Write-Error "boot_app0.bin not found under PlatformIO packages"
|
||||
}
|
||||
Copy-Item $bootApp (Join-Path $dst "boot_app0.bin") -Force
|
||||
|
||||
$git = ""
|
||||
try { $git = (git -C $root rev-parse --short HEAD).Trim() } catch { $git = "unknown" }
|
||||
$date = Get-Date -Format "yyyy-MM-dd"
|
||||
$hashes = Get-ChildItem (Join-Path $dst "*.bin") | Get-FileHash -Algorithm SHA256
|
||||
$lines = @(
|
||||
"name: CYD BLE (cyd)",
|
||||
"git: $git",
|
||||
"date: $date",
|
||||
"board: ESP32-2432S028R (ILI9341)",
|
||||
"features: NimBLE Nordic UART, LVGL status UI, SoftSPI touch",
|
||||
"flash_size: 4MB",
|
||||
"offsets:",
|
||||
" 0x1000 bootloader.bin",
|
||||
" 0x8000 partitions.bin",
|
||||
" 0xe000 boot_app0.bin",
|
||||
" 0x10000 firmware.bin",
|
||||
"sha256:"
|
||||
)
|
||||
foreach ($h in $hashes) {
|
||||
$lines += (" {0,-15} {1}" -f (Split-Path $h.Path -Leaf), $h.Hash)
|
||||
}
|
||||
$lines | Set-Content -Encoding utf8 (Join-Path $dst "VERSION.txt")
|
||||
Write-Host "Exported -> $dst (git $git)"
|
||||
84
include/ble_core.h
Normal file
84
include/ble_core.h
Normal file
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* 通用 BLE 外设核心(无 LVGL 依赖)
|
||||
*
|
||||
* 栈:NimBLE(比 Bluedroid 省 RAM/Flash,后续可与 WiFi 共存)
|
||||
* 服务:Nordic UART Service(NUS)— 手机端 nRF Connect / Serial Bluetooth
|
||||
* Terminal / LightBlue 等主流 App 开箱即用
|
||||
*
|
||||
* 流程:
|
||||
* 1. ble_core_begin() 初始化 NimBLE + NUS,默认开始广播
|
||||
* 2. 手机连上后写 RX 特征 → 回调 / 收包队列
|
||||
* 3. ble_core_send() 经 TX 特征 Notify 发回手机
|
||||
* 4. loop 里 ble_core_loop() 处理断线后自动重广播
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#ifndef BLE_CORE_DEVICE_NAME
|
||||
#define BLE_CORE_DEVICE_NAME "CYD-BLE"
|
||||
#endif
|
||||
|
||||
#ifndef BLE_CORE_RX_QUEUE
|
||||
#define BLE_CORE_RX_QUEUE 8
|
||||
#endif
|
||||
|
||||
#ifndef BLE_CORE_RX_MAX
|
||||
#define BLE_CORE_RX_MAX 128
|
||||
#endif
|
||||
|
||||
/** Nordic UART Service(业界事实标准) */
|
||||
#define BLE_NUS_SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
#define BLE_NUS_RX_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
#define BLE_NUS_TX_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
|
||||
enum BleCoreState : uint8_t {
|
||||
BLE_CORE_IDLE = 0,
|
||||
BLE_CORE_ADVERTISING,
|
||||
BLE_CORE_CONNECTED,
|
||||
};
|
||||
|
||||
struct BleRxPacket {
|
||||
char data[BLE_CORE_RX_MAX + 1];
|
||||
uint16_t len;
|
||||
};
|
||||
|
||||
using BleCoreStateCb = void (*)(BleCoreState state, void *user);
|
||||
using BleCoreRxCb = void (*)(const uint8_t *data, size_t len, void *user);
|
||||
|
||||
void ble_core_set_state_cb(BleCoreStateCb cb, void *user = nullptr);
|
||||
void ble_core_set_rx_cb(BleCoreRxCb cb, void *user = nullptr);
|
||||
|
||||
/** 初始化栈 + NUS;默认开始广播。返回是否成功。 */
|
||||
bool ble_core_begin(const char *device_name = BLE_CORE_DEVICE_NAME);
|
||||
|
||||
void ble_core_end();
|
||||
|
||||
BleCoreState ble_core_state();
|
||||
bool ble_core_is_connected();
|
||||
bool ble_core_is_advertising();
|
||||
|
||||
const char *ble_core_name();
|
||||
String ble_core_address();
|
||||
String ble_core_peer_address();
|
||||
|
||||
void ble_core_advertise_start();
|
||||
void ble_core_advertise_stop();
|
||||
|
||||
/** 经 NUS TX Notify 发送;未连接返回 false */
|
||||
bool ble_core_send(const uint8_t *data, size_t len);
|
||||
bool ble_core_send_str(const char *text);
|
||||
|
||||
/** 演示用:收到数据自动 echo 回去(默认开) */
|
||||
void ble_core_set_echo(bool on);
|
||||
bool ble_core_echo();
|
||||
|
||||
/**
|
||||
* 非回调路径取收包(UI 轮询友好)。有包返回 true 并填 out。
|
||||
* 与 ble_core_set_rx_cb 可同时用。
|
||||
*/
|
||||
bool ble_core_poll_rx(BleRxPacket *out);
|
||||
|
||||
/** 每 loop 调用:断线后自动重广播 */
|
||||
void ble_core_loop();
|
||||
9
include/ble_ui.h
Normal file
9
include/ble_ui.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* LVGL 屏上 BLE 状态 UI(依赖 ble_core)
|
||||
* 复用:拷贝 ble_ui.* + 在业务里 ble_ui_create() / ble_ui_tick()
|
||||
*/
|
||||
|
||||
void ble_ui_create();
|
||||
void ble_ui_tick();
|
||||
1127
include/lv_conf.h
Normal file
1127
include/lv_conf.h
Normal file
File diff suppressed because it is too large
Load Diff
16
include/pins.h
Normal file
16
include/pins.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
// CYD ESP32-2432S028R
|
||||
|
||||
#define TFT_BL_PIN 21
|
||||
|
||||
#define CYD_TOUCH_IRQ 36
|
||||
#define CYD_TOUCH_MOSI 32
|
||||
#define CYD_TOUCH_MISO 39
|
||||
#define CYD_TOUCH_CLK 25
|
||||
#define CYD_TOUCH_CS 33
|
||||
|
||||
#define SCREEN_WIDTH 240
|
||||
#define SCREEN_HEIGHT 320
|
||||
#define UI_HOR_RES 320
|
||||
#define UI_VER_RES 240
|
||||
7
include/touch.h
Normal file
7
include/touch.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <stdint.h>
|
||||
|
||||
void touch_init();
|
||||
void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data);
|
||||
57
platformio.ini
Normal file
57
platformio.ini
Normal file
@@ -0,0 +1,57 @@
|
||||
; CYD ESP32-2432S028 — BLE 基础(NimBLE + Nordic UART + LVGL 状态页)
|
||||
[env:cyd]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
monitor_speed = 460800
|
||||
upload_speed = 460800
|
||||
; LVGL+NimBLE 已近默认 1.2MB 上限;给后续 WiFi 共存留空间(约 1.9MB APP)
|
||||
board_build.partitions = min_spiffs.csv
|
||||
|
||||
lib_deps =
|
||||
bodmer/TFT_eSPI@^2.5.43
|
||||
lvgl/lvgl@9.2.2
|
||||
h2zero/NimBLE-Arduino@^2.3.2
|
||||
|
||||
build_flags =
|
||||
-DCORE_DEBUG_LEVEL=0
|
||||
-DUSER_SETUP_LOADED=1
|
||||
-DILI9341_2_DRIVER=1
|
||||
-DTFT_WIDTH=240
|
||||
-DTFT_HEIGHT=320
|
||||
-DTFT_MISO=12
|
||||
-DTFT_MOSI=13
|
||||
-DTFT_SCLK=14
|
||||
-DTFT_CS=15
|
||||
-DTFT_DC=2
|
||||
-DTFT_RST=-1
|
||||
-DTFT_BL=21
|
||||
-DTFT_BACKLIGHT_ON=HIGH
|
||||
-DTOUCH_CS=-1
|
||||
-DLOAD_GLCD=1
|
||||
-DLOAD_FONT2=1
|
||||
-DLOAD_FONT4=1
|
||||
-DLOAD_FONT6=1
|
||||
-DLOAD_FONT7=1
|
||||
-DLOAD_FONT8=1
|
||||
-DLOAD_GFXFF=1
|
||||
-DSMOOTH_FONT=1
|
||||
-DSPI_FREQUENCY=40000000
|
||||
-DSPI_READ_FREQUENCY=20000000
|
||||
-DLV_CONF_SKIP
|
||||
-DLV_COLOR_DEPTH=16
|
||||
-DLV_MEM_SIZE=49152
|
||||
-DLV_FONT_MONTSERRAT_14=1
|
||||
-DLV_FONT_MONTSERRAT_16=1
|
||||
-DLV_FONT_MONTSERRAT_18=1
|
||||
-DLV_FONT_MONTSERRAT_20=1
|
||||
-DLV_USE_TFT_ESPI=1
|
||||
-DLV_USE_FLEX=1
|
||||
-DLV_USE_BUTTON=1
|
||||
-DLV_USE_LABEL=1
|
||||
-DLV_USE_TEXTAREA=1
|
||||
-I${PROJECT_DIR}/include
|
||||
-DCONFIG_BT_NIMBLE_ROLE_CENTRAL_DISABLED
|
||||
-DCONFIG_BT_NIMBLE_ROLE_OBSERVER_DISABLED
|
||||
|
||||
extra_scripts = pre:copy_lv_conf.py
|
||||
257
src/ble_core.cpp
Normal file
257
src/ble_core.cpp
Normal file
@@ -0,0 +1,257 @@
|
||||
#include "ble_core.h"
|
||||
|
||||
#include <NimBLEDevice.h>
|
||||
#include <WiFi.h>
|
||||
#include <cstring>
|
||||
|
||||
static NimBLEServer *g_server = nullptr;
|
||||
static NimBLECharacteristic *g_tx = nullptr;
|
||||
static NimBLECharacteristic *g_rx = nullptr;
|
||||
|
||||
static BleCoreState g_state = BLE_CORE_IDLE;
|
||||
static BleCoreStateCb g_state_cb = nullptr;
|
||||
static void *g_state_user = nullptr;
|
||||
static BleCoreRxCb g_rx_cb = nullptr;
|
||||
static void *g_rx_user = nullptr;
|
||||
|
||||
static char g_name[32] = BLE_CORE_DEVICE_NAME;
|
||||
static bool g_echo = true;
|
||||
static bool g_want_adv = true;
|
||||
static bool g_connected = false;
|
||||
static char g_peer[24] = {};
|
||||
|
||||
static BleRxPacket g_rx_q[BLE_CORE_RX_QUEUE];
|
||||
static uint8_t g_rx_head = 0;
|
||||
static uint8_t g_rx_tail = 0;
|
||||
static uint8_t g_rx_count = 0;
|
||||
|
||||
static void set_state(BleCoreState s) {
|
||||
if (g_state == s) {
|
||||
return;
|
||||
}
|
||||
g_state = s;
|
||||
if (g_state_cb) {
|
||||
g_state_cb(s, g_state_user);
|
||||
}
|
||||
}
|
||||
|
||||
static void enqueue_rx(const uint8_t *data, size_t len) {
|
||||
if (!data || len == 0) {
|
||||
return;
|
||||
}
|
||||
if (len > BLE_CORE_RX_MAX) {
|
||||
len = BLE_CORE_RX_MAX;
|
||||
}
|
||||
|
||||
BleRxPacket *slot = &g_rx_q[g_rx_tail];
|
||||
memcpy(slot->data, data, len);
|
||||
slot->data[len] = '\0';
|
||||
slot->len = static_cast<uint16_t>(len);
|
||||
g_rx_tail = (g_rx_tail + 1) % BLE_CORE_RX_QUEUE;
|
||||
if (g_rx_count < BLE_CORE_RX_QUEUE) {
|
||||
g_rx_count++;
|
||||
} else {
|
||||
g_rx_head = (g_rx_head + 1) % BLE_CORE_RX_QUEUE;
|
||||
}
|
||||
|
||||
if (g_rx_cb) {
|
||||
g_rx_cb(data, len, g_rx_user);
|
||||
}
|
||||
}
|
||||
|
||||
class ServerCbs : public NimBLEServerCallbacks {
|
||||
void onConnect(NimBLEServer *server, NimBLEConnInfo &connInfo) override {
|
||||
(void)server;
|
||||
g_connected = true;
|
||||
snprintf(g_peer, sizeof(g_peer), "%s",
|
||||
connInfo.getAddress().toString().c_str());
|
||||
Serial.printf("[ble] connected peer=%s\n", g_peer);
|
||||
set_state(BLE_CORE_CONNECTED);
|
||||
}
|
||||
|
||||
void onDisconnect(NimBLEServer *server, NimBLEConnInfo &connInfo,
|
||||
int reason) override {
|
||||
(void)server;
|
||||
(void)connInfo;
|
||||
g_connected = false;
|
||||
g_peer[0] = '\0';
|
||||
Serial.printf("[ble] disconnected reason=%d\n", reason);
|
||||
if (g_want_adv) {
|
||||
set_state(BLE_CORE_ADVERTISING);
|
||||
NimBLEDevice::startAdvertising();
|
||||
} else {
|
||||
set_state(BLE_CORE_IDLE);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class RxCbs : public NimBLECharacteristicCallbacks {
|
||||
void onWrite(NimBLECharacteristic *chr, NimBLEConnInfo &connInfo) override {
|
||||
(void)connInfo;
|
||||
NimBLEAttValue val = chr->getValue();
|
||||
const size_t len = val.size();
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
const uint8_t *data = val.data();
|
||||
Serial.printf("[ble] rx %u bytes: ", (unsigned)len);
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
const char c = static_cast<char>(data[i]);
|
||||
Serial.print((c >= 32 && c < 127) ? c : '.');
|
||||
}
|
||||
Serial.println();
|
||||
|
||||
enqueue_rx(data, len);
|
||||
|
||||
if (g_echo && g_tx && g_connected) {
|
||||
g_tx->setValue(data, len);
|
||||
g_tx->notify();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static ServerCbs g_server_cbs;
|
||||
static RxCbs g_rx_cbs;
|
||||
|
||||
void ble_core_set_state_cb(BleCoreStateCb cb, void *user) {
|
||||
g_state_cb = cb;
|
||||
g_state_user = user;
|
||||
}
|
||||
|
||||
void ble_core_set_rx_cb(BleCoreRxCb cb, void *user) {
|
||||
g_rx_cb = cb;
|
||||
g_rx_user = user;
|
||||
}
|
||||
|
||||
bool ble_core_begin(const char *device_name) {
|
||||
if (g_server) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 关掉 WiFi 射频,给 BLE 腾 RAM;后续与 WiFi 共存时再改
|
||||
WiFi.mode(WIFI_OFF);
|
||||
|
||||
if (device_name && device_name[0]) {
|
||||
strncpy(g_name, device_name, sizeof(g_name) - 1);
|
||||
g_name[sizeof(g_name) - 1] = '\0';
|
||||
}
|
||||
|
||||
NimBLEDevice::init(g_name);
|
||||
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||
|
||||
g_server = NimBLEDevice::createServer();
|
||||
g_server->setCallbacks(&g_server_cbs);
|
||||
|
||||
NimBLEService *nus = g_server->createService(BLE_NUS_SERVICE_UUID);
|
||||
|
||||
g_tx = nus->createCharacteristic(BLE_NUS_TX_UUID, NIMBLE_PROPERTY::NOTIFY);
|
||||
g_rx = nus->createCharacteristic(
|
||||
BLE_NUS_RX_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR);
|
||||
g_rx->setCallbacks(&g_rx_cbs);
|
||||
|
||||
NimBLEAdvertising *adv = NimBLEDevice::getAdvertising();
|
||||
adv->setName(g_name);
|
||||
adv->addServiceUUID(BLE_NUS_SERVICE_UUID);
|
||||
adv->enableScanResponse(true);
|
||||
|
||||
g_want_adv = true;
|
||||
NimBLEDevice::startAdvertising();
|
||||
set_state(BLE_CORE_ADVERTISING);
|
||||
|
||||
Serial.printf("[ble] begin name=\"%s\" addr=%s NUS ready\n", g_name,
|
||||
NimBLEDevice::getAddress().toString().c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
void ble_core_end() {
|
||||
if (!g_server) {
|
||||
return;
|
||||
}
|
||||
g_want_adv = false;
|
||||
NimBLEDevice::stopAdvertising();
|
||||
NimBLEDevice::deinit(true);
|
||||
g_server = nullptr;
|
||||
g_tx = nullptr;
|
||||
g_rx = nullptr;
|
||||
g_connected = false;
|
||||
g_peer[0] = '\0';
|
||||
set_state(BLE_CORE_IDLE);
|
||||
}
|
||||
|
||||
BleCoreState ble_core_state() { return g_state; }
|
||||
|
||||
bool ble_core_is_connected() { return g_connected; }
|
||||
|
||||
bool ble_core_is_advertising() {
|
||||
return g_state == BLE_CORE_ADVERTISING;
|
||||
}
|
||||
|
||||
const char *ble_core_name() { return g_name; }
|
||||
|
||||
String ble_core_address() {
|
||||
if (!g_server) {
|
||||
return String();
|
||||
}
|
||||
return String(NimBLEDevice::getAddress().toString().c_str());
|
||||
}
|
||||
|
||||
String ble_core_peer_address() { return String(g_peer); }
|
||||
|
||||
void ble_core_advertise_start() {
|
||||
g_want_adv = true;
|
||||
if (g_connected) {
|
||||
return;
|
||||
}
|
||||
if (!g_server) {
|
||||
return;
|
||||
}
|
||||
NimBLEDevice::startAdvertising();
|
||||
set_state(BLE_CORE_ADVERTISING);
|
||||
Serial.println("[ble] advertising start");
|
||||
}
|
||||
|
||||
void ble_core_advertise_stop() {
|
||||
g_want_adv = false;
|
||||
if (!g_server) {
|
||||
return;
|
||||
}
|
||||
NimBLEDevice::stopAdvertising();
|
||||
if (!g_connected) {
|
||||
set_state(BLE_CORE_IDLE);
|
||||
}
|
||||
Serial.println("[ble] advertising stop");
|
||||
}
|
||||
|
||||
bool ble_core_send(const uint8_t *data, size_t len) {
|
||||
if (!g_tx || !g_connected || !data || len == 0) {
|
||||
return false;
|
||||
}
|
||||
g_tx->setValue(data, len);
|
||||
g_tx->notify();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ble_core_send_str(const char *text) {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
return ble_core_send(reinterpret_cast<const uint8_t *>(text), strlen(text));
|
||||
}
|
||||
|
||||
void ble_core_set_echo(bool on) { g_echo = on; }
|
||||
|
||||
bool ble_core_echo() { return g_echo; }
|
||||
|
||||
bool ble_core_poll_rx(BleRxPacket *out) {
|
||||
if (!out || g_rx_count == 0) {
|
||||
return false;
|
||||
}
|
||||
*out = g_rx_q[g_rx_head];
|
||||
g_rx_head = (g_rx_head + 1) % BLE_CORE_RX_QUEUE;
|
||||
g_rx_count--;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ble_core_loop() {
|
||||
// 断线重广播已在 onDisconnect 处理;此处预留共存/省电扩展点
|
||||
}
|
||||
277
src/ble_ui.cpp
Normal file
277
src/ble_ui.cpp
Normal file
@@ -0,0 +1,277 @@
|
||||
#include "ble_ui.h"
|
||||
#include "ble_core.h"
|
||||
#include "pins.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <lvgl.h>
|
||||
|
||||
static constexpr uint32_t COL_BG = 0x0A1016;
|
||||
static constexpr uint32_t COL_INK = 0xEAF0F6;
|
||||
static constexpr uint32_t COL_MUTED = 0x7A8B9C;
|
||||
static constexpr uint32_t COL_OK = 0x3DDC97;
|
||||
static constexpr uint32_t COL_WARN = 0xF0C040;
|
||||
static constexpr uint32_t COL_BAD = 0xE85D4C;
|
||||
static constexpr uint32_t COL_ACCENT = 0x4FC3F7;
|
||||
static constexpr uint32_t COL_BTN = 0x243140;
|
||||
static constexpr uint32_t COL_BTN_ACC = 0x1A4A6E;
|
||||
static constexpr uint32_t COL_BTN_WARN = 0x6B3A1E;
|
||||
|
||||
static lv_obj_t *scr = nullptr;
|
||||
static lv_obj_t *lbl_title = nullptr;
|
||||
static lv_obj_t *lbl_state = nullptr;
|
||||
static lv_obj_t *lbl_name = nullptr;
|
||||
static lv_obj_t *lbl_addr = nullptr;
|
||||
static lv_obj_t *lbl_peer = nullptr;
|
||||
static lv_obj_t *lbl_echo = nullptr;
|
||||
static lv_obj_t *lbl_log = nullptr;
|
||||
static lv_obj_t *btn_adv = nullptr;
|
||||
static lv_obj_t *lbl_adv = nullptr;
|
||||
|
||||
static BleCoreState g_last_state = BLE_CORE_IDLE;
|
||||
static bool g_last_echo = true;
|
||||
static char g_log[160] = "Waiting for phone...";
|
||||
static uint32_t g_last_refresh_ms = 0;
|
||||
|
||||
static void style_btn(lv_obj_t *btn, uint32_t bg) {
|
||||
lv_obj_set_style_radius(btn, 8, 0);
|
||||
lv_obj_set_style_bg_color(btn, lv_color_hex(bg), 0);
|
||||
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_shadow_width(btn, 0, 0);
|
||||
lv_obj_set_style_border_width(btn, 0, 0);
|
||||
lv_obj_set_style_pad_all(btn, 0, 0);
|
||||
}
|
||||
|
||||
static lv_obj_t *make_btn(lv_obj_t *parent, const char *text, uint32_t bg,
|
||||
lv_event_cb_t cb, int w, int h) {
|
||||
lv_obj_t *btn = lv_button_create(parent);
|
||||
lv_obj_set_size(btn, w, h);
|
||||
style_btn(btn, bg);
|
||||
lv_obj_t *lab = lv_label_create(btn);
|
||||
lv_label_set_text(lab, text);
|
||||
lv_obj_set_style_text_color(lab, lv_color_hex(COL_INK), 0);
|
||||
lv_obj_set_style_text_font(lab, &lv_font_montserrat_14, 0);
|
||||
lv_obj_center(lab);
|
||||
if (cb) {
|
||||
lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, nullptr);
|
||||
}
|
||||
return btn;
|
||||
}
|
||||
|
||||
static void append_log(const char *line) {
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
char next[160];
|
||||
snprintf(next, sizeof(next), "%s", line);
|
||||
// 保留最近两行:旧半 + 新
|
||||
char combined[160];
|
||||
snprintf(combined, sizeof(combined), "%s\n%s", g_log, next);
|
||||
// 若太长,只留新内容
|
||||
if (strlen(combined) >= sizeof(g_log)) {
|
||||
snprintf(g_log, sizeof(g_log), "%s", next);
|
||||
} else {
|
||||
snprintf(g_log, sizeof(g_log), "%s", combined);
|
||||
}
|
||||
if (lbl_log) {
|
||||
lv_label_set_text(lbl_log, g_log);
|
||||
}
|
||||
}
|
||||
|
||||
static void refresh_static() {
|
||||
if (lbl_name) {
|
||||
char buf[48];
|
||||
snprintf(buf, sizeof(buf), "Name %s", ble_core_name());
|
||||
lv_label_set_text(lbl_name, buf);
|
||||
}
|
||||
if (lbl_addr) {
|
||||
char buf[56];
|
||||
String a = ble_core_address();
|
||||
snprintf(buf, sizeof(buf), "Addr %s", a.length() ? a.c_str() : "-");
|
||||
lv_label_set_text(lbl_addr, buf);
|
||||
}
|
||||
}
|
||||
|
||||
static void refresh_state() {
|
||||
const BleCoreState st = ble_core_state();
|
||||
g_last_state = st;
|
||||
|
||||
uint32_t color = COL_MUTED;
|
||||
const char *text = "Idle";
|
||||
if (st == BLE_CORE_ADVERTISING) {
|
||||
color = COL_WARN;
|
||||
text = "Advertising";
|
||||
} else if (st == BLE_CORE_CONNECTED) {
|
||||
color = COL_OK;
|
||||
text = "Connected";
|
||||
}
|
||||
|
||||
if (lbl_state) {
|
||||
char buf[40];
|
||||
snprintf(buf, sizeof(buf), "State %s", text);
|
||||
lv_label_set_text(lbl_state, buf);
|
||||
lv_obj_set_style_text_color(lbl_state, lv_color_hex(color), 0);
|
||||
}
|
||||
|
||||
if (lbl_peer) {
|
||||
char buf[56];
|
||||
if (ble_core_is_connected()) {
|
||||
String p = ble_core_peer_address();
|
||||
snprintf(buf, sizeof(buf), "Peer %s", p.c_str());
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "Peer -");
|
||||
}
|
||||
lv_label_set_text(lbl_peer, buf);
|
||||
}
|
||||
|
||||
if (lbl_adv) {
|
||||
lv_label_set_text(lbl_adv,
|
||||
ble_core_is_advertising() || ble_core_is_connected()
|
||||
? "Stop Adv"
|
||||
: "Start Adv");
|
||||
}
|
||||
|
||||
if (lbl_echo) {
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "Echo %s", ble_core_echo() ? "ON" : "OFF");
|
||||
lv_label_set_text(lbl_echo, buf);
|
||||
lv_obj_set_style_text_color(
|
||||
lbl_echo, lv_color_hex(ble_core_echo() ? COL_OK : COL_MUTED), 0);
|
||||
}
|
||||
g_last_echo = ble_core_echo();
|
||||
}
|
||||
|
||||
static void on_adv(lv_event_t *e) {
|
||||
(void)e;
|
||||
if (ble_core_is_connected()) {
|
||||
// 已连接时 Stop Adv 无意义;改为提示
|
||||
append_log("Connected — disconnect on phone first");
|
||||
return;
|
||||
}
|
||||
if (ble_core_is_advertising()) {
|
||||
ble_core_advertise_stop();
|
||||
append_log("Advertising stopped");
|
||||
} else {
|
||||
ble_core_advertise_start();
|
||||
append_log("Advertising started");
|
||||
}
|
||||
refresh_state();
|
||||
}
|
||||
|
||||
static void on_echo(lv_event_t *e) {
|
||||
(void)e;
|
||||
ble_core_set_echo(!ble_core_echo());
|
||||
append_log(ble_core_echo() ? "Echo ON" : "Echo OFF");
|
||||
refresh_state();
|
||||
}
|
||||
|
||||
static void on_ping(lv_event_t *e) {
|
||||
(void)e;
|
||||
if (!ble_core_is_connected()) {
|
||||
append_log("Not connected");
|
||||
return;
|
||||
}
|
||||
if (ble_core_send_str("Hello from CYD\n")) {
|
||||
append_log("TX: Hello from CYD");
|
||||
} else {
|
||||
append_log("TX failed");
|
||||
}
|
||||
}
|
||||
|
||||
static void on_clear(lv_event_t *e) {
|
||||
(void)e;
|
||||
snprintf(g_log, sizeof(g_log), "(cleared)");
|
||||
if (lbl_log) {
|
||||
lv_label_set_text(lbl_log, g_log);
|
||||
}
|
||||
}
|
||||
|
||||
void ble_ui_create() {
|
||||
scr = lv_obj_create(nullptr);
|
||||
lv_obj_set_style_bg_color(scr, lv_color_hex(COL_BG), 0);
|
||||
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
|
||||
lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_screen_load(scr);
|
||||
|
||||
lbl_title = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_title, "CYD BLE");
|
||||
lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_ACCENT), 0);
|
||||
lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_20, 0);
|
||||
lv_obj_set_pos(lbl_title, 10, 6);
|
||||
|
||||
lbl_state = lv_label_create(scr);
|
||||
lv_obj_set_style_text_font(lbl_state, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_pos(lbl_state, 10, 34);
|
||||
|
||||
lbl_name = lv_label_create(scr);
|
||||
lv_obj_set_style_text_color(lbl_name, lv_color_hex(COL_INK), 0);
|
||||
lv_obj_set_style_text_font(lbl_name, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_pos(lbl_name, 10, 54);
|
||||
|
||||
lbl_addr = lv_label_create(scr);
|
||||
lv_obj_set_style_text_color(lbl_addr, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_set_style_text_font(lbl_addr, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_pos(lbl_addr, 10, 74);
|
||||
|
||||
lbl_peer = lv_label_create(scr);
|
||||
lv_obj_set_style_text_color(lbl_peer, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_set_style_text_font(lbl_peer, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_pos(lbl_peer, 10, 94);
|
||||
|
||||
lbl_echo = lv_label_create(scr);
|
||||
lv_obj_set_style_text_font(lbl_echo, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_pos(lbl_echo, 10, 114);
|
||||
|
||||
lv_obj_t *hint = lv_label_create(scr);
|
||||
lv_label_set_text(hint, "Phone: nRF Connect / Serial BT Terminal");
|
||||
lv_obj_set_style_text_color(hint, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_pos(hint, 10, 134);
|
||||
|
||||
lbl_log = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_log, g_log);
|
||||
lv_label_set_long_mode(lbl_log, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(lbl_log, UI_HOR_RES - 20);
|
||||
lv_obj_set_style_text_color(lbl_log, lv_color_hex(COL_INK), 0);
|
||||
lv_obj_set_style_text_font(lbl_log, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_pos(lbl_log, 10, 156);
|
||||
|
||||
const int bw = 72;
|
||||
const int bh = 36;
|
||||
const int y = UI_VER_RES - bh - 8;
|
||||
btn_adv = make_btn(scr, "Stop Adv", COL_BTN_ACC, on_adv, bw, bh);
|
||||
lbl_adv = lv_obj_get_child(btn_adv, 0);
|
||||
lv_obj_set_pos(btn_adv, 8, y);
|
||||
|
||||
lv_obj_t *btn_echo = make_btn(scr, "Echo", COL_BTN, on_echo, bw, bh);
|
||||
lv_obj_set_pos(btn_echo, 8 + bw + 6, y);
|
||||
|
||||
lv_obj_t *btn_ping = make_btn(scr, "Ping", COL_BTN_ACC, on_ping, bw, bh);
|
||||
lv_obj_set_pos(btn_ping, 8 + 2 * (bw + 6), y);
|
||||
|
||||
lv_obj_t *btn_clear = make_btn(scr, "Clear", COL_BTN_WARN, on_clear, bw, bh);
|
||||
lv_obj_set_pos(btn_clear, 8 + 3 * (bw + 6), y);
|
||||
|
||||
refresh_static();
|
||||
refresh_state();
|
||||
}
|
||||
|
||||
void ble_ui_tick() {
|
||||
BleRxPacket pkt;
|
||||
while (ble_core_poll_rx(&pkt)) {
|
||||
char line[BLE_CORE_RX_MAX + 8];
|
||||
snprintf(line, sizeof(line), "RX: %s", pkt.data);
|
||||
append_log(line);
|
||||
}
|
||||
|
||||
const uint32_t now = millis();
|
||||
if (now - g_last_refresh_ms < 300) {
|
||||
return;
|
||||
}
|
||||
g_last_refresh_ms = now;
|
||||
|
||||
if (ble_core_state() != g_last_state || ble_core_echo() != g_last_echo) {
|
||||
refresh_state();
|
||||
}
|
||||
}
|
||||
45
src/main.cpp
Normal file
45
src/main.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#include <Arduino.h>
|
||||
#include <lvgl.h>
|
||||
|
||||
#include "ble_core.h"
|
||||
#include "ble_ui.h"
|
||||
#include "pins.h"
|
||||
#include "touch.h"
|
||||
|
||||
static constexpr uint32_t DRAW_BUF_SIZE =
|
||||
(SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8));
|
||||
static uint32_t draw_buf[DRAW_BUF_SIZE / 4];
|
||||
|
||||
void setup() {
|
||||
Serial.begin(460800);
|
||||
delay(200);
|
||||
Serial.println();
|
||||
Serial.printf("BOOT_OK | LVGL %d.%d.%d | CYD BLE\n", lv_version_major(),
|
||||
lv_version_minor(), lv_version_patch());
|
||||
|
||||
pinMode(TFT_BL_PIN, OUTPUT);
|
||||
digitalWrite(TFT_BL_PIN, HIGH);
|
||||
|
||||
touch_init();
|
||||
|
||||
lv_init();
|
||||
lv_display_t *disp =
|
||||
lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf));
|
||||
lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270);
|
||||
|
||||
lv_indev_t *indev = lv_indev_create();
|
||||
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
|
||||
lv_indev_set_read_cb(indev, touch_read_cb);
|
||||
lv_indev_set_display(indev, disp);
|
||||
|
||||
ble_core_begin();
|
||||
ble_ui_create();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
ble_core_loop();
|
||||
ble_ui_tick();
|
||||
lv_timer_handler();
|
||||
lv_tick_inc(5);
|
||||
delay(5);
|
||||
}
|
||||
98
src/touch.cpp
Normal file
98
src/touch.cpp
Normal file
@@ -0,0 +1,98 @@
|
||||
#include "touch.h"
|
||||
#include "pins.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
/**
|
||||
* SoftSPI XPT2046(与 tftest 相同校准)
|
||||
* BL↔TR 主对角线镜像修正,配合 LVGL 270° 旋转。
|
||||
*/
|
||||
|
||||
static void soft_write(uint8_t data) {
|
||||
for (int i = 7; i >= 0; --i) {
|
||||
digitalWrite(CYD_TOUCH_MOSI, (data >> i) & 1);
|
||||
digitalWrite(CYD_TOUCH_CLK, HIGH);
|
||||
delayMicroseconds(2);
|
||||
digitalWrite(CYD_TOUCH_CLK, LOW);
|
||||
delayMicroseconds(2);
|
||||
}
|
||||
}
|
||||
|
||||
static uint16_t soft_read12() {
|
||||
uint16_t v = 0;
|
||||
digitalWrite(CYD_TOUCH_CLK, HIGH);
|
||||
delayMicroseconds(2);
|
||||
digitalWrite(CYD_TOUCH_CLK, LOW);
|
||||
delayMicroseconds(2);
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
digitalWrite(CYD_TOUCH_CLK, HIGH);
|
||||
delayMicroseconds(2);
|
||||
v = (uint16_t)((v << 1) | (digitalRead(CYD_TOUCH_MISO) ? 1u : 0u));
|
||||
digitalWrite(CYD_TOUCH_CLK, LOW);
|
||||
delayMicroseconds(2);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
static uint16_t xpt_read(uint8_t cmd) {
|
||||
digitalWrite(CYD_TOUCH_CS, LOW);
|
||||
soft_write(cmd);
|
||||
uint16_t v = soft_read12();
|
||||
digitalWrite(CYD_TOUCH_CS, HIGH);
|
||||
return v;
|
||||
}
|
||||
|
||||
static int16_t clampi(int32_t v, int32_t lo, int32_t hi) {
|
||||
if (v < lo) {
|
||||
return (int16_t)lo;
|
||||
}
|
||||
if (v > hi) {
|
||||
return (int16_t)hi;
|
||||
}
|
||||
return (int16_t)v;
|
||||
}
|
||||
|
||||
void touch_init() {
|
||||
pinMode(CYD_TOUCH_CS, OUTPUT);
|
||||
pinMode(CYD_TOUCH_CLK, OUTPUT);
|
||||
pinMode(CYD_TOUCH_MOSI, OUTPUT);
|
||||
pinMode(CYD_TOUCH_MISO, INPUT);
|
||||
pinMode(CYD_TOUCH_IRQ, INPUT);
|
||||
digitalWrite(CYD_TOUCH_CS, HIGH);
|
||||
digitalWrite(CYD_TOUCH_CLK, LOW);
|
||||
}
|
||||
|
||||
void touch_read_cb(lv_indev_t * /*indev*/, lv_indev_data_t *data) {
|
||||
uint16_t adc_x = xpt_read(0x90);
|
||||
uint16_t adc_y = xpt_read(0xD0);
|
||||
adc_x = (uint16_t)((adc_x + xpt_read(0x90)) / 2);
|
||||
adc_y = (uint16_t)((adc_y + xpt_read(0xD0)) / 2);
|
||||
|
||||
const bool irq = (digitalRead(CYD_TOUCH_IRQ) == LOW);
|
||||
const bool in_range =
|
||||
(adc_x > 80 && adc_x < 4000 && adc_y > 80 && adc_y < 4000);
|
||||
|
||||
if (!in_range || (!irq && (adc_x < 200 || adc_y < 200))) {
|
||||
data->state = LV_INDEV_STATE_RELEASED;
|
||||
return;
|
||||
}
|
||||
|
||||
const int16_t p_x = (int16_t)adc_x;
|
||||
const int16_t p_y = (int16_t)(4095 - adc_y);
|
||||
|
||||
int16_t x = map(p_x, 200, 3700, 1, SCREEN_WIDTH);
|
||||
int16_t y = map(p_y, 240, 3800, 1, SCREEN_HEIGHT);
|
||||
x = clampi(x, 0, SCREEN_WIDTH - 1);
|
||||
y = clampi(y, 0, SCREEN_HEIGHT - 1);
|
||||
|
||||
const int32_t old_ux = y;
|
||||
const int32_t old_uy = (SCREEN_WIDTH - 1) - x;
|
||||
int16_t ui_x = clampi((old_uy * (UI_HOR_RES - 1)) / (UI_VER_RES - 1), 0,
|
||||
UI_HOR_RES - 1);
|
||||
int16_t ui_y = clampi((old_ux * (UI_VER_RES - 1)) / (UI_HOR_RES - 1), 0,
|
||||
UI_VER_RES - 1);
|
||||
|
||||
data->point.x = (int16_t)((SCREEN_WIDTH - 1) - ui_y);
|
||||
data->point.y = ui_x;
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
}
|
||||
Reference in New Issue
Block a user