tutorial

How to Monitor Circuits.UART with Vigilmon

Monitor your Circuits.UART serial communication layer on Nerves and embedded Linux devices with Vigilmon — catch UART disconnects, data-flow failures, and sensor dropouts before they silently halt your hardware.

How to Monitor Circuits.UART with Vigilmon

Circuits.UART is the Elixir library for communicating over UART (Universal Asynchronous Receiver-Transmitter) serial ports on embedded Linux and Nerves hardware. It lets your Elixir firmware talk to GPS modules, microcontrollers, modems, barcode scanners, industrial sensors, and other serial devices. When a UART device disconnects, produces garbage data, or stops sending frames entirely, your firmware may silently degrade — dropping GPS fixes, losing sensor readings, or missing modem responses — with no external signal that anything is wrong.

Vigilmon adds the external observability layer: heartbeat monitors that alert when your UART loop stops running, HTTP monitors for management APIs, and Slack alerts so you know the moment serial communication breaks.

This tutorial covers:

  • Heartbeat monitoring from within UART read loops
  • HTTP monitoring for Nerves firmware management endpoints
  • Alerting on sensor data gaps and device disconnects
  • Status pages for embedded system fleet operations

What to Monitor in a Circuits.UART Deployment

| Layer | What it is | How to monitor | |---|---|---| | UART read loop | GenServer or Task reading from the serial port | Heartbeat per successful read cycle | | Sensor data pipeline | Data flowing from device into application state | Heartbeat on data receipt | | Nerves firmware API | HTTP management endpoint on the device | HTTP uptime check | | Fleet-level health | All devices reporting in | Heartbeat per device |


Step 1: Add a heartbeat from your UART read loop

The most valuable monitoring signal is the read loop itself. When Circuits.UART loses a device, the port may close or stop delivering frames without raising an exception visible outside the process. Pinging Vigilmon on every successful read cycle makes that failure visible externally.

# lib/my_firmware/gps_reader.ex
defmodule MyFirmware.GpsReader do
  use GenServer
  require Logger

  alias Circuits.UART

  @device "/dev/ttyS0"
  @baud 9600
  # Ping Vigilmon every 60 successful reads (~60 seconds at 1 Hz GPS)
  @heartbeat_every 60

  defstruct [:uart_pid, read_count: 0]

  def start_link(_opts), do: GenServer.start_link(__MODULE__, %__MODULE__{}, name: __MODULE__)

  @impl true
  def init(state) do
    {:ok, uart_pid} = UART.start_link()

    case UART.open(uart_pid, @device, speed: @baud, active: true) do
      :ok ->
        {:ok, %{state | uart_pid: uart_pid}}

      {:error, reason} ->
        Logger.error("Failed to open UART #{@device}: #{inspect(reason)}")
        {:stop, reason}
    end
  end

  @impl true
  def handle_info({:circuits_uart, _port, data}, state) do
    process_frame(data)

    count = state.read_count + 1
    if rem(count, @heartbeat_every) == 0, do: ping_vigilmon()

    {:noreply, %{state | read_count: count}}
  end

  @impl true
  def handle_info({:circuits_uart, port, {:error, reason}}, state) do
    Logger.error("UART error on #{port}: #{inspect(reason)}")
    # No heartbeat → Vigilmon alerts after grace period expires
    {:noreply, state}
  end

  defp ping_vigilmon do
    url = Application.get_env(:my_firmware, :vigilmon_heartbeat_url)

    if url do
      case :httpc.request(:get, {String.to_charlist(url), []}, [{:timeout, 5000}], []) do
        {:ok, _} -> :ok
        {:error, reason} -> Logger.warning("Heartbeat ping failed: #{inspect(reason)}")
      end
    end
  end

  defp process_frame(data) do
    # Parse NMEA, Modbus frames, AT commands, etc.
    Logger.debug("UART frame received: #{inspect(data)}")
  end
end

Add it to your firmware supervision tree:

# lib/my_firmware/application.ex
children = [
  # ... existing children
  MyFirmware.GpsReader
]

Configure the heartbeat URL:

# config/prod.exs
config :my_firmware,
  vigilmon_heartbeat_url: System.get_env("VIGILMON_HEARTBEAT_URL")

In Vigilmon:

  1. Click New Monitor → Heartbeat.
  2. Name it GPS UART read loop.
  3. Set the expected interval to 2 minutes (gives two missed pings before alert).
  4. Copy the heartbeat URL into your firmware environment.

Step 2: Monitor a Nerves device management HTTP endpoint

Nerves devices often expose a local HTTP API for management, diagnostics, or data egress. If your firmware includes Plug or Bandit, add a health endpoint:

# lib/my_firmware_web/router.ex
defmodule MyFirmwareWeb.Router do
  use Plug.Router

  plug :match
  plug :dispatch

  get "/health" do
    uart_ok = MyFirmware.GpsReader.healthy?()

    status = if uart_ok, do: 200, else: 503

    conn
    |> Plug.Conn.put_resp_content_type("application/json")
    |> Plug.Conn.send_resp(status, Jason.encode!(%{
      uart: if(uart_ok, do: "ok", else: "error"),
      uptime_s: :erlang.element(1, :erlang.statistics(:wall_clock)) |> div(1000)
    }))
  end
end

Add a healthy?/0 function to your reader:

# lib/my_firmware/gps_reader.ex
def healthy? do
  case GenServer.call(__MODULE__, :health, 2000) do
    {:ok, last_read_at} ->
      # Healthy if we received data in the last 2 minutes
      DateTime.diff(DateTime.utc_now(), last_read_at) < 120
    _ -> false
  end
rescue
  _ -> false
end

@impl true
def handle_call(:health, _from, state) do
  {:reply, {:ok, state.last_read_at}, state}
end

Point a Vigilmon HTTP monitor at http://<device-ip>:4000/health from a monitoring host on the same network, or expose it through a reverse proxy if the device is internet-accessible.


Step 3: Alert on device disconnects and data gaps

For industrial or field deployments, create separate heartbeat monitors for each critical device type:

# lib/my_firmware/sensor_supervisor.ex
defmodule MyFirmware.SensorSupervisor do
  use Supervisor

  def start_link(_opts), do: Supervisor.start_link(__MODULE__, [], name: __MODULE__)

  @impl true
  def init(_init_arg) do
    children = [
      {MyFirmware.GpsReader, heartbeat_url: System.get_env("VIGILMON_GPS_HEARTBEAT_URL")},
      {MyFirmware.ModemReader, heartbeat_url: System.get_env("VIGILMON_MODEM_HEARTBEAT_URL")},
      {MyFirmware.SensorReader, heartbeat_url: System.get_env("VIGILMON_SENSOR_HEARTBEAT_URL")}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end
end

Each process sends to its own Vigilmon heartbeat URL so failures are isolated per device type.


Step 4: Configure alerts

In Vigilmon go to Alerts and configure notification channels:

  • Slack (#firmware-ops) — team-level UART failure alerts
  • Email — on-call hardware engineer
  • PagerDuty — for systems where sensor loss is safety-critical

Recommended alert policies for Circuits.UART deployments:

| Monitor | Condition | Action | |---|---|---| | GPS read heartbeat | Missed ≥ 2× | Alert hardware on-call | | Sensor data heartbeat | Missed ≥ 1× | Alert immediately (safety-critical) | | Device health HTTP | Non-200 or timeout | Alert firmware team | | Modem read heartbeat | Missed ≥ 3× | Warning — check cellular connectivity |


Step 5: Fleet-wide status page

When you run multiple devices in the field, a shared status page lets field teams and fleet operators check the state without pinging engineering:

  1. Go to Status PagesNew Page.
  2. Add heartbeat monitors for each device class (GPS, sensors, modems).
  3. Set visibility to Team for internal fleet ops.
  4. Share the URL in your ops runbook.

Key Metrics to Watch

| Metric | Threshold | Meaning | |---|---|---| | UART read heartbeat | < 2 missed intervals | Serial device connected and streaming | | Sensor data heartbeat | 0 missed | Data pipeline flowing | | Device HTTP health | Must return 200 | Firmware application alive | | Read loop restart count | < 3/hour | UART not flapping | | Frame error rate | < 0.1% | Cable and baud rate correct |


Why Monitor Circuits.UART?

Serial communication fails quietly:

  • Device disconnects don't raise BEAM exceptions — the read loop simply stops receiving data
  • Baud rate mismatches produce corrupted frames that parse as nonsense without errors
  • Hardware cable faults cause intermittent failures invisible to process supervisors
  • Buffer overflows cause frame loss without application-level errors

Vigilmon's heartbeat model is a natural fit: if the UART loop is healthy, it keeps pinging. If it stops — for any reason — the missing ping tells you exactly when it broke.


Conclusion

Circuits.UART puts Elixir in control of serial hardware. Vigilmon puts you in control of Circuits.UART. With heartbeat monitors on each UART read loop, HTTP checks on your Nerves management endpoint, and per-device-class alert routing, you have end-to-end visibility from serial port to ops dashboard.

Get started free at vigilmon.online.

Monitor your app with Vigilmon

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

Start free →