MobBluetooth.Le (mob_bluetooth v0.3.0)

Copy Markdown View Source

Bluetooth Low Energy (BLE) — GATT peripheral role.

This is a different radio and role from the rest of MobBluetooth, which is Bluetooth Classic (BR/EDR) in the central/host role. Here the phone is the peripheral: it runs a GATT server, advertises a service, and a remote central (a laptop, another phone, a desktop's Bluetooth stack) connects to it. This is the piece BLE-MIDI, custom-accessory, and "expose data over BLE" use cases need.

Unlike Classic, BLE works on iOS without MFi (CoreBluetooth's CBPeripheralManager), so MobBluetooth.Le is cross-platform — iOS and Android both supported; only the host (desktop dev) target is unsupported.

Role & scope

Peripheral / GATT-server only, for now: advertise one service with one or more characteristics, push notifications to subscribed centrals, and receive writes. BLE central (scanning for and connecting to other peripherals) is a separate, future addition.

API style

Same as the rest of Mob: callbacks return socket unchanged, results arrive in handle_info/2. LE events are tagged :bt_le:

{:bt_le, :advertising_started}
{:bt_le, :advertising_failed, %{reason: atom}}
{:bt_le, :central_connected, %{central: integer}}
{:bt_le, :central_disconnected, %{central: integer}}
{:bt_le, :subscribed, %{characteristic: String.t()}}     # central enabled notifications
{:bt_le, :unsubscribed, %{characteristic: String.t()}}
{:bt_le, :write, %{characteristic: String.t(), bytes: binary}}  # central wrote to us

:central is an opaque per-connection integer handle. Most peripherals (and BLE-MIDI in particular) can ignore it and treat the link as 1:1.

Permissions

Advertising requires :bluetooth_advertise on Android 12+ (API 31+), in addition to :bluetooth_connect. Request via Mob.Permissions.request/2 before calling. iOS gates BLE behind the NSBluetoothAlwaysUsageDescription Info.plist key (declared by the plugin manifest).

Example — advertise a service and push a notification

service = %{
  local_name: "Mob Peripheral",
  service_uuid: "0000180D-0000-1000-8000-00805F9B34FB",
  characteristics: [
    %{uuid: "00002A37-0000-1000-8000-00805F9B34FB",
      properties: [:notify, :read]}
  ]
}

socket = MobBluetooth.Le.start_advertising(socket, service)
# ... on {:bt_le, :subscribed, _} ...
socket = MobBluetooth.Le.notify(socket, "00002A37-0000-1000-8000-00805F9B34FB", <<0x3C>>)
# ... later ...
socket = MobBluetooth.Le.stop_advertising(socket)

Summary

Types

A GATT characteristic to expose. :properties is any subset of :read, :write, :write_without_response, :notify, :indicate.

A GATT characteristic property.

A GATT service to advertise as a peripheral.

Functions

Push bytes as a notification on the characteristic char_uuid to every subscribed central.

Start the GATT server and begin advertising service so a remote central can connect.

Stop advertising and tear down the GATT server. Connected centrals are disconnected. Idempotent.

Types

characteristic()

@type characteristic() :: %{uuid: String.t(), properties: [characteristic_property()]}

A GATT characteristic to expose. :properties is any subset of :read, :write, :write_without_response, :notify, :indicate.

characteristic_property()

@type characteristic_property() ::
  :read | :write | :write_without_response | :notify | :indicate

A GATT characteristic property.

service()

@type service() :: %{
  :service_uuid => String.t(),
  :characteristics => [characteristic()],
  optional(:local_name) => String.t(),
  optional(:low_latency) => boolean()
}

A GATT service to advertise as a peripheral.

Functions

notify(socket, char_uuid, bytes)

@spec notify(socket :: term(), String.t(), binary()) :: term()

Push bytes as a notification on the characteristic char_uuid to every subscribed central.

No-op (on the native side) if no central has subscribed yet — wait for {:bt_le, :subscribed, _} before relying on delivery. The characteristic must have been declared with :notify (or :indicate).

start_advertising(socket, service)

@spec start_advertising(socket :: term(), service()) :: term()

Start the GATT server and begin advertising service so a remote central can connect.

Lifecycle arrives as {:bt_le, :advertising_started} or {:bt_le, :advertising_failed, %{reason: atom}}; connections as {:bt_le, :central_connected, _} / {:bt_le, :central_disconnected, _}.

Set :low_latency to request a high-priority (low connection-interval) link — recommended for latency-sensitive payloads like MIDI. It's a hint; the OS and the central negotiate the actual interval.

stop_advertising(socket)

@spec stop_advertising(socket :: term()) :: term()

Stop advertising and tear down the GATT server. Connected centrals are disconnected. Idempotent.