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

Improv-over-BLE Wi-Fi provisioning manager — the GenServer that ties the
protocol codec, GATT server and advertisement into a working subsystem.

Always started on BT targets but **disarmed** by default. It arms (exports the
GATT app + advertisement, starts a session timer) only when the device boots
with no network connectivity, and tears itself back down on a 5-minute idle
timeout or once provisioning completes.

## State machine

    :disarmed → :advertising → :connected → :provisioning → :provisioned

A failed/timed-out connect reverts `:provisioning → :connected` and surfaces the
reason via `state.error` (an error byte to the client) — there is no distinct
`:error` FSM state.

We always advertise current-state AUTHORIZED until a submit moves us to
PROVISIONING; security comes from the no-connectivity arm gate + the session
timeout, not from the Improv authorization handshake.

## Arm policy

Arm **only** on a no-connectivity boot, and **only once per boot** — we do NOT
re-arm if connectivity drops later (re-arm requires a reboot). After a session
ends (timeout or provisioned) we disarm and stay disarmed.

## Timeout reset rule

The 5-minute idle timer resets only on a *meaningful* state advance (first
client connect, a valid submit) — never on arbitrary client activity, so a
flooding peer cannot hold the session open forever. An absolute session cap
(default 15 minutes) bounds the whole session regardless of activity.

State changes broadcast `{:improv_status, status}` on the `"bluetooth:improv"`
PubSub topic (`status_topic/0`) when a `pubsub:` is configured, for host UIs.

Wi-Fi scan/apply goes through the `:wifi` dep (`Improv.Wifi` by default);
this module routes RPCs to it and pushes the resulting GATT notifications.

# `fsm`

```elixir
@type fsm() :: :disarmed | :advertising | :connected | :provisioning | :provisioned
```

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `command_action`

```elixir
@spec command_action(Improv.Protocol.command() | Improv.Protocol.decode_error()) ::
  {:submit, binary(), binary()}
  | :scan
  | :identify
  | :device_info
  | {:reject, atom()}
```

Map a decoded command (or decode error) to the manager action. Pure — the
GenServer executes the returned action. Returns:

  * `{:submit, ssid, pwd}` for a valid submit (SSID 1..32 bytes, password
    empty or 8..63 bytes),
  * `:scan` for request-networks,
  * `{:reject, error_atom}` for an invalid submit / decode failure.

# `current_state_atom`

```elixir
@spec current_state_atom(fsm()) :: atom()
```

Map an FSM state to the current-state atom we advertise/notify. Pure.

# `start_link`

# `status`

```elixir
@spec status(GenServer.server()) :: %{state: fsm(), error: atom() | nil}
```

Current provisioning status: `%{state: fsm, error: atom | nil}`. Returns the
disarmed shape when the manager isn't running (off-target / subtree down).

# `status_topic`

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

PubSub topic carrying `{:improv_status, status}` messages.

# `valid_password?`

```elixir
@spec valid_password?(binary()) :: boolean()
```

Password pre-validation: empty (open network) or the WPA-PSK passphrase rule,
8..63 bytes. Rejecting garbage here keeps it from ever reaching
`VintageNet.configure` (which would raise inside the wifi seam). Pure.

# `valid_ssid?`

```elixir
@spec valid_ssid?(binary()) :: boolean()
```

Improv SSID length rule: 1..32 bytes. Pure.

---

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