Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions examples/win-adapter-select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
const readline = require('readline');
const { withBindings } = require('../');

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

function ask (question) {
return new Promise(resolve => rl.question(question, resolve));
}

function printTable (rows, headers) {
const widths = headers.map((h, i) =>
Math.max(h.length, ...rows.map(r => String(r[i]).length))
);
const sep = widths.map(w => '-'.repeat(w + 2)).join('+');
const fmt = row => row.map((c, i) => ` ${String(c).padEnd(widths[i])} `).join('|');

console.log(fmt(headers));
console.log(sep);
rows.forEach(r => console.log(fmt(r)));
}

async function main () {
const noble = withBindings('win');
await noble.waitForPoweredOnAsync();

// --- Step 1: List adapters ---
const adapters = await noble.getAdaptersAsync();

if (adapters.length === 0) {
console.log('No Bluetooth adapters found.');
cleanup(noble);
return;
}

console.log('\nAvailable Bluetooth adapters:\n');
printTable(
adapters.map((a, i) => [i, a.name, a.address, a.default ? '*' : '']),
['#', 'Name', 'Address', 'Default']
);

// --- Step 2: Select adapter ---
let selected = 0;
if (adapters.length > 1) {
const input = await ask(`\nSelect adapter [0-${adapters.length - 1}]: `);
selected = parseInt(input, 10);
if (isNaN(selected) || selected < 0 || selected >= adapters.length) {
console.log('Invalid selection.');
cleanup(noble);
return;
}

console.log(`\nSwitching to "${adapters[selected].name}"...`);
await noble.setAdapterAsync(adapters[selected].id);
}

console.log(`\nUsing adapter: ${adapters[selected].name} (${adapters[selected].address})`);

// --- Step 3: Discover peripherals ---
console.log('Scanning for peripherals... (press Ctrl+C to stop)\n');

const discovered = new Map();

noble.on('discover', peripheral => {
const name = peripheral.advertisement.localName || '(unknown)';
const isNew = !discovered.has(peripheral.id);
discovered.set(peripheral.id, { name, peripheral });

if (isNew) {
console.log(
` [${discovered.size}] ${name.padEnd(30)} ` +
`${peripheral.address.padEnd(20)} RSSI: ${peripheral.rssi}`
);
}
});

await noble.startScanningAsync([], false);

// Wait for user to press enter to stop scanning
await ask('\nPress Enter to stop scanning...');
await noble.stopScanningAsync();

if (discovered.size === 0) {
console.log('No peripherals found.');
cleanup(noble);
return;
}

// --- Step 4: Pick a peripheral to explore ---
const input = await ask(`\nSelect peripheral to explore [1-${discovered.size}] (or Enter to exit): `);
if (!input) {
cleanup(noble);
return;
}

const idx = parseInt(input, 10);
const entries = [...discovered.values()];
if (isNaN(idx) || idx < 1 || idx > entries.length) {
console.log('Invalid selection.');
cleanup(noble);
return;
}

const { peripheral } = entries[idx - 1];
console.log(`\nConnecting to ${peripheral.advertisement.localName || peripheral.id}...`);

peripheral.on('disconnect', reason => {
console.log(`\nDisconnected (${reason})`);
});

try {
await peripheral.connectAsync();
console.log('Connected!\n');

const services = await peripheral.discoverServicesAsync([]);
for (const service of services) {
console.log(`Service: ${service.uuid}${service.name ? ` (${service.name})` : ''}`);

const chars = await service.discoverCharacteristicsAsync([]);
for (const c of chars) {
let info = ` ${c.uuid}`;
if (c.name) info += ` (${c.name})`;
info += ` [${c.properties.join(', ')}]`;

if (c.properties.includes('read')) {
try {
const data = await c.readAsync();
if (data) info += ` = ${data.toString('hex')}`;
} catch (_) {}

Check failure on line 131 in examples/win-adapter-select.js

View workflow job for this annotation

GitHub Actions / lint

Empty block statement

Check failure on line 131 in examples/win-adapter-select.js

View workflow job for this annotation

GitHub Actions / lint

Empty block statement
}
console.log(info);
}
}

await peripheral.disconnectAsync();
} catch (err) {
console.error('Error:', err.message);
}

cleanup(noble);
}

function cleanup (noble) {
rl.close();
noble.stop();
}

process.on('SIGINT', () => {
console.log('\nInterrupted.');
process.exit();

Check failure on line 152 in examples/win-adapter-select.js

View workflow job for this annotation

GitHub Actions / lint

Don't use process.exit(); throw an error instead

Check failure on line 152 in examples/win-adapter-select.js

View workflow job for this annotation

GitHub Actions / lint

Don't use process.exit(); throw an error instead
});

main().catch(err => {
console.error(err);
process.exit(1);

Check failure on line 157 in examples/win-adapter-select.js

View workflow job for this annotation

GitHub Actions / lint

Don't use process.exit(); throw an error instead

Check failure on line 157 in examples/win-adapter-select.js

View workflow job for this annotation

GitHub Actions / lint

Don't use process.exit(); throw an error instead
});
18 changes: 17 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ declare module '@stoprocent/noble' {
readonly address: string;

waitForPoweredOnAsync(timeout?: number): Promise<void>;
getAdaptersAsync(): Promise<Adapter[]>;
setAdapterAsync(deviceId: string): Promise<AdapterState>;
startScanningAsync(serviceUUIDs?: string[], allowDuplicates?: boolean): Promise<void>;
stopScanningAsync(): Promise<void>;
discoverAsync(): AsyncGenerator<Peripheral, void, unknown>;
Expand Down Expand Up @@ -247,8 +249,22 @@ declare module '@stoprocent/noble' {
userChannel?: boolean;
}

export interface Adapter {
/** WinRT device ID used for selecting this adapter */
id: string;
/** Bluetooth MAC address (e.g. "AA:BB:CC:DD:EE:FF") */
address: string;
/** Friendly adapter name */
name: string;
/** Whether this is the system default adapter */
default: boolean;
}

export interface MacBindingsOptions extends BaseBindingsOptions {}
export interface WinBindingsOptions extends BaseBindingsOptions {}
export interface WinBindingsOptions extends BaseBindingsOptions {
/** WinRT device ID of the Bluetooth adapter to use (from getAdaptersAsync()) */
deviceId?: string;
}

export type WithBindingsOptions = HciBindingsOptions | MacBindingsOptions | WinBindingsOptions;

Expand Down
8 changes: 8 additions & 0 deletions lib/common/include/Emit.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@
#include "Peripheral.h"
#include "ThreadSafeCallback.h"

struct AdapterInfo {
std::string id;
std::string address;
std::string name;
bool isDefault;
};

class Emit
{
public:
// clang-format off
void Wrap(const Napi::Value& receiver, const Napi::Function& callback);
void Adapters(const std::vector<AdapterInfo>& adapters);
void RadioState(const std::string& status);
void Address(const std::string& address);
void ScanState(bool start);
Expand Down
18 changes: 18 additions & 0 deletions lib/common/src/Emit.cc
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,24 @@ void Emit::Wrap(const Napi::Value& receiver, const Napi::Function& callback)
mCallback = std::make_shared<ThreadSafeCallback>(receiver, callback);
}

void Emit::Adapters(const std::vector<AdapterInfo>& adapters)
{
auto adaptersCopy = adapters;
mCallback->call([adaptersCopy](Napi::Env env, std::vector<napi_value>& args) {
auto arr = Napi::Array::New(env, adaptersCopy.size());
for (size_t i = 0; i < adaptersCopy.size(); i++) {
auto obj = Napi::Object::New(env);
obj.Set(_s("id"), _s(adaptersCopy[i].id));
obj.Set(_s("address"), _s(adaptersCopy[i].address));
obj.Set(_s("name"), _s(adaptersCopy[i].name));
obj.Set(_s("default"), _b(adaptersCopy[i].isDefault));
arr.Set(i, obj);
}
// emit('adapters', [...])
args = { _s("adapters"), arr };
});
}

void Emit::RadioState(const std::string& state)
{
mCallback->call([state](Napi::Env env, std::vector<napi_value>& args) {
Expand Down
32 changes: 32 additions & 0 deletions lib/noble.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class Noble extends NobleEventEmitter {
this._bindings.on('handleWrite', this._onHandleWrite.bind(this));
this._bindings.on('handleNotify', this._onHandleNotify.bind(this));
this._bindings.on('onMtu', this._onMtu.bind(this));
this._bindings.on('adapters', this._onAdapters.bind(this));
}

_createPeripheral (uuid, address, addressType, connectable, advertisement, rssi, scannable) {
Expand Down Expand Up @@ -184,6 +185,37 @@ class Noble extends NobleEventEmitter {
});
}

async getAdaptersAsync () {
if (this._initialized === false) {
this._initializeBindings();
}
if (typeof this._bindings.getAdapters !== 'function') {
throw new Error('getAdapters is not supported by the current binding');
}
return new Promise((resolve) => {
this.onceExclusive('adapters', resolve);
this._bindings.getAdapters();
});
}

async setAdapterAsync (deviceId) {
if (this._initialized === false) {
this._initializeBindings();
}
if (typeof this._bindings.setAdapter !== 'function') {
throw new Error('setAdapter is not supported by the current binding');
}
return new Promise((resolve) => {
this.once('stateChange', (state) => resolve(state));
this._bindings.setAdapter(deviceId);
});
}

_onAdapters (adapters) {
debug('adapters', adapters);
this.emit('adapters', adapters);
}

startScanning (serviceUuids, allowDuplicates, callback) {
const self = this;
const scan = (state) => {
Expand Down
19 changes: 18 additions & 1 deletion lib/win/src/ble_manager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,13 @@ const std::vector<ServiceDataTypeInfo> serviceDataTypes = {
}
};

BLEManager::BLEManager(const Napi::Value& receiver, const Napi::Function& callback)
BLEManager::BLEManager(const Napi::Value& receiver, const Napi::Function& callback, const std::string& deviceId)
{
mRadioState = AdapterState::Initial;
mEmit.Wrap(receiver, callback);
if (!deviceId.empty()) {
mWatcher.SetDeviceId(deviceId);
}
auto onRadio = std::bind(&BLEManager::OnRadio, this, std::placeholders::_1, std::placeholders::_2);
mWatcher.Start(onRadio);
mAdvertismentWatcher.ScanningMode(BluetoothLEScanningMode::Active);
Expand All @@ -143,6 +146,20 @@ BLEManager::BLEManager(const Napi::Value& receiver, const Napi::Function& callba
mStoppedRevoker = mAdvertismentWatcher.Stopped(winrt::auto_revoke, onStopped);
}

void BLEManager::GetAdapters()
{
mWatcher.EnumerateAdapters([this](const std::vector<AdapterInfo>& adapters) {
mEmit.Adapters(adapters);
});
}

void BLEManager::SetAdapter(const std::string& deviceId)
{
mRadioState = AdapterState::Initial;
mWatcher.SetDeviceId(deviceId);
mWatcher.OnRadioChanged();
}

void BLEManager::OnRadio(Radio& radio, const AdapterCapabilities& capabilities)
{
auto state = AdapterState::Unsupported;
Expand Down
4 changes: 3 additions & 1 deletion lib/win/src/ble_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ using winrt::Windows::Foundation::IInspectable;
class BLEManager {
public:
// clang-format off
BLEManager(const Napi::Value& receiver, const Napi::Function& callback);
BLEManager(const Napi::Value& receiver, const Napi::Function& callback, const std::string& deviceId = "");
void GetAdapters();
void SetAdapter(const std::string& deviceId);
void Scan(const std::vector<winrt::guid>& serviceUUIDs, bool allowDuplicates);
void StopScan();
bool Connect(const std::string& uuid);
Expand Down
29 changes: 28 additions & 1 deletion lib/win/src/noble_winrt.cc
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,37 @@

NobleWinrt::NobleWinrt(const Napi::CallbackInfo& info) : ObjectWrap(info)
{
if (info.Length() > 0 && info[0].IsObject()) {
auto options = info[0].As<Napi::Object>();
if (options.Has("deviceId") && options.Get("deviceId").IsString()) {
mDeviceId = options.Get("deviceId").As<Napi::String>().Utf8Value();
}
}
}

Napi::Value NobleWinrt::Start(const Napi::CallbackInfo& info)
{
Napi::Function emit = info.This().As<Napi::Object>().Get("emit").As<Napi::Function>();
manager = new BLEManager(info.This(), emit);
manager = new BLEManager(info.This(), emit, mDeviceId);
return info.Env().Undefined();
}

// getAdapters()
Napi::Value NobleWinrt::GetAdapters(const Napi::CallbackInfo& info)
{
CHECK_MANAGER()
manager->GetAdapters();
return info.Env().Undefined();
}

// setAdapter(deviceId)
Napi::Value NobleWinrt::SetAdapter(const Napi::CallbackInfo& info)
{
CHECK_MANAGER()
ARG1(String)
auto deviceId = info[0].As<Napi::String>().Utf8Value();
mDeviceId = deviceId;
manager->SetAdapter(deviceId);
return info.Env().Undefined();
}

Expand Down Expand Up @@ -304,6 +329,8 @@ Napi::Object NobleWinrt::Init(Napi::Env env, Napi::Object exports) {
Napi::Function func = DefineClass(env, "NobleWinrt", {
NobleWinrt::InstanceMethod("start", &NobleWinrt::Start),
NobleWinrt::InstanceMethod("stop", &NobleWinrt::Stop),
NobleWinrt::InstanceMethod("getAdapters", &NobleWinrt::GetAdapters),
NobleWinrt::InstanceMethod("setAdapter", &NobleWinrt::SetAdapter),
NobleWinrt::InstanceMethod("startScanning", &NobleWinrt::Scan),
NobleWinrt::InstanceMethod("stopScanning", &NobleWinrt::StopScan),
NobleWinrt::InstanceMethod("connect", &NobleWinrt::Connect),
Expand Down
3 changes: 3 additions & 0 deletions lib/win/src/noble_winrt.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class NobleWinrt : public Napi::ObjectWrap<NobleWinrt>
NobleWinrt(const Napi::CallbackInfo&);
Napi::Value Start(const Napi::CallbackInfo&);
Napi::Value Stop(const Napi::CallbackInfo&);
Napi::Value GetAdapters(const Napi::CallbackInfo&);
Napi::Value SetAdapter(const Napi::CallbackInfo&);
Napi::Value Scan(const Napi::CallbackInfo&);
Napi::Value StopScan(const Napi::CallbackInfo&);
Napi::Value Connect(const Napi::CallbackInfo&);
Expand All @@ -33,4 +35,5 @@ class NobleWinrt : public Napi::ObjectWrap<NobleWinrt>

private:
BLEManager* manager;
std::string mDeviceId;
};
Loading
Loading