NervesHubLinkAVM

A NervesHub client for AtomVM devices.

NervesHubLinkAVM connects AtomVM-powered microcontrollers (ESP32, etc.) to a NervesHub server for over-the-air (OTA) firmware updates via WebSocket.

Features

Requirements

Installation

Add nerves_hub_link_avm to your mix.exs dependencies:

def deps do
  [
    {:nerves_hub_link_avm, "~> 0.1.0"}
  ]
end

Usage

Starting the client

NervesHubLinkAVM.start_link(
  host: "your-nerveshub-server.com",
  port: 443,
  ssl: true,
  device_cert: "/path/to/device-cert.pem",
  device_key: "/path/to/device-key.pem",
  firmware_meta: %{
    "uuid" => "firmware-uuid",
    "product" => "my-product",
    "architecture" => "esp32",
    "version" => "1.0.0",
    "platform" => "esp32"
  },
  client: MyApp.Client,
  fwup_writer: MyApp.ESP32Writer
)

Or with shared secret authentication:

NervesHubLinkAVM.start_link(
  host: "your-nerveshub-server.com",
  product_key: "nhp_...",
  product_secret: "...",
  identifier: "my-device-001",
  firmware_meta: %{ ... },
  client: MyApp.Client,
  fwup_writer: MyApp.ESP32Writer
)

Required firmware metadata keys: uuid, product, architecture, version, platform.

Public API

# Force a reconnection (resets backoff)
NervesHubLinkAVM.reconnect()

# Confirm firmware is valid after an update (prevents rollback)
NervesHubLinkAVM.confirm_update()

# With a named instance
NervesHubLinkAVM.reconnect(MyDevice)
NervesHubLinkAVM.confirm_update(MyDevice)

Client behaviour

The Client controls update decisions, progress reporting, and device actions. All callbacks are optional with sensible defaults (auto-apply updates, no-op for everything else).

defmodule MyApp.Client do
  @behaviour NervesHubLinkAVM.Client

  @impl true
  def update_available(_meta), do: :apply  # or :ignore or {:reschedule, 60_000}

  @impl true
  def fwup_progress(percent), do: IO.puts("Progress: #{percent}%")

  @impl true
  def fwup_error(error), do: IO.puts("Error: #{inspect(error)}")

  @impl true
  def reboot, do: :ok

  @impl true
  def identify, do: :ok

  @impl true
  def handle_connected, do: :ok

  @impl true
  def handle_disconnected, do: :ok
end

A default implementation (NervesHubLinkAVM.Client.Default) is used if no :client is provided.

FwupWriter behaviour

The FwupWriter handles hardware-specific firmware write operations. Implement this for your target platform.

defmodule MyApp.ESP32Writer do
  @behaviour NervesHubLinkAVM.FwupWriter

  @impl true
  def fwup_begin(size, _meta) do
    # Erase inactive partition, allocate buffers
    {:ok, %{offset: 0, size: size}}
  end

  @impl true
  def fwup_chunk(data, state) do
    # Write chunk to flash
    {:ok, %{state | offset: state.offset + byte_size(data)}}
  end

  @impl true
  def fwup_finish(_state) do
    # Activate new slot, reboot
    :ok
  end

  @impl true
  def fwup_abort(_state) do
    # Clean up partial writes
    :ok
  end

  # Optional: confirm firmware on first boot
  @impl true
  def fwup_confirm, do: :ok
end

Extensions

Extensions are opt-in and pluggable. Currently supported: health reporting.

NervesHubLinkAVM.start_link(
  ...
  extensions: [health: MyApp.HealthProvider]
)

Implement the HealthProvider behaviour:

defmodule MyApp.HealthProvider do
  @behaviour NervesHubLinkAVM.HealthProvider

  @impl true
  def health_check do
    %{"cpu_temp" => 42.5, "mem_used_percent" => 65}
  end
end

Custom extensions can implement the NervesHubLinkAVM.Extension behaviour directly.

Update lifecycle

When the server pushes a firmware update:

  1. Client.update_available/1 is called -- returns :apply, :ignore, or {:reschedule, ms}
  2. If :apply, calls FwupWriter.fwup_begin/2
  3. Reports "downloading" and streams chunks through FwupWriter.fwup_chunk/2
  4. Verifies SHA256 hash of the downloaded firmware
  5. Reports "updating" and calls FwupWriter.fwup_finish/1
  6. Reports "fwup_complete" on success

If SHA256 verification fails or any step errors, FwupWriter.fwup_abort/1 is called and "update_failed" is reported.

Channel messages handled

Server Event Client Action
update Runs update pipeline via UpdateManager
reboot Calls Client.reboot/0
identify Calls Client.identify/0
extensions:get Joins extensions channel if configured
health:check Calls HealthProvider.health_check/0

Architecture

NervesHubLinkAVM (GenServer)    -- connection lifecycle, message dispatch
  |
  |-- Client (behaviour)        -- update decisions, progress, lifecycle hooks
  |   +-- Client.Default        -- auto-apply, log everything
  |
  |-- FwupWriter (behaviour)    -- hardware-specific firmware writes
  |
  |-- Configurator              -- builds config from opts (auth, URL, SSL)
  |-- UpdateManager             -- orchestrates Client + FwupWriter + Downloader
  |-- Downloader                -- HTTP streaming + SHA256 verification
  |
  |-- Extension (behaviour)     -- pluggable extension interface
  |   +-- Extension.Health      -- wraps HealthProvider for health reporting
  |-- Extensions                -- routes extension events by key prefix
  |
  |-- SharedSecret              -- HMAC signing for shared secret auth
  |-- Channel                   -- Phoenix channel protocol codec
  |-- HTTPClient                -- HTTP layer (AtomVM ahttp_client)
  |-- JSON                      -- JSON codec (AtomVM compatible)
  |
  +-- websocket.erl             -- WebSocket client (TCP + TLS) RFC 6455

Author

Eliel A. Gordon (gordoneliel@gmail.com)

License

MIT