Architecture & Data Flow
Technical documentation for developers and contributors.
Dual-Core Architecture
SpojBoard uses both cores of the ESP32-S3 with FreeRTOS tasks for optimal performance:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CORE 0 (WiFi Network Stack) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ WiFi interrupt handlers (sub-ms response) โ
โ LwIP TCP/IP stack โ
โ NO application tasks โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CORE 1 (Application Tasks) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ displayRenderTask() [Priority 2] โ
โ Waits for notification, copies data via mutex, โ
โ renders to HUB75 (~100ms) โ
โ โ
โ apiFetchTask() [Priority 1] โ
โ Handles blocking HTTP calls (200-2000ms) โ
โ Updates departures via mutex, sleeps 100ms โ
โ โ
โ Arduino loop() [Priority 1] โ
โ Web server, ETA recalculation, state management โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ Thread Safety
Two mutexes protect shared data with short lock durations (~1ms):
- displayMutex โ Protects display update requests (display task โ loop)
- apiDataMutex โ Protects departures array and weather data (API task โ loop)
Data is copied under mutex, then processed without locks. Rendering and HTTP calls never hold a mutex.
Configuration Constants
| Constant | Value | Purpose |
|---|---|---|
MAX_DEPARTURES | 12 | Cache size (hardcoded) |
MAX_TEMP_DEPARTURES | 144 | Collection buffer (12 stops ร 12 departures) |
config.numDepartures | 1-3 | Display rows (user setting) |
numDepartures only controls how many rows to show on the LED matrix (1-3), not API fetch size. APIs always fetch 12 departures per stop for better caching and sorting.
Complete Data Pipeline
1. User Configuration
City selection, stop IDs, display rows (1-3)
2. API Queries
Fetch 12 departures per stop โ temp buffer (max 144)
1-second delay between stops for rate limiting
3. Sort by ETA
All departures sorted by time across all stops
4. Copy to Cache
Top 12 soonest departures stored with timestamps
5. ETA Recalculation
Every 10 seconds: recalculate ETAs, filter stale entries
6. Display Rendering
Show configured rows (1-3) on LED matrix
State Machine
The device operates in multiple modes with priority-based evaluation:
Operating Modes
AP Mode
Creates WiFi network for setup. Display shows credentials. API calls disabled.
STA Mode
Connects to configured WiFi. Fetches departures, recalculates ETAs, serves web dashboard.
Demo Mode
Pauses API polling. Shows user-configurable sample departures. Available in both AP and STA modes.
Rest Mode
Display cleared, brightness 0. API polling continues. Triggered manually or by schedule.
Display Priority (highest to lowest)
- Demo mode โ custom sample departures
- Rest mode โ display off
- AP mode โ WiFi setup credentials
- WiFi connecting โ connection status
- Setup required โ web UI address
- API error โ error message
- No departures โ info message
- Normal operation โ real departures
Memory Allocation
| Structure | Size | Location |
|---|---|---|
| Temp buffer (144 departures) | ~7KB | Static in API functions |
| Cache (12 departures) | ~600 bytes | Global in main.cpp |
| JSON buffer (Golemio) | 8KB | Heap during API call |
| JSON buffer (BVG) | 24KB | Heap during API call |
| Configuration | ~1KB | NVS flash (persistent) |
Typical: ~200KB free heap, 21.4% RAM used (70KB of 327KB), 94.7% flash used (1.24MB of 1.31MB)
Module Architecture
Layered design with zero circular dependencies:
Layer 6: Application
main.cpp (orchestrates all modules, runtime API selection)
Layer 5: Business Logic
TransitAPI (abstract), GolemioAPI, BvgAPI, MqttAPI,
GitHubOTA, WeatherAPI
Layer 4: Network Services
WiFiManager, CaptivePortal, ConfigWebServer, OTAUpdateManager
Layer 3: Hardware Abstraction
DisplayController, DisplayManager, DisplayColors,
TimeUtils, RestMode
Layer 2: Data Layer
AppConfig, DepartureData
Layer 1: Foundation
Logger, UTF-8 utilities (gfxlatin2, decodeutf8) Key Patterns
- Zero Circular Dependencies: Lower layers never depend on higher layers
- Callback Pattern: Modules communicate upward via callbacks
- Pure Data Structures: Config passed as parameter, not stored in modules
- Static Allocation: No dynamic allocation in main loop for stability
Multi-Stop Behavior
When multiple stop IDs are configured (comma-separated, max 12 stops):
- Query each stop individually (12 departures per stop)
- Apply 1-second delay between API calls (rate limiting)
- Collect in temp buffer (capacity: 144)
- Sort by ETA across all stops
- Cache top 12 soonest departures
- Display configured rows on LED matrix
This ensures you always see the soonest departures across all stops.
Performance
| Operation | Timing |
|---|---|
| Single stop API call | ~1-2 seconds |
| 12 stops full query | ~12-24 seconds |
| ETA recalculation | <1ms |
| Display render | ~10-20ms |
Debugging
When config.debugMode = true:
- Telnet server on port 23 โ mirrors all debug logs over WiFi
- Memory logging at key checkpoints (api_start, api_complete, display_update)
- API response logging with timestamps
Serial output always available at 115200 baud for boot sequence, WiFi status, and errors.