文档
DL-20 CC2530 Zigbee模块 ESP32 Arduino驱动代码
本代码基于ESP32 Arduino框架,实现DL-20模块的UART驱动、遥测数据采集上报、门锁远程控制、ACK确认重传等完整功能,适用于宿舍物联网节点。
一、config.h — 节点配置文件
#ifndef CONFIG_H
#define CONFIG_H
#include <stdint.h>
namespace NodeConfig {
// ========== 节点身份 ==========
constexpr const char* NODE_ID = "NODE_101";
constexpr const char* DORM_NO = "DORM_A301";
constexpr const char* BUILDING_NO = "B";
constexpr const char* FLOOR_NO = "3";
constexpr const char* COLLEGE = "CS";
// ========== 引脚定义 ==========
constexpr int PIN_UART_RX = 16; // ESP32 GPIO16 → DL-20 TXD
constexpr int PIN_UART_TX = 17; // ESP32 GPIO17 → DL-20 RXD
constexpr int PIN_PIR = 4; // 人体红外传感器 (HC-SR501)
constexpr int PIN_MQ2 = 34; // 烟雾传感器ADC (MQ-2, ADC1_CH6)
constexpr int PIN_DHT = 5; // DHT11温湿度传感器
constexpr int PIN_LOCK_CTRL = 8; // 门锁继电器控制 (注意GPIO8上电状态!)
constexpr int PIN_LOCK_SENSOR = -1; // 门锁状态反馈 (本版本未使用)
// ========== 锁控制特性 ==========
constexpr bool LOCK_CTRL_ACTIVE_LOW = true; // LOW=开锁 (低电平触发继电器)
constexpr bool LOCK_SENSOR_ACTIVE_LOW = true; // 传感器低电平=锁住
// ========== 时序参数 (毫秒) ==========
constexpr unsigned long UART_LEADING_GAP_MS = 3; // 帧前导间隙
constexpr unsigned long UART_INTER_BYTE_DELAY_US = 100; // 字节间延迟(μs)
constexpr unsigned long UART_BAUD = 115200;
constexpr unsigned long POLL_INTERVAL_MS = 200; // 传感器轮询间隔
constexpr unsigned long REPORT_INTERVAL_MS = 60000; // 周期上报间隔(60s)
constexpr unsigned long MIN_UPLINK_GAP_MS = 500; // 最小上行间隔
constexpr unsigned long TELEMETRY_ACK_TIMEOUT_MS = 3000; // ACK等待超时
constexpr unsigned long RETRY_BASE_BACKOFF_MS = 2000; // 重试基础退避
constexpr unsigned long RETRY_RANDOM_JITTER_MS = 1000; // 重试随机抖动
constexpr uint8_t TELEMETRY_MAX_RETRIES = 3; // 最大重试次数
constexpr unsigned long EVENT_STAGGER_WINDOW_MS = 2000; // 事件交错窗口
constexpr unsigned long EVENT_RANDOM_JITTER_MS = 500; // 事件随机抖动
constexpr unsigned long PERIODIC_STAGGER_WINDOW_MS = 5000;// 周期上报交错窗口
constexpr unsigned long BOOT_STAGGER_BASE_MS = 3000; // 启动交错基础延时
// ========== 门锁安全 ==========
constexpr unsigned long BOOT_UNLOCK_GUARD_MS = 8000; // 启动解锁保护窗口
constexpr unsigned long MIN_UNLOCK_PULSE_MS = 500; // 最小解锁脉冲
constexpr unsigned long MAX_UNLOCK_PULSE_MS = 30000; // 最大解锁脉冲(30s)
constexpr unsigned long DEFAULT_UNLOCK_PULSE_MS = 5000; // 默认解锁脉冲(5s)
// ========== 传感器阈值 ==========
constexpr int SMOKE_EVENT_THRESHOLD = 500; // 烟雾ADC事件触发阈值
} // namespace NodeConfig
#endif // CONFIG_H
二、zigbee_node.ino — 主程序
#include <Arduino.h>
#include <ArduinoJson.h>
#include <DHT.h>
#include <driver/gpio.h>
#include <string.h>
#include "config.h"
/* ======================== 全局对象 ======================== */
HardwareSerial ZigbeeSerial(1); // UART1 → DL-20
DHT dht(NodeConfig::PIN_DHT, DHT11);
/* ======================== 遥测数据结构 ======================== */
struct Telemetry {
int human; // 人体检测 (0/1)
float temperature; // 温度 (°C)
float humidity; // 湿度 (%)
int smoke; // 烟雾ADC值
int door; // 门磁状态 (0/1)
int lock; // 锁状态 (0=锁住, 1=开锁)
uint32_t timestampMs; // 毫秒时间戳
};
/* ======================== 全局状态变量 ======================== */
String zigbeeRxLine;
Telemetry lastObserved{};
bool hasLastObserved = false;
float lastGoodTemperature = 25.0f;
float lastGoodHumidity = 50.0f;
unsigned long lastPollMs = 0;
// 门锁控制
bool lockIsUnlocked = false;
bool unlockPulseActive = false;
unsigned long unlockPulseUntilMs = 0;
// 遥测调度
Telemetry pendingEventTelemetry{};
bool hasPendingEventTelemetry = false;
unsigned long lastTelemetrySentMs = 0;
Telemetry scheduledTelemetry{};
String scheduledTelemetryTrigger;
bool hasScheduledTelemetry = false;
unsigned long scheduledTelemetryDueMs = 0;
// ACK确认机制
String awaitingAckPayload;
uint32_t awaitingAckSeq = 0;
uint8_t awaitingAckAttempts = 0;
unsigned long lastAckWaitStartedMs = 0;
unsigned long nextRetryAttemptMs = 0;
// 序列号与定时
uint32_t nextTelemetrySeq = 1;
unsigned long nextPeriodicReportMs = 0;
unsigned long bootTelemetryDueMs = 0;
bool bootTelemetryPending = true;
uint32_t nodeSlotOffsetMs = 0;
/* ======================== 工具函数 ======================== */
uint32_t hashNodeIdentity() {
uint32_t value = 2166136261u;
for (const char* p = NodeConfig::DORM_NO; *p; ++p) {
value ^= static_cast<uint8_t>(*p);
value *= 16777619u;
}
return value;
}
uint32_t boundedOffset(uint32_t windowMs) {
if (windowMs == 0) return 0;
return hashNodeIdentity() % windowMs;
}
uint32_t computeQueueDelayMs(const char* trigger) {
const unsigned long jitter =
random(static_cast<long>(NodeConfig::EVENT_RANDOM_JITTER_MS + 1));
if (strcmp(trigger, "boot") == 0) {
return NodeConfig::BOOT_STAGGER_BASE_MS + nodeSlotOffsetMs;
}
if (strcmp(trigger, "periodic") == 0) {
return boundedOffset(NodeConfig::EVENT_STAGGER_WINDOW_MS) + jitter;
}
return boundedOffset(NodeConfig::EVENT_STAGGER_WINDOW_MS) + jitter;
}
uint32_t computeRetryDelayMs() {
return NodeConfig::RETRY_BASE_BACKOFF_MS +
boundedOffset(NodeConfig::EVENT_STAGGER_WINDOW_MS) +
random(static_cast<long>(NodeConfig::RETRY_RANDOM_JITTER_MS + 1));
}
/* ======================== DL-20 UART发送 ======================== */
/**
* @brief 通过DL-20发送一行数据
* @note 帧前发送'\n'清空模块缓冲,字节间延迟100μs防止FIFO溢出,
* 帧尾发送'\n'作为帧结束标记
*/
void writeZigbeeLine(const char* payload) {
// 帧前导:发送换行符清空DL-20的UART缓冲
ZigbeeSerial.write('\n');
ZigbeeSerial.flush();
delay(NodeConfig::UART_LEADING_GAP_MS);
// 逐字节发送payload,字节间加延迟
for (const char* p = payload; *p; ++p) {
ZigbeeSerial.write(static_cast<uint8_t>(*p));
ZigbeeSerial.flush();
delayMicroseconds(NodeConfig::UART_INTER_BYTE_DELAY_US);
}
// 帧尾换行符
ZigbeeSerial.write('\n');
ZigbeeSerial.flush();
}
/* ======================== 传感器读取 ======================== */
Telemetry readTelemetry(unsigned long nowMs) {
Telemetry telemetry{};
telemetry.human = digitalRead(NodeConfig::PIN_PIR) == HIGH ? 1 : 0;
telemetry.smoke = analogRead(NodeConfig::PIN_MQ2);
telemetry.door = 0; // 本硬件版本未接门磁
// DHT11读取(含容错)
const float humidity = dht.readHumidity();
const float temperature = dht.readTemperature();
if (isnan(humidity) || isnan(temperature)) {
telemetry.temperature = lastGoodTemperature;
telemetry.humidity = lastGoodHumidity;
} else {
telemetry.temperature = temperature;
telemetry.humidity = humidity;
lastGoodTemperature = temperature;
lastGoodHumidity = humidity;
}
// 锁状态:由软件状态推断(无独立传感器时)
telemetry.lock = lockIsUnlocked ? 0 : 1;
telemetry.timestampMs = nowMs;
return telemetry;
}
/* ======================== 事件判断 ======================== */
bool shouldReportEvent(const Telemetry& current) {
if (!hasLastObserved) return true;
const bool lockChanged = current.lock != lastObserved.lock;
const bool humanDetected = lastObserved.human == 0 && current.human == 1;
const bool smokeThresholdCrossed =
lastObserved.smoke < NodeConfig::SMOKE_EVENT_THRESHOLD &&
current.smoke >= NodeConfig::SMOKE_EVENT_THRESHOLD;
return lockChanged || humanDetected || smokeThresholdCrossed;
}
/* ======================== JSON载荷构造 ======================== */
String buildTelemetryPayload(const Telemetry& telemetry, const char* trigger, uint32_t seq) {
StaticJsonDocument<256> doc;
doc["v"] = 1;
doc["n"] = NodeConfig::NODE_ID;
doc["d"] = NodeConfig::DORM_NO;
doc["b"] = NodeConfig::BUILDING_NO;
doc["f"] = NodeConfig::FLOOR_NO;
doc["c"] = NodeConfig::COLLEGE;
doc["h"] = telemetry.human;
doc["t"] = roundf(telemetry.temperature * 10.0f) / 10.0f;
doc["u"] = roundf(telemetry.humidity * 10.0f) / 10.0f;
doc["s"] = telemetry.smoke;
doc["o"] = telemetry.door;
doc["l"] = telemetry.lock;
doc["m"] = telemetry.timestampMs;
doc["g"] = trigger;
doc["q"] = seq;
char payload[256];
serializeJson(doc, payload, sizeof(payload));
return String(payload);
}
/* ======================== 遥测发送与ACK ======================== */
void sendTelemetryPayload(const String& payload, uint32_t seq, bool isRetry) {
writeZigbeeLine(payload.c_str());
lastTelemetrySentMs = millis();
lastAckWaitStartedMs = lastTelemetrySentMs;
awaitingAckSeq = seq;
awaitingAckPayload = payload;
++awaitingAckAttempts;
nextRetryAttemptMs = 0;
Serial.print(isRetry ? "[TX-RETRY] " : "[TX] ");
Serial.println(payload);
}
void queueTelemetry(const Telemetry& telemetry, const char* trigger) {
scheduledTelemetry = telemetry;
scheduledTelemetryTrigger = trigger;
hasScheduledTelemetry = true;
scheduledTelemetryDueMs = millis() + computeQueueDelayMs(trigger);
}
void emitCurrentTelemetry(const char* trigger) {
const unsigned long nowMs = millis();
const Telemetry current = readTelemetry(nowMs);
queueTelemetry(current, trigger);
lastObserved = current;
hasLastObserved = true;
hasPendingEventTelemetry = false;
}
/* ======================== ACK超时与重传 ======================== */
void tickTelemetryAck() {
if (awaitingAckSeq == 0) return;
const unsigned long nowMs = millis();
// 等待重试时间到达
if (nextRetryAttemptMs != 0) {
if (static_cast<long>(nowMs - nextRetryAttemptMs) < 0) return;
sendTelemetryPayload(awaitingAckPayload, awaitingAckSeq, true);
return;
}
// 检查ACK超时
if (nowMs - lastAckWaitStartedMs < NodeConfig::TELEMETRY_ACK_TIMEOUT_MS) return;
// 超过最大重试次数,丢包
if (awaitingAckAttempts >= NodeConfig::TELEMETRY_MAX_RETRIES) {
Serial.printf("ACK timeout, drop seq=%lu after %u attempt(s)\n",
static_cast<unsigned long>(awaitingAckSeq),
static_cast<unsigned int>(awaitingAckAttempts));
awaitingAckSeq = 0;
awaitingAckPayload = "";
awaitingAckAttempts = 0;
nextRetryAttemptMs = 0;
return;
}
// 调度重试
nextRetryAttemptMs = nowMs + computeRetryDelayMs();
Serial.printf("ACK wait expired, retry seq=%lu scheduled in %lu ms\n",
static_cast<unsigned long>(awaitingAckSeq),
static_cast<unsigned long>(nextRetryAttemptMs - nowMs));
}
void flushScheduledTelemetry() {
if (!hasScheduledTelemetry || awaitingAckSeq != 0) return;
const unsigned long nowMs = millis();
if (static_cast<long>(nowMs - scheduledTelemetryDueMs) < 0) return;
if (nowMs - lastTelemetrySentMs < NodeConfig::MIN_UPLINK_GAP_MS) return;
const uint32_t seq = nextTelemetrySeq++;
const String payload =
buildTelemetryPayload(scheduledTelemetry, scheduledTelemetryTrigger.c_str(), seq);
awaitingAckAttempts = 0;
sendTelemetryPayload(payload, seq, false);
hasScheduledTelemetry = false;
}
/* ======================== DL-20接收:下行命令处理 ======================== */
bool commandTargetsThisNode(JsonObjectConst command) {
const char* targetDorm = command["dormNo"] | "";
if (targetDorm[0] != '\0' && strcmp(targetDorm, NodeConfig::DORM_NO) != 0) return false;
const char* targetNode = command["nodeId"] | "";
if (targetNode[0] != '\0' && strcmp(targetNode, NodeConfig::NODE_ID) != 0) return false;
return true;
}
void applyLockCommand(JsonObjectConst command) {
const char* action = command["action"] | "";
if (action[0] == '\0') action = command["cmd"] | "";
if (strcmp(action, "unlock") == 0 || strcmp(action, "pulse_unlock") == 0) {
const unsigned long nowMs = millis();
// 启动守卫:上电后N秒内忽略解锁指令
if (nowMs < NodeConfig::BOOT_UNLOCK_GUARD_MS) {
Serial.printf("Ignore unlock during boot guard: %lu/%lu ms\n",
static_cast<unsigned long>(nowMs),
static_cast<unsigned long>(NodeConfig::BOOT_UNLOCK_GUARD_MS));
emitCurrentTelemetry("lock_boot_guard");
return;
}
const int requestedMs =
command["durationMs"] | static_cast<int>(NodeConfig::DEFAULT_UNLOCK_PULSE_MS);
uint32_t unlockMs = requestedMs;
if (unlockMs < NodeConfig::MIN_UNLOCK_PULSE_MS) unlockMs = NodeConfig::MIN_UNLOCK_PULSE_MS;
if (unlockMs > NodeConfig::MAX_UNLOCK_PULSE_MS) unlockMs = NodeConfig::MAX_UNLOCK_PULSE_MS;
digitalWrite(NodeConfig::PIN_LOCK_CTRL,
NodeConfig::LOCK_CTRL_ACTIVE_LOW ? LOW : HIGH);
unlockPulseActive = true;
unlockPulseUntilMs = millis() + unlockMs;
lockIsUnlocked = true;
Serial.printf("Unlock command accepted, duration=%lu ms\n", static_cast<unsigned long>(unlockMs));
emitCurrentTelemetry("lock_unlock_cmd");
return;
}
if (strcmp(action, "lock") == 0) {
unlockPulseActive = false;
digitalWrite(NodeConfig::PIN_LOCK_CTRL,
NodeConfig::LOCK_CTRL_ACTIVE_LOW ? HIGH : LOW);
lockIsUnlocked = false;
Serial.println("Lock command accepted.");
emitCurrentTelemetry("lock_lock_cmd");
return;
}
Serial.print("Unsupported lock action: ");
Serial.println(action);
}
void handleIncomingCommandLine(String line) {
line.trim();
if (line.isEmpty()) return;
StaticJsonDocument<384> doc;
const DeserializationError error = deserializeJson(doc, line);
if (error) {
Serial.print("Drop invalid command JSON: ");
Serial.println(line);
return;
}
JsonObjectConst command = doc.as<JsonObjectConst>();
const char* type = command["type"] | "";
// 处理ACK
if (strcmp(type, "ack") == 0) {
const char* nodeId = command["nodeId"] | "";
if (nodeId[0] != '\0' && strcmp(nodeId, NodeConfig::NODE_ID) != 0) return;
const uint32_t ackSeq = command["seq"] | 0;
if (ackSeq != 0 && awaitingAckSeq == ackSeq) {
Serial.printf("ACK received for seq=%lu after %u attempt(s)\n",
static_cast<unsigned long>(ackSeq),
static_cast<unsigned int>(awaitingAckAttempts));
awaitingAckSeq = 0;
awaitingAckPayload = "";
awaitingAckAttempts = 0;
nextRetryAttemptMs = 0;
}
return;
}
// 处理锁控制命令
if (strcmp(type, "lock_control") != 0) return;
if (!commandTargetsThisNode(command)) return;
applyLockCommand(command);
}
void consumeZigbeeCommands() {
while (ZigbeeSerial.available() > 0) {
const char c = static_cast<char>(ZigbeeSerial.read());
if (c == '\n') {
handleIncomingCommandLine(zigbeeRxLine);
zigbeeRxLine = "";
continue;
}
if (c == '\r') continue;
if (zigbeeRxLine.length() < 350) {
zigbeeRxLine += c;
} else {
zigbeeRxLine = ""; // 超长保护
}
}
}
/* ======================== 解锁脉冲定时器 ======================== */
void tickUnlockPulse(unsigned long nowMs) {
if (!unlockPulseActive) return;
if (static_cast<long>(nowMs - unlockPulseUntilMs) < 0) return;
unlockPulseActive = false;
digitalWrite(NodeConfig::PIN_LOCK_CTRL,
NodeConfig::LOCK_CTRL_ACTIVE_LOW ? HIGH : LOW);
lockIsUnlocked = false;
Serial.println("Unlock pulse expired, lock restored.");
emitCurrentTelemetry("lock_auto_relock");
}
/* ======================== GPIO安全初始化 ======================== */
/**
* @brief 安全初始化锁控制引脚
* @note ESP32上电时GPIO可能短暂输出不确定电平。若继电器为低电平触发,
* 必须确保在切换为OUTPUT之前,输出寄存器已设为高电平(锁住状态)。
* 本函数通过三步操作确保万无一失:
* 1. digitalWrite设置输出寄存器 → 2. INPUT_PULLUP做短暂保持 →
* 3. 切换OUTPUT → 4. 再次digitalWrite加固
*/
void setupLockPinSafe() {
const int inactiveLevel = NodeConfig::LOCK_CTRL_ACTIVE_LOW ? HIGH : LOW;
const int pin = NodeConfig::PIN_LOCK_CTRL;
// Step 1: 预设输出寄存器(此时还是INPUT模式,但digitalWrite可设置输出锁存器)
digitalWrite(pin, inactiveLevel);
// Step 2: 短暂启用内部上拉/下拉作为保持
if (inactiveLevel == HIGH) {
pinMode(pin, INPUT_PULLUP);
} else {
pinMode(pin, INPUT_PULLDOWN);
}
delay(1);
// Step 3: 切换为OUTPUT(锁存器已预设,切换瞬间不会产生脉冲)
pinMode(pin, OUTPUT);
// Step 4: 再次强化电平
digitalWrite(pin, inactiveLevel);
lockIsUnlocked = false;
}
/* ======================== setup() ======================== */
void setup() {
// 第一步:尽早设置锁输出为安全状态
setupLockPinSafe();
// 初始化其他GPIO
pinMode(NodeConfig::PIN_PIR, INPUT);
Serial.begin(115200);
delay(200);
Serial.println();
Serial.println("Dormitory node booting...");
Serial.printf("Lock config: pin=%d activeLow=%d bootGuard=%lu ms\n",
NodeConfig::PIN_LOCK_CTRL,
NodeConfig::LOCK_CTRL_ACTIVE_LOW ? 1 : 0,
static_cast<unsigned long>(NodeConfig::BOOT_UNLOCK_GUARD_MS));
// 初始化DL-20 Zigbee串口
ZigbeeSerial.begin(
NodeConfig::UART_BAUD,
SERIAL_8N1,
NodeConfig::PIN_UART_RX,
NodeConfig::PIN_UART_TX);
analogReadResolution(12); // ESP32 ADC 12bit
dht.begin();
// 随机种子:节点身份哈希 + 微秒计数器
randomSeed(hashNodeIdentity() ^ micros());
// 计算时隙偏移
lastPollMs = millis();
nodeSlotOffsetMs = boundedOffset(NodeConfig::PERIODIC_STAGGER_WINDOW_MS);
bootTelemetryDueMs = millis() + NodeConfig::BOOT_STAGGER_BASE_MS + nodeSlotOffsetMs;
nextPeriodicReportMs = millis() + NodeConfig::REPORT_INTERVAL_MS + nodeSlotOffsetMs;
}
/* ======================== loop() ======================== */
void loop() {
const unsigned long now = millis();
// 1. 处理DL-20接收的下行命令
consumeZigbeeCommands();
// 2. ACK超时检测与重传
tickTelemetryAck();
// 3. 发送已调度的遥测数据
flushScheduledTelemetry();
// 4. 解锁脉冲超时检测
tickUnlockPulse(now);
// 5. 启动遥测上报
if (bootTelemetryPending && static_cast<long>(now - bootTelemetryDueMs) >= 0) {
bootTelemetryPending = false;
emitCurrentTelemetry("boot");
}
// 6. 传感器轮询
if (now - lastPollMs >= NodeConfig::POLL_INTERVAL_MS) {
lastPollMs = now;
const Telemetry current = readTelemetry(now);
if (shouldReportEvent(current)) {
pendingEventTelemetry = current;
hasPendingEventTelemetry = true;
}
lastObserved = current;
hasLastObserved = true;
}
// 7. 事件遥测排队
if (hasPendingEventTelemetry &&
now - lastTelemetrySentMs >= NodeConfig::MIN_UPLINK_GAP_MS &&
awaitingAckSeq == 0) {
queueTelemetry(pendingEventTelemetry, "event");
hasPendingEventTelemetry = false;
}
// 8. 周期上报
if (static_cast<long>(now - nextPeriodicReportMs) >= 0) {
nextPeriodicReportMs += NodeConfig::REPORT_INTERVAL_MS;
const Telemetry current = readTelemetry(now);
queueTelemetry(current, "periodic");
lastObserved = current;
hasLastObserved = true;
hasPendingEventTelemetry = false;
}
delay(10);
}
三、关键代码解析
3.1 DL-20 UART发送时序
writeZigbeeLine("payload")
│
├─ '\n' (前导换行,清空DL-20缓冲)
├─ delay(3ms) (帧前导间隙)
├─ for each byte:
│ ├─ ZigbeeSerial.write(byte)
│ ├─ ZigbeeSerial.flush()
│ └─ delayMicroseconds(100) (字节间延迟)
└─ '\n' (帧尾换行)
为什么需要字节间延迟? DL-20的CC2530在接收UART数据时,其8051内核需要在字节间处理RF相关中断。若连续无间隔发送,UART FIFO可能溢出导致数据错乱。
3.2 ACK确认与重传状态机
发送遥测 → 等待ACK (3s超时)
│
├─ ACK到达 ──→ 完成,释放序列号
│
└─ 超时 ──→ 重试次数 < 3 ?
│
├─ 是 → 计算退避延迟 → 重发
└─ 否 → 丢弃,释放序列号
3.3 节点时隙交错(防冲突)
多个节点同时上电/周期上报会导致Zigbee网络拥塞。通过boundedOffset()基于节点身份哈希计算不同的上报偏移:
bootTelemetryDueMs = millis() + BOOT_STAGGER_BASE_MS + hash(dormNo) % PERIODIC_STAGGER_WINDOW_MS
这样A301节点的上报时刻和B201节点自然错开。
3.4 锁控制上电安全
setupLockPinSafe():
1. digitalWrite(pin, HIGH) ← 预设输出寄存器为安全电平
2. pinMode(pin, INPUT_PULLUP) ← 短暂启用上拉保持
3. delay(1ms)
4. pinMode(pin, OUTPUT) ← 切换为输出(无脉冲)
5. digitalWrite(pin, HIGH) ← 再次强化
这对于低电平触发的继电器模块至关重要——ESP32 GPIO8上电时有内部弱上拉,但不够强。若直接pinMode(OUTPUT),在切换瞬间可能短暂输出低电平导致误开锁。
四、网关端ACK回复示例
协调器/网关收到遥测帧后,应回复ACK:
{"type":"ack","nodeId":"NODE_101","seq":42}
通过DL-20发送到对应节点即可完成确认。