Bluetooth Classic (BR/EDR) — device-level discovery, pairing, and cross-profile session management.
Profile-specific operations live in submodules:
MobBluetooth.Hfp— Hands-Free Profile (audio + vendor AT commands). Use this for headsets, PTT-equipped earpieces, etc.MobBluetooth.Spp— Serial Port Profile (RFCOMM byte streams). Use this for legacy serial-over-Bluetooth devices (Arduino HC-05, OBD-II readers, marine GPS, industrial sensors).
(HID is not supported on Android — receiving HID input requires input-method/HID-host privileges Android denies ordinary apps.)
API style
Same as the rest of Mob: callbacks return socket unchanged, results
arrive in handle_info/2 as messages. There are two families, and they
do not share a uniform arity:
Device-level events are tagged :bt and carry no session id:
{:bt, :discovery_started} # 2-tuple, no payload
{:bt, :discovery_finished}
{:bt, :discovery_cancelled}
{:bt, :discovered, device} # 3-tuple, device map
{:bt, :paired, device}
{:bt, :pair_failed, %{address: addr, reason: atom}}
{:bt, :unpaired, device}
{:bt, :paired_list, [device]}
{:bt, :error, payload}Profile events are tagged by profile (:bt_hfp, :bt_spp) — not
:bt. Once a session exists they carry its integer session_id as the
third element; pre-session failures omit it:
{:bt_hfp, :connected, session_id, payload} # 4-tuple, has session
{:bt_hfp, :connect_failed, %{address: addr, reason: atom}} # 3-tuple, no session yet
{:bt_hfp, :disconnected, session_id, reason}The profile submodules document their own event sets.
Permissions
Bluetooth requires runtime permissions on Android 12+ (API 31+):
:bluetooth_scan— forstart_discovery/1:bluetooth_connect— forpair/2,connect/*,disconnect/2
Request via Mob.Permissions.request/2 before calling MobBluetooth functions.
iOS
Bluetooth Classic on iOS requires Apple's MFi (Made for iPhone)
certification — a paid, NDA-gated program. MobBluetooth is Android-only.
All functions return {:error, :unsupported} synchronously on iOS.
For iOS-equivalent custom-hardware connectivity, use Mob.Ble.
Pairing flow
Two pairing modes, auto-selected by whether :pin is given:
# System UI flow — Android shows a system pairing dialog
socket = MobBluetooth.pair(socket, device)
# Programmatic — PIN supplied via API, no UI
socket = MobBluetooth.pair(socket, device, pin: "0000")If the programmatic PIN fails or the device requires UI confirmation (e.g. numeric comparison), Android falls back to the system UI automatically.
Disconnect
One canonical disconnect for any profile session:
MobBluetooth.disconnect(socket, session_id)The framework looks up which profile owns the session_id and routes
to the right profile-disconnect internally. Emits a profile-specific
disconnect event ({:bt_hfp, :disconnected, session_id, reason} etc).
Summary
Types
A discovered or paired Bluetooth device.
An opaque session identifier for an active profile connection.
Functions
Cancel an in-progress discovery.
Disconnect a profile session by session_id.
List currently paired (bonded) Bluetooth devices.
Pair (bond) with a Bluetooth device.
Begin Bluetooth Classic discovery. Discovered devices arrive as
individual {:bt, :discovered, device} messages, terminated by
{:bt, :discovery_finished}.
Remove an existing pairing (bond).
Types
@type device() :: %{ :address => String.t(), :name => String.t(), optional(:bond_state) => :none | :bonding | :bonded, optional(:device_class) => non_neg_integer(), optional(:uuids) => [String.t()] }
A discovered or paired Bluetooth device.
@type session_id() :: pos_integer()
An opaque session identifier for an active profile connection.
Functions
Cancel an in-progress discovery.
@spec disconnect(socket :: term(), session_id()) :: term()
Disconnect a profile session by session_id.
Works for any profile (MobBluetooth.Hfp, MobBluetooth.Spp) — the
framework dispatches internally based on which profile owns the session.
Emits a profile-specific disconnect event:
{:bt_hfp, :disconnected, session_id, reason}{:bt_spp, :disconnected, session_id, reason}
List currently paired (bonded) Bluetooth devices.
Result arrives as {:bt, :paired_list, [device]}.
Pair (bond) with a Bluetooth device.
Without :pin, Android shows the system pairing dialog (user enters
PIN). With :pin, attempts programmatic pairing using the supplied
PIN; falls back to system UI if the device demands user confirmation.
Result arrives as one of:
{:bt, :paired, device}{:bt, :pair_failed, %{address: String.t(), reason: atom()}}
Begin Bluetooth Classic discovery. Discovered devices arrive as
individual {:bt, :discovered, device} messages, terminated by
{:bt, :discovery_finished}.
Discovery typically runs ~12 seconds on Android.
Remove an existing pairing (bond).
Result: {:bt, :unpaired, device}.