From 0eb95c15024dc01eaf082b6cf410b60542bc85fc Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 21 Jun 2026 10:48:32 -0500 Subject: [PATCH 1/3] feat(at581x): Add support fo AT581x RADAR presence sensor (found in esp32-s3-box-3 sensor dock) --- .github/workflows/build.yml | 2 + .github/workflows/upload_components.yml | 1 + components/at581x/CMakeLists.txt | 4 + components/at581x/README.md | 60 ++++ components/at581x/example/CMakeLists.txt | 22 ++ components/at581x/example/README.md | 41 +++ components/at581x/example/main/CMakeLists.txt | 2 + .../at581x/example/main/Kconfig.projbuild | 44 +++ .../at581x/example/main/at581x_example.cpp | 165 +++++++++ components/at581x/example/sdkconfig.defaults | 26 ++ .../at581x/example/sdkconfig.defaults.esp32s3 | 2 + components/at581x/idf_component.yml | 23 ++ components/at581x/include/at581x.hpp | 334 ++++++++++++++++++ doc/Doxyfile | 2 + doc/en/index.rst | 1 + doc/en/presence/at581x.rst | 25 ++ doc/en/presence/at581x_example.md | 2 + doc/en/presence/index.rst | 9 + 18 files changed, 765 insertions(+) create mode 100644 components/at581x/CMakeLists.txt create mode 100644 components/at581x/README.md create mode 100644 components/at581x/example/CMakeLists.txt create mode 100644 components/at581x/example/README.md create mode 100644 components/at581x/example/main/CMakeLists.txt create mode 100644 components/at581x/example/main/Kconfig.projbuild create mode 100644 components/at581x/example/main/at581x_example.cpp create mode 100644 components/at581x/example/sdkconfig.defaults create mode 100644 components/at581x/example/sdkconfig.defaults.esp32s3 create mode 100644 components/at581x/idf_component.yml create mode 100644 components/at581x/include/at581x.hpp create mode 100644 doc/en/presence/at581x.rst create mode 100644 doc/en/presence/at581x_example.md create mode 100644 doc/en/presence/index.rst diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0000fa1fd..9e521024f 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,6 +31,8 @@ jobs: target: esp32s3 - path: 'components/as5600/example' target: esp32s3 + - path: 'components/at581x/example' + target: esp32s3 - path: 'components/aw9523/example' target: esp32 - path: 'components/bdc_driver/example' diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml index ceee30dae..cddf7092e 100755 --- a/.github/workflows/upload_components.yml +++ b/.github/workflows/upload_components.yml @@ -33,6 +33,7 @@ jobs: components/ads7138 components/adxl345 components/as5600 + components/at581x components/aw9523 components/base_component components/base_peripheral diff --git a/components/at581x/CMakeLists.txt b/components/at581x/CMakeLists.txt new file mode 100644 index 000000000..d43ade275 --- /dev/null +++ b/components/at581x/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + INCLUDE_DIRS "include" + REQUIRES "base_peripheral" + ) diff --git a/components/at581x/README.md b/components/at581x/README.md new file mode 100644 index 000000000..98fa3cd1c --- /dev/null +++ b/components/at581x/README.md @@ -0,0 +1,60 @@ +# AT581X Radar Presence Sensor Component + +[![Badge](https://components.espressif.com/components/espp/at581x/badge.svg)](https://components.espressif.com/components/espp/at581x) + +The `At581x` component provides a driver for the **AT581X** (AirTouch) 5.8 GHz +microwave-radar human-presence / motion sensor. This is the radar found on +modules such as the MoreSense **MS58-3909S68U4**, including the one on the +**ESP32-S3-BOX-3 sensor / dock board** (`esp32-s3-box-3-sensor-01`), where it is +wired to I2C `SDA=GPIO41`, `SCL=GPIO40`, and its presence/motion output to +`GPIO21`. + +## How it works + +The AT581X is **configured over I2C** (detection distance / sensitivity, RF +frequency, gain, power consumption, and timing). Detection itself is reported on +a dedicated **active-high output GPIO** that stays asserted for the configured +`trigger_keep_time` after the last detection. So: + +- Use `At581x` (this component) to configure the radar over I2C. +- Use a GPIO interrupt (e.g. `espp::Interrupt`) on the radar's output pin to + react to presence/motion. + +## Example + +The [example](./example) configures the radar over I2C, demonstrates changing +the sensitivity / RF state at runtime, and (if the radar's output GPIO is +configured) prints presence transitions using `espp::Interrupt`. + +```cpp +#include "at581x.hpp" +#include "i2c.hpp" + +espp::I2c i2c({.port = I2C_NUM_0, .sda_io_num = GPIO_NUM_41, .scl_io_num = GPIO_NUM_40}); +auto dev = i2c.add_device({.device_address = espp::At581x::DEFAULT_ADDRESS}, ec); + +espp::At581x radar({ + .write = espp::make_i2c_addressed_write(dev), + .read_register = espp::make_i2c_addressed_read_register(dev), + .sensing_distance = 700, // 0..1023, larger = farther / more sensitive + .log_level = espp::Logger::Verbosity::INFO, +}); + +// react to presence on the radar's output GPIO: +espp::Interrupt presence({.interrupts = {{ + .gpio_num = RADAR_OUT_GPIO, + .callback = [](const auto &e) { fmt::print("presence: {}\n", e.active); }, + .active_level = espp::Interrupt::ActiveLevel::HIGH, + .interrupt_type = espp::Interrupt::Type::ANY_EDGE, +}}}); +``` + +## Configuration notes + +- `sensing_distance` (0..1023): larger values increase the detection range / + sensitivity (it is internally converted to the chip's detection-threshold + delta of `1023 - sensing_distance`). +- `frequency_mhz` must be one of `At581x::allowed_frequencies_mhz()` and + `power_consumption_ua` one of `At581x::allowed_power_ua()`. +- After changing any setting, the driver re-writes the configuration and resets + the RF frontend so it takes effect. diff --git a/components/at581x/example/CMakeLists.txt b/components/at581x/example/CMakeLists.txt new file mode 100644 index 000000000..3567db55f --- /dev/null +++ b/components/at581x/example/CMakeLists.txt @@ -0,0 +1,22 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.20) + +set(ENV{IDF_COMPONENT_MANAGER} "0") +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# add the component directories that we want to use +set(EXTRA_COMPONENT_DIRS + "../../../components/" +) + +set( + COMPONENTS + "main esptool_py at581x esp-box i2c interrupt logger task" + CACHE STRING + "List of components to include" + ) + +project(at581x_example) + +set(CMAKE_CXX_STANDARD 20) diff --git a/components/at581x/example/README.md b/components/at581x/example/README.md new file mode 100644 index 000000000..5c7cdad4e --- /dev/null +++ b/components/at581x/example/README.md @@ -0,0 +1,41 @@ +# AT581X Example + +This example demonstrates the use of the `espp::At581x` driver to configure the +AT581X 5.8 GHz radar presence/motion sensor (e.g. the MS58-3909S68U4 module on +the ESP32-S3-BOX-3 sensor dock) over I2C. + +It: + +- creates an I2C bus (default SDA=GPIO41, SCL=GPIO40 for the BOX-3 sensor dock), +- constructs the `At581x` driver, which writes the configuration to the chip, +- periodically changes the sensing distance (sensitivity) at runtime, and +- attaches an `espp::Interrupt` to the radar output GPIO + (`CONFIG_EXAMPLE_RADAR_OUTPUT_GPIO`, default GPIO21 on the BOX-3 dock) and + prints presence/motion transitions. Set it to -1 to disable. + +When the **ESP32-S3-BOX-3** hardware profile is selected (the default on +esp32s3), the example also uses the `esp-box` BSP to show the live radar status +on the screen — a color-coded presence label plus the detection count and +configured sensing distance. The radar is placed on `I2C_NUM_1` in this mode so +it doesn't collide with the box's internal I2C bus (`I2C_NUM_0`, GPIO8/18). + +On the ESP32-S3-BOX-3 with the sensor dock attached, the defaults (SDA=41, +SCL=40, radar output=21) should work as-is. + +## Configuration + +Use `idf.py menuconfig` → *Example Configuration* to set the I2C pins and the +radar output GPIO for your hardware. + +## Build and Flash + +```sh +idf.py set-target esp32s3 +idf.py build flash monitor +``` + +## Output + +The example logs the configuration it applies and, when the radar output GPIO is +wired up, `Radar presence DETECTED` / `Radar presence cleared` as the sensor +sees motion within its configured range. diff --git a/components/at581x/example/main/CMakeLists.txt b/components/at581x/example/main/CMakeLists.txt new file mode 100644 index 000000000..a941e22ba --- /dev/null +++ b/components/at581x/example/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRC_DIRS "." + INCLUDE_DIRS ".") diff --git a/components/at581x/example/main/Kconfig.projbuild b/components/at581x/example/main/Kconfig.projbuild new file mode 100644 index 000000000..03582b98f --- /dev/null +++ b/components/at581x/example/main/Kconfig.projbuild @@ -0,0 +1,44 @@ +menu "Example Configuration" + + choice EXAMPLE_HARDWARE + prompt "Hardware" + default EXAMPLE_HARDWARE_BOX3_SENSOR + help + Select the hardware to run this example on. + + config EXAMPLE_HARDWARE_BOX3_SENSOR + depends on IDF_TARGET_ESP32S3 + bool "ESP32-S3-BOX-3 sensor dock" + + config EXAMPLE_HARDWARE_CUSTOM + bool "Custom" + endchoice + + config EXAMPLE_I2C_SCL_GPIO + int "SCL GPIO Num" + range 0 50 + default 40 if EXAMPLE_HARDWARE_BOX3_SENSOR + default 40 if EXAMPLE_HARDWARE_CUSTOM + help + GPIO number for I2C Master clock line. + + config EXAMPLE_I2C_SDA_GPIO + int "SDA GPIO Num" + range 0 50 + default 41 if EXAMPLE_HARDWARE_BOX3_SENSOR + default 41 if EXAMPLE_HARDWARE_CUSTOM + help + GPIO number for I2C Master data line. + + config EXAMPLE_RADAR_OUTPUT_GPIO + int "Radar output GPIO Num (-1 to disable presence interrupt)" + range -1 50 + default 21 if EXAMPLE_HARDWARE_BOX3_SENSOR + default -1 if EXAMPLE_HARDWARE_CUSTOM + help + GPIO number connected to the AT581X active-high presence/motion + output pin. On the ESP32-S3-BOX-3 sensor dock this is GPIO21 (verify + for your board revision). If set to -1, the presence interrupt is + disabled and the example only exercises the I2C configuration. + +endmenu diff --git a/components/at581x/example/main/at581x_example.cpp b/components/at581x/example/main/at581x_example.cpp new file mode 100644 index 000000000..3322ab459 --- /dev/null +++ b/components/at581x/example/main/at581x_example.cpp @@ -0,0 +1,165 @@ +#include +#include +#include +#include +#include +#include + +#include "at581x.hpp" +#include "i2c.hpp" +#include "interrupt.hpp" +#include "logger.hpp" +#include "task.hpp" + +#if defined(CONFIG_EXAMPLE_HARDWARE_BOX3_SENSOR) +#include + +#include "esp-box.hpp" +#endif + +using namespace std::chrono_literals; + +// The ESP32-S3-BOX-3 BSP uses I2C_NUM_0 (GPIO8/18) for its internal bus, so put the dock radar +// (GPIO41/40) on a different port when targeting the box-3. +#if defined(CONFIG_EXAMPLE_HARDWARE_BOX3_SENSOR) +static constexpr i2c_port_t RADAR_I2C_PORT = I2C_NUM_1; +#else +static constexpr i2c_port_t RADAR_I2C_PORT = I2C_NUM_0; +#endif + +// Live radar state shared between the interrupt callback, the display, and the logging loop. +static std::atomic g_presence{false}; +static std::atomic g_presence_count{0}; +static std::atomic g_sensing_distance{700}; + +extern "C" void app_main(void) { + static espp::Logger logger({.tag = "at581x example", .level = espp::Logger::Verbosity::INFO}); + logger.info("Starting AT581X radar example"); + + //! [at581x example] + // Make the I2C bus the radar is on (SDA=41, SCL=40 on the ESP32-S3-BOX-3 sensor dock). + espp::I2c i2c({ + .port = RADAR_I2C_PORT, + .sda_io_num = (gpio_num_t)CONFIG_EXAMPLE_I2C_SDA_GPIO, + .scl_io_num = (gpio_num_t)CONFIG_EXAMPLE_I2C_SCL_GPIO, + }); + + std::error_code ec; + auto radar_dev = i2c.add_device({.device_address = espp::At581x::DEFAULT_ADDRESS, + .timeout_ms = static_cast(i2c.config().timeout_ms), + .scl_speed_hz = i2c.config().clk_speed, + .log_level = espp::Logger::Verbosity::WARN}, + ec); + if (!radar_dev) { + logger.error("Could not add AT581X I2C device: {}", ec.message()); + return; + } + + // Create the radar driver. auto_init writes the configuration and resets the RF frontend. + espp::At581x radar({ + .write = espp::make_i2c_addressed_write(radar_dev), + .read_register = espp::make_i2c_addressed_read_register(radar_dev), + .sensing_distance = g_sensing_distance.load(), // 0..1023, larger = farther / more sensitive + .trigger_keep_time_ms = 1000, + .log_level = espp::Logger::Verbosity::INFO, + }); + + // The AT581X reports presence on an active-high output GPIO. If it is wired up, attach an + // interrupt to it so we get notified on presence/motion transitions. + std::unique_ptr presence_interrupt; + if (CONFIG_EXAMPLE_RADAR_OUTPUT_GPIO >= 0) { + presence_interrupt = std::make_unique(espp::Interrupt::Config{ + .interrupts = {{ + .gpio_num = CONFIG_EXAMPLE_RADAR_OUTPUT_GPIO, + .callback = + [](const espp::Interrupt::Event &event) { + g_presence = event.active; + if (event.active) { + g_presence_count++; + } + logger.info("Radar presence {}", event.active ? "DETECTED" : "cleared"); + }, + .active_level = espp::Interrupt::ActiveLevel::HIGH, + .interrupt_type = espp::Interrupt::Type::ANY_EDGE, + .pulldown_enabled = true, + }}, + .task_config = {.name = "radar presence", .stack_size_bytes = 4096}, + .log_level = espp::Logger::Verbosity::WARN, + }); + logger.info("Watching radar output on GPIO {}", CONFIG_EXAMPLE_RADAR_OUTPUT_GPIO); + } else { + logger.warn("No radar output GPIO configured (CONFIG_EXAMPLE_RADAR_OUTPUT_GPIO); presence " + "interrupt disabled. Set it to receive presence events."); + } + //! [at581x example] + +#if defined(CONFIG_EXAMPLE_HARDWARE_BOX3_SENSOR) + // On the ESP32-S3-BOX-3 (which has a screen), show the live radar status on the display. + static std::recursive_mutex lvgl_mutex; + lv_obj_t *status_label = nullptr; + lv_obj_t *detail_label = nullptr; + std::unique_ptr lv_task; + + espp::EspBox &box = espp::EspBox::get(); + box.set_log_level(espp::Logger::Verbosity::WARN); + if (box.initialize_lcd() && box.initialize_display(box.lcd_width() * 50)) { + box.brightness(100.0f); + std::lock_guard lock(lvgl_mutex); + + lv_obj_t *bg = lv_obj_create(lv_screen_active()); + lv_obj_set_size(bg, box.lcd_width(), box.lcd_height()); + lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0); + + lv_obj_t *title = lv_label_create(lv_screen_active()); + lv_label_set_text(title, "AT581X Radar"); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 12); + + status_label = lv_label_create(lv_screen_active()); + lv_obj_align(status_label, LV_ALIGN_CENTER, 0, -10); + + detail_label = lv_label_create(lv_screen_active()); + lv_obj_set_style_text_align(detail_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(detail_label, LV_ALIGN_BOTTOM_MID, 0, -20); + + // Run the LVGL task handler periodically to render the screen. + lv_task = std::make_unique(espp::Task::Config{ + .callback = [](std::mutex &m, std::condition_variable &cv) -> bool { + { + std::lock_guard lock(lvgl_mutex); + lv_task_handler(); + } + std::unique_lock lock(m); + cv.wait_for(lock, 16ms); + return false; // don't stop the task + }, + .task_config = {.name = "lvgl", .stack_size_bytes = 6 * 1024}, + }); + lv_task->start(); + } else { + logger.error("Failed to initialize the box-3 display"); + } +#endif + + // Refresh the (optional) display and log the live radar status. + while (true) { +#if defined(CONFIG_EXAMPLE_HARDWARE_BOX3_SENSOR) + if (status_label) { + bool present = g_presence.load(); + std::lock_guard lock(lvgl_mutex); + lv_label_set_text(status_label, present ? "PRESENCE" : "no presence"); + lv_obj_set_style_text_color( + status_label, + present ? lv_palette_main(LV_PALETTE_GREEN) : lv_palette_main(LV_PALETTE_GREY), 0); + lv_obj_align(status_label, LV_ALIGN_CENTER, 0, -10); + static std::string detail; + detail = "detections: " + std::to_string(g_presence_count.load()) + + "\nsensing distance: " + std::to_string(g_sensing_distance.load()); + lv_label_set_text(detail_label, detail.c_str()); + lv_obj_align(detail_label, LV_ALIGN_BOTTOM_MID, 0, -20); + } +#endif + logger.debug("presence={} detections={} distance={}", g_presence.load(), + g_presence_count.load(), g_sensing_distance.load()); + std::this_thread::sleep_for(500ms); + } +} diff --git a/components/at581x/example/sdkconfig.defaults b/components/at581x/example/sdkconfig.defaults new file mode 100644 index 000000000..3b1a0e570 --- /dev/null +++ b/components/at581x/example/sdkconfig.defaults @@ -0,0 +1,26 @@ +CONFIG_IDF_TARGET="esp32s3" + +CONFIG_FREERTOS_HZ=1000 + +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_COMPILER_OPTIMIZATION_PERF=y + +# ESP32-S3-BOX-3 has 16MB flash; use QIO for speed. +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_ESPTOOLPY_FLASHSIZE="16MB" +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y + +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240 + +# Larger stacks for the LVGL / display BSP used on the box-3. +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384 +CONFIG_ESP_TIMER_TASK_STACK_SIZE=6144 + +CONFIG_SPI_MASTER_IN_IRAM=y + +# LVGL configuration (used when targeting the ESP32-S3-BOX-3 display). +CONFIG_LV_DEF_REFR_PERIOD=16 +CONFIG_LV_USE_THEME_DEFAULT=y +CONFIG_LV_THEME_DEFAULT_DARK=y diff --git a/components/at581x/example/sdkconfig.defaults.esp32s3 b/components/at581x/example/sdkconfig.defaults.esp32s3 new file mode 100644 index 000000000..63a06a85e --- /dev/null +++ b/components/at581x/example/sdkconfig.defaults.esp32s3 @@ -0,0 +1,2 @@ +# on the ESP32S3, which has native USB, set the console so output works over USB: +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y diff --git a/components/at581x/idf_component.yml b/components/at581x/idf_component.yml new file mode 100644 index 000000000..7a74f12c5 --- /dev/null +++ b/components/at581x/idf_component.yml @@ -0,0 +1,23 @@ +## IDF Component Manager Manifest File +license: "MIT" +description: "AT581X 5.8GHz radar presence/motion sensor component for ESP-IDF (e.g. the MS58-3909S68U4 module on the ESP32-S3-BOX-3 sensor dock)" +url: "https://github.com/esp-cpp/espp/tree/main/components/at581x" +repository: "git://github.com/esp-cpp/espp.git" +maintainers: + - William Emfinger +documentation: "https://esp-cpp.github.io/espp/sensors/at581x.html" +examples: + - path: example +tags: + - cpp + - Component + - Sensor + - Radar + - Presence + - Motion + - AT581X + - MS58 +dependencies: + idf: + version: '>=5.0' + espp/base_peripheral: '>=1.0' diff --git a/components/at581x/include/at581x.hpp b/components/at581x/include/at581x.hpp new file mode 100644 index 000000000..06c0f124c --- /dev/null +++ b/components/at581x/include/at581x.hpp @@ -0,0 +1,334 @@ +#pragma once + +#include +#include +#include +#include + +#include "base_peripheral.hpp" + +namespace espp { +/// @brief Driver for the AT581X (AirTouch) 5.8 GHz microwave radar +/// human-presence / motion sensor. +/// +/// The AT581X is configured over I2C, while presence/motion is reported on a +/// dedicated active-high output GPIO (which stays asserted for the configured +/// "trigger keep" time after the last detection). This driver handles the I2C +/// configuration of the chip (detection distance/sensitivity, RF frequency, +/// gain, power, and timing); to react to presence, attach an interrupt (e.g. +/// `espp::Interrupt`) to the radar's output GPIO. See the example. +/// +/// The AT581X is the radar found on modules such as the MoreSense +/// `MS58-3909S68U4` used on the ESP32-S3-BOX-3 sensor / dock board (I2C on +/// SDA=GPIO41, SCL=GPIO40 there). +/// +/// The register protocol implemented here follows the AT581X datasheet and the +/// reference drivers from Espressif and ESPHome. +/// +/// \section at581x_ex1 AT581X Example +/// \snippet at581x_example.cpp at581x example +class At581x : public BasePeripheral<> { +public: + static constexpr uint8_t DEFAULT_ADDRESS = 0x28; ///< Default I2C address of the AT581X. + + /// @brief Configuration for the AT581X driver. + struct Config { + uint8_t device_address = DEFAULT_ADDRESS; ///< I2C address of the device. + BasePeripheral::write_fn write; ///< Function for writing to the device. + BasePeripheral::read_register_fn read_register; ///< Function for reading a register. + int frequency_mhz = 5800; ///< RF frequency in MHz. Must be one of allowed_frequencies_mhz(). + int sensing_distance = 823; ///< Detection distance, 0..1023. Larger = farther/more sensitive. + int gain = 3; ///< Gain stage index, 0..12. Higher = more gain. + int power_consumption_ua = 70; ///< Power draw in µA. Must be one of allowed_power_ua(). + int trigger_base_time_ms = 500; ///< Base detection window in ms. + int trigger_keep_time_ms = 1500; ///< How long the output stays asserted after detection, ms. + int protect_time_ms = 1000; ///< Protection (re-trigger lockout) time in ms. + int poweron_selfcheck_time_ms = 2000; ///< Power-on self-check time in ms (0..65535). + bool auto_init = true; ///< If true, write the configuration to the chip on construction. + espp::Logger::Verbosity log_level{espp::Logger::Verbosity::WARN}; ///< Log verbosity. + }; + + /// @brief Construct an AT581X driver. + /// @param config The configuration for the driver. + explicit At581x(const Config &config) + : BasePeripheral({.address = config.device_address, + .write = config.write, + .read_register = config.read_register}, + "At581x", config.log_level) + , frequency_mhz_(config.frequency_mhz) + , sensing_distance_(config.sensing_distance) + , gain_(config.gain) + , power_consumption_ua_(config.power_consumption_ua) + , trigger_base_time_ms_(config.trigger_base_time_ms) + , trigger_keep_time_ms_(config.trigger_keep_time_ms) + , protect_time_ms_(config.protect_time_ms) + , poweron_selfcheck_time_ms_(config.poweron_selfcheck_time_ms) { + if (config.auto_init) { + std::error_code ec; + initialize(ec); + if (ec) { + logger_.error("Failed to initialize: {}", ec.message()); + } + } + } + + /// @brief Initialize the AT581X by writing the current configuration and resetting it. + /// @param ec Set on error. + /// @return True on success. + bool initialize(std::error_code &ec) { return write_config(ec); } + + /// @brief Write the full configuration to the chip and reset its RF frontend so it takes effect. + /// @param ec Set on error. + /// @return True on success. + bool write_config(std::error_code &ec) { + std::lock_guard lock(base_mutex_); + logger_.info("Writing config: freq={}MHz, sensing_distance={}, gain={}, power={}uA, " + "trigger_base={}ms, trigger_keep={}ms, protect={}ms, selfcheck={}ms", + frequency_mhz_, sensing_distance_, gain_, power_consumption_ua_, + trigger_base_time_ms_, trigger_keep_time_ms_, protect_time_ms_, + poweron_selfcheck_time_ms_); + + // 1) Select frequency mode (also marks freq + gain values as present). + write_u8_to_register(REG_FREQ_MODE, FREQ_MODE_VALUE, ec); + if (ec) + return false; + + // 2) Look up the frequency point and write its register values. + int freq_index = index_of(FREQ_TABLE_MHZ, frequency_mhz_); + if (freq_index < 0) { + logger_.error("Invalid frequency {}MHz", frequency_mhz_); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + write_u8_to_register(REG_FREQ_5F, FREQ_5F_TABLE[freq_index], ec); + if (ec) + return false; + write_u8_to_register(REG_FREQ_60, FREQ_60_TABLE[freq_index], ec); + if (ec) + return false; + + // 3) Detection distance / threshold (16-bit, little-endian). Larger distance -> smaller delta. + int delta = 1023 - sensing_distance_; + write_u8_to_register(REG_THRESHOLD_LO, static_cast(delta & 0xFF), ec); + if (ec) + return false; + write_u8_to_register(REG_THRESHOLD_HI, static_cast((delta >> 8) & 0xFF), ec); + if (ec) + return false; + + // 4) Power consumption (bitfields across two registers). + int power_index = index_of(POWER_TABLE_UA, power_consumption_ua_); + if (power_index < 0) { + logger_.error("Invalid power {}uA", power_consumption_ua_); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + uint8_t pwr_lo = PWR_THRESH_VAL_EN | PWR_WORK_TIME_EN | POWER_67_TABLE[power_index]; + uint8_t pwr_hi = PWR_BURST_TIME_EN | PWR_THRESH_EN | POWER_68_TABLE[power_index]; + write_u8_to_register(REG_POWER_LO, pwr_lo, ec); + if (ec) + return false; + write_u8_to_register(REG_POWER_HI, pwr_hi, ec); + if (ec) + return false; + + // 5) Gain stage. + if (gain_ < 0 || static_cast(gain_) >= GAIN_5C_TABLE.size() || + static_cast(gain_ >> 1) >= GAIN_63_TABLE.size()) { + logger_.error("Invalid gain index {}", gain_); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + write_u8_to_register(REG_GAIN_5C, GAIN_5C_TABLE[gain_], ec); + if (ec) + return false; + write_u8_to_register(REG_GAIN_63, GAIN_63_TABLE[gain_ >> 1], ec); + if (ec) + return false; + + // 6) Timing parameters. + write_u32_le(REG_TRIGGER_BASE_TIME, static_cast(trigger_base_time_ms_), ec); + if (ec) + return false; + write_u32_le(REG_TRIGGER_KEEP_TIME, static_cast(trigger_keep_time_ms_), ec); + if (ec) + return false; + write_u16_le(REG_PROTECT_TIME, static_cast(protect_time_ms_), ec); + if (ec) + return false; + write_u16_le(REG_SELF_CHECK_TIME, static_cast(poweron_selfcheck_time_ms_), ec); + if (ec) + return false; + + // 7) Enable the timing output and the chip. + write_u8_to_register(REG_TIME_ENABLE, 0x01, ec); + if (ec) + return false; + write_u8_to_register(REG_CHIP_ENABLE, 0x04, ec); + if (ec) + return false; + + // 8) Reset the RF frontend so the new configuration takes effect. + return reset(ec); + } + + /// @brief Reset the AT581X RF frontend (required for new config to take effect). + /// @param ec Set on error. + /// @return True on success. + bool reset(std::error_code &ec) { + std::lock_guard lock(base_mutex_); + logger_.info("Resetting RF frontend"); + write_u8_to_register(REG_RESET, 0, ec); + if (ec) + return false; + write_u8_to_register(REG_RESET, 1, ec); + return !ec; + } + + /// @brief Turn the RF / analog frontend on or off (for power saving). + /// @param enable True to enable RF, false to disable. + /// @param ec Set on error. + /// @return True on success. + bool set_rf_enabled(bool enable, std::error_code &ec) { + std::lock_guard lock(base_mutex_); + logger_.info("{} RF", enable ? "Enabling" : "Disabling"); + const auto &values = enable ? RF_ON_TABLE : RF_OFF_TABLE; + for (size_t i = 0; i < RF_REG_ADDR.size(); i++) { + write_u8_to_register(RF_REG_ADDR[i], values[i], ec); + if (ec) + return false; + } + return true; + } + + /// @brief Set the detection distance / sensitivity (0..1023, larger = farther) and re-apply. + /// @param distance The new sensing distance. + /// @param ec Set on error. + /// @return True on success. + bool set_sensing_distance(int distance, std::error_code &ec) { + sensing_distance_ = distance; + return write_config(ec); + } + + /// @brief Set the gain stage index (0..12, higher = more gain) and re-apply. + /// @param gain The new gain index. + /// @param ec Set on error. + /// @return True on success. + bool set_gain(int gain, std::error_code &ec) { + gain_ = gain; + return write_config(ec); + } + + /// @brief Set how long the output stays asserted after detection (ms) and re-apply. + /// @param trigger_keep_time_ms The new trigger-keep time in ms. + /// @param ec Set on error. + /// @return True on success. + bool set_trigger_keep_time(int trigger_keep_time_ms, std::error_code &ec) { + trigger_keep_time_ms_ = trigger_keep_time_ms; + return write_config(ec); + } + + /// @brief Get the current configured sensing distance (0..1023). + int get_sensing_distance() const { return sensing_distance_; } + + /// @brief Get the current configured gain index (0..12). + int get_gain() const { return gain_; } + + /// @brief The RF frequencies (MHz) accepted by frequency_mhz / set_frequency. + static std::span allowed_frequencies_mhz() { + return {FREQ_TABLE_MHZ.data(), FREQ_TABLE_MHZ.size()}; + } + + /// @brief The power-consumption values (µA) accepted by power_consumption_ua. + static std::span allowed_power_ua() { + return {POWER_TABLE_UA.data(), POWER_TABLE_UA.size()}; + } + +protected: + // Write a 16-bit value little-endian across two consecutive registers. + void write_u16_le(uint8_t reg, uint16_t value, std::error_code &ec) { + write_u8_to_register(reg, static_cast(value & 0xFF), ec); + if (ec) + return; + write_u8_to_register(reg + 1, static_cast((value >> 8) & 0xFF), ec); + } + + // Write a 32-bit value little-endian across four consecutive registers. + void write_u32_le(uint8_t reg, uint32_t value, std::error_code &ec) { + for (int i = 0; i < 4; i++) { + write_u8_to_register(reg + i, static_cast((value >> (8 * i)) & 0xFF), ec); + if (ec) + return; + } + } + + template static int index_of(const std::array &table, int value) { + for (size_t i = 0; i < N; i++) { + if (table[i] == value) + return static_cast(i); + } + return -1; + } + + // --- Register addresses (AT581X datasheet) --- + static constexpr uint8_t REG_RESET = 0x00; // write 0 then 1 to reset RF frontend + static constexpr uint8_t REG_THRESHOLD_LO = 0x10; // detection threshold, low byte + static constexpr uint8_t REG_THRESHOLD_HI = 0x11; // detection threshold, high byte + static constexpr uint8_t REG_SELF_CHECK_TIME = 0x38; // 2 bytes + static constexpr uint8_t REG_TRIGGER_BASE_TIME = 0x3D; // 4 bytes + static constexpr uint8_t REG_TIME_ENABLE = 0x41; // enable timing output (0x01) + static constexpr uint8_t REG_TRIGGER_KEEP_TIME = 0x42; // 4 bytes + static constexpr uint8_t REG_PROTECT_TIME = 0x4E; // 2 bytes + static constexpr uint8_t REG_CHIP_ENABLE = 0x55; // enable chip (0x04) + static constexpr uint8_t REG_GAIN_5C = 0x5C; + static constexpr uint8_t REG_FREQ_5F = 0x5F; + static constexpr uint8_t REG_FREQ_60 = 0x60; + static constexpr uint8_t REG_FREQ_MODE = 0x61; + static constexpr uint8_t REG_GAIN_63 = 0x63; + static constexpr uint8_t REG_POWER_LO = 0x67; + static constexpr uint8_t REG_POWER_HI = 0x68; + + // Marks frequency (0x02) and gain (0x08) values as present (| 0xC0). + static constexpr uint8_t FREQ_MODE_VALUE = 0xCA; + + // Power register bitfield enables. + static constexpr uint8_t PWR_WORK_TIME_EN = 0x08; // reg 0x67 + static constexpr uint8_t PWR_BURST_TIME_EN = 0x20; // reg 0x68 + static constexpr uint8_t PWR_THRESH_EN = 0x40; // reg 0x68 + static constexpr uint8_t PWR_THRESH_VAL_EN = 0x80; // reg 0x67 + + // --- Lookup tables (from AT581X datasheet / reference drivers) --- + static constexpr std::array FREQ_TABLE_MHZ = {5696, 5715, 5730, 5748, 5765, 5784, + 5800, 5819, 5836, 5851, 5869, 5888}; + static constexpr std::array FREQ_5F_TABLE = {0x40, 0x41, 0x42, 0x43, 0x44, 0x45, + 0x46, 0x47, 0x40, 0x41, 0x42, 0x43}; + static constexpr std::array FREQ_60_TABLE = {0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, + 0x9d, 0x9d, 0x9e, 0x9e, 0x9e, 0x9e}; + + static constexpr std::array GAIN_5C_TABLE = { + 0x08, 0x18, 0x28, 0x38, 0x48, 0x58, 0x68, 0x78, 0x88, 0x98, 0xa8, 0xb8, 0xc8}; + static constexpr std::array GAIN_63_TABLE = {0x00, 0x01, 0x02, 0x03, + 0x04, 0x05, 0x06}; + + static constexpr std::array POWER_TABLE_UA = {48, 56, 63, 70, 77, 91, 105, 115, + 40, 44, 47, 51, 54, 61, 68, 78}; + static constexpr std::array POWER_67_TABLE = { + 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7}; + static constexpr std::array POWER_68_TABLE = {0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, + 24, 24, 24, 24, 24, 24, 24, 24}; + + // RF / analog frontend on/off (registers 0x5d, 0x62, 0x51). + static constexpr std::array RF_REG_ADDR = {0x5d, 0x62, 0x51}; + static constexpr std::array RF_ON_TABLE = {0x45, 0x55, 0xA0}; + static constexpr std::array RF_OFF_TABLE = {0x46, 0xaa, 0x50}; + + int frequency_mhz_; + int sensing_distance_; + int gain_; + int power_consumption_ua_; + int trigger_base_time_ms_; + int trigger_keep_time_ms_; + int protect_time_ms_; + int poweron_selfcheck_time_ms_; +}; +} // namespace espp diff --git a/doc/Doxyfile b/doc/Doxyfile index e1054c51f..9c97c3f3a 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -78,6 +78,7 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/ads7138/example/main/ads7138_example.cpp \ $(PROJECT_PATH)/components/adxl345/example/main/adxl345_example.cpp \ $(PROJECT_PATH)/components/as5600/example/main/as5600_example.cpp \ + $(PROJECT_PATH)/components/at581x/example/main/at581x_example.cpp \ $(PROJECT_PATH)/components/aw9523/example/main/aw9523_example.cpp \ $(PROJECT_PATH)/components/bdc_driver/example/main/bdc_driver_example.cpp \ $(PROJECT_PATH)/components/lp5817/example/main/lp5817_example.cpp \ @@ -192,6 +193,7 @@ INPUT = \ $(PROJECT_PATH)/components/ads7138/include/ads7138.hpp \ $(PROJECT_PATH)/components/adxl345/include/adxl345.hpp \ $(PROJECT_PATH)/components/as5600/include/as5600.hpp \ + $(PROJECT_PATH)/components/at581x/include/at581x.hpp \ $(PROJECT_PATH)/components/aw9523/include/aw9523.hpp \ $(PROJECT_PATH)/components/base_component/include/base_component.hpp \ $(PROJECT_PATH)/components/base_peripheral/include/base_peripheral.hpp \ diff --git a/doc/en/index.rst b/doc/en/index.rst index 124ee07ad..24e00bd8e 100755 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -61,6 +61,7 @@ This is the documentation for esp-idf c++ components, ESPP (`espp Date: Sun, 21 Jun 2026 11:53:05 -0500 Subject: [PATCH 2/3] address sa --- components/at581x/include/at581x.hpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/components/at581x/include/at581x.hpp b/components/at581x/include/at581x.hpp index 06c0f124c..948556871 100644 --- a/components/at581x/include/at581x.hpp +++ b/components/at581x/include/at581x.hpp @@ -1,7 +1,9 @@ #pragma once +#include #include #include +#include #include #include @@ -263,11 +265,8 @@ class At581x : public BasePeripheral<> { } template static int index_of(const std::array &table, int value) { - for (size_t i = 0; i < N; i++) { - if (table[i] == value) - return static_cast(i); - } - return -1; + auto it = std::find(table.begin(), table.end(), value); + return it != table.end() ? static_cast(std::distance(table.begin(), it)) : -1; } // --- Register addresses (AT581X datasheet) --- From ea8f344d495a877ea0fbbd5c2aa20206b4595040 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 21 Jun 2026 17:09:11 -0500 Subject: [PATCH 3/3] add logs and image to readme --- components/at581x/example/README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/components/at581x/example/README.md b/components/at581x/example/README.md index 5c7cdad4e..96d0f06ac 100644 --- a/components/at581x/example/README.md +++ b/components/at581x/example/README.md @@ -1,5 +1,8 @@ # AT581X Example +image + + This example demonstrates the use of the `espp::At581x` driver to configure the AT581X 5.8 GHz radar presence/motion sensor (e.g. the MS58-3909S68U4 module on the ESP32-S3-BOX-3 sensor dock) over I2C. @@ -39,3 +42,25 @@ idf.py build flash monitor The example logs the configuration it applies and, when the radar output GPIO is wired up, `Radar presence DETECTED` / `Radar presence cleared` as the sensor sees motion within its configured range. + + +image + +image + + +```console +I (188) main_task: Calling app_main() +[at581x example/I][0.188]: Starting AT581X radar example +[At581x/I][0.190]: Writing config: freq=5800MHz, sensing_distance=700, gain=3, power=70uA, trigger_base=500ms, trigger_keep=1000ms, protect=1000ms, selfcheck=2000ms +[At581x/I][0.193]: Resetting RF frontend +[at581x example/I][0.196]: Watching radar output on GPIO 21 +[at581x example/I][0.196]: Radar presence DETECTED +[Interrupt/W][0.197]: ISR service already installed, not installing again +W (199) ledc: the binded timer can't keep alive in sleep +[at581x example/I][2.322]: Radar presence cleared +[at581x example/I][5.057]: Radar presence DETECTED +[at581x example/I][7.838]: Radar presence cleared +[at581x example/I][14.645]: Radar presence DETECTED +[at581x example/I][16.186]: Radar presence cleared +```