Skip to content
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ Emitted when the Bluetooth adapter state changes. The listener receives the new

#### `event: 'discover'`

Emitted when a peripheral is discovered during scanning. The listener receives a `peripheral` object with `handle`, `id`, `name`, and `rssi` properties.
Emitted when a peripheral is discovered during scanning. The listener receives a `peripheral` object.

The `peripheral` object has `handle`, `id`, `name`, `rssi`, and `serviceData` properties. `rssi` is the signal strength reported with the most recent advertisement packet. `serviceData` is an object mapping service UUIDs to `Uint8Array` data, or `null` if no service data was advertised in this packet.

The same `peripheral` reference is reused across discover events for a given `id`; its `rssi` and `serviceData` are updated in place to reflect the latest packet.

#### `event: 'connect'`

Expand Down Expand Up @@ -130,6 +134,10 @@ The unique identifier of the peripheral.

The advertised name of the peripheral, or `null` if unavailable.

#### `peripheral.serviceData`

A snapshot of the `serviceData` from the most recent advertisement seen for this peripheral before connect or `null`. Service data is only in advertisement packets, so this value never updates after connect.

#### `peripheral.discoverServices()`

Discover services offered by the peripheral. Results are emitted via the `'servicesDiscover'` event.
Expand Down
68 changes: 66 additions & 2 deletions binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,17 @@ typedef struct {
int32_t state;
} bare_bluetooth_android_central_state_change_t;

typedef struct {
std::string uuid;
std::vector<unsigned char> data;
} bare_bluetooth_android_service_data_entry_t;

typedef struct {
std::string address;
std::string name;
int32_t rssi;
bare_bluetooth_android_device_handle_t *device;
std::vector<bare_bluetooth_android_service_data_entry_t> service_data;
} bare_bluetooth_android_central_discover_t;

typedef struct {
Expand Down Expand Up @@ -826,7 +832,7 @@ bare_bluetooth_android_central__on_discover(js_env_t *env, js_value_t *function,
err = js_get_reference_value(env, central->ctx, &receiver);
assert(err == 0);

js_value_t *argv[4];
js_value_t *argv[5];

js_external_t<bare_bluetooth_android_device_handle_t> ext;
err = js_create_external<bare_bluetooth_android__on_release<bare_bluetooth_android_device_handle_t>>(env, event->device, ext);
Expand All @@ -845,9 +851,32 @@ bare_bluetooth_android_central__on_discover(js_env_t *env, js_value_t *function,
err = js_create_int32(env, event->rssi, &argv[3]);
assert(err == 0);

if (event->service_data.empty()) {
err = js_get_null(env, &argv[4]);
assert(err == 0);
} else {
err = js_create_object(env, &argv[4]);
assert(err == 0);

for (auto &entry : event->service_data) {
js_value_t *arraybuffer;
void *buf;
err = js_create_arraybuffer(env, entry.data.size(), &buf, &arraybuffer);
assert(err == 0);
if (entry.data.size() > 0) memcpy(buf, entry.data.data(), entry.data.size());

js_value_t *u8;
err = js_create_typedarray(env, js_uint8array, entry.data.size(), arraybuffer, 0, &u8);
assert(err == 0);

err = js_set_named_property(env, argv[4], entry.uuid.c_str(), u8);
assert(err == 0);
}
}

delete event;

js_call_function(env, receiver, function, 4, argv, NULL);
js_call_function(env, receiver, function, 5, argv, NULL);

err = js_close_handle_scope(env, scope);
assert(err == 0);
Expand Down Expand Up @@ -1288,6 +1317,39 @@ bare_bluetooth_android_create_uuid(js_env_t *env, js_callback_info_t *info) {
return static_cast<js_value_t *>(handle);
}

static void
bare_bluetooth_android_extract_service_data(
JNIEnv *env,
java_object_t<"android/bluetooth/le/ScanResult"> scan_result,
std::vector<bare_bluetooth_android_service_data_entry_t> &out
) {
auto scan_record = scan_result.get_class().get_method<java_object_t<"android/bluetooth/le/ScanRecord">()>("getScanRecord")(scan_result);
if (static_cast<jobject>(scan_record) == nullptr) return;

auto service_data_map = scan_record.get_class().get_method<java_object_t<"java/util/Map">()>("getServiceData")(scan_record);
if (static_cast<jobject>(service_data_map) == nullptr) return;

auto entry_set = service_data_map.get_class().get_method<java_object_t<"java/util/Set">()>("entrySet")(service_data_map);
auto iterator = entry_set.get_class().get_method<java_object_t<"java/util/Iterator">()>("iterator")(entry_set);

auto has_next_method = iterator.get_class().get_method<bool()>("hasNext");
auto next_method = iterator.get_class().get_method<java_object_t<"java/util/Map$Entry">()>("next");

while (has_next_method(iterator)) {
auto entry = next_method(iterator);

auto key_obj = entry.get_class().get_method<java_object_t<"java/lang/Object">()>("getKey")(entry);
auto value_obj = entry.get_class().get_method<java_object_t<"java/lang/Object">()>("getValue")(entry);

auto parcel_uuid = java_object_t<"android/os/ParcelUuid">(env, static_cast<jobject>(key_obj));
auto uuid_str = bare_bluetooth_android_get_uuid_string(env, parcel_uuid);

auto byte_array = java_array_t<unsigned char>(env, static_cast<jobject>(value_obj));

out.push_back({std::move(uuid_str), byte_array.slice()});
}
}
Comment thread
sethvincent marked this conversation as resolved.
Outdated

static void
bare_bluetooth_android_on_scan_result(java_env_t env, java_object_t<"to/holepunch/bare/bluetooth/ScanCallback"> self, long native_ptr, int callback_type, java_object_t<"android/bluetooth/le/ScanResult"> scan_result) {
auto *central = reinterpret_cast<bare_bluetooth_android_central_t *>(native_ptr);
Expand All @@ -1310,6 +1372,8 @@ bare_bluetooth_android_on_scan_result(java_env_t env, java_object_t<"to/holepunc
event->name = {};
}

bare_bluetooth_android_extract_service_data(env, scan_result, event->service_data);

js_call_threadsafe_function(central->tsfn_discover, event, js_threadsafe_function_nonblocking);
}

Expand Down
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export {
ReadRequest,
WriteRequest
} from './lib/server'
export { default as Central } from './lib/central'
export { default as Central, DiscoveredPeripheral, Advertisement } from './lib/central'
export { default as Peripheral, PeripheralOptions } from './lib/peripheral'
12 changes: 10 additions & 2 deletions lib/central.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ import Peripheral from './peripheral'

export type BluetoothState = 'off' | 'turningOn' | 'on' | 'turningOff'

export interface DiscoveredPeripheral {
handle: ArrayBuffer
Comment thread
sethvincent marked this conversation as resolved.
Outdated
id: string
name: string | null
rssi: number
serviceData: { [uuid: string]: Uint8Array } | null
}

export interface CentralEventMap extends EventMap {
stateChange: [state: BluetoothState]
discover: [peripheral: Peripheral]
discover: [peripheral: DiscoveredPeripheral]
connect: [peripheral: Peripheral, error?: string]
disconnect: [peripheral: Peripheral | null, error?: string]
connectFail: [id: string, error: string]
Expand All @@ -19,7 +27,7 @@ export default class Central extends EventEmitter<CentralEventMap> {

startScan(serviceUUIDs?: string[]): void
stopScan(): void
connect(peripheral: Peripheral): void
connect(peripheral: DiscoveredPeripheral): void
disconnect(peripheral: Peripheral): void
destroy(): void

Expand Down
12 changes: 7 additions & 5 deletions lib/central.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,26 +71,28 @@ module.exports = exports = class Central extends EventEmitter {
this.emit('stateChange', this._state)
}

_ondiscover(handle, id, name, rssi) {
_ondiscover(handle, id, name, rssi, serviceData) {
let peripheral = this._peripherals.get(id)

if (peripheral) {
peripheral.handle = handle
peripheral.rssi = rssi
if (name) peripheral.name = name
peripheral.rssi = rssi
peripheral.serviceData = serviceData
} else {
peripheral = { handle, id, name, rssi }
peripheral = { handle, id, name, rssi, serviceData }
this._peripherals.set(id, peripheral)
}

this.emit('discover', peripheral)
}

_onconnect(handle, id) {
const discovered = this._peripherals.get(id) || null
const discovered = this._peripherals.get(id)
const peripheral = new Peripheral(handle, {
id,
name: discovered ? discovered.name : null,
name: discovered.name,
serviceData: discovered.serviceData,
connectHandle: handle
})
this._connected.set(id, peripheral)
Expand Down
2 changes: 2 additions & 0 deletions lib/peripheral.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface PeripheralOptions {
connectHandle?: ArrayBuffer
id?: string
name?: string
serviceData?: { [uuid: string]: Uint8Array } | null
}

export interface PeripheralEventMap extends EventMap {
Expand All @@ -29,6 +30,7 @@ export default class Peripheral extends EventEmitter<PeripheralEventMap> {

readonly id: string
readonly name: string | null
readonly serviceData: { [uuid: string]: Uint8Array } | null

discoverServices(): void
discoverCharacteristics(service: string): void
Expand Down
8 changes: 7 additions & 1 deletion lib/peripheral.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = exports = class Peripheral extends EventEmitter {
this._connectHandle = opts.connectHandle || null
this._id = opts.id || null
this._name = opts.name === undefined ? null : opts.name
this._serviceData = opts.serviceData === undefined ? null : opts.serviceData
this._destroyed = false
this._services = new Map()
this._characteristics = new Map()
Expand Down Expand Up @@ -42,6 +43,10 @@ module.exports = exports = class Peripheral extends EventEmitter {
return this._name
}

get serviceData() {
return this._serviceData
}

discoverServices() {
return binding.peripheralDiscoverServices(this._handle)
}
Expand Down Expand Up @@ -86,7 +91,8 @@ module.exports = exports = class Peripheral extends EventEmitter {
return {
__proto__: { constructor: Peripheral },
id: this.id,
name: this.name
name: this.name,
serviceData: this.serviceData
}
}

Expand Down
1 change: 1 addition & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ require('./test/central')
require('./test/l2cap')
require('./test/peripheral')
require('./test/server')
require('./test/service-data')
8 changes: 6 additions & 2 deletions test/central.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,13 @@ test('scan discovers peripherals with expected shape', { skip: isCI }, async (t)
t.ok(peripheral.handle, 'peripheral has handle')
t.ok(typeof peripheral.id === 'string', 'peripheral has string id')
t.ok(peripheral.id.length > 0, 'peripheral id is non-empty')
t.ok(typeof peripheral.rssi === 'number', 'peripheral has numeric rssi')
t.ok(peripheral.rssi < 0, 'rssi is negative')
t.ok(peripheral.name === null || typeof peripheral.name === 'string', 'name is string or null')
t.ok(typeof peripheral.rssi === 'number', 'peripheral has numeric rssi')
t.ok(peripheral.rssi < 0, 'peripheral rssi is negative')
t.ok(
peripheral.serviceData === null || typeof peripheral.serviceData === 'object',
'peripheral serviceData is object or null'
)
})

test('scan deduplicates peripherals by id', { skip: isCI }, async (t) => {
Expand Down
Loading