Initial frame device
This commit is contained in:
682
frame.yaml
Normal file
682
frame.yaml
Normal file
@@ -0,0 +1,682 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# frame — beacon-driven sleep mode.
|
||||||
|
#
|
||||||
|
# WiFi is OFF except briefly during a refresh. Every 15 seconds the device
|
||||||
|
# wakes, does a passive BLE scan, and only powers up WiFi if it
|
||||||
|
# sees its own iBeacon (UUID = 10-byte prefix + this device's WiFi STA MAC).
|
||||||
|
#
|
||||||
|
# Beacon command codes (iBeacon Major+Minor):
|
||||||
|
# 0x0000 / 0x0000 → refresh image now
|
||||||
|
# 0x0001 / 0x0001 → stay awake (skip sleep, connect to HA, wait)
|
||||||
|
#
|
||||||
|
# Hardware override: press D2 to wake into stay-awake mode (for USB OTA, debug).
|
||||||
|
#
|
||||||
|
# Every 24h without HA contact the device still wakes, connects, pushes battery
|
||||||
|
# state, and sleeps — without fetching the image.
|
||||||
|
#
|
||||||
|
# HA SETUP: create input_boolean.bigink_stay_awake_request as a Toggle helper.
|
||||||
|
# HA-side automation flips it on → beacon broadcaster emits the stay-awake code
|
||||||
|
# → bigink stays awake. Flip off → bigink returns to its sleep cycle.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
substitutions:
|
||||||
|
# Compile-time constants. Edit + flash to change.
|
||||||
|
wake_interval_s: "30" # seconds between BLE-scan wakes
|
||||||
|
scan_duration_ms: "1200" # BLE scan window per wake
|
||||||
|
debounce_s: "60" # min seconds between two refreshes
|
||||||
|
heartbeat_interval_s: "86400" # 24h status-only HA contact
|
||||||
|
|
||||||
|
# 10-byte (20 hex chars) prefix of the iBeacon UUID this device responds to.
|
||||||
|
# Full UUID = <prefix><6-byte WiFi STA MAC of this device>.
|
||||||
|
ble_uuid_prefix: !secret ble_uuid_prefix
|
||||||
|
|
||||||
|
# Image source prefix. Full URL fetched per refresh is:
|
||||||
|
# ${image_url_prefix}/<wifi_sta_mac_lowercase_no_separators>.png
|
||||||
|
image_url_prefix: !secret image_url_prefix
|
||||||
|
|
||||||
|
# DEBUG: when "true", do_sleep does NOT enter deep sleep — it logs, delays
|
||||||
|
# wake_interval_s, resets per-cycle state, and re-runs the scan loop. Keeps
|
||||||
|
# UART<->USB alive so you can watch the device cycle in the serial console.
|
||||||
|
# Set to "false" to use real deep sleep (production).
|
||||||
|
debug_no_sleep: "false"
|
||||||
|
|
||||||
|
# DEBUG: when "true", do_ble_scan flashes GPIO21 with state-encoded blink
|
||||||
|
# patterns each wake (entered → BLE active? → matched?). Useful because
|
||||||
|
# USB-CDC drops on deep sleep so logger.log is invisible. Set to "false"
|
||||||
|
# in production to keep the wake silent and save a few mA.
|
||||||
|
debug_led_breadcrumbs: "false"
|
||||||
|
|
||||||
|
globals:
|
||||||
|
# Persisted across deep sleep via flash NVS.
|
||||||
|
- id: elapsed_since_ha_s
|
||||||
|
type: uint32_t
|
||||||
|
restore_value: yes
|
||||||
|
initial_value: '0'
|
||||||
|
- id: beacon_seen_count
|
||||||
|
type: uint32_t
|
||||||
|
restore_value: yes
|
||||||
|
initial_value: '0'
|
||||||
|
|
||||||
|
# Transient (per-boot).
|
||||||
|
- id: refresh_in_progress
|
||||||
|
type: bool
|
||||||
|
initial_value: 'false'
|
||||||
|
- id: image_loaded
|
||||||
|
type: bool
|
||||||
|
initial_value: 'false'
|
||||||
|
- id: beacon_matched
|
||||||
|
type: bool
|
||||||
|
initial_value: 'false'
|
||||||
|
- id: beacon_command
|
||||||
|
type: uint32_t
|
||||||
|
initial_value: '0' # (Major << 16) | Minor when matched
|
||||||
|
- id: cold_boot
|
||||||
|
type: bool
|
||||||
|
initial_value: 'false'
|
||||||
|
- id: heartbeat_due
|
||||||
|
type: bool
|
||||||
|
initial_value: 'false'
|
||||||
|
- id: stay_awake_mode
|
||||||
|
type: bool
|
||||||
|
initial_value: 'false'
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: frame
|
||||||
|
libraries:
|
||||||
|
- SPI
|
||||||
|
platformio_options:
|
||||||
|
board_build.arduino.memory_type: qio_opi
|
||||||
|
board_build.f_flash: 80000000L
|
||||||
|
board_build.flash_mode: qio
|
||||||
|
build_flags:
|
||||||
|
- "-DBOARD_HAS_PSRAM"
|
||||||
|
- "-mfix-esp32-psram-cache-issue"
|
||||||
|
lib_ignore:
|
||||||
|
- esp_insights
|
||||||
|
- esp_rainmaker
|
||||||
|
|
||||||
|
on_boot:
|
||||||
|
# WiFi is left disabled at boot (see wifi.enable_on_boot below). Each path
|
||||||
|
# that needs the radio — do_refresh / do_heartbeat / do_stay_awake — flips
|
||||||
|
# it on explicitly. Timer-wake / scan path never enables it.
|
||||||
|
- priority: -100
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
// Approximate elapsed-since-last-HA-contact: each timer cycle adds
|
||||||
|
// wake_interval_s. Refresh and heartbeat paths reset to 0.
|
||||||
|
id(elapsed_since_ha_s) += ${wake_interval_s};
|
||||||
|
id(heartbeat_due) = id(elapsed_since_ha_s) >= ${heartbeat_interval_s};
|
||||||
|
|
||||||
|
auto cause = esp_sleep_get_wakeup_cause();
|
||||||
|
switch (cause) {
|
||||||
|
case ESP_SLEEP_WAKEUP_UNDEFINED:
|
||||||
|
ESP_LOGI("frame", "Cold boot — forcing full refresh");
|
||||||
|
id(cold_boot) = true;
|
||||||
|
break;
|
||||||
|
case ESP_SLEEP_WAKEUP_EXT0:
|
||||||
|
ESP_LOGI("frame", "D2 button wake — stay-awake");
|
||||||
|
id(stay_awake_mode) = true;
|
||||||
|
break;
|
||||||
|
case ESP_SLEEP_WAKEUP_TIMER:
|
||||||
|
ESP_LOGD("frame", "Timer wake (elapsed=%us, heartbeat_due=%d)",
|
||||||
|
id(elapsed_since_ha_s), (int)id(heartbeat_due));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGW("frame", "Unknown wake cause %d — treating as cold boot", (int)cause);
|
||||||
|
id(cold_boot) = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t mac[6];
|
||||||
|
esphome::get_mac_address_raw(mac);
|
||||||
|
ESP_LOGD("frame", "WiFi STA MAC %02X:%02X:%02X:%02X:%02X:%02X",
|
||||||
|
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||||
|
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return id(cold_boot);'
|
||||||
|
then:
|
||||||
|
- script.execute: do_refresh
|
||||||
|
else:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return id(stay_awake_mode);'
|
||||||
|
then:
|
||||||
|
- script.execute: do_stay_awake
|
||||||
|
else:
|
||||||
|
- script.execute: do_ble_scan
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: seeed_xiao_esp32s3
|
||||||
|
variant: esp32s3
|
||||||
|
framework:
|
||||||
|
type: arduino
|
||||||
|
|
||||||
|
psram:
|
||||||
|
mode: octal
|
||||||
|
speed: 80MHz
|
||||||
|
|
||||||
|
external_components:
|
||||||
|
- source:
|
||||||
|
type: git
|
||||||
|
url: "https://github.com/acegallagher/esphome-bigink"
|
||||||
|
path: bigink_component
|
||||||
|
|
||||||
|
# D2 (GPIO3) is the hardware wake pin. Timer wakes come from sleep_duration
|
||||||
|
# passed to deep_sleep.enter calls in scripts.
|
||||||
|
deep_sleep:
|
||||||
|
id: sleep_control
|
||||||
|
wakeup_pin:
|
||||||
|
number: GPIO3
|
||||||
|
inverted: true
|
||||||
|
mode:
|
||||||
|
input: true
|
||||||
|
pullup: true
|
||||||
|
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
||||||
|
|
||||||
|
api:
|
||||||
|
|
||||||
|
ota:
|
||||||
|
- platform: esphome
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
# Never auto-connect at boot. The scripts (do_refresh / do_heartbeat /
|
||||||
|
# do_stay_awake) call wifi.enable when they need the radio, and disable
|
||||||
|
# it again on exit. Keeps the WiFi modem off during BLE-scan wakes.
|
||||||
|
enable_on_boot: false
|
||||||
|
ssid: !secret wifi_ssid
|
||||||
|
password: !secret wifi_password
|
||||||
|
fast_connect: true
|
||||||
|
power_save_mode: NONE
|
||||||
|
manual_ip:
|
||||||
|
static_ip: !secret static_ip
|
||||||
|
gateway: !secret gateway
|
||||||
|
subnet: !secret subnet
|
||||||
|
dns1: !secret dns1
|
||||||
|
|
||||||
|
http_request:
|
||||||
|
useragent: esphome/device
|
||||||
|
timeout: 30s
|
||||||
|
|
||||||
|
# Onboard user LED — lit while BLE is actively scanning (debug aid).
|
||||||
|
# GPIO21 is the user LED on the XIAO ESP32-S3 family; active-low.
|
||||||
|
output:
|
||||||
|
- platform: gpio
|
||||||
|
id: status_led
|
||||||
|
pin:
|
||||||
|
number: GPIO21
|
||||||
|
inverted: true
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Bluetooth — passive iBeacon scanner. Started on demand from do_ble_scan,
|
||||||
|
# not continuously, to keep the radio off most of the time.
|
||||||
|
#
|
||||||
|
# An explicit esp32_ble block (with id) lets do_ble_scan wait for the
|
||||||
|
# controller to reach ACTIVE before issuing start_scan — its init runs in
|
||||||
|
# loop(), not setup(), so it isn't ready when called from on_boot.
|
||||||
|
# =============================================================================
|
||||||
|
esp32_ble:
|
||||||
|
id: ble_id
|
||||||
|
|
||||||
|
esp32_ble_tracker:
|
||||||
|
scan_parameters:
|
||||||
|
interval: 50ms
|
||||||
|
window: 50ms
|
||||||
|
active: false
|
||||||
|
continuous: false
|
||||||
|
on_ble_manufacturer_data_advertise:
|
||||||
|
- manufacturer_id: '004C' # Apple — iBeacons live in their mfr data block
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
// iBeacon: 02 15 <16-byte UUID> <2B major> <2B minor> <1B tx>
|
||||||
|
if (x.size() < 23) return;
|
||||||
|
if (x[0] != 0x02 || x[1] != 0x15) return;
|
||||||
|
|
||||||
|
uint16_t major = ((uint16_t) x[18] << 8) | x[19];
|
||||||
|
uint16_t minor = ((uint16_t) x[20] << 8) | x[21];
|
||||||
|
|
||||||
|
// Log every iBeacon we see (debug visibility).
|
||||||
|
char uuid_str[33];
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
snprintf(uuid_str + 2 * i, 3, "%02X", x[2 + i]);
|
||||||
|
}
|
||||||
|
ESP_LOGI("frame", "iBeacon seen: uuid=%s major=0x%04X minor=0x%04X",
|
||||||
|
uuid_str, major, minor);
|
||||||
|
|
||||||
|
// Compose the expected UUID once: prefix || own WiFi STA MAC.
|
||||||
|
static uint8_t expected[16];
|
||||||
|
static bool built = false;
|
||||||
|
if (!built) {
|
||||||
|
const char *hex = "${ble_uuid_prefix}";
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
unsigned int b = 0;
|
||||||
|
sscanf(hex + 2 * i, "%2x", &b);
|
||||||
|
expected[i] = (uint8_t) b;
|
||||||
|
}
|
||||||
|
uint8_t mac[6];
|
||||||
|
esphome::get_mac_address_raw(mac);
|
||||||
|
memcpy(expected + 10, mac, 6);
|
||||||
|
built = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memcmp(&x[2], expected, 16) != 0) {
|
||||||
|
ESP_LOGD("frame", " → no match (not our UUID)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
id(beacon_matched) = true;
|
||||||
|
id(beacon_command) = ((uint32_t) major << 16) | minor;
|
||||||
|
id(beacon_seen_count) += 1;
|
||||||
|
ESP_LOGI("frame", " → MATCH (this device)");
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Online Image — downloads PNG into PSRAM. The url here is a placeholder;
|
||||||
|
# do_refresh rewrites it to ${image_url_prefix}/<mac>.png before each fetch.
|
||||||
|
# =============================================================================
|
||||||
|
online_image:
|
||||||
|
- url: "${image_url_prefix}/placeholder.png"
|
||||||
|
id: my_image
|
||||||
|
format: PNG
|
||||||
|
type: RGB
|
||||||
|
resize: 1600x1200
|
||||||
|
on_download_finished:
|
||||||
|
- logger.log: "Image downloaded"
|
||||||
|
- globals.set:
|
||||||
|
id: image_loaded
|
||||||
|
value: 'true'
|
||||||
|
- logger.log: "Display rendered — pushing battery, then sleeping"
|
||||||
|
- lambda: 'id(battery).sample();'
|
||||||
|
- delay: 2s
|
||||||
|
- globals.set:
|
||||||
|
id: elapsed_since_ha_s
|
||||||
|
value: '0'
|
||||||
|
- globals.set:
|
||||||
|
id: refresh_in_progress
|
||||||
|
value: 'false'
|
||||||
|
- wifi.disable:
|
||||||
|
- delay: 200ms
|
||||||
|
- lambda: |-
|
||||||
|
if(!cached) {
|
||||||
|
id(eink_display)->update();
|
||||||
|
};
|
||||||
|
- script.execute: do_sleep
|
||||||
|
|
||||||
|
on_error:
|
||||||
|
- logger.log: "Image download failed — sleeping; next wake retries if beacon still on"
|
||||||
|
- globals.set:
|
||||||
|
id: refresh_in_progress
|
||||||
|
value: 'false'
|
||||||
|
- wifi.disable:
|
||||||
|
- delay: 200ms
|
||||||
|
- script.execute: do_sleep
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Scripts — the state machine.
|
||||||
|
# =============================================================================
|
||||||
|
script:
|
||||||
|
# GPIO21 status-LED breadcrumbs (no UART available during deep-sleep wakes).
|
||||||
|
# Pattern per wake: 1 blink (entered) → 2 or 5 blinks (BLE active? yes/no)
|
||||||
|
# → solid during scan → 3 blinks (matched) or 1 long blink (no match).
|
||||||
|
- id: led_blink
|
||||||
|
parameters:
|
||||||
|
count: int
|
||||||
|
then:
|
||||||
|
- repeat:
|
||||||
|
count: !lambda 'return count;'
|
||||||
|
then:
|
||||||
|
- output.turn_on: status_led
|
||||||
|
- delay: 80ms
|
||||||
|
- output.turn_off: status_led
|
||||||
|
- delay: 120ms
|
||||||
|
|
||||||
|
- id: do_ble_scan
|
||||||
|
then:
|
||||||
|
- logger.log: "Starting BLE scan"
|
||||||
|
# Breadcrumb A: 1 blink — do_ble_scan entered.
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return ${debug_led_breadcrumbs};'
|
||||||
|
then:
|
||||||
|
- script.execute:
|
||||||
|
id: led_blink
|
||||||
|
count: 1
|
||||||
|
- script.wait: led_blink
|
||||||
|
# Wait for the BLE controller to finish its async init. The component's
|
||||||
|
# setup() only flags state=ENABLE; the actual esp_bt_controller_init /
|
||||||
|
# bluedroid_init / GAP-callback dance runs in loop(), with a hard-coded
|
||||||
|
# ~200ms internal delay before flipping to ACTIVE. start_scan silently
|
||||||
|
# no-ops if invoked beforehand — which was the deep-sleep bug.
|
||||||
|
- wait_until:
|
||||||
|
condition:
|
||||||
|
lambda: 'return id(ble_id).is_active();'
|
||||||
|
timeout: 3s
|
||||||
|
# Breadcrumb B: 2 blinks if BLE active, 5 blinks if not (= still broken).
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return ${debug_led_breadcrumbs};'
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return id(ble_id).is_active();'
|
||||||
|
then:
|
||||||
|
- script.execute:
|
||||||
|
id: led_blink
|
||||||
|
count: 2
|
||||||
|
else:
|
||||||
|
- script.execute:
|
||||||
|
id: led_blink
|
||||||
|
count: 5
|
||||||
|
- script.wait: led_blink
|
||||||
|
- delay: 150ms
|
||||||
|
- output.turn_on: status_led
|
||||||
|
- esp32_ble_tracker.start_scan:
|
||||||
|
continuous: false
|
||||||
|
- delay: ${scan_duration_ms}ms
|
||||||
|
- esp32_ble_tracker.stop_scan:
|
||||||
|
- output.turn_off: status_led
|
||||||
|
- delay: 200ms
|
||||||
|
- logger.log:
|
||||||
|
format: "Scan done matched=%d cmd=0x%08X"
|
||||||
|
args: ['(int)id(beacon_matched)', 'id(beacon_command)']
|
||||||
|
# Breadcrumb C: 3 blinks if matched, 1 long blink if not.
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return ${debug_led_breadcrumbs};'
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return id(beacon_matched);'
|
||||||
|
then:
|
||||||
|
- script.execute:
|
||||||
|
id: led_blink
|
||||||
|
count: 3
|
||||||
|
- script.wait: led_blink
|
||||||
|
else:
|
||||||
|
- output.turn_on: status_led
|
||||||
|
- delay: 500ms
|
||||||
|
- output.turn_off: status_led
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return id(beacon_matched);'
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return id(beacon_command) == 0x00000000;' # REFRESH
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return id(elapsed_since_ha_s) >= ${debounce_s};'
|
||||||
|
then:
|
||||||
|
- script.execute: do_refresh
|
||||||
|
else:
|
||||||
|
- logger.log: "REFRESH inside debounce window — skip"
|
||||||
|
- script.execute: do_sleep
|
||||||
|
else:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return id(beacon_command) == 0x00010001;' # STAY AWAKE
|
||||||
|
then:
|
||||||
|
- globals.set:
|
||||||
|
id: stay_awake_mode
|
||||||
|
value: 'true'
|
||||||
|
- script.execute: do_stay_awake
|
||||||
|
else:
|
||||||
|
- logger.log: "Beacon match with reserved command — ignored"
|
||||||
|
- script.execute: do_sleep
|
||||||
|
else:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return id(heartbeat_due);'
|
||||||
|
then:
|
||||||
|
- script.execute: do_heartbeat
|
||||||
|
else:
|
||||||
|
- script.execute: do_sleep
|
||||||
|
|
||||||
|
- id: do_refresh
|
||||||
|
then:
|
||||||
|
- logger.log: "REFRESH — WiFi up"
|
||||||
|
- globals.set:
|
||||||
|
id: refresh_in_progress
|
||||||
|
value: 'true'
|
||||||
|
- wifi.enable:
|
||||||
|
- wait_until:
|
||||||
|
condition:
|
||||||
|
wifi.connected:
|
||||||
|
timeout: 10s
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
wifi.connected:
|
||||||
|
then:
|
||||||
|
- online_image.set_url:
|
||||||
|
id: my_image
|
||||||
|
url: !lambda |-
|
||||||
|
uint8_t mac[6];
|
||||||
|
esphome::get_mac_address_raw(mac);
|
||||||
|
char url[128];
|
||||||
|
snprintf(url, sizeof(url),
|
||||||
|
"%s/%02x%02x%02x%02x%02x%02x.png",
|
||||||
|
"${image_url_prefix}",
|
||||||
|
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||||
|
ESP_LOGI("frame", "WiFi connected — fetching %s", url);
|
||||||
|
return std::string(url);
|
||||||
|
- component.update: my_image
|
||||||
|
# Flow continues from online_image on_download_finished / on_error.
|
||||||
|
else:
|
||||||
|
- logger.log: "WiFi connect timed out — sleeping, retry next wake"
|
||||||
|
- globals.set:
|
||||||
|
id: refresh_in_progress
|
||||||
|
value: 'false'
|
||||||
|
- wifi.disable:
|
||||||
|
- script.execute: do_sleep
|
||||||
|
|
||||||
|
- id: do_heartbeat
|
||||||
|
then:
|
||||||
|
- logger.log: "HEARTBEAT — pushing battery to HA"
|
||||||
|
- wifi.enable:
|
||||||
|
- wait_until:
|
||||||
|
condition:
|
||||||
|
wifi.connected:
|
||||||
|
timeout: 10s
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
wifi.connected:
|
||||||
|
then:
|
||||||
|
- lambda: 'id(battery).sample();'
|
||||||
|
- delay: 3s
|
||||||
|
- globals.set:
|
||||||
|
id: elapsed_since_ha_s
|
||||||
|
value: '0'
|
||||||
|
- logger.log: "Heartbeat done"
|
||||||
|
else:
|
||||||
|
- logger.log: "Heartbeat: WiFi failed"
|
||||||
|
- wifi.disable:
|
||||||
|
- delay: 200ms
|
||||||
|
- script.execute: do_sleep
|
||||||
|
|
||||||
|
- id: do_stay_awake
|
||||||
|
then:
|
||||||
|
- logger.log: "STAY-AWAKE — WiFi up; the 30s poll will sleep when HA flips ha_stay_awake off"
|
||||||
|
- globals.set:
|
||||||
|
id: stay_awake_mode
|
||||||
|
value: 'true'
|
||||||
|
- wifi.enable:
|
||||||
|
- wait_until:
|
||||||
|
condition:
|
||||||
|
wifi.connected:
|
||||||
|
timeout: 15s
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
wifi.connected:
|
||||||
|
then:
|
||||||
|
- delay: 5s
|
||||||
|
- lambda: 'id(battery).sample();'
|
||||||
|
- globals.set:
|
||||||
|
id: elapsed_since_ha_s
|
||||||
|
value: '0'
|
||||||
|
- logger.log: "Stay-awake ready"
|
||||||
|
else:
|
||||||
|
- logger.log: "Stay-awake: WiFi failed — sleeping"
|
||||||
|
- wifi.disable:
|
||||||
|
- script.execute: do_sleep
|
||||||
|
|
||||||
|
- id: do_sleep
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return ${debug_no_sleep};'
|
||||||
|
then:
|
||||||
|
# DEBUG: simulate the deep-sleep + wake cycle without dropping UART.
|
||||||
|
- logger.log:
|
||||||
|
format: "[DEBUG] would deep_sleep for %ds (elapsed_since_ha=%us); simulating"
|
||||||
|
args: ['(int)${wake_interval_s}', 'id(elapsed_since_ha_s)']
|
||||||
|
- delay: ${wake_interval_s}s
|
||||||
|
- logger.log: "[DEBUG] simulated wake — running timer-wake path"
|
||||||
|
# Reset transient per-boot state, mimic on_boot's timer-wake branch.
|
||||||
|
- globals.set:
|
||||||
|
id: beacon_matched
|
||||||
|
value: 'false'
|
||||||
|
- globals.set:
|
||||||
|
id: beacon_command
|
||||||
|
value: '0'
|
||||||
|
- globals.set:
|
||||||
|
id: cold_boot
|
||||||
|
value: 'false'
|
||||||
|
- lambda: |-
|
||||||
|
id(elapsed_since_ha_s) += ${wake_interval_s};
|
||||||
|
id(heartbeat_due) = id(elapsed_since_ha_s) >= ${heartbeat_interval_s};
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return id(stay_awake_mode);'
|
||||||
|
then:
|
||||||
|
- logger.log: "[DEBUG] stay_awake_mode active — skipping scan loop"
|
||||||
|
else:
|
||||||
|
- script.execute: do_ble_scan
|
||||||
|
else:
|
||||||
|
- logger.log:
|
||||||
|
format: "Sleeping for %ds (elapsed_since_ha=%us)"
|
||||||
|
args: ['(int)${wake_interval_s}', 'id(elapsed_since_ha_s)']
|
||||||
|
- deep_sleep.enter:
|
||||||
|
id: sleep_control
|
||||||
|
sleep_duration: !lambda 'return ${wake_interval_s} * 1000UL;'
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Stay-awake poll — fires only while we're alive (i.e., in stay-awake mode).
|
||||||
|
# When HA clears the flag, sleep.
|
||||||
|
# =============================================================================
|
||||||
|
interval:
|
||||||
|
- interval: 30s
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
and:
|
||||||
|
- lambda: 'return id(stay_awake_mode);'
|
||||||
|
- wifi.connected:
|
||||||
|
- binary_sensor.is_off: ha_stay_awake
|
||||||
|
then:
|
||||||
|
- logger.log: "Stay-awake cleared — returning to sleep loop"
|
||||||
|
- globals.set:
|
||||||
|
id: stay_awake_mode
|
||||||
|
value: 'false'
|
||||||
|
- wifi.disable:
|
||||||
|
- script.execute: do_sleep
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Display
|
||||||
|
# =============================================================================
|
||||||
|
display:
|
||||||
|
- platform: seeed_epaper_spectra6
|
||||||
|
id: eink_display
|
||||||
|
|
||||||
|
clk_pin: 7
|
||||||
|
mosi_pin: 9
|
||||||
|
cs_master_pin: GPIO44
|
||||||
|
cs_slave_pin: GPIO41
|
||||||
|
dc_pin: GPIO10
|
||||||
|
reset_pin: GPIO38
|
||||||
|
power_pin: GPIO43
|
||||||
|
busy_pin:
|
||||||
|
number: GPIO4
|
||||||
|
inverted: true
|
||||||
|
mode:
|
||||||
|
input: true
|
||||||
|
pullup: true
|
||||||
|
|
||||||
|
flip_mode: false
|
||||||
|
update_interval: never
|
||||||
|
|
||||||
|
lambda: |-
|
||||||
|
if (id(image_loaded)) {
|
||||||
|
it.image(0,0, id(my_image));
|
||||||
|
} else {
|
||||||
|
auto BLACK = Color(0, 0, 0);
|
||||||
|
auto WHITE = Color(255, 255, 255);
|
||||||
|
auto RED = Color(255, 0, 0);
|
||||||
|
auto GRAY = Color(128, 128, 128);
|
||||||
|
|
||||||
|
it.fill(WHITE);
|
||||||
|
|
||||||
|
it.rectangle(20, 20, 1560, 1160, BLACK);
|
||||||
|
it.rectangle(22, 22, 1556, 1156, BLACK);
|
||||||
|
|
||||||
|
it.filled_circle(800, 380, 15, BLACK);
|
||||||
|
it.circle(800, 350, 80, BLACK);
|
||||||
|
it.circle(800, 350, 130, BLACK);
|
||||||
|
it.circle(800, 350, 180, BLACK);
|
||||||
|
it.filled_rectangle(600, 380, 400, 150, WHITE);
|
||||||
|
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
it.line(650+i, 220, 950+i, 450, RED);
|
||||||
|
it.line(650+i, 450, 950+i, 220, RED);
|
||||||
|
}
|
||||||
|
|
||||||
|
it.printf(800, 580, id(font_large), BLACK, TextAlign::CENTER, "No Connection");
|
||||||
|
it.printf(800, 680, id(font_medium), GRAY, TextAlign::CENTER, "Waiting for beacon...");
|
||||||
|
it.printf(800, 760, id(font_medium), GRAY, TextAlign::CENTER, "Press D2 to stay awake");
|
||||||
|
}
|
||||||
|
|
||||||
|
font:
|
||||||
|
- file: "gfonts://Roboto"
|
||||||
|
id: font_large
|
||||||
|
size: 80
|
||||||
|
- file: "gfonts://Roboto"
|
||||||
|
id: font_medium
|
||||||
|
size: 40
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Battery Voltage Monitoring via battery_monitor driver, logs recent
|
||||||
|
# samples and then averages them, looking up SOC via 3.7v
|
||||||
|
# lithium voltage->SOC LUT
|
||||||
|
#
|
||||||
|
# EE02 has voltage divider on GPIO1, enable pin on GPIO6
|
||||||
|
# =============================================================================
|
||||||
|
battery_monitor:
|
||||||
|
id: battery
|
||||||
|
voltage_pin: GPIO1
|
||||||
|
enable_pin: GPIO6
|
||||||
|
samples_to_average: 10
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: battery_monitor
|
||||||
|
battery_monitor_id: battery
|
||||||
|
voltage:
|
||||||
|
name: "Battery Voltage"
|
||||||
|
percentage:
|
||||||
|
name: "Battery Percentage"
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Beacons Seen"
|
||||||
|
accuracy_decimals: 0
|
||||||
|
lambda: 'return id(beacon_seen_count);'
|
||||||
|
update_interval: 60s
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: homeassistant
|
||||||
|
entity_id: input_boolean.bigink_stay_awake_request
|
||||||
|
id: ha_stay_awake
|
||||||
|
name: "Stay Awake Requested"
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Stay Awake Mode"
|
||||||
|
lambda: 'return id(stay_awake_mode);'
|
||||||
Reference in New Issue
Block a user