diff --git a/apps/openwind/ChangeLog b/apps/openwind/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/openwind/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/openwind/README.md b/apps/openwind/README.md new file mode 100644 index 000000000..1df7ea158 --- /dev/null +++ b/apps/openwind/README.md @@ -0,0 +1,22 @@ +# OpenWind + +Receive and display data from a wireless [OpenWind](https://www.openwind.de/) sailing wind instrument on the Bangle. + +## Usage + +Upon startup, the app will attempt to automatically connect to the wind instrument. This typically only takes a few seconds. + +## Features + +The app displays the apparent wind direction (via a green dot) and speed (green numbers, in knots) relative to the mounting direction of the wind vane. +If "True wind" is enabled in settings and a GPS fix is available, the true wind speed and direction (relative to the mounting direction of the vane) is +additionally displayed in red. In this mode, the speed over ground in knots is also shown at the bottom left of the screen. + +## Controls + +There are no controls in the main app, but there are two settings in the settings app that can be changed: + + * True wind: enables or disables true wind calculations; enabling this will turn on GPS inside the app + * Mounting angle: mounting relative to the boat of the wind instrument (in degrees) + +![](openwind_screenshot.png) diff --git a/apps/openwind/app-icon.js b/apps/openwind/app-icon.js new file mode 100644 index 000000000..b86738955 --- /dev/null +++ b/apps/openwind/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4AzhMJF94wtF+QwsF/4APnAACF54wZFoYxNF7guHGBQv0GCwuJGBIvFACov/AD4vvd6Yv/GCoumGIwtpAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHoA==")) diff --git a/apps/openwind/app.js b/apps/openwind/app.js new file mode 100644 index 000000000..210b747d9 --- /dev/null +++ b/apps/openwind/app.js @@ -0,0 +1,113 @@ +OW_CHAR_UUID = '0000cc91-0000-1000-8000-00805f9b34fb'; +require("Font7x11Numeric7Seg").add(Graphics); +gatt = {}; +cx = g.getWidth()/2; +cy = 24+(g.getHeight()-24)/2; +w = (g.getWidth()-24)/2; + +gps_course = { spd: 0 }; + +var settings = require("Storage").readJSON('openwindsettings.json', 1) || {}; + +i = 0; +hullpoly = []; +for (y=-1; y<=1; y+=0.1) { + hullpoly[i++] = cx - (y<0 ? 1+y*0.15 : (Math.sqrt(1-0.7*y*y)-Math.sqrt(0.3))/(1-Math.sqrt(0.3)))*w*0.3; + hullpoly[i++] = cy - y*w*0.7; +} +for (y=1; y>=-1; y-=0.1) { + hullpoly[i++] = cx + (y<0 ? 1+y*0.15 : (Math.sqrt(1-0.7*y*y)-Math.sqrt(0.3))/(1-Math.sqrt(0.3)))*w*0.3; + hullpoly[i++] = cy - y*w*0.7; +} + +function wind_updated(ev) { + if (ev.target.uuid == "0xcc91") { + awa = settings.mount_angle-ev.target.value.getInt16(1, true)*0.1; + aws = ev.target.value.getInt16(3, true)*0.01; +// console.log(awa, aws); + if (gps_course.spd > 0) { + wv = { // wind vector (in fixed reference frame) + lon: Math.sin(Math.PI*(gps_course.course+awa)/180)*aws, + lat: Math.cos(Math.PI*(gps_course.course+awa)/180)*aws + }; + twv = { lon: wv.lon+gps_course.lon, lat: wv.lat+gps_course.lat }; + tws = Math.sqrt(Math.pow(twv.lon,2)+Math.pow(twv.lat, 2)); + twa = Math.atan2(twv.lat, twv.lon)*180/Math.PI-gps_course.course; + if (twa<0) twa += 360; + if (twa>360) twa -=360; + } + else { + tws = -1; + twa = 0; + } + draw_compass(awa,aws,twa,tws); + } +} + +function draw_compass(awa, aws, twa, tws) { + g.clearRect(0, 24, g.getWidth()-1, g.getHeight()-1); + fh = w*0.15; + g.setColor(0, 0, 1).fillPoly(hullpoly); + g.setFontVector(fh).setColor(1, 1, 1); + g.setFontAlign(0, 0, 0).drawString("0", cx, 24+fh/2); + g.setFontAlign(0, 0, 1).drawString("90", g.getWidth()-12-fh, cy); + g.setFontAlign(0, 0, 2).drawString("180", cx, g.getHeight()-fh/2); + g.setFontAlign(0, 0, 3).drawString("270", 12+fh/2, cy); + for (i=0; i<4; ++i) { + a = i*Math.PI/2+Math.PI/4; + g.drawLineAA(cx+Math.cos(a)*w*0.85, cy+Math.sin(a)*w*0.85, cx+Math.cos(a)*w*0.99, cy+Math.sin(a)*w*0.99); + } + g.setColor(0, 1, 0).fillCircle(cx+Math.sin(Math.PI*awa/180)*w*0.9, cy+Math.cos(Math.PI*awa/180)*w*0.9, w*0.1); + if (tws>0) g.setColor(1, 0, 0).fillCircle(cx+Math.sin(Math.PI*twa/180)*w*0.9, cy+Math.cos(Math.PI*twa/180)*w*0.9, w*0.1); + g.setColor(0, 1, 0).setFont("7x11Numeric7Seg",w*0.06); + g.setFontAlign(0, 0, 0).drawString(aws.toFixed(1), cx, cy-0.32*w); + if (tws>0) g.setColor(1, 0, 0).drawString(tws.toFixed(1), cx, cy+0.32*w); + if (settings.truewind && typeof gps_course.spd!=='undefined') { + spd = gps_course.spd/1.852; + g.setColor(1, 1, 1).setFont("7x11Numeric7Seg", w*0.03).setFontAlign(-1, 1, 0).drawString(spd.toFixed(1), 1, g.getHeight()-1); + } +} + +function parseDevice(d) { + device = d; + console.log("Found device"); + device.gatt.connect().then(function(ga) { + console.log("Connected"); + gatt = ga; + return ga.getPrimaryService("cc90"); +}).then(function(s) { + return s.getCharacteristic("cc91"); +}).then(function(c) { + c.on('characteristicvaluechanged', (event)=>wind_updated(event)); + return c.startNotifications(); +}).then(function() { + console.log("Done!"); +}).catch(function(e) { + console.log("ERROR"+e); +});} + +function connection_setup() { + NRF.setScan(); + NRF.setScan(parseDevice, { filters: [{services:["cc90"]}], timeout: 2000}); + console.log("Scanning for OW sensor"); +} + +if (settings.truewind) { + Bangle.on('GPS',function(fix) { + if (fix.fix && fix.satellites>3 && fix.speed>2) { // only uses fixes w/ more than 3 sats and speed > 2kph + gps_course = + { lon: Math.sin(Math.PI*fix.course/180)*fix.speed/1.852, + lat: Math.cos(Math.PI*fix.course/180)*fix.speed/1.852, + spd: fix.speed, + course: fix.course + }; + } + else gps_course.spd = -1; + }); + Bangle.setGPSPower(1, "app"); +} + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +draw_compass(0, 0, 0, 0); +connection_setup(); diff --git a/apps/openwind/app.png b/apps/openwind/app.png new file mode 100644 index 000000000..9fd64efba Binary files /dev/null and b/apps/openwind/app.png differ diff --git a/apps/openwind/metadata.json b/apps/openwind/metadata.json new file mode 100644 index 000000000..e80dcc924 --- /dev/null +++ b/apps/openwind/metadata.json @@ -0,0 +1,15 @@ +{ "id": "openwind", + "name": "OpenWind", + "shortName":"OpenWind", + "version":"0.01", + "description": "OpenWind", + "icon": "openwind.png", + "readme": "README.md", + "tags": "ble,sailing", + "supports" : ["BANGLEJS", "BANGLEJS2"], + "storage": [ + {"name":"openwind.app.js","url":"app.js"}, + {"name":"openwind.img","url":"app-icon.js","evaluate":true}, + {"name":"openwind.settings.js", "url":"settings.js"} + ] +} diff --git a/apps/openwind/openwind.png b/apps/openwind/openwind.png new file mode 100644 index 000000000..9fd64efba Binary files /dev/null and b/apps/openwind/openwind.png differ diff --git a/apps/openwind/openwind_screenshot.png b/apps/openwind/openwind_screenshot.png new file mode 100644 index 000000000..05143e8a4 Binary files /dev/null and b/apps/openwind/openwind_screenshot.png differ diff --git a/apps/openwind/settings.js b/apps/openwind/settings.js new file mode 100644 index 000000000..771a65b0b --- /dev/null +++ b/apps/openwind/settings.js @@ -0,0 +1,43 @@ +// This file should contain exactly one function, which shows the app's settings +/** + * @param {function} back Use back() to return to settings menu + */ +(function(back) { + const SETTINGS_FILE = 'openwindsettings.json' + // initialize with default settings... + let s = { + 'truewind': false, + 'mount_angle': 0 + } + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + const saved = storage.readJSON(SETTINGS_FILE, 1) || {} + for (const key in saved) { + s[key] = saved[key]; + } + // creates a function to safe a specific setting, e.g. save('color')(1) + function save(key) { + return function (value) { + s[key] = value; + storage.write(SETTINGS_FILE, s); + } + } + const menu = { + '': { 'title': 'OpenWind' }, + '< Back': back, + 'True wind': { + value: s.truewind, + format: boolFormat, + onchange: save('truewind'), + }, + 'Mounting angle': { + value: s.mount_angle + min: 0, + max: 355, + step: 5, + onchange: save('mount_angle'), + } + } + E.showMenu(menu); +})