Meshcore (Meshcore v0.1.0)

Copy Markdown View Source

Elixir client for MeshCore companion radios.

This is a port of the Python library 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

PythonElixir
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_infoMeshcore.contacts(mc), Meshcore.self_info(mc)
timeouts in secondstimeouts 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.

Summary

Functions

Whether contacts are refreshed automatically on adverts and path updates.

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

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

Whether the transport is connected.

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

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

Connects over BLE and performs the handshake.

Connects over a serial port and performs the handshake.

Connects over TCP and performs the handshake.

Default command timeout in milliseconds.

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

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

Forgets all pending contacts.

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

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

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

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

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

Enables or disables automatic contact refresh.

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.

Sets the default command timeout in milliseconds.

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.

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

Synchronously stops the connection process without emitting events.

Stops automatic message fetching.

Subscribes a process to events.

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

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

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

Types

mc()

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

Functions

auto_update_contacts?(mc)

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

Whether contacts are refreshed automatically on adverts and path updates.

child_spec(opts)

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

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

connect(mc)

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

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

Whether the transport is connected.

contacts(mc)

@spec contacts(mc()) :: %{required(String.t()) => map()}

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

contacts_dirty?(mc)

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

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

create_ble(opts)

@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(port, opts \\ [])

@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(host, port, opts \\ [])

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

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

Default command timeout in milliseconds.

disconnect(mc)

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

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

ensure_contacts(mc, follow \\ false)

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

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

Forgets all pending contacts.

get_contact_by_key_prefix(mc, prefix)

@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(mc, name)

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

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

pending_contacts(mc)

@spec pending_contacts(mc()) :: %{required(String.t()) => map()}

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

pop_pending_contact(mc, key)

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

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

self_info(mc)

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

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

set_auto_update_contacts(mc, value)

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

Enables or disables automatic contact refresh.

set_decrypt_channel_logs(mc, value)

@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(mc, ms)

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

Sets the default command timeout in milliseconds.

start_auto_message_fetching(mc)

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

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

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

Synchronously stops the connection process without emitting events.

stop_auto_message_fetching(mc)

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

Stops automatic message fetching.

subscribe(mc, type, opts \\ [])

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

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

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

unsubscribe(subscription)

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

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

wait_for_event(mc, type, opts \\ [])

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