Cloned to _reference/sentry-alert-tx-1/. This is the closest existing project to what we're building.
| Sentry Alert TX-1 | OpenPager | |
|---|---|---|
| Board | Adafruit ESP32-S3 Reverse TFT Feather | ESP32 w/ camera + mic (TBC) |
| Display | Built-in 1.14" ST7789, 240x135 | TFT (likely 240x240 ST7789, TBC) |
| Buttons | 3 (GPIO0, GPIO1, GPIO2) | 4 (UP, DOWN, CAPTURE, SELECT) |
| Audio | Passive buzzer GPIO14, RTTTL ringtones | WT-1209T piezo, PWM tones |
| Comms | WiFi + MQTT (Sentry alerts) | WiFi + Telegram (OpenClaw) |
| Battery | LiPo + MAX17048 fuel gauge | Battery (TBC) |
Component (base: bounds, dirty tracking, theme colors)
├── MenuItem (label, selected state, click callback)
└── MenuContainer (array of MenuItems, scroll, auto-layout)
Screen (base: components[10], draw regions[8], lifecycle)
├── SplashScreen
├── MainMenuScreen
├── AlertsScreen → AlertDetailScreen
├── AlertNotificationScreen (popup overlay)
├── SettingsScreen → ThemeSelectionScreen, RingtonesScreen, SystemInfoScreen
├── GamesScreen → PongScreen, SnakeScreen, BeeperHeroScreen
└── HardwareTestScreen
ScreenManager (stack[8] of Screen*, push/pop/switch)
InputRouter (GPIO → ScreenManager → active Screen)
ThemeManager (5 PROGMEM themes, NVS persistence)
RenderManager (dirty rects, FPS limiting)
ScreenManager uses a fixed-size stack (max depth 8):
pushScreen(screen)→ calls current.exit(), pushes to stack, calls new.enter()popScreen()→ calls current.exit(), pops previous, calls previous.enter()switchToScreen(screen)→ replaces current without pushing (no back history)- 200ms transition blocking + 300ms input cooldown after
Global access: GlobalScreenManager::getInstance()->pushScreen(target)
Constructor → enter() → [update() / draw() loop] → exit() → Destructor
enter(): active=true, needsFullRedraw=true, marks all regions dirtyupdate(): iterates visible components calling component.update()draw(): 3-phase — full clear if needed → static regions → dirty components → dynamic regionsexit(): cleanup(), active=falsehandleButtonPress(button): pure virtual, every screen implements
GPIO pins → ButtonManager (50ms debounce)
→ InputRouter (routing + long-press detection)
→ ScreenManager (transition gating)
→ Screen.handleButtonPress(button)
InputRouter behavior:
- Long press ANY button (1500ms): triggers popScreen() (global back)
- Button A short: UP/Previous
- Button B short: DOWN/Next
- Button C release (not press): SELECT/Enter (release distinguishes from long-press)
- Auto-repeat A/B: 250ms initial delay, then every 70ms
Component dirty tracking: components track needsRedraw, Screen.draw() only renders dirty ones.
Draw regions: screens register STATIC (drawn once on enter) and DYNAMIC (drawn when data changes) regions:
addDrawRegion(DirectDrawRegion::STATIC, [this]() { drawHeader(); });
addDrawRegion(DirectDrawRegion::DYNAMIC, [this]() { drawList(); });Call markDynamicContentDirty() when data changes.
5 PROGMEM themes (Default, High Contrast, Terminal, Amber, Sentry). 8 color slots per theme: background, surfaceBackground, primaryText, secondaryText, selectedText, accent, accentDark, border.
Three-state machine: ACTIVE → IDLE_DIM (backlight off after 10s) → DEEP_SLEEP (timer or button wake). USB detection: stays ACTIVE if battery >4.0V. Wake sources: timer (60s default), button GPIO.
Reuse directly:
- ScreenManager (push/pop/switch pattern)
- Screen base class (lifecycle, draw regions, component management)
- Component base class (dirty tracking, bounds, theme integration)
- InputRouter (routing, long-press-back, auto-repeat, select-on-release)
- ThemeManager (PROGMEM colors, NVS persistence)
- DisplayConfig pattern (centralized layout constants)
- AlertNotificationScreen popup overlay pattern
Adapt:
- ButtonManager (4 buttons instead of 3, different GPIO pins)
- PowerManager (different battery gauge, same sleep state machine)
- Display driver (240x240 instead of 240x135, same ST7789)
Replace:
- MQTT → Telegram long polling
- Sentry JSON parsing → OpenClaw message parsing
- AlertsScreen → MessageListScreen + MessageDetailScreen
- Games → not needed (or keep for fun)
Pros: Already a working pager OS. Clean architecture. TFT_eSPI + Adafruit_GFX based. Low memory. We understand it now. Cons: No built-in scrollable text areas, no word-wrap, no font rendering beyond basic GFX fonts. We'd need to build the chat/message display from scratch.
Pros: Built-in lv_menu with nav stack, lv_list with scrollable items, lv_label with word wrap, keypad input groups, smooth fonts, animations. All the hard widget work is done. Cons: Heavier (~40-80KB RAM). Steeper learning curve. Different paradigm from Sentry's framework.
Use Sentry's ScreenManager/InputRouter/PowerManager patterns but swap TFT_eSPI rendering for LVGL widgets inside each screen. Best of both worlds but more integration work.
Current leaning: Option B (LVGL) — the message display and scrolling requirements are the hardest part, and LVGL solves them out of the box.
- LVGL PC Simulator on Mac (SDL) — fastest iteration, keyboard simulates buttons
- Wokwi browser simulator — validates ESP32 + ST7789 + LVGL + button wiring
- Real hardware — final integration with camera, mic, Telegram, buzzer