Watches: persistent step counts + 7-day history subscreen

Fixes a pre-existing bug on both watches. SensorBMA423::begin() calls
bma423_write_config_file(), which re-flashes the BMA423 feature engine and zeroes
the step register on every boot. _stepBaseline and _lastStepDay were RAM-only, so
a reboot mid-day silently reset the day's steps to zero. Persisting the baseline
would not have helped, since the raw count it is measured against restarts at 0.

Replaces the baseline model with accumulate-plus-reset-detection:

  delta = (raw >= lastRaw) ? raw - lastRaw : raw
  todaySteps += delta

lastRaw is seeded from the chip on the first read rather than from the file,
which is correct whether or not the counter survived the reset.

Persists lastStepDay, todaySteps and history[6] to /steps.dat on the LittleFS
'maps' partition, already mounted before UITask::begin(). Written on day rollover
and at most every 5 minutes otherwise: a >=6s PWR hold is a hardware power cut by
the AXP2101, so there is no clean-shutdown hook to flush from. A rollover across
several days shifts in the completed day then pads with zeros for the days the
watch was off; a clock corrected backwards re-anchors without disturbing history.

Adds StepsHistoryScreen: today plus the previous six days as labelled bars, today
in the tile blue. Reached by long-pressing the steps screen, which on the S3 the
PWR key's short press also delivers (both arrive as KEY_ENTER). Any input returns
to the steps screen.

Gated on MECK_TWATCH, so the Plus gets both the fix and the screen.
This commit is contained in:
pelgraine
2026-07-10 20:04:16 +10:00
parent 9754f93504
commit fcd8b70384
3 changed files with 225 additions and 16 deletions
+8
View File
@@ -1840,6 +1840,14 @@ static void lastHeardToggleContact() {
return 0; // consume long-press on the tile page
}
#endif
#if defined(MECK_TWATCH)
// Steps screen: long press opens the 7-day history subscreen.
// (On the S3 the PWR key's short press also delivers KEY_ENTER here.)
if (ui_task.isOnStepsScreen()) {
return (char)KEY_ENTER;
}
#endif
// Home screen: long press = activate current page action
// (BLE toggle, send advert, hibernate, GPS toggle, etc.)
if (ui_task.isOnHomeScreen()) {
+194 -12
View File
@@ -76,6 +76,9 @@
#include "AudiobookPlayerScreen.h"
#include "VoiceMessageScreen.h"
#endif
#if defined(MECK_TWATCH)
#include <LittleFS.h> // step history persistence (the "maps" partition)
#endif
#if defined(LILYGO_TWATCH_S3)
#include "WatchAlarmScreen.h"
#endif
@@ -1521,10 +1524,80 @@ public:
}
bool handleInput(char c) override {
if (c == KEY_ENTER) { // touch long-press, or the S3's PWR key
_task->gotoStepsHistoryScreen();
return true;
}
_task->gotoHomeScreen();
return true;
}
};
// Steps history: today plus the previous six days, as labelled bars. Reached by
// long-pressing the steps screen. Any input returns to the steps screen.
class StepsHistoryScreen : public UIScreen {
UITask* _task;
mesh::RTCClock* _rtc;
NodePrefs* _node_prefs;
static const char* dowShort(int dow) {
static const char* names[7] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
return (dow >= 0 && dow < 7) ? names[dow] : "??";
}
public:
StepsHistoryScreen(UITask* task, mesh::RTCClock* rtc, NodePrefs* node_prefs)
: _task(task), _rtc(rtc), _node_prefs(node_prefs) {}
int render(DisplayDriver& display) override {
LGFXDisplay* d = (LGFXDisplay*)&display;
const int W = display.width();
display.setColor(DisplayDriver::LIGHT);
display.setTextSize(1);
display.drawTextCentered(W / 2, 3, "Last 7 days");
uint32_t vals[7];
uint32_t maxv = 1;
for (int i = 0; i < 7; i++) {
vals[i] = _task->getStepHistory(i);
if (vals[i] > maxv) maxv = vals[i];
}
// Weekday of today, from the local day-of-epoch (1970-01-01 was a Thursday).
int todayDow = -1;
uint32_t now = _rtc->getCurrentTime();
if (now > 1700000000) {
int32_t localDay = ((int32_t)now + ((int32_t)_node_prefs->utc_offset_hours * 3600)) / 86400;
todayDow = (int)((localDay + 4) % 7);
}
char buf[12];
for (int i = 0; i < 7; i++) {
int y = 16 + i * 14;
int barW = (int)(((uint64_t)(W - 6) * vals[i]) / maxv);
if (barW < 2) barW = 2;
d->setRawColor(i == 0 ? 0x231D : 0x18C5); // today in the tile blue
d->fillRoundRect(3, y, barW, 12, 2);
const char* label = "-";
if (i == 0) label = "Today";
else if (todayDow >= 0) label = dowShort((todayDow - i + 14) % 7);
d->setRawColor(0xFFFF);
d->printSmallFont(6, y + 8, label);
snprintf(buf, sizeof(buf), "%lu", (unsigned long)vals[i]);
d->printSmallFont(W - 4 - d->smallTextWidth(buf), y + 8, buf);
}
return 1000;
}
bool handleInput(char c) override {
_task->gotoStepsScreen();
return true;
}
};
#endif
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro)
@@ -1699,6 +1772,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
#if defined(MECK_TWATCH)
lock_screen = new ClockScreen(this, &rtc_clock, node_prefs);
steps_screen = new StepsScreen(this);
steps_history_screen = new StepsHistoryScreen(this, &rtc_clock, node_prefs);
// LittleFS ("/maps" partition) is mounted in setup() before UITask::begin().
loadSteps();
#endif
#if defined(LILYGO_TWATCH_S3)
// LittleFS ("/maps" partition) is mounted in setup() before UITask::begin().
@@ -2694,16 +2770,8 @@ if (curr) curr->poll();
_auto_off = millis() + AUTO_OFF_MILLIS;
}
// Roll the daily step baseline over at local midnight (12am-to-12am total).
// No hardware reset -- the displayed count is raw_count - baseline.
{
uint32_t now = rtc_clock.getCurrentTime();
if (now > 1700000000) { // valid timestamp
int32_t localDay = ((int32_t)now + ((int32_t)the_mesh.getNodePrefs()->utc_offset_hours * 3600)) / 86400;
if (_lastStepDay == 0) _lastStepDay = localDay; // seed on first valid read
else if (localDay != _lastStepDay) { _stepBaseline = board.getStepCount(); _lastStepDay = localDay; }
}
}
// Accumulate steps, roll the day over at local midnight, persist periodically.
updateSteps();
#endif
// Auto-lock idle timer — runs regardless of display on/off state
@@ -3693,9 +3761,114 @@ void UITask::gotoTraceScreen() {
}
#if defined(MECK_TWATCH)
uint32_t UITask::getTodaySteps() {
// Persisted step state. Lives on the LittleFS "maps" partition; the watches have
// no SD card. There is no clean-shutdown hook to save from -- a >=6s hold on the
// PWR key is a hardware power cut by the AXP2101 -- so saving is periodic.
#define STEPS_FILE "/steps.dat"
#define STEPS_MAGIC 0x31505453UL // "STP1"
#define STEPS_VERSION 1
#define STEPS_SAVE_INTERVAL_MS 300000UL // flush at most every 5 minutes
struct StepStoreV1 {
uint32_t magic;
uint8_t version;
uint8_t _pad[3];
int32_t lastStepDay;
uint32_t todaySteps;
uint32_t history[6];
};
void UITask::loadSteps() {
File f = LittleFS.open(STEPS_FILE, "r");
if (!f) return;
StepStoreV1 st;
size_t n = f.read((uint8_t*)&st, sizeof(st));
f.close();
if (n != sizeof(st) || st.magic != STEPS_MAGIC || st.version != STEPS_VERSION) return;
_lastStepDay = st.lastStepDay;
_todaySteps = st.todaySteps;
memcpy(_stepHistory, st.history, sizeof(_stepHistory));
}
void UITask::saveSteps() {
StepStoreV1 st;
memset(&st, 0, sizeof(st));
st.magic = STEPS_MAGIC;
st.version = STEPS_VERSION;
st.lastStepDay = _lastStepDay;
st.todaySteps = _todaySteps;
memcpy(st.history, _stepHistory, sizeof(_stepHistory));
File f = LittleFS.open(STEPS_FILE, "w");
if (!f) {
Serial.println("steps: save FAILED (LittleFS)");
return;
}
f.write((const uint8_t*)&st, sizeof(st));
f.close();
_stepsDirtySince = 0;
}
void UITask::shiftStepHistory(uint32_t completedDay) {
for (int i = 5; i > 0; i--) _stepHistory[i] = _stepHistory[i - 1];
_stepHistory[0] = completedDay;
}
void UITask::rollStepDays(int32_t newDay) {
int32_t gap = newDay - _lastStepDay;
if (gap <= 0) { // clock corrected backwards: re-anchor, keep history
_lastStepDay = newDay;
return;
}
shiftStepHistory(_todaySteps); // the day that just ended
for (int32_t d = 1; d < gap && d < 6; d++) {
shiftStepHistory(0); // days the watch was switched off
}
_todaySteps = 0;
_lastStepDay = newDay;
saveSteps();
}
void UITask::updateSteps() {
uint32_t raw = board.getStepCount();
return (raw >= _stepBaseline) ? (raw - _stepBaseline) : raw;
// Seed from the chip, not from the file: SensorBMA423::begin() re-flashes the
// feature engine (bma423_write_config_file) on every boot, zeroing the step
// register. Seeding here is correct whether or not the count survived.
if (!_stepsSeeded) { _lastRaw = raw; _stepsSeeded = true; }
uint32_t delta = (raw >= _lastRaw) ? (raw - _lastRaw) : raw; // raw < last => counter reset
_lastRaw = raw;
uint32_t now = rtc_clock.getCurrentTime();
if (now <= 1700000000) return; // clock not set: nothing to attribute steps to
int32_t localDay = ((int32_t)now + ((int32_t)the_mesh.getNodePrefs()->utc_offset_hours * 3600)) / 86400;
if (_lastStepDay == 0) {
_lastStepDay = localDay; // first valid read on a fresh install
} else if (localDay != _lastStepDay) {
rollStepDays(localDay);
}
if (delta) {
_todaySteps += delta;
if (_stepsDirtySince == 0) _stepsDirtySince = millis();
}
if (_stepsDirtySince && millis() - _stepsDirtySince >= STEPS_SAVE_INTERVAL_MS) {
saveSteps();
}
}
uint32_t UITask::getTodaySteps() {
return _todaySteps;
}
uint32_t UITask::getStepHistory(int daysAgo) {
if (daysAgo <= 0) return _todaySteps;
if (daysAgo > 6) return 0;
return _stepHistory[daysAgo - 1];
}
void UITask::gotoStepsScreen() {
@@ -3706,6 +3879,15 @@ void UITask::gotoStepsScreen() {
_auto_off = millis() + AUTO_OFF_MILLIS;
_next_refresh = 100;
}
void UITask::gotoStepsHistoryScreen() {
setCurrScreen(steps_history_screen);
if (_display != NULL && !_display->isOn()) {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS;
_next_refresh = 100;
}
#endif
#if defined(LILYGO_TWATCH_S3)
+23 -4
View File
@@ -120,9 +120,24 @@ class UITask : public AbstractUITask {
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_TWATCH)
UIScreen* lock_screen; // Lock screen (big clock + battery + unread)
#if defined(MECK_TWATCH)
UIScreen* steps_screen; // Steps screen (big daily step count)
int32_t _lastStepDay = 0; // local day-of-epoch, for the midnight step reset
uint32_t _stepBaseline = 0; // raw step count at the start of the local day
UIScreen* steps_screen; // Steps screen (big daily step count)
UIScreen* steps_history_screen; // Steps subscreen: today + the previous 6 days
// Step state. The BMA423 feature engine is re-flashed by bma423_write_config_file()
// inside SensorBMA423::begin() on every boot, which zeroes the step register. So
// the raw count cannot be used as an absolute; deltas are accumulated instead and
// the totals are persisted. _lastRaw is seeded from the chip on the first read
// rather than from the file, which is correct whether or not the count survived.
int32_t _lastStepDay = 0; // local day-of-epoch that _todaySteps belongs to
uint32_t _todaySteps = 0; // accumulated steps for _lastStepDay
uint32_t _stepHistory[6] = {0}; // [0] = yesterday ... [5] = six days ago
uint32_t _lastRaw = 0; // last raw BMA423 count seen (runtime only)
bool _stepsSeeded = false; // _lastRaw has been seeded from the chip
unsigned long _stepsDirtySince = 0; // millis of oldest unsaved change; 0 = clean
void loadSteps();
void saveSteps();
void updateSteps(); // called every loop()
void rollStepDays(int32_t newDay);
void shiftStepHistory(uint32_t completedDay);
#endif
#if defined(LILYGO_TWATCH_S3)
UIScreen* watch_alarm_screen; // Vibrate-only alarm clock (no audio hardware)
@@ -230,7 +245,9 @@ public:
void gotoMinesweeperScreen(); // Navigate to minesweeper game
#if defined(MECK_TWATCH)
void gotoStepsScreen(); // Navigate to the step counter screen
uint32_t getTodaySteps(); // Daily step count (raw - baseline)
void gotoStepsHistoryScreen(); // Navigate to the 7-day step history
uint32_t getTodaySteps(); // Steps accumulated today
uint32_t getStepHistory(int daysAgo); // 0 = today, 1..6 = previous days
#endif
#if defined(LILYGO_TWATCH_S3)
void gotoWatchAlarmScreen(); // Navigate to the vibrate alarm clock
@@ -298,6 +315,7 @@ public:
bool isOnSnakeScreen() const { return curr == snake_screen; }
#if defined(MECK_TWATCH)
bool isOnStepsScreen() const { return curr == steps_screen; }
bool isOnStepsHistoryScreen() const { return curr == steps_history_screen; }
#endif
#if defined(LILYGO_TWATCH_S3)
bool isOnWatchAlarmScreen() const { return curr == watch_alarm_screen; }
@@ -393,6 +411,7 @@ public:
UIScreen* getSnakeScreen() const { return snake_screen; }
#if defined(MECK_TWATCH)
UIScreen* getStepsScreen() const { return steps_screen; }
UIScreen* getStepsHistoryScreen() const { return steps_history_screen; }
#endif
#if defined(LILYGO_TWATCH_S3)
UIScreen* getWatchAlarmScreen() const { return watch_alarm_screen; }