Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/upload_components.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
components/ads7138
components/adxl345
components/as5600
components/at581x
components/aw9523
components/base_component
components/base_peripheral
Expand Down
4 changes: 4 additions & 0 deletions components/at581x/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
idf_component_register(
INCLUDE_DIRS "include"
REQUIRES "base_peripheral"
)
60 changes: 60 additions & 0 deletions components/at581x/README.md
Original file line number Diff line number Diff line change
@@ -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<uint8_t>({.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.
22 changes: 22 additions & 0 deletions components/at581x/example/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
66 changes: 66 additions & 0 deletions components/at581x/example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# AT581X Example

<img width="2470" height="1860" alt="image" src="https://github.com/user-attachments/assets/697434b1-7415-4ac7-b73a-6a8f4b92080f" />


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.


<img width="2470" height="1860" alt="image" src="https://github.com/user-attachments/assets/a7b6f343-5e3f-49ad-b927-bb0eeef25655" />

<img width="2470" height="1860" alt="image" src="https://github.com/user-attachments/assets/697434b1-7415-4ac7-b73a-6a8f4b92080f" />


```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
```
2 changes: 2 additions & 0 deletions components/at581x/example/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
idf_component_register(SRC_DIRS "."
INCLUDE_DIRS ".")
44 changes: 44 additions & 0 deletions components/at581x/example/main/Kconfig.projbuild
Original file line number Diff line number Diff line change
@@ -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
165 changes: 165 additions & 0 deletions components/at581x/example/main/at581x_example.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#include <atomic>
#include <chrono>
#include <memory>
#include <sdkconfig.h>
#include <string>
#include <thread>

#include "at581x.hpp"
#include "i2c.hpp"
#include "interrupt.hpp"
#include "logger.hpp"
#include "task.hpp"

#if defined(CONFIG_EXAMPLE_HARDWARE_BOX3_SENSOR)
#include <mutex>

#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<bool> g_presence{false};
static std::atomic<int> g_presence_count{0};
static std::atomic<int> 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<uint8_t>({.device_address = espp::At581x::DEFAULT_ADDRESS,
.timeout_ms = static_cast<int>(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<espp::Interrupt> presence_interrupt;
if (CONFIG_EXAMPLE_RADAR_OUTPUT_GPIO >= 0) {
presence_interrupt = std::make_unique<espp::Interrupt>(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<espp::Task> 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<std::recursive_mutex> 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>(espp::Task::Config{
.callback = [](std::mutex &m, std::condition_variable &cv) -> bool {
{
std::lock_guard<std::recursive_mutex> lock(lvgl_mutex);
lv_task_handler();
}
std::unique_lock<std::mutex> 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<std::recursive_mutex> 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);
}
}
Loading
Loading