Scenic is an Elixir UI framework that renders native interfaces via a scene graph directly to GPU or framebuffer, without a browser. It's the go-to choice for building GUIs on Nerves-powered embedded devices — kiosks, industrial panels, Raspberry Pi displays. These devices run unattended, often in locations where a blank screen goes unnoticed for hours. Vigilmon gives you remote monitoring for Scenic-based applications: heartbeats to confirm the UI process is alive, HTTP health endpoints for device reachability, and alerts when a scene stops updating.
What You'll Set Up
- Cron heartbeat from your Scenic application to confirm it's rendering
- HTTP health endpoint exposing Scenic viewport and driver status
- Scene activity monitoring to detect frozen UIs
- Remote reachability monitoring for embedded Nerves devices
Prerequisites
- Elixir 1.14+ with
scenicand optionallynervesin your project - A running Scenic application (hardware or software renderer)
- A free Vigilmon account
Step 1: Add a Heartbeat from Your Scenic Scene
Scenic scenes are GenServer-backed modules. Add periodic Vigilmon pings from a running scene to confirm the UI loop is alive:
- In Vigilmon, click Add Monitor → Cron Heartbeat.
- Set Expected ping interval to
5minutes (or shorter for critical kiosk apps). - Copy the heartbeat URL (e.g.
https://vigilmon.online/heartbeat/abc123).
Add the ping to a scene that stays resident (your root scene or a background supervisor process):
defmodule MyApp.Scene.Home do
use Scenic.Scene
require Logger
@heartbeat_url System.get_env("VIGILMON_HEARTBEAT_URL")
@ping_interval :timer.minutes(5)
@impl Scenic.Scene
def init(scene, _params, _opts) do
schedule_heartbeat()
{:ok, scene}
end
@impl Scenic.Scene
def handle_info(:heartbeat, scene) do
ping_vigilmon()
schedule_heartbeat()
{:noreply, scene}
end
defp schedule_heartbeat, do: Process.send_after(self(), :heartbeat, @ping_interval)
defp ping_vigilmon do
:httpc.request(:get, {String.to_charlist(@heartbeat_url), []}, [{:timeout, 5000}], [])
rescue
e -> Logger.warning("Vigilmon ping failed: #{inspect(e)}")
end
end
If this scene crashes and restarts, it will resume pinging — but a crash loop that restarts too frequently will stop pinging reliably, which is exactly the failure mode you want Vigilmon to catch.
Step 2: Add an HTTP Health Endpoint
For Scenic applications that also run a network stack (common with Nerves devices exposed via SSH or a web API), add an HTTP health endpoint:
defmodule MyApp.HealthHandler do
@behaviour :cowboy_handler
def init(req, state) do
viewport_running = scenic_viewport_alive?()
status = if viewport_running, do: 200, else: 503
body = Jason.encode!(%{status: "ok", viewport: viewport_running})
req =
:cowboy_req.reply(
status,
%{"content-type" => "application/json"},
body,
req
)
{:ok, req, state}
end
defp scenic_viewport_alive? do
case Process.whereis(Scenic.ViewPort) do
nil -> false
pid -> Process.alive?(pid)
end
end
end
Add a Vigilmon HTTP monitor for this endpoint:
- Click Add Monitor → HTTP / HTTPS.
- Enter
http://192.168.1.100/health(your device's LAN IP or hostname). - Set Check interval to
1 minute. - Set Expected HTTP status to
200. - Click Save.
Step 3: Monitor Scene Activity for Frozen UIs
A Scenic application can remain "running" in the OTP sense while displaying a frozen or blank frame. Detect this by sending the heartbeat only when a frame has actually been drawn recently:
defmodule MyApp.Scene.Home do
use Scenic.Scene
require Logger
@heartbeat_url System.get_env("VIGILMON_HEARTBEAT_URL")
@ping_interval :timer.minutes(5)
@impl Scenic.Scene
def init(scene, _params, _opts) do
schedule_heartbeat()
{:ok, assign(scene, last_draw: System.monotonic_time(:second))}
end
@impl Scenic.Scene
def handle_info(:heartbeat, scene) do
last_draw = scene.assigns.last_draw
now = System.monotonic_time(:second)
if now - last_draw < 300 do
# Frame was drawn in the last 5 minutes — ping Vigilmon
ping_vigilmon()
else
Logger.error("Scene has not drawn a frame in #{now - last_draw}s — skipping heartbeat ping")
end
schedule_heartbeat()
{:noreply, scene}
end
@impl Scenic.Scene
def handle_input(_input, scene) do
# Reset the draw timestamp on any interaction
{:noreply, assign(scene, last_draw: System.monotonic_time(:second))}
end
defp schedule_heartbeat, do: Process.send_after(self(), :heartbeat, @ping_interval)
defp ping_vigilmon do
:httpc.request(:get, {String.to_charlist(@heartbeat_url), []}, [{:timeout, 5000}], [])
rescue
_ -> :ok
end
end
The ping is withheld if no frame activity occurred. After two missed intervals, Vigilmon fires an alert — your screen is frozen.
Step 4: Monitor Nerves Device Reachability
For Nerves-deployed Scenic applications, add a TCP port monitor to confirm the device is reachable on the network:
- In Vigilmon, click Add Monitor → TCP Port.
- Enter the device IP and port (e.g.
192.168.1.100:22for SSH, or your app's HTTP port). - Set Check interval to
2 minutes. - Click Save.
If the device loses network, reboots, or the Nerves image crashes, the TCP monitor will alert before the heartbeat interval even lapses. Use both monitors together:
- TCP port monitor: device is reachable on the network
- Cron heartbeat: Scenic application is running and drawing frames
A dead TCP monitor with a live heartbeat is impossible — but the combination helps you isolate failures precisely.
Step 5: Configure Drivers and Alerts
Scenic supports multiple backends — scenic_driver_local for GPU/DRM rendering and scenic_driver_nerves_rpi for Raspberry Pi. Driver crashes leave the supervision tree running but render nothing. Include driver process health in your health endpoint:
defp scenic_viewport_alive? do
viewport_ok = Process.whereis(Scenic.ViewPort) |> is_pid_alive?()
driver_ok = Process.whereis(Scenic.Driver.Local) |> is_pid_alive?()
viewport_ok and driver_ok
end
defp is_pid_alive?(nil), do: false
defp is_pid_alive?(pid), do: Process.alive?(pid)
In Vigilmon, set Alert Channels to Slack or email, and set Consecutive failures before alert to 2 for the heartbeat monitor (to absorb a single slow ping over a flaky device connection) and 1 for the TCP monitor (a single miss on a wired device usually means something is wrong).
Step 6: Use Maintenance Windows During OTA Updates
Nerves supports over-the-air firmware updates via nerves_hub. During an OTA update, your device reboots and both heartbeat and TCP monitors will miss. Suppress these alerts:
# Before triggering OTA update
curl -X POST https://vigilmon.online/api/maintenance \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"monitor_id": "abc123", "duration_minutes": 10}'
After the device reboots and the new Scenic image starts, heartbeat pings resume and the maintenance window expires automatically.
Summary
| Monitor | Target | What It Catches |
|---|---|---|
| Cron heartbeat | Heartbeat URL | Scene process crash, render loop hang |
| HTTP health check | /health on device | Viewport or driver process down |
| TCP port | Device IP:port | Network loss, device reboot |
| Scene activity check | Heartbeat (conditional) | Frozen frame, blank screen |
Scenic puts Elixir on embedded screens — Vigilmon keeps you informed when those screens go dark. With heartbeats from the scene loop, driver health in the HTTP endpoint, and TCP reachability monitoring for the Nerves device, you have full remote visibility into your deployed UIs without needing physical access to the device.