# `Meshcore`
[🔗](https://github.com/nyannus/meshcore_ex/blob/v0.1.0/lib/meshcore.ex#L1)

Elixir client for [MeshCore](https://meshcore.io) companion radios.

This is a port of the Python library
[`meshcore_py`](https://github.com/fdlamotte/meshcore_py): same features, same
function names, with an OTP/Elixir flavour. Connect over **serial**, **TCP** or
**BLE**, run commands, and react to events.

## Quick start

    {:ok, mc} = Meshcore.create_serial("/dev/ttyUSB0")

    {:ok, contacts} =
      case Meshcore.Commands.get_contacts(mc) do
        {:ok, event} -> {:ok, event.payload}
        {:error, event} -> raise "failed: #{inspect(event.payload)}"
      end

    contact = contacts |> Map.values() |> hd()
    {:ok, _sent} = Meshcore.Commands.send_msg(mc, contact, "Hello from Elixir!")

    Meshcore.disconnect(mc)

## Mapping from Python

| Python                                   | Elixir                                              |
| ---------------------------------------- | --------------------------------------------------- |
| `await MeshCore.create_serial(port)`     | `Meshcore.create_serial(port)`                      |
| `await mc.commands.send_msg(c, "hi")`    | `Meshcore.Commands.send_msg(mc, c, "hi")`           |
| `result.type == EventType.ERROR`         | `{:error, %Meshcore.Event{}}`                       |
| `mc.subscribe(EventType.ACK, cb, filt)`  | `Meshcore.subscribe(mc, :ack, attribute_filters: filt)` |
| `await mc.wait_for_event(...)`           | `Meshcore.wait_for_event(mc, type, opts)`           |
| `mc.contacts`, `mc.self_info`            | `Meshcore.contacts(mc)`, `Meshcore.self_info(mc)`   |
| timeouts in **seconds**                  | timeouts in **milliseconds**                        |

See the *Migrating from Python* guide for the complete list.

## Events

Events arrive as messages: `{:meshcore, subscription_ref, %Meshcore.Event{}}`.

    {:ok, sub} = Meshcore.subscribe(mc, :contact_msg_recv)

    receive do
      {:meshcore, ref, %Meshcore.Event{payload: %{text: text}}} when ref == sub.ref ->
        IO.puts(text)
    end

## Supervision

Put a connection in your own supervision tree with `child_spec/1`:

    children = [
      {Meshcore,
       name: MyApp.Radio,
       transport: {Meshcore.Transport.TCP, host: "192.168.1.100", port: 5000},
       auto_reconnect: true}
    ]

and use the registered name as `mc` everywhere.

# `mc`

```elixir
@type mc() :: GenServer.server()
```

# `auto_update_contacts?`

```elixir
@spec auto_update_contacts?(mc()) :: boolean()
```

Whether contacts are refreshed automatically on adverts and path updates.

# `child_spec`

```elixir
@spec child_spec(keyword()) :: Supervisor.child_spec()
```

Child specification for supervision trees. Restarts only after abnormal
exits (a `disconnect/1` is final).

# `connect`

```elixir
@spec connect(mc()) :: {:ok, Meshcore.Event.t()} | {:error, term()}
```

Connects (when started with `connect: false`) and runs the app-start
handshake. Returns `{:ok, self_info_event}` or `{:error, reason}`.

# `connected?`

```elixir
@spec connected?(mc()) :: boolean()
```

Whether the transport is connected.

# `contacts`

```elixir
@spec contacts(mc()) :: %{required(String.t()) =&gt; map()}
```

The known contacts, `%{public_key_hex => contact}`.

# `contacts_dirty?`

```elixir
@spec contacts_dirty?(mc()) :: boolean()
```

`true` when a push notification suggests the contact list is out of date.

# `create_ble`

```elixir
@spec create_ble(keyword()) :: {:ok, pid()} | {:error, term()}
```

Connects over BLE and performs the handshake.

BLE needs a platform adapter (see `Meshcore.Transport.BLE.Adapter`), passed as
`:adapter`. Options: `:adapter` (required), `:address` (BLE address or part of
the advertised name; `nil` scans for a `MeshCore*` device), `:pin` (pair
after connecting), `:adapter_opts`, plus the options of `create_tcp/3`.

# `create_serial`

```elixir
@spec create_serial(String.t(), keyword()) :: {:ok, pid()} | {:error, term()}
```

Connects over a serial port and performs the handshake.

Requires the optional `:circuits_uart` dependency. If the device does not
answer, the connection is retried once with the DTR line inverted (some
boards reset on DTR).

Options: `:baudrate` (default `115200`), `:dtr` (`true`), `:rts` (`false`),
`:cx_delay` (ms, `100`), plus the options of `create_tcp/3`.

# `create_tcp`

```elixir
@spec create_tcp(String.t() | :inet.ip_address(), :inet.port_number(), keyword()) ::
  {:ok, pid()} | {:error, term()}
```

Connects over TCP and performs the handshake.

Returns `{:ok, mc}` or `{:error, reason}`. The connection process is started
under `Meshcore.ConnectionSupervisor` and is **not** restarted when it
crashes – use `child_spec/1` for that.

Options: `:debug`, `:only_error`, `:default_timeout`, `:auto_reconnect`,
`:max_reconnect_attempts` (see `start_link/1`), `:connect_timeout`.

# `default_timeout`

```elixir
@spec default_timeout(mc()) :: pos_integer()
```

Default command timeout in milliseconds.

# `disconnect`

```elixir
@spec disconnect(mc()) :: :ok
```

Disconnects from the device and stops the connection process, emitting a
`:disconnected` event (`reason: "manual_disconnect"`) first.

# `ensure_contacts`

```elixir
@spec ensure_contacts(mc(), boolean()) :: boolean()
```

Loads the contact list if it is empty – or, with `follow` set, if it is
marked dirty – and returns whether a refresh was requested.

# `flush_pending_contacts`

```elixir
@spec flush_pending_contacts(mc()) :: :ok
```

Forgets all pending contacts.

# `get_contact_by_key_prefix`

```elixir
@spec get_contact_by_key_prefix(mc(), String.t()) :: map() | nil
```

Finds a contact by (case-insensitive, possibly partial) hex public key prefix.

# `get_contact_by_name`

```elixir
@spec get_contact_by_name(mc(), String.t()) :: map() | nil
```

Finds a contact by (case-insensitive) advertised name.

# `pending_contacts`

```elixir
@spec pending_contacts(mc()) :: %{required(String.t()) =&gt; map()}
```

Contacts advertised but not added (manual-add mode), `%{public_key_hex => contact}`.

# `pop_pending_contact`

```elixir
@spec pop_pending_contact(mc(), String.t()) :: map() | nil
```

Removes and returns a pending contact by full hex public key.

# `self_info`

```elixir
@spec self_info(mc()) :: map()
```

The device's `:self_info` payload (empty map before the handshake).

# `set_auto_update_contacts`

```elixir
@spec set_auto_update_contacts(mc(), boolean()) :: :ok
```

Enables or disables automatic contact refresh.

# `set_decrypt_channel_logs`

```elixir
@spec set_decrypt_channel_logs(mc(), boolean()) :: :ok
```

Enables decryption of group-channel packets seen in the RX log. Needed to
annotate `:channel_msg_recv` events with `:path`, `:snr`, `:rssi` and
`:recv_time`.

# `set_default_timeout`

```elixir
@spec set_default_timeout(mc(), pos_integer()) :: :ok
```

Sets the default command timeout in milliseconds.

# `start_auto_message_fetching`

```elixir
@spec start_auto_message_fetching(mc()) :: :ok
```

Starts pulling queued messages whenever the device announces them
(`:messages_waiting`), and once immediately. The messages show up as
`:contact_msg_recv` / `:channel_msg_recv` events for subscribers.

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Starts a connection process, linked to the caller – the supervised variant of
the `create_*` functions.

Unlike `create_tcp/3` and friends it does not block on the device: with
`connect: true` (default) it connects in the background, retrying according to
`:auto_reconnect`. Use `connect/1` to wait for the handshake.

Options:

  * `:transport` (required) – `{module, options}` implementing `Meshcore.Transport`
  * `:name` – register the process under this name
  * `:connect` – connect right after starting (default `true`)
  * `:auto_reconnect` – reconnect with exponential backoff (1s, 2s, 4s, 8s…)
    after the link is lost (default `false`)
  * `:max_reconnect_attempts` – default `3`
  * `:default_timeout` – command timeout in ms (default `15_000`)
  * `:debug` / `:only_error` – raise or lower the log level of this connection
    (equivalent to Python's flags)

# `stop`

```elixir
@spec stop(mc()) :: :ok
```

Synchronously stops the connection process without emitting events.

# `stop_auto_message_fetching`

```elixir
@spec stop_auto_message_fetching(mc()) :: :ok
```

Stops automatic message fetching.

# `subscribe`

```elixir
@spec subscribe(
  mc(),
  Meshcore.Event.type() | [Meshcore.Event.type()] | nil | :all,
  keyword()
) ::
  {:ok, Meshcore.Subscription.t()}
```

Subscribes a process to events.

`type` is an event type (`t:Meshcore.Event.type/0`), a list of types, or `nil`
/ `:all` for everything. Matching events are sent to the subscriber as
`{:meshcore, ref, %Meshcore.Event{}}`, `ref` being `subscription.ref`.

Options:

  * `:attribute_filters` – map (or keyword list) of attributes the event must
    carry, e.g. `%{channel_idx: 1}` or `%{pubkey_prefix: "a1b2c3d4e5f6"}`
  * `:pid` – the subscriber (default: the calling process)

Subscriptions end when the subscriber exits or on `unsubscribe/1`.

# `time`

```elixir
@spec time(mc()) :: non_neg_integer()
```

The last device time seen in a `:current_time` event (`0` if never).

# `unsubscribe`

```elixir
@spec unsubscribe(Meshcore.Subscription.t()) :: :ok
```

Cancels a subscription. Messages already in the mailbox stay there.

# `wait_for_event`

```elixir
@spec wait_for_event(mc(), Meshcore.Event.type() | [Meshcore.Event.type()], keyword()) ::
  {:ok, Meshcore.Event.t()} | {:error, :timeout | :disconnected}
```

Waits for an event of `type` (or any of a list of types) matching
`:attribute_filters`.

Returns `{:ok, event}` or `{:error, :timeout}`.
Options: `:attribute_filters`, `:timeout` (ms, default: the connection's
default timeout).

---

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