// get settings var settings = require("Storage").readJSON("vernierrespirate.json",1)||{}; settings.vibrateBPM = settings.vibrateBPM||27; // settings.vibrate; // undefined / "calculated" / "vernier" function saveSettings() { require("Storage").writeJSON("vernierrespirate.json", settings); } g.clear(); var graphHeight = g.getHeight()-100; var last = { time : Date.now(), x : 0, y : 24, }; var avrValue; var aboveAvr = false; var lastBreath; var lastBreaths = []; var vibrateInterval; function onMsg(txt) { print(txt); E.showMessage(txt); } function setVibrate(isOn) { var wasOn = vibrateInterval!==undefined; if (isOn == wasOn) return; if (isOn) { vibrateInterval = setInterval(function() { Bangle.buzz(); }, 1000); } else { clearInterval(vibrateInterval); vibrateInterval = undefined; } } function onBreath() { var t = Date.now(); if (lastBreath!==undefined) { // time between breaths var value = 60000 / (t-lastBreath); // average of last 3 while (lastBreaths.length>=3) lastBreaths.shift(); // keep length small lastBreaths.push(value); value = E.sum(lastBreaths) / lastBreaths.length; // draw value g.reset(); g.clearRect(0,g.getHeight()-100,g.getWidth(),g.getHeight()-50); g.setFont("6x8").setFontAlign(0,0); g.drawString("Calculated measurement", g.getWidth()/2, g.getHeight()-95); g.setFont("Vector",40).setFontAlign(0,0); g.drawString(value.toFixed(2), g.getWidth()/2, g.getHeight()-70); // set vibration IF we're doing it from our calculations if (settings.vibrate == "calculated") setVibrate(value > settings.vibrateBPM); } lastBreath = t; } function onData(n, value) { g.reset(); if (n==2) { function scale(v) { return Math.max(graphHeight - (1+v*4),24); } if (avrValue==undefined) avrValue=value; avrValue = avrValue*0.95 + value*0.05; if (avrValue < 1) avrValue = 1; if (value > avrValue) { if (!aboveAvr) onBreath(); aboveAvr = true; } else aboveAvr = false; var t = Date.now(); var x = Math.round((t - last.time) / 100) // 10 per second if (last.x>=g.getWidth()) { x = 0; last.x = 0; last.time = t; g.clearRect(0,24,g.getWidth(),graphHeight); } var y = scale(value); g.setPixel(x, scale(avrValue), "#f00"); g.drawLine(last.x, last.y, x, y); last.x = x; last.y = y; } if (n==4) { g.clearRect(0,g.getHeight()-50,g.getWidth(),g.getHeight()); g.setFont("6x8").setFontAlign(0,0); g.drawString("GoDirect measurement", g.getWidth()/2, g.getHeight()-45); g.setFont("Vector",40).setFontAlign(0,0); g.drawString(value.toFixed(2), g.getWidth()/2, g.getHeight()-20); // set vibration IF we're doing it from our calculations if (settings.vibrate == "vernier") setVibrate(value > settings.vibrateBPM); } Bangle.setLCDPower(1); // ensure LCD is on } function connect() { var gatt, service, rx, tx; var rollingCounter = 0xFF; // any button to exit Bangle.setUI("updown", function() { setVibrate(false); Bangle.buzz(); try { if (gatt) gatt.disconnect(); } catch (e) { } setTimeout(mainMenu, 1000); }); function sendCommand(subCommand) { const command = new Uint8Array(4 + subCommand.length); command.set(new Uint8Array(subCommand), 4); // Populate the packet header bytes command[0] = 0x58; // header command[1] = command.length; command[2] = --rollingCounter; command[3] = E.sum(command) & 0xFF; // checksum return tx.writeValue(command); } function firstSetBit(v) { return v & -v; } function handleResponse(dv) { //print(dv.buffer); var resType = dv.getUint8(0); if (resType==0x20) { // [32, 25, 207, 216, 6, 6, 0, 2, 252, 128, 138, 7, 191, 0, 0, 192, 127, 128, 49, 8, 191, 0, 0, 192, 127]) // 6 = data type = real // 6,0 = bit mask for sensors // 2 = value count if (dv.getUint8(4)!=6) return; //throw "Not float32 data"; var sensorIds = dv.getUint16(5, true); // var count = dv.getUint8(7); doesn't seem right var offs = 9; while (sensorIds) { var value = dv.getFloat32(offs, true); var s = firstSetBit(sensorIds); if (isFinite(value)) onData(s,value); //else print(s,value); sensorIds &= ~s; offs += 4; } } else { /*var cmd =*/ dv.getUint8(4); // cmd //print("CMD",dv.buffer); } } onMsg("Searching..."); NRF.requestDevice({ filters: [{ namePrefix: 'GDX-RB' }] }).then(function(device) { device.on("gattserverdisconnected", function() { onMsg("Device disconnected"); }); onMsg("Found. Connecting..."); return device.gatt.connect({minInterval:20, maxInterval:20}); }).then(function(g) { gatt = g; return gatt.getPrimaryService("d91714ef-28b9-4f91-ba16-f0d9a604f112"); }).then(function(s) { service = s; return service.getCharacteristic("f4bf14a6-c7d5-4b6d-8aa8-df1a7c83adcb"); }).then(function(c) { tx = c; return service.getCharacteristic("b41e6675-a329-40e0-aa01-44d2f444babe"); }).then(function(c) { rx = c; rx.on('characteristicvaluechanged', function(event) { //print("EVT",event.target.value.buffer); handleResponse(event.target.value); }); return rx.startNotifications(); }).then(function() { onMsg("Init"); sendCommand([ // init 0x1a, 0xa5, 0x4a, 0x06, 0x49, 0x07, 0x48, 0x08, 0x47, 0x09, 0x46, 0x0a, 0x45, 0x0b, 0x44, 0x0c, 0x43, 0x0d, 0x42, 0x0e, 0x41, ]); /*setTimeout(function() { print("Set measurement period"); var us = 100000; // period in us sendCommand([0x1b, 0xff, 0x00, us & 255, (us >> 8) & 255, (us >> 16) & 255, (us >> 24) & 255, 0x00, 0x00, 0x00, 0x00]); }, 100);*/ /* setTimeout(function() { print("Get sensor info"); sendCommand([0x51, 0]); // get sensor IDs // returns [152, 10, 1, 39, 81, 253, 54, 0, 0, 0] // 54 is the bit mask of available channels //sendCommand([106, 16]); // get sensor info }, 2000);*/ setTimeout(function() { onMsg("Start measurements"); //https://github.com/VernierST/godirect-js/blob/main/src/Device.js#L588 var channels = 6; // data channels 4 and 2 sendCommand([ // start measurements 0x18, 0xff, 0x01, channels, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]); }, 500); }).catch(function() { onMsg("Connect Fail"); }); } Bangle.loadWidgets(); Bangle.drawWidgets(); function mainMenu() { var vibText = ["No","Calculated","Vernier"]; var vibValue = ["","calculated","vernier"]; E.showMenu({"":{title:"Respiration Belt"}, "< Back" : () => { saveSettings(); load(); }, "Connect" : () => { saveSettings(); E.showMenu(); connect(); }, "Vib" : { value : Math.max(vibValue.indexOf(settings.vibrate),0), format : v => vibText[v], min:0,max:2, onchange : v => { settings.vibrate=vibValue[v]; } }, "BPM" : { value : settings.vibrateBPM, min:10,max:50, onchange : v => { settings.vibrateBPM=v; } } }); } mainMenu();