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.
383 lines (329 loc) • 12.2 kB
JavaScript
/**
* Copyright reelyActive 2019-2020
* We believe in an open Internet of Things
*/
// Constants
const PLAY_UPDATE_MILLISECONDS = 1800;
const SIGNATURE_SEPARATOR = '/';
const DIRACT_PACKET_SIGNATURE = 'ff830501';
const DIRACT_PACKET_SIGNATURE_OFFSET = 24;
const EDDYSTONE_UID_PACKET_SIGNATURE = 'aafe00';
const EDDYSTONE_UID_PACKET_SIGNATURE_OFFSET = 34;
const EDDYSTONE_UID_INSTANCE_ID_OFFSET = 66;
const DIRACT_INSTANCE_ID_LENGTH = 8;
const AXIS_NAMES = [ 'x', 'y', 'z' ];
const IMAGE_INTRO_BEAVER = 'images/intro-beaver.png';
const IMAGE_INTRO_PADDLE = 'images/intro-paddle.png';
const IMAGE_START = 'images/poke-nostrike.png';
const IMAGE_BEAVER_STRIKE = 'images/up-strike.png';
const IMAGE_POND_STRIKE = 'images/down-strike.png';
const IMAGES_NOSTRIKE = [
'images/down-nostrike.png',
'images/poke-nostrike.png',
'images/up-nostrike.png'
];
// DOM elements
let beaverImage = document.querySelector('#beaverImage');
let beaverTransmitterId = document.querySelector('#beaverTransmitterId');
let beaverReceiverId = document.querySelector('#beaverReceiverId');
let beaverRssi = document.querySelector('#beaverRssi');
let nearestInstanceId = document.querySelector('#nearestInstanceId');
let nearestRssi = document.querySelector('#nearestRssi');
let paddleTransmitterId = document.querySelector('#paddleTransmitterId');
let paddleReceiverId = document.querySelector('#paddleReceiverId');
let paddleRssi = document.querySelector('#paddleRssi');
let paddleAccelerationCard = document.querySelector('#paddleAccelerationCard');
// Other variables
let beaverId;
let paddleId;
let beaverDirAct;
let paddleCandidateInstanceIds = [];
let paddleAcceleration = [ null, null, null ];
let currentBeaverPosition = 2;
// Connect to the socket.io stream and feed to beaver
let baseUrl = window.location.protocol + '//' + window.location.hostname +
':' + window.location.port;
let socket = io.connect(baseUrl);
beaver.listen(socket, true);
// Non-disappearance events
beaver.on([ 0, 1, 2, 3 ], function(raddec) {
if(isDirAct(raddec)) {
updateBeaver(raddec);
}
else if(isAcceleration(raddec)) {
updatePaddle(raddec);
}
});
// Disappearance events
beaver.on([ 4 ], function(raddec) {
if(raddec.transmitterId === beaverId) {
beaverId = null;
paddleId = null;
}
else if(raddec.transmitterId === paddleId) {
paddleId = null;
}
});
// Determine if the given raddec is from a DirAct device
function isDirAct(raddec) {
let foundDirActPacket = false;
let hasPackets = (raddec.hasOwnProperty('packets') &&
(raddec.packets.length > 0));
if(hasPackets) {
raddec.packets.forEach(function(packet) {
let signature = packet.substr(DIRACT_PACKET_SIGNATURE_OFFSET,
DIRACT_PACKET_SIGNATURE.length);
if(signature === DIRACT_PACKET_SIGNATURE) {
foundDirActPacket = true;
}
});
}
return foundDirActPacket;
}
// Determine if the given raddec contains accelerometer data
function isAcceleration(raddec) {
let foundAccelerationPacket = false;
let hasPackets = (raddec.hasOwnProperty('packets') &&
(raddec.packets.length > 0));
if(hasPackets) {
raddec.packets.forEach(function(packet) {
let isMinew = (packet.substr(26,4) === 'e1ff');
let isAcceleration = (packet.substr(38,4) === 'a103');
if(isMinew && isAcceleration) {
foundAccelerationPacket = true;
}
});
}
return foundAccelerationPacket;
}
// Update the beaver data
function updateBeaver(raddec) {
let isNewBeaver = (beaverId === undefined) || (beaverId === null);
if(isNewBeaver) {
beaverId = raddec.transmitterId;
beaverTransmitterId.textContent = beaverId;
beaverImage.src = IMAGE_INTRO_PADDLE;
}
let isCurrentBeaver = (raddec.transmitterId === beaverId);
if(isCurrentBeaver) {
beaverReceiverId.textContent = raddec.rssiSignature[0].receiverId;
beaverRssi.textContent = raddec.rssiSignature[0].rssi + ' dBm';
raddec.packets.forEach(function(packet) {
let signature = packet.substr(DIRACT_PACKET_SIGNATURE_OFFSET,
DIRACT_PACKET_SIGNATURE.length);
if(signature === DIRACT_PACKET_SIGNATURE) {
beaverDirAct = parseDirActPacket(packet);
let isNearest = (beaverDirAct.nearest.length > 0);
if(isNearest) {
nearestInstanceId.textContent = beaverDirAct.nearest[0].instanceId;
nearestRssi.textContent = beaverDirAct.nearest[0].rssi + ' dBm';
}
else {
nearestInstanceId.textContent = '\u2026';
nearestRssi.textContent = '\u2014 dBm';
}
beaverDirAct.nearest.forEach(function(device) {
let isNewCandidatePaddle = !paddleCandidateInstanceIds.includes(
device.instanceId);
if(isNewCandidatePaddle) {
paddleCandidateInstanceIds.push(device.instanceId);
}
});
}
});
}
}
// Update the paddle data
function updatePaddle(raddec) {
let isNoPaddle = (paddleId === undefined) || (paddleId === null);
if(isNoPaddle) {
let instanceId = determineDirActInstanceId(raddec);
let isCandidatePaddle = paddleCandidateInstanceIds.includes(instanceId);
if(isCandidatePaddle) {
paddleId = raddec.transmitterId;
paddleTransmitterId.textContent = paddleId;
beaverImage.src = IMAGE_START;
play();
}
}
let isCurrentPaddle = (raddec.transmitterId === paddleId);
if(isCurrentPaddle) {
paddleReceiverId.textContent = raddec.rssiSignature[0].receiverId;
paddleRssi.textContent = raddec.rssiSignature[0].rssi + ' dBm';
paddleAcceleration = determineAcceleration(raddec);
let element = createMagnitudeDisplay(paddleAcceleration, 2);
paddleAccelerationCard.innerHTML = '';
paddleAccelerationCard.appendChild(element);
}
}
// Iteratively play the game
function play() {
let isBeaverDisappeared = !beaverId;
let isPaddleDisappeared = !paddleId;
let isBeaverUp = (currentBeaverPosition === 2);
let isStrike = (Math.abs(paddleAcceleration[0]) >
Math.abs(paddleAcceleration[1])) ||
(Math.abs(paddleAcceleration[2]) >
Math.abs(paddleAcceleration[1]));
if(isPaddleDisappeared) {
paddleTransmitterId.textContent = '\u2026';
paddleReceiverId.textContent = '\u2026';
paddleRssi.textContent = '\u2014 dBm';
paddleAccelerationCard.innerHTML = '';
}
if(isBeaverDisappeared) {
beaverImage.src = IMAGE_INTRO_BEAVER;
beaverTransmitterId.textContent = '\u2026';
beaverReceiverId.textContent = '\u2026';
beaverRssi.textContent = '\u2014 dBm';
nearestInstanceId.textContent = '\u2026';
nearestRssi.textContent = '\u2014 dBm';
return;
}
if(isStrike) {
if(isBeaverUp) {
beaverImage.src = IMAGE_BEAVER_STRIKE;
}
else {
beaverImage.src = IMAGE_POND_STRIKE;
currentBeaverPosition = 0;
}
}
else {
if(isBeaverUp) {
currentBeaverPosition = 1;
}
else {
currentBeaverPosition = Math.floor(Math.random() * 3);
}
beaverImage.src = IMAGES_NOSTRIKE[currentBeaverPosition];
}
setTimeout(play, PLAY_UPDATE_MILLISECONDS);
}
// Determine the DirAct instance ID, if in the packets of the given raddec
function determineDirActInstanceId(raddec) {
let instanceId = null;
raddec.packets.forEach(function(packet) {
let signature = packet.substr(EDDYSTONE_UID_PACKET_SIGNATURE_OFFSET,
EDDYSTONE_UID_PACKET_SIGNATURE.length);
if(signature === EDDYSTONE_UID_PACKET_SIGNATURE) {
instanceId = packet.substr(EDDYSTONE_UID_INSTANCE_ID_OFFSET,
DIRACT_INSTANCE_ID_LENGTH);
}
});
return instanceId;
}
// Determine the acceleration from the packets of the given raddec
function determineAcceleration(raddec) {
let acceleration = [ null, null, null ];
raddec.packets.forEach(function(packet) {
let isMinew = (packet.substr(26,4) === 'e1ff');
let isAcceleration = (packet.substr(38,4) === 'a103');
if(isMinew && isAcceleration) {
acceleration[0] = (fixedPointToDecimal(packet.substr(44, 4))).toFixed(2);
acceleration[1] = (fixedPointToDecimal(packet.substr(48, 4))).toFixed(2);
acceleration[2] = (fixedPointToDecimal(packet.substr(52, 4))).toFixed(2);
}
});
return acceleration;
}
// Parse the given DirAct packet, returning an Object with relevant fields
function parseDirActPacket(packet) {
let diract = {};
let data = packet.substr(30);
let frameLength = parseInt(data.substr(2,2), 16) & 0x1f;
diract.cyclicCount = parseInt(data.substr(2,1), 16) >> 1;
diract.instanceId = data.substr(4,8);
diract.acceleration = [];
diract.acceleration.push(toAcceleration(data.substr(12,2), true));
diract.acceleration.push(toAcceleration(data.substr(13,2), false));
diract.acceleration.push(toAcceleration(data.substr(15,2), true));
diract.batteryPercentage = toBatteryPercentage(data.substr(16,2));
diract.nearest = [];
for(nearestIndex = 9; nearestIndex < (frameLength + 2); nearestIndex += 5) {
let instanceId = data.substr(nearestIndex * 2, 8);
let rssi = toRssi(data.substr(nearestIndex * 2 + 8, 2));
diract.nearest.push( { instanceId: instanceId, rssi: rssi } );
}
return diract;
}
// Convert the given bytes to battery percentage.
function toBatteryPercentage(bits) {
var data = parseInt(bits, 16);
data &= 0x3f;
return Math.round(100 * data / 63);
}
// Convert the given twos complement hexadecimal string to acceleration in g.
function toAcceleration(byte, isUpper) {
var data = parseInt(byte, 16);
if(isUpper) {
data = data >> 2;
}
else {
data &= 0x3f;
}
if(data === 32) {
return null;
}
if(data > 31) {
return (data - 64) / 16;
}
return data / 16;
}
// Convert the given byte to RSSI.
function toRssi(bits) {
var data = parseInt(bits, 16);
return (data & 0x3f) - 92;
}
// Convert the given signed 8.8 fixed-point hexadecimal string to decimal
function fixedPointToDecimal(word) {
let integer = parseInt(word.substr(0,2),16);
let decimal = parseInt(word.substr(2,2),16) / 256;
if(integer > 127) {
return ((integer - 256) + decimal);
}
return (integer + decimal);
}
// Create a magnitude display element based on the given readings
function createMagnitudeDisplay(readings, maxMagnitude) {
let element = document.createElement('div');
readings.forEach(function(reading, index) {
let posWidth = 0;
let negWidth = 0;
let axisName = AXIS_NAMES[index];
if(reading >= 0) {
posWidth = Math.min(100, (reading / maxMagnitude) * 100);
}
else {
negWidth = Math.min(100, (Math.abs(reading) / maxMagnitude) * 100);
}
let row = document.createElement('div');
let colNeg = document.createElement('div');
let colPos = document.createElement('div');
let progressNeg = document.createElement('div');
let progressPos = document.createElement('div');
let progressBarNeg = document.createElement('div');
let progressBarPos = document.createElement('div');
let progressBarNegMessage = document.createTextNode(axisName);
let progressBarPosMessage = document.createTextNode(axisName);
row.setAttribute('class', 'row');
colNeg.setAttribute('class', 'col-6');
colPos.setAttribute('class', 'col-6');
progressNeg.setAttribute('class', 'progress flex-row-reverse my-1');
progressPos.setAttribute('class', 'progress my-1');
progressBarNeg.setAttribute('class', 'progress-bar');
progressBarNeg.setAttribute('style', 'width:' + negWidth + '%');
progressBarPos.setAttribute('class', 'progress-bar');
progressBarPos.setAttribute('style', 'width:' + posWidth + '%');
progressBarNeg.appendChild(progressBarNegMessage);
progressBarPos.appendChild(progressBarPosMessage);
progressNeg.appendChild(progressBarNeg);
progressPos.appendChild(progressBarPos);
colNeg.appendChild(progressNeg);
colPos.appendChild(progressPos);
row.appendChild(colNeg);
row.appendChild(colPos);
element.appendChild(row);
});
return element;
}