react-native-oneiot-ble
v0.4.4
Published
React Native library for OneIoT BLE devices (H7, L02S, S02R beacons and MKGW4/MKGW8 gateways). Scanning, sensor streams, GATT configuration, provisioning, and gateway MQTT client.
Maintainers
Readme
react-native-oneiot-ble
React Native library for OneIoT BLE devices — scanning, sensor streams, GATT configuration, fleet provisioning, and gateway management (MQTT).
Supported devices
| Prefix | SKU | Product Name | Category |
|--------|-------|---------------------------|---------------------|
| OHB | H7 | OneIoT Helmet Beacon | Beacon |
| OTH | L02S | OneIoT TH Sensor | Beacon |
| ORS | S02R | OneIoT Range Sensor | Beacon |
| OMG | MKGW4 | OneIoT Mobile Gateway | Gateway (cellular) |
| OWG | MKGW8 | OneIoT Warehouse Gateway | Gateway (Wi-Fi/PoE) |
All devices advertise a BLE local name of the form <PREFIX><12-hex-MAC>, e.g. OTHAABBCCDDEEFF. This is the OneIoT Device UUID used everywhere in the API.
Installation
npm install react-native-oneiot-ble react-native-ble-plx sp-react-native-mqtt
# or
yarn add react-native-oneiot-ble react-native-ble-plx sp-react-native-mqtt
cd ios && pod installsp-react-native-mqtt is an optional peer dependency — only required if you use the GatewayClient for MQTT communication.
iOS setup
Add to ios/YourApp/Info.plist:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Uses Bluetooth to discover and connect to OneIoT sensors.</string>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
</array>Android setup
Add to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />Request runtime permissions with react-native-permissions or PermissionsAndroid before scanning.
Quick start
Scan for OneIoT devices
import { useMokoScan } from 'react-native-oneiot-ble';
export function ScanScreen() {
const { devices, isScanning, start, stop } = useMokoScan();
useEffect(() => {
start();
return stop;
}, [start, stop]);
return (
<FlatList
data={devices}
keyExtractor={(d) => d.deviceUuid}
renderItem={({ item }) => (
<View>
<Text>{item.productName} — {item.macFormatted}</Text>
<Text>RSSI {item.rssi} dBm</Text>
</View>
)}
/>
);
}Live temperature stream
import { useMokoTemperature } from 'react-native-oneiot-ble';
function TempReading({ deviceUuid }: { deviceUuid: string }) {
const t = useMokoTemperature(deviceUuid);
return <Text>{t == null ? '—' : `${t.toFixed(1)} °C`}</Text>;
}Threshold alarm
useMokoAlarm(
'OTHAABBCCDDEEFF',
{ type: 'temperatureAbove', threshold: 30 },
(t) => console.warn(`Overheat! ${t}°C`),
);Gateway MQTT client
import { GatewayClient } from 'react-native-oneiot-ble';
const client = await GatewayClient.connect({
broker: {
host: 'mqtt.example.com',
port: 8883,
clientId: 'my-app',
username: 'user',
password: 'pass',
tls: true,
},
});
await client.subscribeGateway('GW-MAC');
await client.connectBeacon('GW-MAC', 'AABBCCDDEEFF', 'Moko4321').result;
const battery = await client.readBeaconBattery('GW-MAC', 'AABBCCDDEEFF');Features
1. Device Classification & Identity
Every OneIoT device broadcasts a BLE local name of the form <PREFIX><12-hex-MAC> (e.g. OTHAABBCCDDEEFF). The library treats this "Device UUID" as the primary key across every API.
Provides:
parseDeviceUuid(name)— takes any BLE local name and returns{ valid, sku, prefix, mac, macFormatted, productName }. Handles lowercase input, trims whitespace, validates hex format, returnssku: 'UNKNOWN'for non-OneIoT devices.buildDeviceUuid(sku, mac)— constructs a compliant UUID from a SKU and MAC in any format (AA:BB:CC:DD:EE:FF,aabbccddeeff,aa-bb-cc-dd-ee-ff).isValidDeviceUuid(name)— boolean check for compliance.skuMetadata(sku)— static per-SKU capability inventory: has motion? has T&H? has flash? has ToF? has buzzer? has RFID? Plus category (beacon/gateway).prefixForSku(sku)— reverse lookup (L02S→OTH).formatMac(mac)/normalizeMac(mac)— canonical MAC formatting.
Why it matters: iOS hides the hardware MAC and rotates its CBPeripheral.identifier between app installs. Because the Device UUID embeds the MAC in the advertised name, you get a stable, cross-platform identifier that survives reinstalls. useMokoDevice('OTHAABBCCDDEEFF') works identically on iOS and Android.
2. BLE Scanning
Wraps react-native-ble-plx with OneIoT-specific filtering, classification, and typed output. Every advertisement is auto-parsed and mapped to a MokoDevice object.
Provides:
BleScanner.scan(options, listener, onError)— continuous scan, returns an unsubscribe function. Multiple listeners are supported (only stops physical scan when all unsubscribe).BleScanner.scanBySku(sku, listener)— filter one SKU at the listener layer.BleScanner.scanOnce({ timeoutMs })→Promise<MokoDevice[]>— scans for N ms, resolves with de-duplicated device list (latest observation per device).BleScanner.stopScan()/isScanning().
Scan options:
serviceUuids— override the default MOKO service UUID filter (0xFEAAEddystone,0xFEABMOKO custom,0xEA01BXP-S).minRssi— drop packets below a signal floor.allowDuplicates— iOS: mandatorytruefor beacon telemetry (default).scanMode— Android:lowPower(~5 s scan / 500 ms window),balanced(default),lowLatency(continuous, drains battery).timeoutMs— auto-stop after N ms.
Automatic per-packet enrichment: Device UUID parsed and classified, BXP-S sensor payload decoded (if present), RSSI + Tx power + estimated distance computed, first-seen/last-seen timestamps recorded, per-device advertisement count maintained, raw bytes preserved for debug.
Why it matters: Non-OneIoT devices are filtered out automatically. Consumers never handle raw bytes unless they want to.
3. Live Sensor Data (No Connection Needed)
BXP-S devices broadcast sensor readings in every advertisement (~10 Hz). No GATT connection required — just scan and read.
Sensor fields per SKU:
| Field | H7 | L02S | S02R | |---|---|---|---| | Temperature (°C) | — | ✅ | — | | Humidity (%RH) | — | ✅ | — | | Accelerometer XYZ (mg) | ✅ | ✅ | — | | Motion state (moving/static) | ✅ | ✅ | — | | Motion event count | ✅ | ✅ | — | | ToF distance (mm) | — | — | ⏳* | | Battery (voltage OR percent) | ✅ | ✅ | ✅ | | Equipped-sensor bitmap | ✅ | ✅ | ✅ | | Tag ID | ✅ | ✅ | ✅ |
ToF byte position not yet published by MOKO — decoder returns null until spec arrives.
Provides:
watchAll(uuid, cb)— every parsed sensor packet, discriminated union by SKU.watchTemperature(uuid, cb)— L02S temperature stream.watchHumidity(uuid, cb)— L02S humidity stream.watchMotion(uuid, cb)— H7/L02S accelerometer +isMovingflag.watchDistance(uuid, cb)— S02R ToF stream.watchBattery(uuid, cb)— battery reading (voltage or percent).getLatest(uuid)— snapshot without waiting for next packet.
Battery dual-encoding: MOKO packs battery into 2 bytes with a special rule: values > 100 = millivolts, ≤ 100 = percent. Library returns a discriminated union { mode: 'voltage', valueMv: 3276 } or { mode: 'percent', valuePct: 90 } so app code never has to check the raw number.
Capability-aware parsing: if a device's equipped-sensor bitmap says no temperature sensor, temperatureC returns null — not a garbage value from unused bytes.
4. Threshold Alarms
Edge-triggered event helpers for common alarm use cases. Fires once when a threshold is crossed, then re-arms only when the value returns to the safe zone.
Provides:
onTemperatureAbove(uuid, °C, cb)— cold-chain over-temperature alarm.onTemperatureBelow(uuid, °C, cb)— freezer thaw alarm.onHumidityAbove(uuid, %, cb)— condensation alarm.onMotionDetected(uuid, cb)— fires on transition static → moving.onDistanceBelow(uuid, mm, cb)— S02R proximity alarm.onProximityEnter(uuid, rssi, cb)— device came within a signal-strength boundary.
Why edge-triggered: BXP-S broadcasts ~10 Hz. If temperature stays at 32 °C for 2 minutes, a level-triggered alarm would fire 1200 times. These helpers fire exactly once until the value drops below the threshold.
5. Signal Strength & Proximity
Turn noisy raw RSSI into stable distance/proximity data suitable for UI.
Provides:
estimateDistance(rssi, txPower, pathLossN?)— log-distance path loss model.ndefaults to 2.5 (typical indoor); set 2 for free space, 4 for heavy walls.proximityZoneFromRssi(rssi)/proximityZoneFromDistance(m)— classify intoimmediate(< 0.5 m),near(< 3 m),far(< 30 m),out.calibrateTxPower(samplesAt1m)— median-based Tx-power calibration helper.
Smoothing filters (all implement push(rssi) → smoothed):
KalmanRssiSmoother— recommended default; drastically reduces jitter with configurable process/measurement noise.ExponentialMovingAverageSmoother— cheaper, one α parameter.MovingAverageSmoother— simplest, configurable window.createSmoother(method)— factory returning any of the above.
Why it matters: Raw BLE RSSI swings 10–15 dBm packet-to-packet even for a stationary device. Direct UI binding produces flickering distance readouts.
6. Session Device Tracker
Long-lived in-memory store that merges advertisement events into a live device list. Backs the React hooks.
Provides:
start()/stop()— attach/detach from scanner. Idempotent.list()— all devices seen this session.getByUuid(uuid)/getBySku(sku)— lookups.count()/countBySku()— fleet counts.onSeen(cb)— fires when a new device appears.onLost(cb)— fires when a device goes stale (seeforgetStaleAfter).onUpdate(cb)— fires on every packet.forgetStaleAfter(ms)— auto-evict devices not seen for N ms. Passnullto disable.ingest(device)— manually feed a device from an external source (e.g. MQTT gateway uplink).reset()— clear session state.
Merging behaviour: Each new packet updates rssi, lastSeenAt, adCount, sensor data (if present), and raw bytes. firstSeenAt is preserved.
7. Bluetooth Adapter & Permissions
Adapter-state helpers and permission-status queries. Explicitly does not request permissions — apps use react-native-permissions or PermissionsAndroid for that.
Provides:
Bluetooth.getState()→poweredOn/poweredOff/unauthorized/resetting/unsupported/unknown.Bluetooth.observeState(cb)— reactive stream, fires immediately with current state.Bluetooth.getPermissionStatus()— best-effort permission inspection.Bluetooth.canScan()— boolean helper.
Why status-only: requesting Bluetooth permissions is UX-sensitive (Android requires runtime prompts with rationale strings, iOS shows a system dialog on first CBCentralManager init). Different apps handle this differently — the library stays out of the way.
8. GATT Device Configuration (API locked, wire protocol pending MOKO)
Open a GATT connection to a beacon and reconfigure it. All methods are typed and callable today, but throw MokoError('gatt_error', '…pending MOKO GATT spec') until MOKO ships the BXP-S wire protocol. Public API will not change when implementations land.
Connection lifecycle:
MokoConnect.connect(uuid, { password, timeoutMs })→DeviceHandle. Default passwordMoko4321.MokoConnect.disconnect(uuid).MokoConnect.isConnected(uuid).MokoConnect.onConnectionChange(uuid, cb)— subscribe to connect/disconnect events.
DeviceHandle read operations:
readInfo()→{ model, manufacturer, hwVersion, fwVersion, serialNumber }.readBattery()→ dual-mode battery.
Identity & security:
rename(newDeviceUuid)— write new OneIoT-scheme name.setPassword(newPassword)— max 16 chars.
Radio config:
setTxPower(dbm)— −40 to +8 dBm.setAdvInterval(ms)— 100–10000 ms.setConnectable(bool).
Slot configuration (3 slots per device):
getSlots()→SlotConfig[].setSlot(index, config)— configure one slot with frame type (Eddystone-UID/URL/TLM, iBeacon, SensorInfo, None), ADV interval, ADV duration, standby duration, Tx power.setTrigger(config)— with before/after states and lock duration.
Actuators:
blinkLED({ count, color?, intervalMs? }).buzz({ durationMs })— H7 only.
Lifecycle:
factoryReset(),reboot().
9. Fleet Provisioning
Turn a batch of factory-fresh devices into a labelled fleet — connect, read MAC, write OneIoT-scheme name, verify.
Provides:
Provisioner.isProvisioned(device)— does the current name match the scheme?Provisioner.needsProvisioning()— list devices in range missing correct names.Provisioner.assignName(platformId, sku, password?)— connect, look up MAC, writeOXX+MACname. Returns the new Device UUID.Provisioner.batchProvision(devices, options)— iterate over a list withonProgress(done, total)andonDeviceComplete({ deviceUuid, error })callbacks.Provisioner.verifyFleet(expectedUuids)→{ present, missing, extra }— health check before deployment.Provisioner.exportManifest()→ JSON manifest with SKU, MAC, provisioned-at timestamp for every device.
Why it matters: Assigning distinct names is the ONE thing that makes the entire OneIoT device-classification story deterministic. Without it, all H7 / L02S / S02R units look identical on scan. This module makes intake a 30-second-per-device workflow.
10. Gateway BLE Provisioning (API locked, protocol pending MOKO)
First-time onboarding of MKGW4 (cellular) and MKGW8 (Wi-Fi/PoE) gateways over BLE — push network + MQTT credentials so the gateway comes online.
Provides:
Gateway.provisionMKGW4(platformId, { apn, mqtt })— cellular: APN name/user/pass + MQTT broker config.Gateway.provisionMKGW8(platformId, { wifi?, ethernet?, mqtt })— Wi-Fi (SSID + password + security type) OR Ethernet (DHCP or static IP with netmask/gateway/DNS) + MQTT config.Gateway.testConnection(platformId)— dry-run network + MQTT reachability check.Gateway.factoryReset(platformId).
Note: MKGW8 also supports Wi-Fi AP provisioning at http://192.168.22.1 (AP SSID MKGW8LD-XXXX, password Moko4321). The BLE path is an alternative for zero-touch onboarding once MOKO ships the spec.
11. Gateway MQTT Client (Cloud Operations)
Full-featured JSON-over-MQTT client implementing the MOKO Gateway Remote Management Commands V2.3 protocol. Talks to gateways over the cloud to remotely manage the beacons they've connected.
Provides:
GatewayClient.connect({ broker, requestTimeoutMs, topics })— establish MQTT session..disconnect()/.isConnected()..subscribeGateway(gwMac)— subscribe to a gateway's up/down topics.
Fleet discovery:
.getGatewayInfo(gwMac)→ status, uptime, connected-beacon count, MQTT connection state..onGatewayOnline(cb)/.onGatewayOffline(cb)— LWT-based presence.
Beacon operations via gateway:
.listConnectedBeacons(gwMac)— list of(mac, sku)currently connected..connectBeacon(gwMac, beaconMac, password)— returns{ ack: Promise, result: Promise }two-stage promise (ack from gateway, result from BLE round-trip)..disconnectBeacon(gwMac, beaconMac)..readBeaconInfo(gwMac, beaconMac)→DeviceInfo..readBeaconBattery(gwMac, beaconMac)→ dual-mode battery..setBeaconSlot(gwMac, beaconMac, slotIndex, config)..setBeaconTrigger(gwMac, beaconMac, config)..blinkBeaconLED(gwMac, beaconMac, config).
Real-time sensor streams via MQTT:
.enableRealtimeTH(gwMac, beaconMac, cb)— L02S temperature + humidity notify..enableRealtimeMotion(gwMac, beaconMac, cb)— H7/L02S accelerometer notify..enableRealtimeDistance(gwMac, beaconMac, cb)— S02R ToF notify.
Uplink stream:
.onBeaconUplink(cb)— all beacon-adv frames forwarded by all gateways.
Protocol machinery (all handled internally):
- msg_id encoding:
1xxx= write,2xxx= read,3xxx= notify. Auto-assigned from command tables (BXP-S occupies 500–533). - Two-stage promises:
connectBeaconreturns{ ack, result }.ackresolves fast when the gateway echoes the command;resultresolves when the BLE round-trip completes (seconds later). - Per-(gwMac, bleMac) serialization: MOKO protocol has no client request ID. Concurrent commands to the same beacon would collide, so operations queue automatically.
- Timeout handling: default 30 s per command, configurable, surfaces
MokoError('timeout'). - Topic conventions: typical MOKO defaults (
moko/gw/{mac}/up,moko/gw/{mac}/down) with per-consumer overrides.
Optional MQTT dependency: sp-react-native-mqtt is loaded lazily via require() — apps that only scan don't pay the bundle cost. A Transport interface is exported so consumers can plug in a custom broker (Paho, mqtt.js, etc.).
12. React Hooks (15 total)
All hooks handle mount/unmount lifecycle, clean up subscriptions on unmount, and are safe to use with React 18 StrictMode.
Scanning & devices:
useMokoScan(options?)→{ devices, isScanning, error, start, stop, reset }.useMokoScanBySku(sku)→ same, filtered to one SKU.useMokoDevice(deviceUuid)→ single-device live state.
Sensor data:
useMokoAllSensors(uuid)→ latestSensorData(discriminated union).useMokoTemperature(uuid)→number | null.useMokoHumidity(uuid)→number | null.useMokoMotion(uuid)→{ x, y, z, isMoving } | null.useMokoDistance(uuid)→number | null.useMokoBattery(uuid)→Battery | null.
Signal:
useMokoRssi(uuid, { smooth?: 'kalman' | 'ema' | 'movingAvg' })→ live RSSI, optionally smoothed.useMokoDistanceEstimate(uuid)→ meters.useMokoProximity(uuid)→immediate/near/far/out.
Alarms & connection:
useMokoAlarm(uuid, { type, threshold }, cb)— declarative threshold alarms.useMokoConnection(uuid)→{ connected, connect, disconnect, error }.
Adapter:
useBluetoothState()→ current adapter state.
13. Pure Protocol Utilities (Node-Testable)
Zero React Native dependency — usable from unit tests, backend Node scripts, browser tools.
Provides:
parseAdvertisement(bytes)— full BLE AD-structure parser. Handles flags, local names (short & complete), Tx power, 16/32/128-bit service UUIDs, service data, manufacturer-specific data. Never throws — emitswarningsfor malformed input.parseBxpSSensorInfo(payload)— decode BXP-S0xEA01/0x80frame.toSensorData(decoded, sku)— convert decoder output to discriminated union.hexToBytes(hex)/bytesToHex(bytes)— accepts0xprefix, whitespace, upper/lowercase.uuid128ToUuid16(uuid)— extract 16-bit portion from full 128-bit UUID string.- All classifier helpers.
Constants exposed:
AD_TYPE.*— BLE AD-type identifiers (Flags, ServiceData16, ManufacturerSpecific, etc.).BXP_S_SERVICE_UUID_16=0xEA01.BXP_S_SENSOR_INFO_FRAME=0x80.
14. Developer Experience
Type safety:
- Full TypeScript strict mode (including
noUncheckedIndexedAccess). - Discriminated unions for
SensorData— TypeScript narrows based on.kind, so no casting needed. - Every public function fully typed with JSDoc.
Error handling:
MokoErrorclass with a fixedcodeenum (bluetooth_off,unauthorized,scan_failed,connect_failed,timeout,gatt_error,mqtt_disconnected, etc.). Consumers switch oncode, never parse messages.
Logging:
- Structured logger with 6 levels (
silent,error,warn,info,debug,trace). - Runtime-adjustable via
logger.setLevel(). - Custom sink support via
logger.setSink()— pipe to a file, remote logger, analytics.
Bundle optimization:
sideEffects: falseinpackage.json→ aggressive tree-shaking.- Optional peers:
sp-react-native-mqttmarked optional; apps that don't useGatewayClientwon't bundle it. - Lazy
require()for the MQTT module — no import-time cost.
Testability:
- Pure-TS core (classifier, parsers, signal) has zero RN dependency — testable in Node.
- 50 unit tests already passing (classifier, BXP-S parser with golden vectors from MOKO's own spec, AD parser, signal filters).
__resetForTesting()and__setManager()hooks on singleton services for test isolation.- Ships a fake
Transportinterface forGatewayClienttests.
Build:
react-native-builder-bobemits CommonJS, ES Modules, and TypeScript definitions.- Compatible with both the old bridge and the new architecture (TurboModule-ready).
- Autolinking configured (no manual linking).
15. Documentation
- README.md — full API reference with tables, iOS/Android setup, quick-start snippets.
- CHANGELOG.md — Keep-a-Changelog format, semver-aligned.
- JSDoc on every public function including parameter descriptions, return types, behavior notes.
- Golden-vector tests that reference exact table numbers from MOKO's official specs (e.g. "MK Sensor APP UM V1.2 §Table 5") — future spec updates validate against the same fixtures.
- Example app (
example/App.tsx) — working scan + sensor-display screen you can drop into a bare RN project.
Design notes
- Device UUID as primary key — the app-level identifier
OXX+MACis stable across iOS reinstalls (embeds hardware MAC), souseMokoDevice('OTHAABBCCDDEEFF')works identically on both platforms. - Multi-slot devices — BXP-S beacons may emit multiple slots (before/after trigger, Eddystone + iBeacon + Sensor Info). The tracker de-duplicates by
deviceUuidonly, not(uuid + name). - iOS background scanning — local names are stripped by iOS in background mode. Foreground apps get full classification; background apps lose SKU identification.
- Coded PHY (Long Range) — L02S supports LE Coded PHY (350 m). iOS CoreBluetooth does not expose Coded PHY scanning. For Long-Range coverage, use MKGW8 gateways.
Blocked pieces (waiting on MOKO)
These have locked APIs but stubbed implementations — will "just work" once MOKO ships the corresponding docs. Consumer code will not need to change.
| Feature | Blocking doc from MOKO | |----------------------------------------------------------------|-------------------------------------------| | GATT beacon configuration (rename, slots, triggers, LED, buzz) | BXP-S BLE Communication Protocol | | MKGW4 BLE provisioning (APN + MQTT push) | MKGW4 APP Integration / BLE Config | | MKGW8 BLE provisioning (Wi-Fi/Ethernet + MQTT push) | MKGW8 BLE Config Protocol | | S02R ToF distance decoding from advertisement | S02R spec update (byte offset) | | MKGW8 MQTT topic structure verification | MKGW8 MQTT Communication Protocol | | MKGW4 uplink JSON schema verification | MKGW4 Firmware / MQTT Communication doc |
Every locked-but-stubbed method throws a specific, actionable MokoError so consumers see clear errors (not silent failures) until wire protocols land.
Roadmap
- v0.1 (current) — Scanning, sensor streams, tracker, hooks, gateway MQTT client, provisioning + GATT API surface (stubbed pending MOKO wire protocol).
- v0.2 — Real GATT implementation for BXP-S device configuration (once MOKO ships the wire protocol document).
- v0.3 — MKGW4/MKGW8 BLE provisioning (once MOKO ships the gateway BLE config protocol document).
- v0.4 — DFU firmware updates, extended advertising (Coded PHY on Android).
Testing
npm test # pure-TS unit tests (classifier, parsers, signal)
npm run typecheck
npm run lint
npx bob build # emit lib/Native / BLE integration tests require a physical device; use the example/ app as a manual test harness. BLE does not work in the iOS Simulator or Android emulator.
License
MIT © OneIoT
