Skip to content

Commit 7decea1

Browse files
committed
feat: add WiFi provisioning to CameraStream + fix HA stability issues
Arduino Examples: - CameraStream: Add WiFi provisioning with D1 reset button and status LED - CameraStream: Update README.md and README_CN.md with provisioning docs - reTerminal E1001/E1002: Add 30s debounce for HA connection status changes to prevent frequent E-Paper refreshes on unstable WebSocket connections - reTerminal E1002: Remove Chinese characters from print messages HA Plugin: - device.py: Auto-restore subscribed entity states on device reconnection - config_flow.py: Fix device discovery and add reachability check in confirm step Library: - SeeedWiFiProvisioning: Fix provisioning page loading with chunked transfer
1 parent f329635 commit 7decea1

10 files changed

Lines changed: 684 additions & 109 deletions

File tree

arduino/SeeedHADiscovery/examples/CameraStream/CameraStream.ino

Lines changed: 309 additions & 10 deletions
Large diffs are not rendered by default.

arduino/SeeedHADiscovery/examples/CameraStream/README.md

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@ Stream video from XIAO ESP32-S3 Sense camera to Home Assistant via MJPEG. Auto-d
44

55
## Features
66

7+
- **Web-based WiFi provisioning** (captive portal)
78
- MJPEG video streaming
89
- Still image capture
910
- Web UI for camera preview
1011
- Home Assistant auto-discovery
1112
- Dual-core processing (camera on Core 0, HA on Core 1)
1213
- PSRAM support for high quality images
14+
- **WiFi reset button** (long press D1 for 6s)
1315

1416
## Hardware Requirements
1517

1618
- **XIAO ESP32-S3 Sense** with OV2640 camera module
1719
- PSRAM must be enabled
20+
- D1 (GPIO2): Reset button (optional, for WiFi reset)
1821

1922
> ⚠️ This example only works with XIAO ESP32-S3 Sense!
2023
@@ -49,22 +52,24 @@ Install manually from [GitHub](https://github.com/limengdu/SeeedHADiscovery).
4952

5053
## Quick Start
5154

52-
### 1. Configure WiFi
53-
54-
```cpp
55-
const char* WIFI_SSID = "Your_WiFi_SSID";
56-
const char* WIFI_PASSWORD = "Your_WiFi_Password";
57-
```
58-
59-
### 2. Upload
55+
### 1. Upload
6056

6157
1. Select board: **XIAO_ESP32S3**
6258
2. Enable PSRAM: **Tools****PSRAM****OPI PSRAM**
6359
3. Upload the sketch
6460

61+
### 2. WiFi Provisioning (First Boot)
62+
63+
On first boot, the device will create a WiFi hotspot:
64+
65+
1. Connect to WiFi: **XIAO_Camera_AP**
66+
2. Open browser: **http://192.168.4.1**
67+
3. Select your WiFi network and enter password
68+
4. Device will restart and connect to your WiFi
69+
6570
### 3. Access Camera
6671

67-
After upload, check Serial Monitor for URLs:
72+
After WiFi is connected, check Serial Monitor for URLs:
6873

6974
| URL | Description |
7075
|-----|-------------|
@@ -124,6 +129,8 @@ Core 1: Main Loop
124129

125130
## Pin Configuration
126131

132+
### Camera Pins
133+
127134
| Function | GPIO |
128135
|----------|------|
129136
| XCLK | 10 |
@@ -134,6 +141,23 @@ Core 1: Main Loop
134141
| HREF | 47 |
135142
| PCLK | 13 |
136143

144+
### WiFi Provisioning Pins
145+
146+
| Function | GPIO | Description |
147+
|----------|------|-------------|
148+
| Reset Button | D1 (GPIO2) | Long press 6s to reset WiFi |
149+
| Status LED | Built-in LED | Visual feedback |
150+
151+
## WiFi Reset
152+
153+
To clear saved WiFi credentials and enter provisioning mode:
154+
155+
1. **Long press D1 button for 6+ seconds**
156+
2. LED will blink rapidly when threshold is reached
157+
3. Release button to trigger reset
158+
4. Device restarts in AP mode
159+
5. Follow WiFi provisioning steps above
160+
137161
## Troubleshooting
138162

139163
### Camera init failed

arduino/SeeedHADiscovery/examples/CameraStream/README_CN.md

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@
44

55
## 功能特性
66

7+
- **网页配网**(强制门户)
78
- MJPEG 视频流
89
- 静态图片捕获
910
- 摄像头预览 Web UI
1011
- Home Assistant 自动发现
1112
- 双核处理(摄像头在核心 0,HA 在核心 1)
1213
- 支持 PSRAM 以获得高质量图像
14+
- **WiFi 重置按钮**(长按 D1 6秒)
1315

1416
## 硬件要求
1517

1618
- **XIAO ESP32-S3 Sense** 带 OV2640 摄像头模块
1719
- 必须启用 PSRAM
20+
- D1 (GPIO2):重置按钮(可选,用于 WiFi 重置)
1821

1922
> ⚠️ 本示例仅适用于 XIAO ESP32-S3 Sense!
2023
@@ -49,22 +52,24 @@
4952

5053
## 快速开始
5154

52-
### 1. 配置 WiFi
53-
54-
```cpp
55-
const char* WIFI_SSID = "你的WiFi名称";
56-
const char* WIFI_PASSWORD = "你的WiFi密码";
57-
```
58-
59-
### 2. 上传
55+
### 1. 上传
6056

6157
1. 选择开发板:**XIAO_ESP32S3**
6258
2. 启用 PSRAM:**工具****PSRAM****OPI PSRAM**
6359
3. 上传程序
6460

61+
### 2. WiFi 配网(首次启动)
62+
63+
首次启动时,设备会创建 WiFi 热点:
64+
65+
1. 连接到 WiFi:**XIAO_Camera_AP**
66+
2. 打开浏览器:**http://192.168.4.1**
67+
3. 选择你的 WiFi 网络并输入密码
68+
4. 设备将重启并连接到你的 WiFi
69+
6570
### 3. 访问摄像头
6671

67-
上传后,查看串口监视器获取 URL:
72+
WiFi 连接成功后,查看串口监视器获取 URL:
6873

6974
| URL | 说明 |
7075
|-----|------|
@@ -124,6 +129,8 @@ camera:
124129

125130
## 引脚配置
126131

132+
### 摄像头引脚
133+
127134
| 功能 | GPIO |
128135
|-----|------|
129136
| XCLK | 10 |
@@ -134,6 +141,23 @@ camera:
134141
| HREF | 47 |
135142
| PCLK | 13 |
136143

144+
### WiFi 配网引脚
145+
146+
| 功能 | GPIO | 说明 |
147+
|-----|------|------|
148+
| 重置按钮 | D1 (GPIO2) | 长按 6 秒重置 WiFi |
149+
| 状态 LED | 内置 LED | 视觉反馈 |
150+
151+
## WiFi 重置
152+
153+
清除已保存的 WiFi 凭据并进入配网模式:
154+
155+
1. **长按 D1 按钮 6 秒以上**
156+
2. 达到阈值时 LED 会快速闪烁
157+
3. 松开按钮触发重置
158+
4. 设备以 AP 模式重启
159+
5. 按照上述 WiFi 配网步骤操作
160+
137161
## 故障排除
138162

139163
### 摄像头初始化失败

arduino/SeeedHADiscovery/examples/reTerminal_E1001_HASubscribe_Display/reTerminal_E1001_HASubscribe_Display.ino

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ unsigned long configChangeTime = 0;
158158

159159
// HA connection status tracking | HA 连接状态跟踪
160160
bool lastHAConnected = false;
161+
unsigned long haStatusChangeTime = 0; // Time when HA status changed | HA 状态变化时间
162+
bool haStatusPendingRefresh = false; // Flag for pending refresh due to HA status change | HA 状态变化待刷新标志
163+
const unsigned long HA_STATUS_DEBOUNCE = 30000; // 30 seconds debounce for HA status | HA 状态防抖时间30秒
161164

162165
// WiFi provisioning mode tracking | WiFi 配网模式跟踪
163166
bool wifiProvisioningMode = false;
@@ -232,23 +235,25 @@ void resetButtonTask(void* parameter) {
232235
Serial1.println();
233236
Serial1.println("=========================================");
234237
Serial1.println(" WiFi Reset threshold reached (6s)!");
235-
Serial1.println(" WiFi 重置阈值已达到(6秒)!");
236238
Serial1.println(" Release button to reset WiFi...");
237-
Serial1.println(" 松开按钮以重置 WiFi...");
238239
Serial1.println("=========================================");
239240

240241
// Audio + Visual feedback (on Core 0, won't block main loop)
241242
// 声音 + 视觉反馈(在 Core 0,不会阻塞主循环)
242243
for (int i = 0; i < 3; i++) {
243-
tone(PIN_BUZZER, 1500, 100);
244+
tone(PIN_BUZZER, 1500);
244245
setStatusLED(true);
245246
vTaskDelay(pdMS_TO_TICKS(100));
246-
tone(PIN_BUZZER, 1000, 100);
247+
noTone(PIN_BUZZER);
248+
tone(PIN_BUZZER, 1000);
247249
setStatusLED(false);
248250
vTaskDelay(pdMS_TO_TICKS(100));
251+
noTone(PIN_BUZZER);
249252
}
250253
setStatusLED(true);
251-
tone(PIN_BUZZER, 2000, 200);
254+
tone(PIN_BUZZER, 2000);
255+
vTaskDelay(pdMS_TO_TICKS(200));
256+
noTone(PIN_BUZZER);
252257
}
253258
}
254259

@@ -263,10 +268,11 @@ void resetButtonTask(void* parameter) {
263268
Serial1.println();
264269
Serial1.println("=========================================");
265270
Serial1.println(" WiFi Reset triggered!");
266-
Serial1.println(" WiFi 重置已触发!");
267271
Serial1.println("=========================================");
268272

269-
tone(PIN_BUZZER, 800, 500);
273+
tone(PIN_BUZZER, 800);
274+
vTaskDelay(pdMS_TO_TICKS(500));
275+
noTone(PIN_BUZZER);
270276
setStatusLED(false);
271277

272278
// Set flag for main loop (WiFi operations should be on main core)
@@ -901,19 +907,14 @@ void setup() {
901907
Serial1.println();
902908
Serial1.println("============================================");
903909
Serial1.println(" WiFi Provisioning Mode Active!");
904-
Serial1.println(" WiFi 配网模式已激活!");
905910
Serial1.println("============================================");
906911
Serial1.println();
907-
Serial1.println("To configure WiFi: | 配置 WiFi:");
912+
Serial1.println("To configure WiFi:");
908913
Serial1.println(" 1. Connect to WiFi: " + String(AP_SSID));
909-
Serial1.println(" 连接到 WiFi:" + String(AP_SSID));
910914
Serial1.println(" 2. Open browser: http://192.168.4.1");
911-
Serial1.println(" 打开浏览器:http://192.168.4.1");
912915
Serial1.println(" 3. Select network and enter password");
913-
Serial1.println(" 选择网络并输入密码");
914916
Serial1.println();
915917
Serial1.println(" Long press GPIO3 (6s) to reset WiFi credentials");
916-
Serial1.println(" 长按 GPIO3(6秒)重置 WiFi 凭据");
917918
Serial1.println();
918919

919920
wifiProvisioningMode = true;
@@ -985,7 +986,6 @@ void setup() {
985986
Serial1.println();
986987
Serial1.println("WiFi Reset:");
987988
Serial1.println(" Long press GPIO3 (6s) to reset WiFi credentials");
988-
Serial1.println(" 长按 GPIO3(6秒)重置 WiFi 凭据");
989989
#endif
990990
Serial1.println();
991991
}
@@ -999,7 +999,6 @@ void loop() {
999999
if (wifiResetRequested) {
10001000
wifiResetRequested = false;
10011001
Serial1.println(" Clearing credentials and restarting...");
1002-
Serial1.println(" 正在清除凭据并重启...");
10031002
ha.clearWiFiCredentials();
10041003
Serial1.flush();
10051004
delay(500);
@@ -1012,7 +1011,6 @@ void loop() {
10121011
Serial1.println();
10131012
Serial1.println("============================================");
10141013
Serial1.println(" Entered AP Mode (WiFi Reset Triggered)!");
1015-
Serial1.println(" 已进入 AP 模式(WiFi 重置已触发)!");
10161014
Serial1.println("============================================");
10171015
Serial1.println(" Connect to AP: " + String(AP_SSID));
10181016
Serial1.println(" Then visit: http://192.168.4.1");
@@ -1040,7 +1038,6 @@ void loop() {
10401038
Serial1.println();
10411039
Serial1.println("============================================");
10421040
Serial1.println(" WiFi Connected via Provisioning!");
1043-
Serial1.println(" 通过配网连接 WiFi 成功!");
10441041
Serial1.println("============================================");
10451042
Serial1.print("IP: ");
10461043
Serial1.println(ha.getLocalIP());
@@ -1072,14 +1069,13 @@ void loop() {
10721069
// Print status periodically in provisioning mode | 配网模式下定期打印状态
10731070
static unsigned long lastProvisioningStatus = 0;
10741071
unsigned long now = millis();
1075-
if (now - lastProvisioningStatus > 10000) {
1072+
if (now - lastProvisioningStatus > 30000) {
10761073
lastProvisioningStatus = now;
10771074
Serial1.println("Status: WiFi Provisioning mode active...");
10781075
Serial1.println(" Connect to AP: " + String(AP_SSID));
10791076
Serial1.println(" Then visit: http://192.168.4.1");
10801077
}
10811078

1082-
delay(100);
10831079
return;
10841080
}
10851081

@@ -1098,19 +1094,41 @@ void loop() {
10981094
// E-Paper refresh logic | 墨水屏刷新逻辑
10991095
bool shouldRefresh = false;
11001096

1101-
// 0. HA connection status change - refresh when HA connects or disconnects
1102-
// 0. HA 连接状态变化 - 当 HA 上线或掉线时刷新
1097+
// 0. HA connection status change - with debounce to prevent flickering
1098+
// 0. HA 连接状态变化 - 带防抖防止频繁刷新
11031099
bool currentHAConnected = ha.isHAConnected();
11041100
if (initialRefreshDone && lastHAConnected != currentHAConnected) {
1105-
if (currentHAConnected) {
1106-
Serial1.println("HA connected! Refreshing display...");
1107-
} else {
1108-
Serial1.println("HA disconnected! Refreshing display...");
1101+
// Status changed, start debounce timer | 状态变化,开始防抖计时
1102+
if (!haStatusPendingRefresh) {
1103+
haStatusPendingRefresh = true;
1104+
haStatusChangeTime = now;
1105+
if (currentHAConnected) {
1106+
Serial1.println("HA connected! Waiting for stable connection...");
1107+
} else {
1108+
Serial1.println("HA disconnected! Waiting to confirm...");
1109+
}
11091110
}
1110-
shouldRefresh = true;
1111-
lastDisplayUpdate = now;
11121111
}
1113-
lastHAConnected = currentHAConnected;
1112+
1113+
// Check if debounce period passed and status is stable | 检查防抖时间是否过去且状态稳定
1114+
if (haStatusPendingRefresh && (now - haStatusChangeTime >= HA_STATUS_DEBOUNCE)) {
1115+
// Status has been stable for debounce period | 状态已稳定超过防抖时间
1116+
if (currentHAConnected != lastHAConnected) {
1117+
if (currentHAConnected) {
1118+
Serial1.println("HA connection stable! Refreshing display...");
1119+
} else {
1120+
Serial1.println("HA disconnection confirmed! Refreshing display...");
1121+
}
1122+
shouldRefresh = true;
1123+
lastDisplayUpdate = now;
1124+
lastHAConnected = currentHAConnected;
1125+
}
1126+
haStatusPendingRefresh = false;
1127+
} else if (haStatusPendingRefresh && currentHAConnected == lastHAConnected) {
1128+
// Status reverted back, cancel pending refresh | 状态恢复,取消待刷新
1129+
Serial1.println("HA status reverted, canceling refresh.");
1130+
haStatusPendingRefresh = false;
1131+
}
11141132

11151133
// 1. Initial refresh: wait for data collection period after first data
11161134
// 1. 初始刷新:收到第一批数据后等待收集期结束

0 commit comments

Comments
 (0)