mmWave human presence for under $20?!

presence_sensor.h

#include "esphome.h"
#include "Radar.h"

using namespace esphome;

static const char* TAG = "PresenceSensor";

class PresenceSensor : public Component, public UARTDevice {
  public:

  Radar* radar = new Radar(&Serial);

  Sensor* presence_sensor = new Sensor();

  PresenceSensor(UARTComponent *parent) : UARTDevice(parent) {}

  void setup() {
    // delay(1000);    
    // radar->send_write_SystemParameter_ThresholdGear_1();

    // delay(1000);
    // radar->send_write_SystemParameter_SceneSetting_LivingTopMounted();
  }

  std::vector<int> bytes;

  void loop() override {  
    while(available() > 0) {
      int incomingByte = read();
      
      //hit next message
      if(incomingByte == 85) {
        //could check if checksum matches if you feel like it (last 2 bytes)
        
        processPacket();

        bytes.clear();
        bytes.push_back(85);             
      } else {              
        bytes.push_back(incomingByte);     
      }
    }    
  }


  std::vector<float> lastLevels = std::vector<float>(4, 0.0f);

  void processPacket() {            
    //the array could look like 85 7 0 4 3 0 1 1 1 137 92
    //cut off the first 3 header bytes and the last 2 checksum bytes

    bytes.erase(bytes.begin(), bytes.begin() + 3);
    bytes.resize(bytes.size() - 2); //remove last 2 checksum bytes
    
    std::string str = "";


    //NOW YOU HAVE THE DATA PACKET BYTES. DO WHATEVER YOU WANT

    //this is the one you really only care about
    //to get the movement amount
    if((bytes[0] == 0x03 || bytes[0] == 0x04) && (bytes[1] == 0x03) && (bytes[2] == 0x06))
    {        
      str = "Proactive Radar Signs: " + esphome::to_string(getFloat(bytes[3], bytes[4], bytes[5], bytes[6]));
      ESP_LOGI(TAG, str.c_str());
      
      float newSensorValue = getFloat(bytes[3], bytes[4], bytes[5], bytes[6]);
            
      lastLevels.insert(lastLevels.begin(), newSensorValue);
      
      //sometimes when movement starts, the sensor jumps down to zero for one or two readings
      //this fixes that behaviour
      if(newSensorValue < 0.1f && (lastLevels[0] > 5 || lastLevels[1] > 5 || lastLevels[2] > 5 || lastLevels[3] > 5)) {
        //don't publish
      } else {
        presence_sensor->publish_state(newSensorValue);
      }            
      
      return;
    }
    
    if((bytes[0] == 0x04) && (bytes[1] == 0x03) && (bytes[2] == 0x07))
    {
      str = "Proactive Radar Approaching: " + hexStr(bytes[5]);
      ESP_LOGI(TAG, str.c_str());       
      
      return;
    }
    if((bytes[0] == 0x03 || bytes[0] == 0x04) && (bytes[1] == 0x03) && (bytes[2] == 0x05)) //env
    {
      str += "Proactive Radar Env:";
      if(bytes[3] == 0x00 && bytes[4] == 0xFF && bytes[5] == 0xFF)
      {
        str += " Unoccupied";
      }
      else if(bytes[3] == 0x01 && bytes[4] == 0x00 && bytes[5] == 0xFF)
      {
        str += " Stationary";
      }
      else if(bytes[3] == 0x01 && bytes[4] == 0x01 && bytes[5] == 0x01) 
      {
        str += " Exercise";
      }
      else
      {
        str += " Other?";
      }
      ESP_LOGI(TAG, str.c_str());  
      
      return;
    }
    //toss away most of the data (headers, length byte, checksum)
    for (int i = 0; i < bytes.size(); i++) {                
      int code = bytes[i];
      if(i == 0) {
        if(code == 0x03)
        {
          str += "Passive ";
        } 
        else if (code == 0x04)
        {
          str += "Proactive ";
        }
      } else if(i == 1) {
        if(code == 0x03)
        {
          str += "Radar ";
        } 
      } else if(i == 2) {
        if(code == 0x05)
        {
          str += "Env ";
        } 
        else if (code == 0x06)
        {
          str += "Signs ";
        }
      } else {
        str += hexStr(bytes[i]) + " ";                                    
      }      
    }
    
    ESP_LOGI(TAG, str.c_str());
  }

  char hexmap[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

  std::string hexStr(int data)
  {
    std::string s = "    ";
    s[0] = '0';
    s[1] = 'x';
    s[2] = hexmap[(data & 0xF0) >> 4];
    s[3] = hexmap[data & 0x0F];

    //s = "0x" + s;  
    return s;
  }

  
  typedef union
  {
    unsigned char Byte[4];
    float Float;
  }FloatByte;

  float getFloat(int b1, int b2, int b3, int b4)
  {
    FloatByte fb;
    fb.Byte[0] = b1;
    fb.Byte[1] = b2;
    fb.Byte[2] = b3;
    fb.Byte[3] = b4;

    return fb.Float;
  }
};

Radar.cpp

#include "Arduino.h"
#include "Radar.h"

#define MESSAGE_HEAD 0x55
#define ACTIVE_REPORT 0x04
#define FALL_REPORT 0x06

#define REPORT_RADAR 0x03
#define REPORT_OTHER 0x05

#define HEARTBEAT 0x01
#define ABNOEMAL 0x02
#define ENVIRONMENT 0x05
#define BODYSIGN 0x06
#define CLOSE_AWAY 0x07

#define CA_BE 0x01
#define CA_CLOSE 0x02
#define CA_AWAY 0x03
#define SOMEBODY_BE 0x01
#define SOMEBODY_MOVE 0x01
#define SOMEBODY_STOP 0x00
#define NOBODY 0x00

Radar::Radar(Stream *s)
{
  _s = s;
}

//CHECKSUM CALCULATION

const unsigned char cuc_CRCHi[256]= {
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
  0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
  0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40
};

const unsigned char cuc_CRCLo[256]= {
  0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7,
  0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E,
  0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9,
  0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC,
  0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
  0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32,
  0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D,
  0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38,
  0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF,
  0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
  0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1,
  0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4,
  0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB,
  0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA,
  0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
  0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0,
  0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97,
  0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E,
  0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89,
  0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
  0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83,
  0x41, 0x81, 0x80, 0x40
};


unsigned short int Radar::us_CalculateCrc16(unsigned char *lpuc_Frame, unsigned short int lus_Len){
  unsigned char luc_CRCHi = 0xFF;
  unsigned char luc_CRCLo = 0xFF;
  int li_Index=0;
  while(lus_Len--){
    li_Index = luc_CRCLo ^ *( lpuc_Frame++);
    luc_CRCLo = (unsigned char)( luc_CRCHi ^ cuc_CRCHi[li_Index]);
    luc_CRCHi = cuc_CRCLo[li_Index];
  }
  return (unsigned short int )(luc_CRCLo << 8 | luc_CRCHi);
}

char Radar::CRC(char ad1, char ad2, char ad3, char ad4, char ad5, char ad6, char ad7){
  unsigned char data[] = {ad1, ad2, ad3, ad4, ad5, ad6, ad7};
    unsigned short int crc_data = 0x0000;
    unsigned int lenth = sizeof(data)/sizeof(unsigned char);
    crc_data = us_CalculateCrc16(data, lenth);
    return crc_data;
}



void Radar::sendMessage(unsigned char partialData[], int length)
{  
  length += 3;

  unsigned char data[length];
  unsigned char dataWithChecksum[length + 2];

  data[0] = 0x55;
  data[1] = length + 1; //
  data[2] = 0x00;   

  for (int n = 0; n < length - 3; n++)data[n + 3] = partialData[n];

  for (int n = 0; n < length; n++)dataWithChecksum[n] = data[n];
  unsigned short int crc_data = us_CalculateCrc16(data, length);
  dataWithChecksum[length] = (crc_data & 0xff00) >> 8;
  dataWithChecksum[length+1] = crc_data & 0xff;  
  for (int n = 0; n < length + 2; n++){      
    _s->write(dataWithChecksum[n]);
  }
}


unsigned char read_RadarInfo_EnvironmentalStatus[3] = {0x01, 0x03, 0x05};
unsigned char read_RadarInfo_MotorSigns[3] = {0x01, 0x03, 0x06};
unsigned char read_SystemParameter_ThresholdGear[3] = {0x01, 0x04, 0x0C}; //tested
unsigned char read_SystemParameter_SceneSetting[3] = {0x01, 0x04, 0x10};

unsigned char write_SystemParameter_ThresholdGear_1[4] = {0x02, 0x04, 0x0C, 0x01}; //least sensitive
unsigned char write_SystemParameter_ThresholdGear_2[4] = {0x02, 0x04, 0x0C, 0x02};
unsigned char write_SystemParameter_ThresholdGear_3[4] = {0x02, 0x04, 0x0C, 0x03};
unsigned char write_SystemParameter_ThresholdGear_4[4] = {0x02, 0x04, 0x0C, 0x04};
unsigned char write_SystemParameter_ThresholdGear_5[4] = {0x02, 0x04, 0x0C, 0x05};
unsigned char write_SystemParameter_ThresholdGear_6[4] = {0x02, 0x04, 0x0C, 0x06};
unsigned char write_SystemParameter_ThresholdGear_7[4] = {0x02, 0x04, 0x0C, 0x07}; //default
unsigned char write_SystemParameter_ThresholdGear_8[4] = {0x02, 0x04, 0x0C, 0x08};
unsigned char write_SystemParameter_ThresholdGear_9[4] = {0x02, 0x04, 0x0C, 0x09};
unsigned char write_SystemParameter_ThresholdGear_10[4] = {0x02, 0x04, 0x0C, 0x0A}; //most sensitive
unsigned char write_SystemParameter_SceneSetting_Default[4] = {0x02, 0x04, 0x10, 0x00};   //roughly a distance setting?
unsigned char write_SystemParameter_SceneSetting_AreaTopLoading[4] = {0x02, 0x04, 0x10, 0x01};
unsigned char write_SystemParameter_SceneSetting_BathroomTopMounted[4] = {0x02, 0x04, 0x10, 0x02};
unsigned char write_SystemParameter_SceneSetting_BathroomTopLoading[4] = {0x02, 0x04, 0x10, 0x03};
unsigned char write_SystemParameter_SceneSetting_LivingTopMounted[4] = {0x02, 0x04, 0x10, 0x04};
unsigned char write_SystemParameter_SceneSetting_OfficeTopLoading[4] = {0x02, 0x04, 0x10, 0x05};
unsigned char write_SystemParameter_SceneSetting_HotelTopLoading[4] = {0x02, 0x04, 0x10, 0x06}; //hotel is the largest?


void Radar::send_read_RadarInfo_EnvironmentalStatus() { sendMessage(read_RadarInfo_EnvironmentalStatus, 3); };
void Radar::send_read_RadarInfo_MotorSigns() { sendMessage(read_RadarInfo_MotorSigns, 3); };
void Radar::send_read_SystemParameter_ThresholdGear() { sendMessage(read_SystemParameter_ThresholdGear, 3); };
void Radar::send_read_SystemParameter_SceneSetting() { sendMessage(read_SystemParameter_SceneSetting, 3); };

void Radar::send_write_SystemParameter_ThresholdGear_1() { sendMessage(write_SystemParameter_ThresholdGear_1, 4); };
void Radar::send_write_SystemParameter_ThresholdGear_2() { sendMessage(write_SystemParameter_ThresholdGear_2, 4); };
void Radar::send_write_SystemParameter_ThresholdGear_3() { sendMessage(write_SystemParameter_ThresholdGear_3, 4); };
void Radar::send_write_SystemParameter_ThresholdGear_4() { sendMessage(write_SystemParameter_ThresholdGear_4, 4); };
void Radar::send_write_SystemParameter_ThresholdGear_5() { sendMessage(write_SystemParameter_ThresholdGear_5, 4); };
void Radar::send_write_SystemParameter_ThresholdGear_6() { sendMessage(write_SystemParameter_ThresholdGear_6, 4); };
void Radar::send_write_SystemParameter_ThresholdGear_7() { sendMessage(write_SystemParameter_ThresholdGear_7, 4); };
void Radar::send_write_SystemParameter_ThresholdGear_8() { sendMessage(write_SystemParameter_ThresholdGear_8, 4); };
void Radar::send_write_SystemParameter_ThresholdGear_9() { sendMessage(write_SystemParameter_ThresholdGear_9, 4); };
void Radar::send_write_SystemParameter_ThresholdGear_10() { sendMessage(write_SystemParameter_ThresholdGear_10, 4); };
void Radar::send_write_SystemParameter_SceneSetting_Default() { sendMessage(write_SystemParameter_SceneSetting_Default, 4); };
void Radar::send_write_SystemParameter_SceneSetting_AreaTopLoading() { sendMessage(write_SystemParameter_SceneSetting_AreaTopLoading, 4); };
void Radar::send_write_SystemParameter_SceneSetting_BathroomTopMounted() { sendMessage(write_SystemParameter_SceneSetting_BathroomTopMounted, 4); };
void Radar::send_write_SystemParameter_SceneSetting_BathroomTopLoading() { sendMessage(write_SystemParameter_SceneSetting_BathroomTopLoading, 4); };
void Radar::send_write_SystemParameter_SceneSetting_LivingTopMounted() { sendMessage(write_SystemParameter_SceneSetting_LivingTopMounted, 4); };
void Radar::send_write_SystemParameter_SceneSetting_OfficeTopLoading() { sendMessage(write_SystemParameter_SceneSetting_OfficeTopLoading, 4); };
void Radar::send_write_SystemParameter_SceneSetting_HotelTopLoading() { sendMessage(write_SystemParameter_SceneSetting_HotelTopLoading, 4); };

Radar.h

#ifndef _RADAR_H__
#define _RADAR_H__

#include <Arduino.h>

class Radar
{
    private:
        Stream *_s;
        
    public:
        Radar(Stream *s);

        char CRC(char ad1, char ad2, char ad3, char ad4, char ad5, char ad6, char ad7);
        void sendMessage(unsigned char partialData[], int length);

        unsigned short int us_CalculateCrc16(unsigned char *lpuc_Frame, unsigned short int lus_Len);
        
        void send_read_RadarInfo_EnvironmentalStatus();
        void send_read_RadarInfo_MotorSigns();
        void send_read_SystemParameter_ThresholdGear(); //tested
        void send_read_SystemParameter_SceneSetting();        

        void send_write_SystemParameter_ThresholdGear_1(); //least sensitive
        void send_write_SystemParameter_ThresholdGear_2();
        void send_write_SystemParameter_ThresholdGear_3();
        void send_write_SystemParameter_ThresholdGear_4();
        void send_write_SystemParameter_ThresholdGear_5();
        void send_write_SystemParameter_ThresholdGear_6();
        void send_write_SystemParameter_ThresholdGear_7(); //default
        void send_write_SystemParameter_ThresholdGear_8();
        void send_write_SystemParameter_ThresholdGear_9();
        void send_write_SystemParameter_ThresholdGear_10(); //most sensitive
        void send_write_SystemParameter_SceneSetting_Default(); //roughly a distance setting?
        void send_write_SystemParameter_SceneSetting_AreaTopLoading();
        void send_write_SystemParameter_SceneSetting_BathroomTopMounted();
        void send_write_SystemParameter_SceneSetting_BathroomTopLoading();
        void send_write_SystemParameter_SceneSetting_LivingTopMounted();
        void send_write_SystemParameter_SceneSetting_OfficeTopLoading();
        void send_write_SystemParameter_SceneSetting_HotelTopLoading(); //hotel is the largest?

};

#endif

presence-sensor.yaml

esphome:
  name: presence-sensor
  platform: ESP32
  board: esp32dev
  includes:
    - presence_sensor.h
    - libraries/Radar.h
    - libraries/Radar.cpp    
    
logger:
  #level: VERBOSE #makes uart stream available in esphome logstream
  baud_rate: 0 #disable logging over uart

api:

ota:
  password: "password"

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Presence-Sensor"
    password: "password"

captive_portal:
      
#9600 baud is mandatory, GPIOs are to your preference
uart:
  id: uart_bus
  tx_pin: GPIO26
  rx_pin: GPIO27
  baud_rate: 9600      
      

sensor:
  - platform: custom
    lambda: |-
      auto my_sensor = new PresenceSensor(id(uart_bus)); 
      App.register_component(my_sensor);
      return {my_sensor->presence_sensor};
  
    sensors:
      - name: "Presence Level"
        accuracy_decimals: 1
        state_class: measurement

Need 5V, GND, RX, TX. That’s All!

3 Likes