Canonical configurable BLE mesh transport for Capacitor apps
The plugin provides Android and iOS radios, chunking, bounded reassembly, relay, background lifecycle hooks and a stable Capacitor API. Product identity, discovery UUID policy, cryptography and payload semantics stay in the consuming app. See the transport contract.
Until an npm release exists, pin an immutable Git commit over SSH:
npm install 'git+ssh://git@github.com/forgesworn/capacitor-mesh-ble.git#<commit-sha>'For a published release:
npm install capacitor-mesh-ble@0.1.0Sync native files
npx cap syncstart(...)stop()broadcast(...)send(...)setKeepaliveFrame(...)getStatus()startRssiSampling(...)stopRssiSampling()addListener('frame', ...)addListener('peer', ...)addListener('status', ...)addListener('rssi', ...)removeAllListeners()- Interfaces
start(options: MeshBleStartOptions) => Promise<void>| Param | Type |
|---|---|
options |
MeshBleStartOptions |
stop() => Promise<void>broadcast(options: { data: string; }) => Promise<{ queuedPeers: number; }>| Param | Type |
|---|---|
options |
{ data: string; } |
Returns: Promise<{ queuedPeers: number; }>
send(options: { peer: string; data: string; }) => Promise<{ queuedPeers: number; }>| Param | Type |
|---|---|
options |
{ peer: string; data: string; } |
Returns: Promise<{ queuedPeers: number; }>
setKeepaliveFrame(options: { data: string | null; }) => Promise<void>iOS may repeat this frame while JavaScript is suspended in the background.
| Param | Type |
|---|---|
options |
{ data: string | null; } |
getStatus() => Promise<MeshBleStatus>Returns: Promise<MeshBleStatus>
startRssiSampling(options?: MeshBleStartRssiSamplingOptions | undefined) => Promise<void>Start periodic RSSI sampling: polls connected GATT links and attributes scanned advertisement RSSI, for peers already identified via a prior frame exchange. Off by default (battery cost). Idempotent — calling this again while sampling just updates the interval. Stops automatically when the transport is stopped.
Android only. iOS has no such method in its pluginMethods list, so a call
there is REJECTED by the native bridge (not silently ignored) — callers must
guard with try/catch if they run cross-platform. The web implementation, by
contrast, DOES implement this method, as an inert no-op: there is no BLE
hardware to sample, so it resolves rather than rejecting. NB the
security-relevant honesty gate (no attribution while relaying) is likewise
Android-only; if iOS ever grows sampling, the gate MUST be ported in the same
change or iOS emits dishonest attributions.
| Param | Type |
|---|---|
options |
MeshBleStartRssiSamplingOptions |
stopRssiSampling() => Promise<void>Stop RSSI sampling. Idempotent — safe to call when not sampling. Shares startRssiSampling's platform behaviour: Android runs the real stop, iOS rejects (bridge-level, method not implemented), web resolves as a no-op.
addListener(eventName: 'frame', listenerFunc: (event: MeshBleFrameEvent) => void) => Promise<PluginListenerHandle>| Param | Type |
|---|---|
eventName |
'frame' |
listenerFunc |
(event: MeshBleFrameEvent) => void |
Returns: Promise<PluginListenerHandle>
addListener(eventName: 'peer', listenerFunc: (event: MeshBlePeerEvent) => void) => Promise<PluginListenerHandle>| Param | Type |
|---|---|
eventName |
'peer' |
listenerFunc |
(event: MeshBlePeerEvent) => void |
Returns: Promise<PluginListenerHandle>
addListener(eventName: 'status', listenerFunc: (status: MeshBleStatus) => void) => Promise<PluginListenerHandle>| Param | Type |
|---|---|
eventName |
'status' |
listenerFunc |
(status: MeshBleStatus) => void |
Returns: Promise<PluginListenerHandle>
addListener(eventName: 'rssi', listenerFunc: (event: MeshBleRssiSample) => void) => Promise<PluginListenerHandle>| Param | Type |
|---|---|
eventName |
'rssi' |
listenerFunc |
(event: MeshBleRssiSample) => void |
Returns: Promise<PluginListenerHandle>
removeAllListeners() => Promise<void>| Prop | Type | Description |
|---|---|---|
room |
string |
Product-defined room/domain separator carried inside every envelope. |
selfId |
string |
Product-defined local peer identifier, revealed only after a GATT link. |
serviceUuid |
string |
Runtime discovery/GATT UUID. The plugin advertises and hosts its GATT service under exactly this one UUID. The plugin never derives or rotates it. |
scanUuids |
string[] |
Optional set of service UUIDs the scanner filters for, when a product rotates its serviceUuid on a time window and must still discover peers a window or two away (clock skew, a rotation boundary crossed at slightly different moments). Match semantics are OR: a peer advertising ANY of these is a candidate. serviceUuid is always implicitly included. Omit (or leave empty) for the classic single-UUID behaviour, where the plugin scans for exactly serviceUuid. The plugin advertises only serviceUuid, never this whole set. |
hops |
number |
Maximum relay hops for locally-originated frames. Zero disables relaying. |
foregroundService |
boolean |
Keep the Android radio anchored in a connected-device foreground service. |
notificationTitle |
string |
Android foreground-service notification title. |
notificationText |
string |
Android foreground-service notification body. |
maxEnvelopeBytes |
number |
Hard ceiling for a UTF-8 envelope before chunking. Default: 8192. |
maxSeenIds |
number |
Deduplication window. Default: 512 envelope identifiers. |
reassemblyTtlMs |
number |
Incomplete chunk assembly lifetime. Default: 30000ms. |
maxClientLinks |
number |
Android client-role GATT link cap. Default: 3. |
connectCooldownMs |
number |
Android reconnect backoff per BLE address. Default: 4000ms. |
connectThrottleMs |
number |
Android global connection-attempt throttle. Default: 1500ms. |
| Prop | Type | Description |
|---|---|---|
native |
boolean |
|
platform |
'android' | 'ios' | 'web' |
|
supported |
boolean |
|
running |
boolean |
|
bluetooth |
string |
|
permissions |
string |
|
missingPermissions |
string[] |
|
room |
string | null |
|
selfId |
string | null |
|
serviceUuid |
string | null |
|
scanUuids |
string[] |
The UUID set the scanner is filtering for (includes serviceUuid). Single entry unless the product supplied a wider scanUuids window. |
advertising |
boolean |
|
scanning |
boolean |
|
gattServer |
boolean |
|
connectedPeers |
number |
|
writablePeers |
number |
|
knownPeers |
number |
|
queuedChunks |
number |
|
txFrames |
number |
|
txChunks |
number |
|
rxFrames |
number |
|
rxChunks |
number |
|
droppedFrames |
number |
|
relayedFrames |
number |
|
lastError |
string | null |
|
peers |
MeshBlePeerStatus[] |
|
updatedAt |
number |
| Prop | Type |
|---|---|
address |
string |
peerIds |
string[] |
connected |
boolean |
writable |
boolean |
mtu |
number |
queuedChunks |
number |
writing |
boolean |
| Prop | Type | Description |
|---|---|---|
intervalMs |
number |
Sampling interval in ms. Default: 2000, bounded 500-10000. |
| Prop | Type |
|---|---|
remove |
() => Promise<void> |
| Prop | Type |
|---|---|
from |
string |
data |
string |
A product peer became reachable through a learned BLE next hop, or that route was lost.
| Prop | Type |
|---|---|
peer |
string |
connected |
boolean |
A raw RSSI sample, attributed to the mesh peer id bound to the MAC it was observed
on. The binding comes from a prior in-room frame exchange, and attribution is only
emitted on a DIRECT, non-relaying link (discreet mode): while this device relays
(crowd/mesh mode) a bound id can belong to a relayer's MAC, or be injected over the
keyless crowd UUID, so no rssi event is emitted at all. It is therefore a
proximity hint on a trusted-neighbour link, NOT a cryptographic identity proof —
a consumer must not treat it as authentication. Banding or distance estimation is a
product concern; this plugin reports dBm only. Emitted on Android only for now.
| Prop | Type | Description |
|---|---|---|
peer |
string |
The mesh peer id this sample is attributed to (bound via a prior direct frame exchange; only emitted on a non-relaying link — see the interface note). |
address |
string |
The BLE MAC address the sample was observed at. |
rssi |
number |
Signal strength in dBm. |
source |
'gatt' | 'advert' |
Whether the reading came from a live GATT link or a scanned advertisement. |
at |
number |
Epoch ms when the sample was taken. |