pareto-anywhere
Version:
Open source IoT middleware suite that makes the data from just about anything usable. We believe in an open Internet of Things.
363 lines (303 loc) • 11 kB
JavaScript
/**
* Copyright reelyActive 2026
* We believe in an open Internet of Things
*/
// Constants
const DEMO_SEARCH_PARAMETER = 'demo';
const DEFAULT_BLE_URI = 'https://sniffypedia.org/Product/Any_BLE-Device/';
// DOM elements
let connectIcon = document.querySelector('#connectIcon');
let demoalert = document.querySelector('#demoalert');
let novelCount = document.querySelector('#novelCount');
let deviceCount = document.querySelector('#deviceCount');
let bletbody = document.querySelector('#bletbody');
let classifiedtbody = document.querySelector('#classifiedtbody');
let offcanvas = document.querySelector('#offcanvas');
let offcanvasTitle = document.querySelector('#offcanvasTitle');
let offcanvasBody = document.querySelector('#offcanvasBody');
let packetsDisplay = document.querySelector('#packetsDisplay');
// Other variables
let novelDevices = new Map();
let displayedDevices = new Map();
let classifiedIdentifiers = { name: new Map(),
uuid16: new Map(),
companyCode: new Map() };
let bsOffcanvas = new bootstrap.Offcanvas(offcanvas);
let selectedDeviceSignature;
// Initialise based on URL search parameters, if any
let searchParams = new URLSearchParams(location.search);
let isDemo = searchParams.has(DEMO_SEARCH_PARAMETER);
let baseUrl = window.location.protocol + '//' + window.location.hostname + ':' +
window.location.port;
// Handle beaver events
beaver.on('connect', handleConnect);
beaver.on('appearance', handleAppearance);
beaver.on('raddec', handleRaddec);
beaver.on('stats', (stats) => { deviceCount.textContent = beaver.devices.size });
beaver.on('disappearance', handleDisappearance);
beaver.on('error', handleError);
beaver.on('disconnect', handleDisconnect);
// Demo mode: connect to starling.js
if(isDemo) {
let demoIcon = createElement('b', 'animate-breathing text-success', 'DEMO');
connectIcon.replaceChildren(demoIcon);
beaver.stream(null, { io: starling, ioUrl: "http://pareto.local" });
}
// Normal mode: connect to socket.io
else {
beaver.stream(baseUrl, { io: io });
}
// Handle stream connection
function handleConnect() {
demoalert.hidden = true;
connectIcon.replaceChildren(createElement('i', 'fas fa-cloud text-success'));
}
// Handle an appearance
function handleAppearance(deviceSignature, device) {
let isNovelDevice = (device.statid?.uri === DEFAULT_BLE_URI);
if(isNovelDevice) {
updateNovelDevice(deviceSignature, device.raddec);
}
deviceCount.textContent = beaver.devices.size;
}
// Handle a radio decoding
function handleRaddec(raddec) {
let deviceSignature = raddec.transmitterId + '/' + raddec.transmitterIdType;
let device = beaver.devices.get(deviceSignature);
let isNovelDevice = (device.statid?.uri === DEFAULT_BLE_URI);
if(isNovelDevice) {
updateNovelDevice(deviceSignature, raddec);
}
else if(novelDevices.has(deviceSignature)) {
removeNovelDevice(deviceSignature);
}
}
// Handle a disappearance
function handleDisappearance(deviceSignature) {
if(novelDevices.has(deviceSignature)) {
removeNovelDevice(deviceSignature);
}
deviceCount.textContent = beaver.devices.size;
}
// Handle stream disconnection
function handleDisconnect() {
connectIcon.replaceChildren(createElement('i', 'fas fa-cloud text-warning'));
}
// Handle error
function handleError(error) {
connectIcon.replaceChildren(createElement('i', 'fas fa-cloud text-danger'));
demoalert.hidden = false;
}
// Handle device click
function handleDeviceClick(deviceSignature) {
selectedDeviceSignature = deviceSignature;
offcanvasTitle.textContent = selectedDeviceSignature;
updateOffcanvasBody(selectedDeviceSignature);
bsOffcanvas.show();
}
// Update a novel device
function updateNovelDevice(deviceSignature, raddec) {
let identifiers = novelDevices.get(deviceSignature) || {};
updateIdentifiers(raddec, identifiers);
lookupIdentifiers(identifiers);
novelDevices.set(deviceSignature, identifiers);
let tr = displayedDevices.get(deviceSignature);
if(tr) {
updateRow(tr, identifiers);
}
else {
tr = createRow(deviceSignature, identifiers);
displayedDevices.set(deviceSignature, tr);
bletbody.appendChild(tr);
}
novelCount.textContent = displayedDevices.size;
}
// Remove a novel device
function removeNovelDevice(deviceSignature) {
let tr = displayedDevices.get(deviceSignature);
bletbody.removeChild(tr);
displayedDevices.delete(deviceSignature);
novelDevices.delete(deviceSignature);
novelCount.textContent = displayedDevices.size;
}
// Update identifiers
function updateIdentifiers(raddec, identifiers) {
if(Array.isArray(raddec?.packets)) {
raddec.packets.forEach((packet) => {
mergeIdentifiers(identifiers, determineIdentifiers(packet));
});
}
}
// Look up identifiers from the lists of assigned numbers
function lookupIdentifiers(identifiers) {
for(identifierType in identifiers) {
if(classifiedIdentifiers.hasOwnProperty(identifierType)) {
identifiers[identifierType].forEach((identifier) => {
if(!classifiedIdentifiers[identifierType].has(identifier)) {
let numericalId = parseInt(identifier, 16);
let classifiedName = null;
if(identifierType === 'uuid16') {
classifiedName = BLUETOOTH_MEMBER_UUIDS.get(numericalId) ||
BLUETOOTH_CHARACTERISTIC_UUIDS.get(numericalId);
}
else if(identifierType === 'companyCode') {
classifiedName = BLUETOOTH_COMPANY_IDENTIFIERS.get(numericalId);
}
if(classifiedName) {
let tr = createClassifiedRow(identifierType, identifier,
classifiedName);
classifiedIdentifiers[identifierType].set(identifier, tr);
classifiedtbody.appendChild(tr);
}
}
});
}
}
}
// Merge identifiers
function mergeIdentifiers(targetIdentifiers, sourceIdentifiers) {
for(identifierType in sourceIdentifiers) {
let sourceIdentifier = sourceIdentifiers[identifierType];
if(targetIdentifiers.hasOwnProperty(identifierType)) {
let targetIdentifier = targetIdentifiers[identifierType];
sourceIdentifier.forEach((identifier) => {
if(!targetIdentifier.includes(identifier)) {
targetIdentifier.push(identifier);
}
});
}
else {
targetIdentifiers[identifierType] = sourceIdentifier;
}
}
}
// Determine identifiers
function determineIdentifiers(packet) {
let identifiers = {};
let advData = packet.substring(16);
let gapElements = determineGapElements(advData);
gapElements.forEach((gapElement) => {
let flag = gapElement.substring(2, 4);
let data = gapElement.substring(4);
let identifier = interpretGapElement(flag, data);
mergeIdentifiers(identifiers, identifier);
});
return identifiers;
}
// Determine the GAP elements
function determineGapElements(advData) {
let gapElements = [];
let index = 0;
while(advData.length > (index + 2)) {
let length = parseInt(advData.substring(index, index + 2), 16);
let nextIndex = index + 2 + (length * 2);
if(length < 2) { return []; }
gapElements.push(advData.substring(index, nextIndex));
index = nextIndex;
}
return gapElements;
}
// Interpret the given GAP element
function interpretGapElement(flag, data) {
switch(flag) {
case '02':
case '03':
return { uuid16: [ reendify(data, 2) ] };
case '06':
case '07':
return { uuid128: [ reendify(data, 16) ] };
case '08':
case '09':
let nameBytes = new Uint8Array(data.length / 2);
for(let i = 0; i < data.length; i += 2) {
nameBytes[i / 2] = parseInt(data.substr(i, 2), 16);
}
let name = new TextDecoder().decode(nameBytes);
return { name: [ name ] };
case '16':
return { uuid16: [ reendify(data, 2) ] };
case 'ff':
return { companyCode: [ reendify(data, 2) ] };
default:
return {};
}
}
// Reverse the endianness
function reendify(data, numberOfBytes) {
let reendified = '';
for(let index = (numberOfBytes * 2) - 2; index >= 0; index -= 2) {
reendified += data.substring(index, index + 2);
}
return reendified;
}
// Create the table row
function createRow(signature, identifiers) {
let tds = [];
tds.push(createElement('td', null, createDeviceSignature(signature)));
tds.push(createElement('td', null, identifiers.name || ''));
tds.push(createElement('td', null, identifiers.companyCode || ''));
tds.push(createElement('td', null, identifiers.uuid16 || ''));
tds.push(createElement('td', null, identifiers.uuid128 || ''));
return createElement('tr', null, tds);
}
// Create the classified table row
function createClassifiedRow(identifierType, identifier, classifiedName) {
let tds = [];
let typeContent = '';
if(identifierType === 'companyCode') {
typeContent = [ createElement('i', 'fab fa-bluetooth'), ' Company Code' ];
}
else if(identifierType === 'uuid16') {
typeContent = [ createElement('i', 'fab fa-bluetooth'), ' UUID-16' ];
}
tds.push(createElement('td', null, typeContent));
tds.push(createElement('td', 'font-monospace', identifier));
tds.push(createElement('td', null, classifiedName));
return createElement('tr', null, tds);
}
// Create the device signature
function createDeviceSignature(signature) {
let a = createElement('a', 'font-monospace text-decoration-none', signature);
a.addEventListener('click', (event) => { handleDeviceClick(signature); });
return a;
}
// Update the given table row
function updateRow(deviceRow, identifiers) {
deviceRow.childNodes[1].textContent = identifiers.name || '';
deviceRow.childNodes[2].textContent = identifiers.companyCode || '';
deviceRow.childNodes[3].textContent = identifiers.uuid16 || '';
deviceRow.childNodes[4].textContent = identifiers.uuid128 || '';
}
// Update the offcanvas body based on the selected device
function updateOffcanvasBody(deviceSignature) {
let device = beaver.devices.get(deviceSignature) || {};
let packets = device?.raddec?.packets || [];
packetsDisplay.textContent = '';
packets.forEach((packet) => {
packetsDisplay.textContent += packet + '\r\n';
});
}
// Create an element as specified
function createElement(elementName, classNames, content) {
let element = document.createElement(elementName);
if(classNames) {
element.setAttribute('class', classNames);
}
if((content instanceof Element) || (content instanceof DocumentFragment)) {
element.appendChild(content);
}
else if(Array.isArray(content)) {
content.forEach((item) => {
if((item instanceof Element) || (item instanceof DocumentFragment)) {
element.appendChild(item);
}
else {
element.appendChild(document.createTextNode(item));
}
});
}
else if(content) {
element.appendChild(document.createTextNode(content));
}
return element;
}