<task-notification>
<task-id>a669285611c2e4794</task-id>
<tool-use-id>call_01_W4SVsAkWKYYPD3AllArZ6173</tool-use-id>
<output-file>C:\default\AppData\Local\Temp\claude\d--PlatformIO1-Projects-Healthv2-0\49fba966-60be-4542-a6c1-b5b7e0e566bb\tasks\a669285611c2e4794.output</output-file>
<status>completed</status>
<summary>Agent "Research U8g2 Chinese font options" finished</summary>
<note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result>Here is the complete report of findings across all five areas.

---

## 1. U8g2 Library Status and Installation

**U8g2 is NOT currently installed.** The `.pio/libdeps/esp32-s3-devkitc-1/` directory contains only these libraries:

- Adafruit Unified Sensor
- ArduinoJson
- DHT sensor library
- PubSubClient
- WiFiManager

There is no `U8g2` or `U8g2@` directory, and no references to u8g2 exist anywhere in the project source. No display-related code (OLED, SSD1306, screen) exists in `src/` or `include/`.

**Correct PlatformIO library ID:** `olikraus/U8g2` (latest stable as of now is `@^2.35.15`). Add this line to the `lib_deps` section of `d:\PlatformIO1\Projects\Healthv2.0\platformio.ini`:

```
olikraus/U8g2@^2.35.15
```

---

## 2. U8g2 Fonts Supporting Chinese Characters

U8g2 provides several font families with Chinese (CJK) coverage. For your target characters -- `温度 湿度 心率 呼吸 有人 无人 跌倒 水浸 运行` -- all are within the GB2312 character set, so the compact `_gb2312` fonts are sufficient.

### GB2312 subset fonts (smaller memory footprint, ~6763 hanzi):

| Font Name | Pixel Height | Notes |
|-----------|-------------|-------|
| `u8g2_font_wqy12_t_gb2312` | 12px | Very compact, fits well on 128x64 |
| `u8g2_font_wqy12_t_gb2312a` | 12px | Larger subset of GB2312 |
| `u8g2_font_wqy12_t_gb2312b` | 12px | Complement subset of GB2312a |
| `u8g2_font_wqy13_t_gb2312` | 13px | Slightly taller |
| `u8g2_font_wqy14_t_gb2312` | 14px | Good readability on 128x64 |
| `u8g2_font_wqy15_t_gb2312` | 15px | Balanced size |
| `u8g2_font_wqy16_t_gb2312` | 16px | Largest GB2312 variant |

### Unifont Chinese subsets (16px, broader coverage, larger binary):

| Font Name | Coverage |
|-----------|----------|
| `u8g2_font_unifont_t_chinese1` | First subset of Unifont CJK |
| `u8g2_font_unifont_t_chinese2` | Second subset |
| `u8g2_font_unifont_t_chinese3` | Third subset |

### Recommended font for your use case:

For a 128x64 OLED displaying multiple data lines, **`u8g2_font_wqy12_t_gb2312`** is the best fit. At 12px height, you can fit approximately 5 text lines plus spacing on 64px vertical. The GB2312 charset covers all Chinese characters you listed (verified: these are all standard high-frequency characters within GB2312).

If you need larger, more readable text with fewer lines, **`u8g2_font_wqy14_t_gb2312`** gives 4 lines comfortably.

Usage pattern:
```cpp
u8g2.begin();
u8g2.enableUTF8Print();    // REQUIRED for Chinese text
u8g2.setFont(u8g2_font_wqy12_t_gb2312);
u8g2.drawUTF8(x, y, "温度: 25.3℃");
```

---

## 3. U8G2 Constructor for SSD1306 128x64 I2C on ESP32 with Custom Pins

The project does not yet define any I2C/OLED pins. You need to add them to `config.h`.

### Constructor with Hardware I2C (Wire) -- recommended for ESP32:

```cpp
// HW I2C: uses Wire object, with custom SDA/SCL specified
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE, /* clock=*/ SCL_PIN, /* data=*/ SDA_PIN);
```

On ESP32-S3 with ESP32 Arduino core, the default I2C pins for `Wire` are usually GPIO21 (SDA) and GPIO22 (SCL), but any GPIO pair can be passed. Since GPIO21/22 are still free in your pin map (see `config.h`: used pins are 0,4,6,7,8,9,10,11,17,18), using them is a safe choice.

Example pin assignment to add to `config.h`:
```cpp
#define OLED_SCL_PIN          21
#define OLED_SDA_PIN          22
```

### Constructor with Software I2C (SW I2C) -- fallback option:

```cpp
U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, /* clock=*/ SCL_PIN, /* data=*/ SDA_PIN, /* reset=*/ U8X8_PIN_NONE);
```

### Key constructor suffix meanings:

- `_F_` -- Full framebuffer mode (keeps a 1024-byte buffer in RAM: 128x64/8=1024)
- `_1_` -- Page buffer mode (one page at a time, 128 bytes, much less RAM)
- `_2_` -- Two-page buffer mode (256 bytes)

For ESP32-S3 with PSRAM (`-DBOARD_HAS_PSRAM` already in build flags), the `_F_` full buffer mode is the best choice for smooth Chinese text rendering.

### I2C address note:

SSD1306 modules are typically `0x3C`. U8G2 auto-detects the I2C address, but the default constructor uses `0x3C`. If yours uses `0x3D`, the constructor is:
```cpp
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE, /* clock=*/ SCL_PIN, /* data=*/ SDA_PIN);
```

(`U8G2_R0` means no rotation -- connector at top. Use `U8G2_R2` for 180-degree rotation.)

---

## 4. TelemetryData Fields Available for Display

Defined in `d:\PlatformIO1\Projects\Healthv2.0\include\config.h` lines 157-193. All fields:

| Field | Type | Description |
|-------|------|-------------|
| `saleid[12]` | char[] | Site number, e.g. "10100001" |
| `gateid[16]` | char[] | Gateway number, e.g. "10100001001" |
| `meterId[8]` | char[] | Node ID, e.g. "ENV001" |
| `meterName[16]` | char[] | Device model, e.g. "RadarDHT11" |
| `temp` | float | Temperature in Celsius |
| `humi` | float | Humidity in %RH |
| `dhtValid` | bool | DHT sensor validity |
| `presence` | bool | Person present (1=yes) |
| `motion` | bool | Movement detected (1=yes) |
| `fall` | bool | Fall detected (1=triggered) |
| `resident` | bool | Stationary resident (1=yes) |
| `heartRate` | uint8_t | Heart rate in bpm (0=invalid) |
| `breathRate` | uint8_t | Breath rate per minute (0=invalid) |
| `body` | uint8_t | Body energy/vital sign value |
| `radarValid` | bool | Radar data validity |
| `vitalValid` | bool | Heart/breath data validity |
| `rainDO` | bool | Water leak (1=normal, 0=triggered) |
| `rainValid` | bool | Water sensor validity |
| `wifiReady` | bool | WiFi/MQTT connected |
| `uptimeSec` | uint32_t | Device uptime in seconds |
| `buzzerState` | bool | Buzzer on/off (1=on) |
| `lastUpdate` | uint32_t | Last update timestamp in ms |

For an OLED display, the most relevant Chinese-labeled fields would be:
- **温度** (temp) -- from DHT11
- **湿度** (humi) -- from DHT11
- **心率** (heartRate) -- from R60ABD1 radar
- **呼吸** (breathRate) -- from R60ABD1 radar
- **有人/无人** (presence) -- from R60ABD1 radar
- **跌倒** (fall) -- from R60ABD1 radar
- **水浸** (rainDO, inverted logic) -- from water sensor
- **运行** (wifiReady + uptimeSec) -- system status

---

## 5. AlarmManager Buzzer State Query

Defined in `d:\PlatformIO1\Projects\Healthv2.0\include\alarm_manager.h` line 59:

```cpp
bool isBuzzerOn() const { return _buzzerOn; }
```

This is a direct inline accessor. No parameter needed; returns `true` when the buzzer is actively sounding.

**Also available from `TelemetryData`** -- the field `buzzerState` (in config.h line 189) is already populated by the reporting code and reflects the same state. This means you can check the buzzer either via the AlarmManager singleton directly or by reading the latest `TelemetryData.buzzerState` without calling the manager.

**Additional related method** from AlarmManager:
```cpp
bool hasActiveAlarm() const { return _activeCount > 0; }   // line 60
const char* getActiveAlarmType() const;                     // line 61
```

These can drive alarm indicator icons on the OLED display.

---

## Summary: What You Need to Do

1. **Add to `platformio.ini`** under `lib_deps`: `olikraus/U8g2@^2.35.15`
2. **Add to `include/config.h`**: `OLED_SCL_PIN 21` and `OLED_SDA_PIN 22` (both unused in your current pin map; only 0,4,6,7,8,9,10,11,17,18 are occupied)
3. **Declare in a new display module or main.cpp**: `U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, OLED_SCL_PIN, OLED_SDA_PIN);`
4. **Use font**: `u8g2_font_wqy12_t_gb2312` with `u8g2.enableUTF8Print()` for Chinese text
5. **Display data** from the existing `TelemetryData` struct fields (temp, humi, heartRate, breathRate, presence, fall, rainDO, etc.)
6. **Buzzer dismiss** via `AlarmManager::isBuzzerOn()` (or `TelemetryData.buzzerState`) -- no polling needed, just read the field</result>
<usage><subagent_tokens>25163</subagent_tokens><tool_uses>17</tool_uses><duration_ms>69543</duration_ms></usage>
</task-notification>


<task-notification>
<task-id>a7eebde983f855823</task-id>
<tool-use-id>call_00_wiJSGewBjcUTBxzRBDKV0217</tool-use-id>
<output-file>C:\default\AppData\Local\Temp\claude\d--PlatformIO1-Projects-Healthv2-0\49fba966-60be-4542-a6c1-b5b7e0e566bb\tasks\a7eebde983f855823.output</output-file>
<status>completed</status>
<summary>Agent "Explore existing patterns and GPIO" finished</summary>
<note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result>Here are my detailed findings across all 5 areas.

---

## 1. Button Handling: Long-Press Detection Logic

**File:** `d:\PlatformIO1\Projects\Healthv2.0\src\wifi_manager.cpp`

The long-press detection is implemented in the private method `_checkButtonLongPress()` at **lines 254-270**. The pattern is a simple state machine:

```
254: bool WiFiMgmt::_checkButtonLongPress() {
255:     bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);
257:     if (pressed && !_buttonPressed) {
258:         // Press-down edge: record press start time
259:         _buttonPressed = true;
260:         _buttonPressTime = millis();
261:     } else if (!pressed && _buttonPressed) {
262:         // Release edge: check elapsed time
263:         _buttonPressed = false;
264:         if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) {
265:             return true;  // Long press confirmed
266:         }
267:     }
269:     return false;
270: }
```

Key design points of this pattern:
- Pin is configured as `INPUT_PULLUP` (line 47), so pressed = `LOW`.
- It detects on the **release edge** only -- the action fires when the user releases the button after holding at least `BUTTON_LONG_PRESS_MS` (3000ms, defined at line 43 of `config.h`).
- It does NOT support short-press detection. There is no "press" callback; the method only returns `true` for long-press events.
- It only handles a single button. There is no debouncing, no callback registration, and no mechanism for distinguishing multiple button types (click vs. long-press vs. double-click).

The state variables are declared in the header at `d:\PlatformIO1\Projects\Healthv2.0\include\wifi_manager.h`:
- Line 116: `uint32_t _buttonPressTime;`
- Line 117: `bool _buttonPressed;`
- Line 43 (config.h): `#define CONFIG_BUTTON_PIN 0`
- Line 44 (config.h): `#define BUTTON_LONG_PRESS_MS 3000`

The method is called from `loop()` at line 141 of `wifi_manager.cpp`, which in turn is called from `main.cpp` loop at line 382.

**To reuse/extend this pattern:** You could generalize it into a reusable `Button` class that tracks `_buttonPressed`, `_buttonPressTime`, and `_lastReleaseTime` per pin, exposing callbacks for `onShortPress()`, `onLongPress()`, and optionally `onDoubleClick()`. The existing `_checkButtonLongPress()` returns a bool for the caller to handle the action inline; a cleaner extension would register callback function pointers (similar to how `MQTTManager` registers callbacks: `MQTTAlarmCallback`, `MQTTControlCallback`, `MQTTProvisionCallback`).

---

## 2. GPIO Pin Inventory (All Currently Used Pins)

**File:** `d:\PlatformIO1\Projects\Healthv2.0\include\config.h`, lines 19-48

| GPIO Pin | Macro | Purpose |
|---|---|---|
| **0** | `CONFIG_BUTTON_PIN` | WiFi reset button (INPUT_PULLUP, long-press 3s) |
| **4** | `DHT11_PIN` | DHT11 temperature/humidity sensor |
| **6** | `WATER_SENSOR_PIN` | Water leak sensor (digital input, LOW = water) |
| **7** | `BUZZER_PIN` | Buzzer PWM output (channel 0, 2kHz, 8-bit) |
| **8** | `LED_WIFI_PIN` | Blue LED - WiFi status indicator |
| **9** | `LED_ALARM_PIN` | Red LED - Alarm indicator |
| **10** | `RADAR_GPIO1_PIN` | Radar active/stationary indicator |
| **11** | `RADAR_GPIO2_PIN` | Radar presence/absence indicator |
| **17** | `RADAR_RX_PIN` | Radar UART RX (Serial2) |
| **18** | `RADAR_TX_PIN` | Radar UART TX (Serial2) |

**Total used pins: 10** (0, 4, 6, 7, 8, 9, 10, 11, 17, 18).

**Recommended free pins for new hardware:**

For the **OLED I2C (SDA + SCL)**, the safest and most conventional choice on ESP32-S3 is:
- **GPIO 21 (SDA)** and **GPIO 22 (SCL)** -- these are rarely conflicted and are the Arduino-ecosystem default for I2C on ESP32 boards. Alternatively, GPIO 1/2 or GPIO 41/42 could work, but 21/22 is the most portable choice.

For the **3 new buttons**, plenty of pins are free. Good candidates that are easy to access and avoid flash/PSRAM conflicts:
- **GPIO 1, 2, 3** (adjacent, easy to wire)
- Or **GPIO 12, 13, 14** (also adjacent)
- Or **GPIO 5, 15, 16**

Note: GPIOs 19 and 20 are used by USB on many ESP32-S3 dev boards, so avoid those for general I/O unless you know your board does not use them. GPIOs 33-37 are typically safe. GPIOs 26-32 can have ADC conflicts but are fine for digital button input.

---

## 3. PlatformIO.ini Library Dependencies

**File:** `d:\PlatformIO1\Projects\Healthv2.0\platformio.ini`, lines 27-39

Current `lib_deps`:
```ini
lib_deps =
    ; WiFi 配网 (自带 captive portal)
    tzapu/WiFiManager@^2.0.17

    ; MQTT 客户端
    knolleary/PubSubClient@^2.8

    ; JSON 解析 (更省内存)
    bblanchon/ArduinoJson@^7.1.0

    ; DHT11 温湿度传感器
    adafruit/DHT sensor library@^1.4.6
    adafruit/Adafruit Unified Sensor@^1.1.14
```

Other notable build settings:
- Board: `esp32-s3-devkitc-1` (line 5)
- Framework: `arduino` (line 7)
- Optimization: `-Os` (line 21)
- Filesystem: `littlefs` (line 15)
- Flash: `qio_opi`, 8MB partition (lines 14,16-17)

**To add U8g2**, you would add to the `lib_deps` section:
```ini
    ; OLED 显示屏 (SSD1306 128x64 I2C)
    olikraus/U8g2@^2.35.30
```

U8g2 is the recommended library for SSD1306 on ESP32 -- it supports I2C, hardware SPI, and software SPI, has built-in font handling, and is well-maintained. The PlatformIO library ID is `olikraus/U8g2`. Version `2.35.30` is a recent stable release; you can also omit the version constraint and use `olikraus/U8g2` for the latest.

---

## 4. Main Loop Structure

**File:** `d:\PlatformIO1\Projects\Healthv2.0\src\main.cpp`, lines 378-428

The `loop()` function runs on Core 1 and operates as a **fast, non-blocking super-loop** with a `delay(10)` at line 427. This means each iteration completes in roughly 10ms plus execution time. Here is the execution order:

```
378: void loop() {
379:     uint32_t now = millis();
382:     bool wifiOk = g_wifi.loop();            // 1. WiFi + button check
385:     if (wifiOk) {
386:         g_mqtt.loop();                       // 2. MQTT (if WiFi up)
389:         static bool wasDisconnected...       // 2b. Re-sync time on reconnect
401:     }
402:     g_sensor.read();                         // 3. Poll DHT11 + water sensor
405:     RadarRawData radar = g_radar.getData();  // 4. Get radar data
406:     checkLocalAlarms(radar);                 // 4b. Local alarm check
409:     g_alarm.loop();                          // 5. Buzzer/LED updates
412:     if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {   // 6. 1Hz reporting
415:         TelemetryData tele = buildTelemetry(radar);
416:         g_mqtt.publishAllPoints(tele, g_sequence++);
420:         printDebug(tele);                    // (every 5s, line 281)
421:     }
427:     delay(10);
428: }
```

**Where to add display update logic:**

The best location is inside the **1-second report block** (lines 412-421). This block runs at exactly 1Hz (controlled by `REPORT_INTERVAL_MS` = 1000 at config.h line 107). A 1Hz refresh rate is perfectly adequate for an OLED status display showing sensor readings, WiFi/MQTT status, and alarm state.

Specifically, I recommend adding display update after line 420 (after `buildTelemetry` and `printDebug`) because at that point you have the fully populated `TelemetryData` struct `tele` containing all sensor values, radar data, WiFi status, uptime, and alarm state -- everything you would want to display.

If you want a faster refresh for button-driven menu navigation or screen transitions, you could break the display update out of the 1-second block and call it every loop iteration (with its own millis-based throttling, e.g., every 100ms), passing `radar` and a reference to `g_sensor` for live data.

The `buildTelemetry()` function at lines 185-222 constructs the complete data struct from all subsystems, making it a natural single source of truth for display content.

---

## 5. Display-Related Code: None Found

There is **zero existing display, OLED, SSD1306, U8g2, LCD, or screen-related code** anywhere in the project source files. The grep search across all `*.cpp`, `*.h`, and `*.ini` files in the project only returned hits inside the WiFiManager library's HTML template CSS strings (e.g., `display:inline-block` in `wm_strings_zh.h`), which are unrelated.

All project source files are:
```
src/main.cpp
src/wifi_manager.cpp
src/radar_manager.cpp
src/mqtt_manager.cpp
src/sensor_manager.cpp
src/alarm_manager.cpp
include/config.h
include/wifi_manager.h
include/radar_manager.h
include/radar_protocol.h
include/mqtt_manager.h
include/sensor_manager.h
include/alarm_manager.h
include/wm_strings_zh.h
```

You will be adding display support completely from scratch. There are no existing abstractions to conflict with or modify -- you just need a new `DisplayManager` class (e.g., `src/display_manager.cpp` + `include/display_manager.h`) initialized after `g_alarm.begin()` in `setup()` and updated in the main `loop()`.</result>
<usage><subagent_tokens>38094</subagent_tokens><tool_uses>16</tool_uses><duration_ms>80611</duration_ms></usage>
</task-notification>


Good. Now I have all the info I need. Let me check if the agents have reported back yet, and then design the plan.

Actually, I have enough information now. Let me design the plan:

1. **GPIO allocation**:
   - OLED I2C: SDA=12, SCL=13 (free pins)
   - Button 1 (配网, long press 3s): GPIO 14
   - Button 2 (报警解除, short press): GPIO 15
   - Button 3 (翻页, short press): GPIO 16

2. **Files to create**:
   - `include/display_manager.h` / `src/display_manager.cpp` - OLED display manager
   - `include/button_manager.h` / `src/button_manager.cpp` - 3-button manager

3. **Files to modify**:
   - `include/config.h` - Add new GPIO definitions
   - `src/main.cpp` - Integrate display + buttons into main loop
   - `platformio.ini` - Add U8g2 library dependency

4. **Display pages**:
   - Page 0: 环境数据 (温湿度 + WiFi)
   - Page 1: 生命体征 (心率 + 呼吸 + 体动)
   - Page 2: 安全状态 (有人/运动/跌倒/驻留/水浸)

5. **Button behavior**:
   - 配网按钮: Long press 3s → startConfigPortal(). Reuses existing button pattern.
   - 报警解除: Short press → clearAllLocal() + buzzer off
   - 翻页: Short press → cycle display pages (0→1→2→0)

Let me now write the plan.


我已经掌握了所有需要的信息，直接写方案。


Write

# OLED 显示屏 + 三按钮交互方案

## 背景
在 V1.0 硬件基础上，增加 OLED 屏幕（SSD1306 128x64 I2C）和三个物理按钮，实现离线状态下的本地信息查看与交互控制。前期调试/交互阶段使用，无需手机/云端即可查看设备状态。

## GPIO 分配

| GPIO | 功能 | 说明 |
|---|---|---|
| 12 | OLED SDA | I2C 数据线 |
| 13 | OLED SCL | I2C 时钟线 |
| 14 | 按钮1: 配网 | 长按3秒→启动配网模式 (INPUT_PULLUP) |
| 15 | 按钮2: 解除警报 | 短按→关闭蜂鸣器+清除本地报警 (INPUT_PULLUP) |
| 16 | 按钮3: 翻页 | 短按→切换显示页 (共3页循环) (INPUT_PULLUP) |

## 新增文件

### 1. `include/display_manager.h` + `src/display_manager.cpp`
- 封装 U8g2 库 (SSD1306 128x64 HW I2C)
- 3 个显示页面，用 `_pageIndex` (0-2) 控制
- `begin()` 初始化 OLED，显示启动画面
- `update(const TelemetryData&, bool wifiOk, uint32_t uptime, bool buzzerOn)` 刷新当前页
- `nextPage()` 切换到下一页
- 中文渲染使用 `u8g2_font_wqy12_t_gb2312` 字体

**页面布局 (128x64)**：

```
Page 0 ─ 环境数据          Page 1 ─ 生命体征          Page 2 ─ 安全状态
┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐
│ 🏥 健康监测  V1.0 │  │ 🫀 生命体征      │  │ 🛡 安全状态      │
│                  │  │                  │  │                  │
│ 温度:  26.5°C    │  │ 心率:  72 bpm    │  │ 有人: ✅ 运动:✅ │
│ 湿度:  60.0%     │  │ 呼吸:  16 次/分  │  │ 跌倒: ❌        │
│ WiFi:  ✅ 已连接 │  │ 体动:  正常      │  │ 驻留: ❌        │
│ 运行:  02:30:15  │  │                  │  │ 水浸: ✅ 正常   │
│ [蜂鸣器图标]     │  │ [页脚: 1/3]      │  │ [页脚: 2/3]      │
└──────────────────┘  └──────────────────┘  └──────────────────┘
```

### 2. `include/button_manager.h` + `src/button_manager.cpp`
- 管理 3 个按钮的去抖和长按检测
- 复用 `wifi_manager.cpp` 中 `_checkButtonLongPress()` 的逻辑模式
- 对外暴露回调:
  - `onProvisionLongPress` — 配网长按回调
  - `onAlarmDismiss` — 报警解除回调
  - `onPageSwitch` — 翻页回调
- 去抖参数: 50ms 消抖, 3000ms 长按阈值
- `loop()` 在主循环中每 10ms 轮询

## 修改文件

### 3. `include/config.h`
新增约 8 行引脚定义：
```cpp
#define OLED_SDA_PIN          12
#define OLED_SCL_PIN          13
#define BTN_PROVISION_PIN     14   // 配网按钮 (长按)
#define BTN_ALARM_DISMISS_PIN 15   // 解除报警按钮
#define BTN_PAGE_SWITCH_PIN   16   // 翻页按钮
#define BTN_DEBOUNCE_MS       50
#define BTN_LONG_PRESS_MS     3000
```

### 4. `platformio.ini`
`lib_deps` 新增一行: `olikraus/U8g2@^2.35`

### 5. `src/main.cpp`
- `#include "display_manager.h"` 和 `#include "button_manager.h"`
- 新增全局对象: `DisplayManager g_display;` `ButtonManager g_buttons;`
- `setup()` 中:
  - `g_display.begin()` (在雷达之后, WiFi 之前)
  - `g_buttons.begin()`
  - 注册按钮回调 (配网→startConfigPortal, 报警→clearAllLocal, 翻页→nextPage)
- `loop()` 中:
  - 主循环末尾调用 `g_buttons.loop()`
  - `printDebug()` 中同步调用 `g_display.update(tele, wifiOk, uptime, buzzerOn)`

## 实现细节

### 显示刷新策略
- 不与遥测上报频率绑定
- 跟随 `printDebug()` 的 5 秒周期刷新 (足够人眼阅读)
- 翻页时立即刷新

### U8g2 I2C 构造函数
```cpp
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, OLED_SCL_PIN, OLED_SDA_PIN);
```

### 按钮防抖逻辑
复刻 `WiFiMgmt::_checkButtonLongPress()` 模式:
- `lastState` / `currentState` 跟踪电平变化
- 按下时记录 `pressTime`，松开时检查是否超 `LONG_PRESS_MS`
- 短按: 松开时 < 3 秒 → 触发展示翻页/报警解除
- 长按: 松开时 ≥ 3 秒 → 触发配网

## 验证方式
1. 编译通过无报错 (`pio run`)
2. 上电后 OLED 显示 "🏥 健康监测 V1.0" 启动画面
3. 按翻页按钮 → 页面在 0/1/2 之间循环切换
4. 触发本地报警 → 蜂鸣器响 → 按解除按钮 → 蜂鸣器停
5. 长按配网按钮 3 秒 → 进入配网模式 (AP: HealthMonitor-Setup)


File created successfully at: C:\Users\杨志佳\.claude\plans\magical-orbiting-cookie.md (file state is current in your context — no need to Read it back)

# OLED 显示屏 + 三按钮交互方案

## 背景
在 V1.0 硬件基础上，增加 OLED 屏幕（SSD1306 128x64 I2C）和三个物理按钮，实现离线状态下的本地信息查看与交互控制。前期调试/交互阶段使用，无需手机/云端即可查看设备状态。

## GPIO 分配

| GPIO | 功能 | 说明 |
|---|---|---|
| 12 | OLED SDA | I2C 数据线 |
| 13 | OLED SCL | I2C 时钟线 |
| 14 | 按钮1: 配网 | 长按3秒→启动配网模式 (INPUT_PULLUP) |
| 15 | 按钮2: 解除警报 | 短按→关闭蜂鸣器+清除本地报警 (INPUT_PULLUP) |
| 16 | 按钮3: 翻页 | 短按→切换显示页 (共3页循环) (INPUT_PULLUP) |

## 新增文件

### 1. `include/display_manager.h` + `src/display_manager.cpp`
- 封装 U8g2 库 (SSD1306 128x64 HW I2C)
- 3 个显示页面，用 `_pageIndex` (0-2) 控制
- `begin()` 初始化 OLED，显示启动画面
- `update(const TelemetryData&, bool wifiOk, uint32_t uptime, bool buzzerOn)` 刷新当前页
- `nextPage()` 切换到下一页
- 中文渲染使用 `u8g2_font_wqy12_t_gb2312` 字体

**页面布局 (128x64)**：

```
Page 0 ─ 环境数据          Page 1 ─ 生命体征          Page 2 ─ 安全状态
┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐
│ 🏥 健康监测  V1.0 │  │ 🫀 生命体征      │  │ 🛡 安全状态      │
│                  │  │                  │  │                  │
│ 温度:  26.5°C    │  │ 心率:  72 bpm    │  │ 有人: ✅ 运动:✅ │
│ 湿度:  60.0%     │  │ 呼吸:  16 次/分  │  │ 跌倒: ❌        │
│ WiFi:  ✅ 已连接 │  │ 体动:  正常      │  │ 驻留: ❌        │
│ 运行:  02:30:15  │  │                  │  │ 水浸: ✅ 正常   │
│ [蜂鸣器图标]     │  │ [页脚: 1/3]      │  │ [页脚: 2/3]      │
└──────────────────┘  └──────────────────┘  └──────────────────┘
```

### 2. `include/button_manager.h` + `src/button_manager.cpp`
- 管理 3 个按钮的去抖和长按检测
- 复用 `wifi_manager.cpp` 中 `_checkButtonLongPress()` 的逻辑模式
- 对外暴露回调:
  - `onProvisionLongPress` — 配网长按回调
  - `onAlarmDismiss` — 报警解除回调
  - `onPageSwitch` — 翻页回调
- 去抖参数: 50ms 消抖, 3000ms 长按阈值
- `loop()` 在主循环中每 10ms 轮询

## 修改文件

### 3. `include/config.h`
新增约 8 行引脚定义：
```cpp
#define OLED_SDA_PIN          12
#define OLED_SCL_PIN          13
#define BTN_PROVISION_PIN     14   // 配网按钮 (长按)
#define BTN_ALARM_DISMISS_PIN 15   // 解除报警按钮
#define BTN_PAGE_SWITCH_PIN   16   // 翻页按钮
#define BTN_DEBOUNCE_MS       50
#define BTN_LONG_PRESS_MS     3000
```

### 4. `platformio.ini`
`lib_deps` 新增一行: `olikraus/U8g2@^2.35`

### 5. `src/main.cpp`
- `#include "display_manager.h"` 和 `#include "button_manager.h"`
- 新增全局对象: `DisplayManager g_display;` `ButtonManager g_buttons;`
- `setup()` 中:
  - `g_display.begin()` (在雷达之后, WiFi 之前)
  - `g_buttons.begin()`
  - 注册按钮回调 (配网→startConfigPortal, 报警→clearAllLocal, 翻页→nextPage)
- `loop()` 中:
  - 主循环末尾调用 `g_buttons.loop()`
  - `printDebug()` 中同步调用 `g_display.update(tele, wifiOk, uptime, buzzerOn)`

## 实现细节

### 显示刷新策略
- 不与遥测上报频率绑定
- 跟随 `printDebug()` 的 5 秒周期刷新 (足够人眼阅读)
- 翻页时立即刷新

### U8g2 I2C 构造函数
```cpp
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, OLED_SCL_PIN, OLED_SDA_PIN);
```

### 按钮防抖逻辑
复刻 `WiFiMgmt::_checkButtonLongPress()` 模式:
- `lastState` / `currentState` 跟踪电平变化
- 按下时记录 `pressTime`，松开时检查是否超 `LONG_PRESS_MS`
- 短按: 松开时 < 3 秒 → 触发展示翻页/报警解除
- 长按: 松开时 ≥ 3 秒 → 触发配网

## 验证方式
1. 编译通过无报错 (`pio run`)
2. 上电后 OLED 显示 "🏥 健康监测 V1.0" 启动画面
3. 按翻页按钮 → 页面在 0/1/2 之间循环切换
4. 触发本地报警 → 蜂鸣器响 → 按解除按钮 → 蜂鸣器停
5. 长按配网按钮 3 秒 → 进入配网模式 (AP: HealthMonitor-Setup)


Prompt:
<task-notification>
<task-id>a669285611c2e4794</task-id>
<tool-use-id>call_01_W4SVsAkWKYYPD3AllArZ6173</tool-use-id>
<output-file>C:\default\AppData\Local\Temp\claude\d--PlatformIO1-Projects-Healthv2-0\49fba966-60be-4542-a6c1-b5b7e0e566bb\tasks\a669285611c2e4794.output</output-file>
<status>completed</status>
<summary>Agent "Research U8g2 Chinese font options" finished</summary>
<note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result>Here is the complete report of findings across all five areas.

---

## 1. U8g2 Library Status and Installation

**U8g2 is NOT currently installed.** The `.pio/libdeps/esp32-s3-devkitc-1/` directory contains only these libraries:

- Adafruit Unified Sensor
- ArduinoJson
- DHT sensor library
- PubSubClient
- WiFiManager

There is no `U8g2` or `U8g2@` directory, and no references to u8g2 exist anywhere in the project source. No display-related code (OLED, SSD1306, screen) exists in `src/` or `include/`.

**Correct PlatformIO library ID:** `olikraus/U8g2` (latest stable as of now is `@^2.35.15`). Add this line to the `lib_deps` section of `d:\PlatformIO1\Projects\Healthv2.0\platformio.ini`:

```
olikraus/U8g2@^2.35.15
```

---

## 2. U8g2 Fonts Supporting Chinese Characters

U8g2 provides several font families with Chinese (CJK) coverage. For your target characters -- `温度 湿度 心率 呼吸 有人 无人 跌倒 水浸 运行` -- all are within the GB2312 character set, so the compact `_gb2312` fonts are sufficient.

### GB2312 subset fonts (smaller memory footprint, ~6763 hanzi):

| Font Name | Pixel Height | Notes |
|-----------|-------------|-------|
| `u8g2_font_wqy12_t_gb2312` | 12px | Very compact, fits well on 128x64 |
| `u8g2_font_wqy12_t_gb2312a` | 12px | Larger subset of GB2312 |
| `u8g2_font_wqy12_t_gb2312b` | 12px | Complement subset of GB2312a |
| `u8g2_font_wqy13_t_gb2312` | 13px | Slightly taller |
| `u8g2_font_wqy14_t_gb2312` | 14px | Good readability on 128x64 |
| `u8g2_font_wqy15_t_gb2312` | 15px | Balanced size |
| `u8g2_font_wqy16_t_gb2312` | 16px | Largest GB2312 variant |

### Unifont Chinese subsets (16px, broader coverage, larger binary):

| Font Name | Coverage |
|-----------|----------|
| `u8g2_font_unifont_t_chinese1` | First subset of Unifont CJK |
| `u8g2_font_unifont_t_chinese2` | Second subset |
| `u8g2_font_unifont_t_chinese3` | Third subset |

### Recommended font for your use case:

For a 128x64 OLED displaying multiple data lines, **`u8g2_font_wqy12_t_gb2312`** is the best fit. At 12px height, you can fit approximately 5 text lines plus spacing on 64px vertical. The GB2312 charset covers all Chinese characters you listed (verified: these are all standard high-frequency characters within GB2312).

If you need larger, more readable text with fewer lines, **`u8g2_font_wqy14_t_gb2312`** gives 4 lines comfortably.

Usage pattern:
```cpp
u8g2.begin();
u8g2.enableUTF8Print();    // REQUIRED for Chinese text
u8g2.setFont(u8g2_font_wqy12_t_gb2312);
u8g2.drawUTF8(x, y, "温度: 25.3℃");
```

---

## 3. U8G2 Constructor for SSD1306 128x64 I2C on ESP32 with Custom Pins

The project does not yet define any I2C/OLED pins. You need to add them to `config.h`.

### Constructor with Hardware I2C (Wire) -- recommended for ESP32:

```cpp
// HW I2C: uses Wire object, with custom SDA/SCL specified
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE, /* clock=*/ SCL_PIN, /* data=*/ SDA_PIN);
```

On ESP32-S3 with ESP32 Arduino core, the default I2C pins for `Wire` are usually GPIO21 (SDA) and GPIO22 (SCL), but any GPIO pair can be passed. Since GPIO21/22 are still free in your pin map (see `config.h`: used pins are 0,4,6,7,8,9,10,11,17,18), using them is a safe choice.

Example pin assignment to add to `config.h`:
```cpp
#define OLED_SCL_PIN          21
#define OLED_SDA_PIN          22
```

### Constructor with Software I2C (SW I2C) -- fallback option:

```cpp
U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, /* clock=*/ SCL_PIN, /* data=*/ SDA_PIN, /* reset=*/ U8X8_PIN_NONE);
```

### Key constructor suffix meanings:

- `_F_` -- Full framebuffer mode (keeps a 1024-byte buffer in RAM: 128x64/8=1024)
- `_1_` -- Page buffer mode (one page at a time, 128 bytes, much less RAM)
- `_2_` -- Two-page buffer mode (256 bytes)

For ESP32-S3 with PSRAM (`-DBOARD_HAS_PSRAM` already in build flags), the `_F_` full buffer mode is the best choice for smooth Chinese text rendering.

### I2C address note:

SSD1306 modules are typically `0x3C`. U8G2 auto-detects the I2C address, but the default constructor uses `0x3C`. If yours uses `0x3D`, the constructor is:
```cpp
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE, /* clock=*/ SCL_PIN, /* data=*/ SDA_PIN);
```

(`U8G2_R0` means no rotation -- connector at top. Use `U8G2_R2` for 180-degree rotation.)

---

## 4. TelemetryData Fields Available for Display

Defined in `d:\PlatformIO1\Projects\Healthv2.0\include\config.h` lines 157-193. All fields:

| Field | Type | Description |
|-------|------|-------------|
| `saleid[12]` | char[] | Site number, e.g. "10100001" |
| `gateid[16]` | char[] | Gateway number, e.g. "10100001001" |
| `meterId[8]` | char[] | Node ID, e.g. "ENV001" |
| `meterName[16]` | char[] | Device model, e.g. "RadarDHT11" |
| `temp` | float | Temperature in Celsius |
| `humi` | float | Humidity in %RH |
| `dhtValid` | bool | DHT sensor validity |
| `presence` | bool | Person present (1=yes) |
| `motion` | bool | Movement detected (1=yes) |
| `fall` | bool | Fall detected (1=triggered) |
| `resident` | bool | Stationary resident (1=yes) |
| `heartRate` | uint8_t | Heart rate in bpm (0=invalid) |
| `breathRate` | uint8_t | Breath rate per minute (0=invalid) |
| `body` | uint8_t | Body energy/vital sign value |
| `radarValid` | bool | Radar data validity |
| `vitalValid` | bool | Heart/breath data validity |
| `rainDO` | bool | Water leak (1=normal, 0=triggered) |
| `rainValid` | bool | Water sensor validity |
| `wifiReady` | bool | WiFi/MQTT connected |
| `uptimeSec` | uint32_t | Device uptime in seconds |
| `buzzerState` | bool | Buzzer on/off (1=on) |
| `lastUpdate` | uint32_t | Last update timestamp in ms |

For an OLED display, the most relevant Chinese-labeled fields would be:
- **温度** (temp) -- from DHT11
- **湿度** (humi) -- from DHT11
- **心率** (heartRate) -- from R60ABD1 radar
- **呼吸** (breathRate) -- from R60ABD1 radar
- **有人/无人** (presence) -- from R60ABD1 radar
- **跌倒** (fall) -- from R60ABD1 radar
- **水浸** (rainDO, inverted logic) -- from water sensor
- **运行** (wifiReady + uptimeSec) -- system status

---

## 5. AlarmManager Buzzer State Query

Defined in `d:\PlatformIO1\Projects\Healthv2.0\include\alarm_manager.h` line 59:

```cpp
bool isBuzzerOn() const { return _buzzerOn; }
```

This is a direct inline accessor. No parameter needed; returns `true` when the buzzer is actively sounding.

**Also available from `TelemetryData`** -- the field `buzzerState` (in config.h line 189) is already populated by the reporting code and reflects the same state. This means you can check the buzzer either via the AlarmManager singleton directly or by reading the latest `TelemetryData.buzzerState` without calling the manager.

**Additional related method** from AlarmManager:
```cpp
bool hasActiveAlarm() const { return _activeCount > 0; }   // line 60
const char* getActiveAlarmType() const;                     // line 61
```

These can drive alarm indicator icons on the OLED display.

---

## Summary: What You Need to Do

1. **Add to `platformio.ini`** under `lib_deps`: `olikraus/U8g2@^2.35.15`
2. **Add to `include/config.h`**: `OLED_SCL_PIN 21` and `OLED_SDA_PIN 22` (both unused in your current pin map; only 0,4,6,7,8,9,10,11,17,18 are occupied)
3. **Declare in a new display module or main.cpp**: `U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, OLED_SCL_PIN, OLED_SDA_PIN);`
4. **Use font**: `u8g2_font_wqy12_t_gb2312` with `u8g2.enableUTF8Print()` for Chinese text
5. **Display data** from the existing `TelemetryData` struct fields (temp, humi, heartRate, breathRate, presence, fall, rainDO, etc.)
6. **Buzzer dismiss** via `AlarmManager::isBuzzerOn()` (or `TelemetryData.buzzerState`) -- no polling needed, just read the field</result>
<usage><subagent_tokens>25163</subagent_tokens><tool_uses>17</tool_uses><duration_ms>69543</duration_ms></usage>
</task-notification>


Prompt:
<task-notification>
<task-id>a7eebde983f855823</task-id>
<tool-use-id>call_00_wiJSGewBjcUTBxzRBDKV0217</tool-use-id>
<output-file>C:\default\AppData\Local\Temp\claude\d--PlatformIO1-Projects-Healthv2-0\49fba966-60be-4542-a6c1-b5b7e0e566bb\tasks\a7eebde983f855823.output</output-file>
<status>completed</status>
<summary>Agent "Explore existing patterns and GPIO" finished</summary>
<note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result>Here are my detailed findings across all 5 areas.

---

## 1. Button Handling: Long-Press Detection Logic

**File:** `d:\PlatformIO1\Projects\Healthv2.0\src\wifi_manager.cpp`

The long-press detection is implemented in the private method `_checkButtonLongPress()` at **lines 254-270**. The pattern is a simple state machine:

```
254: bool WiFiMgmt::_checkButtonLongPress() {
255:     bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);
257:     if (pressed && !_buttonPressed) {
258:         // Press-down edge: record press start time
259:         _buttonPressed = true;
260:         _buttonPressTime = millis();
261:     } else if (!pressed && _buttonPressed) {
262:         // Release edge: check elapsed time
263:         _buttonPressed = false;
264:         if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) {
265:             return true;  // Long press confirmed
266:         }
267:     }
269:     return false;
270: }
```

Key design points of this pattern:
- Pin is configured as `INPUT_PULLUP` (line 47), so pressed = `LOW`.
- It detects on the **release edge** only -- the action fires when the user releases the button after holding at least `BUTTON_LONG_PRESS_MS` (3000ms, defined at line 43 of `config.h`).
- It does NOT support short-press detection. There is no "press" callback; the method only returns `true` for long-press events.
- It only handles a single button. There is no debouncing, no callback registration, and no mechanism for distinguishing multiple button types (click vs. long-press vs. double-click).

The state variables are declared in the header at `d:\PlatformIO1\Projects\Healthv2.0\include\wifi_manager.h`:
- Line 116: `uint32_t _buttonPressTime;`
- Line 117: `bool _buttonPressed;`
- Line 43 (config.h): `#define CONFIG_BUTTON_PIN 0`
- Line 44 (config.h): `#define BUTTON_LONG_PRESS_MS 3000`

The method is called from `loop()` at line 141 of `wifi_manager.cpp`, which in turn is called from `main.cpp` loop at line 382.

**To reuse/extend this pattern:** You could generalize it into a reusable `Button` class that tracks `_buttonPressed`, `_buttonPressTime`, and `_lastReleaseTime` per pin, exposing callbacks for `onShortPress()`, `onLongPress()`, and optionally `onDoubleClick()`. The existing `_checkButtonLongPress()` returns a bool for the caller to handle the action inline; a cleaner extension would register callback function pointers (similar to how `MQTTManager` registers callbacks: `MQTTAlarmCallback`, `MQTTControlCallback`, `MQTTProvisionCallback`).

---

## 2. GPIO Pin Inventory (All Currently Used Pins)

**File:** `d:\PlatformIO1\Projects\Healthv2.0\include\config.h`, lines 19-48

| GPIO Pin | Macro | Purpose |
|---|---|---|
| **0** | `CONFIG_BUTTON_PIN` | WiFi reset button (INPUT_PULLUP, long-press 3s) |
| **4** | `DHT11_PIN` | DHT11 temperature/humidity sensor |
| **6** | `WATER_SENSOR_PIN` | Water leak sensor (digital input, LOW = water) |
| **7** | `BUZZER_PIN` | Buzzer PWM output (channel 0, 2kHz, 8-bit) |
| **8** | `LED_WIFI_PIN` | Blue LED - WiFi status indicator |
| **9** | `LED_ALARM_PIN` | Red LED - Alarm indicator |
| **10** | `RADAR_GPIO1_PIN` | Radar active/stationary indicator |
| **11** | `RADAR_GPIO2_PIN` | Radar presence/absence indicator |
| **17** | `RADAR_RX_PIN` | Radar UART RX (Serial2) |
| **18** | `RADAR_TX_PIN` | Radar UART TX (Serial2) |

**Total used pins: 10** (0, 4, 6, 7, 8, 9, 10, 11, 17, 18).

**Recommended free pins for new hardware:**

For the **OLED I2C (SDA + SCL)**, the safest and most conventional choice on ESP32-S3 is:
- **GPIO 21 (SDA)** and **GPIO 22 (SCL)** -- these are rarely conflicted and are the Arduino-ecosystem default for I2C on ESP32 boards. Alternatively, GPIO 1/2 or GPIO 41/42 could work, but 21/22 is the most portable choice.

For the **3 new buttons**, plenty of pins are free. Good candidates that are easy to access and avoid flash/PSRAM conflicts:
- **GPIO 1, 2, 3** (adjacent, easy to wire)
- Or **GPIO 12, 13, 14** (also adjacent)
- Or **GPIO 5, 15, 16**

Note: GPIOs 19 and 20 are used by USB on many ESP32-S3 dev boards, so avoid those for general I/O unless you know your board does not use them. GPIOs 33-37 are typically safe. GPIOs 26-32 can have ADC conflicts but are fine for digital button input.

---

## 3. PlatformIO.ini Library Dependencies

**File:** `d:\PlatformIO1\Projects\Healthv2.0\platformio.ini`, lines 27-39

Current `lib_deps`:
```ini
lib_deps =
    ; WiFi 配网 (自带 captive portal)
    tzapu/WiFiManager@^2.0.17

    ; MQTT 客户端
    knolleary/PubSubClient@^2.8

    ; JSON 解析 (更省内存)
    bblanchon/ArduinoJson@^7.1.0

    ; DHT11 温湿度传感器
    adafruit/DHT sensor library@^1.4.6
    adafruit/Adafruit Unified Sensor@^1.1.14
```

Other notable build settings:
- Board: `esp32-s3-devkitc-1` (line 5)
- Framework: `arduino` (line 7)
- Optimization: `-Os` (line 21)
- Filesystem: `littlefs` (line 15)
- Flash: `qio_opi`, 8MB partition (lines 14,16-17)

**To add U8g2**, you would add to the `lib_deps` section:
```ini
    ; OLED 显示屏 (SSD1306 128x64 I2C)
    olikraus/U8g2@^2.35.30
```

U8g2 is the recommended library for SSD1306 on ESP32 -- it supports I2C, hardware SPI, and software SPI, has built-in font handling, and is well-maintained. The PlatformIO library ID is `olikraus/U8g2`. Version `2.35.30` is a recent stable release; you can also omit the version constraint and use `olikraus/U8g2` for the latest.

---

## 4. Main Loop Structure

**File:** `d:\PlatformIO1\Projects\Healthv2.0\src\main.cpp`, lines 378-428

The `loop()` function runs on Core 1 and operates as a **fast, non-blocking super-loop** with a `delay(10)` at line 427. This means each iteration completes in roughly 10ms plus execution time. Here is the execution order:

```
378: void loop() {
379:     uint32_t now = millis();
382:     bool wifiOk = g_wifi.loop();            // 1. WiFi + button check
385:     if (wifiOk) {
386:         g_mqtt.loop();                       // 2. MQTT (if WiFi up)
389:         static bool wasDisconnected...       // 2b. Re-sync time on reconnect
401:     }
402:     g_sensor.read();                         // 3. Poll DHT11 + water sensor
405:     RadarRawData radar = g_radar.getData();  // 4. Get radar data
406:     checkLocalAlarms(radar);                 // 4b. Local alarm check
409:     g_alarm.loop();                          // 5. Buzzer/LED updates
412:     if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {   // 6. 1Hz reporting
415:         TelemetryData tele = buildTelemetry(radar);
416:         g_mqtt.publishAllPoints(tele, g_sequence++);
420:         printDebug(tele);                    // (every 5s, line 281)
421:     }
427:     delay(10);
428: }
```

**Where to add display update logic:**

The best location is inside the **1-second report block** (lines 412-421). This block runs at exactly 1Hz (controlled by `REPORT_INTERVAL_MS` = 1000 at config.h line 107). A 1Hz refresh rate is perfectly adequate for an OLED status display showing sensor readings, WiFi/MQTT status, and alarm state.

Specifically, I recommend adding display update after line 420 (after `buildTelemetry` and `printDebug`) because at that point you have the fully populated `TelemetryData` struct `tele` containing all sensor values, radar data, WiFi status, uptime, and alarm state -- everything you would want to display.

If you want a faster refresh for button-driven menu navigation or screen transitions, you could break the display update out of the 1-second block and call it every loop iteration (with its own millis-based throttling, e.g., every 100ms), passing `radar` and a reference to `g_sensor` for live data.

The `buildTelemetry()` function at lines 185-222 constructs the complete data struct from all subsystems, making it a natural single source of truth for display content.

---

## 5. Display-Related Code: None Found

There is **zero existing display, OLED, SSD1306, U8g2, LCD, or screen-related code** anywhere in the project source files. The grep search across all `*.cpp`, `*.h`, and `*.ini` files in the project only returned hits inside the WiFiManager library's HTML template CSS strings (e.g., `display:inline-block` in `wm_strings_zh.h`), which are unrelated.

All project source files are:
```
src/main.cpp
src/wifi_manager.cpp
src/radar_manager.cpp
src/mqtt_manager.cpp
src/sensor_manager.cpp
src/alarm_manager.cpp
include/config.h
include/wifi_manager.h
include/radar_manager.h
include/radar_protocol.h
include/mqtt_manager.h
include/sensor_manager.h
include/alarm_manager.h
include/wm_strings_zh.h
```

You will be adding display support completely from scratch. There are no existing abstractions to conflict with or modify -- you just need a new `DisplayManager` class (e.g., `src/display_manager.cpp` + `include/display_manager.h`) initialized after `g_alarm.begin()` in `setup()` and updated in the main `loop()`.</result>
<usage><subagent_tokens>38094</subagent_tokens><tool_uses>16</tool_uses><duration_ms>80611</duration_ms></usage>
</task-notification>


还有就是增加oled屏幕，增加三个按钮，一个是长按配网，一个是警报解除，一个是显示屏翻页，显示屏显示温湿度，心率啥的，前期交互用。


Both agents have completed. Let me summarize the key findings and update the plan:

1. **GPIO**: Used pins are 0,4,6,7,8,9,10,11,17,18. The agents recommend GPIO 21(SDA)/22(SCL) for OLED I2C, and 1,2,3 or 12,13,14 or 5,15,16 for buttons.

2. **U8g2 library**: Not installed. Need to add `olikraus/U8g2@^2.35.30` to platformio.ini. Constructor: `U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, SCL_PIN, SDA_PIN)`. Font: `u8g2_font_wqy12_t_gb2312` with `enableUTF8Print()`.

3. **Button pattern**: From wifi_manager.cpp, the long-press detection is a simple edge-detection pattern. Can extend for short press + long press on multiple pins.

4. **Loop structure**: Best place for display update is inside the 1Hz reporting block (after `buildTelemetry()`).

Now let me update my plan with the corrected GPIO pins. The agents recommend GPIO 21/22 for OLED (the standard Arduino I2C pins), and I'll use GPIO 14/15/16 for buttons (they're adjacent and unused).

Wait actually, I used GPIO 12/13 for OLED in my plan. The agent recommends 21/22. Let me use 21/22 since they're the standard I2C pins and definitely available. For buttons, I'll use 14/15/16 since they're adjacent and definitely free.

Let me update the plan.


两个代理人探索完毕。更新方案中的 GPIO 分配：


Edit

| GPIO | 功能 | 说明 |
|---|---|---|
| 12 | OLED SDA | I2C 数据线 |
| 13 | OLED SCL | I2C 时钟线 |
| 14 | 按钮1: 配网 | 长按3秒→启动配网模式 (INPUT_PULLUP) |
| 15 | 按钮2: 解除警报 | 短按→关闭蜂鸣器+清除本地报警 (INPUT_PULLUP) |
| 16 | 按钮3: 翻页 | 短按→切换显示页 (共3页循环) (INPUT_PULLUP) |

| GPIO | 功能 | 说明 |
|---|---|---|
| 21 | OLED SDA | I2C 数据线 (ESP32 Arduino 默认 I2C) |
| 22 | OLED SCL | I2C 时钟线 |
| 14 | 按钮1: 配网 | 长按3秒→启动配网模式 (INPUT_PULLUP) |
| 15 | 按钮2: 解除警报 | 短按→关闭蜂鸣器+清除本地报警 (INPUT_PULLUP) |
| 16 | 按钮3: 翻页 | 短按→切换显示页 (共3页循环) (INPUT_PULLUP) |


The file C:\Users\杨志佳\.claude\plans\magical-orbiting-cookie.md has been updated successfully. (file state is current in your context — no need to Read it back)

| GPIO | 功能 | 说明 |
|---|---|---|
| 21 | OLED SDA | I2C 数据线 (ESP32 Arduino 默认 I2C) |
| 22 | OLED SCL | I2C 时钟线 |
| 14 | 按钮1: 配网 | 长按3秒→启动配网模式 (INPUT_PULLUP) |
| 15 | 按钮2: 解除警报 | 短按→关闭蜂鸣器+清除本地报警 (INPUT_PULLUP) |
| 16 | 按钮3: 翻页 | 短按→切换显示页 (共3页循环) (INPUT_PULLUP) |

# OLED 显示屏 + 三按钮交互方案

## 背景
在 V1.0 硬件基础上，增加 OLED 屏幕（SSD1306 128x64 I2C）和三个物理按钮，实现离线状态下的本地信息查看与交互控制。前期调试/交互阶段使用，无需手机/云端即可查看设备状态。

## GPIO 分配

| GPIO | 功能 | 说明 |
|---|---|---|
| 12 | OLED SDA | I2C 数据线 |
| 13 | OLED SCL | I2C 时钟线 |
| 14 | 按钮1: 配网 | 长按3秒→启动配网模式 (INPUT_PULLUP) |
| 15 | 按钮2: 解除警报 | 短按→关闭蜂鸣器+清除本地报警 (INPUT_PULLUP) |
| 16 | 按钮3: 翻页 | 短按→切换显示页 (共3页循环) (INPUT_PULLUP) |

## 新增文件

### 1. `include/display_manager.h` + `src/display_manager.cpp`
- 封装 U8g2 库 (SSD1306 128x64 HW I2C)
- 3 个显示页面，用 `_pageIndex` (0-2) 控制
- `begin()` 初始化 OLED，显示启动画面
- `update(const TelemetryData&, bool wifiOk, uint32_t uptime, bool buzzerOn)` 刷新当前页
- `nextPage()` 切换到下一页
- 中文渲染使用 `u8g2_font_wqy12_t_gb2312` 字体

**页面布局 (128x64)**：

```
Page 0 ─ 环境数据          Page 1 ─ 生命体征          Page 2 ─ 安全状态
┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐
│ 🏥 健康监测  V1.0 │  │ 🫀 生命体征      │  │ 🛡 安全状态      │
│                  │  │                  │  │                  │
│ 温度:  26.5°C    │  │ 心率:  72 bpm    │  │ 有人: ✅ 运动:✅ │
│ 湿度:  60.0%     │  │ 呼吸:  16 次/分  │  │ 跌倒: ❌        │
│ WiFi:  ✅ 已连接 │  │ 体动:  正常      │  │ 驻留: ❌        │
│ 运行:  02:30:15  │  │                  │  │ 水浸: ✅ 正常   │
│ [蜂鸣器图标]     │  │ [页脚: 1/3]      │  │ [页脚: 2/3]      │
└──────────────────┘  └──────────────────┘  └──────────────────┘
```

### 2. `include/button_manager.h` + `src/button_manager.cpp`
- 管理 3 个按钮的去抖和长按检测
- 复用 `wifi_manager.cpp` 中 `_checkButtonLongPress()` 的逻辑模式
- 对外暴露回调:
  - `onProvisionLongPress` — 配网长按回调
  - `onAlarmDismiss` — 报警解除回调
  - `onPageSwitch` — 翻页回调
- 去抖参数: 50ms 消抖, 3000ms 长按阈值
- `loop()` 在主循环中每 10ms 轮询

## 修改文件

### 3. `include/config.h`
新增约 8 行引脚定义：
```cpp
#define OLED_SDA_PIN          12
#define OLED_SCL_PIN          13
#define BTN_PROVISION_PIN     14   // 配网按钮 (长按)
#define BTN_ALARM_DISMISS_PIN 15   // 解除报警按钮
#define BTN_PAGE_SWITCH_PIN   16   // 翻页按钮
#define BTN_DEBOUNCE_MS       50
#define BTN_LONG_PRESS_MS     3000
```

### 4. `platformio.ini`
`lib_deps` 新增一行: `olikraus/U8g2@^2.35`

### 5. `src/main.cpp`
- `#include "display_manager.h"` 和 `#include "button_manager.h"`
- 新增全局对象: `DisplayManager g_display;` `ButtonManager g_buttons;`
- `setup()` 中:
  - `g_display.begin()` (在雷达之后, WiFi 之前)
  - `g_buttons.begin()`
  - 注册按钮回调 (配网→startConfigPortal, 报警→clearAllLocal, 翻页→nextPage)
- `loop()` 中:
  - 主循环末尾调用 `g_buttons.loop()`
  - `printDebug()` 中同步调用 `g_display.update(tele, wifiOk, uptime, buzzerOn)`

## 实现细节

### 显示刷新策略
- 不与遥测上报频率绑定
- 跟随 `printDebug()` 的 5 秒周期刷新 (足够人眼阅读)
- 翻页时立即刷新

### U8g2 I2C 构造函数
```cpp
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, OLED_SCL_PIN, OLED_SDA_PIN);
```

### 按钮防抖逻辑
复刻 `WiFiMgmt::_checkButtonLongPress()` 模式:
- `lastState` / `currentState` 跟踪电平变化
- 按下时记录 `pressTime`，松开时检查是否超 `LONG_PRESS_MS`
- 短按: 松开时 < 3 秒 → 触发展示翻页/报警解除
- 长按: 松开时 ≥ 3 秒 → 触发配网

## 验证方式
1. 编译通过无报错 (`pio run`)
2. 上电后 OLED 显示 "🏥 健康监测 V1.0" 启动画面
3. 按翻页按钮 → 页面在 0/1/2 之间循环切换
4. 触发本地报警 → 蜂鸣器响 → 按解除按钮 → 蜂鸣器停
5. 长按配网按钮 3 秒 → 进入配网模式 (AP: HealthMonitor-Setup)

+| 21 | OLED SDA | I2C 数据线 (ESP32 Arduino 默认 I2C) |


```cpp
#define OLED_SDA_PIN          12
#define OLED_SCL_PIN          13

```cpp
#define OLED_SDA_PIN          21
#define OLED_SCL_PIN          22


```cpp
#define OLED_SDA_PIN          21
#define OLED_SCL_PIN          22

# OLED 显示屏 + 三按钮交互方案

## 背景
在 V1.0 硬件基础上，增加 OLED 屏幕（SSD1306 128x64 I2C）和三个物理按钮，实现离线状态下的本地信息查看与交互控制。前期调试/交互阶段使用，无需手机/云端即可查看设备状态。

## GPIO 分配

| GPIO | 功能 | 说明 |
|---|---|---|
| 21 | OLED SDA | I2C 数据线 (ESP32 Arduino 默认 I2C) |
| 22 | OLED SCL | I2C 时钟线 |
| 14 | 按钮1: 配网 | 长按3秒→启动配网模式 (INPUT_PULLUP) |
| 15 | 按钮2: 解除警报 | 短按→关闭蜂鸣器+清除本地报警 (INPUT_PULLUP) |
| 16 | 按钮3: 翻页 | 短按→切换显示页 (共3页循环) (INPUT_PULLUP) |

## 新增文件

### 1. `include/display_manager.h` + `src/display_manager.cpp`
- 封装 U8g2 库 (SSD1306 128x64 HW I2C)
- 3 个显示页面，用 `_pageIndex` (0-2) 控制
- `begin()` 初始化 OLED，显示启动画面
- `update(const TelemetryData&, bool wifiOk, uint32_t uptime, bool buzzerOn)` 刷新当前页
- `nextPage()` 切换到下一页
- 中文渲染使用 `u8g2_font_wqy12_t_gb2312` 字体

**页面布局 (128x64)**：

```
Page 0 ─ 环境数据          Page 1 ─ 生命体征          Page 2 ─ 安全状态
┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐
│ 🏥 健康监测  V1.0 │  │ 🫀 生命体征      │  │ 🛡 安全状态      │
│                  │  │                  │  │                  │
│ 温度:  26.5°C    │  │ 心率:  72 bpm    │  │ 有人: ✅ 运动:✅ │
│ 湿度:  60.0%     │  │ 呼吸:  16 次/分  │  │ 跌倒: ❌        │
│ WiFi:  ✅ 已连接 │  │ 体动:  正常      │  │ 驻留: ❌        │
│ 运行:  02:30:15  │  │                  │  │ 水浸: ✅ 正常   │
│ [蜂鸣器图标]     │  │ [页脚: 1/3]      │  │ [页脚: 2/3]      │
└──────────────────┘  └──────────────────┘  └──────────────────┘
```

### 2. `include/button_manager.h` + `src/button_manager.cpp`
- 管理 3 个按钮的去抖和长按检测
- 复用 `wifi_manager.cpp` 中 `_checkButtonLongPress()` 的逻辑模式
- 对外暴露回调:
  - `onProvisionLongPress` — 配网长按回调
  - `onAlarmDismiss` — 报警解除回调
  - `onPageSwitch` — 翻页回调
- 去抖参数: 50ms 消抖, 3000ms 长按阈值
- `loop()` 在主循环中每 10ms 轮询

## 修改文件

### 3. `include/config.h`
新增约 8 行引脚定义：
```cpp
#define OLED_SDA_PIN          12
#define OLED_SCL_PIN          13
#define BTN_PROVISION_PIN     14   // 配网按钮 (长按)
#define BTN_ALARM_DISMISS_PIN 15   // 解除报警按钮
#define BTN_PAGE_SWITCH_PIN   16   // 翻页按钮
#define BTN_DEBOUNCE_MS       50
#define BTN_LONG_PRESS_MS     3000
```

### 4. `platformio.ini`
`lib_deps` 新增一行: `olikraus/U8g2@^2.35`

### 5. `src/main.cpp`
- `#include "display_manager.h"` 和 `#include "button_manager.h"`
- 新增全局对象: `DisplayManager g_display;` `ButtonManager g_buttons;`
- `setup()` 中:
  - `g_display.begin()` (在雷达之后, WiFi 之前)
  - `g_buttons.begin()`
  - 注册按钮回调 (配网→startConfigPortal, 报警→clearAllLocal, 翻页→nextPage)
- `loop()` 中:
  - 主循环末尾调用 `g_buttons.loop()`
  - `printDebug()` 中同步调用 `g_display.update(tele, wifiOk, uptime, buzzerOn)`

## 实现细节

### 显示刷新策略
- 不与遥测上报频率绑定
- 跟随 `printDebug()` 的 5 秒周期刷新 (足够人眼阅读)
- 翻页时立即刷新

### U8g2 I2C 构造函数
```cpp
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, OLED_SCL_PIN, OLED_SDA_PIN);
```

### 按钮防抖逻辑
复刻 `WiFiMgmt::_checkButtonLongPress()` 模式:
- `lastState` / `currentState` 跟踪电平变化
- 按下时记录 `pressTime`，松开时检查是否超 `LONG_PRESS_MS`
- 短按: 松开时 < 3 秒 → 触发展示翻页/报警解除
- 长按: 松开时 ≥ 3 秒 → 触发配网

## 验证方式
1. 编译通过无报错 (`pio run`)
2. 上电后 OLED 显示 "🏥 健康监测 V1.0" 启动画面
3. 按翻页按钮 → 页面在 0/1/2 之间循环切换
4. 触发本地报警 → 蜂鸣器响 → 按解除按钮 → 蜂鸣器停
5. 长按配网按钮 3 秒 → 进入配网模式 (AP: HealthMonitor-Setup)

新增约 8 行引脚定义：

#define BTN_PAGE_SWITCH_PIN   16   // 翻页按钮


Plan is complete and updated. Let me also update the U8g2 version based on the agent's findings (2.35.30 from agent vs my original 2.35). Let me also update the library version and add a few more details.

Actually, the plan is comprehensive enough. Let me exit plan mode.


方案已完善，退出计划模式。


ExitPlanMode

Prompt: compile to verify

Prompt: build project

Prompt: run platformio build

# OLED 显示屏 + 三按钮交互方案

## 背景
在 V1.0 硬件基础上，增加 OLED 屏幕（SSD1306 128x64 I2C）和三个物理按钮，实现离线状态下的本地信息查看与交互控制。前期调试/交互阶段使用，无需手机/云端即可查看设备状态。

## GPIO 分配

| GPIO | 功能 | 说明 |
|---|---|---|
| 21 | OLED SDA | I2C 数据线 (ESP32 Arduino 默认 I2C) |
| 22 | OLED SCL | I2C 时钟线 |
| 14 | 按钮1: 配网 | 长按3秒→启动配网模式 (INPUT_PULLUP) |
| 15 | 按钮2: 解除警报 | 短按→关闭蜂鸣器+清除本地报警 (INPUT_PULLUP) |
| 16 | 按钮3: 翻页 | 短按→切换显示页 (共3页循环) (INPUT_PULLUP) |

## 新增文件

### 1. `include/display_manager.h` + `src/display_manager.cpp`
- 封装 U8g2 库 (SSD1306 128x64 HW I2C)
- 3 个显示页面，用 `_pageIndex` (0-2) 控制
- `begin()` 初始化 OLED，显示启动画面
- `update(const TelemetryData&, bool wifiOk, uint32_t uptime, bool buzzerOn)` 刷新当前页
- `nextPage()` 切换到下一页
- 中文渲染使用 `u8g2_font_wqy12_t_gb2312` 字体

**页面布局 (128x64)**：

```
Page 0 ─ 环境数据          Page 1 ─ 生命体征          Page 2 ─ 安全状态
┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐
│ 🏥 健康监测  V1.0 │  │ 🫀 生命体征      │  │ 🛡 安全状态      │
│                  │  │                  │  │                  │
│ 温度:  26.5°C    │  │ 心率:  72 bpm    │  │ 有人: ✅ 运动:✅ │
│ 湿度:  60.0%     │  │ 呼吸:  16 次/分  │  │ 跌倒: ❌        │
│ WiFi:  ✅ 已连接 │  │ 体动:  正常      │  │ 驻留: ❌        │
│ 运行:  02:30:15  │  │                  │  │ 水浸: ✅ 正常   │
│ [蜂鸣器图标]     │  │ [页脚: 1/3]      │  │ [页脚: 2/3]      │
└──────────────────┘  └──────────────────┘  └──────────────────┘
```

### 2. `include/button_manager.h` + `src/button_manager.cpp`
- 管理 3 个按钮的去抖和长按检测
- 复用 `wifi_manager.cpp` 中 `_checkButtonLongPress()` 的逻辑模式
- 对外暴露回调:
  - `onProvisionLongPress` — 配网长按回调
  - `onAlarmDismiss` — 报警解除回调
  - `onPageSwitch` — 翻页回调
- 去抖参数: 50ms 消抖, 3000ms 长按阈值
- `loop()` 在主循环中每 10ms 轮询

## 修改文件

### 3. `include/config.h`
新增约 8 行引脚定义：
```cpp
#define OLED_SDA_PIN          21
#define OLED_SCL_PIN          22
#define BTN_PROVISION_PIN     14   // 配网按钮 (长按)
#define BTN_ALARM_DISMISS_PIN 15   // 解除报警按钮
#define BTN_PAGE_SWITCH_PIN   16   // 翻页按钮
#define BTN_DEBOUNCE_MS       50
#define BTN_LONG_PRESS_MS     3000
```

### 4. `platformio.ini`
`lib_deps` 新增一行: `olikraus/U8g2@^2.35`

### 5. `src/main.cpp`
- `#include "display_manager.h"` 和 `#include "button_manager.h"`
- 新增全局对象: `DisplayManager g_display;` `ButtonManager g_buttons;`
- `setup()` 中:
  - `g_display.begin()` (在雷达之后, WiFi 之前)
  - `g_buttons.begin()`
  - 注册按钮回调 (配网→startConfigPortal, 报警→clearAllLocal, 翻页→nextPage)
- `loop()` 中:
  - 主循环末尾调用 `g_buttons.loop()`
  - `printDebug()` 中同步调用 `g_display.update(tele, wifiOk, uptime, buzzerOn)`

## 实现细节

### 显示刷新策略
- 不与遥测上报频率绑定
- 跟随 `printDebug()` 的 5 秒周期刷新 (足够人眼阅读)
- 翻页时立即刷新

### U8g2 I2C 构造函数
```cpp
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, OLED_SCL_PIN, OLED_SDA_PIN);
```

### 按钮防抖逻辑
复刻 `WiFiMgmt::_checkButtonLongPress()` 模式:
- `lastState` / `currentState` 跟踪电平变化
- 按下时记录 `pressTime`，松开时检查是否超 `LONG_PRESS_MS`
- 短按: 松开时 < 3 秒 → 触发展示翻页/报警解除
- 长按: 松开时 ≥ 3 秒 → 触发配网

## 验证方式
1. 编译通过无报错 (`pio run`)
2. 上电后 OLED 显示 "🏥 健康监测 V1.0" 启动画面
3. 按翻页按钮 → 页面在 0/1/2 之间循环切换
4. 触发本地报警 → 蜂鸣器响 → 按解除按钮 → 蜂鸣器停
5. 长按配网按钮 3 秒 → 进入配网模式 (AP: HealthMonitor-Setup)


User has approved your plan. You can now start coding. Start with updating your todo list if applicable

Your plan has been saved to: C:\Users\杨志佳\.claude\plans\magical-orbiting-cookie.md
You can refer back to it if needed during implementation.

## Approved Plan:
# OLED 显示屏 + 三按钮交互方案

## 背景
在 V1.0 硬件基础上，增加 OLED 屏幕（SSD1306 128x64 I2C）和三个物理按钮，实现离线状态下的本地信息查看与交互控制。前期调试/交互阶段使用，无需手机/云端即可查看设备状态。

## GPIO 分配

| GPIO | 功能 | 说明 |
|---|---|---|
| 21 | OLED SDA | I2C 数据线 (ESP32 Arduino 默认 I2C) |
| 22 | OLED SCL | I2C 时钟线 |
| 14 | 按钮1: 配网 | 长按3秒→启动配网模式 (INPUT_PULLUP) |
| 15 | 按钮2: 解除警报 | 短按→关闭蜂鸣器+清除本地报警 (INPUT_PULLUP) |
| 16 | 按钮3: 翻页 | 短按→切换显示页 (共3页循环) (INPUT_PULLUP) |

## 新增文件

### 1. `include/display_manager.h` + `src/display_manager.cpp`
- 封装 U8g2 库 (SSD1306 128x64 HW I2C)
- 3 个显示页面，用 `_pageIndex` (0-2) 控制
- `begin()` 初始化 OLED，显示启动画面
- `update(const TelemetryData&, bool wifiOk, uint32_t uptime, bool buzzerOn)` 刷新当前页
- `nextPage()` 切换到下一页
- 中文渲染使用 `u8g2_font_wqy12_t_gb2312` 字体

**页面布局 (128x64)**：

```
Page 0 ─ 环境数据          Page 1 ─ 生命体征          Page 2 ─ 安全状态
┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐
│ 🏥 健康监测  V1.0 │  │ 🫀 生命体征      │  │ 🛡 安全状态      │
│                  │  │                  │  │                  │
│ 温度:  26.5°C    │  │ 心率:  72 bpm    │  │ 有人: ✅ 运动:✅ │
│ 湿度:  60.0%     │  │ 呼吸:  16 次/分  │  │ 跌倒: ❌        │
│ WiFi:  ✅ 已连接 │  │ 体动:  正常      │  │ 驻留: ❌        │
│ 运行:  02:30:15  │  │                  │  │ 水浸: ✅ 正常   │
│ [蜂鸣器图标]     │  │ [页脚: 1/3]      │  │ [页脚: 2/3]      │
└──────────────────┘  └──────────────────┘  └──────────────────┘
```

### 2. `include/button_manager.h` + `src/button_manager.cpp`
- 管理 3 个按钮的去抖和长按检测
- 复用 `wifi_manager.cpp` 中 `_checkButtonLongPress()` 的逻辑模式
- 对外暴露回调:
  - `onProvisionLongPress` — 配网长按回调
  - `onAlarmDismiss` — 报警解除回调
  - `onPageSwitch` — 翻页回调
- 去抖参数: 50ms 消抖, 3000ms 长按阈值
- `loop()` 在主循环中每 10ms 轮询

## 修改文件

### 3. `include/config.h`
新增约 8 行引脚定义：
```cpp
#define OLED_SDA_PIN          21
#define OLED_SCL_PIN          22
#define BTN_PROVISION_PIN     14   // 配网按钮 (长按)
#define BTN_ALARM_DISMISS_PIN 15   // 解除报警按钮
#define BTN_PAGE_SWITCH_PIN   16   // 翻页按钮
#define BTN_DEBOUNCE_MS       50
#define BTN_LONG_PRESS_MS     3000
```

### 4. `platformio.ini`
`lib_deps` 新增一行: `olikraus/U8g2@^2.35`

### 5. `src/main.cpp`
- `#include "display_manager.h"` 和 `#include "button_manager.h"`
- 新增全局对象: `DisplayManager g_display;` `ButtonManager g_buttons;`
- `setup()` 中:
  - `g_display.begin()` (在雷达之后, WiFi 之前)
  - `g_buttons.begin()`
  - 注册按钮回调 (配网→startConfigPortal, 报警→clearAllLocal, 翻页→nextPage)
- `loop()` 中:
  - 主循环末尾调用 `g_buttons.loop()`
  - `printDebug()` 中同步调用 `g_display.update(tele, wifiOk, uptime, buzzerOn)`

## 实现细节

### 显示刷新策略
- 不与遥测上报频率绑定
- 跟随 `printDebug()` 的 5 秒周期刷新 (足够人眼阅读)
- 翻页时立即刷新

### U8g2 I2C 构造函数
```cpp
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, OLED_SCL_PIN, OLED_SDA_PIN);
```

### 按钮防抖逻辑
复刻 `WiFiMgmt::_checkButtonLongPress()` 模式:
- `lastState` / `currentState` 跟踪电平变化
- 按下时记录 `pressTime`，松开时检查是否超 `LONG_PRESS_MS`
- 短按: 松开时 < 3 秒 → 触发展示翻页/报警解除
- 长按: 松开时 ≥ 3 秒 → 触发配网

## 验证方式
1. 编译通过无报错 (`pio run`)
2. 上电后 OLED 显示 "🏥 健康监测 V1.0" 启动画面
3. 按翻页按钮 → 页面在 0/1/2 之间循环切换
4. 触发本地报警 → 蜂鸣器响 → 按解除按钮 → 蜂鸣器停
5. 长按配网按钮 3 秒 → 进入配网模式 (AP: HealthMonitor-Setup)


The plan is approved. Let me now implement everything. I'll follow the plan step by step:

1. Update config.h with new GPIO pins
2. Update platformio.ini with U8g2 library
3. Create display_manager.h
4. Create display_manager.cpp
5. Create button_manager.h
6. Create button_manager.cpp
7. Update main.cpp to integrate everything

Let me start implementing all of these.


开始实施。先并行创建所有文件：


TodoWrite

更新 config.h (GPIO引脚) 和 platformio.ini (U8g2库)

新建 OLED 显示管理器

新建三按钮管理器

集成显示和按钮到主程序

编译验证全部修改


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

更新 config.h (GPIO引脚) 和 platformio.ini (U8g2库)

新建 OLED 显示管理器

新建三按钮管理器

集成显示和按钮到主程序

编译验证全部修改


Let me start implementing. First, update config.h and platformio.ini, then create the new files.


**1. 更新配置文件和添加库依赖：**


Edit

// --- 状态指示灯 ---
#define LED_WIFI_PIN          8             // 蓝色 - WiFi 状态
#define LED_ALARM_PIN         9             // 红色 - 报警指示

// --- 状态指示灯 ---
#define LED_WIFI_PIN          8             // 蓝色 - WiFi 状态
#define LED_ALARM_PIN         9             // 红色 - 报警指示

// --- OLED 显示屏 (SSD1306 128x64 I2C) ---
#define OLED_SDA_PIN          21            // I2C 数据线
#define OLED_SCL_PIN          22            // I2C 时钟线

// --- 交互按钮 ---
#define BTN_PROVISION_PIN     14            // 配网按钮 (长按3秒)
#define BTN_ALARM_DISMISS_PIN 15            // 解除报警按钮
#define BTN_PAGE_SWITCH_PIN   16            // 翻页按钮
#define BTN_DEBOUNCE_MS       50            // 去抖时间
#define BTN_LONG_PRESS_MS     3000          // 长按判定


The file d:\PlatformIO1\Projects\Healthv2.0\include\config.h has been updated successfully. (file state is current in your context — no need to Read it back)

// --- 状态指示灯 ---
#define LED_WIFI_PIN          8             // 蓝色 - WiFi 状态
#define LED_ALARM_PIN         9             // 红色 - 报警指示

// --- OLED 显示屏 (SSD1306 128x64 I2C) ---
#define OLED_SDA_PIN          21            // I2C 数据线
#define OLED_SCL_PIN          22            // I2C 时钟线

// --- 交互按钮 ---
#define BTN_PROVISION_PIN     14            // 配网按钮 (长按3秒)
#define BTN_ALARM_DISMISS_PIN 15            // 解除报警按钮
#define BTN_PAGE_SWITCH_PIN   16            // 翻页按钮
#define BTN_DEBOUNCE_MS       50            // 去抖时间
#define BTN_LONG_PRESS_MS     3000          // 长按判定

/**
 * @file config.h
 * @brief 全局配置文件 - 五劳无感康养监测系统
 *
 * 协议版本: V0.08 (2026-07-17)
 * 包含: 硬件引脚定义、WiFi/MQTT 默认参数、
 *        雷达协议常量、告警阈值、任务配置等
 */

#ifndef CONFIG_H
#define CONFIG_H

#include <Arduino.h>

// ============================================================
// 1. 硬件引脚定义 (ESP32-S3)
// ============================================================

// --- R60ABD1 毫米波雷达 ---
#define RADAR_UART_PORT       Serial2
#define RADAR_RX_PIN          17
#define RADAR_TX_PIN          18
#define RADAR_BAUD_RATE       115200

// --- 雷达备用 GPIO ---
#define RADAR_GPIO1_PIN       10            // 活跃/静止指示
#define RADAR_GPIO2_PIN       11            // 有人/无人指示

// --- DHT11 温湿度传感器 ---
#define DHT11_PIN             4             // 温湿度传感器

// --- 水浸传感器 ---
#define WATER_SENSOR_PIN      6             // 数字输入, 低电平=有水

// --- 蜂鸣器 (PWM) ---
#define BUZZER_PIN            7
#define BUZZER_PWM_CHANNEL    0
#define BUZZER_PWM_FREQ       2000
#define BUZZER_PWM_RESOLUTION 8

// --- 配网按钮 ---
#define CONFIG_BUTTON_PIN     0             // 长按 3 秒复位 WiFi
#define BUTTON_LONG_PRESS_MS  3000

// --- 状态指示灯 ---
#define LED_WIFI_PIN          8             // 蓝色 - WiFi 状态
#define LED_ALARM_PIN         9             // 红色 - 报警指示

// ============================================================
// 2. 设备标识 (协议规范 §2)
// ============================================================

#define SALEID_DEFAULT        "10100001"    // 站点/系统编号
#define GATEID_DEFAULT        "10100001001" // 网关唯一编号
#define METER_ID_DEFAULT      "ENV001"      // 感知节点ID (卧室=ENV001, 卫生间=ENV002, 客厅=ENV003)
#define METER_NAME_DEFAULT    "RadarDHT11"  // 设备型号名称
#define FW_VERSION            "V1.0.0"

// ============================================================
// 3. WiFi 默认配置 (配网使用)
// ============================================================

#define WIFI_AP_SSID          "HealthMonitor-Setup"
#define WIFI_AP_PASSWORD      "12345678"
#define WIFI_CONFIG_TIMEOUT   300
#define WIFI_CONNECT_TIMEOUT  15
#define WIFI_RETRY_INTERVAL   10000

// ============================================================
// 4. MQTT 默认配置 (协议规范 §2)
// ============================================================

#define MQTT_DEFAULT_SERVER   "202.110.234.102"
#define MQTT_DEFAULT_PORT     8000
#define MQTT_DEFAULT_PORT_STR "8000"         // WiFiManager 参数用字符串
#define MQTT_DEFAULT_USER     "test"
#define MQTT_DEFAULT_PASS     "testtest"
#define MQTT_DEFAULT_DEVICE   "10100001001"  // 默认网关ID = gateid
#define MQTT_DEFAULT_ENV      "主卧"           // 默认环境备注
#undef MQTT_KEEPALIVE
#define MQTT_KEEPALIVE        30              // 30秒 (协议规范)
#define MQTT_RECONNECT_MIN    1000
#define MQTT_RECONNECT_MAX    60000

// ============================================================
// 5. MQTT 主题定义 (协议规范 §3)
// ============================================================

// --- 上行主题 (设备 → 服务端) ---
#define MQTT_TOPIC_REPORT_ALLPOINTS   "report/allpoints"                // 全量遥测上报 QoS0
#define MQTT_TOPIC_STATUS_ONLINE      "status/%s/online"                // 上线通知 QoS1
#define MQTT_TOPIC_STATUS_OFFLINE     "status/%s/offline"               // 离线遗嘱 QoS1
#define MQTT_TOPIC_STATUS_PROVISION   "status/%s/provision"             // 配网状态 QoS1
#define MQTT_TOPIC_RESPONSE_CMD       "response/%s/cmd"                 // 指令响应 QoS1
#define MQTT_TOPIC_RESPONSE_PROVISION "response/%s/provision"           // 配网响应 QoS1

// --- 下行主题 (服务端 → 设备) ---
#define MQTT_TOPIC_CMD_ALARM          "cmd/%s/alarm"                    // 告警指令 QoS1
#define MQTT_TOPIC_CMD_ALARM_RESTORE  "cmd/%s/alarm/restore"            // 告警恢复 QoS1
#define MQTT_TOPIC_CMD_CONTROL        "cmd/%s/control"                  // 控制指令 QoS1
#define MQTT_TOPIC_CMD_PROVISION_WIFI "cmd/%s/provision/wifi"           // WiFi配网 QoS1

// ============================================================
// 6. 上报配置 (协议规范 §4.1, §7.2)
// ============================================================

#define REPORT_INTERVAL_MS    1000                // 遥测上报周期 1秒
#define REPORT_QOS            0                   // 遥测 QoS 0
#define STATUS_QOS            1                   // 状态/响应 QoS 1

// ============================================================
// 7. 报警阈值 (协议规范 §6, 服务端判定用)
// ============================================================

#define HEART_RATE_MIN        50
#define HEART_RATE_MAX        120
#define BREATH_RATE_MIN       8
#define BREATH_RATE_MAX       24
#define TEMP_LOW              5.0f
#define TEMP_HIGH             45.0f
#define HUMI_LOW              15.0f
#define HUMI_HIGH             90.0f
#define WATER_DEBOUNCE_MS     50

// ============================================================
// 8. 任务配置
// ============================================================

#define RADAR_TASK_STACK      4096
#define RADAR_TASK_PRIORITY   5
#define RADAR_TASK_CORE       0

#define MQTT_TASK_STACK       8192
#define MQTT_TASK_PRIORITY    4
#define MQTT_TASK_CORE        1

// ============================================================
// 9. NTP 时间同步
// ============================================================

#define NTP_SERVER            "ntp.aliyun.com"
#define NTP_GMT_OFFSET_SEC    28800             // UTC+8 (北京时间)
#define NTP_DAYLIGHT_OFFSET   0

// ============================================================
// 10. LittleFS 配置文件名
// ============================================================

#define CONFIG_FILE_MQTT      "/mqtt_config.json"
#define CONFIG_FILE_WIFI      "/wifi_config.json"
#define CONFIG_FILE_DEVICE    "/device_config.json"

// ============================================================
// 11. 结构体: 全量遥测数据 (协议规范 §4.1)
// ============================================================

struct TelemetryData {
    // --- 设备标识 ---
    char     saleid[12];          // 站点编号 "10100001"
    char     gateid[16];          // 网关编号 "10100001001"
    char     meterId[8];          // 节点ID "ENV001"
    char     meterName[16];       // 设备型号 "RadarDHT11"

    // --- 环境数据 (DHT11) ---
    float    temp;                // 温度 ℃
    float    humi;                // 湿度 %RH
    bool     dhtValid;            // 温湿度有效性

    // --- 雷达: 存在/运动 ---
    bool     presence;            // 有人=1 无人=0
    bool     motion;              // 有运动=1 无运动=0
    bool     fall;                // 跌倒检测 1=触发
    bool     resident;            // 驻留状态 1=驻留

    // --- 雷达: 生命体征 ---
    uint8_t  heartRate;           // 心率 bpm (无效=0)
    uint8_t  breathRate;          // 呼吸 次/分 (无效=0)
    uint8_t  body;                // 人体能量/体征值
    bool     radarValid;          // 雷达数据有效性
    bool     vitalValid;          // 心率呼吸有效性

    // --- 水浸 ---
    bool     rainDO;              // 1=正常 0=水浸触发
    bool     rainValid;           // 水浸传感器有效性

    // --- 设备状态 ---
    bool     wifiReady;           // WiFi/MQTT 连接状态
    uint32_t uptimeSec;           // 设备累计运行秒数
    bool     buzzerState;         // 蜂鸣器状态 1=开启

    // --- 时间戳 ---
    uint32_t lastUpdate;          // 最后更新 ms
};

// 结构体: 下行报警指令 (协议规范 §5.1, §5.2)
struct AlarmCommand {
    char     cmdId[32];           // 指令唯一ID
    char     type[20];            // alarm / alarm_restore
    char     alarmType[24];       // fall/water_leak/heart_abnormal/breath_abnormal
    char     alarmLevel[8];       // low/medium/high
    char     alarmDesc[64];       // 告警描述
    char     meterId[8];          // 触发节点ID
    char     relatedCmdId[32];    // 关联原始告警ID (恢复用)

    // 联动动作
    bool     buzzerOn;            // 蜂鸣器开关
    uint16_t buzzerDuration;      // 蜂鸣持续秒数 (0=持续到恢复)
    bool     nightLight;          // 夜灯

    uint32_t timestamp;
};

// 结构体: 下行控制指令 (协议规范 §5.3)
struct ControlCommand {
    char     cmdId[32];
    char     type[20];            // control
    char     controlType[24];     // buzzer_test/device_restart/report_interval_set/factory_reset
    uint32_t params_duration;     // 参数: 持续秒数
    uint32_t params_interval;     // 参数: 上报间隔
    uint32_t timestamp;
};

// 结构体: 下行配网指令 (协议规范 §5.4)
struct ProvisionCommand {
    char     cmdId[32];
    char     wifi_ssid[33];
    char     wifi_password[65];
    char     wifi_encrypt[8];     // OPEN/WPA2/WPA3
    char     mqtt_broker[64];
    uint16_t mqtt_port;
    bool     mqtt_useTls;
    uint32_t timestamp;
};

#endif // CONFIG_H

#define LED_WIFI_PIN          8             // 蓝色 - WiFi 状态

#define LED_ALARM_PIN         9             // 红色 - 报警指示

+#define OLED_SDA_PIN          21            // I2C 数据线

+#define OLED_SCL_PIN          22            // I2C 时钟线

+#define BTN_PROVISION_PIN     14            // 配网按钮 (长按3秒)

+#define BTN_ALARM_DISMISS_PIN 15            // 解除报警按钮

+#define BTN_PAGE_SWITCH_PIN   16            // 翻页按钮

+#define BTN_DEBOUNCE_MS       50            // 去抖时间

+#define BTN_LONG_PRESS_MS     3000          // 长按判定

// 2. 设备标识 (协议规范 §2)


    ; DHT11 温湿度传感器
    adafruit/DHT sensor library@^1.4.6
    adafruit/Adafruit Unified Sensor@^1.1.14

    ; DHT11 温湿度传感器
    adafruit/DHT sensor library@^1.4.6
    adafruit/Adafruit Unified Sensor@^1.1.14

    ; OLED 显示屏 (SSD1306 128x64 I2C)
    olikraus/U8g2@^2.35.30


The file d:\PlatformIO1\Projects\Healthv2.0\platformio.ini has been updated successfully. (file state is current in your context — no need to Read it back)

    ; DHT11 温湿度传感器
    adafruit/DHT sensor library@^1.4.6
    adafruit/Adafruit Unified Sensor@^1.1.14

    ; OLED 显示屏 (SSD1306 128x64 I2C)
    olikraus/U8g2@^2.35.30

; PlatformIO Project Configuration File
; ESP32-S3 + R60ABD1 60G 毫米波雷达 健康监测系统

[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino

; 串口监视器
monitor_speed = 115200
monitor_filters = direct

; Flash 配置
board_build.flash_mode = qio
board_build.filesystem = littlefs
board_build.partitions = default_8MB.csv
board_build.arduino.memory_type = qio_opi

; 编译选项 - 优化速度
build_flags =
    -Os
    -DCORE_DEBUG_LEVEL=0
    -DARDUINO_USB_CDC_ON_BOOT=0
    -DBOARD_HAS_PSRAM

; 库依赖
lib_deps =
    ; WiFi 配网 (自带 captive portal)
    tzapu/WiFiManager@^2.0.17

    ; MQTT 客户端
    knolleary/PubSubClient@^2.8

    ; JSON 解析 (更省内存)
    bblanchon/ArduinoJson@^7.1.0

    ; DHT11 温湿度传感器
    adafruit/DHT sensor library@^1.4.6
    adafruit/Adafruit Unified Sensor@^1.1.14

; 上传配置
upload_speed = 921600
upload_port = COM15


**2. 创建显示管理器：**


Write

/**
 * @file display_manager.h
 * @brief OLED 显示管理器 - SSD1306 128x64 I2C
 *
 * 3 个显示页面:
 *   Page 0 - 环境数据 (温湿度 + WiFi + 运行时间)
 *   Page 1 - 生命体征 (心率 + 呼吸 + 体动)
 *   Page 2 - 安全状态 (有人/运动/跌倒/驻留/水浸)
 */

#ifndef DISPLAY_MANAGER_H
#define DISPLAY_MANAGER_H

#include <Arduino.h>
#include <U8g2lib.h>
#include "config.h"

class DisplayManager {
public:
    DisplayManager();
    ~DisplayManager();

    /** 初始化 OLED */
    bool begin();

    /**
     * @brief 更新显示内容 (每 5 秒调用一次即可)
     * @param data  遥测数据
     * @param wifiOk  WiFi 是否已连接
     * @param buzzerOn 蜂鸣器是否在响
     */
    void update(const TelemetryData& data, bool wifiOk, bool buzzerOn);

    /** 切换到下一页 (0→1→2→0 循环) */
    void nextPage();

    /** 获取当前页码 */
    uint8_t getPage() const { return _pageIndex; }

private:
    void _drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn);
    void _drawPage1(const TelemetryData& data);
    void _drawPage2(const TelemetryData& data);

    /** 格式化秒数为 HH:MM:SS */
    String _fmtUptime(uint32_t secs) const;

    /** 格式化 float 为一位小数 */
    String _fmtFloat(float val) const;

    U8G2_SSD1306_128X64_NONAME_F_HW_I2C _u8g2;
    uint8_t  _pageIndex;
    uint32_t _lastDrawTime;
    bool     _initialized;
};

#endif // DISPLAY_MANAGER_H


File created successfully at: d:\PlatformIO1\Projects\Healthv2.0\include\display_manager.h (file state is current in your context — no need to Read it back)

/**
 * @file display_manager.h
 * @brief OLED 显示管理器 - SSD1306 128x64 I2C
 *
 * 3 个显示页面:
 *   Page 0 - 环境数据 (温湿度 + WiFi + 运行时间)
 *   Page 1 - 生命体征 (心率 + 呼吸 + 体动)
 *   Page 2 - 安全状态 (有人/运动/跌倒/驻留/水浸)
 */

#ifndef DISPLAY_MANAGER_H
#define DISPLAY_MANAGER_H

#include <Arduino.h>
#include <U8g2lib.h>
#include "config.h"

class DisplayManager {
public:
    DisplayManager();
    ~DisplayManager();

    /** 初始化 OLED */
    bool begin();

    /**
     * @brief 更新显示内容 (每 5 秒调用一次即可)
     * @param data  遥测数据
     * @param wifiOk  WiFi 是否已连接
     * @param buzzerOn 蜂鸣器是否在响
     */
    void update(const TelemetryData& data, bool wifiOk, bool buzzerOn);

    /** 切换到下一页 (0→1→2→0 循环) */
    void nextPage();

    /** 获取当前页码 */
    uint8_t getPage() const { return _pageIndex; }

private:
    void _drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn);
    void _drawPage1(const TelemetryData& data);
    void _drawPage2(const TelemetryData& data);

    /** 格式化秒数为 HH:MM:SS */
    String _fmtUptime(uint32_t secs) const;

    /** 格式化 float 为一位小数 */
    String _fmtFloat(float val) const;

    U8G2_SSD1306_128X64_NONAME_F_HW_I2C _u8g2;
    uint8_t  _pageIndex;
    uint32_t _lastDrawTime;
    bool     _initialized;
};

#endif // DISPLAY_MANAGER_H


/**
 * @file button_manager.h
 * @brief 三按钮管理器 - 去抖 + 长按/短按识别
 *
 * 按钮1 (GPIO14): 长按3秒 → 配网
 * 按钮2 (GPIO15): 短按 → 解除报警
 * 按钮3 (GPIO16): 短按 → 翻页
 */

#ifndef BUTTON_MANAGER_H
#define BUTTON_MANAGER_H

#include <Arduino.h>
#include "config.h"

typedef void (*ButtonCallback)(void);

class ButtonManager {
public:
    ButtonManager();

    /** 初始化三个按钮引脚 */
    void begin();

    /** 主循环轮询 (每 10ms 调用) */
    void loop();

    /** 注册回调 */
    void onProvisionLongPress(ButtonCallback cb)   { _cbProvision = cb; }
    void onAlarmDismiss(ButtonCallback cb)          { _cbDismiss  = cb; }
    void onPageSwitch(ButtonCallback cb)            { _cbPage     = cb; }

private:
    struct ButtonState {
        uint8_t  pin;
        bool     pressed;        // 当前是否在按下状态
        uint32_t pressTime;      // 按下时刻 (ms)
        bool     longTriggered;  // 防止长按时重复触发短按
    };

    /** 处理单个按钮的状态机 */
    void _processButton(ButtonState& btn, bool hasShort, bool hasLong);
    bool _readPin(uint8_t pin);

    ButtonState _btnProvision;
    ButtonState _btnDismiss;
    ButtonState _btnPage;

    ButtonCallback _cbProvision;
    ButtonCallback _cbDismiss;
    ButtonCallback _cbPage;
};

#endif // BUTTON_MANAGER_H


File created successfully at: d:\PlatformIO1\Projects\Healthv2.0\include\button_manager.h (file state is current in your context — no need to Read it back)

/**
 * @file button_manager.h
 * @brief 三按钮管理器 - 去抖 + 长按/短按识别
 *
 * 按钮1 (GPIO14): 长按3秒 → 配网
 * 按钮2 (GPIO15): 短按 → 解除报警
 * 按钮3 (GPIO16): 短按 → 翻页
 */

#ifndef BUTTON_MANAGER_H
#define BUTTON_MANAGER_H

#include <Arduino.h>
#include "config.h"

typedef void (*ButtonCallback)(void);

class ButtonManager {
public:
    ButtonManager();

    /** 初始化三个按钮引脚 */
    void begin();

    /** 主循环轮询 (每 10ms 调用) */
    void loop();

    /** 注册回调 */
    void onProvisionLongPress(ButtonCallback cb)   { _cbProvision = cb; }
    void onAlarmDismiss(ButtonCallback cb)          { _cbDismiss  = cb; }
    void onPageSwitch(ButtonCallback cb)            { _cbPage     = cb; }

private:
    struct ButtonState {
        uint8_t  pin;
        bool     pressed;        // 当前是否在按下状态
        uint32_t pressTime;      // 按下时刻 (ms)
        bool     longTriggered;  // 防止长按时重复触发短按
    };

    /** 处理单个按钮的状态机 */
    void _processButton(ButtonState& btn, bool hasShort, bool hasLong);
    bool _readPin(uint8_t pin);

    ButtonState _btnProvision;
    ButtonState _btnDismiss;
    ButtonState _btnPage;

    ButtonCallback _cbProvision;
    ButtonCallback _cbDismiss;
    ButtonCallback _cbPage;
};

#endif // BUTTON_MANAGER_H


Good. Now let me create the implementation files.


TodoWrite

更新 config.h (GPIO引脚) 和 platformio.ini (U8g2库)

新建 OLED 显示管理器实现

新建三按钮管理器

集成显示和按钮到主程序

编译验证全部修改


新建 OLED 显示管理器

新建三按钮管理器

集成显示和按钮到主程序

编译验证全部修改

新建 OLED 显示管理器实现

新建三按钮管理器

集成显示和按钮到主程序

编译验证全部修改


Let me create the display_manager.cpp now. This is the biggest file to create.


**3. 显示管理器实现：**


Write

/**
 * @file display_manager.cpp
 * @brief OLED 显示管理器实现 - SSD1306 128x64 I2C
 */

#include "display_manager.h"

// ============================================================
// 构造 & 析构
// ============================================================

DisplayManager::DisplayManager()
    : _u8g2(U8G2_R0, U8X8_PIN_NONE, OLED_SCL_PIN, OLED_SDA_PIN)
    , _pageIndex(0)
    , _lastDrawTime(0)
    , _initialized(false)
{
}

DisplayManager::~DisplayManager() {
}

// ============================================================
// 初始化
// ============================================================

bool DisplayManager::begin() {
    if (!_u8g2.begin()) {
        Serial.println(F("[显示] ❌ OLED 初始化失败!"));
        return false;
    }

    _u8g2.enableUTF8Print();
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _initialized = true;

    // 显示启动画面
    _u8g2.clearBuffer();
    _u8g2.setFont(u8g2_font_wqy16_t_gb2312);
    _u8g2.drawUTF8(10, 30, "健康监测系统");
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(30, 52, "启动中...");
    _u8g2.sendBuffer();

    Serial.println(F("[显示] ✅ OLED 初始化完成 (SSD1306 128x64 I2C)"));
    return true;
}

// ============================================================
// 翻页
// ============================================================

void DisplayManager::nextPage() {
    _pageIndex = (_pageIndex + 1) % 3;
    _lastDrawTime = 0;  // 强制立即刷新
}

// ============================================================
// 主更新入口
// ============================================================

void DisplayManager::update(const TelemetryData& data, bool wifiOk, bool buzzerOn) {
    if (!_initialized) return;

    // 每 5 秒刷新一次 (防止 OLED 烧屏，也避免 SPI 占用过多)
    if (millis() - _lastDrawTime < 5000) return;
    _lastDrawTime = millis();

    _u8g2.clearBuffer();

    switch (_pageIndex) {
    case 0: _drawPage0(data, wifiOk, buzzerOn); break;
    case 1: _drawPage1(data); break;
    case 2: _drawPage2(data); break;
    default: _drawPage0(data, wifiOk, buzzerOn); break;
    }

    _u8g2.sendBuffer();
}

// ============================================================
// Page 0: 环境数据
// ============================================================

void DisplayManager::_drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn) {
    // 标题栏
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(2, 12, "🏥 健康监测 V1.0");

    // 分隔线
    _u8g2.drawHLine(0, 15, 128);

    // 温湿度
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(4, 30, "温度:");
    _u8g2.setCursor(52, 30);
    if (data.dhtValid) {
        _u8g2.printf("%s°C", _fmtFloat(data.temp).c_str());
    } else {
        _u8g2.print("-- °C");
    }

    _u8g2.drawUTF8(4, 44, "湿度:");
    _u8g2.setCursor(52, 44);
    if (data.dhtValid) {
        _u8g2.printf("%s%%", _fmtFloat(data.humi).c_str());
    } else {
        _u8g2.print("-- %");
    }

    // WiFi 状态
    _u8g2.drawUTF8(4, 58, "WiFi:");
    _u8g2.setCursor(52, 58);
    _u8g2.print(wifiOk ? "已连接" : "未连接");

    // 运行时间 + 蜂鸣器状态
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.setCursor(2, 64);
    _u8g2.print(_fmtUptime(data.uptimeSec));

    if (buzzerOn) {
        _u8g2.drawUTF8(104, 64, "🔔");
    }

    // 页码
    _u8g2.setCursor(110, 12);
    _u8g2.print("1/3");
}

// ============================================================
// Page 1: 生命体征
// ============================================================

void DisplayManager::_drawPage1(const TelemetryData& data) {
    // 标题栏
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(2, 12, "🫀 生命体征");
    _u8g2.drawHLine(0, 15, 128);

    // 心率
    _u8g2.drawUTF8(4, 32, "心率:");
    _u8g2.setCursor(52, 32);
    if (data.vitalValid && data.heartRate > 0) {
        _u8g2.printf("%d bpm", data.heartRate);
    } else {
        _u8g2.print("-- bpm");
    }

    // 呼吸
    _u8g2.drawUTF8(4, 48, "呼吸:");
    _u8g2.setCursor(52, 48);
    if (data.vitalValid && data.breathRate > 0) {
        _u8g2.printf("%d 次/分", data.breathRate);
    } else {
        _u8g2.print("-- 次/分");
    }

    // 体动/能量值
    _u8g2.drawUTF8(4, 64, "体动:");
    _u8g2.setCursor(52, 64);
    _u8g2.printf("%d", data.body);

    // 页码
    _u8g2.setCursor(110, 12);
    _u8g2.print("2/3");
}

// ============================================================
// Page 2: 安全状态
// ============================================================

void DisplayManager::_drawPage2(const TelemetryData& data) {
    // 标题栏
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(2, 12, "🛡 安全状态");
    _u8g2.drawHLine(0, 15, 128);

    // 有人 / 运动
    _u8g2.drawUTF8(4, 30, "有人:");
    _u8g2.drawUTF8(52, 30, data.presence ? "✅" : "❌");
    _u8g2.drawUTF8(80, 30, "运动:");
    _u8g2.drawUTF8(110, 30, data.motion ? "✅" : "❌");

    // 跌倒 / 驻留
    _u8g2.drawUTF8(4, 44, "跌倒:");
    _u8g2.drawUTF8(52, 44, data.fall ? "⚠" : "❌");
    _u8g2.drawUTF8(80, 44, "驻留:");
    _u8g2.drawUTF8(110, 44, data.resident ? "✅" : "❌");

    // 水浸状态
    _u8g2.drawUTF8(4, 58, "水浸:");
    _u8g2.setCursor(52, 58);
    if (data.rainValid) {
        _u8g2.print(data.rainDO ? "正常" : "⚠ 报警!");
    } else {
        _u8g2.print("--");
    }

    // 页码
    _u8g2.setCursor(110, 12);
    _u8g2.print("3/3");
}

// ============================================================
// 辅助方法
// ============================================================

String DisplayManager::_fmtUptime(uint32_t secs) const {
    uint32_t h = secs / 3600;
    uint32_t m = (secs % 3600) / 60;
    uint32_t s = secs % 60;
    char buf[12];
    snprintf(buf, sizeof(buf), "%02d:%02d:%02d", (int)h, (int)m, (int)s);
    return String(buf);
}

String DisplayManager::_fmtFloat(float val) const {
    char buf[8];
    snprintf(buf, sizeof(buf), "%.1f", val);
    return String(buf);
}


File created successfully at: d:\PlatformIO1\Projects\Healthv2.0\src\display_manager.cpp (file state is current in your context — no need to Read it back)

/**
 * @file display_manager.cpp
 * @brief OLED 显示管理器实现 - SSD1306 128x64 I2C
 */

#include "display_manager.h"

// ============================================================
// 构造 & 析构
// ============================================================

DisplayManager::DisplayManager()
    : _u8g2(U8G2_R0, U8X8_PIN_NONE, OLED_SCL_PIN, OLED_SDA_PIN)
    , _pageIndex(0)
    , _lastDrawTime(0)
    , _initialized(false)
{
}

DisplayManager::~DisplayManager() {
}

// ============================================================
// 初始化
// ============================================================

bool DisplayManager::begin() {
    if (!_u8g2.begin()) {
        Serial.println(F("[显示] ❌ OLED 初始化失败!"));
        return false;
    }

    _u8g2.enableUTF8Print();
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _initialized = true;

    // 显示启动画面
    _u8g2.clearBuffer();
    _u8g2.setFont(u8g2_font_wqy16_t_gb2312);
    _u8g2.drawUTF8(10, 30, "健康监测系统");
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(30, 52, "启动中...");
    _u8g2.sendBuffer();

    Serial.println(F("[显示] ✅ OLED 初始化完成 (SSD1306 128x64 I2C)"));
    return true;
}

// ============================================================
// 翻页
// ============================================================

void DisplayManager::nextPage() {
    _pageIndex = (_pageIndex + 1) % 3;
    _lastDrawTime = 0;  // 强制立即刷新
}

// ============================================================
// 主更新入口
// ============================================================

void DisplayManager::update(const TelemetryData& data, bool wifiOk, bool buzzerOn) {
    if (!_initialized) return;

    // 每 5 秒刷新一次 (防止 OLED 烧屏，也避免 SPI 占用过多)
    if (millis() - _lastDrawTime < 5000) return;
    _lastDrawTime = millis();

    _u8g2.clearBuffer();

    switch (_pageIndex) {
    case 0: _drawPage0(data, wifiOk, buzzerOn); break;
    case 1: _drawPage1(data); break;
    case 2: _drawPage2(data); break;
    default: _drawPage0(data, wifiOk, buzzerOn); break;
    }

    _u8g2.sendBuffer();
}

// ============================================================
// Page 0: 环境数据
// ============================================================

void DisplayManager::_drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn) {
    // 标题栏
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(2, 12, "🏥 健康监测 V1.0");

    // 分隔线
    _u8g2.drawHLine(0, 15, 128);

    // 温湿度
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(4, 30, "温度:");
    _u8g2.setCursor(52, 30);
    if (data.dhtValid) {
        _u8g2.printf("%s°C", _fmtFloat(data.temp).c_str());
    } else {
        _u8g2.print("-- °C");
    }

    _u8g2.drawUTF8(4, 44, "湿度:");
    _u8g2.setCursor(52, 44);
    if (data.dhtValid) {
        _u8g2.printf("%s%%", _fmtFloat(data.humi).c_str());
    } else {
        _u8g2.print("-- %");
    }

    // WiFi 状态
    _u8g2.drawUTF8(4, 58, "WiFi:");
    _u8g2.setCursor(52, 58);
    _u8g2.print(wifiOk ? "已连接" : "未连接");

    // 运行时间 + 蜂鸣器状态
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.setCursor(2, 64);
    _u8g2.print(_fmtUptime(data.uptimeSec));

    if (buzzerOn) {
        _u8g2.drawUTF8(104, 64, "🔔");
    }

    // 页码
    _u8g2.setCursor(110, 12);
    _u8g2.print("1/3");
}

// ============================================================
// Page 1: 生命体征
// ============================================================

void DisplayManager::_drawPage1(const TelemetryData& data) {
    // 标题栏
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(2, 12, "🫀 生命体征");
    _u8g2.drawHLine(0, 15, 128);

    // 心率
    _u8g2.drawUTF8(4, 32, "心率:");
    _u8g2.setCursor(52, 32);
    if (data.vitalValid && data.heartRate > 0) {
        _u8g2.printf("%d bpm", data.heartRate);
    } else {
        _u8g2.print("-- bpm");
    }

    // 呼吸
    _u8g2.drawUTF8(4, 48, "呼吸:");
    _u8g2.setCursor(52, 48);
    if (data.vitalValid && data.breathRate > 0) {
        _u8g2.printf("%d 次/分", data.breathRate);
    } else {
        _u8g2.print("-- 次/分");
    }

    // 体动/能量值
    _u8g2.drawUTF8(4, 64, "体动:");
    _u8g2.setCursor(52, 64);
    _u8g2.printf("%d", data.body);

    // 页码
    _u8g2.setCursor(110, 12);
    _u8g2.print("2/3");
}

// ============================================================
// Page 2: 安全状态
// ============================================================

void DisplayManager::_drawPage2(const TelemetryData& data) {
    // 标题栏
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(2, 12, "🛡 安全状态");
    _u8g2.drawHLine(0, 15, 128);

    // 有人 / 运动
    _u8g2.drawUTF8(4, 30, "有人:");
    _u8g2.drawUTF8(52, 30, data.presence ? "✅" : "❌");
    _u8g2.drawUTF8(80, 30, "运动:");
    _u8g2.drawUTF8(110, 30, data.motion ? "✅" : "❌");

    // 跌倒 / 驻留
    _u8g2.drawUTF8(4, 44, "跌倒:");
    _u8g2.drawUTF8(52, 44, data.fall ? "⚠" : "❌");
    _u8g2.drawUTF8(80, 44, "驻留:");
    _u8g2.drawUTF8(110, 44, data.resident ? "✅" : "❌");

    // 水浸状态
    _u8g2.drawUTF8(4, 58, "水浸:");
    _u8g2.setCursor(52, 58);
    if (data.rainValid) {
        _u8g2.print(data.rainDO ? "正常" : "⚠ 报警!");
    } else {
        _u8g2.print("--");
    }

    // 页码
    _u8g2.setCursor(110, 12);
    _u8g2.print("3/3");
}

// ============================================================
// 辅助方法
// ============================================================

String DisplayManager::_fmtUptime(uint32_t secs) const {
    uint32_t h = secs / 3600;
    uint32_t m = (secs % 3600) / 60;
    uint32_t s = secs % 60;
    char buf[12];
    snprintf(buf, sizeof(buf), "%02d:%02d:%02d", (int)h, (int)m, (int)s);
    return String(buf);
}

String DisplayManager::_fmtFloat(float val) const {
    char buf[8];
    snprintf(buf, sizeof(buf), "%.1f", val);
    return String(buf);
}


Good. Now create the button manager implementation.


**4. 按钮管理器实现：**


/**
 * @file button_manager.cpp
 * @brief 三按钮管理器实现 - 去抖 + 长按/短按
 */

#include "button_manager.h"

// ============================================================
// 构造
// ============================================================

ButtonManager::ButtonManager()
    : _cbProvision(nullptr)
    , _cbDismiss(nullptr)
    , _cbPage(nullptr)
{
    _btnProvision = { BTN_PROVISION_PIN,     false, 0, false };
    _btnDismiss   = { BTN_ALARM_DISMISS_PIN, false, 0, false };
    _btnPage      = { BTN_PAGE_SWITCH_PIN,   false, 0, false };
}

// ============================================================
// 初始化
// ============================================================

void ButtonManager::begin() {
    pinMode(BTN_PROVISION_PIN,     INPUT_PULLUP);
    pinMode(BTN_ALARM_DISMISS_PIN, INPUT_PULLUP);
    pinMode(BTN_PAGE_SWITCH_PIN,   INPUT_PULLUP);

    Serial.println(F("[按钮] ✅ 三按钮初始化完成"));
    Serial.printf("  配网: GPIO%d | 解除报警: GPIO%d | 翻页: GPIO%d\n",
                  BTN_PROVISION_PIN, BTN_ALARM_DISMISS_PIN, BTN_PAGE_SWITCH_PIN);
}

// ============================================================
// 主循环轮询
// ============================================================

void ButtonManager::loop() {
    // 按钮1: 配网 (仅长按)
    _processButton(_btnProvision, false, true);

    // 按钮2: 解除报警 (仅短按)
    _processButton(_btnDismiss, true, false);

    // 按钮3: 翻页 (仅短按)
    _processButton(_btnPage, true, false);
}

// ============================================================
// 单按钮状态机
// ============================================================

void ButtonManager::_processButton(ButtonState& btn, bool hasShort, bool hasLong) {
    bool raw = _readPin(btn.pin);  // LOW = 按下

    if (raw && !btn.pressed) {
        // 按下沿
        btn.pressed = true;
        btn.pressTime = millis();
        btn.longTriggered = false;
    }
    else if (!raw && btn.pressed) {
        // 松开沿
        btn.pressed = false;
        uint32_t holdTime = millis() - btn.pressTime;

        if (holdTime < BTN_DEBOUNCE_MS) {
            return;  // 抖动脉冲，忽略
        }

        if (hasLong && holdTime >= BTN_LONG_PRESS_MS && !btn.longTriggered) {
            // 长按触发
            btn.longTriggered = true;
            if (_cbProvision) _cbProvision();
        }
        else if (hasShort && holdTime >= BTN_DEBOUNCE_MS && holdTime < BTN_LONG_PRESS_MS) {
            // 短按触发
            if (btn.pin == BTN_ALARM_DISMISS_PIN && _cbDismiss) _cbDismiss();
            if (btn.pin == BTN_PAGE_SWITCH_PIN   && _cbPage)    _cbPage();
        }
    }
}

bool ButtonManager::_readPin(uint8_t pin) {
    return digitalRead(pin) == LOW;  // INPUT_PULLUP: LOW = 按下
}


File created successfully at: d:\PlatformIO1\Projects\Healthv2.0\src\button_manager.cpp (file state is current in your context — no need to Read it back)

/**
 * @file button_manager.cpp
 * @brief 三按钮管理器实现 - 去抖 + 长按/短按
 */

#include "button_manager.h"

// ============================================================
// 构造
// ============================================================

ButtonManager::ButtonManager()
    : _cbProvision(nullptr)
    , _cbDismiss(nullptr)
    , _cbPage(nullptr)
{
    _btnProvision = { BTN_PROVISION_PIN,     false, 0, false };
    _btnDismiss   = { BTN_ALARM_DISMISS_PIN, false, 0, false };
    _btnPage      = { BTN_PAGE_SWITCH_PIN,   false, 0, false };
}

// ============================================================
// 初始化
// ============================================================

void ButtonManager::begin() {
    pinMode(BTN_PROVISION_PIN,     INPUT_PULLUP);
    pinMode(BTN_ALARM_DISMISS_PIN, INPUT_PULLUP);
    pinMode(BTN_PAGE_SWITCH_PIN,   INPUT_PULLUP);

    Serial.println(F("[按钮] ✅ 三按钮初始化完成"));
    Serial.printf("  配网: GPIO%d | 解除报警: GPIO%d | 翻页: GPIO%d\n",
                  BTN_PROVISION_PIN, BTN_ALARM_DISMISS_PIN, BTN_PAGE_SWITCH_PIN);
}

// ============================================================
// 主循环轮询
// ============================================================

void ButtonManager::loop() {
    // 按钮1: 配网 (仅长按)
    _processButton(_btnProvision, false, true);

    // 按钮2: 解除报警 (仅短按)
    _processButton(_btnDismiss, true, false);

    // 按钮3: 翻页 (仅短按)
    _processButton(_btnPage, true, false);
}

// ============================================================
// 单按钮状态机
// ============================================================

void ButtonManager::_processButton(ButtonState& btn, bool hasShort, bool hasLong) {
    bool raw = _readPin(btn.pin);  // LOW = 按下

    if (raw && !btn.pressed) {
        // 按下沿
        btn.pressed = true;
        btn.pressTime = millis();
        btn.longTriggered = false;
    }
    else if (!raw && btn.pressed) {
        // 松开沿
        btn.pressed = false;
        uint32_t holdTime = millis() - btn.pressTime;

        if (holdTime < BTN_DEBOUNCE_MS) {
            return;  // 抖动脉冲，忽略
        }

        if (hasLong && holdTime >= BTN_LONG_PRESS_MS && !btn.longTriggered) {
            // 长按触发
            btn.longTriggered = true;
            if (_cbProvision) _cbProvision();
        }
        else if (hasShort && holdTime >= BTN_DEBOUNCE_MS && holdTime < BTN_LONG_PRESS_MS) {
            // 短按触发
            if (btn.pin == BTN_ALARM_DISMISS_PIN && _cbDismiss) _cbDismiss();
            if (btn.pin == BTN_PAGE_SWITCH_PIN   && _cbPage)    _cbPage();
        }
    }
}

bool ButtonManager::_readPin(uint8_t pin) {
    return digitalRead(pin) == LOW;  // INPUT_PULLUP: LOW = 按下
}


Good. Now update main.cpp to integrate display and buttons.


TodoWrite

更新 config.h (GPIO引脚) 和 platformio.ini (U8g2库)

新建 OLED 显示管理器

新建三按钮管理器

集成显示和按钮到主程序

编译验证全部修改


新建 OLED 显示管理器实现

新建三按钮管理器

集成显示和按钮到主程序

编译验证全部修改

新建 OLED 显示管理器

新建三按钮管理器

集成显示和按钮到主程序

编译验证全部修改


Now update main.cpp to integrate display and buttons.


**5. 集成到主程序：**


Edit

#include "sensor_manager.h"
#include "alarm_manager.h"

#include "sensor_manager.h"
#include "alarm_manager.h"
#include "display_manager.h"
#include "button_manager.h"


The file d:\PlatformIO1\Projects\Healthv2.0\src\main.cpp has been updated successfully. (file state is current in your context — no need to Read it back)

#include "sensor_manager.h"
#include "alarm_manager.h"
#include "display_manager.h"
#include "button_manager.h"


RadarManager   g_radar;
WiFiMgmt       g_wifi;
MQTTManager    g_mqtt;
SensorManager  g_sensor;
AlarmManager   g_alarm;

RadarManager   g_radar;
WiFiMgmt       g_wifi;
MQTTManager    g_mqtt;
SensorManager  g_sensor;
AlarmManager   g_alarm;
DisplayManager g_display;
ButtonManager  g_buttons;


RadarManager   g_radar;
WiFiMgmt       g_wifi;
MQTTManager    g_mqtt;
SensorManager  g_sensor;
AlarmManager   g_alarm;
DisplayManager g_display;
ButtonManager  g_buttons;

MQTTManager    g_mqtt;
SensorManager  g_sensor;
AlarmManager   g_alarm;
+DisplayManager g_display;
+ButtonManager  g_buttons;
// 上报控制
uint32_t g_lastReportTime  = 0;


Now I need to add the setup code for display and buttons. Let me add them in setup():

1. After radar.begin() and before WiFi.begin() → display.begin()
2. Right after display → buttons.begin() and register callbacks


现在在 `setup()` 中添加显示和按钮初始化：


    // 2. 雷达
    g_radar.begin();

    // 3. WiFi (WiFiManager 配网)
    g_wifi.begin();

    // 2. 雷达
    g_radar.begin();

    // 3. OLED 显示
    g_display.begin();

    // 4. 按钮
    g_buttons.begin();
    g_buttons.onPageSwitch([]() {
        g_display.nextPage();
    });
    g_buttons.onAlarmDismiss([]() {
        g_alarm.clearAllLocal();
    });
    g_buttons.onProvisionLongPress([]() {
        g_wifi.startConfigPortal();
    });

    // 5. WiFi (WiFiManager 配网)
    g_wifi.begin();


    // 2. 雷达
    g_radar.begin();

    // 3. WiFi (WiFiManager 配网)
    g_wifi.begin();

    // 2. 雷达
    g_radar.begin();

    // 3. OLED 显示
    g_display.begin();

    // 4. 按钮
    g_buttons.begin();
    g_buttons.onPageSwitch([]() {
        g_display.nextPage();
    });
    g_buttons.onAlarmDismiss([]() {
        g_alarm.clearAllLocal();
    });
    g_buttons.onProvisionLongPress([]() {
        g_wifi.startConfigPortal();
    });

    // 5. WiFi (WiFiManager 配网)
    g_wifi.begin();

+    g_buttons.onProvisionLongPress([]() {


Now I need to update the numbering comments for the rest of setup() and add button loop + display update in the main loop.


更新 setup 注释编号和 main loop：


    // 4. 时间同步
    if (g_wifi.isConnected()) {
        syncTime();
    }

    // 5. MQTT

    // 6. 时间同步
    if (g_wifi.isConnected()) {
        syncTime();
    }

    // 7. MQTT


    // 4. 时间同步
    if (g_wifi.isConnected()) {
        syncTime();
    }

    // 5. MQTT

    // 6. 时间同步
    if (g_wifi.isConnected()) {
        syncTime();
    }

    // 7. MQTT


    // 注册 MQTT 回调
    g_mqtt.onAlarmCommand(handleAlarmCommand);
    g_mqtt.onControlCommand(handleControlCommand);
    g_mqtt.onProvisionCommand(handleProvisionCommand);

    // 连接 MQTT
    if (g_wifi.isConnected()) {
        g_mqtt.connect();
    }

    // 6. 传感器
    g_sensor.begin();

    // 7. 雷达任务 (核心0)

    // 注册 MQTT 回调
    g_mqtt.onAlarmCommand(handleAlarmCommand);
    g_mqtt.onControlCommand(handleControlCommand);
    g_mqtt.onProvisionCommand(handleProvisionCommand);

    // 连接 MQTT
    if (g_wifi.isConnected()) {
        g_mqtt.connect();
    }

    // 8. 传感器
    g_sensor.begin();

    // 9. 雷达任务 (核心0)


    // 注册 MQTT 回调
    g_mqtt.onAlarmCommand(handleAlarmCommand);
    g_mqtt.onControlCommand(handleControlCommand);
    g_mqtt.onProvisionCommand(handleProvisionCommand);

    // 连接 MQTT
    if (g_wifi.isConnected()) {
        g_mqtt.connect();
    }

    // 6. 传感器
    g_sensor.begin();

    // 7. 雷达任务 (核心0)

    // 注册 MQTT 回调
    g_mqtt.onAlarmCommand(handleAlarmCommand);
    g_mqtt.onControlCommand(handleControlCommand);
    g_mqtt.onProvisionCommand(handleProvisionCommand);

    // 连接 MQTT
    if (g_wifi.isConnected()) {
        g_mqtt.connect();
    }

    // 8. 传感器
    g_sensor.begin();

    // 9. 雷达任务 (核心0)

g_mqtt.connect();
}
-    // 6. 传感器
+    // 8. 传感器
g_sensor.begin();
-    // 7. 雷达任务 (核心0)
+    // 9. 雷达任务 (核心0)
xTaskCreatePinnedToCore(
radarTask, "RadarTask",
RADAR_TASK_STACK, nullptr,


Now add button polling and display update to the main loop. I need to:
1. Add `g_buttons.loop()` in the main loop (near the top)
2. Add `g_display.update()` after `printDebug()`


在 `loop()` 中添加按钮轮询和显示更新：


void loop() {
    uint32_t now = millis();

    // --- 1. WiFi ---
    bool wifiOk = g_wifi.loop();

void loop() {
    uint32_t now = millis();

    // --- 0. 按钮轮询 ---
    g_buttons.loop();

    // --- 1. WiFi ---
    bool wifiOk = g_wifi.loop();


void loop() {
    uint32_t now = millis();

    // --- 1. WiFi ---
    bool wifiOk = g_wifi.loop();

void loop() {
    uint32_t now = millis();

    // --- 0. 按钮轮询 ---
    g_buttons.loop();

    // --- 1. WiFi ---
    bool wifiOk = g_wifi.loop();

void loop() {
uint32_t now = millis();
+    // --- 0. 按钮轮询 ---
+    g_buttons.loop();
+
// --- 1. WiFi ---
bool wifiOk = g_wifi.loop();


        // 调试输出
        printDebug(tele);
    }

        // 调试输出 + OLED 显示
        printDebug(tele);
        g_display.update(tele, wifiOk, g_alarm.isBuzzerOn());
    }


        // 调试输出
        printDebug(tele);
    }

        // 调试输出 + OLED 显示
        printDebug(tele);
        g_display.update(tele, wifiOk, g_alarm.isBuzzerOn());
    }

-        // 调试输出

+        g_display.update(tele, wifiOk, g_alarm.isBuzzerOn());

// --- 7. MQTT 配网状态 (WiFiManager 触发时) ---


新建 OLED 显示管理器

新建三按钮管理器

集成显示和按钮到主程序

编译验证全部修改