Skip to content

ld2410.tc

HLK-LD2410 24GHz mmWave Human Presence Sensor

Source on GitHub

// HLK-LD2410 24GHz mmWave Human Presence Sensor
// Serial: 256000 baud 8N1, continuous reporting every ~50ms
// Default wiring: ESP RX=GPIO8 ← LD2410 TX
//                 ESP TX=GPIO7 → LD2410 RX
//                 VCC=3.3V, GND=GND
//
// Reports: target state (none/moving/stationary/both),
//          moving + stationary distance (cm) and energy (0-100),
//          overall detection distance (cm)
//
// Frame format (normal mode, 23 bytes):
//   Header:  F4 F3 F2 F1
//   Length:  0D 00 (13 bytes)
//   Data:    02 AA state move_lo move_hi move_e
//            stat_lo stat_hi stat_e det_lo det_hi 55 00
//   Footer:  F8 F7 F6 F5

#define LD_BAUD 256000
#define LD_SBUF 256

int ld_rxpin = 8;
int ld_txpin = 7;
int ld_serial = 0;
int ld_ok = 0;

// parsed sensor values
int ld_state = 0;       // 0=none 1=moving 2=stationary 3=both
int ld_mdist = 0;       // moving target distance (cm)
int ld_menrg = 0;       // moving target energy (0-100)
int ld_sdist = 0;       // stationary target distance (cm)
int ld_senrg = 0;       // stationary target energy (0-100)
int ld_dist = 0;        // detection distance (cm)

// frame parser state machine
char ld_buf[64];
int ld_fsm = 0;
int ld_idx = 0;
int ld_dlen = 0;

// web label
char ld_lbl[32];

// ---- frame decoder ----
void ld_decode() {
    // buf[0..1]=length, [2]=type, [3]=0xAA head
    // [4]=state, [5..6]=move_dist, [7]=move_energy
    // [8..9]=stat_dist, [10]=stat_energy, [11..12]=det_dist
    if (ld_buf[3] != 0xAA) return;
    ld_state = ld_buf[4] & 0x03;
    ld_mdist = (ld_buf[5] & 0xFF) | ((ld_buf[6] & 0xFF) << 8);
    ld_menrg = ld_buf[7] & 0xFF;
    ld_sdist = (ld_buf[8] & 0xFF) | ((ld_buf[9] & 0xFF) << 8);
    ld_senrg = ld_buf[10] & 0xFF;
    ld_dist  = (ld_buf[11] & 0xFF) | ((ld_buf[12] & 0xFF) << 8);
    ld_ok = 1;
}

// ---- serial reader with header sync ----
void ld_read() {
    while (serialAvailable() > 0) {
        int b = serialRead();
        if (b < 0) break;

        // find header F4 F3 F2 F1
        if (ld_fsm == 0) {
            if (b == 0xF4) ld_fsm = 1;
        } else if (ld_fsm == 1) {
            if (b == 0xF3) {
                ld_fsm = 2;
            } else if (b != 0xF4) {
                ld_fsm = 0;
            }
        } else if (ld_fsm == 2) {
            if (b == 0xF2) {
                ld_fsm = 3;
            } else if (b == 0xF4) {
                ld_fsm = 1;
            } else {
                ld_fsm = 0;
            }
        } else if (ld_fsm == 3) {
            if (b == 0xF1) {
                ld_fsm = 4;
                ld_idx = 0;
                ld_dlen = 0;
            } else if (b == 0xF4) {
                ld_fsm = 1;
            } else {
                ld_fsm = 0;
            }
        } else {
            // reading frame payload
            ld_buf[ld_idx] = b;
            ld_idx = ld_idx + 1;

            // after 2 bytes we know the data length
            if (ld_idx == 2) {
                ld_dlen = (ld_buf[0] & 0xFF) | ((ld_buf[1] & 0xFF) << 8);
                if (ld_dlen < 10 || ld_dlen > 50) {
                    ld_fsm = 0;
                }
            }

            // complete frame: 2(len) + dlen(data) + 4(footer)
            if (ld_idx >= 2 && ld_idx == ld_dlen + 6) {
                // verify footer F8 F7 F6 F5
                int fi = ld_dlen + 2;
                if ((ld_buf[fi] & 0xFF) == 0xF8 &&
                    (ld_buf[fi + 1] & 0xFF) == 0xF7 &&
                    (ld_buf[fi + 2] & 0xFF) == 0xF6 &&
                    (ld_buf[fi + 3] & 0xFF) == 0xF5) {
                    ld_decode();
                }
                ld_fsm = 0;
            }

            // buffer overflow guard
            if (ld_idx >= 60) {
                ld_fsm = 0;
            }
        }
    }
}

// ---- callbacks ----

void EverySecond() {
    if (ld_serial) ld_read();
}

void WebCall() {
    char vt[48];
    if (!ld_serial) {
        webSend("{s}LD2410{m}serial error{e}");
        return;
    }
    if (!ld_ok) {
        webSend("{s}LD2410{m}no data{e}");
        return;
    }

    // presence state
    webSend("{s}LD2410 Presence{m}");
    if (ld_state == 0) {
        webSend("None{e}");
    } else if (ld_state == 1) {
        webSend("Moving{e}");
    } else if (ld_state == 2) {
        webSend("Stationary{e}");
    } else {
        webSend("Mov+Stat{e}");
    }

    // moving target: distance / energy
    sprintf(vt, "{s}LD2410 Moving{m}%d cm / ", ld_mdist);
    webSend(vt);
    sprintf(vt, "%d %{e}", ld_menrg);
    webSend(vt);

    // stationary target: distance / energy
    sprintf(vt, "{s}LD2410 Static{m}%d cm / ", ld_sdist);
    webSend(vt);
    sprintf(vt, "%d %{e}", ld_senrg);
    webSend(vt);

    // detection distance (localized label)
    LGetString(16, ld_lbl);
    strcpy(vt, "{s}LD2410 ");
    strcat(vt, ld_lbl);
    strcat(vt, "{m}");
    webSend(vt);
    sprintf(vt, "%d cm{e}", ld_dist);
    webSend(vt);
}

void JsonCall() {
    if (!ld_ok) return;
    char buf[64];
    responseAppend(",\"LD2410\":{");
    sprintf(buf, "\"State\":%d", ld_state);
    responseAppend(buf);
    sprintf(buf, ",\"MovingDist\":%d", ld_mdist);
    responseAppend(buf);
    sprintf(buf, ",\"MovingEnergy\":%d", ld_menrg);
    responseAppend(buf);
    sprintf(buf, ",\"StaticDist\":%d", ld_sdist);
    responseAppend(buf);
    sprintf(buf, ",\"StaticEnergy\":%d", ld_senrg);
    responseAppend(buf);
    sprintf(buf, ",\"Distance\":%d}", ld_dist);
    responseAppend(buf);
}

void Command(char cmd[]) {
    char buf[96];
    if (!ld_ok) {
        responseCmnd("no data");
        return;
    }
    sprintf(buf, "State=%d", ld_state);
    responseCmnd(buf);
    sprintf(buf, " MovDist=%d", ld_mdist);
    responseAppend(buf);
    sprintf(buf, " MovE=%d", ld_menrg);
    responseAppend(buf);
    sprintf(buf, " StatDist=%d", ld_sdist);
    responseAppend(buf);
    sprintf(buf, " StatE=%d", ld_senrg);
    responseAppend(buf);
    sprintf(buf, " Dist=%d", ld_dist);
    responseAppend(buf);
}

void WebUI() {
    webPulldown(ld_rxpin, "RX Pin", "@getfreepins");
    webPulldown(ld_txpin, "TX Pin", "@getfreepins");
}

void OnExit() {
    serialClose();
}

int main() {
    delay(10000);

    // restore persisted pin config
    webPulldown(ld_rxpin, "RX Pin", "@getfreepins");
    webPulldown(ld_txpin, "TX Pin", "@getfreepins");

    int ret = serialBegin(ld_rxpin, ld_txpin, LD_BAUD, 3, LD_SBUF);
    if (ret == 1) {
        ld_serial = 1;
    }

    addCommand("Radar");

    return 0;
}