From 51fcf6de833ea4486b4bf5981a30c1cfdd159b5e Mon Sep 17 00:00:00 2001 From: Pelgraine Date: Wed, 8 Jul 2026 21:03:24 +1000 Subject: [PATCH] t-watch - initial step counter implementation --- examples/companion_radio/main.cpp | 2 +- examples/companion_radio/ui-new/HomeIcons.h | 6 ++ examples/companion_radio/ui-new/UITask.cpp | 67 ++++++++++++++----- examples/companion_radio/ui-new/UITask.h | 15 +++++ .../TWatchS3PlusBoard.cpp | 61 +++++++++++++++++ .../lilygo_twatch_s3_plus/TWatchS3PlusBoard.h | 3 + 6 files changed, 137 insertions(+), 17 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 2d264c0d..9bf548a9 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1787,7 +1787,7 @@ static void lastHeardToggleContact() { if (row == 2 && col == 0) { ui_task.gotoTraceScreen(); return 0; } // row 2 col 1 = Maps -- TODO subscreen not yet built; no-op for now. if (row == 3 && col == 0) { ui_task.gotoNotesScreen(); return 0; } - // row 3 col 1 = Games -- placeholder, no target yet; no-op for now. + if (row == 3 && col == 1) { ui_task.gotoStepsScreen(); return 0; } } return 0; // consume long-press on the tile page } diff --git a/examples/companion_radio/ui-new/HomeIcons.h b/examples/companion_radio/ui-new/HomeIcons.h index e9c3118f..85d0f9eb 100644 --- a/examples/companion_radio/ui-new/HomeIcons.h +++ b/examples/companion_radio/ui-new/HomeIcons.h @@ -81,6 +81,12 @@ static const uint8_t icon_gamepad[] PROGMEM = { 0x40,0x20, 0x30,0xC0, 0x10,0x80, 0x1F,0x80, 0x00,0x00, 0x00,0x00, }; +// Star -- 12x12 home tile icon (Steps). MSB-first, 2 bytes per row. +static const uint8_t icon_star[] PROGMEM = { + 0x06,0x00, 0x06,0x00, 0x0F,0x00, 0xFF,0xF0, 0x7F,0xE0, 0x3F,0xC0, + 0x1F,0x80, 0x3F,0xC0, 0x39,0xC0, 0x70,0xE0, 0x60,0x60, 0x00,0x00, +}; + // 🔔 Bell -- 7x8 status bar indicator (alarm enabled) // MSB-first, 1 byte per row #define BELL_ICON_W 7 diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 9e03edd7..c10ca6b6 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -34,7 +34,7 @@ #endif #ifndef AUTO_OFF_MILLIS - #define AUTO_OFF_MILLIS 45000 // 45 seconds + #define AUTO_OFF_MILLIS 15000 // 15 seconds #endif // Right-aligned values on the dense home pages compensate for the e-ink X // origin offset so they are not clipped at the right edge. Default 0 for @@ -686,7 +686,7 @@ public: { {icon_envelope, "Messages", 0x0CB1}, {icon_people, "Contacts", 0xCAA0} }, { {icon_gear, "Settings", 0x0560}, {icon_search, "Discover", 0xF81F} }, { {icon_trace, "Trace", 0xF800}, {icon_map, "Maps", 0x231D} }, - { {icon_notepad, "Notes", 0x07FF}, {icon_gamepad, "Games", 0xFFE0} }, + { {icon_notepad, "Notes", 0x07FF}, {icon_star, "Steps", 0xFFE0} }, }; const uint16_t TILE_FILL = 0x18C5; // dark navy @@ -1485,6 +1485,27 @@ public: return true; } }; + +// Steps screen: big daily step count in the clock font. Opened by long-pressing +// the Steps home tile; any tap dismisses back to the tiles. +class StepsScreen : public UIScreen { + UITask* _task; +public: + StepsScreen(UITask* task) : _task(task) { } + + int render(DisplayDriver& display) override { + display.setColor(DisplayDriver::LIGHT); + char buf[16]; + snprintf(buf, sizeof(buf), "%u", (unsigned)_task->getTodaySteps()); + ((LGFXDisplay*)&display)->printClockFont(display.width() / 2, display.height() / 2 - 18, buf); + return 1000; + } + + bool handleInput(char c) override { + _task->gotoHomeScreen(); + return true; + } +}; #endif #if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) @@ -1658,6 +1679,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #endif #if defined(LILYGO_TWATCH_S3_PLUS) lock_screen = new ClockScreen(this, &rtc_clock, node_prefs); + steps_screen = new StepsScreen(this); #endif #ifdef TWATCH_COMPOSE_ENABLED tw_picker = new TWatchChannelPicker(_display); @@ -2223,20 +2245,6 @@ void UITask::loop() { } else { c = checkDisplayOn(KEY_NEXT); } -#elif defined(LILYGO_TWATCH_S3_PLUS) - // Watch: on the contacts screen a button click opens the path editor for - // the selected contact; elsewhere it wakes the display / emits KEY_NEXT. - if (isOnContactsScreen()) { - int idx = ((ContactsScreen*)contacts_screen)->getSelectedContactIdx(); - if (idx >= 0) { - gotoPathEditor(idx); - c = 0; - } else { - c = checkDisplayOn(KEY_NEXT); - } - } else { - c = checkDisplayOn(KEY_NEXT); - } #else c = checkDisplayOn(KEY_NEXT); #endif @@ -2635,6 +2643,17 @@ if (curr) curr->poll(); setCurrScreen(lock_screen); _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; } + } + } #endif // Auto-lock idle timer — runs regardless of display on/off state @@ -3617,6 +3636,22 @@ void UITask::gotoTraceScreen() { _next_refresh = 100; } +#if defined(LILYGO_TWATCH_S3_PLUS) +uint32_t UITask::getTodaySteps() { + uint32_t raw = board.getStepCount(); + return (raw >= _stepBaseline) ? (raw - _stepBaseline) : raw; +} + +void UITask::gotoStepsScreen() { + setCurrScreen(steps_screen); + if (_display != NULL && !_display->isOn()) { + _display->turnOn(); + } + _auto_off = millis() + AUTO_OFF_MILLIS; + _next_refresh = 100; +} +#endif + void UITask::gotoGamesMenu() { GamesMenuScreen* gm = (GamesMenuScreen*)games_menu_screen; gm->enter(); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index bb751348..6d39065f 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -119,6 +119,11 @@ class UITask : public AbstractUITask { int _tileGridVY = 44; // Virtual Y of tile grid top (updated each render) #if defined(LilyGo_T5S3_EPaper_Pro) || defined(LILYGO_TWATCH_S3_PLUS) UIScreen* lock_screen; // Lock screen (big clock + battery + unread) +#if defined(LILYGO_TWATCH_S3_PLUS) + 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 +#endif UIScreen* _screenBeforeLock = nullptr; bool _locked = false; unsigned long _lastInputMillis = 0; // Auto-lock idle tracking @@ -220,6 +225,10 @@ public: void gotoGamesMenu(); // Navigate to games launcher menu void gotoSnakeScreen(); // Navigate to snake game void gotoMinesweeperScreen(); // Navigate to minesweeper game +#if defined(LILYGO_TWATCH_S3_PLUS) + void gotoStepsScreen(); // Navigate to the step counter screen + uint32_t getTodaySteps(); // Daily step count (raw - baseline) +#endif #if HAS_GPS void gotoMapScreen(); // Navigate to map tile screen #endif @@ -281,6 +290,9 @@ public: bool isOnTraceScreen() const { return curr == trace_screen; } bool isOnGamesMenu() const { return curr == games_menu_screen; } bool isOnSnakeScreen() const { return curr == snake_screen; } +#if defined(LILYGO_TWATCH_S3_PLUS) + bool isOnStepsScreen() const { return curr == steps_screen; } +#endif bool isOnMinesweeperScreen() const { return curr == minesweeper_screen; } bool isOnMapScreen() const { return curr == map_screen; } #if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) || defined(LILYGO_TWATCH_S3_PLUS) @@ -370,6 +382,9 @@ public: UIScreen* getTraceScreen() const { return trace_screen; } UIScreen* getGamesMenuScreen() const { return games_menu_screen; } UIScreen* getSnakeScreen() const { return snake_screen; } +#if defined(LILYGO_TWATCH_S3_PLUS) + UIScreen* getStepsScreen() const { return steps_screen; } +#endif UIScreen* getMinesweeperScreen() const { return minesweeper_screen; } UIScreen* getMapScreen() const { return map_screen; } #ifdef MECK_WEB_READER diff --git a/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.cpp b/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.cpp index d8d17673..9a7fe685 100644 --- a/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.cpp +++ b/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.cpp @@ -8,6 +8,56 @@ volatile uint32_t TWatchS3PlusBoard::_tilt_isr_count = 0; // TEMP diagnostic void IRAM_ATTR TWatchS3PlusBoard::onTiltISR() { _tilt_flag = true; _tilt_isr_count++; } +// ---- Wrapper-free BMA423 step counter (raw I2C) ---------------------------- +// SensorLib's SensorBMA423 step-counter methods do not compile in this build, +// so the step counter is driven directly over I2C. Register/offset/mask values +// are from the Bosch BMA423 driver. +#define BMA423_REG_STEP_CNT_OUT 0x1E // 4-byte little-endian step count output +#define BMA423_REG_FEATURE_CONFIG 0x5E // 64-byte feature config stream +#define BMA423_FEATURE_LEN 64 +#define BMA423_STEP_EN_BYTE 0x37 // BMA423_STEP_CNTR_OFFSET(0x36) + 1 +#define BMA423_STEP_EN_BIT 0x10 // BMA423_STEP_CNTR_EN_MSK + +#define BMA423_REG_POWER_CONF 0x7C // BMA4_POWER_CONF_ADDR +#define BMA423_ADV_PWR_SAVE_BIT 0x01 // BMA4_ADVANCE_POWER_SAVE_MSK + +static bool bma423ReadRegs(uint8_t reg, uint8_t* buf, uint8_t len) { + Wire.beginTransmission(I2C_ADDR_ACCEL); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) return false; + if (Wire.requestFrom((int)I2C_ADDR_ACCEL, (int)len) != len) return false; + for (uint8_t i = 0; i < len; i++) buf[i] = Wire.read(); + return true; +} + +static bool bma423WriteRegs(uint8_t reg, const uint8_t* buf, uint8_t len) { + Wire.beginTransmission(I2C_ADDR_ACCEL); + Wire.write(reg); + for (uint8_t i = 0; i < len; i++) Wire.write(buf[i]); + return Wire.endTransmission() == 0; +} + +// Enable the step counter by setting its enable bit in the feature config, +// preserving every other byte (tilt lives at a different offset, 0x3A, so it is +// untouched). The feature config can only be written with advanced-power-save +// disabled, so we bracket the write and restore the prior power state after. +static void bma423EnableStepCounter() { + uint8_t pc; + if (!bma423ReadRegs(BMA423_REG_POWER_CONF, &pc, 1)) return; // save power state + uint8_t off = pc & ~BMA423_ADV_PWR_SAVE_BIT; // disable adv power save + bma423WriteRegs(BMA423_REG_POWER_CONF, &off, 1); + delay(2); // wake from low-power (>=450us) + + uint8_t cfg[BMA423_FEATURE_LEN]; + if (bma423ReadRegs(BMA423_REG_FEATURE_CONFIG, cfg, BMA423_FEATURE_LEN)) { + cfg[BMA423_STEP_EN_BYTE] |= BMA423_STEP_EN_BIT; + bma423WriteRegs(BMA423_REG_FEATURE_CONFIG, cfg, BMA423_FEATURE_LEN); + delay(1); // write settle + } + + bma423WriteRegs(BMA423_REG_POWER_CONF, &pc, 1); // restore power state +} + void TWatchS3PlusBoard::begin() { ESP32Board::begin(); power_init(); @@ -29,6 +79,10 @@ void TWatchS3PlusBoard::begin() { // on a self-clearing line otherwise locks tilt-wake out permanently). attachInterrupt(digitalPinToInterrupt(PIN_ACCEL_IRQ), onTiltISR, RISING); _accel->enableTiltDetector(true, true); + // Enable the hardware step counter via raw I2C (SensorLib's wrapper method + // does not compile in this build). It then counts in the BMA423 feature + // engine with no CPU cost, even while the display is off. + bma423EnableStepCounter(); } esp_reset_reason_t reason = esp_reset_reason(); @@ -140,4 +194,11 @@ bool TWatchS3PlusBoard::tiltFired() { digitalRead(PIN_ACCEL_IRQ), (unsigned)_tilt_isr_count); } return false; +} + +uint32_t TWatchS3PlusBoard::getStepCount() { + uint8_t d[4]; + if (!bma423ReadRegs(BMA423_REG_STEP_CNT_OUT, d, 4)) return 0; + return (uint32_t)d[0] | ((uint32_t)d[1] << 8) | + ((uint32_t)d[2] << 16) | ((uint32_t)d[3] << 24); } \ No newline at end of file diff --git a/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.h b/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.h index 04c6123b..5da5cc1d 100644 --- a/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.h +++ b/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.h @@ -57,6 +57,9 @@ public: return PMU ? PMU->getBatteryPercent() : 0; } + // Wrapper-free BMA423 step count (raw I2C; defined in the .cpp). + uint32_t getStepCount(); + // GPS power is the AXP2101 BLDO1 rail. Off at boot; toggled at runtime via // the gps_enabled pref (boot) and the "gps on/off" CLI command (live). void gpsPowerOn();