tutorial

How to Monitor Scenic with Vigilmon

Scenic is a native UI framework for Elixir on embedded and Nerves devices. Learn how to monitor your Scenic application's health, driver connectivity, and scene rendering with Vigilmon.

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 scenic and optionally nerves in 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:

  1. In Vigilmon, click Add MonitorCron Heartbeat.
  2. Set Expected ping interval to 5 minutes (or shorter for critical kiosk apps).
  3. 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:

  1. Click Add MonitorHTTP / HTTPS.
  2. Enter http://192.168.1.100/health (your device's LAN IP or hostname).
  3. Set Check interval to 1 minute.
  4. Set Expected HTTP status to 200.
  5. 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:

  1. In Vigilmon, click Add MonitorTCP Port.
  2. Enter the device IP and port (e.g. 192.168.1.100:22 for SSH, or your app's HTTP port).
  3. Set Check interval to 2 minutes.
  4. 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →