Improv.Protocol (improv v0.1.0)

Copy Markdown View Source

Pure wire codec for the Improv Wi-Fi BLE protocol.

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.

Summary

Types

Decoded inbound RPC command.

Reasons a command frame is rejected.

Functions

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.

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

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 an inbound RPC-command frame written to the rpc-command characteristic.

Command id for a device-info result frame.

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

Encode a framed RPC result for command_id carrying strings.

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

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, []).

Command id of the identify command.

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

The Improv primary service UUID.

Command id for a submit-wifi result frame.

Types

command()

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

Decoded inbound RPC command.

decode_error()

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

Reasons a command frame is rejected.

Functions

capabilities(opts \\ [])

@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()

@spec characteristic_uuids() :: %{required(atom()) => String.t()}

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

checksum(bin)

@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(arg1)

@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 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()

@spec device_info_command() :: byte()

Command id for a device-info result frame.

encode_error(error)

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

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

encode_rpc_result(command_id, strings)

@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(state)

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

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

encode_wifi_network_entry(ssid, rssi, secured)

@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()

@spec identify_command() :: byte()

Command id of the identify command.

request_networks_command()

@spec request_networks_command() :: byte()

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

service_uuid()

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

The Improv primary service UUID.

submit_wifi_command()

@spec submit_wifi_command() :: byte()

Command id for a submit-wifi result frame.