node-red-contrib-ble-scanner 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/reader.js ADDED
@@ -0,0 +1,143 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { DEFAULTS } from './constants.js';
3
+ import { bluezBackend } from './ble/bluez.js';
4
+ import { nobleBackend } from './ble/noble.js';
5
+ import { displayDevice, normalizeUuid } from './ble/utils.js';
6
+ export async function discoverDevices(options = {}) {
7
+ const logger = options.logger || (() => { });
8
+ const bluetooth = options.bluetooth || 'auto';
9
+ const backend = await selectBluetoothBackend(bluetooth, logger);
10
+ try {
11
+ return await backend.discover({
12
+ namePrefix: options.namePrefix ?? DEFAULTS.namePrefix,
13
+ deviceName: options.deviceName || null,
14
+ timeoutMs: options.timeoutMs || DEFAULTS.timeoutMs,
15
+ scanServiceUuid: options.scanServiceUuid ?? null,
16
+ matchServiceUuid: options.matchServiceUuid ?? null,
17
+ logger
18
+ });
19
+ }
20
+ catch (error) {
21
+ if (bluetooth === 'auto' && isBluetoothUnavailableError(error)) {
22
+ logger(`No usable Bluetooth backend found: ${error instanceof Error ? error.message : String(error)}.`);
23
+ return [];
24
+ }
25
+ throw error;
26
+ }
27
+ }
28
+ export async function readDevices(options = {}) {
29
+ const logger = options.logger || (() => { });
30
+ const devices = await discoverDevices({ ...options, logger });
31
+ const readings = [];
32
+ for (const device of devices) {
33
+ logger(`Reading raw BLE notifications from ${displayDevice(device)}.`);
34
+ readings.push(await readDevice(device, { ...options, logger }));
35
+ }
36
+ return readings;
37
+ }
38
+ export async function readDevice(device, options = {}) {
39
+ const reader = await connectDevice(device, options);
40
+ try {
41
+ return await reader.read(options);
42
+ }
43
+ finally {
44
+ await reader.disconnect().catch(() => { });
45
+ }
46
+ }
47
+ export async function connectReaders(options = {}) {
48
+ const logger = options.logger || (() => { });
49
+ const devices = await discoverDevices({ ...options, logger });
50
+ const readers = [];
51
+ try {
52
+ for (const device of devices) {
53
+ logger(`Connecting ${displayDevice(device)}.`);
54
+ readers.push(await connectDevice(device, { ...options, logger }));
55
+ }
56
+ return readers;
57
+ }
58
+ catch (error) {
59
+ await Promise.all(readers.map((reader) => reader.disconnect().catch(() => { })));
60
+ throw error;
61
+ }
62
+ }
63
+ export async function connectDevice(device, options = {}) {
64
+ const logger = options.logger || (() => { });
65
+ const backend = backendForDevice(device);
66
+ const notifyUuid = normalizeUuid(options.notifyUuid || DEFAULTS.notifyUuid);
67
+ const session = await backend.connect({
68
+ device,
69
+ timeoutMs: options.timeoutMs || DEFAULTS.timeoutMs,
70
+ connectTimeoutMs: options.connectTimeoutMs || DEFAULTS.connectTimeoutMs,
71
+ notifyUuid,
72
+ logger
73
+ });
74
+ let disconnected = false;
75
+ return {
76
+ device: session.device,
77
+ read: (readOptions) => readSession(session, { ...options, ...readOptions, notifyUuid, logger }),
78
+ disconnect: async () => {
79
+ if (disconnected)
80
+ return;
81
+ disconnected = true;
82
+ await session.disconnect();
83
+ }
84
+ };
85
+ }
86
+ export async function readSession(session, options = {}) {
87
+ const logger = options.logger || (() => { });
88
+ const listenMs = options.listenMs ?? DEFAULTS.listenMs;
89
+ const notifyUuid = normalizeUuid(options.notifyUuid || session.notify.uuid || DEFAULTS.notifyUuid);
90
+ const messages = [];
91
+ const onData = (data) => {
92
+ const raw = Buffer.from(data);
93
+ const message = {
94
+ timestamp: new Date().toISOString(),
95
+ uuid: session.notify.uuid,
96
+ length: raw.length,
97
+ data: raw,
98
+ hex: raw.toString('hex'),
99
+ base64: raw.toString('base64'),
100
+ bytes: [...raw]
101
+ };
102
+ logger(`BLE notification ${message.hex} (${message.length} byte${message.length === 1 ? '' : 's'}).`);
103
+ messages.push(message);
104
+ };
105
+ session.notify.onData(onData);
106
+ try {
107
+ logger(`Listening for ${listenMs}ms on characteristic ${notifyUuid}.`);
108
+ await delay(listenMs);
109
+ }
110
+ finally {
111
+ session.notify.removeDataListener(onData);
112
+ }
113
+ return {
114
+ device: session.device,
115
+ timestamp: new Date().toISOString(),
116
+ notifyUuid,
117
+ messages
118
+ };
119
+ }
120
+ export async function shutdownBluetooth() {
121
+ await Promise.all([nobleBackend.shutdown(), bluezBackend.shutdown()]);
122
+ }
123
+ async function selectBluetoothBackend(bluetooth, logger) {
124
+ if (bluetooth === 'bluez')
125
+ return bluezBackend;
126
+ if (bluetooth === 'noble')
127
+ return nobleBackend;
128
+ if (await bluezBackend.isAvailable(logger))
129
+ return bluezBackend;
130
+ return nobleBackend;
131
+ }
132
+ function backendForDevice(device) {
133
+ const backends = {
134
+ bluez: bluezBackend,
135
+ noble: nobleBackend
136
+ };
137
+ return backends[device.backend];
138
+ }
139
+ function isBluetoothUnavailableError(error) {
140
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
141
+ return message.includes('bluetooth adapter state is unsupported') || message.includes('no compatible bluetooth') || message.includes('not available on');
142
+ }
143
+ //# sourceMappingURL=reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reader.js","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,135 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('ble-reader-device', {
3
+ category: 'config',
4
+ defaults: {
5
+ name: { value: '' },
6
+ bluetooth: { value: 'auto' },
7
+ namePrefix: { value: '' },
8
+ serviceUuid: { value: '' },
9
+ notifyUuid: { value: 'fff1', required: true },
10
+ listenMs: { value: 5000, required: true, validate: RED.validators.number() },
11
+ targetMode: { value: 'all' },
12
+ deviceName: { value: '' }
13
+ },
14
+ label: function () {
15
+ return this.name || this.deviceName || 'BLE reader';
16
+ },
17
+ oneditprepare: function () {
18
+ var deviceSelect = $('#node-config-input-deviceNameSelect');
19
+ var deviceName = $('#node-config-input-deviceName');
20
+ var targetMode = $('#node-config-input-targetMode');
21
+ var searchButton = $('#node-config-input-search');
22
+ var searchStatus = $('#node-config-input-search-status');
23
+
24
+ function hasOption(value) {
25
+ var found = false;
26
+ deviceSelect.find('option').each(function () {
27
+ if ($(this).attr('value') === value) found = true;
28
+ });
29
+ return found;
30
+ }
31
+
32
+ function setSelected(mode, name) {
33
+ targetMode.val(mode);
34
+ deviceName.val(name || '');
35
+ deviceSelect.val(mode === 'all' ? '' : name);
36
+ }
37
+
38
+ function ensureCurrentOption() {
39
+ var currentName = deviceName.val();
40
+ if (currentName && !hasOption(currentName)) {
41
+ deviceSelect.append($('<option></option>').attr('value', currentName).text(currentName));
42
+ }
43
+ setSelected(targetMode.val() || (currentName ? 'name' : 'all'), currentName);
44
+ }
45
+
46
+ deviceSelect.on('change', function () {
47
+ var selected = deviceSelect.val();
48
+ setSelected(selected ? 'name' : 'all', selected);
49
+ });
50
+
51
+ searchButton.on('click', function () {
52
+ searchButton.prop('disabled', true);
53
+ searchStatus.text('searching...');
54
+ $.getJSON('ble-reader/devices', {
55
+ bluetooth: $('#node-config-input-bluetooth').val(),
56
+ namePrefix: $('#node-config-input-namePrefix').val(),
57
+ serviceUuid: $('#node-config-input-serviceUuid').val()
58
+ })
59
+ .done(function (devices) {
60
+ var previous = deviceName.val();
61
+ deviceSelect.empty();
62
+ deviceSelect.append($('<option></option>').attr('value', '').text('All discovered devices'));
63
+ devices.forEach(function (device) {
64
+ if (!device.name) return;
65
+ var details = device.address ? ' (' + device.address + ')' : '';
66
+ deviceSelect.append($('<option></option>').attr('value', device.name).text(device.name + details));
67
+ });
68
+ if (previous && hasOption(previous)) {
69
+ setSelected('name', previous);
70
+ } else {
71
+ setSelected('all', '');
72
+ }
73
+ searchStatus.text(devices.length + ' found');
74
+ })
75
+ .fail(function (xhr) {
76
+ var response = xhr.responseJSON || {};
77
+ searchStatus.text(response.error || 'search failed');
78
+ })
79
+ .always(function () {
80
+ searchButton.prop('disabled', false);
81
+ });
82
+ });
83
+
84
+ ensureCurrentOption();
85
+ }
86
+ });
87
+ </script>
88
+
89
+ <script type="text/html" data-template-name="ble-reader-device">
90
+ <div class="form-row">
91
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
92
+ <input type="text" id="node-config-input-name">
93
+ </div>
94
+ <div class="form-row">
95
+ <label for="node-config-input-bluetooth"><i class="fa fa-bluetooth"></i> Bluetooth</label>
96
+ <select id="node-config-input-bluetooth">
97
+ <option value="auto">auto</option>
98
+ <option value="bluez">bluez</option>
99
+ <option value="noble">noble</option>
100
+ </select>
101
+ </div>
102
+ <div class="form-row">
103
+ <label for="node-config-input-namePrefix"><i class="fa fa-filter"></i> Name prefix</label>
104
+ <input type="text" id="node-config-input-namePrefix" placeholder="optional">
105
+ </div>
106
+ <div class="form-row">
107
+ <label for="node-config-input-serviceUuid"><i class="fa fa-filter"></i> Service UUID</label>
108
+ <input type="text" id="node-config-input-serviceUuid" placeholder="optional">
109
+ </div>
110
+ <div class="form-row">
111
+ <label for="node-config-input-notifyUuid"><i class="fa fa-bell"></i> Notify UUID</label>
112
+ <input type="text" id="node-config-input-notifyUuid" placeholder="fff1">
113
+ </div>
114
+ <div class="form-row">
115
+ <label for="node-config-input-listenMs"><i class="fa fa-clock-o"></i> Listen ms</label>
116
+ <input type="number" id="node-config-input-listenMs" min="0" step="100">
117
+ </div>
118
+ <div class="form-row">
119
+ <label for="node-config-input-deviceNameSelect"><i class="fa fa-search"></i> Device</label>
120
+ <select id="node-config-input-deviceNameSelect" style="width:60%;">
121
+ <option value="">All discovered devices</option>
122
+ </select>
123
+ <button type="button" id="node-config-input-search" class="red-ui-button" style="margin-left:4px;">Search</button>
124
+ <input type="hidden" id="node-config-input-targetMode">
125
+ <input type="hidden" id="node-config-input-deviceName">
126
+ </div>
127
+ <div class="form-row">
128
+ <label></label>
129
+ <span id="node-config-input-search-status" style="color:#666;"></span>
130
+ </div>
131
+ </script>
132
+
133
+ <script type="text/html" data-help-name="ble-reader-device">
134
+ <p>Configures BLE discovery and the notification characteristic used by the raw reader.</p>
135
+ </script>
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ module.exports = function registerBleReaderDeviceNode(RED) {
4
+ const loadLibrary = async () => import('../dist/index.js');
5
+
6
+ function BleReaderDeviceNode(config) {
7
+ RED.nodes.createNode(this, config);
8
+ const node = this;
9
+
10
+ node.name = config.name;
11
+ node.bluetooth = config.bluetooth || 'auto';
12
+ node.namePrefix = config.namePrefix || '';
13
+ node.serviceUuid = config.serviceUuid || '';
14
+ node.notifyUuid = config.notifyUuid || 'fff1';
15
+ node.listenMs = Number(config.listenMs || 5000);
16
+ node.targetMode = config.targetMode || 'all';
17
+ node.deviceName = config.deviceName || '';
18
+ node.queue = Promise.resolve();
19
+
20
+ node.loadLibrary = loadLibrary;
21
+
22
+ node.enqueue = async (operation) => {
23
+ const run = node.queue.then(async () => {
24
+ const ble = await node.loadLibrary();
25
+ return operation(ble);
26
+ });
27
+ node.queue = run.catch(() => {});
28
+ return run;
29
+ };
30
+
31
+ node.on('close', (_removed, done) => {
32
+ node.loadLibrary()
33
+ .then((ble) => ble.shutdownBluetooth())
34
+ .catch(() => {})
35
+ .finally(done);
36
+ });
37
+ }
38
+
39
+ RED.nodes.registerType('ble-reader-device', BleReaderDeviceNode);
40
+
41
+ const permission = RED.auth?.needsPermission ? RED.auth.needsPermission('flows.read') : (_req, _res, next) => next();
42
+ RED.httpAdmin.get('/ble-reader/devices', permission, async (req, res) => {
43
+ try {
44
+ const ble = await loadLibrary();
45
+ const serviceUuid = typeof req.query.serviceUuid === 'string' && req.query.serviceUuid ? req.query.serviceUuid : null;
46
+ const devices = await ble.discoverDevices({
47
+ bluetooth: normalizeBluetooth(req.query.bluetooth),
48
+ namePrefix: typeof req.query.namePrefix === 'string' ? req.query.namePrefix : '',
49
+ scanServiceUuid: serviceUuid,
50
+ matchServiceUuid: serviceUuid
51
+ });
52
+ res.json(devices);
53
+ } catch (error) {
54
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
55
+ }
56
+ });
57
+ };
58
+
59
+ function normalizeBluetooth(value) {
60
+ return value === 'bluez' || value === 'noble' || value === 'auto' ? value : 'auto';
61
+ }
@@ -0,0 +1,38 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('ble-reader-read', {
3
+ category: 'BLE',
4
+ color: '#d8eef8',
5
+ defaults: {
6
+ name: { value: '' },
7
+ device: { type: 'ble-reader-device', required: true },
8
+ listenMs: { value: 0, validate: RED.validators.number() }
9
+ },
10
+ inputs: 1,
11
+ outputs: 1,
12
+ icon: 'font-awesome/fa-bluetooth',
13
+ label: function () {
14
+ return this.name || 'BLE read';
15
+ }
16
+ });
17
+ </script>
18
+
19
+ <script type="text/html" data-template-name="ble-reader-read">
20
+ <div class="form-row">
21
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
22
+ <input type="text" id="node-input-name">
23
+ </div>
24
+ <div class="form-row">
25
+ <label for="node-input-device"><i class="fa fa-bluetooth"></i> Device</label>
26
+ <input type="text" id="node-input-device">
27
+ </div>
28
+ <div class="form-row">
29
+ <label for="node-input-listenMs"><i class="fa fa-clock-o"></i> Listen ms</label>
30
+ <input type="number" id="node-input-listenMs" min="0" step="100" placeholder="use config">
31
+ </div>
32
+ </script>
33
+
34
+ <script type="text/html" data-help-name="ble-reader-read">
35
+ <p>Collects raw BLE notifications. Output <code>msg.payload</code> is an array with one entry per matching device.</p>
36
+ <p>Each message contains <code>data</code> as a Buffer plus <code>hex</code>, <code>base64</code>, <code>bytes</code>, <code>length</code>, and <code>timestamp</code>.</p>
37
+ <p>Incoming <code>msg.payload</code> may contain <code>{ deviceName, listenMs, serviceUuid, notifyUuid }</code> to override the configured values.</p>
38
+ </script>
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ module.exports = function registerBleReaderReadNode(RED) {
4
+ function BleReaderReadNode(config) {
5
+ RED.nodes.createNode(this, config);
6
+ const node = this;
7
+
8
+ node.device = RED.nodes.getNode(config.device);
9
+ node.listenMs = Number(config.listenMs || 0);
10
+
11
+ node.on('input', async (msg, send, done) => {
12
+ const emit = send || ((message) => node.send(message));
13
+ try {
14
+ if (!node.device) throw new Error('BLE reader device node is not configured.');
15
+ const override = normalizePayload(msg.payload);
16
+ const deviceName = override.deviceName || (node.device.targetMode === 'name' ? node.device.deviceName : undefined);
17
+ const listenMs = override.listenMs ?? (node.listenMs || node.device.listenMs);
18
+ const serviceUuid = override.serviceUuid ?? node.device.serviceUuid;
19
+ const notifyUuid = override.notifyUuid ?? node.device.notifyUuid;
20
+
21
+ node.status({ fill: 'yellow', shape: 'ring', text: 'listening' });
22
+ const result = await node.device.enqueue((ble) =>
23
+ ble.readDevices({
24
+ bluetooth: node.device.bluetooth,
25
+ namePrefix: node.device.namePrefix,
26
+ deviceName,
27
+ listenMs,
28
+ notifyUuid,
29
+ scanServiceUuid: serviceUuid || null,
30
+ matchServiceUuid: serviceUuid || null
31
+ })
32
+ );
33
+ const messageCount = result.reduce((count, reading) => count + reading.messages.length, 0);
34
+ node.status({ fill: 'green', shape: 'dot', text: `${messageCount} message${messageCount === 1 ? '' : 's'}` });
35
+ msg.payload = result;
36
+ emit(msg);
37
+ done?.();
38
+ } catch (error) {
39
+ node.status({ fill: 'red', shape: 'ring', text: 'error' });
40
+ done ? done(error) : node.error(error, msg);
41
+ }
42
+ });
43
+ }
44
+
45
+ RED.nodes.registerType('ble-reader-read', BleReaderReadNode);
46
+ };
47
+
48
+ function normalizePayload(payload) {
49
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return {};
50
+ const result = {};
51
+ if (typeof payload.deviceName === 'string' && payload.deviceName) result.deviceName = payload.deviceName;
52
+ if (typeof payload.serviceUuid === 'string') result.serviceUuid = payload.serviceUuid;
53
+ if (typeof payload.notifyUuid === 'string' && payload.notifyUuid) result.notifyUuid = payload.notifyUuid;
54
+ if (payload.listenMs !== undefined) {
55
+ const listenMs = Number(payload.listenMs);
56
+ if (Number.isFinite(listenMs) && listenMs >= 0) result.listenMs = listenMs;
57
+ }
58
+ return result;
59
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "node-red-contrib-ble-scanner",
3
+ "version": "0.0.1",
4
+ "description": "Generic Node-RED BLE reader for raw notification messages.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/node-red-contrib/node-red-contrib-ble-scanner.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/node-red-contrib/node-red-contrib-ble-scanner/issues"
14
+ },
15
+ "homepage": "https://github.com/node-red-contrib/node-red-contrib-ble-scanner#readme",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "bin",
24
+ "dist",
25
+ "nodes",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "bin": {
30
+ "ble-reader": "bin/ble-reader.js"
31
+ },
32
+ "node-red": {
33
+ "version": ">=4.0.0",
34
+ "nodes": {
35
+ "ble-reader-device": "nodes/ble-reader-device.js",
36
+ "ble-reader-read": "nodes/ble-reader-read.js"
37
+ }
38
+ },
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.json",
41
+ "clean": "tsc -p tsconfig.json --build --clean",
42
+ "discover": "npm run build --silent && node ./bin/ble-reader.js discover",
43
+ "read": "npm run build --silent && node ./bin/ble-reader.js read",
44
+ "lint": "eslint .",
45
+ "check": "npm run build && npm run lint && npm test",
46
+ "test": "npm run build --silent && node --test"
47
+ },
48
+ "dependencies": {
49
+ "commander": "^14.0.3",
50
+ "dbus-next": "^0.10.2"
51
+ },
52
+ "optionalDependencies": {
53
+ "@abandonware/noble": "1.9.2-26"
54
+ },
55
+ "devDependencies": {
56
+ "@eslint/js": "^10.0.1",
57
+ "@types/node": "^25.6.0",
58
+ "eslint": "^10.3.0",
59
+ "typescript": "^6.0.3",
60
+ "typescript-eslint": "^8.59.1"
61
+ },
62
+ "engines": {
63
+ "node": ">=20.0.0"
64
+ },
65
+ "keywords": [
66
+ "node-red",
67
+ "bluetooth",
68
+ "ble",
69
+ "scanner",
70
+ "notifications"
71
+ ],
72
+ "license": "MIT"
73
+ }