diff --git a/apps.json b/apps.json index c1f51f272..682c399c4 100644 --- a/apps.json +++ b/apps.json @@ -4649,7 +4649,7 @@ "id": "sensible", "name": "SensiBLE", "shortName": "SensiBLE", - "version": "0.03", + "version": "0.04", "description": "Collect, display and advertise real-time sensor data.", "icon": "sensible.png", "screenshots": [ diff --git a/apps/sensible/ChangeLog b/apps/sensible/ChangeLog index baa93f297..c50431f51 100644 --- a/apps/sensible/ChangeLog +++ b/apps/sensible/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Corrected variable initialisation 0.03: Advertise app name, added screenshots +0.04: Advertise bar, GPS, HRM and mag services diff --git a/apps/sensible/README.md b/apps/sensible/README.md index f79b61aea..fcff3b0f9 100644 --- a/apps/sensible/README.md +++ b/apps/sensible/README.md @@ -17,7 +17,7 @@ Currently implements: - Heart Rate Monitor - Magnetometer -in the menu display but NOT YET in Bluetooth Low Energy advertising (which will be implemented in a subsequent version). +in the menu display, and broadcasts all sensor data readings _except_ acceleration in Bluetooth Low Energy advertising packets as GATT characteristic services. ## Controls diff --git a/apps/sensible/sensible.js b/apps/sensible/sensible.js index 45852adab..3da39998e 100644 --- a/apps/sensible/sensible.js +++ b/apps/sensible/sensible.js @@ -7,6 +7,9 @@ // Non-user-configurable constants const APP_ID = 'sensible'; const ESPRUINO_COMPANY_CODE = 0x0590; +const APP_ADVERTISING_DATA = [ 0x12, 0xff, 0x90, 0x05, 0x7b, 0x6e, 0x61, 0x6d, + 0x65, 0x3a, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x62, + 0x6c, 0x65, 0x7d ]; // Global variables @@ -20,6 +23,12 @@ let isBarEnabled = true; let isGpsEnabled = true; let isHrmEnabled = true; let isMagEnabled = true; +let isNewAccData = false; +let isNewBarData = false; +let isNewGpsData = false; +let isNewHrmData = false; +let isNewMagData = false; + // Menus @@ -91,22 +100,121 @@ let magMenu = { }; -// Transmit the app name under the Espruino company code to facilitate discovery -function transmitAppName() { - let options = { - showName: false, - manufacturer: ESPRUINO_COMPANY_CODE, - manufacturerData: JSON.stringify({ name: APP_ID }), - interval: 2000 +// Check for new sensor data and update the advertising sequence +function transmitUpdatedSensorData() { + let data = [ APP_ADVERTISING_DATA ]; // Always advertise at least app name + + if(isNewBarData) { + data.push(encodeBarServiceData()); + isNewBarData = false; } - NRF.setAdvertising({}, options); + if(isNewGpsData && gps.lat && gps.lon) { + data.push(encodeGpsServiceData()); + isNewGpsData = false; + } + + if(isNewHrmData) { + data.push({ 0x2a37: [ 0, hrm.bpm ] }); + isNewHrmData = false; + } + + if(isNewMagData) { + data.push(encodeMagServiceData()); + isNewMagData = false; + } + + NRF.setAdvertising(data, { showName: false, interval: 200 }); +} + + +// 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 ]; + + return [ + 0x02, 0x01, 0x06, // Flags + 0x05, 0x16, 0x6e, 0x2a, t[0], t[1], // Temperature + 0x07, 0x16, 0x6d, 0x2a, p[0], p[1], p[2], p[3], // Pressure + 0x06, 0x16, 0x6c, 0x2a, e[0], e[1], e[2] // Elevation + ]; +} + + +// 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 ]; + + 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 + ]; +} + + +// 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 ]; + + return [ + 0x02, 0x01, 0x06, // Flags + 0x09, 0x16, 0xa1, 0x2a, x[0], x[1], y[0], y[1], z[0], z[1] // Mag 3D + ]; } // Update acceleration Bangle.on('accel', function(newAcc) { acc = newAcc; + isNewAccData = true; if(isAccMenu) { accMenu.x.value = acc.x.toFixed(2); @@ -119,6 +227,7 @@ Bangle.on('accel', function(newAcc) { // Update barometer Bangle.on('pressure', function(newBar) { bar = newBar; + isNewBarData = true; if(isBarMenu) { barMenu.Altitude.value = bar.altitude.toFixed(1) + 'm'; @@ -131,6 +240,7 @@ Bangle.on('pressure', function(newBar) { // Update GPS Bangle.on('GPS', function(newGps) { gps = newGps; + isNewGpsData = true; if(isGpsMenu) { gpsMenu.Lat.value = gps.lat.toFixed(4); @@ -145,6 +255,7 @@ Bangle.on('GPS', function(newGps) { // Update heart rate monitor Bangle.on('HRM', function(newHrm) { hrm = newHrm; + isNewHrmData = true; if(isHrmMenu) { hrmMenu.BPM.value = hrm.bpm; @@ -156,6 +267,7 @@ Bangle.on('HRM', function(newHrm) { // Update magnetometer Bangle.on('mag', function(newMag) { mag = newMag; + isNewMagData = true; if(isMagMenu) { magMenu.x.value = mag.x; @@ -169,9 +281,9 @@ Bangle.on('mag', function(newMag) { // On start: enable sensors and display main menu g.clear(); -transmitAppName(); Bangle.setBarometerPower(isBarEnabled, APP_ID); Bangle.setGPSPower(isGpsEnabled, APP_ID); Bangle.setHRMPower(isHrmEnabled, APP_ID); Bangle.setCompassPower(isMagEnabled, APP_ID); -E.showMenu(mainMenu); \ No newline at end of file +E.showMenu(mainMenu); +setInterval(transmitUpdatedSensorData, 1000); \ No newline at end of file