advlib-ble-manufacturers
Version:
Wireless advertising packet decoding library for Bluetooth Low Energy manufacturer-specific data. We believe in an open Internet of Things.
211 lines (180 loc) • 7.29 kB
JavaScript
/**
* Copyright reelyActive 2022-2024
* We believe in an open Internet of Things
*/
const utils = require('./utils');
const MIN_DATA_LENGTH_BYTES = 7;
const BLUETOOTH_SENSOR_DATA_LENGTH_BYTES = 24;
const DECODING_ADVERTISING_FRAME_DATA_LENGTH_BYTES = 24;
const BLUETOOTH_SENSOR_MANUFACTURING_DATA_VERSION = 0x02;
const DECODING_ADVERTISING_MANUFACTURING_DATA_VERSION = 0x03;
const DECODING_SCAN_RESPONSE_MANUFACTURING_DATA_VERSION = 0x04;
const SOFTWARE_VERSION_MAJOR_OFFSET = 1;
const SOFTWARE_VERSION_MINOR_OFFSET = 2;
const V5_STATUS_FLAGS_OFFSET = 3;
const V6_STATUS_FLAGS_OFFSET = 9;
const MEASUREMENT_COUNTER_OFFSET = 4;
const MEASUREMENT_PERIOD_OFFSET = 8;
const SENSOR_TYPES_OFFSET = 11;
const MEASUREMENT_SLOTS_OFFSET = 14;
const NUMBER_OF_SENSORS = 3;
const BATTERY_LEVEL_MASK = 0x01;
const DECODING_ADVERTISING_FRAME_MAC_OFFSET = 1;
const DECODING_SCAN_RESPONSE_FRAME_MEASUREMENT_OFFSET = 1;
const CRC16_LENGTH_BYTES = 2;
const IDENTIFIER_TYPE_EUI_48 = 2;
const EFENTO_URI = "https://sniffypedia.org/Organization/Efento_Sp_zoo/";
/**
* Process Efento manufacturer-specific data.
* @param {Object} data The manufacturer data as a hexadecimal-string or Buffer.
* @return {Object} The processed Efento data as JSON.
*/
function process(data) {
let buf = utils.convertToBuffer(data);
if((buf === null) || (buf.length < MIN_DATA_LENGTH_BYTES)) {
return null;
}
let manufacturingDataVersion = buf.readUInt8();
switch(manufacturingDataVersion) {
case BLUETOOTH_SENSOR_MANUFACTURING_DATA_VERSION:
return processBluetoothSensor(buf);
case DECODING_ADVERTISING_MANUFACTURING_DATA_VERSION:
return processDecodingAdvertisingFrame(buf);
case DECODING_SCAN_RESPONSE_MANUFACTURING_DATA_VERSION:
return processDecodingScanResponseFrame(buf);
default:
return null;
}
}
/**
* Process Efento Bluetooth Sensor data.
* @param {Buffer} data The manufacturer data as a Buffer.
* @return {Object} The processed Efento data as JSON.
*/
function processBluetoothSensor(data) {
if(data.length !== BLUETOOTH_SENSOR_DATA_LENGTH_BYTES) {
return null;
}
let efentoSensor = { uri: EFENTO_URI };
efentoSensor.version = data.readUInt8(SOFTWARE_VERSION_MAJOR_OFFSET) + '.' +
data.readUInt8(SOFTWARE_VERSION_MINOR_OFFSET);
efentoSensor.txCount = data.readUInt32BE(MEASUREMENT_COUNTER_OFFSET);
let statusFlags = data.readUInt8(V5_STATUS_FLAGS_OFFSET);
let isBatteryOk = ((statusFlags & BATTERY_LEVEL_MASK) === BATTERY_LEVEL_MASK);
let measurementPeriod = data.readUInt16BE(MEASUREMENT_PERIOD_OFFSET);
let isInSeconds = (measurementPeriod >= 0x8000);
measurementPeriod &= 0x7fff;
efentoSensor.measurementPeriodSeconds = (isInSeconds ? measurementPeriod :
measurementPeriod * 60);
efentoSensor.batteryPercentage = isBatteryOk ? 100 : 0;
for(let sensorCount = 0; sensorCount < NUMBER_OF_SENSORS; sensorCount++) {
let sensorType = data.readUInt8(SENSOR_TYPES_OFFSET + sensorCount);
let measurementIndex = MEASUREMENT_SLOTS_OFFSET + (sensorCount * 2);
let measurement = data.subarray(measurementIndex, measurementIndex + 2);
Object.assign(efentoSensor, processMeasurement(sensorType, measurement));
}
return efentoSensor;
}
/**
* Process Efento decoding advertising frame data.
* @param {Buffer} data The manufacturer data as a Buffer.
* @return {Object} The processed Efento data as JSON.
*/
function processDecodingAdvertisingFrame(data) {
if(data.length !== DECODING_ADVERTISING_FRAME_DATA_LENGTH_BYTES) {
return null;
}
let efentoSensor = { uri: EFENTO_URI };
let macSignature = toMacSignature(data,
DECODING_ADVERTISING_FRAME_MAC_OFFSET);
let statusFlags = data.readUInt8(V6_STATUS_FLAGS_OFFSET);
let isBatteryOk = ((statusFlags & BATTERY_LEVEL_MASK) === BATTERY_LEVEL_MASK);
efentoSensor.batteryPercentage = isBatteryOk ? 100 : 0;
efentoSensor.deviceIds = [ macSignature ];
return efentoSensor;
}
/**
* Process Efento decoding scan response frame data.
* @param {Buffer} data The manufacturer data as a Buffer.
* @return {Object} The processed Efento data as JSON.
*/
function processDecodingScanResponseFrame(data) {
let efentoSensor = { uri: EFENTO_URI };
let crcOffset = data.length - CRC16_LENGTH_BYTES;
let measurementIndex = DECODING_SCAN_RESPONSE_FRAME_MEASUREMENT_OFFSET;
while(measurementIndex < crcOffset) {
let sensorType = data.readUInt8(measurementIndex);
let measurement = data.subarray(measurementIndex + 1, measurementIndex + 3);
Object.assign(efentoSensor, processMeasurement(sensorType, measurement));
measurementIndex += 3;
}
return efentoSensor;
}
/**
* Process measurement based on the given sensor type.
* @param {Number} sensorType The type of sensor that produced the measurement.
* @param {Buffer} measurement The raw measurement data to process.
* @return {Object} The processed measurement data as JSON.
*/
function processMeasurement(sensorType, measurement) {
switch(sensorType) {
case 0x00: // None
return;
case 0x01: // Temperature
let temperature = (measurement.readUInt16BE() / 100) - 150;
if(temperature <= 150) {
return { temperature: temperature };
}
break;
case 0x02: // Humidity
let relativeHumidity = measurement.readUInt16BE();
if(relativeHumidity <= 100) {
return { relativeHumidity: relativeHumidity };
}
break;
case 0x03: // Pressure
let pressure = measurement.readUInt16BE() * 10;
if(pressure <= 652790) {
return { pressure: pressure };
}
break;
case 0x04: // Differential pressure
let differentialPressure = measurement.readUInt16BE() - 0x8000;
if((differentialPressure >= -32512) && (differentialPressure <= 32511)) {
return { differentialPressure: differentialPressure };
}
break;
case 0x05: // Alarm
break; // TODO: support in future?
case 0x06: // Indoor Air Quality
break; // TODO: support in future?
case 0x07: // Water leakage
break; // TODO: support in future?
case 0x08: // Pulse counter
break; // TODO: support in future?
case 0x09: // Watt-Hours
break; // TODO: support in future?
case 0x0a: // Water counter
break; // TODO: support in future?
case 0x0b: // Soil moisture
break; // TODO: support in future?
case 0x16: // High pressure
break; // TODO: support in future?
}
}
/**
* Convert the given 48-bit data into a MAC address signature.
* @param {Buffer} data The buffer containing the big-endian MAC address.
* @param {Integer} index The index to the start of the MAC address.
* @return {String} The MAC address signature.
*/
function toMacSignature(data, index) {
return data.toString('hex', index + 0, index + 1) +
data.toString('hex', index + 1, index + 2) +
data.toString('hex', index + 2, index + 3) +
data.toString('hex', index + 3, index + 4) +
data.toString('hex', index + 4, index + 5) +
data.toString('hex', index + 5, index + 6) +
'/' + IDENTIFIER_TYPE_EUI_48;
}
module.exports.process = process;