# `Improv.Protocol`
[🔗](https://github.com/bbangert/improv/blob/v0.1.1/lib/improv/protocol.ex#L1)

Pure wire codec for the [Improv Wi-Fi BLE protocol](https://www.improv-wifi.com/ble/).

No D-Bus, no processes — every byte the GATT server reads or notifies is built
and parsed here, so the protocol is 100% host-testable. Higher layers
(`Improv` and its GATT server) only move opaque binaries.

## Wire format (locked against improv-wifi.com/ble + improv-wifi/sdk-js)

The RPC command/result characteristics carry framed packets:

    [command_id] [data_length] [data … data_length bytes] [checksum]

`checksum` is the low byte of the sum of every preceding byte in the frame
(command, length, and all data bytes). The current-state, error-state and
capabilities characteristics each carry a single bare byte (no framing).

### Submit Wi-Fi (`0x01`) data payload

    [ssid_len] [ssid …] [pwd_len] [pwd …]

Open networks send `pwd_len = 0`.

### Identify (`0x02`) / Device Info (`0x03`)

Both carry no data (`[cmd][0x00][cs]`). Identify has **no RPC result** — the
device just does something physically observable. Device Info replies with one
RPC result carrying four length-prefixed strings: firmware name, firmware
version, hardware variant, device name.

### Request scanned networks (`0x04`)

Command has no data (`[0x04][0x00][cs]`). The reply is sent as **one RPC-result
notification per network**, each carrying three strings `[ssid, rssi, auth]`,
followed by a final **empty** result (`data_len 0`) that terminates the list.

## Capabilities

The capabilities byte is derived from what the host configures (see
`capabilities/1`): bit2 (`0x04`, scan-wifi) is always set — the command is
built in, and it's what makes improv-wifi.com show the network dropdown.
bit0 (`0x01`) is set iff an identify callback is configured, bit1 (`0x02`)
iff device-info strings are provided. bit3 (hostname) is unsupported.

# `command`

```elixir
@type command() ::
  {:submit_wifi, ssid :: binary(), password :: binary()}
  | {:identify}
  | {:device_info}
  | {:request_wifi_networks}
```

Decoded inbound RPC command.

# `decode_error`

```elixir
@type decode_error() :: {:error, :bad_checksum | :unknown_command | :invalid}
```

Reasons a command frame is rejected.

# `capabilities`

```elixir
@spec capabilities(keyword()) :: binary()
```

Capabilities characteristic value (single byte), derived from what the host
configures: bit2 (scan-wifi) is always set — the command is built in;
bit0 iff `identify?: true`; bit1 iff `device_info?: true`. Pure.

The advertisement's ServiceData capabilities byte MUST carry this same
derived value — derive once and share (the supervisor does this).

# `characteristic_uuids`

```elixir
@spec characteristic_uuids() :: %{required(atom()) =&gt; String.t()}
```

Map of the five characteristic roles to their UUIDs:
`:current_state`, `:error_state`, `:rpc_command`, `:rpc_result`, `:capabilities`.

# `checksum`

```elixir
@spec checksum(binary()) :: byte()
```

Checksum of a complete frame *without* its trailing checksum byte:
the low byte of the sum of every byte. Exposed for framing helpers/tests.

# `decode_command`

```elixir
@spec decode_command(binary()) :: command() | decode_error()
```

Decode an inbound RPC-command frame written to the rpc-command characteristic.

Validates structure and checksum before dispatching. Returns the decoded
`t:command/0`, or `{:error, reason}`:

  * `:invalid` — too short, length field inconsistent, or a malformed
    submit-wifi payload (maps to Improv error `invalid RPC packet`).
  * `:bad_checksum` — checksum mismatch (also `invalid RPC packet`).
  * `:unknown_command` — well-formed frame for a command we don't implement.

# `device_info_command`

```elixir
@spec device_info_command() :: byte()
```

Command id for a device-info result frame.

# `encode_error`

```elixir
@spec encode_error(atom()) :: binary()
```

Encode an error-state value (single byte, no framing).

# `encode_rpc_result`

```elixir
@spec encode_rpc_result(byte(), [binary()]) :: binary()
```

Encode a framed RPC result for `command_id` carrying `strings`.

Each string is length-prefixed; an empty list yields a bare `[cmd][0][cs]`
frame (the terminator for a scan, or a result with no payload).

# `encode_state`

```elixir
@spec encode_state(atom()) :: binary()
```

Encode a current-state value (single byte, no framing).

# `encode_wifi_network_entry`

```elixir
@spec encode_wifi_network_entry(binary(), integer(), boolean()) :: binary()
```

Encode one Wi-Fi network entry as a scan RPC-result notification:
three strings `[ssid, rssi, auth]` where `rssi` is rendered as a signed
decimal and `secured` becomes `"YES"`/`"NO"`. Terminate the list with
`encode_rpc_result(0x04, [])`.

# `identify_command`

```elixir
@spec identify_command() :: byte()
```

Command id of the identify command.

# `request_networks_command`

```elixir
@spec request_networks_command() :: byte()
```

Command id for a request-scanned-networks result frame.

# `service_uuid`

```elixir
@spec service_uuid() :: String.t()
```

The Improv primary service UUID.

# `submit_wifi_command`

```elixir
@spec submit_wifi_command() :: byte()
```

Command id for a submit-wifi result frame.

---

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