Skip to content

Commit b071430

Browse files
authored
powerbread: new breadboard power monitor app (QT Py ESP32-C3) (#53)
* docs: add powerbread app design spec * docs: add powerbread implementation plan * powerbread: scaffold new app (QT Py ESP32-C3) * powerbread: add INA3221 sampler (100 Hz, stats, energy, chart ring) * docs: powerbread plan — fix fmt sign-loss and app_version include path * powerbread: fix CSV sign loss for small negative values * powerbread: add shell commands (read, reset, stream) * powerbread: add ST7735 display and LVGL dashboard * powerbread: add chart and stats screens * powerbread: add dial wheel input and mode/channel/dial shell commands * powerbread: avoid re-running ADC channel setup in dial command * powerbread: add README with wiring and usage * powerbread: drop unused pb_ui_get_mode, include errno.h explicitly * powerbread: hardware bring-up fixes, UI restyle, native_sim target
1 parent 2c9ec88 commit b071430

18 files changed

Lines changed: 2769 additions & 1 deletion
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
cmake_minimum_required(VERSION 3.20.0)
2+
3+
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
4+
project(powerbread)
5+
6+
target_sources(app PRIVATE
7+
src/main.c
8+
src/sampler.c
9+
src/shell.c
10+
src/ui.c
11+
src/input.c
12+
)

applications/powerbread/Kconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mainmenu "Application options"
2+
3+
source "Kconfig.zephyr"

applications/powerbread/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# PowerBread
2+
3+
Dual-channel breadboard power monitor on the Adafruit QT Py ESP32-C3, ported
4+
from [XIAO-powerbread](https://github.com/nicho810/XIAO-powerbread): an
5+
INA3221 measures voltage/current/power on two supply rails, a 0.96" ST7735
6+
LCD shows an LVGL UI (dashboard, line chart, statistics), and a resistor-ladder
7+
dial wheel navigates it. Measurements stream as CSV over USB serial on demand.
8+
9+
## Wiring
10+
11+
| Function | QT Py pin | GPIO | Notes |
12+
|-------------------|-----------|--------|--------------------------------|
13+
| INA3221 SDA/SCL | SDA/SCL | 5 / 6 | Stemma QT, addr 0x40, 50 mR shunts |
14+
| LCD DIN (MOSI) | MO | 7 | SPI2 |
15+
| LCD CLK | SCK | 10 | SPI2 |
16+
| LCD DC | MI | 8 | re-pinmuxed from SPI2 MISO |
17+
| LCD RST | A3 | 0 | |
18+
| LCD CS ||| tied low on the module |
19+
| Dial wheel ladder | A2 / D2 | 1 | ADC1 channel 1 |
20+
21+
## Controls
22+
23+
- Dial up/down: cycle mode (dashboard → chart → stats)
24+
- Dial short press: switch focused channel (1 ↔ 2)
25+
- Dial long press (≥1 s): reset stats and energy counters
26+
27+
Shell (USB serial), root command `powerbread`:
28+
`read`, `reset`, `stream <on|off>` (CSV: `t_ms,ch,mV,mA,mW`),
29+
`mode <dash|chart|stats>`, `channel <1|2>`, `dial` (raw dial mV, for
30+
calibrating the `adc-keys` thresholds in the board overlay).
31+
32+
## Build & flash
33+
34+
uv run poe app powerbread
35+
uv run poe flash powerbread
36+
37+
## Simulator
38+
39+
The UI runs on the host with synthetic sensor data (SDL window, no
40+
hardware needed) — useful for iterating on screens:
41+
42+
uv run poe app powerbread --board native_sim/native/64
43+
./builds/powerbread/zephyr/zephyr.exe -rt

applications/powerbread/VERSION

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
VERSION_MAJOR = 1
2+
VERSION_MINOR = 0
3+
PATCHLEVEL = 0
4+
VERSION_TWEAK = 0
5+
EXTRAVERSION =
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright (c) 2026 Richard Osterloh
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
#include <zephyr/dt-bindings/adc/adc.h>
8+
#include <zephyr/dt-bindings/input/input-event-codes.h>
9+
10+
&i2c0 {
11+
status = "okay";
12+
13+
ina3221: ina3221@40 {
14+
compatible = "ti,ina3221";
15+
reg = <0x40>;
16+
/* PowerBread schematic: 50 mOhm shunts on channels 1+2 */
17+
shunt-resistors = <50 50 50>;
18+
enable-channel = <1 1 0>;
19+
/* 140 us conversions, x4 averaging: full 2-ch cycle ~2.24 ms */
20+
conv-time-shunt = <0>;
21+
conv-time-bus = <0>;
22+
avg-mode = <1>;
23+
};
24+
};
25+
26+
/*
27+
* ST7735 0.96" 80x160 LCD, wired per the XIAO PowerBread schematic:
28+
* DIN=GPIO7 (SPI2 MOSI), CLK=GPIO10 (SPI2 SCLK), DC=GPIO8, RST=GPIO0,
29+
* CS tied low on the module (no CS GPIO), backlight hardwired on.
30+
* GPIO8 is SPIM2_MISO on the base board; re-pinmux SPI2 without MISO so
31+
* GPIO8 is free to drive the DC line (display is write-only).
32+
*/
33+
34+
&pinctrl {
35+
spim2_lcd: spim2_lcd {
36+
group1 {
37+
pinmux = <SPIM2_SCLK_GPIO10>;
38+
};
39+
group2 {
40+
pinmux = <SPIM2_MOSI_GPIO7>;
41+
output-low;
42+
};
43+
};
44+
};
45+
46+
&spi2 {
47+
pinctrl-0 = <&spim2_lcd>;
48+
pinctrl-names = "default";
49+
};
50+
51+
/ {
52+
chosen {
53+
zephyr,display = &st7735r;
54+
};
55+
56+
mipi_dbi {
57+
compatible = "zephyr,mipi-dbi-spi";
58+
spi-dev = <&spi2>;
59+
dc-gpios = <&gpio0 8 GPIO_ACTIVE_HIGH>;
60+
reset-gpios = <&gpio0 0 GPIO_ACTIVE_LOW>;
61+
write-only;
62+
#address-cells = <1>;
63+
#size-cells = <0>;
64+
65+
/* Panel init values from the in-tree heltec_wireless_tracker
66+
* (same 0.96" 160x80 ST7735 panel), rotated to portrait.
67+
*/
68+
st7735r: st7735r@0 {
69+
compatible = "sitronix,st7735r";
70+
reg = <0>;
71+
mipi-max-frequency = <20000000>;
72+
mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";
73+
width = <80>;
74+
height = <160>;
75+
/* Match upstream LGFX_096_XPB: invert=false, offset 24/0,
76+
* rotation 0 (no MX/MY), BGR color order.
77+
*/
78+
x-offset = <24>;
79+
y-offset = <0>;
80+
madctl = <0x00>;
81+
colmod = <0x05>;
82+
invctr = <7>;
83+
vmctr1 = <0x0e>;
84+
pwctr1 = [a2 02 84];
85+
pwctr2 = [c1];
86+
pwctr3 = [0a 00];
87+
pwctr4 = [8a 2a];
88+
pwctr5 = [8a ee];
89+
frmctr1 = [01 26 2e];
90+
frmctr2 = [01 26 2e];
91+
frmctr3 = [01 26 2e 01 26 2e];
92+
gamctrp1 = [0f 1a 0f 18 2f 28 20 22 1f 1b 23 37 00 07 02 10];
93+
gamctrn1 = [0f 1b 0f 17 33 2c 29 2e 30 2e 30 3b 00 07 03 10];
94+
};
95+
};
96+
};
97+
98+
/*
99+
* PowerBread dial wheel: resistor ladder on D2/A2 = GPIO1 = ADC1 channel 1.
100+
* Idle sits near 0 V; press/down/up produce distinct levels. Thresholds
101+
* derived from upstream's 10-bit values (XIAO ESP32-C3 build); calibrate
102+
* with `powerbread dial` and adjust here if the module differs.
103+
*/
104+
105+
&adc0 {
106+
status = "okay";
107+
#address-cells = <1>;
108+
#size-cells = <0>;
109+
110+
channel@1 {
111+
reg = <1>;
112+
zephyr,gain = "ADC_GAIN_1_4";
113+
zephyr,reference = "ADC_REF_INTERNAL";
114+
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
115+
zephyr,resolution = <12>;
116+
};
117+
};
118+
119+
/ {
120+
dial: dial {
121+
compatible = "adc-keys";
122+
io-channels = <&adc0 1>;
123+
keyup-threshold-mv = <0>;
124+
sample-period-ms = <20>;
125+
126+
dial_press {
127+
press-thresholds-mv = <510>;
128+
zephyr,code = <INPUT_KEY_ENTER>;
129+
};
130+
dial_down {
131+
press-thresholds-mv = <910>;
132+
zephyr,code = <INPUT_KEY_DOWN>;
133+
};
134+
dial_up {
135+
press-thresholds-mv = <1580>;
136+
zephyr,code = <INPUT_KEY_UP>;
137+
};
138+
};
139+
140+
longpress {
141+
compatible = "zephyr,input-longpress";
142+
input = <&dial>;
143+
input-codes = <INPUT_KEY_ENTER>;
144+
short-codes = <INPUT_KEY_A>;
145+
long-codes = <INPUT_KEY_B>;
146+
long-delay-ms = <1000>;
147+
};
148+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# native_sim: no I2C sensor or ADC dial hardware
2+
CONFIG_I2C=n
3+
CONFIG_SENSOR=n
4+
CONFIG_ADC=n
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright (c) 2026 Richard Osterloh
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*
6+
* native_sim: SDL window stands in for the ST7735 (80x160); no INA3221 or
7+
* dial hardware — the sampler falls back to synthetic data.
8+
*/
9+
10+
&sdl_dc {
11+
width = <80>;
12+
height = <160>;
13+
};

applications/powerbread/prj.conf

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Logging. CSV streaming printk must NOT be routed through the deferred
2+
# log core: packaging ~200 printk/s with %s args crashes the logging
3+
# thread (illegal instruction, mepc=0). With LOG_PRINTK=n printk writes
4+
# synchronously to the console from the sampler thread instead.
5+
CONFIG_LOG=y
6+
CONFIG_LOG_PRINTK=n
7+
8+
# Shell over USB serial (board chosen: zephyr,shell-uart = &usb_serial)
9+
CONFIG_SHELL=y
10+
11+
CONFIG_MAIN_STACK_SIZE=8192
12+
13+
# INA3221 current/voltage monitor
14+
CONFIG_I2C=y
15+
CONFIG_SENSOR=y
16+
17+
# ST7735 display via MIPI-DBI SPI
18+
CONFIG_SPI=y
19+
CONFIG_MIPI_DBI=y
20+
CONFIG_DISPLAY=y
21+
22+
# LVGL
23+
CONFIG_LVGL=y
24+
CONFIG_LV_COLOR_DEPTH_16=y
25+
CONFIG_LV_Z_MEM_POOL_SIZE=49152
26+
CONFIG_LV_Z_VDB_SIZE=100
27+
CONFIG_LV_USE_CHART=y
28+
CONFIG_LV_FONT_MONTSERRAT_12=y
29+
CONFIG_LV_FONT_MONTSERRAT_16=y
30+
CONFIG_LV_FONT_DEFAULT_MONTSERRAT_12=y
31+
32+
# Dial wheel (adc-keys resistor ladder + longpress)
33+
CONFIG_ADC=y
34+
CONFIG_INPUT=y
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) 2026 Richard Osterloh
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
#include "sampler.h"
8+
#include "ui.h"
9+
10+
#include <zephyr/input/input.h>
11+
12+
static void dial_cb(struct input_event *evt, void *user_data)
13+
{
14+
ARG_UNUSED(user_data);
15+
16+
if (evt->type != INPUT_EV_KEY || evt->value != 1) {
17+
return;
18+
}
19+
20+
switch (evt->code) {
21+
case INPUT_KEY_UP:
22+
pb_ui_step_mode(1);
23+
break;
24+
case INPUT_KEY_DOWN:
25+
pb_ui_step_mode(-1);
26+
break;
27+
case INPUT_KEY_A: /* dial short press */
28+
pb_ui_set_channel((pb_ui_get_channel() + 1) % PB_NUM_CH);
29+
break;
30+
case INPUT_KEY_B: /* dial long press */
31+
sampler_reset_stats();
32+
break;
33+
default:
34+
break;
35+
}
36+
}
37+
38+
INPUT_CALLBACK_DEFINE(NULL, dial_cb, NULL);

applications/powerbread/src/main.c

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2026 Richard Osterloh
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
#include "ui.h"
8+
9+
#include <lvgl.h>
10+
#include <zephyr/kernel.h>
11+
#include <zephyr/logging/log.h>
12+
13+
#include <zephyr/app_version.h>
14+
15+
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
16+
17+
#define REFRESH_LOOP_CAP_MS 50
18+
19+
int main(void)
20+
{
21+
LOG_INF("PowerBread %s", APP_VERSION_STRING);
22+
23+
if (pb_ui_init() != 0) {
24+
LOG_ERR("UI disabled; sampler and shell remain active");
25+
k_sleep(K_FOREVER);
26+
}
27+
28+
while (true) {
29+
uint32_t delay = lv_timer_handler();
30+
31+
k_msleep(MIN(delay, REFRESH_LOOP_CAP_MS));
32+
}
33+
return 0;
34+
}

0 commit comments

Comments
 (0)