Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f9f6c39
feat: add 11_Clock_Dashboard Arduino example (desk clock)
platima Oct 5, 2026
926d51c
fix: act on review comments (settings flash, OTA confirmation, weathe…
platima Oct 5, 2026
4e92d72
feat: calibrate the battery voltage, config.h defaults for the battery
platima Oct 5, 2026
226be1b
feat: charging bolt inside the battery gauge
platima Oct 5, 2026
24d3607
feat: every setting the SD card can set also has a build-time default
platima Oct 5, 2026
8b54bea
fix: an unplugged full battery no longer reads as full for hours
platima Oct 6, 2026
c6a5a71
feat: WiFi sync mode and an idle CPU clock, to draw less from the bat…
platima Oct 6, 2026
414ab58
feat: sync mode looks for a missing network; console and Spotify powe…
platima Oct 6, 2026
557e070
fix: Spotify's token and poll timers after 25 days of uptime
platima Oct 6, 2026
49845d7
chore: ignore exported firmware images
platima Oct 7, 2026
f7dc4fe
feat: hold KEY for five seconds to restart (and so read the SD card)
platima Oct 7, 2026
bfaa0a1
fix: say what is on an SD card that will not mount
platima Oct 7, 2026
ce78311
fix: the SD card updater checked the file from the wrong place
platima Oct 7, 2026
ff106cc
feat: version 1.4, with a build id on the System info page
platima Oct 7, 2026
f9ecc3c
fix: say what the card's settings did after a firmware update (1.5)
platima Oct 7, 2026
06f9786
fix: a runtime estimate under a light load, and no "full" without a c…
platima Oct 7, 2026
d00e5bd
feat: a sheet for logging battery runs, and a finer drain rate (1.6)
platima Oct 7, 2026
1eba95d
fix: the runtime estimate of a cell that reads over 4.20 V
platima Oct 7, 2026
f1383d7
feat: the clock keeps its own power log (power_log, 1.7)
platima Oct 7, 2026
dce4af3
test: a row copied with its length given (no compiler warning)
platima Oct 8, 2026
05e53d4
feat: something for those who hold on (1.8)
platima Oct 8, 2026
4c98952
feat: two cubes, and buttons for them
platima Oct 8, 2026
37be523
fix: the clock's outline is whole; KEY shoves the cube (1.9)
platima Oct 8, 2026
6127ca2
docs: TESTING.md, what still has to be seen on the board
platima Oct 17, 2026
6677b1b
docs: TESTING.md, the owner's meter readings
platima Oct 17, 2026
0206693
feat: light sleep between the frames (light_sleep, 1.10)
platima Oct 17, 2026
556173c
feat: the battery curve, measured (1.10)
platima Oct 20, 2026
9105ab9
feat: the display's low power mode (display_power, 1.11)
platima Oct 20, 2026
c0ee351
merge: light sleep and the display's low power mode (1.11)
platima Oct 22, 2026
b6237e8
feat: the display rests during the low-battery shutdown (1.12)
platima Oct 22, 2026
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
6 changes: 6 additions & 0 deletions 02_Example/Arduino/11_Clock_Dashboard/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Keep LF line ends in the working tree whatever core.autocrlf says: the shell scripts in tools/ run in
# WSL / Linux (a CR breaks them) and the tests compare docs/ESP32-S3-RLCD-Config.example.txt byte for byte.
* text=auto eol=lf
*.png binary
*.pgm binary
*.bin binary
12 changes: 12 additions & 0 deletions 02_Example/Arduino/11_Clock_Dashboard/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Local credentials - copy secrets.example.h to secrets.h
secrets.h

# Firmware images: what "Sketch > Export Compiled Binary" (or arduino-cli --export-binaries) writes here.
# They hold whatever secrets.h had (the WiFi password, the Spotify id), so they must never be committed.
/build/
/*.bin

# Host-side UI preview tool: build products
tools/ui_preview/build/
tools/ui_preview/out/
tools/tests/build/
1,632 changes: 1,632 additions & 0 deletions 02_Example/Arduino/11_Clock_Dashboard/11_Clock_Dashboard.ino

Large diffs are not rendered by default.

1,412 changes: 1,412 additions & 0 deletions 02_Example/Arduino/11_Clock_Dashboard/README.md

Large diffs are not rendered by default.

336 changes: 336 additions & 0 deletions 02_Example/Arduino/11_Clock_Dashboard/ST7305_U8g2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
#include "ST7305_U8g2.h"
#include "display_power.h"

#define SPI_CLK 24000000
#define ST7305_TILE_WIDTH 38
#define ST7305_TILE_HEIGHT 50
#define ST7305_BUFFER_ROW_BYTES (ST7305_TILE_WIDTH * 8)

static ST7305_U8g2 *g_lcd_instance = nullptr;

static u8x8_display_info_t st7305_display_info = {
/* chip_enable_level = */ 0,
/* chip_disable_level = */ 1,
/* post_chip_enable_wait_ns = */ 0,
/* pre_chip_disable_wait_ns = */ 0,
/* reset_pulse_width_ms = */ 20,
/* post_reset_wait_ms = */ 50,
/* sda_setup_time_ns = */ 0,
/* sck_pulse_width_ns = */ 0,
/* sck_clock_hz = */ SPI_CLK,
/* spi_mode = */ 0,
/* i2c_bus_clock_100kHz = */ 4,
/* data_setup_time_ns = */ 0,
/* write_pulse_width_ns = */ 0,
/* tile_width = */ ST7305_TILE_WIDTH,
/* tile_height = */ ST7305_TILE_HEIGHT,
/* default_x_offset = */ 0,
/* flip_mode_x_offset = */ 0,
/* pixel_width = */ 300,
/* pixel_height = */ 400
};

ST7305_U8g2::ST7305_U8g2(int sck, int mosi, int dc, int cs, int rst)
: _sck(sck), _mosi(mosi), _dc(dc), _cs(cs), _rst(rst)
{
_spi = new SPIClass(HSPI);
g_lcd_instance = this;
}

ST7305_U8g2::~ST7305_U8g2()
{
busRelease(); // end() takes the bus lock that the long transaction holds
if (_spi) {
_spi->end();
delete _spi;
}
if (_my_buf) {
free(_my_buf);
}
if (g_lcd_instance == this) {
g_lcd_instance = nullptr;
}
}

void ST7305_U8g2::_cmd(uint8_t cmd)
{
digitalWrite(_dc, LOW);
digitalWrite(_cs, LOW);
_spi->transfer(cmd);
digitalWrite(_cs, HIGH);
}

void ST7305_U8g2::_data(const uint8_t *data, size_t len)
{
digitalWrite(_dc, HIGH);
digitalWrite(_cs, LOW);
_spi->transferBytes((uint8_t *)data, nullptr, len);
digitalWrite(_cs, HIGH);
}

void ST7305_U8g2::_cmd_data(uint8_t cmd, const uint8_t *data, size_t len)
{
digitalWrite(_dc, LOW);
digitalWrite(_cs, LOW);
_spi->transfer(cmd);
if (len > 0) {
digitalWrite(_dc, HIGH);
_spi->transferBytes((uint8_t *)data, nullptr, len);
}
digitalWrite(_cs, HIGH);
}

void ST7305_U8g2::reset()
{
digitalWrite(_rst, HIGH);
delay(50);
digitalWrite(_rst, LOW);
delay(20);
digitalWrite(_rst, HIGH);
delay(50);
}

uint8_t ST7305_U8g2::u8x8_byte_custom(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
(void)u8x8;
(void)arg_int;
(void)arg_ptr;

switch (msg) {
case U8X8_MSG_BYTE_INIT:
case U8X8_MSG_BYTE_START_TRANSFER:
case U8X8_MSG_BYTE_END_TRANSFER:
case U8X8_MSG_BYTE_SET_DC:
case U8X8_MSG_BYTE_SEND:
default:
return 1;
}
}

uint8_t ST7305_U8g2::u8x8_d_st7305_custom(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
(void)arg_int;

if (!g_lcd_instance) {
return 0;
}

switch (msg) {
case U8X8_MSG_DISPLAY_SETUP_MEMORY:
u8x8_d_helper_display_setup_memory(u8x8, &st7305_display_info);
break;

case U8X8_MSG_DISPLAY_INIT:
g_lcd_instance->fullInit();
break;

case U8X8_MSG_DISPLAY_DRAW_TILE: {
u8x8_tile_t *tile = (u8x8_tile_t *)arg_ptr;
uint8_t cnt = tile->cnt;
uint8_t y_pos = tile->y_pos;
uint8_t x_pos = tile->x_pos;

int first_col = x_pos * 8;
int last_col = (x_pos + cnt) * 8 - 1;
if (last_col >= 300) {
last_col = 299;
}

int addr_start = 0x12 + first_col / 12;
int addr_end = 0x12 + last_col / 12;
int send_start = (addr_start - 0x12) * 3;
int send_cnt = (addr_end - addr_start + 1) * 3;

int addr_first_col = (addr_start - 0x12) * 12;
int addr_last_col = (addr_end - 0x12) * 12 + 11;
if (addr_last_col >= 300) {
addr_last_col = 299;
}

uint8_t *row_base = tile->tile_ptr - ((uint16_t)x_pos * 8U);

uint8_t col_bounds[] = {(uint8_t)(0x3C - addr_end), (uint8_t)(0x3C - addr_start)};
g_lcd_instance->_cmd_data(0x2A, col_bounds, sizeof(col_bounds));

uint8_t row_bounds[] = {(uint8_t)(y_pos * 4), (uint8_t)(y_pos * 4 + 3)};
g_lcd_instance->_cmd_data(0x2B, row_bounds, sizeof(row_bounds));

static const uint8_t st_lut[4][4] = {
{0x00, 0x80, 0x40, 0xC0},
{0x00, 0x20, 0x10, 0x30},
{0x00, 0x08, 0x04, 0x0C},
{0x00, 0x02, 0x01, 0x03},
};

// XOR mask applied to the U8g2 frame buffer: 0xFF flips every pixel.
const uint8_t inv = g_lcd_instance->_invert ? 0xFF : 0x00;

uint8_t all_rows[300] = {0};
for (int sr = 0; sr < 4; sr++) {
int shift = sr * 2;
int base_off = sr * send_cnt;
int idx = base_off + (addr_first_col >> 2) - send_start;

for (int col = addr_first_col; col <= addr_last_col; col += 4, idx++) {
all_rows[idx] = st_lut[0][((row_base[col] ^ inv) >> shift) & 3]
| st_lut[1][((row_base[col + 1] ^ inv) >> shift) & 3]
| st_lut[2][((row_base[col + 2] ^ inv) >> shift) & 3]
| st_lut[3][((row_base[col + 3] ^ inv) >> shift) & 3];
}
}

g_lcd_instance->_cmd_data(0x2C, all_rows, (size_t)send_cnt * 4U);
break;
}

default:
return 0;
}

return 1;
}

void ST7305_U8g2::busRelease()
{
if (_spi && _busHeld) {
_spi->endTransaction();
_busHeld = false;
}
}

void ST7305_U8g2::busAcquire()
{
if (_spi && !_busHeld) {
_spi->beginTransaction(SPISettings(SPI_CLK, MSBFIRST, SPI_MODE0));
_busHeld = true;
}
}

bool ST7305_U8g2::begin(uint8_t tile_buf_height, const u8g2_cb_t *rotation)
{
pinMode(_dc, OUTPUT);
pinMode(_cs, OUTPUT);
pinMode(_rst, OUTPUT);
digitalWrite(_cs, HIGH);
digitalWrite(_dc, HIGH);
digitalWrite(_rst, HIGH);

_spi->begin(_sck, -1, _mosi, -1);
busAcquire();

u8g2_t *u = u8g2_wrapper.getU8g2();
u8x8_Setup(u8g2_GetU8x8(u), u8x8_d_st7305_custom, u8x8_dummy_cb, u8x8_byte_custom, u8x8_dummy_cb);

uint8_t tbh = tile_buf_height == 0 ? ST7305_TILE_HEIGHT : tile_buf_height;
if (tbh > ST7305_TILE_HEIGHT) {
tbh = ST7305_TILE_HEIGHT;
}

size_t buf_sz = ST7305_BUFFER_ROW_BYTES * tbh;
_my_buf = (uint8_t *)malloc(buf_sz);
if (!_my_buf) {
return false;
}
memset(_my_buf, 0, buf_sz);

u8g2_SetupBuffer(u, _my_buf, tbh, u8g2_ll_hvline_vertical_top_lsb, rotation);
u8g2_InitDisplay(u);
u8g2_SetPowerSave(u, 0);
return true;
}

// The source voltages, the same in each of the four sets (VSHP, VSLP, VSHN, VSLN), and the set in use: sent by
// fullInit(), and again on every switch between the power modes (the datasheet's sequence has them there).
static const uint8_t c1[] = {0x69, 0x69, 0x69, 0x69};
static const uint8_t c2[] = {0x19, 0x19, 0x19, 0x19};
static const uint8_t c4[] = {0x4B, 0x4B, 0x4B, 0x4B};
static const uint8_t c9[] = {0x00};

void ST7305_U8g2::_sourceVoltages()
{
_cmd_data(0xC1, c1, sizeof(c1));
_cmd_data(0xC2, c2, sizeof(c2));
_cmd_data(0xC4, c4, sizeof(c4));
_cmd_data(0xC5, c2, sizeof(c2));
_cmd_data(0xC9, c9, sizeof(c9));
}

void ST7305_U8g2::setLowPower(bool low)
{
if (low == _lowPower) {
return;
}
dispower::Step steps[dispower::kMaxSteps];
const size_t n = dispower::switchSteps(low, steps);
for (size_t i = 0; i < n; i++) {
if (steps[i].cmd == dispower::VOLTAGES) {
_sourceVoltages();
} else {
_cmd(steps[i].cmd);
}
delay(steps[i].waitMs);
}
_lowPower = low;
}

void ST7305_U8g2::setRestMode()
{
setLowPower(false);
const uint8_t b2[] = {dispower::kRestFrameRate};
_cmd_data(0xB2, b2, sizeof(b2));
setLowPower(true);
}

void ST7305_U8g2::fullInit()
{
reset();
_lowPower = false; // (a reset leaves it in the high power mode, and so does the 0x38 below)

const uint8_t d6[] = {0x17, 0x02};
const uint8_t d1[] = {0x01};
const uint8_t c0[] = {0x11, 0x04};
const uint8_t d8[] = {0x80, 0xE9};
const uint8_t b2[] = {dispower::kFrameRate}; // 25.5 Hz in the high power mode as Waveshare's 0x02; 8 Hz, not 1, in the low one
const uint8_t b3[] = {0xE5, 0xF6, 0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45};
const uint8_t b4[] = {0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45};
const uint8_t g_timing[] = {0x32, 0x03, 0x1F};
const uint8_t b7[] = {0x13};
const uint8_t b0[] = {0x64};
const uint8_t m36[] = {0x48};
const uint8_t m3a[] = {0x11};
const uint8_t b9[] = {0x20};
const uint8_t b8[] = {0x29};
const uint8_t win_a[] = {0x12, 0x2A};
const uint8_t win_b[] = {0x00, 0xC7};
const uint8_t m35[] = {0x00};
const uint8_t d0[] = {0xFF};

_cmd_data(0xD6, d6, sizeof(d6));
_cmd_data(0xD1, d1, sizeof(d1));
_cmd_data(0xC0, c0, sizeof(c0));
_cmd_data(0xC1, c1, sizeof(c1));
_cmd_data(0xC2, c2, sizeof(c2));
_cmd_data(0xC4, c4, sizeof(c4));
_cmd_data(0xC5, c2, sizeof(c2));
_cmd_data(0xD8, d8, sizeof(d8));
_cmd_data(0xB2, b2, sizeof(b2));
_cmd_data(0xB3, b3, sizeof(b3));
_cmd_data(0xB4, b4, sizeof(b4));
_cmd_data(0x62, g_timing, sizeof(g_timing));
_cmd_data(0xB7, b7, sizeof(b7));
_cmd_data(0xB0, b0, sizeof(b0));
_cmd(0x11);
delay(120);
_cmd_data(0xC9, c9, sizeof(c9));
_cmd_data(0x36, m36, sizeof(m36));
_cmd_data(0x3A, m3a, sizeof(m3a));
_cmd_data(0xB9, b9, sizeof(b9));
_cmd_data(0xB8, b8, sizeof(b8));
_cmd(0x21);
_cmd_data(0x2A, win_a, sizeof(win_a));
_cmd_data(0x2B, win_b, sizeof(win_b));
_cmd_data(0x35, m35, sizeof(m35));
_cmd_data(0xD0, d0, sizeof(d0));
_cmd(0x38);
_cmd(0x29);
}
Loading