barnowl-aruba
Version:
Collect ambient Bluetooth Low Energy, WiFi and EnOcean Alliance packets from HPE Aruba Networking access points for real-time location and sensing. We believe in an open Internet of Things.
346 lines (303 loc) • 11.1 kB
JavaScript
/**
* Copyright reelyActive 2024
* We believe in an open Internet of Things
*/
const aos8Telemetry = require('./aos8v1proto.js').aruba_telemetry;
// TODO: move to configuration file or create from standard JSON request
const UUID_ACTIONS = new Map([
// Sensor-Works BluVib ------------------------------------------------------
[ "1c930001d45911e79296b8e856369374", {
payloadPattern: "110674933656e8b89692e71159d40100931c",
timeoutMilliseconds: 60000,
actions: [{ // Connect
"serviceUuid": null,
"value": null,
"characteristicUuid": null,
"actionId": "00000001",
"timeOut": 45,
"type": "bleConnect"
},
{ // Mode
"serviceUuid": "1c930003-d459-11e7-9296-b8e856369374",
"value": null,
"characteristicUuid": "1c930031-d459-11e7-9296-b8e856369374",
"actionId": "00000002",
"timeOut": 20,
"type": "gattRead"
},
{ // Temp
"serviceUuid": "1c930003-d459-11e7-9296-b8e856369374",
"value": null,
"characteristicUuid": "1c930032-d459-11e7-9296-b8e856369374",
"actionId": "00000003",
"timeOut": 20,
"type": "gattRead"
},
{ // Battery
"serviceUuid": "1c930003-d459-11e7-9296-b8e856369374",
"value": null,
"characteristicUuid": "1c930038-d459-11e7-9296-b8e856369374",
"actionId": "00000004",
"timeOut": 20,
"type": "gattRead"
},
{ // Sample rate
"serviceUuid": "1c930002-d459-11e7-9296-b8e856369374",
"value": null,
"characteristicUuid": "1c930023-d459-11e7-9296-b8e856369374",
"actionId": "00000005",
"timeOut": 20,
"type": "gattRead"
},
{ // Trace length
"serviceUuid": "1c930002-d459-11e7-9296-b8e856369374",
"value": null,
"characteristicUuid": "1c930024-d459-11e7-9296-b8e856369374",
"actionId": "00000006",
"timeOut": 20,
"type": "gattRead"
},
{ // Calibration
"serviceUuid": "1c930002-d459-11e7-9296-b8e856369374",
"value": null,
"characteristicUuid": "1c930029-d459-11e7-9296-b8e856369374",
"actionId": "00000007",
"timeOut": 20,
"type": "gattRead"
},
{ // Data: notifications
"serviceUuid": "1c930002-d459-11e7-9296-b8e856369374",
"value": "01",
"characteristicUuid": "1c930020-d459-11e7-9296-b8e856369374",
"actionId": "00000008",
"timeOut": 60,
"type": "gattNotification"
}],
concludingActions: [{ // Release
"serviceUuid": "1c930003-d459-11e7-9296-b8e856369374",
"value": "00",
"characteristicUuid": "1c930030-d459-11e7-9296-b8e856369374",
"actionId": "00000009",
"timeOut": 20,
"type": "gattWrite"
},
{ // Disconnect
"serviceUuid": null,
"value": null,
"characteristicUuid": null,
"actionId": "00000010",
"timeOut": 20,
"type": "bleDisconnect"
}]
}]
]);
const DEFAULT_TIMEOUT_MILLISECONDS = 60000;
const TYPE_GATT_READ = 2;
const TYPE_GATT_NOTIFICATION = 5;
/**
* GattManager Class
* Manages GATT interaction.
* @constructor
*/
class GattManager {
/**
* GattManager constructor
* @param {Object} options The options as a JSON object.
* @constructor
*/
constructor(options) {
this.barnowl = options.barnowl;
this.activeConnections = new Map();
}
/**
* Handle the given GAP packet
* @param {Object} packet The GAP packet to handle.
* @param {String} transmitterId The ID of the transmitter of the GAP packet.
* @param {String} receiverId The ID of the receiver of the GAP packet.
* @param {Raddec} raddec The compiled radio decoding.
* @param {Function} responseHandler The function to handle any response.
*/
handlePacket(packet, transmitterId, receiverId, raddec, responseHandler) {
let self = this;
UUID_ACTIONS.forEach((instructions, uuid) => {
if(packet.includes(instructions.payloadPattern) &&
!self.activeConnections.has(transmitterId) &&
(typeof responseHandler === 'function')) {
self.activeConnections.set(transmitterId, { raddec: raddec, gatt: [] });
initiateActions(self, instructions, transmitterId, receiverId,
responseHandler);
}
});
}
/**
* Handle the given characteristic
* @param {Object} characteristic The characteristic to handle.
* @param {String} type The type of update (read/notification).
*/
handleCharacteristic(characteristic, type) {
let self = this;
if((typeof characteristic.deviceMac !== 'string') ||
(typeof characteristic.serviceUuid !== 'string') ||
(typeof characteristic.characteristicUuid !== 'string') ||
(typeof characteristic.value !== 'string') ||
!self.activeConnections.has(characteristic.deviceMac)) {
return;
}
let device = self.activeConnections.get(characteristic.deviceMac);
if(type === TYPE_GATT_READ) {
device.gatt.push({ serviceUuid: characteristic.serviceUuid,
characteristicUuid: characteristic.characteristicUuid,
value: characteristic.value });
}
else if(type === TYPE_GATT_NOTIFICATION) {
let isNew = true;
device.gatt.forEach((element) => {
if((element.serviceUuid === characteristic.serviceUuid) &&
(element.characteristicUuid === characteristic.characteristicUuid)) {
element.values.push(characteristic.value);
isNew = false;
}
});
if(isNew) {
device.gatt.push({ serviceUuid: characteristic.serviceUuid,
characteristicUuid: characteristic.characteristicUuid,
values: [ characteristic.value ] });
}
}
}
}
/**
* Initiate the actions.
* @param {GattManager} instance The given GattManager instance.
* @param {Object} instructions The instructions, including actions.
* @param {String} transmitterId The ID of the transmitter of the GAP packet.
* @param {String} receiverId The ID of the receiver of the GAP packet.
* @param {Function} responseHandler The function to handle any response.
*/
function initiateActions(instance, instructions, transmitterId, receiverId,
responseHandler) {
let timeoutMilliseconds = instructions.timeoutMilliseconds ||
DEFAULT_TIMEOUT_MILLISECONDS;
let formattedActions = formatActions(instructions.actions, transmitterId,
receiverId);
setTimeout(initiateConcludingActions, timeoutMilliseconds, instance,
instructions, transmitterId, receiverId, responseHandler);
if(formattedActions) {
responseHandler(formattedActions);
}
}
/**
* Initiate the concluding actions.
* @param {GattManager} instance The given GattManager instance.
* @param {Object} instructions The instructions, including concluding actions.
* @param {String} transmitterId The ID of the transmitter of the GAP packet.
* @param {String} receiverId The ID of the receiver of the GAP packet.
* @param {Function} responseHandler The function to handle any response.
*/
function initiateConcludingActions(instance, instructions, transmitterId,
receiverId, responseHandler) {
let device = instance.activeConnections.get(transmitterId);
let isGattDataPresent = (device.gatt.length > 0);
let formattedActions = formatActions(instructions.concludingActions,
transmitterId, receiverId);
if(formattedActions) {
responseHandler(formattedActions);
}
if(isGattDataPresent) {
device.raddec.protocolSpecificData = { gatt: device.gatt };
device.raddec.timestamp = Date.now();
instance.barnowl.handleRaddec(device.raddec);
}
instance.activeConnections.delete(transmitterId);
}
/**
* Convert the given device ID to the colon-separated MAC format.
* @param {String} deviceId The ID of the device.
*/
function createMac(deviceId) {
return deviceId.substring(0, 2) + ':' + deviceId.substring(2, 4) + ':' +
deviceId.substring(4, 6) + ':' + deviceId.substring(6, 8) + ':' +
deviceId.substring(8, 10) + ':' + deviceId.substring(10, 12);
}
/**
* Format the given actions for the Aruba southbound API.
* @param {Array} actions The actions to format.
* @param {String} transmitterId The ID of the transmitter of the GAP packet.
* @param {String} receiverId The ID of the receiver of the GAP packet.
*/
function formatActions(actions, transmitterId, receiverId) {
if(!Array.isArray(actions) || (actions.length === 0)) {
return null;
}
let mac = { deviceMac: createMac(transmitterId) };
let macActions = [];
actions.forEach((action) => {
macActions.push(Object.assign({}, mac, action));
});
return formatSbBleMessage({
meta: {
version: 1,
sbTopic: "actions"
},
actions: macActions,
receiver: {
apMac: createMac(receiverId),
all: false
}
});
}
/**
* Format the given message for the Aruba southbound API.
* @param {Object} message The message to format.
*/
function formatSbBleMessage(message) {
let formattedMessage = Object.assign({}, message);
if(Array.isArray(message.actions)) {
formattedMessage.actions = [];
message.actions.forEach((action) => {
let formattedAction = Object.assign({}, action);
if(typeof formattedAction.deviceMac === 'string') {
formattedAction.deviceMac = encodeBase64(formattedAction.deviceMac);
}
if(typeof formattedAction.apbMac === 'string') {
formattedAction.apbMac = encodeBase64(formattedAction.apbMac);
}
if(typeof formattedAction.serviceUuid === 'string') {
formattedAction.serviceUuid = encodeBase64(formattedAction.serviceUuid);
}
if(typeof formattedAction.characteristicUuid === 'string') {
formattedAction.characteristicUuid =
encodeBase64(formattedAction.characteristicUuid);
}
if(typeof formattedAction.value === 'string') {
formattedAction.value = encodeBase64(formattedAction.value);
}
formattedMessage.actions.push(formattedAction);
});
}
if(message.hasOwnProperty('receiver')) {
formattedMessage.receiver = Object.assign({}, message.receiver);
if(typeof formattedMessage.receiver.apMac === 'string') {
formattedMessage.receiver.apMac =
encodeBase64(formattedMessage.receiver.apMac);
}
}
try {
let reply = aos8Telemetry.IotSbMessage.fromObject(formattedMessage);
aos8Telemetry.IotSbMessage.verify(reply);
return aos8Telemetry.IotSbMessage.encode(reply).finish();
}
catch(error) {
console.log('barnowl-aruba: failed to convert sb message to protobuf');
return null;
}
}
/**
* Encode the given data in base64.
* @param {String} data The data to encode.
*/
function encodeBase64(data) {
let hexString = data.replaceAll(':', '').replaceAll('-', '').toLowerCase();
return Buffer.from(hexString, 'hex').toString('base64');
}
module.exports = GattManager;