Adhan Media Caster

An edge-AI IoT orchestration engine on a Raspberry Pi 4 — precision prayer-audio casting, ADB device choreography, and a fully on-device LLM that never touches the real-time path.

Node.js 18+ · Express 5 Gemma 3 (1B) · Ollama Google Cast · mDNS/castv2 ADB · Sony Android TV FFmpeg · Canvas Firestore · PM2

Layered Architecture

One Raspberry Pi is the entire backend. External data flows down through the edge core's service mesh, out over the local network to physical devices, and up to the cloud for the operations dashboard.

Layer 0 · External Data
Source APIs
Aladhan API
api.aladhan.com · calendarByCity
Annual prayer-time calendar, fetched once per year and cached to annual_schedule.json (incl. Hijri dates).
Open-Meteo
api.open-meteo.com/v1/forecast
Current WMO weather code + day/night, on a 15-min cache, driving procedural rain/snow/fog FFmpeg filters.
Layer 1 · Edge Core — Raspberry Pi 4
boot.js — Orchestration Core + REST API
:3001 /health /api/metrics /api/ask /api/trigger/prayer
Service Mesh
CoreScheduler
CoreScheduler.js
Authoritative scheduler & player — pre-flight, adaptive lead, cast lifecycle, retries.
MediaService
MediaService.js
Audio cache, Adhan selection, FFmpeg encode + weather filters.
visual_generator
visual_generator.js
HD 1280×800 dashboard render via Canvas; procedural weather.
HardwareService
HardwareService.js
ADB control of the Sony TV — pause/resume, ghost-power detect.
DiscoveryService
DiscoveryService.js
mDNS Cast discovery with a cache-first warm path (6h TTL).
PlaybackLogger
PlaybackLogger.js
Durable event log + rolling metrics, latency p75/p95, streaks.
FirestoreSync
FirestoreSync.js
Debounced push of metrics + schedule to Firestore.
SecurityService
SecurityService.js
Self-healing npm audit fix, dependency hygiene.
On-Device AI Layer (local-first, no cloud, no API keys) ⛔ fenced off the cast path
OllamaService
Gemma 3 1B · 127.0.0.1:11434
Single-flight queue, circuit breaker, watchdog, quiet-window guard.
AdvisoryAgent
AdvisoryAgent.js
Failure diagnosis, daily blurb, advisory-only tuning (never auto-applied).
aiContext
aiContext.js
Grounds Gemma in computed schedule + log so it never invents times.
Local Data Stores (on-disk)
annual_schedule.json
Yearly prayer times
playback_log.json
Event journal + archives
.cast-cache.json
Last-known device host:port
ai-memory · ai-blurb
Diagnoses, suggestions, greeting
Layer 2 · Local Network & Protocols
Google Cast — castv2 / mDNS
Service discovery + media session control to the Nest Hub. Hybrid watchdog: passive event listening + serialized active polling.
ADB — Android Debug Bridge
Deep state inspection (dumpsys power / media_session / audio) and key-event injection to the Android TV, with 8s timeout + SIGKILL guards.
Layer 3 · Physical Devices
🕌 Google Nest Hub Max
Casts the HD Adhan video + live dashboard, then the Dua image. Volume tuned per-prayer and per TV-activity state.
📺 Sony Android TV
Netflix/YouTube intelligently paused (or muted for live streams) before the Adhan and resumed after — zero user intervention.
Layer 4 · Cloud & Operations
Firestore
project adhan-79908
dailyMetrics · dailyEvents · meta/prayerSchedule.
dashboard.html
GitHub Pages
Live ops dashboard reading Firestore in real time.
PM2
ecosystem.config.cjs
Supervises boot.js — restart on crash, log rotation.
GitHub Actions
.github/workflows
Weekly npm audit fix + CI.

System Design — Data & Control Flow

The Pi core sits at the center. Hover any node. Flowing lines show direction and payload: cyan = data ingest, gold = real-time cast/control, emerald = device telemetry, violet = local AI.

yearly schedule weather 15m /api/ask · curl Cast · castv2 ADB control metrics sync Gemma 3
Aladhan API
prayer times
Open-Meteo
weather FX
REST clients
curl · ext.
Ollama · Gemma 3
local LLM
Raspberry Pi 4
boot.js · :3001
Nest Hub Max
Adhan + dash
Sony Android TV
pause / resume
Firestore
adhan-79908
dashboard.html
GitHub Pages
Data ingest Real-time cast / control Device telemetry Local AI

REST Control & Telemetry Surface — boot.js · default :3001

GET/healthbuild + SHA
GET/api/metrics?days=Nrollup
GET/api/metrics/todaytoday
POST/api/askGemma + fallback
GET/api/ask/healthLLM reachable?
GET/api/blurbdaily greeting
GET/api/advisorytuning notes
POST/api/trigger/prayermanual fire
POST/api/metrics/sync→ Firestore

Live Cast Cycle — the Prayer Lifecycle

One full autonomous cycle, from idle monitoring through the millisecond-precise trigger to recovery and cloud sync. This is the real sequence encoded in CoreScheduler.js.

speed
Sony Android TV
ADB · dumpsys
Playing Netflix
media session active
idle — monitoring
Raspberry Pi · CoreScheduler
05:00
Monitoring
Waiting for the next prayer window.
AI: idle
Nest Hub Max
Cast · castv2
Standby
awaiting cast
idle
media-caster — pm2 log · adhan-caster

The AI Fence — keeping Gemma off the real-time path

The on-device LLM is powerful but a Pi 4 inference can take seconds. The architecture forbids any AI work inside each prayer's critical window, so the Adhan always fires on time — no matter what the model is doing.

⛔ Critical Window — T−5m → T+8m

Watch the AI agent (🤖) travel across the day. In a red prayer window it is blocked; in the green quiet zones it is free to run.

🕌Fajr
🕌Dhuhr
🕌Asr
🕌Maghrib
🕌Isha
🤖
00:0006:0012:0018:0024:00
— quiet window · AI free to run —

Jobs that run only in quiet windows

🩺
Self-diagnosing failures
A failed cast is queued, then explained in plain English and attached to the dashboard history.
drains every ~20 min
💬
Daily dashboard greeting
A warm one-liner generated once per day, grounded in the live schedule and stats.
~05:30 daily
🎛️
Advisory tuning loop
Reviews rolling 14-day reliability and proposes env-var tuning — surfaced for a human, never auto-applied.
~03:15 daily

Circuit Breaker + Watchdog

A Pi 4 can't run two inferences at once, and Ollama can wedge. The breaker fails fast and self-heals.

CLOSEDhealthy
OPEN2 fails · 60s cooldown
HALF-OPEN1 probe
On trip → watchdog runs systemctl restart ollama.
During cooldown every call fails fast to a deterministic answer.

Why it never blocks the Adhan

Single-flight queue — asks serialized; one inference at a time.
Quiet-window guard — isQuiet() gates every job.
Model warm-keeping — avoids cold-start stalls.
Deterministic fallback/api/ask never hard-fails.
No cloud, no keys — nothing leaves the house.
Optional — caster runs fully without Ollama.
Net effect: the LLM is a bolt-on observer and advisor. Strip it out entirely and prayers still fire to the millisecond — the AI only ever makes the system more explainable, never less reliable.