hlc-server
Version:
Serves real-time real-world context at a human scale by combining RFID, RTLS and M2M with structured, linked data on the web. We believe in an open Internet of Things.
316 lines (254 loc) • 8.53 kB
JavaScript
/**
* Copyright reelyActive 2020
* We believe in an open Internet of Things
*/
// Constants
const UPDATE_INTERVAL_MILLISECONDS = 2000;
const SIGNATURE_SEPARATOR = '/';
const SNIFFYPEDIA_BASE_URL = 'https://sniffypedia.org/';
const ICON_DEVICES = 'fas fa-wifi';
const ICON_APPEARANCE = 'fas fa-sign-in-alt';
// DOM elements
let numTransmitters = document.querySelector('#numTransmitters');
let digitalTwinsRatio = document.querySelector('#digitalTwinsRatio');
let cards = document.querySelector('#cards');
// Other variables
let devices = {};
let urls = {};
let isUpdateRequired = false;
let baseUrl = window.location.protocol + '//' + window.location.hostname +
':' + window.location.port;
// Connect to the socket.io stream and feed to beaver
let socket = io.connect(baseUrl);
beaver.listen(socket, true);
// Non-disappearance events
beaver.on([ 0, 1, 2, 3 ], function(raddec) {
let transmitterSignature = raddec.transmitterId +
SIGNATURE_SEPARATOR +
raddec.transmitterIdType;
let isNewDevice = !devices.hasOwnProperty(transmitterSignature);
if(isNewDevice) {
let appearanceTime = new Date().toLocaleTimeString();
devices[transmitterSignature] = { url: null,
appearanceTime: appearanceTime };
determineUrl(transmitterSignature, raddec.packets,
function(url, isSniffypedia) {
if(url) {
devices[transmitterSignature].url = url;
let isNewUrl = !urls.hasOwnProperty(url);
if(isNewUrl) {
urls[url] = { count: 1, isSniffypedia: isSniffypedia };
if(!isSniffypedia) { // TODO: optionally display Sniffypedia twins?
cormorant.retrieveStory(url, function(story) {
let card = createCard(story, appearanceTime, ICON_APPEARANCE);
cards.appendChild(card);
});
}
}
else {
urls[url].count++;
isUpdateRequired = true;
}
}
});
}
});
// Disappearance events
beaver.on([ 4 ], function(raddec) {
let transmitterSignature = raddec.transmitterId +
SIGNATURE_SEPARATOR +
raddec.transmitterIdType;
let isExistingDevice = devices.hasOwnProperty(transmitterSignature);
if(isExistingDevice) {
let url = devices[transmitterSignature].url;
let isValidUrl = url && urls.hasOwnProperty(url);
if(isValidUrl) {
urls[url].count--;
isUpdateRequired = true;
}
delete devices[transmitterSignature];
}
});
// Determine the URL associated with the given device
function determineUrl(transmitterSignature, packets, callback) {
cormorant.retrieveAssociations(baseUrl, transmitterSignature, false,
function(associations) {
if(associations && associations.hasOwnProperty('url')) {
callback(associations.url, false);
}
else {
let identifiers = {
uuid16: [],
uuid128: [],
companyIdentifiers: []
};
packets.forEach(function(packet) {
parsePacketIdentifiers(packet, identifiers);
});
callback(lookupIdentifiers(identifiers), true);
}
});
}
// Parse the given packets, extracting all identifiers
// TODO: in future this will be handled server-side, just a stopgap for now
function parsePacketIdentifiers(packet, identifiers) {
let isTooShort = (packet.length <= 16);
if(isTooShort) {
return identifiers;
}
let length = parseInt(packet.substr(2,2),16) % 64;
let isInvalidLength = (packet.length !== ((length + 2) * 2));
if(isInvalidLength) {
return identifiers;
}
let data = packet.substr(16);
let dataLength = data.length;
let index = 0;
while(index < dataLength) {
let length = parseInt(data.substr(index,2), 16) + 1;
let dataType = data.substr(index + 2, (length + 1) * 2);
parseDataType(dataType, identifiers);
index += (length * 2);
}
return identifiers;
}
// Parse the data type at the given index, extracting any identifier(s)
function parseDataType(dataType, identifiers) {
let gapType = parseInt(dataType.substr(0,2), 16);
let identifier = '';
switch(gapType) {
case 0x02: // Incomplete list of 16-bit UUIDs
case 0x03: // Complete list of 16-bit UUIDs
for(let cByte = 2; cByte > 0; cByte--) {
identifier += dataType.substr(cByte * 2, 2);
}
identifiers.uuid16.push(identifier);
break;
case 0x06: // Incomplete list of 128-bit UUIDs
case 0x07: // Complete list of 128-bit UUIDs
for(let cByte = 16; cByte > 0; cByte--) {
identifier += dataType.substr(cByte * 2, 2);
}
identifiers.uuid128.push(identifier);
break;
case 0xff: // Manufacturer specific data
identifier = dataType.substr(4,2) + dataType.substr(2,2);
identifiers.companyIdentifiers.push(identifier);
break;
}
}
// Lookup in the Sniffypedia index the given identifiers, return URL
function lookupIdentifiers(identifiers) {
let route;
// Company identifiers have lowest precedence
identifiers.companyIdentifiers.forEach(function(companyIdentifier) {
if(ble.companyIdentifiers.hasOwnProperty(companyIdentifier)) {
route = ble.companyIdentifiers[companyIdentifier];
}
});
identifiers.uuid128.forEach(function(uuid128) {
if(ble.uuid128.hasOwnProperty(uuid128)) {
route = ble.uuid128[uuid128];
}
});
// 16-bit UUIDs have highest precedence
identifiers.uuid16.forEach(function(uuid16) {
if(ble.uuid16.hasOwnProperty(uuid16)) {
route = ble.uuid16[uuid16];
}
});
if(route) {
return SNIFFYPEDIA_BASE_URL + route;
}
return null;
}
// Create the card from the given story
function createCard(story, text, iconClass) {
let card = document.createElement('div');
let listGroupItems = [ {
text: text,
itemClass: "text-white bg-dark lead",
iconClass: iconClass || "fas fa-info-circle"
} ];
card.setAttribute('class', 'card');
cuttlefish.render(story, card, { listGroupItems: listGroupItems });
return card;
}
// Update all the cards
function updateCards() {
if(!isUpdateRequired) {
return;
}
let updatedCards = document.createDocumentFragment();
let orderedUrls = [];
let orderedCounts = [];
// Order the urls by device counts
for(let url in urls) {
if(!urls[url].isSniffypedia) { // TODO: display Sniffypedia twins?
let count = urls[url].count;
if(orderedUrls.length === 0) {
orderedUrls.push(url);
orderedCounts.push(count);
}
else {
for(let cUrl = 0; cUrl < orderedUrls.length; cUrl++) {
if(count > orderedCounts[cUrl]) {
orderedUrls.splice(cUrl, 0, url);
orderedCounts.splice(cUrl, 0, count);
cUrl = orderedUrls.length;
}
else if(cUrl === (orderedUrls.length - 1)) {
orderedUrls.push(url);
orderedCounts.push(count);
cUrl = orderedUrls.length;
}
}
}
}
}
// Create the updated cards in order of device counts
for(let cUrl = 0; cUrl < orderedUrls.length; cUrl++) {
let url = orderedUrls[cUrl];
let count = orderedCounts[cUrl];
if(count > 0) {
let text;
let iconClass = ICON_DEVICES;
if(count === 1) {
text = '1 device';
for(deviceSignature in devices) {
let deviceUrl = devices[deviceSignature].url;
if(deviceUrl === url) {
text = devices[deviceSignature].appearanceTime;
iconClass = ICON_APPEARANCE;
}
}
}
else {
text = count + ' devices';
}
let story = cormorant.stories[url];
let card = createCard(story, text, iconClass);
updatedCards.appendChild(card);
}
}
cards.innerHTML = '';
cards.appendChild(updatedCards);
isUpdateRequired = false;
}
// Update the stats
function updateStats() {
let twinnedCount = 0;
let deviceCount = Object.keys(devices).length;
let twinPercentage = 0;
for(let url in urls) {
let count = urls[url].count;
twinnedCount += count;
}
if(deviceCount > 0) {
twinPercentage = (100 * (twinnedCount / deviceCount)).toFixed(0);
}
numTransmitters.textContent = deviceCount;
digitalTwinsRatio.textContent = twinPercentage + '%';
}
setInterval(updateCards, UPDATE_INTERVAL_MILLISECONDS);
setInterval(updateStats, UPDATE_INTERVAL_MILLISECONDS);