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.
Renaming and restoring are asynchronous, so a start waits until every
rename / restore it or an earlier start asked for has landed (or about
1.5 s, logging a warning) before advertising —
{:bt_le, :advertising_started} arrives correspondingly later. That
applies when the adapter doesn't already carry
local_name, and to a start without :local_name while the adapter's own
name is still being restored (e.g. straight after a named advert); a start
without :local_name on an unrenamed adapter begins at once. This is what
makes the advertisement carry the intended name rather than the previous
one, and makes a name too long for the scan response fail that same start
with :data_too_large.
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).
Each start delivers exactly one of :advertising_started /
:advertising_failed, unless a newer start_advertising/2 or a
stop_advertising/1 lands before that outcome: on Android the superseded
or stopped start then delivers nothing (events don't say which start they
belong to, so a late one would read as the newer start's outcome), and a
start still waiting for the adapter's name to change never begins
advertising. A newer start that fails before setup (e.g.
:bad_service_uuid) also stops the previous advert, so the last event
always matches what is advertising.
Stop advertising and tear down the GATT server. Connected centrals are
disconnected. On Android, the adapter name :local_name replaced is put
back. Idempotent.