# ESP32 传感器数据上报 · 对接说明

> 本文档写给**负责编写 ESP32 固件的 AI / 开发者**。
> 服务器端是一个 PHP + MySQL 的接收站：**不做任何鉴权，不校验字段，ESP32 发什么 JSON 就原样存什么**。

---

## 1. 需要访问的 URL

| 用途 | 地址 | 方法 |
|---|---|---|
| **数据上报（核心）** | `https://longtimeproject.xyz/project/p4/api/ingest.php` | POST |
| 可视化看板（实时曲线 + 历史，SSE 自动刷新） | `https://longtimeproject.xyz/project/p4/` | GET |
| 纯 JSON 数据接口（最新 + 历史查询） | `https://longtimeproject.xyz/project/p4/api/readings.php` | GET |
| 实时数据流（SSE，新数据自动推送） | `https://longtimeproject.xyz/project/p4/api/stream.php` | GET |
| 本地联调（WAMP，电脑与 ESP32 连同一 WiFi） | `http://<电脑局域网IP>/html/project/p4/api/ingest.php` | POST |

- 云端是 **HTTPS**，本地是 **HTTP**，两套代码差异仅在 `WiFiClient` / `WiFiClientSecure`（见第 4 节）。
- **无需 token、无需登录、无需 Cookie**。HTTP 状态码 `200` 且响应 JSON 中 `success: true` 即上报成功。

---

## 2. 上报协议

- **方法**：`POST`
- **请求头**：`Content-Type: application/json`
- **请求体**：任意合法 JSON 对象，建议带上设备名和传感器读数，例如：

```json
{
  "device": "esp32-01",
  "temp": 25.6,
  "humi": 60.2,
  "mq2": 856,
  "rssi": -52
}
```

当前固件（SHT30 + MQ-2）各字段含义：

| 字段 | 含义 | 单位 / 范围 |
|---|---|---|
| `temp` | SHT30 温度 | °C |
| `humi` | SHT30 湿度 | %RH |
| `mq2` | **MQ-2 可燃气体/烟雾传感器的 ADC 原始值**（`analogRead` 12 位采样），数值越大浓度越高 | 0~4095（对应 0~3.3V），**不是 PPM** |
| `rssi` | WiFi 信号强度 | dBm |

> **关于 MQ-2 与 PPM**：当前固件直接上报模拟量，未做气体浓度换算。
> MQ-2 输出 PPM 需要先在清洁空气中预热 24h+ 校准出基准电阻 R0，再按数据手册对数曲线 `ppm = a × (Rs/R0)^b` 换算（不同气体 a/b 系数不同）。
> 网页端目前按经验阈值对 0~4095 做「清洁 / 微量 / 偏高 / 高浓度」四级相对提示（清洁 <1000，微量 <1800，偏高 <2800，高浓度 ≥2800），仅供趋势参考；
> 若后续固件完成校准并上报真实 PPM（例如新增 `mq2_ppm` 字段），网页无需改动——任意字段都会原样存储，可再扩展显示。

- 字段名、字段数量、嵌套结构**完全自由**，服务器原样保存。
- 设备名识别规则：自动从 JSON 的 `device` / `device_id` / `deviceId` / `id` / `node` / `name` 字段中取第一个非空值；都没有则归入默认设备 `esp32`。
- **单条请求体上限 8 KB**；建议上报间隔 **10 ~ 60 秒**，不要高于 1 秒一次。
- 时间戳不用发，服务器会自动记录接收时间。

### 成功响应（HTTP 200）

```json
{
  "success": true,
  "message": "已接收",
  "data": {
    "id": 123,
    "device": "esp32-01",
    "source": "json",
    "received": { "device": "esp32-01", "temp": 25.6, "humi": 60.2 },
    "server_time": "2026-09-05 18:30:00",
    "server_ts": 1788604200
  }
}
```

### 失败响应

- `400`：请求体为空；`413`：超过 8 KB；`500`：服务器数据库异常。
- 失败时响应体同样是 JSON，`success: false`，`message` 为原因。**建议固件对非 200 响应做重试（间隔 30 秒以上），不要死循环狂发。**

### 浏览器快速自测（不用写固件也能验证链路）

直接在浏览器地址栏打开（GET 查询参数也能上报）：

```
https://longtimeproject.xyz/project/p4/api/ingest.php?device=test-01&temp=26.5&humi=55
```

---

## 3. 查看已上报的数据

- 浏览器打开 `https://longtimeproject.xyz/project/p4/` → **可视化看板**：
  - 设备卡片：每台设备最新温度 / 湿度 / 信号，新数据到达时卡片高亮闪烁；
  - 历史曲线：温度、湿度走势（SVG 手绘，悬停可查看每个点的数值）；
  - 上报历史：最新在前的记录表，可展开查看原始 JSON，可"加载更早的记录"；
  - 页面通过 SSE 实时刷新，无需手动刷新浏览器。
- **纯 JSON 接口**（程序 / 调试调用）：`GET https://longtimeproject.xyz/project/p4/api/readings.php`
  - `?limit=200`：返回条数（默认 60，最大 200）
  - `?device=esp32-01`：只看某台设备
  - `?before=123`：向前翻页（返回 id 小于 123 的更早记录）
  - `?after=123`：增量拉取（返回 id 大于 123 的更新记录）
  - 兼容旧地址：`https://longtimeproject.xyz/project/p4/?format=json` 输出相同
- 每条记录形如：

```json
{
  "id": 123,
  "device": "esp32-01",
  "payload": { "device": "esp32-01", "temp": 25.6, "humi": 60.2 },
  "time": "2026-09-05 18:30:00",
  "ts": 1788604200
}
```

- **实时数据流（SSE）**：`GET https://longtimeproject.xyz/project/p4/api/stream.php`
  - 连上后先收到 `event: ready`（data 为当前最大 id）；
  - 之后 ESP32 每上报一条，服务器立刻推送 `event: reading`，data 就是该条记录 JSON；
  - 每秒有 `event: ping` 心跳。网页端用 `new EventSource(url)` 即可接收，无需轮询。

---

## 4. ESP32（Arduino 框架）示例代码

依赖：`WiFi.h`、`HTTPClient.h`、`WiFiClientSecure.h`（均为 ESP32 Arduino 核心自带，无需第三方库）。
JSON 用 `String` 拼接，避免引入额外库；如工程中已有 ArduinoJson，用它序列化更好。

```cpp
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>

const char* WIFI_SSID = "你的WiFi名";
const char* WIFI_PASS = "WiFi密码";

// 云端 HTTPS 地址（正式使用）
const char* INGEST_URL = "https://longtimeproject.xyz/project/p4/api/ingest.php";
// 本地 HTTP 联调地址（二选一，注意本地用 WiFiClient 而非 Secure）
// const char* INGEST_URL = "http://192.168.1.100/html/project/p4/api/ingest.php";

void postReading(float temp, float humi) {
    WiFiClientSecure client;   // 本地 HTTP 联调时改用：WiFiClient client;
    client.setInsecure();      // 跳过证书校验，最简单可靠；如需严格校验可改用 setCACert

    HTTPClient http;
    if (!http.begin(client, INGEST_URL)) {
        Serial.println("HTTP begin 失败");
        return;
    }
    http.addHeader("Content-Type", "application/json");

    // 拼 JSON（字段可自由增减）
    char body[256];
    snprintf(body, sizeof(body),
        "{\"device\":\"esp32-01\",\"temp\":%.1f,\"humi\":%.1f,\"rssi\":%d}",
        temp, humi, WiFi.RSSI());

    int code = http.POST(body);
    Serial.printf("上报结果 HTTP %d\n", code);
    if (code == 200) {
        Serial.println(http.getString());   // 服务器返回的 JSON 回执
    } else {
        Serial.println("上报失败，将在下次循环重试");
    }
    http.end();
}

void setup() {
    Serial.begin(115200);
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
    Serial.println("\nWiFi 已连接");
}

void loop() {
    if (WiFi.status() == WL_CONNECTED) {
        float temp = 25.0 + random(0, 50) / 10.0;  // 替换为真实传感器读数
        float humi = 55.0 + random(0, 150) / 10.0;
        postReading(temp, humi);
    }
    delay(30000);   // 30 秒上报一次
}
```

### HTTPS 注意事项（重要）

1. 云端域名是 **HTTPS**，必须用 `WiFiClientSecure`；用 `WiFiClient` 明文连 443 端口会握手失败。
2. `setInsecure()` 不校验证书，联调和一般场景足够；若要严格校验，用 `client.setCACert(根证书)` 并注意证书到期更换。
3. 若设备所在网络对 HTTPS 不友好，可先用本地 HTTP 地址（`http://电脑IP/html/project/p4/api/ingest.php`）把链路调通，再切云端。
4. 每次上报前判断 `WiFi.status()`，断线先重连；HTTP 非 200 时不要立刻重发，等下一个周期。

---

## 5. 数据存储说明（给需要了解服务端的人）

- 数据库：MySQL，库名 `sensor_hub_v4`（配置在 `4/config/database.php`，云端部署时改账号密码）。
- 表 `readings`：`id`（自增）、`device`（设备标识，索引）、`payload`（上报的原始 JSON 全文）、`ip`（来源 IP）、`created_at`（服务器接收时间）。
- 首次访问任意页面会自动建库建表，无需手动导入 SQL。
