diff --git a/esphome/components/seeed_mr60bha2/__init__.py b/esphome/components/seeed_mr60bha2/__init__.py new file mode 100644 index 0000000000..e1cd56a63d --- /dev/null +++ b/esphome/components/seeed_mr60bha2/__init__.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +DEPENDENCIES = ["uart"] +# is the code owner of the relevant code base +CODEOWNERS = ["@limengdu"] +# The current component or platform can be configured or defined multiple times in the same configuration file. +MULTI_CONF = True + +# This line of code creates a new namespace called mr60fda2_ns. +# This namespace will be used as a prefix for all classes, functions and variables associated with the mr60fda2_ns component, ensuring that they do not conflict with the names of other components. +mr60fda2_ns = cg.esphome_ns.namespace("seeed_mr60bha2") +# This MR24HPC1Component class will be a periodically polled UART device +MR60BHA2Component = mr60fda2_ns.class_( + "MR60BHA2Component", cg.Component, uart.UARTDevice +) + +CONF_MR60BHA2_ID = "mr60fda2_id" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MR60BHA2Component), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +# This code extends the current CONFIG_SCHEMA by adding all the configuration parameters for the UART device and components. +# This means that in the YAML configuration file, the user can use these parameters to configure this component. +CONFIG_SCHEMA = cv.All( + CONFIG_SCHEMA.extend(uart.UART_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) +) + +# A verification mode was created to verify the configuration parameters of a UART device named "seeed_mr60bha2". +# This authentication mode requires that the device must have transmit and receive functionality, a parity mode of "NONE", and a stop bit of one. +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "seeed_mr60bha2", + require_tx=True, + require_rx=True, + parity="NONE", + stop_bits=1, +) + + +# The async def keyword is used to define a concurrent function. +# Concurrent functions are special functions designed to work with Python's asyncio library to support asynchronous I/O operations. +async def to_code(config): + # This line of code creates a new Pvariable (a Python object representing a C++ variable) with the variable's ID taken from the configuration. + var = cg.new_Pvariable(config[CONF_ID]) + # This line of code registers the newly created Pvariable as a component so that ESPHome can manage it at runtime. + await cg.register_component(var, config) + # This line of code registers the newly created Pvariable as a device. + await uart.register_uart_device(var, config) diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp new file mode 100644 index 0000000000..5b97b22eda --- /dev/null +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -0,0 +1,248 @@ +#include "esphome/core/log.h" +#include "seeed_mr60bha2.h" + +#include + +namespace esphome { +namespace seeed_mr60bha2 { + +static const char *const TAG = "seeed_mr60bha2"; + +// Prints the component's configuration data. dump_config() prints all of the component's configuration +// items in an easy-to-read format, including the configuration key-value pairs. +void MR60BHA2Component::dump_config() { + ESP_LOGCONFIG(TAG, "MR60BHA2:"); +#ifdef USE_SENSOR + LOG_SENSOR(" ", "Breath Rate Sensor", this->breath_rate_sensor_); + LOG_SENSOR(" ", "Heart Rate Sensor", this->heart_rate_sensor_); + LOG_SENSOR(" ", "Distance Sensor", this->distance_sensor_); +#endif +} + +// Initialisation functions +void MR60BHA2Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MR60BHA2..."); + this->check_uart_settings(115200); + + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + this->current_frame_id_ = 0; + this->current_frame_len_ = 0; + this->current_data_frame_len_ = 0; + this->current_frame_type_ = 0; + this->current_breath_rate_int_ = 0; + this->current_heart_rate_int_ = 0; + this->current_distance_int_ = 0; + + memset(this->current_frame_buf, 0, FRAME_BUF_MAX_SIZE); + memset(this->current_data_buf, 0, DATA_BUF_MAX_SIZE); + + ESP_LOGCONFIG(TAG, "Set up MR60BHA2 complete"); +} + +// main loop +void MR60BHA2Component::loop() { + uint8_t byte; + + // Is there data on the serial port + while (this->available()) { + this->read_byte(&byte); + this->splitFrame(byte); // split data frame + } +} + +/** + * @brief Calculate the checksum for a byte array. + * + * This function calculates the checksum for the provided byte array using an + * XOR-based checksum algorithm. + * + * @param data The byte array to calculate the checksum for. + * @param len The length of the byte array. + * @return The calculated checksum. + */ +uint8_t MR60BHA2Component::calculateChecksum(const uint8_t *data, size_t len) { + uint8_t checksum = 0; + for (size_t i = 0; i < len; i++) { + checksum ^= data[i]; + } + checksum = ~checksum; + return checksum; +} + +/** + * @brief Validate the checksum of a byte array. + * + * This function validates the checksum of the provided byte array by comparing + * it to the expected checksum. + * + * @param data The byte array to validate. + * @param len The length of the byte array. + * @param expected_checksum The expected checksum. + * @return True if the checksum is valid, false otherwise. + */ +bool MR60BHA2Component::validateChecksum(const uint8_t *data, size_t len, uint8_t expected_checksum) { + return calculateChecksum(data, len) == expected_checksum; +} + +void MR60BHA2Component::splitFrame(uint8_t buffer) { + switch (this->current_frame_locate_) { + case LOCATE_FRAME_HEADER: // starting buffer + if (buffer == FRAME_HEADER_BUFFER) { + this->current_frame_len_ = 1; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + } + break; + case LOCATE_ID_FRAME1: + this->current_frame_id_ = buffer << 8; + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + break; + case LOCATE_ID_FRAME2: + this->current_frame_id_ += buffer; + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + break; + case LOCATE_LENGTH_FRAME_H: + this->current_data_frame_len_ = buffer << 8; + if (this->current_data_frame_len_ == 0x00) { + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + } else { + // ESP_LOGD(TAG, "DATA_FRAME_LEN_H: 0x%02x", buffer); + // ESP_LOGD(TAG, "CURRENT_FRAME_LEN_H: 0x%04x", this->current_data_frame_len_); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + } + break; + case LOCATE_LENGTH_FRAME_L: + this->current_data_frame_len_ += buffer; + if (this->current_data_frame_len_ > DATA_BUF_MAX_SIZE) { + // ESP_LOGD(TAG, "DATA_FRAME_LEN_L: 0x%02x", buffer); + // ESP_LOGD(TAG, "CURRENT_FRAME_LEN: 0x%04x", this->current_data_frame_len_); + // ESP_LOGD(TAG, "DATA_FRAME_LEN ERROR: %d", this->current_data_frame_len_); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + } else { + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + } + break; + case LOCATE_TYPE_FRAME1: + this->current_frame_type_ = buffer << 8; + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + // ESP_LOGD(TAG, "GET LOCATE_TYPE_FRAME1: 0x%02x", this->current_frame_buf[this->current_frame_len_ - 1]); + break; + case LOCATE_TYPE_FRAME2: + this->current_frame_type_ += buffer; + if ((this->current_frame_type_ == BREATH_RATE_TYPE_BUFFER) || + (this->current_frame_type_ == HEART_RATE_TYPE_BUFFER) || + (this->current_frame_type_ == DISTANCE_TYPE_BUFFER)) { + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + // ESP_LOGD(TAG, "GET CURRENT_FRAME_TYPE: 0x%02x 0x%02x", this->current_frame_buf[this->current_frame_len_ - 2], + // this->current_frame_buf[this->current_frame_len_ - 1]); + } else { + // ESP_LOGD(TAG, "CURRENT_FRAME_TYPE NOT FOUND: 0x%02x 0x%02x", + // this->current_frame_buf[this->current_frame_len_ - 2], + // this->current_frame_buf[this->current_frame_len_ - 1]); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + } + break; + case LOCATE_HEAD_CKSUM_FRAME: + if (this->validateChecksum(this->current_frame_buf, this->current_frame_len_, buffer)) { + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + } else { + ESP_LOGD(TAG, "HEAD_CKSUM_FRAME ERROR: 0x%02x", buffer); + ESP_LOGD(TAG, "GET CURRENT_FRAME:"); + for (size_t i = 0; i < this->current_frame_len_; i++) { + ESP_LOGD(TAG, " 0x%02x", current_frame_buf[i]); + } + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + } + break; + case LOCATE_DATA_FRAME: + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_data_buf[this->current_frame_len_ - LEN_TO_DATA_FRAME] = buffer; + if (this->current_frame_len_ - LEN_TO_HEAD_CKSUM == this->current_data_frame_len_) { + this->current_frame_locate_++; + } + if (this->current_frame_len_ > FRAME_BUF_MAX_SIZE) { + ESP_LOGD(TAG, "PRACTICE_DATA_FRAME_LEN ERROR: %d", this->current_frame_len_ - LEN_TO_HEAD_CKSUM); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + } + break; + case LOCATE_DATA_CKSUM_FRAME: + if (this->validateChecksum(this->current_data_buf, this->current_data_frame_len_, buffer)) { + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + this->processFrame(); + } else { + ESP_LOGD(TAG, "DATA_CKSUM_FRAME ERROR: 0x%02x", buffer); + ESP_LOGD(TAG, "GET CURRENT_FRAME:"); + for (size_t i = 0; i < this->current_frame_len_; i++) { + ESP_LOGD(TAG, " 0x%02x", current_frame_buf[i]); + } + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + } + break; + default: + break; + } +} + +void MR60BHA2Component::processFrame() { + switch (this->current_frame_type_) { + case BREATH_RATE_TYPE_BUFFER: + if (this->breath_rate_sensor_ != nullptr) { + this->current_breath_rate_int_ = + (static_cast(current_data_buf[3]) << 24) | (static_cast(current_data_buf[2]) << 16) | + (static_cast(current_data_buf[1]) << 8) | static_cast(current_data_buf[0]); + float breath_rate_float; + memcpy(&breath_rate_float, ¤t_breath_rate_int_, sizeof(float)); + this->breath_rate_sensor_->publish_state(breath_rate_float); + } + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + break; + case HEART_RATE_TYPE_BUFFER: + if (this->heart_rate_sensor_ != nullptr) { + this->current_heart_rate_int_ = + (static_cast(current_data_buf[3]) << 24) | (static_cast(current_data_buf[2]) << 16) | + (static_cast(current_data_buf[1]) << 8) | static_cast(current_data_buf[0]); + float heart_rate_float; + memcpy(&heart_rate_float, ¤t_heart_rate_int_, sizeof(float)); + this->heart_rate_sensor_->publish_state(heart_rate_float); + } + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + break; + case DISTANCE_TYPE_BUFFER: + if (!current_data_buf[0]) { + // ESP_LOGD(TAG, "Successfully set the mounting height"); + if (this->distance_sensor_ != nullptr) { + this->current_distance_int_ = + (static_cast(current_data_buf[7]) << 24) | (static_cast(current_data_buf[6]) << 16) | + (static_cast(current_data_buf[5]) << 8) | static_cast(current_data_buf[4]); + float distance_float; + memcpy(&distance_float, ¤t_distance_int_, sizeof(float)); + this->distance_sensor_->publish_state(distance_float); + } + } else + ESP_LOGD(TAG, "Distance information is not output"); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + break; + default: + break; + } +} + +} // namespace seeed_mr60bha2 +} // namespace esphome diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h new file mode 100644 index 0000000000..bfd86247e0 --- /dev/null +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h @@ -0,0 +1,73 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/component.h" +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace seeed_mr60bha2 { + +static const uint8_t DATA_BUF_MAX_SIZE = 12; +static const uint8_t FRAME_BUF_MAX_SIZE = 21; +static const uint8_t LEN_TO_HEAD_CKSUM = 8; +static const uint8_t LEN_TO_DATA_FRAME = 9; + +static const uint8_t FRAME_HEADER_BUFFER = 0x01; +static const uint16_t BREATH_RATE_TYPE_BUFFER = 0x0A14; +static const uint16_t HEART_RATE_TYPE_BUFFER = 0x0A15; +static const uint16_t DISTANCE_TYPE_BUFFER = 0x0A16; + +enum FrameLocation { + LOCATE_FRAME_HEADER, + LOCATE_ID_FRAME1, + LOCATE_ID_FRAME2, + LOCATE_LENGTH_FRAME_H, + LOCATE_LENGTH_FRAME_L, + LOCATE_TYPE_FRAME1, + LOCATE_TYPE_FRAME2, + LOCATE_HEAD_CKSUM_FRAME, // Header checksum: [from the first byte to the previous byte of the HEAD_CKSUM bit] + LOCATE_DATA_FRAME, + LOCATE_DATA_CKSUM_FRAME, // Data checksum: [from the first to the previous byte of the DATA_CKSUM bit] + LOCATE_PROCESS_FRAME, +}; + +class MR60BHA2Component : public Component, + public uart::UARTDevice { // The class name must be the name defined by text_sensor.py +#ifdef USE_SENSOR + SUB_SENSOR(breath_rate); + SUB_SENSOR(heart_rate); + SUB_SENSOR(distance); +#endif + + protected: + uint8_t current_frame_locate_; + uint8_t current_frame_buf[FRAME_BUF_MAX_SIZE]; + uint8_t current_data_buf[DATA_BUF_MAX_SIZE]; + uint16_t current_frame_id_; + size_t current_frame_len_; + size_t current_data_frame_len_; + uint16_t current_frame_type_; + uint32_t current_breath_rate_int_; + uint32_t current_heart_rate_int_; + uint32_t current_distance_int_; + + bool validateChecksum(const uint8_t *data, size_t len, uint8_t expected_checksum); + uint8_t calculateChecksum(const uint8_t *data, size_t len); + void splitFrame(uint8_t buffer); + void processFrame(); + + public: + float get_setup_priority() const override { return esphome::setup_priority::LATE; } + void setup() override; + void dump_config() override; + void loop() override; +}; + +} // namespace seeed_mr60bha2 +} // namespace esphome diff --git a/esphome/components/seeed_mr60bha2/sensor.py b/esphome/components/seeed_mr60bha2/sensor.py new file mode 100644 index 0000000000..546b09e34d --- /dev/null +++ b/esphome/components/seeed_mr60bha2/sensor.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_DISTANCE, + UNIT_CENTIMETER, +) +from . import CONF_MR60BHA2_ID, MR60BHA2Component + +AUTO_LOAD = ["seeed_mr60bha2"] + +CONF_BREATH_RATE = "breath_rate" +CONF_HEART_RATE= "heart_rate" +CONF_DISTANCE = "distance" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_MR60BHA2_ID): cv.use_id(MR60BHA2Component), + cv.Optional(CONF_BREATH_RATE): sensor.sensor_schema( + accuracy_decimals=2, + icon="mdi:counter", + ), + cv.Optional(CONF_HEART_RATE): sensor.sensor_schema( + accuracy_decimals=2, + icon="mdi:counter", + ), + cv.Optional(CONF_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_CENTIMETER, + accuracy_decimals=2, # Specify the number of decimal places + icon="mdi:signal-distance-variant", + ), + } +) + + +async def to_code(config): + mr60bha2_component = await cg.get_variable(config[CONF_MR60BHA2_ID]) + if breath_rate_config := config.get(CONF_BREATH_RATE): + sens = await sensor.new_sensor(breath_rate_config) + cg.add(mr60bha2_component.set_breath_rate_sensor(sens)) + if heart_rate_config := config.get(CONF_HEART_RATE): + sens = await sensor.new_sensor(heart_rate_config) + cg.add(mr60bha2_component.set_heart_rate_sensor(sens)) + if distance_config := config.get(CONF_DISTANCE): + sens = await sensor.new_sensor(distance_config) + cg.add(mr60bha2_component.set_distance_sensor(sens)) diff --git a/esphome/components/seeed_mr60fda2/__init__.py b/esphome/components/seeed_mr60fda2/__init__.py new file mode 100644 index 0000000000..a32bc20359 --- /dev/null +++ b/esphome/components/seeed_mr60fda2/__init__.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +DEPENDENCIES = ["uart"] +# is the code owner of the relevant code base +CODEOWNERS = ["@limengdu"] +# The current component or platform can be configured or defined multiple times in the same configuration file. +MULTI_CONF = True + +# This line of code creates a new namespace called mr60fda2_ns. +# This namespace will be used as a prefix for all classes, functions and variables associated with the mr60fda2_ns component, ensuring that they do not conflict with the names of other components. +mr60fda2_ns = cg.esphome_ns.namespace("seeed_mr60fda2") +# This MR24HPC1Component class will be a periodically polled UART device +MR60FDA2Component = mr60fda2_ns.class_( + "MR60FDA2Component", cg.Component, uart.UARTDevice +) + +CONF_MR60FDA2_ID = "mr60fda2_id" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MR60FDA2Component), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +# This code extends the current CONFIG_SCHEMA by adding all the configuration parameters for the UART device and components. +# This means that in the YAML configuration file, the user can use these parameters to configure this component. +CONFIG_SCHEMA = cv.All( + CONFIG_SCHEMA.extend(uart.UART_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) +) + +# A verification mode was created to verify the configuration parameters of a UART device named "seeed_mr60fda2". +# This authentication mode requires that the device must have transmit and receive functionality, a parity mode of "NONE", and a stop bit of one. +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "seeed_mr60fda2", + require_tx=True, + require_rx=True, + parity="NONE", + stop_bits=1, +) + + +# The async def keyword is used to define a concurrent function. +# Concurrent functions are special functions designed to work with Python's asyncio library to support asynchronous I/O operations. +async def to_code(config): + # This line of code creates a new Pvariable (a Python object representing a C++ variable) with the variable's ID taken from the configuration. + var = cg.new_Pvariable(config[CONF_ID]) + # This line of code registers the newly created Pvariable as a component so that ESPHome can manage it at runtime. + await cg.register_component(var, config) + # This line of code registers the newly created Pvariable as a device. + await uart.register_uart_device(var, config) diff --git a/esphome/components/seeed_mr60fda2/binary_sensor.py b/esphome/components/seeed_mr60fda2/binary_sensor.py new file mode 100644 index 0000000000..d0e456b774 --- /dev/null +++ b/esphome/components/seeed_mr60fda2/binary_sensor.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv +from esphome.const import DEVICE_CLASS_OCCUPANCY +from . import CONF_MR60FDA2_ID, MR60FDA2Component + +AUTO_LOAD = ["seeed_mr60fda2"] + +CONF_PEOPLE_EXIST = "people_exist" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_MR60FDA2_ID): cv.use_id(MR60FDA2Component), + cv.Optional(CONF_PEOPLE_EXIST): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY, icon="mdi:motion-sensor" + ), +} + + +async def to_code(config): + mr60fda2_component = await cg.get_variable(config[CONF_MR60FDA2_ID]) + if people_exist_config := config.get(CONF_PEOPLE_EXIST): + sens = await binary_sensor.new_binary_sensor(people_exist_config) + cg.add(mr60fda2_component.set_people_exist_binary_sensor(sens)) diff --git a/esphome/components/seeed_mr60fda2/button/__init__.py b/esphome/components/seeed_mr60fda2/button/__init__.py new file mode 100644 index 0000000000..32f82813a0 --- /dev/null +++ b/esphome/components/seeed_mr60fda2/button/__init__.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_UPDATE, + ENTITY_CATEGORY_NONE, + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_DIAGNOSTIC, +) +from .. import CONF_MR60FDA2_ID, MR60FDA2Component, mr60fda2_ns + +GetRadarParametersButton = mr60fda2_ns.class_("GetRadarParametersButton", button.Button) +ResetRadarButton = mr60fda2_ns.class_("ResetRadarButton", button.Button) + +CONF_GET_RADAR_PARAMETERS = "get_radar_parameters" +CONF_RESET_RADAR = "reset_radar" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_MR60FDA2_ID): cv.use_id(MR60FDA2Component), + cv.Optional(CONF_GET_RADAR_PARAMETERS): button.button_schema( + GetRadarParametersButton, + device_class=DEVICE_CLASS_UPDATE, + entity_category=ENTITY_CATEGORY_NONE, + ), + cv.Optional(CONF_RESET_RADAR): button.button_schema( + ResetRadarButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +} + + +async def to_code(config): + mr60fda2_component = await cg.get_variable(config[CONF_MR60FDA2_ID]) + if get_radar_parameters_config := config.get(CONF_GET_RADAR_PARAMETERS): + b = await button.new_button(get_radar_parameters_config) + await cg.register_parented(b, config[CONF_MR60FDA2_ID]) + cg.add(mr60fda2_component.set_get_radar_parameters_button(b)) + if reset_radar_config := config.get(CONF_RESET_RADAR): + b = await button.new_button(reset_radar_config) + await cg.register_parented(b, config[CONF_MR60FDA2_ID]) + cg.add(mr60fda2_component.set_reset_radar_button(b)) diff --git a/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.cpp b/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.cpp new file mode 100644 index 0000000000..88be6dfe7c --- /dev/null +++ b/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.cpp @@ -0,0 +1,9 @@ +#include "get_radar_parameters_button.h" + +namespace esphome { +namespace seeed_mr60fda2 { + +void GetRadarParametersButton::press_action() { this->parent_->get_radar_parameters(); } + +} // namespace seeed_mr60fda2 +} // namespace esphome diff --git a/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.h b/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.h new file mode 100644 index 0000000000..9d6d507383 --- /dev/null +++ b/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../seeed_mr60fda2.h" + +namespace esphome { +namespace seeed_mr60fda2 { + +class GetRadarParametersButton : public button::Button, public Parented { + public: + GetRadarParametersButton() = default; + + protected: + void press_action() override; +}; + +} // namespace seeed_mr60fda2 +} // namespace esphome diff --git a/esphome/components/seeed_mr60fda2/button/reset_radar_button.cpp b/esphome/components/seeed_mr60fda2/button/reset_radar_button.cpp new file mode 100644 index 0000000000..5a7569be57 --- /dev/null +++ b/esphome/components/seeed_mr60fda2/button/reset_radar_button.cpp @@ -0,0 +1,9 @@ +#include "reset_radar_button.h" + +namespace esphome { +namespace seeed_mr60fda2 { + +void ResetRadarButton::press_action() { this->parent_->reset_radar(); } + +} // namespace seeed_mr60fda2 +} // namespace esphome \ No newline at end of file diff --git a/esphome/components/seeed_mr60fda2/button/reset_radar_button.h b/esphome/components/seeed_mr60fda2/button/reset_radar_button.h new file mode 100644 index 0000000000..5970f0a978 --- /dev/null +++ b/esphome/components/seeed_mr60fda2/button/reset_radar_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../seeed_mr60fda2.h" + +namespace esphome { +namespace seeed_mr60fda2 { + +class ResetRadarButton : public button::Button, public Parented { + public: + ResetRadarButton() = default; + + protected: + void press_action() override; +}; + +} // namespace seeed_mr60fda2 +} // namespace esphome \ No newline at end of file diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp new file mode 100644 index 0000000000..a3904bf8ab --- /dev/null +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -0,0 +1,438 @@ +#include "esphome/core/log.h" +#include "seeed_mr60fda2.h" + +#include + +namespace esphome { +namespace seeed_mr60fda2 { + +static const char *const TAG = "seeed_mr60fda2"; + +// Prints the component's configuration data. dump_config() prints all of the component's configuration +// items in an easy-to-read format, including the configuration key-value pairs. +void MR60FDA2Component::dump_config() { + ESP_LOGCONFIG(TAG, "MR60FDA2:"); +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "People Exist Binary Sensor", this->people_exist_binary_sensor_); +#endif +#ifdef USE_BUTTON + LOG_BUTTON(" ", "Get Radar Parameters Button", this->get_radar_parameters_button_); + LOG_BUTTON(" ", "Reset Radar Button", this->reset_radar_button_); +#endif +#ifdef USE_SELECT + LOG_SELECT(" ", "Install Height Select", this->install_height_select_); + LOG_SELECT(" ", "Height Threshold Select", this->height_threshold_select_); + LOG_SELECT(" ", "Sensitivity Select", this->sensitivity_select_); +#endif +#ifdef USE_TEXT_SENSOR + LOG_TEXT_SENSOR(" ", "Is Fall Text Sensor", this->is_fall_text_sensor_); +#endif +} + +// Initialisation functions +void MR60FDA2Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MR60FDA2..."); + this->check_uart_settings(115200); + + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + this->current_frame_id_ = 0; + this->current_frame_len_ = 0; + this->current_data_frame_len_ = 0; + this->current_frame_type_ = 0; + this->current_install_height_int_ = 0; + this->current_height_threshold_int_ = 0; + this->current_sensitivity_ = 0; + this->select_index_ = 0; + + this->get_radar_parameters(); + + memset(this->current_frame_buf, 0, FRAME_BUF_MAX_SIZE); + memset(this->current_data_buf, 0, DATA_BUF_MAX_SIZE); + + ESP_LOGCONFIG(TAG, "Set up MR60FDA2 complete"); +} + +// main loop +void MR60FDA2Component::loop() { + uint8_t byte; + + // Is there data on the serial port + while (this->available()) { + this->read_byte(&byte); + this->splitFrame(byte); // split data frame + } +} + +/** + * @brief Calculate the checksum for a byte array. + * + * This function calculates the checksum for the provided byte array using an + * XOR-based checksum algorithm. + * + * @param data The byte array to calculate the checksum for. + * @param len The length of the byte array. + * @return The calculated checksum. + */ +uint8_t MR60FDA2Component::calculateChecksum(const uint8_t *data, size_t len) { + uint8_t checksum = 0; + for (size_t i = 0; i < len; i++) { + checksum ^= data[i]; + } + checksum = ~checksum; + return checksum; +} + +/** + * @brief Validate the checksum of a byte array. + * + * This function validates the checksum of the provided byte array by comparing + * it to the expected checksum. + * + * @param data The byte array to validate. + * @param len The length of the byte array. + * @param expected_checksum The expected checksum. + * @return True if the checksum is valid, false otherwise. + */ +bool MR60FDA2Component::validateChecksum(const uint8_t *data, size_t len, uint8_t expected_checksum) { + return calculateChecksum(data, len) == expected_checksum; +} + +uint8_t MR60FDA2Component::find_nearest_index(float value, const float *arr, int size) { + int nearest_index = 0; + float min_diff = std::abs(value - arr[0]); + for (int i = 1; i < size; ++i) { + float diff = std::abs(value - arr[i]); + if (diff < min_diff) { + min_diff = diff; + nearest_index = i; + } + } + return nearest_index; +} + +void MR60FDA2Component::splitFrame(uint8_t buffer) { + switch (this->current_frame_locate_) { + case LOCATE_FRAME_HEADER: // starting buffer + if (buffer == FRAME_HEADER_BUFFER) { + this->current_frame_len_ = 1; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + } + break; + case LOCATE_ID_FRAME1: + this->current_frame_id_ = buffer << 8; + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + break; + case LOCATE_ID_FRAME2: + this->current_frame_id_ += buffer; + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + break; + case LOCATE_LENGTH_FRAME_H: + this->current_data_frame_len_ = buffer << 8; + if (this->current_data_frame_len_ == 0x00) { + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + } else { + // ESP_LOGD(TAG, "DATA_FRAME_LEN_H: 0x%02x", buffer); + // ESP_LOGD(TAG, "CURRENT_FRAME_LEN_H: 0x%04x", this->current_data_frame_len_); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + } + break; + case LOCATE_LENGTH_FRAME_L: + this->current_data_frame_len_ += buffer; + if (this->current_data_frame_len_ > DATA_BUF_MAX_SIZE) { + // ESP_LOGD(TAG, "DATA_FRAME_LEN_L: 0x%02x", buffer); + // ESP_LOGD(TAG, "CURRENT_FRAME_LEN: 0x%04x", this->current_data_frame_len_); + // ESP_LOGD(TAG, "DATA_FRAME_LEN ERROR: %d", this->current_data_frame_len_); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + } else { + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + } + break; + case LOCATE_TYPE_FRAME1: + this->current_frame_type_ = buffer << 8; + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + // ESP_LOGD(TAG, "GET LOCATE_TYPE_FRAME1: 0x%02x", this->current_frame_buf[this->current_frame_len_ - 1]); + break; + case LOCATE_TYPE_FRAME2: + this->current_frame_type_ += buffer; + if ((this->current_frame_type_ == IS_FALL_TYPE_BUFFER) || + (this->current_frame_type_ == PEOPLE_EXIST_TYPE_BUFFER) || + (this->current_frame_type_ == RUSULT_INSTALL_HEIGHT) || (this->current_frame_type_ == RUSULT_PARAMETERS) || + (this->current_frame_type_ == RUSULT_HEIGHT_THRESHOLD) || (this->current_frame_type_ == RUSULT_SENSITIVITY)) { + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + // ESP_LOGD(TAG, "GET CURRENT_FRAME_TYPE: 0x%02x 0x%02x", this->current_frame_buf[this->current_frame_len_ - 2], + // this->current_frame_buf[this->current_frame_len_ - 1]); + } else { + // ESP_LOGD(TAG, "CURRENT_FRAME_TYPE NOT FOUND: 0x%02x 0x%02x", + // this->current_frame_buf[this->current_frame_len_ - 2], + // this->current_frame_buf[this->current_frame_len_ - 1]); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + } + break; + case LOCATE_HEAD_CKSUM_FRAME: + if (this->validateChecksum(this->current_frame_buf, this->current_frame_len_, buffer)) { + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + } else { + ESP_LOGD(TAG, "HEAD_CKSUM_FRAME ERROR: 0x%02x", buffer); + ESP_LOGD(TAG, "GET CURRENT_FRAME: 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x", + this->current_frame_buf[this->current_frame_len_ - 9], + this->current_frame_buf[this->current_frame_len_ - 8], + this->current_frame_buf[this->current_frame_len_ - 7], + this->current_frame_buf[this->current_frame_len_ - 6], + this->current_frame_buf[this->current_frame_len_ - 5], + this->current_frame_buf[this->current_frame_len_ - 4], + this->current_frame_buf[this->current_frame_len_ - 3], + this->current_frame_buf[this->current_frame_len_ - 2], + this->current_frame_buf[this->current_frame_len_ - 1], buffer); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + } + break; + case LOCATE_DATA_FRAME: + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_data_buf[this->current_frame_len_ - LEN_TO_DATA_FRAME] = buffer; + if (this->current_frame_len_ - LEN_TO_HEAD_CKSUM == this->current_data_frame_len_) { + this->current_frame_locate_++; + } + if (this->current_frame_len_ > FRAME_BUF_MAX_SIZE) { + ESP_LOGD(TAG, "PRACTICE_DATA_FRAME_LEN ERROR: %d", this->current_frame_len_ - LEN_TO_HEAD_CKSUM); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + } + break; + case LOCATE_DATA_CKSUM_FRAME: + if (this->validateChecksum(this->current_data_buf, this->current_data_frame_len_, buffer)) { + this->current_frame_len_++; + this->current_frame_buf[this->current_frame_len_ - 1] = buffer; + this->current_frame_locate_++; + this->processFrame(); + } else { + ESP_LOGD(TAG, "DATA_CKSUM_FRAME ERROR: 0x%02x", buffer); + ESP_LOGD(TAG, "GET CURRENT_FRAME: 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x", + this->current_frame_buf[this->current_frame_len_ - 9], + this->current_frame_buf[this->current_frame_len_ - 8], + this->current_frame_buf[this->current_frame_len_ - 7], + this->current_frame_buf[this->current_frame_len_ - 6], + this->current_frame_buf[this->current_frame_len_ - 5], + this->current_frame_buf[this->current_frame_len_ - 4], + this->current_frame_buf[this->current_frame_len_ - 3], + this->current_frame_buf[this->current_frame_len_ - 2], + this->current_frame_buf[this->current_frame_len_ - 1], buffer); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + } + break; + default: + break; + } +} + +void MR60FDA2Component::processFrame() { + switch (this->current_frame_type_) { + case IS_FALL_TYPE_BUFFER: + if (this->is_fall_text_sensor_ != nullptr) { + if (this->current_frame_buf[LEN_TO_HEAD_CKSUM] == 0) { + this->is_fall_text_sensor_->publish_state("Normal"); + } else if (this->current_frame_buf[LEN_TO_HEAD_CKSUM] == 1) { + this->is_fall_text_sensor_->publish_state("Falling"); + } + } + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + break; + case PEOPLE_EXIST_TYPE_BUFFER: + if (this->people_exist_binary_sensor_ != nullptr) + this->people_exist_binary_sensor_->publish_state(this->current_frame_buf[LEN_TO_HEAD_CKSUM]); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + break; + case RUSULT_INSTALL_HEIGHT: + if (this->current_data_buf[0]) + ESP_LOGD(TAG, "Successfully set the mounting height"); + else + ESP_LOGD(TAG, "Failed to set the mounting height"); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + break; + case RUSULT_HEIGHT_THRESHOLD: + if (this->current_data_buf[0]) + ESP_LOGD(TAG, "Successfully set the height threshold"); + else + ESP_LOGD(TAG, "Failed to set the height threshold"); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + break; + case RUSULT_SENSITIVITY: + if (this->current_data_buf[0]) + ESP_LOGD(TAG, "Successfully set the sensitivity"); + else + ESP_LOGD(TAG, "Failed to set the sensitivity"); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + break; + case RUSULT_PARAMETERS: + // ESP_LOGD( + // TAG, + // "GET CURRENT_FRAME: 0x%02x 0x%02x 0x%02x 0x%02x, 0x%02x 0x%02x 0x%02x 0x%02x, 0x%02x 0x%02x 0x%02x 0x%02x", + // this->current_frame_buf[8], this->current_frame_buf[9], this->current_frame_buf[10], + // this->current_frame_buf[11], this->current_frame_buf[12], this->current_frame_buf[13], + // this->current_frame_buf[14], this->current_frame_buf[15], this->current_frame_buf[16], + // this->current_frame_buf[17], this->current_frame_buf[18], this->current_frame_buf[19]); + // ESP_LOGD( + // TAG, + // "GET CURRENT_FRAME_2: 0x%02x 0x%02x 0x%02x 0x%02x, 0x%02x 0x%02x 0x%02x 0x%02x, 0x%02x 0x%02x 0x%02x + // 0x%02x", this->current_data_buf[0], this->current_data_buf[1], this->current_data_buf[2], + // this->current_data_buf[3], this->current_data_buf[4], this->current_data_buf[5], this->current_data_buf[6], + // this->current_data_buf[7], this->current_data_buf[8], this->current_data_buf[9], + // this->current_data_buf[10], this->current_data_buf[11]); + this->current_install_height_int_ = + (static_cast(current_data_buf[3]) << 24) | (static_cast(current_data_buf[2]) << 16) | + (static_cast(current_data_buf[1]) << 8) | static_cast(current_data_buf[0]); + float install_height_float; + memcpy(&install_height_float, ¤t_install_height_int_, sizeof(float)); + select_index_ = find_nearest_index(install_height_float, INSTALL_HEIGHT, 7); + this->install_height_select_->publish_state(INSTALL_HEIGHT_STR[select_index_]); + this->current_height_threshold_int_ = + (static_cast(current_data_buf[7]) << 24) | (static_cast(current_data_buf[6]) << 16) | + (static_cast(current_data_buf[5]) << 8) | static_cast(current_data_buf[4]); + float height_threshold_float; + memcpy(&height_threshold_float, ¤t_height_threshold_int_, sizeof(float)); + select_index_ = find_nearest_index(height_threshold_float, HEIGHT_THRESHOLD, 7); + this->height_threshold_select_->publish_state(HEIGHT_THRESHOLD_STR[select_index_]); + this->current_sensitivity_ = + (static_cast(current_data_buf[11]) << 24) | (static_cast(current_data_buf[10]) << 16) | + (static_cast(current_data_buf[9]) << 8) | static_cast(current_data_buf[8]); + select_index_ = find_nearest_index(this->current_sensitivity_, SENSITIVITY, 3); + this->sensitivity_select_->publish_state(SENSITIVITY_STR[select_index_]); + ESP_LOGD(TAG, "Mounting height: %.2f, Height threshold: %.2f, Sensitivity: %lu", install_height_float, + height_threshold_float, this->current_sensitivity_); + this->current_frame_locate_ = LOCATE_FRAME_HEADER; + break; + default: + break; + } +} + +// Sending data frames +void MR60FDA2Component::send_query_(uint8_t *query, size_t string_length) { this->write_array(query, string_length); } + +/** + * @brief Convert a float value to a byte array. + * + * This function converts a float value to a byte array. + * + * @param value The float value to convert. + * @param bytes The byte array to store the converted value. + */ +void MR60FDA2Component::float_to_bytes(float value, unsigned char *bytes) { + union { + float float_value; + unsigned char byte_array[4]; + } u; + + u.float_value = value; + memcpy(bytes, u.byte_array, 4); +} + +/** + * @brief Convert a 32-bit unsigned integer to a byte array. + * + * This function converts a 32-bit unsigned integer to a byte array. + * + * @param value The 32-bit unsigned integer to convert. + * @param bytes The byte array to store the converted value. + */ +void MR60FDA2Component::int_to_bytes(uint32_t value, unsigned char *bytes) { + bytes[0] = value & 0xFF; + bytes[1] = (value >> 8) & 0xFF; + bytes[2] = (value >> 16) & 0xFF; + bytes[3] = (value >> 24) & 0xFF; +} + +// Send Heartbeat Packet Command +void MR60FDA2Component::set_install_height(uint8_t index) { + size_t send_data_len = 13; + uint8_t send_data[send_data_len] = {0x01, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x04, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t data_frame[4] = {0x00, 0x00, 0x00, 0x00}; + + float_to_bytes(INSTALL_HEIGHT[index], &send_data[8]); + + for (int i = 0; i < 4; i++) { + data_frame[i] = send_data[i + 8]; + } + + send_data[12] = calculateChecksum(data_frame, 4); + this->send_query_(send_data, send_data_len); + ESP_LOGD(TAG, + "SEND INSTALL HEIGHT FRAME: 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x " + "0x%02x 0x%02x", + send_data[0], send_data[1], send_data[2], send_data[3], send_data[4], send_data[5], send_data[6], + send_data[7], send_data[8], send_data[9], send_data[10], send_data[11], send_data[12]); +} + +void MR60FDA2Component::set_height_threshold(uint8_t index) { + size_t send_data_len = 13; + uint8_t send_data[send_data_len] = {0x01, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x08, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t data_frame[4] = {0x00, 0x00, 0x00, 0x00}; + + float_to_bytes(HEIGHT_THRESHOLD[index], &send_data[8]); + + for (int i = 0; i < 4; i++) { + data_frame[i] = send_data[i + 8]; + } + + send_data[12] = calculateChecksum(data_frame, 4); + this->send_query_(send_data, send_data_len); + ESP_LOGD(TAG, + "SEND HEIGHT THRESHOLD: 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x " + "0x%02x 0x%02x", + send_data[0], send_data[1], send_data[2], send_data[3], send_data[4], send_data[5], send_data[6], + send_data[7], send_data[8], send_data[9], send_data[10], send_data[11], send_data[12]); +} + +void MR60FDA2Component::set_sensitivity(uint8_t index) { + size_t send_data_len = 13; + uint8_t send_data[send_data_len] = {0x01, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x0A, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t data_frame[4] = {0x00, 0x00, 0x00, 0x00}; + + int_to_bytes(SENSITIVITY[index], &send_data[8]); + + for (int i = 0; i < 4; i++) { + data_frame[i] = send_data[i + 8]; + } + + send_data[12] = calculateChecksum(data_frame, 4); + this->send_query_(send_data, send_data_len); + ESP_LOGD(TAG, + "SEND SET SENSITIVITY: 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x " + "0x%02x 0x%02x", + send_data[0], send_data[1], send_data[2], send_data[3], send_data[4], send_data[5], send_data[6], + send_data[7], send_data[8], send_data[9], send_data[10], send_data[11], send_data[12]); +} + +void MR60FDA2Component::get_radar_parameters() { + size_t send_data_len = 8; + uint8_t send_data[send_data_len] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x06, 0xF6}; + this->send_query_(send_data, send_data_len); + ESP_LOGD(TAG, "SEND GET PARAMETERS: 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x", send_data[0], + send_data[1], send_data[2], send_data[3], send_data[4], send_data[5], send_data[6], send_data[7]); +} + +void MR60FDA2Component::reset_radar() { + size_t send_data_len = 8; + uint8_t send_data[send_data_len] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x21, 0x10, 0xCF}; + this->send_query_(send_data, send_data_len); + ESP_LOGD(TAG, "SEND RESET: 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x", send_data[0], send_data[1], + send_data[2], send_data[3], send_data[4], send_data[5], send_data[6], send_data[7]); + this->get_radar_parameters(); +} + +} // namespace seeed_mr60fda2 +} // namespace esphome diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.h b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.h new file mode 100644 index 0000000000..ececaada49 --- /dev/null +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.h @@ -0,0 +1,113 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/component.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace seeed_mr60fda2 { + +static const uint8_t DATA_BUF_MAX_SIZE = 28; +static const uint8_t FRAME_BUF_MAX_SIZE = 37; +static const uint8_t LEN_TO_HEAD_CKSUM = 8; +static const uint8_t LEN_TO_DATA_FRAME = 9; + +static const uint8_t FRAME_HEADER_BUFFER = 0x01; +static const uint16_t IS_FALL_TYPE_BUFFER = 0x0E02; +static const uint16_t PEOPLE_EXIST_TYPE_BUFFER = 0x0F09; +static const uint16_t RUSULT_INSTALL_HEIGHT = 0x0E04; +static const uint16_t RUSULT_PARAMETERS = 0x0E06; +static const uint16_t RUSULT_HEIGHT_THRESHOLD = 0x0E08; +static const uint16_t RUSULT_SENSITIVITY = 0x0E0A; + +enum FrameLocation { + LOCATE_FRAME_HEADER, + LOCATE_ID_FRAME1, + LOCATE_ID_FRAME2, + LOCATE_LENGTH_FRAME_H, + LOCATE_LENGTH_FRAME_L, + LOCATE_TYPE_FRAME1, + LOCATE_TYPE_FRAME2, + LOCATE_HEAD_CKSUM_FRAME, // Header checksum: [from the first byte to the previous byte of the HEAD_CKSUM bit] + LOCATE_DATA_FRAME, + LOCATE_DATA_CKSUM_FRAME, // Data checksum: [from the first to the previous byte of the DATA_CKSUM bit] + LOCATE_PROCESS_FRAME, +}; + +static const float INSTALL_HEIGHT[7] = {2.4f, 2.5f, 2.6f, 2.7f, 2.8f, 2.9f, 3.0f}; +static const float HEIGHT_THRESHOLD[7] = {0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f}; +static const float SENSITIVITY[3] = {3, 15, 30}; + +static const char *const INSTALL_HEIGHT_STR[7] = {"2.4m", "2.5m", "2.6", "2.7m", "2.8", "2.9m", "3.0m"}; +static const char *const HEIGHT_THRESHOLD_STR[7] = {"0.0m", "0.1m", "0.2m", "0.3m", "0.4m", "0.5m", "0.6m"}; +static const char *const SENSITIVITY_STR[3] = {"1", "2", "3"}; + +class MR60FDA2Component : public Component, + public uart::UARTDevice { // The class name must be the name defined by text_sensor.py +#ifdef USE_BINARY_SENSOR + SUB_BINARY_SENSOR(people_exist) +#endif +#ifdef USE_BUTTON + SUB_BUTTON(get_radar_parameters) + SUB_BUTTON(reset_radar) +#endif +#ifdef USE_SELECT + SUB_SELECT(install_height) + SUB_SELECT(height_threshold) + SUB_SELECT(sensitivity) +#endif +#ifdef USE_TEXT_SENSOR + SUB_TEXT_SENSOR(is_fall) +#endif + + protected: + uint8_t current_frame_locate_; + uint8_t current_frame_buf[FRAME_BUF_MAX_SIZE]; + uint8_t current_data_buf[DATA_BUF_MAX_SIZE]; + uint16_t current_frame_id_; + size_t current_frame_len_; + size_t current_data_frame_len_; + uint16_t current_frame_type_; + uint32_t current_install_height_int_; + uint32_t current_height_threshold_int_; + uint32_t current_sensitivity_; + uint8_t select_index_; + + bool validateChecksum(const uint8_t *data, size_t len, uint8_t expected_checksum); + uint8_t calculateChecksum(const uint8_t *data, size_t len); + void splitFrame(uint8_t buffer); + void processFrame(); + void send_query_(uint8_t *query, size_t string_length); + void float_to_bytes(float value, unsigned char *bytes); + void int_to_bytes(uint32_t value, unsigned char *bytes); + uint8_t find_nearest_index(float value, const float *arr, int size); + + public: + float get_setup_priority() const override { return esphome::setup_priority::LATE; } + void setup() override; + void dump_config() override; + void loop() override; + void set_install_height(uint8_t index); + void set_height_threshold(uint8_t index); + void set_sensitivity(uint8_t index); + void get_radar_parameters(); + void reset_radar(); +}; + +} // namespace seeed_mr60fda2 +} // namespace esphome diff --git a/esphome/components/seeed_mr60fda2/select/__init__.py b/esphome/components/seeed_mr60fda2/select/__init__.py new file mode 100644 index 0000000000..60051d4453 --- /dev/null +++ b/esphome/components/seeed_mr60fda2/select/__init__.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import ( + ENTITY_CATEGORY_CONFIG, +) +from .. import CONF_MR60FDA2_ID, MR60FDA2Component, mr60fda2_ns + +InstallHeightSelect = mr60fda2_ns.class_("InstallHeightSelect", select.Select) +HeightThresholdSelect = mr60fda2_ns.class_("HeightThresholdSelect", select.Select) +SensitivitySelect = mr60fda2_ns.class_("SensitivitySelect", select.Select) + +CONF_INSTALL_HEIGHT = "install_height" +CONF_HEIGHT_THRESHOLD = "height_threshold" +CONF_SENSITIVITY = "sensitivity" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_MR60FDA2_ID): cv.use_id(MR60FDA2Component), + cv.Optional(CONF_INSTALL_HEIGHT): select.select_schema( + InstallHeightSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:axis-z-arrow", + ), + cv.Optional(CONF_HEIGHT_THRESHOLD): select.select_schema( + HeightThresholdSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:axis-z-arrow", + ), + cv.Optional(CONF_SENSITIVITY): select.select_schema( + SensitivitySelect, + entity_category=ENTITY_CATEGORY_CONFIG, + ), +} + + +async def to_code(config): + mr60fda2_component = await cg.get_variable(config[CONF_MR60FDA2_ID]) + if install_height_config := config.get(CONF_INSTALL_HEIGHT): + s = await select.new_select( + install_height_config, + options=["2.4m", "2.5m", "2.6m", "2.7m", "2.8m", "2.9m", "3.0m"], + ) + await cg.register_parented(s, config[CONF_MR60FDA2_ID]) + cg.add(mr60fda2_component.set_install_height_select(s)) + if height_threshold_config := config.get(CONF_HEIGHT_THRESHOLD): + s = await select.new_select( + height_threshold_config, + options=["0.0m", "0.1m", "0.2m", "0.3m", "0.4m", "0.5m", "0.6m"], + ) + await cg.register_parented(s, config[CONF_MR60FDA2_ID]) + cg.add(mr60fda2_component.set_height_threshold_select(s)) + if sensitivity_config := config.get(CONF_SENSITIVITY): + s = await select.new_select( + sensitivity_config, + options=["1", "2", "3"], + ) + await cg.register_parented(s, config[CONF_MR60FDA2_ID]) + cg.add(mr60fda2_component.set_sensitivity_select(s)) diff --git a/esphome/components/seeed_mr60fda2/select/height_threshold_select.cpp b/esphome/components/seeed_mr60fda2/select/height_threshold_select.cpp new file mode 100644 index 0000000000..037f8c6036 --- /dev/null +++ b/esphome/components/seeed_mr60fda2/select/height_threshold_select.cpp @@ -0,0 +1,15 @@ +#include "height_threshold_select.h" + +namespace esphome { +namespace seeed_mr60fda2 { + +void HeightThresholdSelect::control(const std::string &value) { + this->publish_state(value); + auto index = this->index_of(value); + if (index.has_value()) { + this->parent_->set_height_threshold(index.value()); + } +} + +} // namespace seeed_mr60fda2 +} // namespace esphome diff --git a/esphome/components/seeed_mr60fda2/select/height_threshold_select.h b/esphome/components/seeed_mr60fda2/select/height_threshold_select.h new file mode 100644 index 0000000000..b856dbc89a --- /dev/null +++ b/esphome/components/seeed_mr60fda2/select/height_threshold_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../seeed_mr60fda2.h" + +namespace esphome { +namespace seeed_mr60fda2 { + +class HeightThresholdSelect : public select::Select, public Parented { + public: + HeightThresholdSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace seeed_mr60fda2 +} // namespace esphome diff --git a/esphome/components/seeed_mr60fda2/select/install_height_select.cpp b/esphome/components/seeed_mr60fda2/select/install_height_select.cpp new file mode 100644 index 0000000000..e791911613 --- /dev/null +++ b/esphome/components/seeed_mr60fda2/select/install_height_select.cpp @@ -0,0 +1,15 @@ +#include "install_height_select.h" + +namespace esphome { +namespace seeed_mr60fda2 { + +void InstallHeightSelect::control(const std::string &value) { + this->publish_state(value); + auto index = this->index_of(value); + if (index.has_value()) { + this->parent_->set_install_height(index.value()); + } +} + +} // namespace seeed_mr60fda2 +} // namespace esphome diff --git a/esphome/components/seeed_mr60fda2/select/install_height_select.h b/esphome/components/seeed_mr60fda2/select/install_height_select.h new file mode 100644 index 0000000000..7430da3493 --- /dev/null +++ b/esphome/components/seeed_mr60fda2/select/install_height_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../seeed_mr60fda2.h" + +namespace esphome { +namespace seeed_mr60fda2 { + +class InstallHeightSelect : public select::Select, public Parented { + public: + InstallHeightSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace seeed_mr60fda2 +} // namespace esphome diff --git a/esphome/components/seeed_mr60fda2/select/sensitivity_select.cpp b/esphome/components/seeed_mr60fda2/select/sensitivity_select.cpp new file mode 100644 index 0000000000..e2507fb7cc --- /dev/null +++ b/esphome/components/seeed_mr60fda2/select/sensitivity_select.cpp @@ -0,0 +1,15 @@ +#include "sensitivity_select.h" + +namespace esphome { +namespace seeed_mr60fda2 { + +void SensitivitySelect::control(const std::string &value) { + this->publish_state(value); + auto index = this->index_of(value); + if (index.has_value()) { + this->parent_->set_sensitivity(index.value()); + } +} + +} // namespace seeed_mr60fda2 +} // namespace esphome diff --git a/esphome/components/seeed_mr60fda2/select/sensitivity_select.h b/esphome/components/seeed_mr60fda2/select/sensitivity_select.h new file mode 100644 index 0000000000..d1accc1b5b --- /dev/null +++ b/esphome/components/seeed_mr60fda2/select/sensitivity_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../seeed_mr60fda2.h" + +namespace esphome { +namespace seeed_mr60fda2 { + +class SensitivitySelect : public select::Select, public Parented { + public: + SensitivitySelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace seeed_mr60fda2 +} // namespace esphome diff --git a/esphome/components/seeed_mr60fda2/text_sensor.py b/esphome/components/seeed_mr60fda2/text_sensor.py new file mode 100644 index 0000000000..519a79c05f --- /dev/null +++ b/esphome/components/seeed_mr60fda2/text_sensor.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ENTITY_CATEGORY_DIAGNOSTIC +from . import CONF_MR60FDA2_ID, MR60FDA2Component + +AUTO_LOAD = ["seeed_mr60fda2"] + +CONF_IS_FALL = "is_fall" + +# The entity category for read only diagnostic values, for example RSSI, uptime or MAC Address +CONFIG_SCHEMA = { + cv.GenerateID(CONF_MR60FDA2_ID): cv.use_id(MR60FDA2Component), + cv.Optional(CONF_IS_FALL): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon="mdi:walk" + ), +} + + +async def to_code(config): + mr60fda2_component = await cg.get_variable(config[CONF_MR60FDA2_ID]) + if is_fall_config := config.get(CONF_IS_FALL): + sens = await text_sensor.new_text_sensor(is_fall_config) + cg.add(mr60fda2_component.set_is_fall_text_sensor(sens))