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
On Android, request :bluetooth_connect via Mob.Permissions.request/2
before calling — on Android 12+ (API 31+) it grants the "Nearby devices"
group, which includes BLUETOOTH_ADVERTISE. iOS gates BLE behind the
NSBluetoothAlwaysUsageDescription Info.plist key (declared by the plugin
manifest).
:local_name on Android
Android advertises the adapter's name, so :local_name renames the phone's
Bluetooth adapter while advertising — system-wide: other apps and the
Bluetooth settings screen see local_name too. The adapter's own name is
saved first and put back on stop_advertising/1 and when advertising fails
({:bt_le, :advertising_failed, _}); restarting with another :local_name
keeps the saved name, and restarting without one restores it. If the app
process dies while advertising, the rename persists until the user changes
it. On iOS :local_name only goes into the advertisement.
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. On Android, the adapter name :local_name replaced is put
back. Idempotent.
Types
@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.
@type characteristic_property() ::
:read | :write | :write_without_response | :notify | :indicate
A GATT characteristic property.
@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
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 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.
On Android, :local_name is the phone's Bluetooth adapter name for as long
as advertising runs (see ":local_name on Android" above).
Stop advertising and tear down the GATT server. Connected centrals are
disconnected. On Android, the adapter name :local_name replaced is put
back. Idempotent.