# `NervesGate`

Public commands and status queries for the gateway. No client process is needed.

## Network

    {:ok, settings} = NervesGate.network_validate(settings)
    {:ok, trial_id} = NervesGate.network_propose(settings)
    :ok = NervesGate.network_confirm(trial_id)
    # Or: NervesGate.network_cancel(trial_id)

Proposals acknowledge temporary application, not persistence.
Unconfirmed changes roll back after 60 seconds. Only first setup through
server-verified USB may auto-save, within its independent 15-second bound.

## Tailscale

    :ok = NervesGate.remote_connect("gateway")
    {:ok, url} = NervesGate.remote_login_url()
    :ok = NervesGate.remote_configure("new-name")
    :ok = NervesGate.remote_disconnect()

Remote writes acknowledge acceptance, not daemon completion. Read `status/1`
for progress. Login URLs are private and never included in status.

## Reading and observing

    :ok = NervesGate.subscribe()
    status = NervesGate.status()

Status reads the domains independently; it is not an atomic snapshot or
cached state. Subscriptions belong to the caller. Property notifications are
hints to read again; subscribe before reading and after reconnecting.

Commands return `:ok`, `{:ok, value}` or `{:error, %{code: code, message: message}}`.
They do not bundle a status read. On an unknown outcome, read before retrying:
caller timeout does not cancel admitted work.

Options:

  * `:access` — trusted server-observed USB access for `network_propose/2` only.
    Never populate it from user parameters or forwarded headers.
  * `:expected` — the state an editor's draft started from: saved network settings
    for `network_propose/2`, or the Tailscale hostname for `remote_connect/2` and
    `remote_configure/2`. The owning process rejects a stale draft before any change.
  * `:network`, `:remote`, `:equipment`, `:indicators`, `:buses` — domain servers for tests or independent instances.
    No client object or extra process is required.

# `command_result`

```elixir
@type command_result() :: :ok | {:error, error()}
```

# `error`

```elixir
@type error() :: %{code: String.t(), message: String.t()}
```

# `result`

```elixir
@type result(value) :: {:ok, value} | {:error, error()}
```

# `bus_capture`

Start a receive-only capture: at most five seconds, 200 CAN frames or 4096 serial bytes.

# `bus_configure`

Configure an allowlisted bus port. CAN applies to the kernel; RS485 opt-in resets on restart.

# `distribution_cookie`

Read the private distribution cookie. Never include it in status, notifications or logs.

# `equipment_label`

```elixir
@spec equipment_label(String.t(), String.t(), keyword()) :: command_result()
```

Save a local equipment label. IDs come from status; an empty label removes it.

# `equipment_scan`

```elixir
@spec equipment_scan(keyword()) :: command_result()
```

Run one explicit ARP-only scan of the confirmed equipment subnet (at most /24 and five seconds).

# `equipment_share`

```elixir
@spec equipment_share(keyword()) :: command_result()
```

Advertise the confirmed dedicated equipment IPv4 subnet. Returns after local readback.

# `equipment_unshare`

```elixir
@spec equipment_unshare(keyword()) :: command_result()
```

Withdraw the confirmed equipment subnet, preserving other routes and Tailscale preferences.

# `identify`

```elixir
@spec identify(keyword()) :: command_result()
```

Identify with all three LEDs for five seconds, then resume current status patterns.

# `modbus_read`

Send one explicitly authorized Modbus RTU read (function 3/4), with no retry or scan.

# `network_cancel`

```elixir
@spec network_cancel(String.t(), keyword()) :: command_result()
```

Restore the saved network settings for the matching trial.

# `network_confirm`

```elixir
@spec network_confirm(String.t(), keyword()) :: command_result()
```

Persist an applied trial after checking its ID, deadline and current readiness.

# `network_propose`

```elixir
@spec network_propose(map(), keyword()) :: result(String.t())
```

Apply a protected trial. Returns its ID; use that exact ID to confirm or cancel.

# `network_validate`

```elixir
@spec network_validate(map()) :: result(map())
```

Validate and normalize network settings without a running gateway or any effects.

# `remote_configure`

```elixir
@spec remote_configure(String.t(), keyword()) :: command_result()
```

Request a Tailscale hostname change without replacing other daemon preferences.

# `remote_connect`

```elixir
@spec remote_connect(String.t(), keyword()) :: command_result()
```

Request Tailscale connection/enrollment; optional `auth_key:` is used once, never saved. Returns acceptance.

# `remote_disconnect`

```elixir
@spec remote_disconnect(keyword()) :: command_result()
```

Request Tailscale down without signing out. May interrupt callers using that VPN.

# `remote_login_url`

```elixir
@spec remote_login_url(keyword()) :: result(String.t() | nil)
```

Read the private HTTPS enrollment URL, or nil. Do not log or broadcast it.

# `ssh_add_key`

```elixir
@spec ssh_add_key(String.t()) :: command_result()
```

Validate and persist one OpenSSH public key before allowing new SSH logins.

# `ssh_remove_key`

```elixir
@spec ssh_remove_key(String.t()) :: command_result()
```

Persist removal by fingerprint from status.ssh.keys. Existing sessions remain open.

# `status`

```elixir
@spec status(keyword()) :: map()
```

Read domain status, capabilities and action availability.

# `storage_setup`

Commission application storage: use a card, explicitly format it, or skip.

# `subscribe`

```elixir
@spec subscribe(keyword()) :: command_result()
```

Subscribe the calling process to domain observations; read status afterward.

# `target`

# `version`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
