Last active
May 27, 2025 22:41
-
-
Save gretel/477e054e7044be67c8161c4e57118b76 to your computer and use it in GitHub Desktop.
ESPHome configuration for a 'Seeed MR60BHA2' presence radar that visualizes detected heart rate with an adaptive brightness LED heartbeat effect
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
substitutions: | |
devicename: 'humanscanner' | |
upper_devicename: 'HumanScanner' | |
logger_level: 'INFO' | |
esphome: | |
min_version: 2025.5.0 | |
name: "${devicename}" | |
comment: 'https://esphome.io/components/seeed_mr60bha2.html' | |
project: | |
name: 'gretel.crs' | |
version: '0.25.5.28' | |
on_boot: | |
priority: 600 | |
then: | |
- lambda: |- | |
// Initialize heartbeat state | |
id(heartbeat_state) = false; | |
id(last_beat_time) = 0; | |
// If no presence on boot, turn off LED | |
if (!id(presence_sensor).state) { | |
auto call = id(pixel).turn_off(); | |
call.perform(); | |
} | |
platformio_options: | |
board_upload.maximum_size: 4194304 | |
# should be merged soon? | |
external_components: | |
- source: github://pr#8209 | |
components: [ seeed_mr60bha2 ] | |
refresh: 0s | |
esp32: | |
board: esp32-c6-devkitc-1 | |
variant: esp32c6 | |
flash_size: 4MB | |
framework: | |
type: esp-idf | |
wifi: | |
ssid: !secret wifi_ssid | |
password: !secret wifi_pass | |
domain: !secret domain | |
fast_connect: on | |
ota: | |
- platform: esphome | |
password: !secret ota_key | |
logger: | |
level: ${logger_level} | |
api: | |
reboot_timeout: 0s # disabled | |
encryption: | |
key: !secret humanscanner_key # yours, not mine :) | |
on_client_connected: | |
- logger.log: | |
format: "Client %s connected to API with IP %s" | |
args: ["client_info.c_str()", "client_address.c_str()"] | |
- light.turn_on: | |
id: pixel | |
brightness: 5% | |
red: 0% | |
green: 100% | |
blue: 0% | |
on_client_disconnected: | |
- logger.log: "API client disconnected!" | |
- light.turn_off: | |
id: pixel | |
time: | |
- platform: homeassistant | |
id: homeassistant_time | |
i2c: | |
sda: GPIO22 | |
scl: GPIO23 | |
#scan: true | |
id: bus_a | |
uart: | |
id: uart_bus | |
baud_rate: 115200 | |
rx_pin: 17 | |
tx_pin: 16 | |
parity: NONE | |
stop_bits: 1 | |
seeed_mr60bha2: | |
id: mmwave | |
globals: | |
- id: heart_rate_value | |
type: float | |
restore_value: no | |
initial_value: "60.0" | |
- id: last_beat_time | |
type: uint32_t | |
restore_value: no | |
initial_value: "0" | |
- id: heartbeat_state | |
type: bool | |
restore_value: no | |
initial_value: "false" | |
- id: max_brightness | |
type: float | |
restore_value: no | |
initial_value: "0.5" # Starting with 50% max brightness | |
text_sensor: | |
- platform: version | |
name: "Version" | |
icon: "mdi:exponent-box" | |
- platform: wifi_info | |
ip_address: | |
icon: "mdi:counter" | |
name: "IP Address" | |
ssid: | |
name: "SSID" | |
# Virtual switch to toggle heartbeat feature | |
switch: | |
- platform: template | |
name: "Heartbeat Visualization" | |
id: heartbeat_enabled | |
icon: "mdi:heart-pulse" | |
optimistic: true | |
restore_mode: RESTORE_DEFAULT_OFF | |
binary_sensor: | |
- platform: status | |
name: "Status" | |
- platform: seeed_mr60bha2 | |
has_target: | |
id: presence_sensor | |
name: "Presence" | |
on_state: | |
then: | |
- if: | |
condition: | |
lambda: "return !x;" # When presence is lost | |
then: | |
- light.turn_off: pixel | |
sensor: | |
- platform: uptime | |
name: "Uptime" | |
update_interval: 60s | |
icon: "mdi:clock-start" | |
- platform: wifi_signal | |
name: "WiFi Signal" | |
update_interval: 60s | |
icon: "mdi:wifi" | |
id: wifi_sig | |
- platform: bh1750 | |
name: "Illuminance" | |
address: 0x23 | |
update_interval: 5s | |
id: light_sensor | |
unit_of_measurement: "lx" | |
filters: | |
- median: | |
on_value: | |
then: | |
- lambda: |- | |
// Adjust max brightness based on illuminance | |
// Scale UP brightness as light level increases | |
// Get the current illuminance in lux | |
float lux = x; | |
// Define thresholds for scaling - adjusted for indoor lighting | |
const float MIN_LUX = 0.1; // Very dark room | |
const float MAX_LUX = 200.0; // Typical indoor lighting | |
// Define brightness limits - higher minimum for always visible | |
const float MIN_BRIGHTNESS = 0.25; // Minimum brightness (25%) | |
const float MAX_BRIGHTNESS = 0.5; // Maximum brightness (50%) | |
// Handle edge case where lux is extremely low or zero | |
if (lux < 0.01) { | |
// Force a default visible brightness for near-zero readings | |
id(max_brightness) = 0.3; // 30% brightness - always clearly visible | |
// Log for debugging | |
// ESP_LOGD("LIGHT", "Near-zero lux detected (%.3f), using default visibility brightness: %.2f", lux, id(max_brightness)); | |
return; | |
} | |
// Calculate adaptive brightness | |
if (lux <= MIN_LUX) { | |
id(max_brightness) = MIN_BRIGHTNESS; // Dark room -> still visible LED | |
} else if (lux >= MAX_LUX) { | |
id(max_brightness) = MAX_BRIGHTNESS; // Bright room -> bright LED | |
} else { | |
// Use logarithmic scaling for better human perception | |
float log_min = log10(MIN_LUX); | |
float log_max = log10(MAX_LUX); | |
float log_current = log10(lux); | |
// Calculate ratio on logarithmic scale | |
float ratio = (log_current - log_min) / (log_max - log_min); | |
id(max_brightness) = MIN_BRIGHTNESS + (ratio * (MAX_BRIGHTNESS - MIN_BRIGHTNESS)); | |
} | |
// Ensure brightness is always within valid range | |
id(max_brightness) = std::max(MIN_BRIGHTNESS, std::min(MAX_BRIGHTNESS, id(max_brightness))); | |
// Log brightness level for debugging | |
// ESP_LOGD("LIGHT", "Lux: %.2f, Brightness: %.2f", lux, id(max_brightness)); | |
- platform: seeed_mr60bha2 | |
breath_rate: | |
name: "Respiratory rate" | |
filters: | |
- median: | |
heart_rate: | |
id: heart_rate_sensor | |
name: "Heart rate" | |
on_value: | |
then: | |
- lambda: |- | |
// Store heart rate value in global variable | |
if (x > 20 && x < 200) { // Validate heart rate range | |
id(heart_rate_value) = x; | |
} | |
distance: | |
name: "Distance to detection" | |
num_targets: | |
name: "Target Count" | |
button: | |
- platform: restart | |
name: "Restart" | |
icon: "mdi:restart" | |
light: | |
- platform: esp32_rmt_led_strip | |
id: pixel | |
name: "Pixel" | |
pin: GPIO1 | |
num_leds: 1 | |
rgb_order: GRB | |
chipset: ws2812 | |
default_transition_length: 0ms | |
max_refresh_rate: 16ms | |
internal: true # Hide from Home Assistant | |
# Heartbeat effect | |
interval: | |
- interval: 200ms | |
then: | |
- if: | |
condition: | |
and: | |
- binary_sensor.is_on: presence_sensor | |
- switch.is_on: heartbeat_enabled | |
then: | |
- lambda: |- | |
// Calculate beat interval based on heart rate | |
float heart_rate = id(heart_rate_value); | |
uint32_t beat_interval_ms = static_cast<uint32_t>(60000.0 / heart_rate); | |
uint32_t now = millis(); | |
// Check if it's time for a new heartbeat | |
if (now - id(last_beat_time) >= beat_interval_ms) { | |
// Start a new heartbeat | |
id(last_beat_time) = now; | |
id(heartbeat_state) = true; | |
// Pulse the LED bright red with adaptive brightness | |
auto call = id(pixel).turn_on(); | |
call.set_brightness(id(max_brightness)); | |
call.set_red(255); | |
call.set_green(0); | |
call.set_blue(0); | |
call.perform(); | |
} | |
// Handle the fading part of the heartbeat | |
if (id(heartbeat_state)) { | |
// Calculate time since last beat | |
uint32_t time_since_beat = now - id(last_beat_time); | |
// After 20% of the beat interval, start fading | |
if (time_since_beat >= (beat_interval_ms * 0.2)) { | |
id(heartbeat_state) = false; | |
// Fade to dim - 10% of the max brightness | |
auto fade_call = id(pixel).turn_on(); | |
fade_call.set_brightness(id(max_brightness) * 0.1); | |
fade_call.set_red(255); | |
fade_call.set_green(0); | |
fade_call.set_blue(0); | |
fade_call.set_transition_length(350); | |
fade_call.perform(); | |
} | |
} | |
else: | |
- if: | |
condition: | |
and: | |
- binary_sensor.is_on: presence_sensor | |
- not: | |
switch.is_on: heartbeat_enabled | |
then: | |
# Steady dim red when presence detected but heartbeat disabled | |
- light.turn_on: | |
id: pixel | |
red: 100% | |
green: 0% | |
blue: 0% | |
brightness: 5% | |
else: | |
# No presence, LED off | |
- light.turn_off: pixel |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment