Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d82b4bcb8 | ||
|
|
6ca234a205 |
No files matched your search
@@ -1,224 +1,206 @@
|
||||
# ClearESP Wind Gateway
|
||||
# ClearESP WindSensors
|
||||
|
||||
## 1. Product Overview
|
||||
## 项目简介
|
||||
|
||||
ClearESP Wind Gateway is an ESP32-C3 based industrial gateway for RS485 Modbus-RTU wind speed and wind direction sensors.
|
||||
ClearESP WindSensors 是基于 ESP32-C3 的 RS485 风速风向传感器采集网关固件,面向现场部署和批量配置使用。
|
||||
|
||||
This firmware is designed for commercial field deployment and provides:
|
||||
当前固件版本:`RC1.0.3`
|
||||
|
||||
- Guided one-by-one sensor address provisioning
|
||||
- Multi-sensor polling on a shared RS485 bus
|
||||
- Local AP maintenance access
|
||||
- Optional STA uplink to external WiFi
|
||||
- Read-only HTTP API for third-party systems
|
||||
- MQTT cloud publishing with device MAC identification
|
||||
- Persistent configuration storage across power cycles
|
||||
主要能力:
|
||||
|
||||
The current firmware targets `esp32-c3-devkitm-1` and is managed with PlatformIO.
|
||||
- 单个传感器逐个引导配置站号
|
||||
- 多传感器共线轮询
|
||||
- AP 本地维护入口
|
||||
- STA 外部 WiFi 接入
|
||||
- 只读 HTTP API
|
||||
- MQTT 云端上报
|
||||
- 断电配置保存
|
||||
|
||||
## 2. Core Capabilities
|
||||
当前硬件目标板:
|
||||
|
||||
### 2.1 Sensor Provisioning
|
||||
- `esp32-c3-devkitm-1`
|
||||
|
||||
The gateway supports guided station ID assignment for multiple sensors.
|
||||
## 通信参数
|
||||
|
||||
- Configuration uses special address `0xFE`
|
||||
- The target sensor address register is `0x0030`
|
||||
- Sensors are assigned sequentially as `1..N`
|
||||
- Address write is followed by automatic verification
|
||||
- Verification retries are built in
|
||||
- Configuration count is stored in non-volatile storage
|
||||
### RS485 / Modbus
|
||||
|
||||
Important field rule:
|
||||
- 协议:Modbus RTU
|
||||
- 波特率:`9600`
|
||||
- 数据位:`8`
|
||||
- 校验位:`None`
|
||||
- 停止位:`1`
|
||||
|
||||
- During provisioning, only one target sensor may remain connected on the bus
|
||||
- Previously configured sensors must be disconnected before assigning the next station ID
|
||||
### 当前接线
|
||||
|
||||
### 2.2 Runtime Polling
|
||||
- UART:`UART1`
|
||||
- TX:`GPIO0`
|
||||
- RX:`GPIO1`
|
||||
|
||||
In runtime mode, the gateway polls all configured sensors cyclically.
|
||||
默认假设使用自动收发方向的 TTL 转 RS485 模块。
|
||||
|
||||
Current data model:
|
||||
## 寄存器说明
|
||||
|
||||
- Wind speed: `m/s`
|
||||
- Wind level
|
||||
- Wind speed: `km/h`
|
||||
- Direction angle: `deg`
|
||||
- Direction code / direction text
|
||||
固件当前使用以下寄存器:
|
||||
|
||||
### 2.3 Networking
|
||||
- `0x0000`:风速 `m/s`
|
||||
- `0x0001`:风级
|
||||
- `0x0002`:风速 `km/h`
|
||||
- `0x0003`:风向角度
|
||||
- `0x0004`:风向方位
|
||||
- `0x0030`:设备地址
|
||||
|
||||
The firmware runs in `AP + STA` mode.
|
||||
## 功能说明
|
||||
|
||||
- AP is always retained for local maintenance
|
||||
- STA is optional and uses DHCP
|
||||
- STA reconnect is automatic after link loss
|
||||
- AP access is not affected by STA failure
|
||||
### 1. 引导配置
|
||||
|
||||
AP naming rule:
|
||||
系统支持逐个给传感器分配站号。
|
||||
|
||||
规则:
|
||||
|
||||
- 每次配置时,总线上只能保留 `1` 个待配置传感器
|
||||
- 已经配置好的设备必须先断开
|
||||
- 站号按 `1..N` 顺序分配
|
||||
|
||||
配置流程:
|
||||
|
||||
1. 进入设备 AP
|
||||
2. 打开网页中的“引导配置”
|
||||
3. 只接入 1 个待配置传感器
|
||||
4. 点击“配置下一个传感器”
|
||||
5. 等待系统自动读取、写入和验证
|
||||
6. 成功后拔掉当前传感器
|
||||
7. 接入下一个传感器继续
|
||||
8. 全部完成后点击“结束配置并进入运行”
|
||||
|
||||
说明:
|
||||
|
||||
- 某些传感器改址后不会返回标准写应答
|
||||
- 当前固件会在写入后自动读回验证
|
||||
- 只要新站号验证通过,就按“已生效,不影响使用”处理
|
||||
|
||||
### 2. 实时轮询
|
||||
|
||||
运行模式下,系统会循环轮询所有已配置传感器,并显示:
|
||||
|
||||
- 风速 `m/s`
|
||||
- 风级
|
||||
- 风速 `km/h`
|
||||
- 风向角度
|
||||
- 风向方位
|
||||
- 在线 / 离线状态
|
||||
|
||||
### 3. 网络模式
|
||||
|
||||
设备运行在 `AP + STA` 模式:
|
||||
|
||||
- AP 始终保留,便于现场维护
|
||||
- STA 可选,用于接入现场网络
|
||||
- STA 掉线后自动重连
|
||||
- STA 异常不影响 AP 本地访问
|
||||
|
||||
AP 名称格式:
|
||||
|
||||
- `ClearESP-Wind-<MAC>`
|
||||
|
||||
Example:
|
||||
### 4. Web 控制台
|
||||
|
||||
- `ClearESP-Wind-B43A455471D4`
|
||||
当前网页包含以下页面:
|
||||
|
||||
### 2.4 Web Console
|
||||
|
||||
The built-in web console includes the following sections:
|
||||
|
||||
- Real-time Data
|
||||
- Guided Setup
|
||||
- STA Settings
|
||||
- 实时数据
|
||||
- 引导配置
|
||||
- STA 设置
|
||||
- API
|
||||
- Cloud Service
|
||||
- System Status
|
||||
- 云服务
|
||||
- 系统状态
|
||||
|
||||
The interface is optimized for mobile portrait access in field use.
|
||||
系统状态页包含:
|
||||
|
||||
### 2.5 MQTT Cloud Service
|
||||
- AP / STA 信息
|
||||
- MQTT 状态
|
||||
- 固件版本
|
||||
- 已配置传感器数量
|
||||
- 恢复出厂
|
||||
- 系统重启
|
||||
|
||||
The firmware supports MQTT cloud publishing after configuration.
|
||||
### 5. 恢复出厂
|
||||
|
||||
Supported settings:
|
||||
“恢复出厂”会清空全部持久化配置,包括:
|
||||
|
||||
- MQTT server host
|
||||
- MQTT port
|
||||
- MQTT username
|
||||
- MQTT password
|
||||
- Topic prefix
|
||||
- 传感器配置
|
||||
- WiFi 配置
|
||||
- MQTT 配置
|
||||
|
||||
Behavior:
|
||||
执行后系统会自动重启。
|
||||
|
||||
- MQTT settings are stored in non-volatile memory
|
||||
- The gateway retries automatically when disconnected
|
||||
- Sensor inventory and real-time data are published
|
||||
- The device MAC is included in both topic path and payload
|
||||
### 6. MQTT 上报
|
||||
|
||||
Default topic prefix:
|
||||
配置 MQTT 后,系统会自动连接并定时发布:
|
||||
|
||||
- 传感器清单
|
||||
- 实时数据
|
||||
|
||||
默认主题前缀:
|
||||
|
||||
- `clearesp/wind`
|
||||
|
||||
Published topics:
|
||||
完整主题:
|
||||
|
||||
- `<topicPrefix>/<MAC>/sensors`
|
||||
- `<topicPrefix>/<MAC>/realtime`
|
||||
|
||||
### 2.6 Read-Only HTTP API
|
||||
### 7. OTA
|
||||
|
||||
The firmware exposes read-only HTTP endpoints for third-party systems.
|
||||
云服务页面新增 OTA 卡片,显示以下内容:
|
||||
|
||||
Available endpoints:
|
||||
- Gitea 项目地址
|
||||
- 当前版本发布页地址
|
||||
- 当前版本 bin 固件直链
|
||||
|
||||
当前 OTA 来源仓库:
|
||||
|
||||
- `http://107.173.157.41:3000/clear/wind_vt_esp32`
|
||||
|
||||
固件升级可直接使用发布页中的 bin 文件。
|
||||
|
||||
## HTTP API
|
||||
|
||||
所有接口均为只读 JSON。
|
||||
|
||||
### 状态接口
|
||||
|
||||
- `GET /api/state`
|
||||
|
||||
返回整机完整状态,包括:
|
||||
|
||||
- AP / STA 状态
|
||||
- MQTT 状态
|
||||
- 引导配置状态
|
||||
- 传感器列表
|
||||
- 固件版本
|
||||
|
||||
### 传感器接口
|
||||
|
||||
- `GET /api/ro/sensors`
|
||||
|
||||
### 网络接口
|
||||
|
||||
- `GET /api/ro/network`
|
||||
|
||||
### 系统接口
|
||||
|
||||
- `GET /api/ro/system`
|
||||
|
||||
All API responses are JSON.
|
||||
## MQTT 数据示例
|
||||
|
||||
## 3. Hardware and Bus Parameters
|
||||
### 传感器清单
|
||||
|
||||
### 3.1 Controller
|
||||
|
||||
- MCU: ESP32-C3
|
||||
- Board: `esp32-c3-devkitm-1`
|
||||
- Framework: Arduino
|
||||
- Build system: PlatformIO
|
||||
|
||||
### 3.2 RS485 / UART
|
||||
|
||||
Current wiring:
|
||||
|
||||
- UART: `UART1`
|
||||
- TX: `GPIO0`
|
||||
- RX: `GPIO1`
|
||||
|
||||
This firmware assumes an auto-direction TTL-to-RS485 transceiver.
|
||||
|
||||
### 3.3 Modbus Parameters
|
||||
|
||||
- Baud rate: `9600`
|
||||
- Data bits: `8`
|
||||
- Parity: `None`
|
||||
- Stop bits: `1`
|
||||
|
||||
Registers used by the firmware:
|
||||
|
||||
- Data start register: `0x0000`
|
||||
- Data register count: `5`
|
||||
- Device address register: `0x0030`
|
||||
|
||||
## 4. Web Workflow
|
||||
|
||||
### 4.1 Initial Access
|
||||
|
||||
1. Power on the gateway
|
||||
2. Connect to the AP `ClearESP-Wind-<MAC>`
|
||||
3. Open `http://192.168.4.1/`
|
||||
|
||||
### 4.2 Guided Sensor Setup
|
||||
|
||||
1. Disconnect all already-configured sensors
|
||||
2. Connect exactly one target sensor
|
||||
3. Open `Guided Setup`
|
||||
4. Press `Setup Next Sensor`
|
||||
5. Wait for address read, write, and verification
|
||||
6. Disconnect that sensor
|
||||
7. Connect the next target sensor
|
||||
8. Repeat until all sensors are provisioned
|
||||
9. Press `Finish And Run`
|
||||
|
||||
### 4.3 External WiFi Setup
|
||||
|
||||
1. Open `STA Settings`
|
||||
2. Enter external WiFi SSID and password
|
||||
3. Save configuration
|
||||
4. Confirm STA status and acquired IP
|
||||
|
||||
### 4.4 MQTT Setup
|
||||
|
||||
1. Open `Cloud Service`
|
||||
2. Enter MQTT server parameters
|
||||
3. Save and connect
|
||||
4. Confirm MQTT status
|
||||
5. Verify topic paths shown in the page
|
||||
|
||||
## 5. HTTP API
|
||||
|
||||
### `GET /api/state`
|
||||
|
||||
Full device state including:
|
||||
|
||||
- AP information
|
||||
- STA information
|
||||
- MQTT information
|
||||
- Sensor list
|
||||
- Guided setup state
|
||||
|
||||
### `GET /api/ro/sensors`
|
||||
|
||||
Sensor list and measurement data.
|
||||
|
||||
### `GET /api/ro/network`
|
||||
|
||||
AP and STA network state.
|
||||
|
||||
### `GET /api/ro/system`
|
||||
|
||||
System-level device information.
|
||||
|
||||
## 6. MQTT Payload Model
|
||||
|
||||
### 6.1 Sensor Inventory Topic
|
||||
|
||||
Topic:
|
||||
主题:
|
||||
|
||||
```text
|
||||
<topicPrefix>/<MAC>/sensors
|
||||
```
|
||||
|
||||
Payload example:
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -231,15 +213,15 @@ Payload example:
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Real-Time Data Topic
|
||||
### 实时数据
|
||||
|
||||
Topic:
|
||||
主题:
|
||||
|
||||
```text
|
||||
<topicPrefix>/<MAC>/realtime
|
||||
```
|
||||
|
||||
Payload example:
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -249,47 +231,41 @@ Payload example:
|
||||
{
|
||||
"id": 1,
|
||||
"online": true,
|
||||
"wind_speed_ms": 0.0,
|
||||
"wind_level": 0,
|
||||
"wind_speed_kmh": 0.0,
|
||||
"angle_deg": 206.3,
|
||||
"direction_code": 9,
|
||||
"direction_name": "SSW",
|
||||
"wind_speed_ms": 3.8,
|
||||
"wind_level": 3,
|
||||
"wind_speed_kmh": 13.68,
|
||||
"angle_deg": 45.0,
|
||||
"direction_code": 2,
|
||||
"direction_name": "NE",
|
||||
"age_ms": 180
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Build and Flash
|
||||
## 编译与烧录
|
||||
|
||||
### 7.1 Dependencies
|
||||
|
||||
Configured in `platformio.ini`:
|
||||
|
||||
- `ModbusMaster`
|
||||
- `PubSubClient`
|
||||
|
||||
### 7.2 Build
|
||||
### 编译
|
||||
|
||||
```powershell
|
||||
platformio run
|
||||
```
|
||||
|
||||
### 7.3 Upload
|
||||
### 烧录
|
||||
|
||||
```powershell
|
||||
platformio run -t upload
|
||||
```
|
||||
|
||||
## 8. Project Status
|
||||
## 当前版本更新点
|
||||
|
||||
This repository contains the current commercial firmware baseline including:
|
||||
|
||||
- AP + STA networking
|
||||
- Guided RS485 sensor provisioning
|
||||
- Real-time web dashboard
|
||||
- Read-only HTTP API
|
||||
- MQTT cloud publishing
|
||||
- Release-ready firmware artifact workflow
|
||||
`RC1.0.3` 包含以下调整:
|
||||
|
||||
- 页面标题更新为 `ClearESP WindSensors`
|
||||
- 顶部新增“数字植物实验室”标识
|
||||
- 改址成功提示改为更适合现场使用的话术
|
||||
- 系统状态页新增固件版本显示
|
||||
- 云服务页新增 OTA 卡片与固件下载地址
|
||||
- 系统状态页新增“恢复出厂”和“系统重启”
|
||||
- “清空配置并重新引导”不再误删 STA / MQTT 配置
|
||||
- 针对部分不返回标准写应答的传感器,采用写后读回验证确认站号是否生效
|
||||
+247
-24
@@ -28,6 +28,8 @@ constexpr uint32_t MODBUS_RESPONSE_TIMEOUT_MS = 500;
|
||||
constexpr uint32_t WIFI_RETRY_INTERVAL_MS = 15000;
|
||||
constexpr uint32_t WIFI_CONNECT_TIMEOUT_MS = 20000;
|
||||
constexpr bool ENABLE_BOOT_DIAGNOSTICS = false;
|
||||
constexpr char FIRMWARE_VERSION[] = "RC1.0.3";
|
||||
constexpr char OTA_PROJECT_URL[] = "http://107.173.157.41:3000/clear/wind_vt_esp32";
|
||||
constexpr uint16_t MQTT_DEFAULT_PORT = 1883;
|
||||
constexpr uint32_t MQTT_RETRY_INTERVAL_MS = 10000;
|
||||
constexpr uint32_t MQTT_PUBLISH_INTERVAL_MS = 1000;
|
||||
@@ -156,9 +158,10 @@ const char WEB_PAGE[] PROGMEM = R"HTML(
|
||||
}
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
.brand h1 {
|
||||
margin: 0;
|
||||
@@ -166,6 +169,12 @@ const char WEB_PAGE[] PROGMEM = R"HTML(
|
||||
line-height: 1;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.brand-note {
|
||||
font-size: clamp(13px, 2vw, 16px);
|
||||
color: var(--cyan2);
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
}
|
||||
.nav {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -375,6 +384,12 @@ const char WEB_PAGE[] PROGMEM = R"HTML(
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.sys-actions {
|
||||
margin-top: 14px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.api-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@@ -437,6 +452,13 @@ const char WEB_PAGE[] PROGMEM = R"HTML(
|
||||
.topbar, .panel { border-radius: 20px; }
|
||||
.topbar { padding: 12px; }
|
||||
.panel { padding: 14px; }
|
||||
.brand {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
.brand-note {
|
||||
text-align: left;
|
||||
}
|
||||
.sensor-grid { grid-template-columns: 1fr; }
|
||||
.stat-line { gap: 8px; padding: 10px; }
|
||||
.stat .label { font-size: 10px; }
|
||||
@@ -450,7 +472,8 @@ const char WEB_PAGE[] PROGMEM = R"HTML(
|
||||
<section class="topbar">
|
||||
<div class="brandline">
|
||||
<div class="brand">
|
||||
<h1>ClearESP Wind</h1>
|
||||
<h1>ClearESP WindSensors</h1>
|
||||
<div class="brand-note">数字植物实验室</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav">
|
||||
@@ -611,12 +634,23 @@ const char WEB_PAGE[] PROGMEM = R"HTML(
|
||||
<div class="label">上报主题</div>
|
||||
<code class="mono" id="mqttTopics">--</code>
|
||||
</div>
|
||||
<div class="api-card">
|
||||
<div class="label">OTA</div>
|
||||
<div class="subtext">当前固件可直接通过 Gitea 发布页中的 bin 文件进行升级。</div>
|
||||
<code class="mono" id="otaProject">--</code>
|
||||
<code class="mono" id="otaRelease">--</code>
|
||||
<code class="mono" id="otaBin">--</code>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="page-system" class="panel page">
|
||||
<h2>系统状态</h2>
|
||||
<div id="systemWrap" class="sys-grid"></div>
|
||||
<div class="sys-actions">
|
||||
<button class="action warn" onclick="factoryReset()">恢复出厂</button>
|
||||
<button class="action ghost" onclick="rebootSystem()">系统重启</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -750,6 +784,28 @@ const char WEB_PAGE[] PROGMEM = R"HTML(
|
||||
document.getElementById('systemWrap').innerHTML = [
|
||||
text('AP SSID', data.apSsid),
|
||||
text('AP IP', data.apIp),
|
||||
text('固件版本', data.firmwareVersion),
|
||||
text('STA SSID', data.staSsid || 'NA'),
|
||||
text('STA IP', data.staIp),
|
||||
text('\u5916\u90e8 WiFi \u72b6\u6001', data.staStatus),
|
||||
text('MQTT', data.mqttStatus),
|
||||
text('\u5df2\u914d\u7f6e\u4f20\u611f\u5668', String(data.configuredCount)),
|
||||
text('\u5f53\u524d\u6a21\u5f0f', data.setupComplete ? '\u8fd0\u884c\u4e2d' : '\u5f15\u5bfc\u914d\u7f6e\u4e2d'),
|
||||
text('\u4e0b\u4e00\u76ee\u6807\u7ad9\u53f7', String(data.nextTargetId))
|
||||
].join('');
|
||||
}
|
||||
|
||||
function renderOtaPanel(data) {
|
||||
document.getElementById('otaProject').textContent = `Project: ${data.otaProjectUrl}`;
|
||||
document.getElementById('otaRelease').textContent = `Release: ${data.otaReleaseUrl}`;
|
||||
document.getElementById('otaBin').textContent = `BIN: ${data.otaBinUrl}`;
|
||||
}
|
||||
|
||||
function renderSystemPanel(data) {
|
||||
document.getElementById('systemWrap').innerHTML = [
|
||||
text('AP SSID', data.apSsid),
|
||||
text('AP IP', data.apIp),
|
||||
text('\u56fa\u4ef6\u7248\u672c', data.firmwareVersion),
|
||||
text('STA SSID', data.staSsid || 'NA'),
|
||||
text('STA IP', data.staIp),
|
||||
text('\u5916\u90e8 WiFi \u72b6\u6001', data.staStatus),
|
||||
@@ -765,7 +821,8 @@ const char WEB_PAGE[] PROGMEM = R"HTML(
|
||||
renderSetup(data);
|
||||
renderWifi(data);
|
||||
renderMqtt(data);
|
||||
renderSystem(data);
|
||||
renderOtaPanel(data);
|
||||
renderSystemPanel(data);
|
||||
document.getElementById('apiBase').textContent = window.location.origin || ('http://' + data.apIp);
|
||||
}
|
||||
|
||||
@@ -884,6 +941,16 @@ const char WEB_PAGE[] PROGMEM = R"HTML(
|
||||
postAction('/api/config/reset');
|
||||
}
|
||||
|
||||
function factoryReset() {
|
||||
if (!confirm('确定恢复出厂设置吗?这将清空传感器、WiFi 和 MQTT 的全部已保存配置。')) return;
|
||||
postAction('/api/system/factory-reset');
|
||||
}
|
||||
|
||||
function rebootSystem() {
|
||||
if (!confirm('确定立即重启系统吗?')) return;
|
||||
postAction('/api/system/reboot');
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
scheduleRefresh(document.hidden ? 1200 : 100);
|
||||
});
|
||||
@@ -989,6 +1056,30 @@ const char *modbusResultText(uint8_t code) {
|
||||
}
|
||||
}
|
||||
|
||||
String modbusResultSummary(const char *label, uint8_t code) {
|
||||
return String(label) + "=" + String(code) + "(" + String(modbusResultText(code)) + ")";
|
||||
}
|
||||
|
||||
String otaReleaseTag() {
|
||||
String tag = FIRMWARE_VERSION;
|
||||
if (tag.startsWith("RC")) {
|
||||
tag.remove(0, 2);
|
||||
}
|
||||
return "v" + tag;
|
||||
}
|
||||
|
||||
String otaAssetName() {
|
||||
return "ClearESP-WindSensors-ESP32C3-" + otaReleaseTag() + ".bin";
|
||||
}
|
||||
|
||||
String otaReleaseUrl() {
|
||||
return String(OTA_PROJECT_URL) + "/releases/tag/" + otaReleaseTag();
|
||||
}
|
||||
|
||||
String otaBinUrl() {
|
||||
return otaReleaseUrl() + "/" + otaAssetName();
|
||||
}
|
||||
|
||||
const char *directionCodeToText(uint16_t code, float angle) {
|
||||
static constexpr const char *kDir8[] = {
|
||||
"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
|
||||
@@ -1106,6 +1197,35 @@ uint8_t sendSpecialWriteAddress(uint8_t newAddress, uint8_t &replyAddress) {
|
||||
return ModbusMaster::ku8MBSuccess;
|
||||
}
|
||||
|
||||
uint8_t sendWriteAddressRaw(uint8_t slaveAddress, uint8_t newAddress, uint8_t &replyAddress) {
|
||||
uint8_t request[8] = {slaveAddress, 0x06, 0x00, 0x30, 0x00, newAddress, 0x00, 0x00};
|
||||
const uint16_t requestCrc = modbusCrc16(request, 6);
|
||||
request[6] = static_cast<uint8_t>(requestCrc & 0x00FF);
|
||||
request[7] = static_cast<uint8_t>((requestCrc >> 8) & 0x00FF);
|
||||
|
||||
clearModbusRxBuffer();
|
||||
rs485Serial->write(request, sizeof(request));
|
||||
rs485Serial->flush();
|
||||
|
||||
uint8_t response[8] = {};
|
||||
if (!readModbusFrame(response, sizeof(response))) {
|
||||
return ModbusMaster::ku8MBResponseTimedOut;
|
||||
}
|
||||
|
||||
const uint16_t responseCrc = static_cast<uint16_t>(response[6]) |
|
||||
(static_cast<uint16_t>(response[7]) << 8);
|
||||
if (modbusCrc16(response, 6) != responseCrc) {
|
||||
return ModbusMaster::ku8MBInvalidCRC;
|
||||
}
|
||||
if (response[0] != slaveAddress || response[1] != 0x06 || response[2] != 0x00 ||
|
||||
response[3] != 0x30 || response[4] != 0x00 || response[5] != newAddress) {
|
||||
return ModbusMaster::ku8MBInvalidFunction;
|
||||
}
|
||||
|
||||
replyAddress = response[0];
|
||||
return ModbusMaster::ku8MBSuccess;
|
||||
}
|
||||
|
||||
void setNodeAddress(uint8_t address) {
|
||||
modbus->begin(address, *rs485Serial);
|
||||
}
|
||||
@@ -1195,29 +1315,27 @@ void clearRuntimeData() {
|
||||
}
|
||||
|
||||
void resetConfiguration() {
|
||||
preferences->clear();
|
||||
preferences->remove(PREF_COUNT);
|
||||
preferences->remove(PREF_DONE);
|
||||
clearRuntimeData();
|
||||
state.configuredCount = 0;
|
||||
state.setupComplete = false;
|
||||
state.staSsid = "";
|
||||
state.staPassword = "";
|
||||
state.staEnabled = false;
|
||||
state.staConnected = false;
|
||||
state.mqttHost = "";
|
||||
state.mqttPort = MQTT_DEFAULT_PORT;
|
||||
state.mqttUsername = "";
|
||||
state.mqttPassword = "";
|
||||
state.mqttTopicPrefix = "clearesp/wind";
|
||||
state.mqttEnabled = false;
|
||||
state.mqttConnected = false;
|
||||
state.mqttStatus = "Not configured";
|
||||
state.mqttLastAttemptMs = 0;
|
||||
state.mqttLastPublishMs = 0;
|
||||
for (uint8_t i = 0; i < MAX_SENSORS; ++i) {
|
||||
sensors[i].id = 0;
|
||||
}
|
||||
state.lastPublishedSensorCount = 0xFF;
|
||||
setStaStatus("未配置", "NA");
|
||||
setWizardText(
|
||||
"已清空全部配置。",
|
||||
"请先断开全部传感器,再只接入 1 个待配置传感器,然后点击“配置下一个传感器”。");
|
||||
"已清空传感器配置。",
|
||||
"WiFi 和 MQTT 配置已保留。请先断开全部传感器,再只接入 1 个待配置传感器,然后点击“配置下一个传感器”。");
|
||||
}
|
||||
|
||||
void resetToFactoryDefaults() {
|
||||
preferences->clear();
|
||||
clearRuntimeData();
|
||||
state = AppState{};
|
||||
initializeStateTexts();
|
||||
setStaStatus("未配置", "NA");
|
||||
setMqttStatus("Not configured");
|
||||
}
|
||||
|
||||
void disconnectSta() {
|
||||
@@ -1401,6 +1519,18 @@ bool writeAddressViaSpecial(uint8_t newAddress, uint8_t &resultCode) {
|
||||
return resultCode == modbus->ku8MBSuccess;
|
||||
}
|
||||
|
||||
bool writeAddressAtAddress(uint8_t currentAddress, uint8_t newAddress, uint8_t &resultCode) {
|
||||
setNodeAddress(currentAddress);
|
||||
resultCode = modbus->writeSingleRegister(REG_DEVICE_ADDRESS, newAddress);
|
||||
return resultCode == modbus->ku8MBSuccess;
|
||||
}
|
||||
|
||||
bool writeAddressAtAddressRaw(uint8_t currentAddress, uint8_t newAddress, uint8_t &resultCode) {
|
||||
uint8_t replyAddress = 0;
|
||||
resultCode = sendWriteAddressRaw(currentAddress, newAddress, replyAddress);
|
||||
return resultCode == modbus->ku8MBSuccess;
|
||||
}
|
||||
|
||||
bool verifySensorPresenceAtAddress(uint8_t address, uint8_t &resultCode) {
|
||||
setNodeAddress(address);
|
||||
resultCode = modbus->readHoldingRegisters(REG_DEVICE_ADDRESS, 1);
|
||||
@@ -1503,10 +1633,79 @@ bool configureNextSensor() {
|
||||
}
|
||||
|
||||
if (currentId != targetId) {
|
||||
if (!writeAddressViaSpecial(targetId, resultCode)) {
|
||||
uint8_t specialWriteCode = 0xFF;
|
||||
uint8_t rawWriteCode = 0xFF;
|
||||
uint8_t masterWriteCode = 0xFF;
|
||||
bool writeOk = writeAddressAtAddressRaw(currentId, targetId, resultCode);
|
||||
rawWriteCode = resultCode;
|
||||
if (!writeOk) {
|
||||
delay(150);
|
||||
if (verifySensorWithRetry(targetId, resultCode, attemptsUsed)) {
|
||||
sensors[state.configuredCount].id = targetId;
|
||||
sensors[state.configuredCount].online = false;
|
||||
sensors[state.configuredCount].modbusResult = 0xFF;
|
||||
sensors[state.configuredCount].lastReadMs = 0;
|
||||
state.configuredCount = targetId;
|
||||
state.setupComplete = false;
|
||||
saveConfig();
|
||||
setWizardText(
|
||||
"站号 " + String(targetId) + " 已生效,设备未返回标准写入应答。",
|
||||
"已通过读回验证确认新站号生效,不影响使用。");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!writeOk) {
|
||||
writeOk = writeAddressAtAddress(currentId, targetId, resultCode);
|
||||
masterWriteCode = resultCode;
|
||||
}
|
||||
if (!writeOk) {
|
||||
delay(150);
|
||||
if (verifySensorWithRetry(targetId, resultCode, attemptsUsed)) {
|
||||
sensors[state.configuredCount].id = targetId;
|
||||
sensors[state.configuredCount].online = false;
|
||||
sensors[state.configuredCount].modbusResult = 0xFF;
|
||||
sensors[state.configuredCount].lastReadMs = 0;
|
||||
state.configuredCount = targetId;
|
||||
state.setupComplete = false;
|
||||
saveConfig();
|
||||
setWizardText(
|
||||
"站号 " + String(targetId) + " 已生效,设备未返回标准 Modbus 应答。",
|
||||
"已通过读回验证确认新站号生效,不影响使用。");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!writeOk) {
|
||||
writeOk = writeAddressViaSpecial(targetId, resultCode);
|
||||
specialWriteCode = resultCode;
|
||||
}
|
||||
if (!writeOk) {
|
||||
delay(150);
|
||||
if (verifySensorWithRetry(targetId, resultCode, attemptsUsed)) {
|
||||
sensors[state.configuredCount].id = targetId;
|
||||
sensors[state.configuredCount].online = false;
|
||||
sensors[state.configuredCount].modbusResult = 0xFF;
|
||||
sensors[state.configuredCount].lastReadMs = 0;
|
||||
state.configuredCount = targetId;
|
||||
state.setupComplete = false;
|
||||
saveConfig();
|
||||
setWizardText(
|
||||
"站号 " + String(targetId) + " 已生效,设备未返回特殊地址应答。",
|
||||
"已通过读回验证确认新站号生效,不影响使用。");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!writeOk) {
|
||||
const String detail = modbusResultSummary("raw", rawWriteCode) + ", " +
|
||||
modbusResultSummary("master", masterWriteCode) + ", " +
|
||||
modbusResultSummary("special", specialWriteCode);
|
||||
Serial.printf(
|
||||
"CFG: write target=%u from current=%u failed, special=%u(%s), raw=%u(%s), master=%u(%s)\n",
|
||||
targetId, currentId, specialWriteCode, modbusResultText(specialWriteCode), rawWriteCode,
|
||||
modbusResultText(rawWriteCode), masterWriteCode, modbusResultText(masterWriteCode));
|
||||
setWizardText(
|
||||
"已读到当前地址 " + String(currentId) + ",但写入目标站号 " + String(targetId) + " 失败。",
|
||||
"请检查接线、供电,并确认总线上仍然只有这 1 个待配置传感器。");
|
||||
"结果: " + detail +
|
||||
"。已按厂家文档依次尝试原地址裸帧、标准 Modbus、特殊地址改写,并在每步后读回验证。");
|
||||
return false;
|
||||
}
|
||||
delay(150);
|
||||
@@ -1573,6 +1772,10 @@ String buildStateJson() {
|
||||
json += "\"mqttResolvedTopic\":\"" + jsonEscaped(mqttResolvedTopicPrefix()) + "\",";
|
||||
json += "\"mqttStatus\":\"" + jsonEscaped(state.mqttStatus) + "\",";
|
||||
json += "\"mqttHint\":\"" + jsonEscaped(mqttHintText()) + "\",";
|
||||
json += "\"firmwareVersion\":\"" + String(FIRMWARE_VERSION) + "\",";
|
||||
json += "\"otaProjectUrl\":\"" + jsonEscaped(String(OTA_PROJECT_URL)) + "\",";
|
||||
json += "\"otaReleaseUrl\":\"" + jsonEscaped(otaReleaseUrl()) + "\",";
|
||||
json += "\"otaBinUrl\":\"" + jsonEscaped(otaBinUrl()) + "\",";
|
||||
json += "\"configuredCount\":" + String(state.configuredCount) + ",";
|
||||
json += "\"setupComplete\":" + String(state.setupComplete ? "true" : "false") + ",";
|
||||
json += "\"nextTargetId\":" + String(state.configuredCount + 1) + ",";
|
||||
@@ -1660,6 +1863,10 @@ String buildSystemJson() {
|
||||
String json = "{";
|
||||
json += "\"device\":\"ClearESP-Wind\",";
|
||||
json += "\"mac\":\"" + hexMacString() + "\",";
|
||||
json += "\"firmware_version\":\"" + String(FIRMWARE_VERSION) + "\",";
|
||||
json += "\"ota_project_url\":\"" + jsonEscaped(String(OTA_PROJECT_URL)) + "\",";
|
||||
json += "\"ota_release_url\":\"" + jsonEscaped(otaReleaseUrl()) + "\",";
|
||||
json += "\"ota_bin_url\":\"" + jsonEscaped(otaBinUrl()) + "\",";
|
||||
json += "\"uptime_ms\":" + String(millis()) + ",";
|
||||
json += "\"free_heap\":" + String(ESP.getFreeHeap()) + ",";
|
||||
json += "\"configured_sensor_count\":" + String(state.configuredCount) + ",";
|
||||
@@ -1687,6 +1894,7 @@ String buildMqttInventoryPayload() {
|
||||
String buildMqttRealtimePayload() {
|
||||
String json = "{";
|
||||
json += "\"mac\":\"" + hexMacString() + "\",";
|
||||
json += "\"firmware_version\":\"" + String(FIRMWARE_VERSION) + "\",";
|
||||
json += "\"uptime_ms\":" + String(millis()) + ",";
|
||||
json += "\"sensors\":[";
|
||||
for (uint8_t i = 0; i < state.configuredCount; ++i) {
|
||||
@@ -1784,6 +1992,19 @@ void handleReset() {
|
||||
sendStateJson();
|
||||
}
|
||||
|
||||
void handleFactoryReset() {
|
||||
resetToFactoryDefaults();
|
||||
sendStateJson();
|
||||
delay(200);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
void handleReboot() {
|
||||
sendStateJson();
|
||||
delay(200);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
void handleWiFiSave() {
|
||||
const String ssid = server->arg("ssid");
|
||||
const String password = server->arg("password");
|
||||
@@ -1908,6 +2129,8 @@ void setupWebServer() {
|
||||
server->on("/api/config/finish", HTTP_POST, handleFinish);
|
||||
server->on("/api/config/resume", HTTP_POST, handleResume);
|
||||
server->on("/api/config/reset", HTTP_POST, handleReset);
|
||||
server->on("/api/system/factory-reset", HTTP_POST, handleFactoryReset);
|
||||
server->on("/api/system/reboot", HTTP_POST, handleReboot);
|
||||
server->on("/api/wifi/save", HTTP_POST, handleWiFiSave);
|
||||
server->on("/api/wifi/retry", HTTP_POST, handleWiFiRetry);
|
||||
server->on("/api/wifi/clear", HTTP_POST, handleWiFiClear);
|
||||
|
||||
Reference in new issue
Block a user