Merge pull request #1133 from reelyactive/master
Refactored for efficiency, corrected sensor value inaccuraciesmaster
commit
22420ea269
19
apps.json
19
apps.json
|
|
@ -4652,7 +4652,7 @@
|
|||
"id": "sensible",
|
||||
"name": "SensiBLE",
|
||||
"shortName": "SensiBLE",
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "Collect, display and advertise real-time sensor data.",
|
||||
"icon": "sensible.png",
|
||||
"screenshots": [
|
||||
|
|
@ -5063,5 +5063,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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"))
|
||||
|
|
@ -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();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -2,3 +2,4 @@
|
|||
0.02: Corrected variable initialisation
|
||||
0.03: Advertise app name, added screenshots
|
||||
0.04: Advertise bar, GPS, HRM and mag services
|
||||
0.05: Refactored for efficiency, corrected sensor value inaccuracies
|
||||
|
|
@ -124,27 +124,16 @@ function transmitUpdatedSensorData() {
|
|||
isNewMagData = false;
|
||||
}
|
||||
|
||||
NRF.setAdvertising(data, { showName: false, interval: 200 });
|
||||
let interval = 1000 / data.length;
|
||||
NRF.setAdvertising(data, { showName: false, interval: interval });
|
||||
}
|
||||
|
||||
|
||||
// Encode the bar service data to fit in a Bluetooth PDU
|
||||
function encodeBarServiceData() {
|
||||
let tEncoded = Math.round(bar.temperature * 100);
|
||||
let pEncoded = Math.round(bar.pressure * 100);
|
||||
let eEncoded = Math.round(bar.altitude * 100);
|
||||
|
||||
if(bar.temperature < 0) {
|
||||
tEncoded += 0x10000;
|
||||
}
|
||||
if(bar.altitude < 0) {
|
||||
eEncoded += 0x1000000;
|
||||
}
|
||||
|
||||
let t = [ tEncoded & 0xff, (tEncoded >> 8) & 0xff ];
|
||||
let p = [ pEncoded & 0xff, (pEncoded >> 8) & 0xff, (pEncoded >> 16) & 0xff,
|
||||
(pEncoded >> 24) & 0xff ];
|
||||
let e = [ eEncoded & 0xff, (eEncoded >> 8) & 0xff, (eEncoded >> 16) & 0xff ];
|
||||
let t = toByteArray(Math.round(bar.temperature * 100), 2, true);
|
||||
let p = toByteArray(Math.round(bar.pressure * 1000), 4, false);
|
||||
let e = toByteArray(Math.round(bar.altitude * 100), 3, true);
|
||||
|
||||
return [
|
||||
0x02, 0x01, 0x06, // Flags
|
||||
|
|
@ -157,52 +146,26 @@ function encodeBarServiceData() {
|
|||
|
||||
// Encode the GPS service data using the Location and Speed characteristic
|
||||
function encodeGpsServiceData() {
|
||||
let latEncoded = Math.round(gps.lat * 10000000);
|
||||
let lonEncoded = Math.round(gps.lon * 10000000);
|
||||
let hEncoded = Math.round(gps.course * 100);
|
||||
let sEncoded = Math.round(1000 * gps.speed / 36);
|
||||
|
||||
if(gps.lat < 0) {
|
||||
latEncoded += 0x100000000;
|
||||
}
|
||||
if(gps.lon < 0) {
|
||||
lonEncoded += 0x100000000;
|
||||
}
|
||||
|
||||
let s = [ sEncoded & 0xff, (sEncoded >> 8) & 0xff ];
|
||||
let lat = [ latEncoded & 0xff, (latEncoded >> 8) & 0xff,
|
||||
(latEncoded >> 16) & 0xff, (latEncoded >> 24) & 0xff ];
|
||||
let lon = [ lonEncoded & 0xff, (lonEncoded >> 8) & 0xff,
|
||||
(lonEncoded >> 16) & 0xff, (lonEncoded >> 24) & 0xff ];
|
||||
let h = [ hEncoded & 0xff, (hEncoded >> 8) & 0xff ];
|
||||
let s = toByteArray(Math.round(1000 * gps.speed / 36), 2, false);
|
||||
let lat = toByteArray(Math.round(gps.lat * 10000000), 4, true);
|
||||
let lon = toByteArray(Math.round(gps.lon * 10000000), 4, true);
|
||||
let e = toByteArray(Math.round(gps.alt * 100), 3, true);
|
||||
let h = toByteArray(Math.round(gps.course * 100), 2, false);
|
||||
|
||||
return [
|
||||
0x02, 0x01, 0x06, // Flags
|
||||
0x11, 0x16, 0x67, 0x2a, 0x95, 0x02, s[0], s[1], lat[0], lat[1], lat[2],
|
||||
lat[3], lon[0], lon[1], lon[2], lon[3], h[0], h[1] // Location and Speed
|
||||
0x02, 0x01, 0x06, // Flags
|
||||
0x14, 0x16, 0x67, 0x2a, 0x9d, 0x02, s[0], s[1], lat[0], lat[1], lat[2],
|
||||
lat[3], lon[0], lon[1], lon[2], lon[3], e[0], e[1], e[2], h[0], h[1]
|
||||
// Location and Speed
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// Encode the mag service data using the magnetic flux density 3D characteristic
|
||||
function encodeMagServiceData() {
|
||||
let xEncoded = mag.x; // TODO: units???
|
||||
let yEncoded = mag.y;
|
||||
let zEncoded = mag.z;
|
||||
|
||||
if(xEncoded < 0) {
|
||||
xEncoded += 0x10000;
|
||||
}
|
||||
if(yEncoded < 0) {
|
||||
yEncoded += 0x10000;
|
||||
}
|
||||
if(yEncoded < 0) {
|
||||
yEncoded += 0x10000;
|
||||
}
|
||||
|
||||
let x = [ xEncoded & 0xff, (xEncoded >> 8) & 0xff ];
|
||||
let y = [ yEncoded & 0xff, (yEncoded >> 8) & 0xff ];
|
||||
let z = [ zEncoded & 0xff, (zEncoded >> 8) & 0xff ];
|
||||
let x = toByteArray(mag.x, 2, true);
|
||||
let y = toByteArray(mag.y, 2, true);
|
||||
let z = toByteArray(mag.z, 2, true);
|
||||
|
||||
return [
|
||||
0x02, 0x01, 0x06, // Flags
|
||||
|
|
@ -211,6 +174,22 @@ function encodeMagServiceData() {
|
|||
}
|
||||
|
||||
|
||||
// Convert the given value to a little endian byte array
|
||||
function toByteArray(value, numberOfBytes, isSigned) {
|
||||
let byteArray = new Array(numberOfBytes);
|
||||
|
||||
if(isSigned && (value < 0)) {
|
||||
value += 1 << (numberOfBytes * 8);
|
||||
}
|
||||
|
||||
for(let index = 0; index < numberOfBytes; index++) {
|
||||
byteArray[index] = (value >> (index * 8)) & 0xff;
|
||||
}
|
||||
|
||||
return byteArray;
|
||||
}
|
||||
|
||||
|
||||
// Update acceleration
|
||||
Bangle.on('accel', function(newAcc) {
|
||||
acc = newAcc;
|
||||
|
|
|
|||
Loading…
Reference in New Issue