Initial commit of DirAct app

pull/1133/head
jeffyactive 2021-12-21 22:08:05 -05:00
parent 0dce0afbbe
commit 6cb5bd5da9
6 changed files with 595 additions and 0 deletions

View File

@ -5062,5 +5062,22 @@
{"name":"ltherm.app.js","url":"app.js"},
{"name":"ltherm.img","url":"icon.js","evaluate":true}
]
},
{
"id": "diract",
"name": "DirAct",
"shortName": "DirAct",
"version": "0.01",
"description": "Proximity interaction detection.",
"icon": "diract.png",
"type": "app",
"tags": "tool,sensors",
"supports" : [ "BANGLEJS2" ],
"allow_emulator": false,
"readme": "README.md",
"storage": [
{ "name": "diract.app.js", "url": "diract.js" },
{ "name": "diract.img", "url": "diract-icon.js", "evaluate": true }
]
}
]

1
apps/diract/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App!

28
apps/diract/README.md Normal file
View File

@ -0,0 +1,28 @@
# DirAct
[DirAct](https://www.reelyactive.com/diract/) implementation for the Bangle.js.
## Usage
Real-time interactions will be recognised by [Pareto Anywhere](https://www.reelyactive.com/pareto/anywhere/) open source middleware and any other program which observes the [DirAct open standard](https://reelyactive.github.io/diract/).
## Features
Currently implements DirAct real-time functionality.
## Controls
None.
## Requests
[Contact reelyActive](https://www.reelyactive.com/contact/) for support/updates.
## Creator
Developed by [jeffyactive](https://github.com/jeffyactive) of [reelyActive](https://www.reelyactive.com). DirAct is jointly developed by reelyActive and Code Blue Consulting.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkE/4AImUQgMjBpIAI+UQFAMBn4XRmJBDiYXRFwQwCC9HzF93/kAXDgSPWj4XRMAZGSDAUhiTWSAH4Ag+URAAUvRBkSAofxgUzmchX4khSw3ygIID+UiFwMiF4bIBGowIBiYvEmUjF4kxEwgABmU/mMCC4cBiUhiAXDkET+cjC4cgj5IE+MAI4MAC4RGCHQIXEgRREF44kCCIIeCDoIIBMAYkDHQJeDEwJBBn/xgYGBn8hC5cS+YoBmEfFoP/IoMxgYXJmETJIISBj/zgBOBDgITCSgYXDBoYUCDQIXBiYXNBIIXBA4IXHI44XEJIJHIC5JHEVwRkBO5qKBbYqPGgMikTXDmKPDEAPyAIJJCX4cAAAQXDIoQtBl6IEGIIXJFoYmCSAQ4BSYIXJRYJWBDQIACkM/eYQXJdYcSC4fzG4J2CC5MwK4I+CAAR4BLwQXJBwowDiQfDC5HzLAIXFGwoXILAQALC5IANC/4X/C+w"))

548
apps/diract/diract.js Normal file
View File

@ -0,0 +1,548 @@
/**
* Copyright reelyActive 2017-2021
* We believe in an open Internet of Things
*
* DirAct is jointly developed by reelyActive and Code Blue Consulting
*/
// User-configurable constants
const INSTANCE_ID = [ 0x00, 0x00, 0x00, 0x01 ];
const NAMESPACE_FILTER_ID = [ 0xc0, 0xde, 0xb1, 0x0e, 0x1d,
0xd1, 0xe0, 0x1b, 0xed, 0x0c ];
const EXCITER_INSTANCE_IDS = new Uint32Array([ 0xe8c17e45 ]);
const RESETTER_INSTANCE_IDS = new Uint32Array([ 0x4e5e77e4 ]);
const PROXIMITY_RSSI_THRESHOLD = -65;
const PROXIMITY_LED_RSSI_THRESHOLD = -65;
const PROXIMITY_TABLE_SIZE = 8;
const DIGEST_TABLE_SIZE = 32;
const OBSERVE_PERIOD_MILLISECONDS = 400;
const BROADCAST_PERIOD_MILLISECONDS = 3600;
const BROADCAST_DIGEST_PAGE_MILLISECONDS = 400;
const PROXIMITY_PACKET_INTERVAL_MILLISECONDS = 400;
const DIGEST_PACKET_INTERVAL_MILLISECONDS = 100;
const DIGEST_TIME_CYCLE_THRESHOLD = 86400;
const EXCITER_HOLDOFF_SECONDS = 60;
const BLINK_ON_PROXIMITY = true;
const BLINK_ON_DISTANCING = true;
const BLINK_ON_DIGEST = true;
const BLINK_ON_RESET = true;
// Eddystone protocol constants
const EDDYSTONE_UUID = 'feaa';
const EDDYSTONE_UID_FRAME = 0x00;
const EDDYSTONE_NAMESPACE_OFFSET = 2;
const EDDYSTONE_NAMESPACE_LENGTH = 10;
const EDDYSTONE_INSTANCE_OFFSET = 14;
// DirAct constants
const DIRACT_MANUFACTURER_ID = 0x0583; // Code Blue Consulting
const DIRACT_PROXIMITY_FRAME = 0x01;
const DIRACT_DIGEST_FRAME = 0x11;
const DIRACT_DEFAULT_COUNT_LENGTH = 0x07;
const DIRACT_INSTANCE_LENGTH = 4;
const DIRACT_INSTANCE_OFFSET = 2;
const MAX_NUMBER_STRONGEST = 3;
const MAX_BATTERY_VOLTAGE = 3.3;
const MIN_BATTERY_VOLTAGE = 3.0;
const MAX_RSSI_TO_ENCODE = -28;
const MIN_RSSI_TO_ENCODE = -92;
const MAX_ACCELERATION_TO_ENCODE = 2;
const MAX_ACCELERATION_MAGNITUDE = 0x1f;
const INVALID_ACCELERATION_CODE = 0x20;
const SCAN_OPTIONS = {
filters: [
{ manufacturerData: { 0x0583: {} } },
{ services: [ EDDYSTONE_UUID ] }
]
};
// Other constants
const BITS_PER_BYTE = 8;
const DUMMY_INSTANCE_ID = 0;
const DUMMY_RSSI = MIN_RSSI_TO_ENCODE;
// Global variables
let proximityInstances = new Uint32Array(PROXIMITY_TABLE_SIZE);
let proximityRssis = new Int8Array(PROXIMITY_TABLE_SIZE);
let digestInstances = new Uint32Array(DIGEST_TABLE_SIZE);
let digestCounts = new Uint16Array(DIGEST_TABLE_SIZE);
let digestTime = new Uint8Array([ 0, 0, 0 ]);
let numberOfDigestPages = 0;
let sensorData = [ 0x82, 0x08, 0x3f ];
let cyclicCount = 0;
let lastDigestTime = Math.round(getTime());
let lastResetTime = Math.round(getTime());
let isExciterPresent = false;
let isResetterPresent = false;
let isProximityDetected = false;
let menu = {
"": { "title": "-- DirAct --" }
};
/**
* Initiate observer mode, scanning for devices in proximity.
*/
function observe() {
proximityInstances.fill(DUMMY_INSTANCE_ID); // Reset proximity
proximityRssis.fill(DUMMY_RSSI); // table data
isExciterPresent = false;
isResetterPresent = false;
NRF.setScan(handleDiscoveredDevice, SCAN_OPTIONS); // Start scanning
setTimeout(broadcast, OBSERVE_PERIOD_MILLISECONDS); // ...until period end
}
/**
* Compile the scan results and initiate broadcaster mode, advertising either
* proximity or digest packets, in consequence.
*/
function broadcast() {
NRF.setScan(); // Stop scanning
let sortedProximityIndices = getSortedIndices(proximityRssis);
updateDigestTable(sortedProximityIndices);
updateSensorData();
let currentTime = Math.round(getTime());
let isExcited = isExciterPresent &&
((currentTime - lastDigestTime) > EXCITER_HOLDOFF_SECONDS);
if(isResetterPresent) {
if(BLINK_ON_RESET) {
Bangle.setLCDPower(true);
}
lastResetTime = currentTime;
resetDigest();
broadcastProximity(sortedProximityIndices);
}
else if(isExcited) {
let sortedDigestIndices = getSortedIndices(digestCounts);
compileDigest();
broadcastDigest(sortedDigestIndices, 0);
}
else {
broadcastProximity(sortedProximityIndices);
}
}
/**
* Initiate broadcaster mode advertising proximity packets.
* @param {TypedArray} sortedIndices The sorted proximity table indices.
*/
function broadcastProximity(sortedIndices) {
let advertisingOptions = {
interval: PROXIMITY_PACKET_INTERVAL_MILLISECONDS,
showName: false,
manufacturer: DIRACT_MANUFACTURER_ID
};
advertisingOptions.manufacturerData = compileProximityData(sortedIndices);
NRF.setAdvertising({}, advertisingOptions); // Start advertising
setTimeout(observe, BROADCAST_PERIOD_MILLISECONDS); // ...until period end
}
/**
* Initiate broadcaster mode advertising digest packets.
* @param {TypedArray} sortedIndices The sorted digest table indices.
* @param {Number} pageNumber The page number to broadcast.
*/
function broadcastDigest(sortedIndices, pageNumber) {
let isLastPage = (pageNumber === (numberOfDigestPages - 1));
let advertisingOptions = {
interval: DIGEST_PACKET_INTERVAL_MILLISECONDS,
showName: false,
manufacturer: DIRACT_MANUFACTURER_ID
};
advertisingOptions.manufacturerData = compileDigestData(sortedIndices,
pageNumber);
NRF.setAdvertising({}, advertisingOptions); // Start advertising
if(isLastPage) {
setTimeout(observe, BROADCAST_DIGEST_PAGE_MILLISECONDS);
if(getTime() > DIGEST_TIME_CYCLE_THRESHOLD) {
lastResetTime = Math.round(getTime());
resetDigest();
}
if(BLINK_ON_DIGEST) {
Bangle.setLCDPower(true);
}
}
else {
setTimeout(broadcastDigest, BROADCAST_DIGEST_PAGE_MILLISECONDS,
sortedIndices, ++pageNumber);
}
}
/**
* Handle the given device discovered on scan and process further if
* Eddystone-UID or DirAct.
* @param {BluetoothDevice} device The discovered device.
*/
function handleDiscoveredDevice(device) {
let isEddystone = (device.hasOwnProperty('services') &&
device.hasOwnProperty('serviceData') &&
(device.services[0] === EDDYSTONE_UUID));
let isManufacturer = (device.hasOwnProperty('manufacturer') &&
device.manufacturer === DIRACT_MANUFACTURER_ID);
if(isEddystone) {
let isEddystoneUID = (device.serviceData[EDDYSTONE_UUID][0] ===
EDDYSTONE_UID_FRAME);
if(isEddystoneUID) {
handleEddystoneUidDevice(device.serviceData[EDDYSTONE_UUID], device.rssi);
}
}
else if(isManufacturer) {
let isDirAct = ((device.manufacturerData[0] === DIRACT_PROXIMITY_FRAME) ||
(device.manufacturerData[0] === DIRACT_DIGEST_FRAME));
if(isDirAct) {
handleDirActDevice(device.manufacturerData, device.rssi);
}
}
}
/**
* Handle the given Eddystone-UID device, adding to the devices in range if
* it meets the filter criteria.
* @param {Array} serviceData The Eddystone service data.
* @param {Number} rssi The received signal strength.
*/
function handleEddystoneUidDevice(serviceData, rssi) {
for(let cByte = 0; cByte < EDDYSTONE_NAMESPACE_LENGTH; cByte++) {
let namespaceIndex = EDDYSTONE_NAMESPACE_OFFSET + cByte;
if(serviceData[namespaceIndex] !== NAMESPACE_FILTER_ID[cByte]) {
return;
}
}
let instanceId = 0;
let bitShift = (DIRACT_INSTANCE_LENGTH - 1) * BITS_PER_BYTE;
for(let cByte = 0; cByte < DIRACT_INSTANCE_LENGTH; cByte++) {
let instanceByte = serviceData[EDDYSTONE_INSTANCE_OFFSET + cByte];
instanceId += instanceByte << bitShift;
bitShift -= BITS_PER_BYTE;
}
let unsignedInstanceId = new Uint32Array([instanceId])[0];
if(EXCITER_INSTANCE_IDS.indexOf(unsignedInstanceId) >= 0) {
isExciterPresent = true;
}
else if(RESETTER_INSTANCE_IDS.indexOf(unsignedInstanceId) >= 0) {
isResetterPresent = true;
}
else {
updateProximityTable(instanceId, rssi);
}
}
/**
* Handle the given DirAct device, adding to the devices in range if
* it meets the filter criteria.
* @param {Array} manufacturerData The DirAct manufacturer data.
* @param {Number} rssi The received signal strength.
*/
function handleDirActDevice(manufacturerData, rssi) {
let instanceId = 0;
let bitShift = (DIRACT_INSTANCE_LENGTH - 1) * BITS_PER_BYTE;
for(let cByte = DIRACT_INSTANCE_OFFSET;
cByte < DIRACT_INSTANCE_OFFSET + DIRACT_INSTANCE_LENGTH; cByte++) {
let instanceByte = manufacturerData[cByte];
instanceId += instanceByte << bitShift;
bitShift -= BITS_PER_BYTE;
}
updateProximityTable(instanceId, rssi);
}
/**
* Update the proximity table with the given instance's RSSI. If the instance
* already exists, combine RSSI values in a weighted average.
* @param {String} instanceId The DirAct 4-byte instance id as a 32-bit integer.
* @param {Number} rssi The received signal strength.
*/
function updateProximityTable(instanceId, rssi) {
let instanceIndex = proximityInstances.indexOf(instanceId);
let isNewInstance = (instanceIndex < 0);
if(isNewInstance) {
let nextIndex = proximityInstances.indexOf(DUMMY_INSTANCE_ID);
if(nextIndex >= 0) {
proximityInstances[nextIndex] = instanceId;
proximityRssis[nextIndex] = rssi;
}
}
else {
proximityRssis[instanceIndex] = (proximityRssis[instanceIndex] + rssi) / 2;
}
}
/**
* Update the digest table based on the proximity table and its sorted indices.
* @param {TypedArray} sortedIndices The sorted proximity table indices.
*/
function updateDigestTable(sortedIndices) {
for(let cInstance = 0; cInstance < PROXIMITY_TABLE_SIZE; cInstance++) {
let proximityIndex = sortedIndices[cInstance];
if(proximityRssis[proximityIndex] >= PROXIMITY_RSSI_THRESHOLD) {
let instanceId = proximityInstances[proximityIndex];
let instanceIndex = digestInstances.indexOf(instanceId);
let isNewInstance = (instanceIndex < 0);
if(isNewInstance) {
let nextIndex = digestInstances.indexOf(DUMMY_INSTANCE_ID);
if(nextIndex >= 0) {
digestInstances[nextIndex] = instanceId;
digestCounts[nextIndex] = 1;
}
}
else if(digestCounts[instanceIndex] < 65535) {
digestCounts[instanceIndex]++;
}
}
else {
cInstance = PROXIMITY_TABLE_SIZE; // Break
}
}
}
/*
* Compile the digest from the digest table.
*/
function compileDigest() {
let numberOfEntries = digestCounts.findIndex(count => count === 0);
let currentTime = Math.round(getTime());
let elapsedTime = currentTime - lastResetTime;
if(numberOfEntries < 0) {
numberOfEntries = DIGEST_TABLE_SIZE;
}
digestTime[0] = (elapsedTime >> 16) & 0xff;
digestTime[1] = (elapsedTime >> 8) & 0xff;
digestTime[2] = elapsedTime & 0xff;
numberOfDigestPages = Math.max(1, Math.min(8, Math.ceil(numberOfEntries/3)));
lastDigestTime = currentTime;
}
/*
* Clear the digest table.
*/
function resetDigest() {
digestInstances.fill(DUMMY_INSTANCE_ID);
digestCounts.fill(0);
}
/**
* Compile the DirAct proximity data.
* @param {TypedArray} sortedIndices The sorted proximity table indices.
*/
function compileProximityData(sortedIndices) {
let data = [
DIRACT_PROXIMITY_FRAME, DIRACT_DEFAULT_COUNT_LENGTH,
INSTANCE_ID[0], INSTANCE_ID[1], INSTANCE_ID[2], INSTANCE_ID[3],
sensorData[0], sensorData[1], sensorData[2]
];
let isNewProximityDetected = false;
for(let cInstance = 0; cInstance < MAX_NUMBER_STRONGEST; cInstance++) {
let index = sortedIndices[cInstance];
if(proximityRssis[index] >= PROXIMITY_RSSI_THRESHOLD) {
let instanceId = proximityInstances[index];
data.push((instanceId >> 24) & 0xff, (instanceId >> 16) & 0xff,
(instanceId >> 8) & 0xff, instanceId & 0xff,
encodeRssi(proximityRssis[index]));
if(proximityRssis[index] >= PROXIMITY_LED_RSSI_THRESHOLD) {
isNewProximityDetected = true;
}
}
else {
cInstance = PROXIMITY_TABLE_SIZE; // Break
}
}
cyclicCount = (cyclicCount + 1) % 8;
data[1] = (cyclicCount << 5) + (data.length - 2);
if(isProximityDetected && !isNewProximityDetected && BLINK_ON_DISTANCING) {
Bangle.setLCDPower(true);
}
else if(isNewProximityDetected && BLINK_ON_PROXIMITY) {
Bangle.setLCDPower(true);
}
isProximityDetected = isNewProximityDetected;
return data;
}
/**
* Compile the DirAct digest data.
* @param {TypedArray} sortedIndices The sorted digest table indices.
* @param {Number} digestPage The page of the digest to compile.
*/
function compileDigestData(sortedIndices, digestPage) {
let isLastPage = (digestPage === (numberOfDigestPages - 1));
let digestStatus = digestTime[0] & 0x7f;
if(isLastPage) {
digestStatus |= 0x80;
}
let data = [
DIRACT_DIGEST_FRAME, DIRACT_DEFAULT_COUNT_LENGTH,
INSTANCE_ID[0], INSTANCE_ID[1], INSTANCE_ID[2], INSTANCE_ID[3],
digestStatus, digestTime[1], digestTime[2]
];
let pageIndex = digestPage * 3;
for(let cInstance = pageIndex; cInstance < (pageIndex + 3); cInstance++) {
let index = sortedIndices[cInstance];
if(digestCounts[index] > 0) {
let instanceId = digestInstances[index];
let encodedCount = digestCounts[index];
if(encodedCount > 127) {
encodedCount = 0x80 | (Math.min((encodedCount >> 8), 0x7f) & 0x7f);
}
data.push((instanceId >> 24) & 0xff, (instanceId >> 16) & 0xff,
(instanceId >> 8) & 0xff, instanceId & 0xff,
encodedCount);
}
else {
cInstance = pageIndex + 3; // Break
}
}
data[1] = (digestPage << 5) + (data.length - 2);
return data;
}
/**
* Encode the given RSSI.
* @param {Number} rssi The given RSSI.
* @return {Number} The encoded RSSI.
*/
function encodeRssi(rssi) {
rssi = Math.round(rssi);
if(rssi >= MAX_RSSI_TO_ENCODE) {
return 0x3f;
}
if(rssi <= MIN_RSSI_TO_ENCODE) {
return 0x00;
}
return rssi - MIN_RSSI_TO_ENCODE;
}
/**
* Encode the battery percentage.
* @return {Number} The battery percentage.
*/
function encodeBatteryPercentage() {
let voltage = NRF.getBattery();
if(voltage <= MIN_BATTERY_VOLTAGE) {
return 0x00;
}
if(voltage >= MAX_BATTERY_VOLTAGE) {
return 0x3f;
}
return Math.round(0x3f * (voltage - MIN_BATTERY_VOLTAGE) /
(MAX_BATTERY_VOLTAGE - MIN_BATTERY_VOLTAGE));
}
/**
* Encode the acceleration.
* @return {Array} The encoded acceleration [ x, y, z ].
*/
function encodeAcceleration() {
let encodedAcceleration = { x: INVALID_ACCELERATION_CODE,
y: INVALID_ACCELERATION_CODE,
z: INVALID_ACCELERATION_CODE };
let acceleration = { x: Bangle.getAccel().x,
y: Bangle.getAccel().y,
z: Bangle.getAccel().z };
for(let axis in acceleration) {
let magnitude = acceleration[axis];
let encodedMagnitude = Math.min(MAX_ACCELERATION_MAGNITUDE,
Math.round(MAX_ACCELERATION_MAGNITUDE *
(Math.abs(magnitude) /
MAX_ACCELERATION_TO_ENCODE)));
if(magnitude < 0) {
encodedMagnitude = 0x3f - encodedMagnitude;
}
encodedAcceleration[axis] = encodedMagnitude;
}
return encodedAcceleration;
}
/**
* Update the sensor data (battery & acceleration) for the advertising packet.
*/
function updateSensorData() {
// Update the battery measurement each time the cyclic count resets
if(cyclicCount === 0) {
encodedBattery = encodeBatteryPercentage();
}
encodedAcceleration = encodeAcceleration();
sensorData[0] = ((encodedAcceleration.x << 2) & 0xfc) |
((encodedAcceleration.y >> 4) & 0x3f);
sensorData[1] = ((encodedAcceleration.y << 4) & 0xf0) |
((encodedAcceleration.z >> 2) & 0x0f);
sensorData[2] = ((encodedAcceleration.z << 6) & 0xc0) |
(encodedBattery & 0x3f);
}
/**
* Determine the sorted order of the indices of the given array.
* @return {Uint8Array} The array of indices sorted in descending order.
*/
function getSortedIndices(unsortedArray) {
let sortedIndices = new Uint8Array(unsortedArray.length);
sortedIndices.forEach((value, index) => sortedIndices[index] = index);
sortedIndices.sort((a, b) => unsortedArray[b] - unsortedArray[a]);
return sortedIndices;
}
// On start: begin DirAct operation and display the menu
g.clear();
E.showMenu(menu);
observe();

BIN
apps/diract/diract.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB