Skip to content

Latest commit

 

History

History
138 lines (108 loc) · 5.94 KB

File metadata and controls

138 lines (108 loc) · 5.94 KB

OpenPager Architecture Reference

Sentry Alert TX-1 ("Beeper OS") — Deep Analysis

Cloned to _reference/sentry-alert-tx-1/. This is the closest existing project to what we're building.

Hardware (theirs vs ours)

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)

Class Hierarchy

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)

Navigation — Stack-Based Push/Pop

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)

Screen Lifecycle

Constructor → enter() → [update() / draw() loop] → exit() → Destructor
  • enter(): active=true, needsFullRedraw=true, marks all regions dirty
  • update(): iterates visible components calling component.update()
  • draw(): 3-phase — full clear if needed → static regions → dirty components → dynamic regions
  • exit(): cleanup(), active=false
  • handleButtonPress(button): pure virtual, every screen implements

Input Flow

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

Rendering — Dual Strategy

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.

Theme System

5 PROGMEM themes (Default, High Contrast, Terminal, Amber, Sentry). 8 color slots per theme: background, surfaceBackground, primaryText, secondaryText, selectedText, accent, accentDark, border.

Power Management

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.

What We Reuse vs Replace

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)

Decision: LVGL vs Sentry-Style Custom Framework

Option A: Fork Sentry Alert TX-1 custom framework

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.

Option B: Build on LVGL

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.

Option C: Hybrid — Sentry's architecture + LVGL for rendering

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.


Prototyping Path

  1. LVGL PC Simulator on Mac (SDL) — fastest iteration, keyboard simulates buttons
  2. Wokwi browser simulator — validates ESP32 + ST7789 + LVGL + button wiring
  3. Real hardware — final integration with camera, mic, Telegram, buzzer