DL-20 CC2530 UART Zigbee模块

元器件
通信模块
库存 200

介绍

DL-20是基于TI CC2530方案的UART串口Zigbee透传模块,支持点对点/点对多点/广播通信,工作在2.4GHz ISM频段,基于Zigbee/Z-Stack协议。最大传输距离空旷约250米,UART波特率默认115200 bps 8N1,TTL 3.3V电平。适用于宿舍物联网、智能家居传感器网络等低功耗无线数据采集场景,支持AT指令配置和透明传输模式。

规格参数

参数
PAN ID可配置
信道11-26 (16个信道)
尺寸约25mm x 18mm
波特率默认115200 bps, 8N1
UART接口TXD/RXD, TTL 3.3V
休眠电流<1μA (PM2模式)
发射功率最大+4.5dBm (可配置)
发射电流约28mA @+4.5dBm
天线接口PCB板载天线 / IPEX可选
工作温度-40°C ~ +85°C
工作电压2.0V-3.6V (典型3.3V)
工作频段2.4GHz ISM (2405-2480MHz)
接收电流约24mA
网络拓扑点对点/点对多点/广播
芯片方案TI CC2530
通信协议Zigbee / Z-Stack
通信距离空旷约250米
配置方式AT指令
接收灵敏度-97dBm

代码例程

DL-20 CC2530 Zigbee模块 ESP32 Arduino驱动代码.md

# DL-20 CC2530 Zigbee模块 ESP32 Arduino驱动代码

> 本代码基于ESP32 Arduino框架,实现DL-20模块的UART驱动、遥测数据采集上报、门锁远程控制、ACK确认重传等完整功能,适用于宿舍物联网节点。

---

## 一、config.h — 节点配置文件

```cpp
#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 — 主程序

```cpp
#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:

```json
{"type":"ack","nodeId":"NODE_101","seq":42}
```

通过DL-20发送到对应节点即可完成确认。

参考资料

暂无参考文献