Skip to content

Commit bc67d0e

Browse files
committed
Document authoring plugins in Embedded Swift
1 parent f148df9 commit bc67d0e

1 file changed

Lines changed: 100 additions & 0 deletions

File tree

PluginSDK/README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# BLE Parser Plugin SDK (Embedded Swift)
2+
3+
Write BluetoothExplorer parser plugins in **Embedded Swift**, compiled to a small wasm32 module the
4+
app loads at runtime. A plugin decodes one kind of Bluetooth payload — manufacturer data, service
5+
data, a characteristic or a descriptor value — into labelled fields the app displays.
6+
7+
- `BLEPluginSDK/` — the guest SDK: envelope decoding, a payload cursor, and a CBOR field builder.
8+
- `Examples/BatteryLevel/` — a complete plugin (GATT Battery Level, `0x2A19`) you can copy.
9+
- `../Documentation/PluginABI.md` — the normative wire contract.
10+
11+
## Prerequisites
12+
13+
```sh
14+
swift sdk list # need swift-<version>_wasm-embedded
15+
brew install binaryen # for wasm-opt
16+
```
17+
18+
If the embedded SDK is missing, install the `wasm-embedded` Swift SDK artifact bundle matching your
19+
toolchain with `swift sdk install <url>`.
20+
21+
## Writing a plugin
22+
23+
The only file you write is a pure function from `ParseInput` to `Fields?`. Return `nil` for input
24+
your plugin does not recognize — that is not an error, the app simply falls back.
25+
26+
```swift
27+
import BLEPluginSDK
28+
29+
func parseCharacteristic(_ input: ParseInput) -> Fields? {
30+
guard input.uuid?.assignedNumber16 == 0x2A19 else { return nil }
31+
var payload = input.payload
32+
guard let level = payload.readUInt8() else { return nil }
33+
34+
var fields = Fields(summary: "Battery")
35+
fields.uint("level", label: "Battery Level", UInt64(level), unit: "%")
36+
return fields
37+
}
38+
```
39+
40+
`Exports.swift` in the example is boilerplate you copy unchanged — it declares the ABI exports and
41+
forwards them to `PluginRuntime`. It lives in *your* module rather than the SDK because
42+
`@_expose(wasm)` symbols are dropped when they come from a dependency module
43+
(swiftlang/swift#77812). Delete the capability shims your plugin does not implement; the host only
44+
invokes exports that are present.
45+
46+
### Reading the payload
47+
48+
`PayloadReader` is an allocation-free cursor: `readUInt8`, `readInt8`, `readUInt16BigEndian`,
49+
`readUInt16LittleEndian`, `readUInt32LittleEndian`, `readUUID`, `readBytes(_:_:)`, plus `remaining`
50+
and `remainingBytes(_:)`.
51+
52+
### Emitting fields
53+
54+
`Fields` builds the CBOR result incrementally: `uint`, `int`, `double`, `bool`, `string`
55+
(compile-time literal), `text` (UTF-8 from the payload), `bytes` (rendered as hex by the app), and
56+
`uuid`. Keys, labels and units are `StaticString` so they cost no allocation.
57+
58+
## Building
59+
60+
```sh
61+
cd Examples/BatteryLevel
62+
make # build only
63+
make install # build, wasm-opt -Oz, copy into the app, refresh the manifest sha256
64+
```
65+
66+
`make install` writes the module into
67+
`Sources/BluetoothExplorerPluginEngine/Plugins/` and updates the manifest hash, so the app picks it
68+
up on next launch. Expect roughly 90–110 KB per plugin after `wasm-opt -Oz`.
69+
70+
## The manifest
71+
72+
Every module needs a `<name>.bleplugin.json` sidecar telling the host what to route to it. The host
73+
never runs a plugin for data it did not declare:
74+
75+
```json
76+
{
77+
"manifestVersion": 1,
78+
"id": "org.example.plugin.battery-level",
79+
"name": "Battery Level",
80+
"version": "1.0.0",
81+
"abi": 1,
82+
"module": "battery-level.wasm",
83+
"sha256": "<filled in by make install>",
84+
"matches": { "characteristicUUIDs": ["2A19"] },
85+
"limits": { "maxMemoryPages": 16, "maxOutputBytes": 256 }
86+
}
87+
```
88+
89+
## What plugins can and cannot do
90+
91+
Plugins are pure decoders. The host runs them under a WebAssembly interpreter with:
92+
93+
- no filesystem, network, environment or clock access — the only host capability provided is
94+
randomness (`random_get`), which the Embedded Swift runtime requires;
95+
- a linear-memory cap from `limits.maxMemoryPages`;
96+
- a wall-clock deadline per call — a plugin that hangs is quarantined for the session;
97+
- output validated against a strict CBOR subset and `limits.maxOutputBytes`.
98+
99+
Your parse function must be a **pure function of its input**: the host may cache results for
100+
identical payloads and may recycle your instance between calls.

0 commit comments

Comments
 (0)