
关键词:以太网变送器、环境记录仪开发、本地存储架构、远端上云、断网续传、多协议上报、边缘计算、腾讯云物联网平台、数据同步、混合云存储 标签:#物联网 #Modbus #TCP/IP #UDP #POE供电 #腾讯云 #Python #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控 #边缘计算
前几篇讲的是如何把现成的 RJ45 温湿度变送器接入系统。本篇换个视角——如果你要自己开发一款以太网环境记录仪,核心需求是:
这不是简单的"传感器 + MCU + 网口"拼装,而是一台边缘数据记录设备。本文从硬件选型、固件架构、存储设计、上云通道四个维度展开。


方案 | SoC | 内存 | 存储 | 网络 | 功耗 | 开发难度 | 适合场景 |
|---|---|---|---|---|---|---|---|
ESP32 + LAN8720 | 双核 240MHz | 520KB SRAM + PSRAM | SPI Flash 4~16MB | 10/100M RMII | <2W | 低(ESP-IDF/Arduino) | 低成本单点记录仪 |
树莓派 CM4 | BCM2711 4核 1.5GHz | 1~8GB | eMMC/SD | 千兆 | 5~8W | 低(Linux 全栈) | 多通道、高算力 |
i.MX6ULL + Linux | Cortex-A7 800MHz | 256~512MB DDR | NAND/eMMC | 10/100M | 3~5W | 中(Yocto/Buildroot) | 工业级、长生命周期 |
全志 F1C200s | ARM9 900MHz | 64MB DDR | SPI NAND | 10/100M | 2~3W | 中 | 超低成本、精简功能 |
STM32H7 + LAN8742 | Cortex-M7 480MHz | 1MB SRAM | 外部 QSPI Flash | 10/100M | <1W | 高(裸机/RT-Thread) | 超低功耗、实时性要求 |
推荐选择:
记录仪不只是温湿度,典型需要接入:
传感器 | 接口 | 说明 |
|---|---|---|
温湿度 | I²C(SHT30/SHT35)或 1-Wire(DS18B20) | 精度 ±0.3℃/±2%RH |
露点 | 算法计算 | T + RH → 露点公式 |
气压 | I²C(BMP280/BME280) | 可选 |
水浸 | 干接点/模拟量 | 开关量输入 |
烟雾 | I²C(SGP30)或 4~20mA | 可选 |
门磁 | GPIO 中断 | 状态变化触发记录 |
层级 | 介质 | 容量 | 用途 |
|---|---|---|---|
L1:掉电保护缓冲 | FRAM / 超级电容 + SRAM | 几 KB | 最近一条记录,断电不丢 |
L2:高速缓存 | SPI Flash / eMMC 分区 | 几十 MB | 环形缓冲,最近数万条 |
L3:长期存储 | SD 卡(工业级) / eMMC | 8~64GB | 历史数据、补传队列 |
L4:远端 | 腾讯云 IoT / InfluxDB | 无限 | 云端持久化、分析 |
┌─────────────────────────────────────────────────────────────┐
│ 应用层(业务逻辑) │
├─────────────────────────────────────────────────────────────┤
│ 采集调度 │ 本地存储 │ 网络管理 │ 云端通道 │ Web 配置 │
│ (采样周 │ (环形缓 │ (WiFi/ │ (MQTT/ │ (REST/ │
│ 期控制) │ 冲管理) │ 以太网) │ HTTPS) │ mDNS) │
├─────────────────────────────────────────────────────────────┤
│ 中间件层 │
├─────────────────────────────────────────────────────────────┤
│ lwIP │ Flash FS │ SQLite │ TLS │ JSON │ Modbus Slave │
├─────────────────────────────────────────────────────────────┤
│ 系统层 │
├─────────────────────────────────────────────────────────────┤
│ Linux (CM4/i.MX6ULL) 或 FreeRTOS (ESP32/STM32) │
├─────────────────────────────────────────────────────────────┤
│ 硬件抽象层(HAL) │
├─────────────────────────────────────────────────────────────┤
│ GPIO │ I²C │ SPI │ Ethernet MAC/PHY │ RTC │ Watchdog │
└─────────────────────────────────────────────────────────────┘/* 以 FreeRTOS + ESP32 为例,展示任务划分 */
void app_main() {
/* 硬件初始化 */
i2c_init();
spi_flash_init();
ethernet_init();
rtc_init();
watchdog_init();
/* 创建队列 */
QueueHandle_t sensor_queue = xQueueCreate(10, sizeof(sensor_record_t));
QueueHandle_t storage_queue = xQueueCreate(100, sizeof(storage_record_t));
QueueHandle_t cloud_queue = xQueueCreate(50, sizeof(cloud_record_t));
/* 启动任务 */
xTaskCreate(sensor_task, "sensor", 4096, sensor_queue, 4, NULL);
xTaskCreate(storage_task, "storage", 8192, storage_queue, 3, NULL);
xTaskCreate(network_task, "network", 4096, NULL, 3, NULL);
xTaskCreate(cloud_task, "cloud", 8192, cloud_queue, 2, NULL);
xTaskCreate(web_server_task, "web", 6144, NULL, 2, NULL);
xTaskCreate(watchdog_task, "watchdog", 2048, NULL, 5, NULL);
}方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
二进制环形缓冲 | 写入快、空间紧凑、掉电安全(顺序写) | 查询不便、需自定义工具 | 高频采样(1~5s) |
SQLite | SQL 查询、事务、成熟 | 写入开销大、Flash 磨损 | 低频采样(≥30s)、需本地查询 |
LittleFS/SPIFFS | 文件接口、磨损均衡 | 随机写性能差 | 配置存储、日志 |
混合:二进制 + SQLite 索引 | 兼顾写入速度和查询 | 实现复杂 | 专业记录仪 |
推荐:高频采样用二进制环形缓冲(顺序写 Flash),配合 SQLite 做索引和元数据。
Flash 分区(以 16MB SPI Flash 为例):
0x000000 ~ 0x00FFFF 引导区 + 配置(64KB)
0x010000 ~ 0x01FFFF 元数据区:环形头/尾指针、序列号、校准参数(64KB)
0x020000 ~ 0x7FFFFF 主数据区:约 8MB,每条 32 字节(约 26 万条)
0x800000 ~ 0xFFFFFF 备用/扩展区
每条记录 32 字节:
┌────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│ 时标 │ 温度 │ 湿度 │ 露点 │ 气压 │ 状态 │ CRC16 │
│ 4B │ 2B×10 │ 2B×10 │ 2B×10 │ 2B │ 1B │ 2B │
└────────┴────────┴────────┴────────┴────────┴────────┴────────┘/* storage.c - 存储核心逻辑 */
#include "esp_flash.h"
#include "crc16.h"
#define DATA_START 0x020000
#define DATA_END 0x800000
#define RECORD_SIZE 32
#define MAX_RECORDS ((DATA_END - DATA_START) / RECORD_SIZE)
typedef struct {
uint32_t timestamp;
int16_t temperature; /* ×10 */
int16_t humidity; /* ×10 */
int16_t dewpoint; /* ×10 */
int16_t pressure; /* ×0.1 hPa */
uint8_t status; /* bit0=sensor_ok, bit1=sd_card, bit2=cloud_connected */
uint16_t crc;
} record_t;
typedef struct {
uint32_t head; /* 最旧记录位置 */
uint32_t tail; /* 下次写入位置 */
uint32_t count; /* 当前记录数 */
uint32_t total_written;
uint32_t write_errors;
} storage_meta_t;
static storage_meta_t meta;
static SemaphoreHandle_t storage_mutex;
esp_err_t storage_write_record(const record_t *rec) {
xSemaphoreTake(storage_mutex, portMAX_DELAY);
record_t to_write = *rec;
to_write.crc = crc16((uint8_t*)&to_write, RECORD_SIZE - 2);
esp_err_t ret = esp_flash_write(NULL, &to_write,
DATA_START + meta.tail * RECORD_SIZE,
RECORD_SIZE);
if (ret != ESP_OK) {
meta.write_errors++;
xSemaphoreGive(storage_mutex);
return ret;
}
meta.tail = (meta.tail + 1) % MAX_RECORDS;
meta.count = (meta.count < MAX_RECORDS) ? meta.count + 1 : MAX_RECORDS;
if (meta.tail == meta.head && meta.count == MAX_RECORDS) {
meta.head = (meta.head + 1) % MAX_RECORDS; /* 覆盖最旧 */
}
meta.total_written++;
storage_save_meta(); /* 持久化元数据 */
xSemaphoreGive(storage_mutex);
return ESP_OK;
}
esp_err_t storage_read_range(uint32_t start_seq, uint32_t end_seq,
record_t *out, uint32_t *count) {
/* 按序列号范围读取 */
uint32_t idx = meta.head;
uint32_t found = 0;
for (uint32_t i = 0; i < meta.count && found < *count; i++) {
record_t rec;
esp_flash_read(NULL, &rec, DATA_START + idx * RECORD_SIZE, RECORD_SIZE);
if (rec.crc != crc16((uint8_t*)&rec, RECORD_SIZE - 2)) {
idx = (idx + 1) % MAX_RECORDS;
continue; /* 坏块跳过 */
}
uint32_t seq = rec.timestamp; /* 简化:用时间戳作序列 */
if (seq >= start_seq && seq <= end_seq) {
out[found++] = rec;
}
idx = (idx + 1) % MAX_RECORDS;
}
*count = found;
return ESP_OK;
}关键设计:
通道 | 协议 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
腾讯云 IoT Explorer | MQTT over TLS | 托管、规则引擎、小程序、告警 | 需联网注册设备、有费用 | 快速上云、多租户 |
腾讯云 IoT Hub | MQTT/CoAP | 更底层、自定义强 | 开发量大 | 工业级、私有协议 |
自建 InfluxDB + HTTPS | Line Protocol / JSON | 完全自主、无厂商锁定 | 需自建高可用 | 企业私有云 |
MQTT 公有 Broker | MQTT | 简单、开源生态 | 不可靠、无 SLA | 测试/非关键 |
双通道:云 + 本地服务器 | MQTT + HTTP | 冗余、断网本地存 | 复杂度高 | 关键机房 |
推荐:腾讯云 IoT Explorer(设备接入 + 规则引擎转发到时序数据库)+ 本地 HTTP 推送(可选),双通道冗余。
"""
cloud_uploader.py - 腾讯云 IoT Explorer MQTT 上云
运行在记录仪本地(Linux/Python)或作为边缘网关服务
"""
import paho.mqtt.client as mqtt
import ssl
import json
import time
import hmac
import hashlib
import random
from datetime import datetime
class TencentCloudIoT:
def __init__(self, product_id, device_name, device_secret, region="ap-guangzhou"):
self.product_id = product_id
self.device_name = device_name
self.device_secret = device_secret
self.region = region
self.broker = f"{product_id}.iotcloud.tencentdevices.com"
self.port = 8883
self.client = mqtt.Client(client_id=self._gen_client_id())
self._setup_tls()
self._setup_auth()
self.connected = False
self.pending_queue = [] # 未确认消息
def _gen_client_id(self):
# 格式: {product_id}{device_name}
return f"{self.product_id}{self.device_name}"
def _setup_tls(self):
self.client.tls_set(ca_certs="root_cert.pem",
cert_reqs=ssl.CERT_REQUIRED,
tls_version=ssl.PROTOCOL_TLSv1_2)
def _setup_auth(self):
# 用户名: {product_id}{device_name};{sdkappid};{connid};{expiry}
# 密码: 签名
username = f"{self.product_id}{self.device_name};12010126;12345;{int(time.time())+3600 * 24}"
password = self._sign(username)
self.client.username_pw_set(username, password)
def _sign(self, username):
# HMAC-SHA256(username, device_secret)
return hmac.new(
self.device_secret.encode(),
username.encode(),
hashlib.sha256
).hexdigest()
def on_connect(self, client, userdata, flags, rc):
if rc == 0:
self.connected = True
# 订阅属性下发
client.subscribe(f"$thing/down/property/{self.product_id}/{self.device_name}")
# 重发未确认消息
self._resend_pending()
else:
self.connected = False
def on_message(self, client, userdata, msg):
# 处理云端下发的属性设置/服务调用
payload = json.loads(msg.payload)
self._handle_downlink(payload)
def publish_telemetry(self, temp, hum, dewpoint, status):
if not self.connected:
self.pending_queue.append({
"temp": temp, "hum": hum, "dewpoint": dewpoint,
"status": status, "ts": time.time()
})
return False
payload = {
"method": "report",
"clientToken": f"{self.device_name}_{int(time.time()*1000)}",
"params": {
"temperature": temp,
"humidity": hum,
"dewpoint": dewpoint,
"status": status,
"timestamp": int(time.time())
}
}
topic = f"$thing/up/property/{self.product_id}/{self.device_name}"
result = self.client.publish(topic, json.dumps(payload), qos=1)
return result.rc == mqtt.MQTT_ERR_SUCCESS
def _resend_pending(self):
while self.pending_queue and self.connected:
item = self.pending_queue.pop(0)
self.publish_telemetry(item["temp"], item["hum"],
item["dewpoint"], item["status"])
def _handle_downlink(self, payload):
# 处理云端下发的设定值、重启指令等
if "params" in payload:
params = payload["params"]
if "set_temperature" in params:
# 写本地寄存器/触发动作
pass
if "reset" in params:
# 软重启
pass
def start(self):
self.client.on_connect = self.on_connect
self.client.on_message = self.on_message
self.client.connect(self.broker, self.port, keepalive=60)
self.client.loop_start()┌─────────────────────────────────────────────────────────────┐
│ 数据同步状态机 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 采样 → 写本地存储 → 尝试上云 │
│ │ │ │
│ │ ├── 成功 → 标记已同步 │
│ │ └── 失败 → 加入待同步队列 │
│ │ │
│ 网络恢复检测(每 30s) │
│ │ │
│ ├── 有网 → 从待同步队列取数据上云 │
│ │ ├── 成功 → 标记已同步,继续取 │
│ │ └── 失败 → 保持队列,等待重试 │
│ │ │
│ └── 无网 → 仅本地存储,等待网络恢复 │
│ │
│ 补传策略: │
│ - 按时间顺序补传,避免时序错乱 │
│ - 批量补传(每批 50 条),限速(每批间隔 1s) │
│ - 补传完成条件:队列空 或 追上实时 │
│ - 云端去重:消息携带 seq/timestamp,云端幂等 │
└─────────────────────────────────────────────────────────────┘"""
sync_manager.py - 本地存储与云端同步管理
"""
import asyncio
import time
from dataclasses import dataclass
@dataclass
class SyncState:
last_synced_seq: int = 0
last_sync_attempt: float = 0.0
sync_in_progress: bool = False
pending_count: int = 0
consecutive_failures: int = 0
class SyncManager:
def __init__(self, storage, cloud, batch_size=50, batch_interval=1.0):
self.storage = storage
self.cloud = cloud
self.batch_size = batch_size
self.batch_interval = batch_interval
self.state = SyncState()
async def sync_loop(self):
"""主同步循环"""
while True:
try:
if not self.cloud.connected:
await asyncio.sleep(5)
continue
if self.state.sync_in_progress:
await asyncio.sleep(1)
continue
# 查询未同步记录数
unsynced = self.storage.get_unsynced_count()
if unsynced == 0:
await asyncio.sleep(10)
continue
self.state.sync_in_progress = True
self.state.pending_count = unsynced
# 批量读取未同步记录
records = self.storage.read_unsynced(self.batch_size)
for rec in records:
success = self.cloud.publish_telemetry(
rec.temperature / 10.0,
rec.humidity / 10.0,
rec.dewpoint / 10.0,
rec.status
)
if success:
self.storage.mark_synced(rec.seq)
self.state.last_synced_seq = rec.seq
self.state.consecutive_failures = 0
else:
self.state.consecutive_failures += 1
break
self.state.sync_in_progress = False
self.state.last_sync_attempt = time.time()
# 限速
await asyncio.sleep(self.batch_interval)
except Exception as e:
print(f"sync error: {e}")
self.state.sync_in_progress = False
await asyncio.sleep(5)记录仪内置 Web 服务器,通过浏览器直接配置:
"""
web_server.py - 轻量 Web 配置界面(aiohttp)
"""
from aiohttp import web
import json
routes = web.RouteTableDef()
@routes.get('/api/status')
async def api_status(request):
"""系统状态"""
return web.json_response({
"uptime_s": get_uptime(),
"firmware": FIRMWARE_VERSION,
"storage": {
"total_records": storage.get_total_count(),
"unsynced": storage.get_unsynced_count(),
"health": storage.get_health()
},
"network": {
"ip": get_ip_address(),
"rssi": get_wifi_rssi(), # 如果有 WiFi
"cloud_connected": cloud.connected
},
"sensors": {
"temperature": current_reading.temp,
"humidity": current_reading.hum,
"last_sample_ts": current_reading.ts
}
})
@routes.post('/api/config/network')
async def api_config_network(request):
"""网络配置"""
data = await request.json()
# 验证参数
# 写入配置(持久化到 Flash)
# 触发网络重连
return web.json_response({"status": "ok"})
@routes.post('/api/config/cloud')
async def api_config_cloud(request):
"""云端配置"""
data = await request.json()
# 保存腾讯云 IoT 凭证
# 重启 MQTT 连接
return web.json_response({"status": "ok"})
@routes.post('/api/storage/backfill')
async def api_storage_backfill(request):
"""手动触发补传"""
asyncio.create_task(sync_manager.force_sync())
return web.json_response({"status": "started"})
@routes.get('/api/storage/export')
async def api_storage_export(request):
"""导出历史数据为 CSV"""
start = request.query.get('start')
end = request.query.get('end')
records = storage.read_time_range(start, end)
# 生成 CSV
csv_data = generate_csv(records)
return web.Response(text=csv_data, content_type='text/csv')
app = web.Application()
app.add_routes(routes)
# 静态文件(前端页面)
app.router.add_static('/', path='web_root', name='static')
web.run_app(app, host='0.0.0.0', port=80)前端用 Vue/React 或纯 HTML + fetch,轻量即可。
开发一款以太网环境记录仪,核心在三个设计决策:存储架构决定数据可靠性、上云通道决定可用性、Web 配置决定可运维性。本地存储用环形缓冲保证高频写入和掉电安全,上云用腾讯云 IoT Explorer 托管设备生命周期和规则引擎,Web 配置让现场零运维。三者配合,记录仪从"能采"变成"能用、好用、可靠用"。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。