mmWave Wars: one sensor (module) to rule them all

TL;DR

This project isn’t done. It’s not a review. It’s not a how-to. It compares what mmWave sensor modules that I have on hand (and actually work).

Intro

The original goal was for this project was a number of tight battles, a close finish between DIY mmWave sensor modules. There are a lot of them out there, so the opening gauntlet had to be challenging. Yet few contenders actually qualify. This might not be a fair fight…

Contenders

  • DIY/ESPHome contenders only.
  • presence sensors only (aka “static motion” or “breathing” type)
  • a useful static/presence detection range ( <4M isn’t all that useful ) motion range is not what we are testing

A few thoughts on contenders

Coming from the world of PIR, where walking or waving your arms will trigger them. We are not considering mmWave sensors that work in this fashion. Many differentiate between this type of motion (walking/waving) and micro-motion (standing still/sitting/sleeping). We will be looking at the micro-kinesis aspect here and delving deeper into the differences of standing still, sitting still and sleeping (still :stuck_out_tongue: ).

It is important to understand that many sensor manufacturers advertise a combined detection distance, where micro-motion is mere fraction of can be “seen” by the sensor.

MicRadar do a very good job at depicting this;

The Heavyweight: DFRobot / Leapmmw

Luck of the draw has this sensor being the favorite. With four deployed in production 6+ months; this is the module to dethrone.

Benefits include commonality across three sensors for varying FOV with clear and accurate documentation.

Constraints include price and lack of support from the ODM (Leapmmw).

The Wildcard: Hi-Link

Are these the sensor modules of your dreams? Inexpensive and readily available on AliExpress?

These sensors vary wildly in capability and all suffer from extremely poor documentation. Said documentation is only provided upon request from Hi-Link; a suspicious start. And don’t count on it being particularly readable or accurate. They have confirmed errata based on my testing feedback do other people actually use these??.

LD1115H

This model appears at first to be analogous to the DFRobot. But appearances are skin deep. The lack of any distance configuration parameters will hamper this sensor.

At least is has the typical 2.54mm header spacing.

LD2410

The smallest form factor makes this an interesting sensor module. A narrow FOV also makes this useful for targeted applications. Along with distance configuration parameters, we may have a contender here. But with 1.27mm header spacing, good luck soldering!

LD1125H

This sensor arrived DOA and HLK failed to provide useful support. This is unfortunate because it might have been a better alternative to the LD1115H.

Failed to Qualify

SeeedStudio 24Ghz Human Presence Sensor.

This sensor suffers from extreme sensitivity. I have experienced it detecting motion 50+ feet away and directionality cannot be controlled without a metal box. In order to qualify, a sensor needs to report no motion detection in scenarios where people might be present in other rooms. The SeeedStudio fails this benchmark.

Failed to Consider

Any sensor with <4M presence/static* detection range*check the datasheets folks…

  • SeeedStudio 60Ghz Respiratory Heartbeat or Fall Detection sensors.
  • MicRadar R60AFDx

Qualifying Gauntlet

In order to qualify for the mmWave Wars, each sensor must prove that it can, not only be sensitive, but reliable. This will be demonstrated using the hardest test for a presence sensor; sleeping.

Why Sleeping?

All presence sensors that rely upon physical movement (PIR/mmWave) must have a target to track. Sleeping generally infers an obscured target with minimal movement. In order to detect this, a sensor must be exceptionally sensitive. Yet being too sensitive means it might not “turn off”. Hence the engineering challenge for any manufacturer.

Some ODM’s might opt for a narrow FOV/distance (looking at you 60Ghz SeeedStudio). Others might choose to refine their detection FFT algorithms. We are here to determine who have performed the latter and to what degree they work in this use-case.

Qualifying Test Setup

LD1115H, LD2410 & DFRobot mounted on a shelf facing the bed. 16" from the foot of the bed and 8’ to the head of the bed.

None of the sensors pass this test with stock settings. The following were used;

DFRobot Qualifying Settings

Minimal settings change necessary. Increase in default sensitivity from 7 to 8.

Since this sensor controls WLED lighting, distance has been increased since it has been determined that its placement will catch you arriving into the room (45° angle from placement). Bonus point goes to the DFRobot.

LD2410 Qualifying Settings

This sensor is much more intuitive to tune as there is a direct correlation between TargetEnergy and the sensitivity_threshold. They are the same value. Customized code deployed for a simplified configuration and report of presence (below).


Testing is currently down to single digit % changes in the hopes of finding the sweet-spot.

LD1115H Qualifying Settings

This one gets a little complicated. The LD1115H does not report distance, yet the th3 parameter is “long distance” according to HLK. I have set that value to very low sensitivity as this sensor can otherwise see you coming from a long way away.

It would appear that it triggers presence based on the mov_SNR_target which is also very sensitive. So this setting is configured for reduced sensitivity as false positives are still present. More tuning needed.

Lastly there is the occ_SNR_target which maintains presence on micro motion detection. Testing is slow, I only sleep once a day!


Customized code deployed for a simplified configuration and report of presence (below).

Qualifying Results (to date)

Test #1 - presence while sleeping

  • A pass should be a continuous report of presence.

Test #2 - no presence while away (I walked in three times)

  • A pass should be no false positives when the room is empty

As you can see, the LD2410 & LD1115 are not quite there yet with the LD2410 being much closer to the target.

The LD1115 has been particularly challenging since the sensitivity is already so low that going below 50 appears to be going below the “noise floor” for the deployment. Therefore occ_sn settings are still being tested as these are not well documented.

Let the Wars Begin?

Not so fast. In the process of getting the various sensors to work, it came to light that they all worked in different ways.

Unlike a PIR where you typically have sensitivity and delay configuration parameters, these sensors may have far more options and they all operate in disparate manners.

For some sensor modules, these configuration parameters are clear-cut. The DFRobot/Leapmmw are this way. You get three knobs; distance, latency and sensitivity. These three knobs control the GPIO detection. That’s it.

For others, you might have a sole sensitivity option that does little; “Seeeeeeeeed

And then there is Hi-Link. Not only does each sensor have a variety of settings, many are not documented. And each different module operates them in different ways. There is no contenuity in the product lineup much less in how they report movement and static presence as separate thingswhy do I want this? It either sees something or it doesn’t. Am I right…!?.

I have taken existing code and simplified it in order to present a more uniform interface to the sensor module. It detects or it doesn’t. While maintaining global configuration parameters.

Considering that a mere two-wire UART connection can confuse people I believe the global configuration with a single reporting mechanism for movement is best for the testing moving forward.

Let the Wars Begin

TODO: the rest of the post isn’t written yet… …I need a solid third contender first
I see no point in testing unreliable sensor modules. So once we find the top three, we shall see how they hold up to their marketing in the following categories;

Methodology

The competition will;

  • be in a clean environment with no fans, not cats, no people.
  • test default settings to start.
  • tweak settings when they start to fail a test.
  • have different sized targets
  • that will be tested at different distances
  • use a precise servo to move them at varying degrees of movement.

Targets

My thoughts to dates on targets are;

  • a head sized target (aka you are in bed with covers up to chin)
  • a “obscured torso” sized target (aka, you are at a desk)
  • an adult sitting sized target
  • an adjust standing sized target

Hypothesis

People assume that that max range listed by the manufacturer means that it has to detect presence when they are in bed, across the room, with a thousand covers. I expect this to be proven incorrect.

I expect that small targets, further away will no be detected and I want to find out at what threshold this occurs for each sensor. And how hard it is to “adjust settings” to alter this behavior.

Marketing vs Reality

SAMPLE DATA - SAMPLE DATA - SAMPLE DATA - SAMPLE DATA - SAMPLE DATA

Sensor TargetSize Dist Angle 10° 15° 25° Settings Notes
DFRobot 10x15cm 1M PASS PASS PASS PASS Default
DFRobot 10x15cm 2M PASS PASS PASS PASS Default
DFRobot 10x15cm 3M FAIL FAIL FAIL PASS Default
DFRobot 10x15cm 3M FAIL FAIL FAIL PASS Sensitivity 9 No change
DFRobot 10x15cm 1M 45° PASS PASS PASS PASS Default
DFRobot 10x15cm 2M 45° FAIL FAIL FAIL FAIL Default
DFRobot 30x20cm 3M FAIL PASS PASS PASS Default
DFRobot 30x20cm 3M FAIL PASS PASS PASS Sensitivity 9 No change

SAMPLE DATA - SAMPLE DATA - SAMPLE DATA - SAMPLE DATA - SAMPLE DATA

Distance

You thought that because it marketed 9M that you could be sleeping 9M away ? lol

Angle

See above comment… :wink:

Deployment Flexibility

Does wide-angle or narrow FOV work best? Let’s find out…

FOV

Is narrow better for beds/desks? Are they better for fans or other disruptive influences? Let’s test that!

Configuration Parameters

Oh so confusing sometimes… …is there a hidden formula?

Appendix:

FAQ:

  • what about [this other sensor module]?
    ** I don’t have it therefore I didn’t test it.
  • what about the FP1/2?
    ** see the Contenders section
  • what is ‘angle’ in the comparison?
    ** angle is the number of degrees that the target offset from perpendicular
  • what are those “5°|10°|15°|25°” listed in the chart?
    ** that is the amount of degrees the servo “wiggles” the target back and forth. The lower number representing very little movement which will be harder to detect. Just like waving your arms is easier to detect, so is 25°
  • what is the “10x15cm” target size?
    ** this is what I had laying around to represent a “head-sized” target
  • what is the “30x20cm” target size?
    ** this is what I had laying around to represent a “obscured-torso-sized” target you have cadavers laying around!? you sicko! /s

My mmWave Projects

Tracking Radar
mmWave Presence Detection
Low-Latency DFRobot+PIR
mmWave Wars: one sensor (module) to rule them all

45 Likes

Sensor Code

HLK-LD1115J

YAML

esphome:
  name: tinypico-mmwave-ld1115h
  platform: ESP32
  board: esp32dev
  includes:
    - uart_read_line_sensor_ld1115h.h # https://esphome.io/cookbook/uart_text_sensor.html

# Enable logging
logger:
  level: INFO

# Enable Home Assistant API
api:
  reboot_timeout: 6h
  services:
      # Service to send a command directly to the display. Useful for testing
    - service: send_command
      variables:
        cmd: string
      then:
        - uart.write: !lambda
            std::string command = to_string(cmd) +"\n";
            return std::vector<uint8_t>(command.begin(), command.end());

ota:
  password: !secret ota

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: !secret ap_ssid
    password: !secret ap_password

substitutions:
  device_name: tinypico_mmwave_ld1115h

web_server:
  port: 80
  version: 2
  include_internal: true
  ota: false

captive_portal:

uart:
  id: uart_bus
  tx_pin: GPIO5
  rx_pin: GPIO18
  baud_rate: 115200

binary_sensor:
  - platform: gpio
    name: mmwave_presence_ld1115h
    id: mmwave_presence_ld1115h
    device_class: motion
    pin:
      number: GPIO33
      mode: INPUT_PULLDOWN
    on_state:
      then:
        - if:
            condition:
              - binary_sensor.is_off: mmwave_presence_ld1115h
            then:
              - lambda: |-
                  id(mov_SNR).publish_state(0.0);
                  id(occ_SNR).publish_state(0.0);

text_sensor:
  - platform: template
    name: uptime_human_readable
    id: uptime_human_readable
    icon: mdi:clock-start
    update_interval: 60s

sensor:
  - platform: uptime
    name: uptime_sensor
    id: uptime_sensor
    update_interval: 60s
    internal: true
    on_raw_value:
      then:
        - text_sensor.template.publish:
            id: uptime_human_readable
            state: !lambda |-
                      int seconds = round(id(uptime_sensor).raw_state);
                      int days = seconds / (24 * 3600);
                      seconds = seconds % (24 * 3600);
                      int hours = seconds / 3600;
                      seconds = seconds % 3600;
                      int minutes = seconds /  60;
                      seconds = seconds % 60;
                      return (
                        (days ? to_string(days)+":" : "00:") +
                        (hours ? to_string(hours)+":" : "00:") +
                        (minutes ? to_string(minutes)+":" : "00:") +
                        (to_string(seconds))
                      ).c_str();

  - platform: custom
    lambda: |-
      auto s = new hilink(id(uart_bus));
      App.register_component(s);
      //return {};
      return {s->mov_SNR, s->occ_SNR};
    sensors:
      - name: mov_SNR
        id: mov_SNR
        internal: true
      
      - name: occ_SNR
        id: occ_SNR
        internal: true
      
switch:
  - platform: safe_mode
    name: use_safe_mode
    
  - platform: template
    name: show_SNR
    id: show_SNR
    internal: true
    optimistic: true
    turn_off_action:
      - lambda: |-
          id(mov_SNR).publish_state(0.0);
          id(occ_SNR).publish_state(0.0);

number:
  - platform: template
    name: dtime
    id: dtime # do not change
    entity_category: config
    min_value: 1
    max_value: 600
    lambda: |-
      hilink(id(uart_bus)).getmmwConf("get_all");
      return {};
    step: 1
    unit_of_measurement: s
    mode: box
    set_action:
      - uart.write: !lambda
          std::string setdtime = "dtime=" + to_string((int)x);
          return std::vector<unsigned char>(setdtime.begin(), setdtime.end());
      - delay: 250ms
      - uart.write: "save\n"

  - platform: template
    name: mov_SNR_target
    id: mov_SNR_target # do not change
    entity_category: config
    min_value: 0
    max_value: 65536
    mode: box
    optimistic: true
    step: 1
    set_action:
      - uart.write: !lambda
          std::string setth1 = "th1=" + to_string((int)x);
          return std::vector<unsigned char>(setth1.begin(), setth1.end());
      - delay: 250ms
      - uart.write: "save\n"

  - platform: template
    name: th3_SNR_target_long_dist
    id: th3_SNR_target # do not change
    entity_category: config
    min_value: 0
    max_value: 65536
    mode: box
    optimistic: true
    step: 1
    set_action:
      - uart.write: !lambda
          std::string setth3 = "th3=" + to_string((int)x);
          return std::vector<unsigned char>(setth3.begin(), setth3.end());
      - delay: 250ms
      - uart.write: "save\n"

  - platform: template
    name: occ_SNR_target
    id: occ_SNR_target # do not change
    entity_category: config
    min_value: 0
    max_value: 65536
    mode: box
    optimistic: true
    step: 1
    set_action:
      - uart.write: !lambda
          std::string setth2 = "th2=" + to_string((int)x);
          return std::vector<unsigned char>(setth2.begin(), setth2.end());
      - delay: 250ms
      - uart.write: "save\n"
      
  - platform: template
    name: ind_min
    id: ind_min # do not change
    entity_category: config
    min_value: 0
    max_value: 32
    mode: box 
    optimistic: true
    step: 1
    set_action:
      - uart.write: !lambda
          std::string setind_min = "ind_min=" + to_string((int)x);
          return std::vector<unsigned char>(setind_min.begin(), setind_min.end());
      - delay: 250ms
      - uart.write: "save\n"

  - platform: template
    name: ind_max
    id: ind_max # do not change
    entity_category: config
    min_value: 0
    max_value: 32
    mode: box 
    optimistic: true
    step: 1
    set_action:
      - uart.write: !lambda
          std::string setind_max = "ind_max=" + to_string((int)x);
          return std::vector<unsigned char>(setind_max.begin(), setind_max.end());
      - delay: 250ms
      - uart.write: "save\n"

  - platform: template
    name: mov_sn
    id: mov_sn # do not change
    entity_category: config
    min_value: 0
    max_value: 4
    mode: box 
    optimistic: true
    step: 1
    set_action:
      - uart.write: !lambda
          std::string setmov_sn = "mov_sn=" + to_string((int)x);
          return std::vector<unsigned char>(setmov_sn.begin(), setmov_sn.end());
      - delay: 250ms
      - uart.write: "save\n"

  - platform: template
    name: occ_sn
    id: occ_sn # do not change
    entity_category: config
    min_value: 0
    max_value: 16
    mode: box 
    optimistic: true
    step: 1
    set_action:
      - uart.write: !lambda
          std::string setocc_sn = "occ_sn=" + to_string((int)x);
          return std::vector<unsigned char>(setocc_sn.begin(), setocc_sn.end());
      - delay: 250ms
      - uart.write: "save\n"

button:
  - platform: restart
    name: Restart_ESP_$device_name
    entity_category: diagnostic

uart_read_line_sensor_ld1115h.h

#include "esphome.h"
#include <string>

class hilink : public Component, public UARTDevice {
 public:
  hilink(UARTComponent *parent) : UARTDevice(parent) {}
  Sensor *mov_SNR = new Sensor();
  Sensor *occ_SNR = new Sensor();
  void setup() override {
    //
  }

  void getmmwConf(std::string mmwparam) {
    mmwparam = mmwparam + "\n";
    write_array(std::vector<unsigned char>(mmwparam.begin(), mmwparam.end()));
  }

  int readline(int readch, char *buffer, int len)
  {
    static int pos = 0;
    int rpos;

    if (readch > 0) {
      switch (readch) {
        case '\n': // Ignore new-lines
          break;
        case '\r': // Return on new-lines
          rpos = pos;
          pos = 0;  // Reset position index ready for next time
          return rpos;
        default:
          if (pos < len-1) {
            buffer[pos++] = readch;
            buffer[pos] = 0;
          }
      }
    }
    // No end of line has been found, so return -1.
    return -1;
  }

  void loop() override {
    const int max_line_length = 80;
    static char buffer[max_line_length];

    while (available()) {
      if(readline(read(), buffer, max_line_length) > 0) {
        std::string line = buffer;

        //ESP_LOGD("custom", "Line is: %s", line.c_str());
        if (line.substr(0,3) == "th1") {
          ESP_LOGD("custom", "Found th1: %s", line.c_str());
          id(mov_SNR_target).publish_state(parse_number<int>(line.substr(7)).value());
        }
        else if (line.substr(0,3) == "th2") {
          ESP_LOGD("custom", "Found th2: %s", line.c_str());
          id(occ_SNR_target).publish_state(parse_number<int>(line.substr(7)).value());
        }
        else if (line.substr(0,3) == "th3") {
          ESP_LOGD("custom", "Found th3: %s", line.c_str());
          id(th3_SNR_target).publish_state(parse_number<int>(line.substr(7)).value());
        }
        else if (line.substr(0,7) == "ind_min") {
          ESP_LOGD("custom", "Found ind_min: %s", line.c_str());
          id(ind_min).publish_state(parse_number<int>(line.substr(11)).value());
        }
        else if (line.substr(0,7) == "ind_max") {
          ESP_LOGD("custom", "Found ind_max: %s", line.c_str());
          id(ind_max).publish_state(parse_number<int>(line.substr(11)).value());
        }
        else if (line.substr(0,6) == "mov_sn") {
          ESP_LOGD("custom", "Found mov_sn: %s", line.c_str());
          id(mov_sn).publish_state(parse_number<int>(line.substr(10)).value());
        }
        else if (line.substr(0,6) == "occ_sn") {
          ESP_LOGD("custom", "Found occ_sn: %s", line.c_str());
          id(occ_sn).publish_state(parse_number<int>(line.substr(10)).value());
        }
        else if (line.substr(0,5) == "dtime") {
          ESP_LOGD("custom", "Found dtime: '%s'", line.c_str());
          std::string dtime_str = line.substr(9,(line.length() - 12));
          int dtime_ms = parse_number<int>(dtime_str).value();
          ESP_LOGD("custom", "Found dtime_ms: %i", dtime_ms);
          id(dtime).publish_state(dtime_ms/1000);
        }
        if (line.substr(0,4) == "mov," && id(show_SNR).state) {
          // ESP_LOGD("custom", "Found occ_sn: %s", line.c_str());
          mov_SNR->publish_state(parse_number<int>(line.substr(7)).value());
        }
        else if (line.substr(0,4) == "occ," && id(show_SNR).state) {
          // ESP_LOGD("custom", "Found occ_sn: %s", line.c_str());
          occ_SNR->publish_state(parse_number<int>(line.substr(7)).value());
        }
      }
    }
  }
};

HLK-LD2410

YAML

esphome:
  name: "tinypico-dev-ld2410"
  platform: ESP32
  board: esp32dev
  includes:
    - uart_read_line_sensor_ld2410v3.h
  on_boot:
    priority: -100
    then:
      - script.execute: get_config

# Enable logging
logger:
  baud_rate: 0
  logs:
    sensor: INFO # DEBUG level with uart_target_output = overload!
    binary_sensor: INFO
    text_sensor: INFO

# Enable Home Assistant API
api:

ota:
  password: !secret ota

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: !secret ap_ssid
    password: !secret ap_password

substitutions:
  device_name: dev-sensor

web_server:
  port: 80
  version: 2
  include_internal: true
  ota: false

captive_portal:

uart:
  id: uart_bus
  tx_pin:
    number: GPIO18
  rx_pin: 
    number: GPIO23
  baud_rate: 256000
  parity: NONE
  stop_bits: 1

switch:
  - platform: safe_mode
    name: use_safe_mode
    
  - platform: template
    name: configmode
    id: configmode
    optimistic: true
    # assumed_state: false
    turn_on_action:
      # - switch.turn_off: engineering_mode
      - lambda: 'static_cast<LD2410 *>(ld2410)->setConfigMode(true);'
      - delay: 100ms
      - script.execute: clear_targets
    turn_off_action:
      - lambda: 'static_cast<LD2410 *>(ld2410)->setConfigMode(false);'

  - platform: template
    name: show_target_stats
    id: show_stats
    optimistic: true
    internal: true
    turn_off_action:
      - script.execute: clear_targets

text_sensor:
  - platform: template
    name: uptime_human_readable
    id: uptime_human_readable
    icon: mdi:clock-start
    update_interval: 60s

sensor:
  - platform: uptime
    name: uptime_sensor
    id: uptime_sensor
    update_interval: 60s
    internal: true
    on_raw_value:
      then:
        - text_sensor.template.publish:
            id: uptime_human_readable
            state: !lambda |-
                      int seconds = round(id(uptime_sensor).raw_state);
                      int days = seconds / (24 * 3600);
                      seconds = seconds % (24 * 3600);
                      int hours = seconds / 3600;
                      seconds = seconds % 3600;
                      int minutes = seconds /  60;
                      seconds = seconds % 60;
                      return (
                        (days ? to_string(days)+":" : "00:") +
                        (hours ? to_string(hours)+":" : "00:") +
                        (minutes ? to_string(minutes)+":" : "00:") +
                        (to_string(seconds))
                      ).c_str();

  - platform: custom # currently crashes ESP32
    lambda: |-
      auto uart_component = static_cast<LD2410 *>(ld2410);
      //return {uart_component->movingTargetDistance,uart_component->movingTargetEnergy,uart_component->stillTargetDistance,uart_component->stillTargetEnergy,uart_component->detectDistance};
      return {};
    sensors:
    
  - platform: template
    name: movingTargetDistance
    id: movingTargetDistance
    unit_of_measurement: "cm"
    accuracy_decimals: 0
    internal: true
    
  - platform: template
    name: movingTargetEnergy
    id: movingTargetEnergy
    unit_of_measurement: "%"
    accuracy_decimals: 0
    internal: true
    
  - platform: template
    name: stillTargetDistance
    id: stillTargetDistance
    unit_of_measurement: "cm"
    accuracy_decimals: 0
    internal: true
    
  - platform: template
    name: stillTargetEnergy
    id: stillTargetEnergy
    unit_of_measurement: "%"
    accuracy_decimals: 0
    internal: true
    
  - platform: template
    name: detectDistance
    id: detectDistance
    unit_of_measurement: "cm"
    accuracy_decimals: 0
    internal: true
    
custom_component:
  - lambda: |-
      return {new LD2410(id(uart_bus))};
    components:
      - id: ld2410
      
binary_sensor:
  - platform: gpio
    name: mmwave_presence_ld2410
    id: mmwave_presence_ld2410
    pin: GPIO5
    device_class: motion
    on_state:
      then:
        - if: 
            condition: 
              - binary_sensor.is_off: mmwave_presence_ld2410
            then: 
              - delay: 150ms
              - script.execute: clear_targets

number:  
  - platform: template
    name: configMaxDistance
    id: maxconfigDistance
    unit_of_measurement: "M"
    min_value: 0.75
    max_value: 6
    step: 0.75
    update_interval: never
    optimistic: true
    set_action:
      - switch.turn_on: configmode
      - delay: 50ms
      - lambda: |-
          auto uart_component = static_cast<LD2410 *>(ld2410);
          uart_component->setMaxDistancesAndNoneDuration(x/0.75,x/0.75,id(noneDuration).state);
      - delay: 50ms
      - lambda: 'static_cast<LD2410 *>(ld2410)->queryParameters();'
      - delay: 50ms
      - switch.turn_off: configmode

  - platform: template
    name: "sensitivity_threshold_(%)"
    id: allSensitivity
    min_value: 10
    max_value: 100
    step: 5
    mode: box
    update_interval: never
    optimistic: true
    set_action:
      - switch.turn_on: configmode
      - delay: 50ms
      - lambda: |-
          auto uart_component = static_cast<LD2410 *>(ld2410);
          uart_component->setAllSensitivity(x);
      - delay: 50ms
      - lambda: 'static_cast<LD2410 *>(ld2410)->queryParameters();'
      - delay: 50ms
      - switch.turn_off: configmode
      
  - platform: template
    name: "motion_hold_(sec)"
    id: noneDuration
    min_value: 0
    # max_value: 32767
    max_value: 900
    step: 1
    mode: box
    update_interval: never
    optimistic: true
    set_action:
      - switch.turn_on: configmode
      - delay: 50ms
      - lambda: |-
          auto uart_component = static_cast<LD2410 *>(ld2410);
          uart_component->setMaxDistancesAndNoneDuration(id(maxconfigDistance).state, id(maxconfigDistance).state, x);
      - delay: 50ms
      - lambda: 'static_cast<LD2410 *>(ld2410)->queryParameters();'
      - delay: 50ms
      - switch.turn_off: configmode
button:
  - platform: restart
    name: "reset/restart_ESP/MCU"
    entity_category: diagnostic
    on_press:
      - switch.turn_on: configmode
      - delay: 50ms
      - lambda: 'static_cast<LD2410 *>(ld2410)->factoryReset();'
      - delay: 150ms
      - lambda: 'static_cast<LD2410 *>(ld2410)->reboot();'
      - delay: 150ms

script:
  - id: get_config
    then:
      - switch.turn_on: configmode
      - delay: 500ms
      - lambda: 'static_cast<LD2410 *>(ld2410)->queryParameters();'
      - delay: 500ms
      - switch.turn_off: configmode
      
  - id: clear_targets
    then:
      - lambda: |-
          //id(hasTarget).publish_state(0);
          //id(hasMovingTarget).publish_state(0);
          //id(hasStillTarget).publish_state(0);
          id(movingTargetDistance).publish_state(0);
          id(movingTargetEnergy).publish_state(0);
          id(stillTargetDistance).publish_state(0);
          id(stillTargetEnergy).publish_state(0);
          id(detectDistance).publish_state(0);
          

uart_read_line_sensor_ld2410v3.h

#include "esphome.h"

class LD2410 : public Component, public UARTDevice
{
public:
  LD2410(UARTComponent *parent) : UARTDevice(parent) {}
  std::vector<uint8_t> bytes;
  const std::vector<uint8_t> config_header = {0xFD, 0xFC, 0xFB, 0xFA, 0x1C, 0x00};
  const std::vector<uint8_t> target_header = {0xF4, 0xF3, 0xF2, 0xF1, 0x0D, 0x00};
  const std::vector<uint8_t> ld2410_end_conf = {0x04, 0x03, 0x02, 0x01};

  void ESP_LOGD_HEX(std::vector<uint8_t> bytes, uint8_t separator) {
    std::string res;
    size_t len = bytes.size();
    char buf[5];
    for (size_t i = 0; i < len; i++) {
      if (i > 0) {
        res += separator;
      }
      sprintf(buf, "%02X", bytes[i]);
      res += buf;
    }
    ESP_LOGD("custom", "%s", res.c_str());
  }

  void sendCommand(char *commandStr, char *commandValue, int commandValueLen)
  {
    uint16_t len = 2;
    if (commandValue != nullptr) {
      len += commandValueLen;
    }
    std::vector<uint8_t> ld2410_conf = {0xFD, 0xFC, 0xFB, 0xFA, lowByte(len), highByte(len), commandStr[0], commandStr[1]};
    if (commandValue != nullptr)
    {
      for (int i = 0; i < commandValueLen; i++)
      {
        ld2410_conf.push_back(commandValue[i]);
      }
    }
    for (int i = 0; i < ld2410_end_conf.size(); i++) 
    {
      ld2410_conf.push_back(ld2410_end_conf[i]);
    }
    // ESP_LOGD_HEX(ld2410_conf,':');
    write_array(std::vector<uint8_t>(ld2410_conf.begin(), ld2410_conf.end()));
  }

  int twoByteToInt(char firstByte, char secondByte)
  {
    return (int16_t)(secondByte << 8) + firstByte;
  }

  void handleTargetData(std::vector<uint8_t> buffer)
  {
    TARGETUnion targetUnion;
    std::copy(buffer.begin(), buffer.end(), targetUnion.bytes);
    if (id(show_stats).state == 1 && targetUnion.target.type == 0x02 && targetUnion.target.state != 0x00)
    {
      int movdist = twoByteToInt(targetUnion.target.movdist, targetUnion.target.movdist2);
      if (id(movingTargetDistance).state != movdist)
      {
        id(movingTargetDistance).publish_state(movdist);
      }
      if (id(movingTargetEnergy).state != targetUnion.target.movval)
      {
        id(movingTargetEnergy).publish_state(targetUnion.target.movval);
      }
      int stadist = twoByteToInt(targetUnion.target.stadist, targetUnion.target.stadist2);
      if (id(stillTargetDistance).state != stadist)
      {
        id(stillTargetDistance).publish_state(stadist);
      }
      if (id(stillTargetEnergy).state != targetUnion.target.staval)
      {
        id(stillTargetEnergy).publish_state(targetUnion.target.staval);
      }
      int decdist = twoByteToInt(targetUnion.target.decdist, targetUnion.target.decdist2);
      if (id(detectDistance).state != decdist)
      {
        id(detectDistance).publish_state(decdist);
      }
    }
    else 
    {
      return; 
    }
    // Engineering data - datasheet is horrible
    // if (targetUnion.target.type == 0x01)
    // }
  }

  void handleConfData(std::vector<uint8_t> buffer)
  {
    CONFUnion confUnion;
    std::copy(buffer.begin(), buffer.end(), confUnion.bytes);
    if (confUnion.conf.cmd == 0x61 && confUnion.conf.cmd_val == 0x01 && confUnion.conf.ack_stat == 0x00 && confUnion.conf.head == 0xAA)
    {
      id(maxconfigDistance).publish_state(float(confUnion.conf.max_sta_dist * 0.75));
      id(allSensitivity).publish_state(confUnion.conf.mov0sen);
      id(noneDuration).publish_state(confUnion.conf.none);
    }
  }

  void setConfigMode(bool confenable)
  {
    char cmd[2] = {confenable ? 0xFF : 0xFE,0x00};
    char value[2] = {0x01, 0x00};
    sendCommand(cmd, confenable ? value : nullptr, 2);
  }

  void queryParameters()
  {
    char cmd_query[2] = {0x61, 0x00};
    sendCommand(cmd_query, nullptr, 0);
  }

  void setup() override
  {  }

  void loop() override
  {
    while (available())
    {
      bytes.push_back(read());
      if (bytes.size() < 6)
      {
        continue;
      }
      if (doesHeaderMatch(bytes, config_header))
      {
        if (bytes.size() < sizeof(CONF))
        {
          continue;
        }
        handleConfData(bytes);
        bytes.clear();
      }
      else if (doesHeaderMatch(bytes, target_header)) {
        if (bytes.size() < sizeof(TARGET))
        {
          continue;
        }
        handleTargetData(bytes);
        bytes.clear();
      }
      else
      {
        bytes.erase(bytes.begin());
        continue;
      }
    }
  }

  void setEngineeringMode(bool engenable)
  {
    char cmd[2] = {engenable ? 0x62 : 0x63,0x00};
    sendCommand(cmd, nullptr, 0);
  }

  void setMaxDistancesAndNoneDuration(int maxMovingDistanceRange, int maxStillDistanceRange, int noneDuration)
  {
    char cmd[2] = {0x60, 0x00};
    char value[18] = {0x00, 0x00, lowByte(maxMovingDistanceRange), highByte(maxMovingDistanceRange), 0x00, 0x00, 0x01, 0x00, lowByte(maxStillDistanceRange), highByte(maxStillDistanceRange), 0x00, 0x00, 0x02, 0x00, lowByte(noneDuration), highByte(noneDuration), 0x00, 0x00};
    sendCommand(cmd, value, sizeof(value));
  }

  void setAllSensitivity(int senval)
  {
    //  64 00  00 00  FF FF 00 00 01 00  28 00 00 00 02 00 28 00 00 00 04 03 02 01
    // {cmd  }{dword}{   dgate  }{mword} {   mval   }{sword}{   sval   }{    MFR  }
    char cmd[2] = {0x64, 0x00};
    char value[18] = {0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x01, 0x00, lowByte(senval), highByte(senval), 0x00, 0x00, 0x02, 0x00, lowByte(senval), highByte(senval), 0x00, 0x00};
    sendCommand(cmd, value, sizeof(value));
  }

  void factoryReset()
  {
    char cmd[2] = {0xA2, 0x00};
    sendCommand(cmd, nullptr, 0);
  }

  void reboot()
  {
    char cmd[2] = {0xA3, 0x00};
    sendCommand(cmd, nullptr, 0);
    // not need to exit config mode because the ld2410 will reboot automatically
  }

  void setBaudrate(int index)
  {
    char cmd[2] = {0xA1, 0x00};
    char value[2] = {index, 0x00};
    sendCommand(cmd, value, sizeof(value));
  }

  bool doesHeaderMatch(std::vector<uint8_t> bytes, std::vector<uint8_t> header)
  {
    bool is_equal = std::equal(header.begin(), header.end(), bytes.begin());
    return (is_equal == true ? true : false);
  }

/*
TARGET EXAMPLE DATA
{F4:F3:F2:F1}:{0D:00}:{02}:{AA}: 02  : 4B:00:  4F  : 00:00 : 64   :  29:00 :{55}: {00} :{F8:F7:F6:F5}
{  header   }  {len}  {typ}{hd}{state}{mdist}{mval}{stadis}{staval}{decdis} {tl} {chck} {    MFR    }
*/
  typedef struct
  {
    uint32_t MFR;
    uint16_t len;
    uint8_t type;                 // target or engineering
    uint8_t head;                // fixed head
    uint8_t state;              // state
    uint8_t movdist;          // movement distance
    uint8_t movdist2;          // movement distance
    uint8_t movval;           // movement energy value
    uint8_t stadist;        // stationary distance
    uint8_t stadist2;        // stationary distance
    uint8_t staval;         // stationary energy value
    uint8_t decdist;      // detection distance
    uint8_t decdist2;      // detection distance
    uint8_t tail;         // tail
    uint8_t chk;         // unused
    uint32_t MFR_end ;  // end
  } TARGET;

  typedef union
  {
    TARGET target;
    uint8_t bytes[sizeof(TARGET)];
  } TARGETUnion;

/*
CONF EXAMPLE DATA
FD:FC:FB:FA MFR[0-3]
1C:00 len[4-5]
61:01 CMD[6-7]
00:00 ACKstat[8-9]
AA Head [10]
08 maxDist [11]
06 maxMovDist[12]
06 maxStaDist[13]
1E:1E:1E:1E:1E:1E:1E:1E:1E:1E:1E:1E:1E:1E:1E:1E:1E:1E (9mov & 9sta sensitivities)
5A:00 none[32-33]
04:03:02:01[34-37]
*/
  typedef struct
  {
    uint32_t MFR;
    uint16_t len;
    uint8_t cmd;
    uint8_t cmd_val;
    uint8_t ack_stat;
    uint8_t ack_stathigh;
    uint8_t head;
    uint8_t max_dist;
    uint8_t max_mov_dist;
    uint8_t max_sta_dist;
    uint8_t mov0sen;
    uint8_t mov1sen;
    uint8_t mov2sen;
    uint8_t mov3sen;
    uint8_t mov4sen;
    uint8_t mov5sen;
    uint8_t mov6sen;
    uint8_t mov7sen;
    uint8_t mov8sen;
    uint8_t sta0sen;
    uint8_t sta1sen;
    uint8_t sta2sen;
    uint8_t sta3sen;
    uint8_t sta4sen;
    uint8_t sta5sen;
    uint8_t sta6sen;
    uint8_t sta7sen;
    uint8_t sta8sen;
    uint16_t none;
    uint32_t MFR_end;
  } CONF;

  typedef union
  {
    CONF conf;
    uint8_t bytes[sizeof(CONF)];
  } CONFUnion;
};

18 Likes

Reserved for future sensor code.

Thank you so much for all the effort.

3 Likes

Awesome review! Thank you so much!

2 Likes

But, but… … we’re not done yet!

1 Like

Great job, wish you wrote this before I ordered some of these sensors for testing :stuck_out_tongue:

My 2 cents would be that the LD2410 price makes a great replacement for a classic PIR to be used in less critical areas

3 Likes

I think that the LD1115H sensors will also be good for places where people move and sometimes sit down, for example the kitchen. I am still waiting for the delivery of the LD2410 including the board and even if there is not much time yet I will start testing.

@crlogic for the sleeping test, maybe you could test top mounted. I have the LD2410, i can see that the “Still Energy” coincides with my breathing when my chest face the radar. Or maybe below the bed as shown in DFRobot’s product page.

Let’s not lose sight that this is just qualifying for upcoming testing of actual detection at different ranges. I’ll never get there if every permutation is tested along the way plus it would be a pain in the butt for me

I will table that idea for now. After all, I don’t need to w/ the DFRobot. It works great where it is. With the added benefit that it can “catch me coming” into the room with the current placement. So top-mount now has two negatives - just to make it easier for other sensors…? nahhh :slight_smile:

Maybe we will get there one day by adding a handicap section :stuck_out_tongue: . It just isn’t on my forecast right now for this project.

@crlogic oh ok. I thought you said none of the sensors passed the “sleeping”. I thought “sleeping” meant a sleep sensor aka bed occupancy sensor.

Btw, I tested your code here on my LD2410 and setting the max distance and sensitivity doesn’t work. I set it to 0.75 and it still triggered motion 3meters away. Your old code on GitHub that you posted on ESPresence worked better.

I can change those :man_shrugging: Would need more info. Recommend you enable UART debug logging to see the data exchange in a new thread w/ your yamlbecause the number one reason people don’t follow project threads is all the troubleshooting. Code confirmed working and I know how you can get odd results… …but a new thread is required to find out.

[04:17:03][D][uart_debug:114]: <<< F4:F3:F2:F1:0D:00:02:AA:02:43:00:00:00:00:64:00:00:55:00:F8:F7:F6:F5
[04:17:03][D][number:054]: 'configMaxDistance' - Setting number value
[04:17:03][D][number:113]:   New number value: 1.500000
[04:17:03][D][switch:013]: 'configmode' Turning ON.
[04:17:03][D][switch:037]: 'configmode': Sending state ON
[04:17:03][D][number:012]: 'configMaxDistance': Sending state 1.500000
[04:17:03][D][uart_debug:114]: >>> FD:FC:FB:FA:04:00:FF:00:01:00:04:03:02:01
[04:17:03][D][uart_debug:114]: <<< F4:F3:F2:F1:0D:00:02:AA:02:43:00:00:00:00:64:00:00:55:00:F8:F7:F6:F5
[04:17:03][D][uart_debug:114]: >>> FD:FC:FB:FA:14:00:60:00:00:00:02:00:00:00:01:00:02:00:00:00:02:00:00:00:00:00:04:03:02:01
[04:17:03][D][uart_debug:114]: <<< F4:F3:F2:F1:0D:00:02:AA:02:43:00:00:00:00:64:00:00:55:00:F8:F7:F6:F5
[04:17:03][D][uart_debug:114]: >>> FD:FC:FB:FA:02:00:61:00:04:03:02:01
[04:17:03][D][uart_debug:114]: <<< F4:F3:F2:F1:0D:00:02:AA:02:43:00:00:00:00:64:00:00:55:00:F8:F7:F6:F5
[04:17:03][D][switch:017]: 'configmode' Turning OFF.
[04:17:03][D][switch:037]: 'configmode': Sending state OFF
[04:17:03][D][uart_debug:114]: >>> FD:FC:FB:FA:02:00:FE:00:04:03:02:01
[04:17:03][D][uart_debug:114]: <<< F4:F3:F2:F1:0D:00:02:AA:02:43:00:00:00:00:64:00:00:55:00:F8:F7:F6:F5

YAML is all the same as the one you posted. I have no other sensors connected, only using ESP8266. The old code works fine when I was tuning my AC wall-mounted fan.

It’s ok, I found out the issue. The wire I soldered to TX(LD2410) got disconnected. The issue with soldering the small pitch pin headers… :rofl:

@crlogic I’ve installed all my LD2410 just below 1.5meter height all this time (around 1.2m-1.3m). Reading the user manual, the recommended installation height is between 1.5m-2m, so I raised it up to about 1.6m. Previously, it does not detect presence when I lay down in bed about 2m away from the sensor. Now at 1.6m installation height, it detects presence even when I lay down on the bed with no motion. Even detects my cat sleeping on my bed.

Yes an overhead or underneath installation would indeed be the optimal installation approach for a dedicated sleep sensor if that is your only use-case.

And use-case is the segway for me to realize that perhaps not everyone watches sports/Olympics/F1 where “qualifying” is often used to determine if you get into the competition and what order you compete in.

We must understand that qualifying isn’t the competition. And the parameters for qualifying are not always designed to showcase the best possible conditions.

So in this case, where the competition is designed to identify the sensor that is the most versatile and matches most closely to advertised specifications. We are not going to make it easy on them by testing them in “perfect conditions.” Especially when there are sensors that don’t need perfect conditions to work well. This really highlights how easy or hard a sensor might be to deploy/work-with.

1 Like

You need to at least follow the manufacturer’s recommended settings and installation recommendations for a fair comparison. At least use the default sensitivity for initial comparison. If you test based on your placement and settings it would only be a test of your own “environment”. None of my sensors face the bed, I have HX711 as a bed occupancy sensor.

There is one issue with most using mmwave like FP1 and even the LD2410, ceiling fan or AC standing wall fan, like the issue I highlighted earlier. I can tune the sensitivity down so it would not detect the fan switching on, but once I enter and leave the room, the presence detection does not turn off with the fan on. This is one of the most common issues I’ve seen with mmWave.

2 Likes

No. Nowhere is it written that qualifying is fair.

The competition [which hasn’t happened yet!!] is different detection ranges vs RCS vs degree of movement. I don’t need to handicap the sensor you like just because it didn’t do well in “imperfect qualifying conditions.”

Qualifying tells me what sensors might not be worth spending time looking into further. That’s it. I don’t want to test every single sensor out there. I will test what I have and what has potential. Qualifying infers potential in imperfect conditions because I decided :stuck_out_tongue: .

Yes, the competition will do this which still hasn’t been published yet!!!.

Yup. Qualifying is what I got, not what you want are you a Formula 1 driver!? /s.

There is a project thread dedicated to bed sensors, I am sure they will be happy to have your thoughts. This is not that thread.

Bingo. That’s why I “didn’t use default settings”. The LD1115H would never turn off.

The competition will;

  • be in a clean environment with no fans, not cats, no people.
  • test default settings to start.
  • tweak settings when they start to fail a test.
  • have different sized targets
  • that will be tested at different distances
  • using a precise servo to move them at varying degrees of movement.

This is a lot of work and I won’t be showcasing something that only works in “perfect conditions”.

Don’t get hung up on qualifying.

Thanks.

[edit] - I have updated the OP to reflect the plan moving forward and better explain the intentions.

Ok, cr’s own logic… Anyways, some of the sensor auto-calibrate installation height and angle and it’s not stated in documentation.

1 Like

Recommendations accepted? Not sure if it fits your criteria. If you can get your hands on this: https://www.glyn.com.au/worlds-first-coin-cell-battery-operated-radar-system/