diff --git a/apps/sensortools/ChangeLog b/apps/sensortools/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/sensortools/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/sensortools/README.md b/apps/sensortools/README.md new file mode 100644 index 000000000..8b89add7c --- /dev/null +++ b/apps/sensortools/README.md @@ -0,0 +1,44 @@ +# Sensor tools + +This allows to simulate sensor behaviour for development purposes + + +## Per Sensor settings: + +enabled: + true or false +mode: + emulate: Completely craft events for this sensor + modify: Take existing events from real sensor and modify their data +name: + name of the emulation or modification mode +power: + emulate: Simulate Bangle._PWR changes, but do not call real power function + nop: Do nothing, ignore all power calls for this sensor but return true + passthrough: Just pass all power calls unmodified + on: Do not allow switching the sensor off, all calls are switching the real sensor on + +### HRM + +Modes: modify, emulate +Modification: + bpmtrippled: Multiply the bpm value of the original HRM values with 3 +Emulation: + sin: Calculate bpm changes by using sin + +### GPS + +Modes: emulate +Emulation: + staticfix: static complete fix with all values + route: A square route starting in the SW corner and moving SW->NW->NO->SW... + routeFuzzy: Roughly the same square as route, but with 100m seqments with some variaton in course + nofix: All values NaN but time,sattelites,fix and fix == 0 + changingfix: A fix with randomly changing values + +### Compass + +Modes: emulate +Emulation: + static: All values but heading are 1, heading == 0 + rotate: All values but heading are 1, heading rotates 360° diff --git a/apps/sensortools/boot.js b/apps/sensortools/boot.js new file mode 100644 index 000000000..82c2036a9 --- /dev/null +++ b/apps/sensortools/boot.js @@ -0,0 +1,351 @@ +(function() { + var settings = Object.assign( + require('Storage').readJSON("sensortools.default.json", true) || {}, + require('Storage').readJSON("sensortools.json", true) || {} + ); + + var log = function(text, param) { + var logline = new Date().toISOString() + " - " + "Sensortools - " + text; + if (param) logline += ": " + JSON.stringify(param); + print(logline); + }; + + if (settings.enabled) { + + log("Enabled"); + const POWER_DELAY = 10000; + + var onEvents = []; + + Bangle.sensortoolsOrigOn = Bangle.on; + Bangle.sensortoolsOrigEmit = Bangle.emit; + Bangle.sensortoolsOrigRemoveListener = Bangle.removeListener; + + Bangle.on = function(name, callback) { + if (onEvents[name]) { + log("Redirecting listener for", name, "to", name + "_mod"); + Bangle.sensortoolsOrigOn(name + "_mod", callback); + Bangle.sensortoolsOrigOn(name, (e) => { + log("Redirected event for", name, "to", name + "_mod"); + Bangle.sensortoolsOrigEmit(name + "_mod", onEvents[name](e)); + }); + } else { + log("Pass through on call for", name, callback); + Bangle.sensortoolsOrigOn(name, callback); + } + }; + + Bangle.removeListener = function(name, callback) { + if (onEvents[name]) { + log("Removing augmented listener for", name, onEvents[name]); + Bangle.sensortoolsOrigRemoveListener(name + "_mod", callback); + } else { + log("Pass through remove listener for", name); + Bangle.sensortoolsOrigRemoveListener(name, callback); + } + }; + + Bangle.emit = function(name, event) { + if (onEvents[name]) { + log("Augmenting emit call for", name, onEvents[name]); + Bangle.sensortoolsOrigEmit(name + "_mod", event); + } else { + log("Pass through emit call for", name); + Bangle.sensortoolsOrigEmit(name, event); + } + }; + + var createPowerFunction = function(type, name, origPower) { + return function(isOn, app) { + if (type == "nop") { + return true; + }else if (type == "delay") { + setTimeout(() => { + origPower(isOn, app); + }, POWER_DELAY); + } else if (type == "on") { + origPower(1, "sensortools_force_on"); + } else if (type == "passthrough"){ + origPower(isOn, "app"); + } else if (type == "emulate"){ + if (!Bangle._PWR) Bangle._PWR={}; + if (!Bangle._PWR[name]) Bangle._PWR[name] = []; + if (!app) app="?"; + if (isOn) { + Bangle._PWR[name].push(app); + return true; + } else { + Bangle._PWR[name] = Bangle._PWR[name].filter((v)=>{return v == app;}); + return false; + } + } + }; + }; + + if (settings.hrm && settings.hrm.enabled) { + log("HRM", settings.hrm); + if (settings.hrm.power) { + log("HRM power"); + Bangle.sensortoolsOrigSetHRMPower = Bangle.setHRMPower; + Bangle.setHRMPower = createPowerFunction(settings.hrm.power, "HRM", Bangle.sensortoolsOrigSetHRMPower); + } + if (settings.hrm.mode == "modify") { + if (settings.hrm.name == "bpmtrippled") { + onEvents.HRM = (e) => { + return { + bpm: e.bpm * 3 + }; + }; + } + } else if (settings.hrm.mode == "emulate") { + if (settings.hrm.name == "sin") { + setInterval(() => { + Bangle.sensortoolsOrigEmit(60 + 3 * Math.sin(Date.now() / 10000)); + }, 1000); + } + } + } + if (settings.gps && settings.gps.enabled) { + log("GPS", settings.gps); + let modGps = function(dataProvider) { + Bangle.getGPSFix = dataProvider; + setInterval(() => { + Bangle.sensortoolsOrigEmit("GPS", dataProvider()); + }, 1000); + }; + if (settings.gps.power) { + Bangle.sensortoolsOrigSetGPSPower = Bangle.setGPSPower; + Bangle.setGPSPower = createPowerFunction(settings.gps.power, "GPS", Bangle.sensortoolsOrigSetGPSPower); + } + if (settings.gps.mode == "emulate") { + function radians(a) { + return a*Math.PI/180; + } + + function degrees(a) { + var d = a*180/Math.PI; + return (d+360)%360; + } + + function bearing(a,b){ + if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity; + var delta = radians(b.lon-a.lon); + var alat = radians(a.lat); + var blat = radians(b.lat); + var y = Math.sin(delta) * Math.cos(blat); + var x = Math.cos(alat)*Math.sin(blat) - + Math.sin(alat)*Math.cos(blat)*Math.cos(delta); + return Math.round(degrees(Math.atan2(y, x))); + } + + function interpolate(a,b,progress){ + return { + lat: a.lat * progress + b.lat * (1-progress), + lon: a.lon * progress + b.lon * (1-progress), + ele: a.ele * progress + b.ele * (1-progress) + } + } + + function getSquareRoute(){ + return [ + {lat:"47.2577411",lon:"11.9927442",ele:2273}, + {lat:"47.266761",lon:"11.9926673",ele:2166}, + {lat:"47.2667605",lon:"12.0059511",ele:2245}, + {lat:"47.2577516",lon:"12.0059925",ele:1994} + ]; + } + function getSquareRouteFuzzy(){ + return [ + {lat:"47.2578455",lon:"11.9929891",ele:2265}, + {lat:"47.258592",lon:"11.9923341",ele:2256}, + {lat:"47.2594506",lon:"11.9927412",ele:2230}, + {lat:"47.2603323",lon:"11.9924949",ele:2219}, + {lat:"47.2612056",lon:"11.9928175",ele:2199}, + {lat:"47.2621002",lon:"11.9929817",ele:2182}, + {lat:"47.2629025",lon:"11.9923915",ele:2189}, + {lat:"47.2637828",lon:"11.9926486",ele:2180}, + {lat:"47.2646733",lon:"11.9928167",ele:2191}, + {lat:"47.2655617",lon:"11.9930357",ele:2185}, + {lat:"47.2662862",lon:"11.992252",ele:2186}, + {lat:"47.2669305",lon:"11.993173",ele:2166}, + {lat:"47.266666",lon:"11.9944419",ele:2171}, + {lat:"47.2667579",lon:"11.99576",ele:2194}, + {lat:"47.2669409",lon:"11.9970579",ele:2207}, + {lat:"47.2666562",lon:"11.9983128",ele:2212}, + {lat:"47.2666027",lon:"11.9996335",ele:2262}, + {lat:"47.2667245",lon:"12.0009395",ele:2278}, + {lat:"47.2668457",lon:"12.002256",ele:2297}, + {lat:"47.2666126",lon:"12.0035373",ele:2303}, + {lat:"47.2664554",lon:"12.004841",ele:2251}, + {lat:"47.2669461",lon:"12.005948",ele:2245}, + {lat:"47.2660877",lon:"12.006323",ele:2195}, + {lat:"47.2652729",lon:"12.0057552",ele:2163}, + {lat:"47.2643926",lon:"12.0060123",ele:2131}, + {lat:"47.2634978",lon:"12.0058302",ele:2095}, + {lat:"47.2626129",lon:"12.0060759",ele:2066}, + {lat:"47.2617325",lon:"12.0058188",ele:2037}, + {lat:"47.2608668",lon:"12.0061784",ele:1993}, + {lat:"47.2600155",lon:"12.0057392",ele:1967}, + {lat:"47.2591203",lon:"12.0058233",ele:1949}, + {lat:"47.2582307",lon:"12.0059718",ele:1972}, + {lat:"47.2578014",lon:"12.004804",ele:2011}, + {lat:"47.2577232",lon:"12.0034834",ele:2044}, + {lat:"47.257745",lon:"12.0021656",ele:2061}, + {lat:"47.2578682",lon:"12.0008597",ele:2065}, + {lat:"47.2577082",lon:"11.9995526",ele:2071}, + {lat:"47.2575917",lon:"11.9982348",ele:2102}, + {lat:"47.2577401",lon:"11.996924",ele:2147}, + {lat:"47.257715",lon:"11.9956061",ele:2197}, + {lat:"47.2578996",lon:"11.9943081",ele:2228} + ]; + } + + if (settings.gps.name == "staticfix") { + modGps(() => { return { + "lat": 52, + "lon": 8, + "alt": 100, + "speed": 10, + "course": 12, + "time": Date.now(), + "satellites": 7, + "fix": 1, + "hdop": 1 + };}); + } else if (settings.gps.name.includes("route")) { + let route; + let interpSteps; + if (settings.gps.name == "routeFuzzy"){ + route = getSquareRouteFuzzy(); + interpSteps = 5; + } else { + route = getSquareRoute(); + interpSteps = 50; + } + + let step = 0; + let routeIndex = 0; + modGps(() => { + let newIndex = (routeIndex + 1)%route.length; + + let result = { + "speed": Math.random() * 3 + 2, + "time": Date.now(), + "satellites": Math.floor(Math.random()*5)+3, + "fix": 1, + "hdop": Math.floor(Math.random(30)+1) + }; + + let oldPos = route[routeIndex]; + if (step != 0){ + oldPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,step/interpSteps)); + } + let newPos = route[newIndex]; + if (step < interpSteps - 1){ + newPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,(step+1)%interpSteps/interpSteps)); + } + + if (step == interpSteps - 1){ + let followingIndex = (routeIndex + 2)%route.length; + newPos = interpolate(route[newIndex], route[followingIndex], E.clip(0,1,1/interpSteps)); + } + + result.lat = oldPos.lat; + result.lon = oldPos.lon; + result.alt = oldPos.ele; + + result.course = bearing(oldPos,newPos); + + step++; + if (step == interpSteps){ + routeIndex = (routeIndex + 1) % route.length; + step = 0; + } + return result; + }); + } else if (settings.gps.name == "nofix") { + modGps(() => { return { + "lat": NaN, + "lon": NaN, + "alt": NaN, + "speed": NaN, + "course": NaN, + "time": Date.now(), + "satellites": 2, + "fix": 0, + "hdop": NaN + };}); + } else if (settings.gps.name == "changingfix") { + let currentSpeed=1; + let currentLat=20; + let currentLon=10; + let currentCourse=10; + let currentAlt=-100; + let currentSats=5; + modGps(() => { + currentLat += 0.1; + if (currentLat > 50) currentLat = 20; + currentLon += 0.1; + if (currentLon > 20) currentLon = 10; + currentSpeed *= 10; + if (currentSpeed > 1000) currentSpeed = 1; + currentCourse += 12; + if (currentCourse > 360) currentCourse -= 360; + currentSats += 1; + if (currentSats > 10) currentSats = 5; + currentAlt *= 10; + if (currentAlt > 1000) currentAlt = -100; + return { + "lat": currentLat, + "lon": currentLon, + "alt": currentAlt, + "speed": currentSpeed, + "course": currentCourse, + "time": Date.now(), + "satellites": currentSats, + "fix": 1, + "hdop": 1 + };}); + } + } + } + + if (settings.mag && settings.mag.enabled) { + log("MAG", settings.mag); + let modMag = function(data) { + setInterval(() => { + Bangle.getCompass = data; + Bangle.sensortoolsOrigEmit("mag", data()); + }, 100); + }; + if (settings.mag.power) { + Bangle.sensortoolsOrigSetCompassPower = Bangle.setCompassPower; + Bangle.setCompassPower = createPowerFunction(settings.mag.power, "Compass", Bangle.sensortoolsOrigSetCompassPower); + } + if (settings.mag.mode == "emulate") { + if (settings.mag.name == "static") { + modMag(()=>{return { + x: 1, + y: 1, + z: 1, + dx: 1, + dy: 1, + dz: 1, + heading: 0 + };}); + } else if (settings.mag.name == "rotate"){ + let last = 0; + modMag(()=>{return { + x: 1, + y: 1, + z: 1, + dx: 1, + dy: 1, + dz: 1, + heading: last = (last+1)%360, + };}); + } + } + } + } +})(); diff --git a/apps/sensortools/default.json b/apps/sensortools/default.json new file mode 100644 index 000000000..a85e1ddeb --- /dev/null +++ b/apps/sensortools/default.json @@ -0,0 +1,18 @@ +{ + "enabled": false, + "mag": { + "enabled": false, + "mode": "emulate", + "name": "static" + }, + "hrm": { + "enabled": false, + "mode": "modify", + "name": "bpmtrippled" + }, + "gps": { + "enabled": false, + "mode": "emulate", + "name": "changingfix" + } +} diff --git a/apps/sensortools/icon.png b/apps/sensortools/icon.png new file mode 100644 index 000000000..b7b5ec9ea Binary files /dev/null and b/apps/sensortools/icon.png differ diff --git a/apps/sensortools/metadata.json b/apps/sensortools/metadata.json new file mode 100644 index 000000000..550fa5edc --- /dev/null +++ b/apps/sensortools/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "sensortools", + "name": "Sensor tools", + "shortName": "Sensor tools", + "version": "0.01", + "description": "Tools for testing and debugging apps that use sensor input", + "icon": "icon.png", + "type": "bootloader", + "tags": "tool,boot,debug", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"sensortools.0.boot.js","url":"boot.js"}, + {"name":"sensortools.settings.js","url":"settings.js"}, + {"name":"sensortools.default.json","url":"default.json"} + ] +} diff --git a/apps/sensortools/settings.js b/apps/sensortools/settings.js new file mode 100644 index 000000000..231ab8467 --- /dev/null +++ b/apps/sensortools/settings.js @@ -0,0 +1,99 @@ +(function(back) { + function writeSettings(key, value) { + var s = require('Storage').readJSON(FILE, true) || {}; + s[key] = value; + require('Storage').writeJSON(FILE, s); + readSettings(); + } + + function writeSettingsParent(parent, key, value) { + var s = require('Storage').readJSON(FILE, true) || {}; + if (!s[parent]) s[parent] = {}; + s[parent][key] = value; + require('Storage').writeJSON(FILE, s); + readSettings(); + } + + function readSettings(){ + settings = Object.assign( + require('Storage').readJSON("sensortools.default.json", true) || {}, + require('Storage').readJSON(FILE, true) || {} + ); + } + + var FILE="sensortools.json"; + var settings; + readSettings(); + + + let modes = ["nop", "emulate", "modify"]; + let modesPower = ["nop", "emulate", "passthrough", "delay", "on"]; + + function showSubMenu(name,key,typesEmulate,typesModify){ + var menu = { + '': { 'title': name, + back: ()=>{E.showMenu(buildMainMenu());}}, + 'Enabled': { + value: !!settings[key].enabled, + onchange: v => { + writeSettingsParent(key, "enabled",v); + } + }, + 'Mode': { + value: modes.indexOf(settings[key].mode||"nop"), + min: 0, max: modes.length-1, + format: v => { return modes[v]; }, + onchange: v => { + writeSettingsParent(key,"mode",modes[v]); + showSubMenu(name,key,typesEmulate,typesModify); + } + }, + 'Name': {}, + 'Power': { + value: modesPower.indexOf(settings[key].power||"nop"), + min: 0, max: modesPower.length-1, + format: v => { return modesPower[v]; }, + onchange: v => { + writeSettingsParent(key,"power",modesPower[v]); + } + }, + }; + + if (settings[key].mode != "nop"){ + let types = typesEmulate; + if (settings[key].mode == "modify") types = typesModify; + menu.Name = { + value: types.indexOf(settings[key].name||"static"), + min: 0, max: types.length-1, + format: v => { return types[v]; }, + onchange: v => { + writeSettingsParent(key,"name",types[v]); + } + }; + } else { + delete menu.Name; + } + + E.showMenu(menu); + } + + + function buildMainMenu(){ + var mainmenu = { + '': { 'title': 'Sensor tools' }, + '< Back': back, + 'Enabled': { + value: !!settings.enabled, + onchange: v => { + writeSettings("enabled",v); + }, + }, + 'GPS': ()=>{showSubMenu("GPS","gps",["nop", "staticfix", "nofix", "changingfix", "route", "routeFuzzy"],[]);}, + 'Compass': ()=>{showSubMenu("Compass","mag",["nop", "static", "rotate"],[]);}, + 'HRM': ()=>{showSubMenu("HRM","hrm",["nop", "static"],["bpmtrippled"],["sin"]);} + }; + return mainmenu; + } + + E.showMenu(buildMainMenu()); +});