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:
- Click New Monitor → Heartbeat.
- Name it
GPS UART read loop. - Set the expected interval to 2 minutes (gives two missed pings before alert).
- 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:
- Go to Status Pages → New Page.
- Add heartbeat monitors for each device class (GPS, sensors, modems).
- Set visibility to Team for internal fleet ops.
- 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.