TRMNL DIY E-Ink Dashboard
Handy little dashboard that has been living on the fridge since May 2026 and saves me looking at the Sigenergy app every now and then. Today’s section for Calendar shows 4 events, tomorrow shows 2, Sat/Sun each shows 2 events. Built on the TRMNL 7.5" OG DIY Kit. Shows weather and refreshes every 60 minutes via deep sleep.

How It Works Link to heading
- ESP32 wakes from deep sleep (every 60 min)
- Connects to WiFi, fetches sensor/text states from Home Assistant
- Renders the layout to the e-ink framebuffer
- E-ink updates (~10 seconds)
- Enters deep sleep to save battery
┌──────────────────────────────────────────────────────────┐
│ Tuesday, 27 May [DevBat] │
├──────────────────────┬───────────────────────────────────┤
│ Today │ ☀️ (weather icon) │
│ ───────── │ 22°C │
│ 14:30 Meeting │ ▼12° ▲24° │
│ All-day Birthday │ │
│ │ ───────────────────────────── │
│ Tomorrow │ 🔋 (battery icon) │
│ ───────── │ 87% │
│ 09:00 Standup │ Discharging │
│ │ │
│ Weekend │ │
│ ───────── │ │
│ Sat 31 May │ │
│ All-day BBQ │ │
│ Sun 1 Jun │ │
│ No events │ │
├──────────────────────┴───────────────────────────────────┤
│ Built with ESPHome Refreshed at 08:15 │
└──────────────────────────────────────────────────────────┘
Home Assistant Setup Link to heading
Integrations Link to heading
Ensure these are enabled in Home Assistant:
- ESPHome
- Google Calendar
- A weather integration (Met.no is built-in; BoM, OpenWeatherMap, or Pirate Weather also work)
- A battery/solar integration (e.g. Sigenergy ESS via HACS, Enphase Envoy, Tesla Powerwall, etc.)
Input Text Helpers Link to heading
Create four helpers (configuration.yaml or UI > Settings > Devices & Services > Helpers):
input_text:
trmnl_calendar:
name: "TRMNL Calendar"
max: 255
trmnl_calendar_tomorrow:
name: "TRMNL Calendar Tomorrow"
max: 255
trmnl_calendar_sat:
name: "TRMNL Calendar Sat"
max: 255
trmnl_calendar_sun:
name: "TRMNL Calendar Sun"
max: 255
Template Sensors Link to heading
Add to configuration.yaml. The dashboard’s ESPHome lambda matches weather condition by exact string. The map below translates Met.no’s raw values (partlycloudy) to the labels expected by the display (Partly Cloudy).
template:
- sensor:
- name: "TRMNL Weather Temperature"
unique_id: trmnl_weather_temperature
state: "{{ state_attr('weather.forecast_home', 'temperature') }}"
unit_of_measurement: "°C"
- name: "TRMNL Weather Condition"
unique_id: trmnl_weather_condition
state: >
{% set cond = states('weather.forecast_home') %}
{% set map = {
'clear-night': 'Clear Night', 'cloudy': 'Cloudy', 'fog': 'Foggy',
'hail': 'Hail', 'lightning': 'Lightning', 'lightning-rainy': 'Stormy',
'partlycloudy': 'Partly Cloudy', 'pouring': 'Heavy Rain',
'rainy': 'Rainy', 'snowy': 'Snowy', 'snowy-rainy': 'Sleet',
'sunny': 'Sunny', 'windy': 'Windy', 'windy-variant': 'Windy',
'exceptional': 'Exceptional' } %}
{{ map.get(cond, cond | title) }}
- name: "TRMNL Weather Low"
unique_id: trmnl_weather_low
state: >
{% set lo = states('sensor.your_temp_min_0') %}
{% if lo | float(-999) != -999 and lo | float(0) > -50 %}
{{ lo }}
{% else %}
{{ state_attr('weather.forecast_home', 'forecast')[0].templow | default(0) }}
{% endif %}
unit_of_measurement: "°C"
- name: "TRMNL Weather High"
unique_id: trmnl_weather_high
state: >
{% set hi = states('sensor.your_temp_max_0') %}
{% if hi | float(-999) != -999 and hi | float(0) > -50 %}
{{ hi }}
{% else %}
{{ state_attr('weather.forecast_home', 'forecast')[0].temperature | default(0) }}
{% endif %}
unit_of_measurement: "°C"
Calendar Automation Link to heading
Create this automation in Home Assistant (Settings > Automations > New Automation > Edit in YAML). It fetches 7 days of events in one call, formats them as "HH:MM Summary" or "All-day Summary", and writes to the four input_text helpers. Runs every 10 minutes and on HA startup.
alias: "TRMNL: Update Calendar Display"
description: >-
Fetch today's, tomorrow's and weekend calendar events for the TRMNL e-ink screen
triggers:
- trigger: time_pattern
minutes: "/10"
- trigger: homeassistant
event: start
actions:
- action: calendar.get_events
target:
entity_id: calendar.your_calendar
data:
start_date_time: >-
{{ now().replace(hour=0, minute=0, second=0, microsecond=0)
.strftime('%Y-%m-%dT%H:%M:%S') }}
duration:
days: 7
response_variable: cal_result
- variables:
cal_key: calendar.your_calendar
evts: >-
{{ cal_result.get(cal_key, {}).get('events', []) if cal_result is mapping else [] }}
today_str: "{{ now().strftime('%Y-%m-%d') }}"
tomorrow_str: "{{ (now() + timedelta(days=1)).strftime('%Y-%m-%d') }}"
sat_str: >-
{{ (now() + timedelta(days=(5 - now().weekday()) % 7)).strftime('%Y-%m-%d') }}
sun_str: >-
{{ (now() + timedelta(days=(6 - now().weekday()) % 7)).strftime('%Y-%m-%d') }}
# Today (upcoming events only)
- action: input_text.set_value
target:
entity_id: input_text.trmnl_calendar
data:
value: >
{%- set ns = namespace(lines=[]) %}
{%- for evt in evts %}
{%- set s = evt.start | string %}
{%- if s[:10] == today_str %}
{%- if 'T' in s %}
{%- if (s | as_datetime | as_local) > now() %}
{%- set ns.lines = ns.lines + [(s | as_datetime | as_local).strftime('%H:%M')
~ ' ' ~ (evt.summary | default('(no title)') | truncate(28, true))] %}
{%- endif %}
{%- else %}
{%- set ns.lines = ns.lines + ['All-day '
~ (evt.summary | default('(no title)') | truncate(28, true))] %}
{%- endif %}
{%- endif %}
{%- endfor %}
{{ ns.lines | join('\n') }}
# Tomorrow
- action: input_text.set_value
target:
entity_id: input_text.trmnl_calendar_tomorrow
data:
value: >
{%- set ns = namespace(lines=[]) %}
{%- for evt in evts %}
{%- set s = evt.start | string %}
{%- if s[:10] == tomorrow_str %}
{%- if 'T' in s %}
{%- set ns.lines = ns.lines + [(s | as_datetime | as_local).strftime('%H:%M')
~ ' ' ~ (evt.summary | default('(no title)') | truncate(28, true))] %}
{%- else %}
{%- set ns.lines = ns.lines + ['All-day '
~ (evt.summary | default('(no title)') | truncate(28, true))] %}
{%- endif %}
{%- endif %}
{%- endfor %}
{{ ns.lines | join('\n') }}
# Saturday
- action: input_text.set_value
target:
entity_id: input_text.trmnl_calendar_sat
data:
value: >
{%- set ns = namespace(lines=[]) %}
{%- for evt in evts %}
{%- set s = evt.start | string %}
{%- if s[:10] == sat_str %}
{%- if 'T' in s %}
{%- set ns.lines = ns.lines + [(s | as_datetime | as_local).strftime('%H:%M')
~ ' ' ~ (evt.summary | default('(no title)') | truncate(28, true))] %}
{%- else %}
{%- set ns.lines = ns.lines + ['All-day '
~ (evt.summary | default('(no title)') | truncate(28, true))] %}
{%- endif %}
{%- endif %}
{%- endfor %}
{{ ns.lines | join('\n') }}
# Sunday
- action: input_text.set_value
target:
entity_id: input_text.trmnl_calendar_sun
data:
value: >
{%- set ns = namespace(lines=[]) %}
{%- for evt in evts %}
{%- set s = evt.start | string %}
{%- if s[:10] == sun_str %}
{%- if 'T' in s %}
{%- set ns.lines = ns.lines + [(s | as_datetime | as_local).strftime('%H:%M')
~ ' ' ~ (evt.summary | default('(no title)') | truncate(28, true))] %}
{%- else %}
{%- set ns.lines = ns.lines + ['All-day '
~ (evt.summary | default('(no title)') | truncate(28, true))] %}
{%- endif %}
{%- endif %}
{%- endfor %}
{{ ns.lines | join('\n') }}
mode: single
max_exceeded: silent
ESPHome Config Link to heading
Fonts Link to heading
Place these in fonts/ inside your ESPHome config directory:
Secrets Link to heading
wifi_ssid: "YourWiFiName"
wifi_password: "YourWiFiPassword"
api_encryption_key: "your-base64-32-byte-key"
ota_password: "your-ota-password"
ap_password: "your-fallback-ap-password"
Generate an API key:
openssl rand -base64 32
Customize trmnl.yaml
Link to heading
Copy trmnl.yaml to your ESPHome config. Adjust these entity references:
Full trmnl.yaml
esphome:
name: "trmnl"
friendly_name: "trmnl"
on_boot:
priority: 600
then:
- output.turn_on: bsp_battery_enable
- delay: 45s
- component.update: eink
esp32:
board: esp32-s3-devkitc-1
framework:
type: arduino
logger:
level: DEBUG
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "Terminal Fallback Hotspot"
password: !secret ap_password
captive_portal:
deep_sleep:
run_duration: 180s
sleep_duration: 60min
output:
- platform: gpio
pin: GPIO6
id: bsp_battery_enable
spi:
clk_pin: GPIO7
mosi_pin: GPIO9
time:
- platform: homeassistant
id: ha_time
timezone: Australia/Sydney
sensor:
- platform: adc
pin: GPIO1
id: device_battery_voltage
name: "TRMNL Battery Voltage"
attenuation: 12db
update_interval: 30s
filters:
- multiply: 3.0
- platform: template
id: device_battery_pct
name: "TRMNL Battery Level"
unit_of_measurement: "%"
lambda: "return id(device_battery_voltage).state;"
update_interval: 30s
filters:
- calibrate_linear:
- 4.20 -> 100
- 4.10 -> 90
- 4.00 -> 80
- 3.90 -> 70
- 3.80 -> 60
- 3.70 -> 50
- 3.60 -> 40
- 3.50 -> 30
- 3.40 -> 20
- 3.30 -> 10
- 2.50 -> 5
- 2.00 -> 0
- clamp:
min_value: 0
max_value: 100
- platform: homeassistant
id: battery_soc
entity_id: sensor.your_battery_soc
- platform: homeassistant
id: weather_temp
entity_id: sensor.trmnl_weather_temperature
- platform: homeassistant
id: weather_lo
entity_id: sensor.trmnl_weather_low
- platform: homeassistant
id: weather_hi
entity_id: sensor.trmnl_weather_high
text_sensor:
- platform: homeassistant
id: calendar_today
entity_id: input_text.trmnl_calendar
- platform: homeassistant
id: calendar_tomorrow
entity_id: input_text.trmnl_calendar_tomorrow
- platform: homeassistant
id: calendar_sat
entity_id: input_text.trmnl_calendar_sat
- platform: homeassistant
id: calendar_sun
entity_id: input_text.trmnl_calendar_sun
- platform: homeassistant
id: weather_condition
entity_id: sensor.trmnl_weather_condition
binary_sensor:
- platform: homeassistant
id: bat_charging
entity_id: binary_sensor.your_battery_charging
- platform: homeassistant
id: bat_discharging
entity_id: binary_sensor.your_battery_discharging
font:
- file: "fonts/Roboto-Regular.ttf"
id: font_title
size: 36
- file: "fonts/Roboto-Bold.ttf"
id: font_bold
size: 22
- file: "fonts/Roboto-Regular.ttf"
id: font_header
size: 22
- file: "fonts/Roboto-Regular.ttf"
id: font_body
size: 18
- file: "fonts/Roboto-Regular.ttf"
id: font_body_small
size: 16
- file: "fonts/Roboto-Regular.ttf"
id: font_pct
size: 36
- file: "fonts/Roboto-Regular.ttf"
id: font_pct_small
size: 28
- file: "fonts/Roboto-Regular.ttf"
id: font_footnote
size: 14
- file: "fonts/materialdesignicons-webfont.ttf"
id: font_mdi_large
size: 80
glyphs:
- "\U000F0599"
- "\U000F0595"
- "\U000F0F2F"
- "\U000F0597"
- "\U000F0598"
- "\U000F059B"
- "\U000F0594"
- "\U000F0596"
- "\U000F1902"
- file: "fonts/materialdesignicons-webfont.ttf"
id: font_mdi_small
size: 22
glyphs:
- "\U000F0E02"
- "\U000F0E03"
color:
- id: black
red: 0%
green: 0%
blue: 0%
- id: white
red: 100%
green: 100%
blue: 100%
display:
- platform: waveshare_epaper
id: eink
model: 7.50inv2
cs_pin: GPIO44
dc_pin: GPIO10
reset_pin: GPIO38
busy_pin:
number: GPIO4
inverted: true
update_interval: never
lambda: |-
ESP_LOGD("epaper", "=== Display lambda started ===");
ESP_LOGD("epaper", "battery_soc has_state: %s", id(battery_soc).has_state() ? "yes" : "no");
ESP_LOGD("epaper", "weather_temp has_state: %s", id(weather_temp).has_state() ? "yes" : "no");
if (!id(battery_soc).has_state() || !id(weather_temp).has_state()) {
ESP_LOGD("epaper", "Sensors not ready, skipping update");
return;
}
ESP_LOGD("epaper", "Proceeding to draw...");
time_t now_ts = id(ha_time).now().timestamp;
struct tm now_lt;
localtime_r(&now_ts, &now_lt);
it.fill(id(black));
// ═══ HEADER ═══
char hdr_buf[48];
strftime(hdr_buf, sizeof(hdr_buf), "%A, %d %B", &now_lt);
it.print(400, 8, id(font_title), id(white), TextAlign::TOP_CENTER, hdr_buf);
// ═══ TOP-RIGHT DEVICE BATTERY ═══
float dbat = id(device_battery_pct).state;
if (std::isnan(dbat)) dbat = 0;
int bx = 740, by = 14, bw = 40, bh = 16;
it.rectangle(bx, by, bw, bh, id(white));
it.filled_rectangle(bx + bw, by + 4, 3, 8, id(white));
int fill_w = (int)((bw - 4) * dbat / 100.0);
if (fill_w < 0) fill_w = 0;
if (fill_w > bw - 4) fill_w = bw - 4;
if (fill_w > 0)
it.filled_rectangle(bx + 2, by + 2, fill_w, bh - 4, id(white));
auto num_color = (dbat > 50) ? id(black) : id(white);
it.printf(bx + bw/2, by + bh/2 + 1, id(font_footnote), num_color,
TextAlign::CENTER, "%.0f", dbat);
// ═══ AESTHETIC SECTION DIVIDERS ═══
it.line(20, 50, 780, 50, id(white));
it.line(410, 50, 410, 445, id(white));
it.line(20, 445, 780, 445, id(white));
// ═══ TODAY — bold heading, regular events ═══
it.print(20, 65, id(font_bold), id(white), "Today");
it.line(20, 92, 390, 92, id(white));
int y = 112;
std::string today_txt = id(calendar_today).state;
if (today_txt.empty() || today_txt == "unknown") {
it.print(20, y, id(font_body), id(white), "No events");
y += 22;
} else {
std::string line;
for (size_t i = 0; i < today_txt.length(); i++) {
if (today_txt[i] == '\n') {
if (!line.empty() && y < 210) {
it.print(20, y, id(font_body), id(white), line.c_str());
y += 22;
line.clear();
}
} else {
line += today_txt[i];
}
}
if (!line.empty() && y < 210) {
it.print(20, y, id(font_body), id(white), line.c_str());
y += 22;
}
}
// ═══ TOMORROW — regular heading, compact events ═══
y = 225;
it.print(20, y, id(font_header), id(white), "Tomorrow");
it.line(20, y + 27, 390, y + 27, id(white));
y += 35;
std::string tomorrow_txt = id(calendar_tomorrow).state;
if (tomorrow_txt.empty() || tomorrow_txt == "unknown") {
it.print(20, y, id(font_body_small), id(white), "No events");
y += 18;
} else {
std::string line;
for (size_t i = 0; i < tomorrow_txt.length(); i++) {
if (tomorrow_txt[i] == '\n') {
if (!line.empty() && y < 295) {
it.print(20, y, id(font_body_small), id(white), line.c_str());
y += 18;
line.clear();
}
} else {
line += tomorrow_txt[i];
}
}
if (!line.empty() && y < 295) {
it.print(20, y, id(font_body_small), id(white), line.c_str());
y += 18;
}
}
// ═══ WEEKEND — small heading, date labels, tiny events ═══
y = 300;
it.print(20, y, id(font_body_small), id(white), "Weekend");
it.line(20, y + 25, 390, y + 25, id(white));
int wd = now_lt.tm_wday;
int days_to_sat = (6 - wd + 7) % 7;
int days_to_sun = (7 - wd + 7) % 7;
if (days_to_sat == 0 && wd != 6) days_to_sat = 7;
if (days_to_sun == 0 && wd != 0) days_to_sun = 7;
time_t sat_ts = now_ts + (days_to_sat * 86400);
time_t sun_ts = now_ts + (days_to_sun * 86400);
struct tm sat_tm, sun_tm;
localtime_r(&sat_ts, &sat_tm);
localtime_r(&sun_ts, &sun_tm);
char sat_label[16], sun_label[16];
strftime(sat_label, sizeof(sat_label), "%a %d %b", &sat_tm);
strftime(sun_label, sizeof(sun_label), "%a %d %b", &sun_tm);
y += 28;
it.printf(20, y, id(font_body_small), id(white), "%s", sat_label);
y += 17;
std::string sat_txt = id(calendar_sat).state;
if (sat_txt.empty() || sat_txt == "unknown") {
it.print(28, y, id(font_footnote), id(white), "No events");
y += 15;
} else {
std::string line;
for (size_t i = 0; i < sat_txt.length(); i++) {
if (sat_txt[i] == '\n') {
if (!line.empty() && y < 444) {
it.print(28, y, id(font_footnote), id(white), line.c_str());
y += 15;
line.clear();
}
} else {
line += sat_txt[i];
}
}
if (!line.empty() && y < 444) {
it.print(28, y, id(font_footnote), id(white), line.c_str());
y += 15;
}
}
y += 4;
it.printf(20, y, id(font_body_small), id(white), "%s", sun_label);
y += 17;
std::string sun_txt = id(calendar_sun).state;
if (sun_txt.empty() || sun_txt == "unknown") {
it.print(28, y, id(font_footnote), id(white), "No events");
y += 15;
} else {
std::string line;
for (size_t i = 0; i < sun_txt.length(); i++) {
if (sun_txt[i] == '\n') {
if (!line.empty() && y < 444) {
it.print(28, y, id(font_footnote), id(white), line.c_str());
y += 15;
line.clear();
}
} else {
line += sun_txt[i];
}
}
if (!line.empty() && y < 444) {
it.print(28, y, id(font_footnote), id(white), line.c_str());
y += 15;
}
}
// ═══ RIGHT COLUMN: WEATHER ═══
it.line(410, 65, 410, 430, id(white));
std::string cond = id(weather_condition).state;
std::string wicon = "\U000F0599";
if (cond == "Clear Night") wicon = "\U000F0594";
else if (cond == "Cloudy") wicon = "\U000F0F2F";
else if (cond == "Foggy") wicon = "\U000F0594";
else if (cond == "Partly Cloudy") wicon = "\U000F0595";
else if (cond == "Heavy Rain") wicon = "\U000F0597";
else if (cond == "Rainy") wicon = "\U000F0597";
else if (cond == "Snowy") wicon = "\U000F0598";
else if (cond == "Sleet") wicon = "\U000F0598";
else if (cond == "Sunny") wicon = "\U000F0599";
else if (cond == "Windy") wicon = "\U000F059B";
else if (cond == "Windy Variant") wicon = "\U000F059B";
else if (cond == "Stormy") wicon = "\U000F0596";
it.printf(605, 70, id(font_mdi_large), id(white), TextAlign::TOP_CENTER, "%s", wicon.c_str());
float tmp = id(weather_temp).state;
if (!std::isnan(tmp))
it.printf(605, 160, id(font_pct), id(white), TextAlign::CENTER, "%.0f°C", tmp);
else
it.print(605, 160, id(font_body), id(white), TextAlign::CENTER, "--°C");
float lo = id(weather_lo).state;
float hi = id(weather_hi).state;
it.printf(525, 210, id(font_mdi_small), id(white), TextAlign::TOP_CENTER, "\U000F0E02");
if (!std::isnan(lo))
it.printf(555, 212, id(font_body), id(white), "%.0f°", lo);
else
it.print(555, 212, id(font_body), id(white), "--°");
it.printf(645, 210, id(font_mdi_small), id(white), TextAlign::TOP_CENTER, "\U000F0E03");
if (!std::isnan(hi))
it.printf(675, 212, id(font_body), id(white), "%.0f°", hi);
else
it.print(675, 212, id(font_body), id(white), "--°");
// ═══ RIGHT COLUMN: SIGENERGY BATTERY ═══
it.line(430, 260, 780, 260, id(white));
it.printf(605, 275, id(font_mdi_large), id(white), TextAlign::TOP_CENTER, "\U000F1902");
float bat = id(battery_soc).state;
if (!std::isnan(bat))
it.printf(605, 360, id(font_pct_small), id(white), TextAlign::TOP_CENTER, "%.0f%%", bat);
else
it.print(605, 360, id(font_body), id(white), TextAlign::TOP_CENTER, "--%");
std::string bat_status;
if (id(bat_charging).state)
bat_status = "Charging";
else if (id(bat_discharging).state)
bat_status = "Discharging";
else
bat_status = "Idle";
it.printf(605, 400, id(font_body), id(white), TextAlign::TOP_CENTER, "%s", bat_status.c_str());
// ═══ FOOTER ═══
it.print(20, 455, id(font_footnote), id(white), "Built with ESPHome");
char ft_buf[32];
strftime(ft_buf, sizeof(ft_buf), "Display refreshed at %H:%M", &now_lt);
it.print(780, 455, id(font_footnote), id(white), TextAlign::TOP_RIGHT, ft_buf);
ESP_LOGD("epaper", "=== Display lambda completed ===");
Entity ID in trmnl.yaml |
Replace with yours |
|---|---|
sensor.your_battery_soc |
Your battery SoC sensor |
binary_sensor.your_battery_charging |
Your battery charging binary sensor |
binary_sensor.your_battery_discharging |
Your battery discharging binary sensor |
sensor.trmnl_weather_temperature |
Created above (keep same name) |
sensor.trmnl_weather_low |
Created above (keep same name) |
sensor.trmnl_weather_condition |
Created above (keep same name) |
sensor.trmnl_weather_high |
Created above (keep same name) |
input_text.trmnl_calendar |
Created above (keep same name) |
input_text.trmnl_calendar_tomorrow |
Created above (keep same name) |
input_text.trmnl_calendar_sat |
Created above (keep same name) |
input_text.trmnl_calendar_sun |
Created above (keep same name) |
Also set:
time.timezone: your IANA timezonedeep_sleep.sleep_duration: refresh interval (default:60min)- Footer text: customize or remove
Weather Icons Link to heading
If your weather integration uses different condition labels, update the wicon if-else chain in the lambda to match.
Enclosure Link to heading
I used the TRMNL enclosure and drilled small holes in the back for 2mm round magnets so it sticks to any magnetic surface.
Battery Life Link to heading
~2000mAh LiPo with 60-minute refresh, roughly 4-6 weeks per charge.
Customization Ideas Link to heading
- Multiple calendars: add more
entity_identries tocalendar.get_eventsand merge results - Different weather source: swap
weather.forecast_homewith your entity, update the condition map - More sensors: the right column has space, extend the lambda to draw additional elements
- Faster refresh: reduce
sleep_duration(e.g.30min), at the cost of battery life
Troubleshooting Link to heading
| Symptom | Fix |
|---|---|
| Blank screen | Verify display model is 7.50inv2 in YAML; check GPIO wiring matches the Seeed guide |
| “No events” always | Restart the calendar automation; verify calendar entity ID |
| Weather shows “–°C” | Template sensors not updating; check entity in Developer Tools > States |
| Battery shows 0% | Verify ADC wiring and voltage divider multiplier in sensor block |
| Won’t wake from deep sleep | Hold BOOT, press RESET, release BOOT; re-flash via USB |