diff --git a/.eslintignore b/.eslintignore
index 1e3abd9ff..a82960313 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,6 +1,8 @@
# Needs to be ignored because it uses ESM export/import
apps/gipy/pkg/gps.js
+apps/gipy/pkg/gps.d.ts
+apps/gipy/pkg/gps_bg.wasm.d.ts
# Needs to be ignored because it includes broken JS
apps/health/chart.min.js
diff --git a/.eslintrc.js b/.eslintrc.js
index e79f87a5d..b7590a77e 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -112,6 +112,7 @@ module.exports = {
"getSerial": "readonly",
"getTime": "readonly",
"global": "readonly",
+ "globalThis": "readonly",
"HIGH": "readonly",
"I2C1": "readonly",
"Infinity": "readonly",
diff --git a/README.md b/README.md
index d595c7df1..ddcf23f25 100644
--- a/README.md
+++ b/README.md
@@ -289,6 +289,7 @@ and which gives information about the app for the Launcher.
"dependencies" : { "message":"widget" } // optional, depend on a specific type of widget - see provides_widgets
"provides_modules" : ["messageicons"] // optional, this app provides a module that can be used with 'require'
"provides_widgets" : ["battery"] // optional, this app provides a type of widget - 'alarm/battery/bluetooth/pedometer/message'
+ "provides_features" : ["welcome"] // optional, this app provides some feature, used to ensure two aren't installed at once. Currently just 'welcome'
"default" : true, // set if an app is the default implementer of something (a widget/module/etc)
"readme": "README.md", // if supplied, a link to a markdown-style text file
// that contains more information about this app (usage, etc)
diff --git a/android.html b/android.html
index a0bc6075a..900376bfb 100644
--- a/android.html
+++ b/android.html
@@ -187,14 +187,18 @@
-
+
Install
@@ -203,7 +207,7 @@
-
+
@@ -416,7 +420,7 @@ if (el) el.addEventListener("click", event=>{
if (webrtc) showWebRTCID(webrtc.peerId);
else {
webrtc = webrtcInit({
- bridge:true,
+ bridge:true,
onStatus : function(s) {
showToast(s);
},
@@ -432,7 +436,7 @@ if (el) el.addEventListener("click", event=>{
onPortDisconnect : function(serialPort) {
},
onPortWrite : function(data, cb) {
- Puck.write(data, cb);
+ Puck.write(data, cb);
}
});
connection.on("data", function(d) {
diff --git a/apps/8ball/8ball.png b/apps/8ball/8ball.png
new file mode 100644
index 000000000..72344261a
Binary files /dev/null and b/apps/8ball/8ball.png differ
diff --git a/apps/8ball/ChangeLog b/apps/8ball/ChangeLog
new file mode 100644
index 000000000..3bcffb19b
--- /dev/null
+++ b/apps/8ball/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/8ball/app-icon.js b/apps/8ball/app-icon.js
new file mode 100644
index 000000000..399dbef21
--- /dev/null
+++ b/apps/8ball/app-icon.js
@@ -0,0 +1 @@
+atob("MDCBAAAAAAAAAAAAAAAAAAAAH/AAAAAAf/4AAAAB4AeAAAAHgAHgAAAOAABwAAAcAAA4AAA4AMAcAABwAMAOAABgA/AGAADAAeADAAHAAMADgAGAAAABgAGAAAABgAMAAAAAwAMBAAAAwAMDAAAAwAMHwAAAwAMHwAAAwAMDAACAwAMBAADAwAMAAAPgwAMAAAPgwAMAAADAwAGAAACBgAGAAAABgAHAAAADgADAAAADAADgAAAHAAB////+AAB////+AABgAAAGAABgAAAGAABgAAAGAADAAAADAADAAAADAADAAAADAAGAAAABgAGAAAABgAH/////gAP/////wAYAAAAAYAYAAAAAYAf/////4AP/////wAAAAAAAAAAAAAAAAA==")
diff --git a/apps/8ball/app.js b/apps/8ball/app.js
new file mode 100644
index 000000000..8a3ee427e
--- /dev/null
+++ b/apps/8ball/app.js
@@ -0,0 +1,92 @@
+var keyboard = "textinput";
+var Name = "";
+Bangle.setLCDTimeout(0);
+var menuOpen = 1;
+var answers = new Array("no", "yes","WHAT????","What do you think", "That was a bad question", "YES!!!", "NOOOOO!!", "nope","100%","yup","why should I answer that?","think for yourself","ask again later, I'm busy", "what Was that horrible question","how dare you?","you wanted to hear yes? okay, yes", "Don't get angry when I say no","you are 100% wrong","totally, for sure","hmmm... I'll ponder it and get back to you later","wow, you really have a lot of questions", "NOPE","is the sky blue, hmmm...","I don't have time to answer","How many more questions before you change my name?","theres this thing called wikipedia","hmm... I don't seem to be able to reach the internet right now","if you phrase it like that, yes","Huh, never thought so hard in my life","The winds of time say no");
+var consonants = new Array("b","c","d","f","g","h","j","k","l","m","n","p","q","r","s","t","v","w","x","y","z");
+var vowels = new Array("a","e","i","o","u");
+try {keyboard = require(keyboard);} catch(e) {keyboard = null;}
+function generateName()
+{
+ Name = "";
+ var nameLength = Math.round(Math.random()*5);
+ for(var i = 0; i < nameLength; i++){
+ var cosonant = consonants[Math.round(Math.random()*consonants.length/2)];
+ var vowel = vowels[Math.round(Math.random()*vowels.length/2)];
+ Name = Name + cosonant + vowel;
+ if(Name == "")
+ {
+ generateName();
+ }
+ }
+}
+generateName();
+function menu()
+{
+ g.clear();
+ E.showMenu();
+ menuOpen = 1;
+ E.showMenu({
+ "" : { title : Name },
+ "< Back" : () => menu(),
+ "Start" : () => {
+ E.showMenu();
+ g.clear();
+ menuOpen = 0;
+ Drawtext("ask " + Name + " a yes or no question");
+ },
+ "regenerate name" : () => {
+ menu();
+ generateName();
+ },
+ "show answers" : () => {
+ var menu = new Array([]);
+ for(var i = 0; i < answers.length; i++){
+ menu.push({title : answers[i]});
+ }
+ E.showMenu(menu);
+
+
+ },
+
+ "Add answer" : () => {
+ E.showMenu();
+ keyboard.input({}).then(result => {if(result != ""){answers.push(result);} menu();});
+ },
+ "Edit name" : () => {
+ E.showMenu();
+ keyboard.input({}).then(result => {if(result != ""){Name = result;} menu();});
+
+ },
+ "Exit" : () => load(),
+ });
+}
+menu();
+
+ var answer;
+function Drawtext(text)
+{
+ g.clear();
+ g.setFont("Vector", 20);
+ g.drawString(g.wrapString(text, g.getWidth(), -20).join("\n"));
+}
+function WriteAnswer()
+{
+ if (menuOpen == 0)
+ {
+ var randomnumber = Math.round(Math.random()*answers.length);
+ answer = answers[randomnumber];
+ Drawtext(answer);
+ setTimeout(function() {
+ Drawtext("ask " + Name + " a yes or no question");
+}, 3000);
+
+ }
+
+}
+setWatch(function() {
+ menu();
+
+}, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat:true, edge:"falling"});
+
+ Bangle.on('touch', function(button, xy) { WriteAnswer(); });
diff --git a/apps/8ball/metadata.json b/apps/8ball/metadata.json
new file mode 100644
index 000000000..da387d3d6
--- /dev/null
+++ b/apps/8ball/metadata.json
@@ -0,0 +1,19 @@
+{ "id": "8ball",
+ "name": "Magic 8 ball",
+ "shortName":"8ball",
+ "icon": "8ball.png",
+ "version":"0.01",
+ "screenshots": [
+ {"url":"screenshot.png"},
+ {"url":"screenshot-1.png"},
+ {"url":"screenshot-2.png"}
+ ],
+ "allow_emulator": true,
+ "description": "A very sarcastic magic 8ball",
+ "tags": "game",
+ "supports": ["BANGLEJS2"],
+ "storage": [
+ {"name":"8ball.app.js","url":"app.js"},
+ {"name":"8ball.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/8ball/screenshot-1.png b/apps/8ball/screenshot-1.png
new file mode 100644
index 000000000..edf1a4695
Binary files /dev/null and b/apps/8ball/screenshot-1.png differ
diff --git a/apps/8ball/screenshot-2.png b/apps/8ball/screenshot-2.png
new file mode 100644
index 000000000..c5c607089
Binary files /dev/null and b/apps/8ball/screenshot-2.png differ
diff --git a/apps/8ball/screenshot.png b/apps/8ball/screenshot.png
new file mode 100644
index 000000000..f1f888cf3
Binary files /dev/null and b/apps/8ball/screenshot.png differ
diff --git a/apps/_example_clock/app.js b/apps/_example_clock/app.js
index d1f997136..a5d114b3a 100644
--- a/apps/_example_clock/app.js
+++ b/apps/_example_clock/app.js
@@ -24,10 +24,10 @@
var dateStr = require("locale").date(date);
// draw time
g.setFontAlign(0,0).setFont("Vector",48);
- g.clearRect(0,y-15,g.getWidth(),y+25); // clear the background
+ g.clearRect(0,y-20,g.getWidth(),y+25); // clear the background
g.drawString(timeStr,x,y);
// draw date
- y += 35;
+ y += 30;
g.setFontAlign(0,0).setFont("6x8");
g.clearRect(0,y-4,g.getWidth(),y+4); // clear the background
g.drawString(dateStr,x,y);
@@ -41,6 +41,8 @@
// Show launcher when middle button pressed
Bangle.setUI({mode:"clock", remove:function() {
// free any memory we allocated to allow fast loading
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
}});
// Load widgets
Bangle.loadWidgets();
diff --git a/apps/a_dndtoggle/settings.js b/apps/a_dndtoggle/settings.js
index 5316525b3..483af8c97 100644
--- a/apps/a_dndtoggle/settings.js
+++ b/apps/a_dndtoggle/settings.js
@@ -29,5 +29,4 @@
}
E.showMenu(buildMainMenu());
- });
-
\ No newline at end of file
+ })
diff --git a/apps/activepedom/settings.js b/apps/activepedom/settings.js
index 3b64d8735..16799f0db 100644
--- a/apps/activepedom/settings.js
+++ b/apps/activepedom/settings.js
@@ -109,4 +109,4 @@
},
};
E.showMenu(menu);
-});
+})
diff --git a/apps/agpsdata/settings.js b/apps/agpsdata/settings.js
index 64fa25330..95b06fe55 100644
--- a/apps/agpsdata/settings.js
+++ b/apps/agpsdata/settings.js
@@ -68,4 +68,4 @@ function buildMainMenu() {
}
E.showMenu(buildMainMenu());
-});
+})
diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog
index a3a5dfc1c..15afa790b 100644
--- a/apps/alarm/ChangeLog
+++ b/apps/alarm/ChangeLog
@@ -51,3 +51,7 @@
0.46: Show alarm groups if the Show Group setting is ON. Scroll alarms menu back to previous position when getting back to it.
0.47: Fix wrap around when snoozed through midnight
0.48: Use datetimeinput for Events, if available. Scroll back when getting out of group. Menu date format setting for shorter dates on current year.
+0.49: fix uncaught error if no scroller (Bangle 1). Would happen when trying
+ to select an alarm in the main menu.
+0.50: Bangle.js 2: Long touch of alarm in main menu toggle it on/off. Touching the icon on
+ the right will do the same.
diff --git a/apps/alarm/README.md b/apps/alarm/README.md
index 77aa61d2c..d7b64b3c2 100644
--- a/apps/alarm/README.md
+++ b/apps/alarm/README.md
@@ -20,6 +20,8 @@ It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master
- `Disable All` → Disable _all_ enabled alarms & timers
- `Delete All` → Delete _all_ alarms & timers
+On Bangle.js 2 it's possible to toggle alarms, timers and events from the main menu. This is done by clicking the indicator icons of corresponding entries. Or long pressing anywhere on them.
+
## Creator
- [Gordon Williams](https://github.com/gfwilliams)
@@ -29,6 +31,7 @@ It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master
- [Alessandro Cocco](https://github.com/alessandrococco) - New UI, full rewrite, new features
- [Sabin Iacob](https://github.com/m0n5t3r) - Auto snooze support
- [storm64](https://github.com/storm64) - Fix redrawing in submenus
+- [thyttan](https://github.com/thyttan) - Toggle alarms directly from main menu.
## Attributions
diff --git a/apps/alarm/app.js b/apps/alarm/app.js
index 053505187..0318be6d3 100644
--- a/apps/alarm/app.js
+++ b/apps/alarm/app.js
@@ -88,13 +88,23 @@ function showMainMenu(scroll, group, scrollback) {
const getGroups = settings.showGroup && !group;
const groups = getGroups ? {} : undefined;
var showAlarm;
+ const getIcon = (e)=>{return e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff);};
alarms.forEach((e, index) => {
showAlarm = !settings.showGroup || (group ? e.group === group : !e.group);
if(showAlarm) {
- menu[trimLabel(getLabel(e),40)] = {
- value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff),
- onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index, undefined, scroller.scroll, group)
+ const label = trimLabel(getLabel(e),40);
+ menu[label] = {
+ value: e.on,
+ onchange: (v, touch) => {
+ if (touch && (2==touch.type || 145getIcon(e)
};
} else if (getGroups) {
groups[e.group] = undefined;
@@ -102,7 +112,7 @@ function showMainMenu(scroll, group, scrollback) {
});
if (!group) {
- Object.keys(groups).sort().forEach(g => menu[g] = () => showMainMenu(null, g, scroller.scroll));
+ Object.keys(groups).sort().forEach(g => menu[g] = () => showMainMenu(null, g, scroller?scroller.scroll:undefined));
menu[/*LANG*/"Advanced"] = () => showAdvancedMenu();
}
diff --git a/apps/alarm/metadata.json b/apps/alarm/metadata.json
index 78cd4bd4e..17dd147e3 100644
--- a/apps/alarm/metadata.json
+++ b/apps/alarm/metadata.json
@@ -2,7 +2,7 @@
"id": "alarm",
"name": "Alarms & Timers",
"shortName": "Alarms",
- "version": "0.48",
+ "version": "0.50",
"description": "Set alarms and timers on your Bangle",
"icon": "app.png",
"tags": "tool,alarm",
diff --git a/apps/alarm/settings.js b/apps/alarm/settings.js
index 765e5a5fa..2843fbdb1 100644
--- a/apps/alarm/settings.js
+++ b/apps/alarm/settings.js
@@ -48,4 +48,4 @@
};
E.showMenu(appMenu);
-});
+})
diff --git a/apps/alpinenav/README.md b/apps/alpinenav/README.md
index d18cdfd6d..823d6c9f8 100644
--- a/apps/alpinenav/README.md
+++ b/apps/alpinenav/README.md
@@ -2,13 +2,26 @@ Alpine Navigator
================
App that performs GPS monitoring to track and display position relative to a given origin in realtime.
-
+
+
+ [compass 5]
+
+ altitude
+[start 1] [current 2]
+
+ distance
+[from start 3] [track 4]
+
+
+[btn1 -- screen lock]
+[btn2 -- remove points]
+[btn3 -- pause]
Functions
---------
-Note if you've not used GPS yet I suggest using one of the GPS apps to get your first fix and confirm as I've found that helps initially.
+Note if you've not used GPS yet, I suggest using one of the GPS apps to get your first fix and confirm, as I've found that helps initially.
-The GPS and magnetometer will be turned on and after a few moments, when the watch buzzes and the dot turns from red to pink, that means the GPS is fixed. all your movements now will be displayed with a line drawn back to show your position relative to the start. New waypoints will be added based on checking every 10 seconds for at least 5 meters of movement. The map will scale to your distance travelled so the route will always remain within the window, the accelerometer/pedometer is not used - this is a purely GPS and compass solution so can be used for driving/cycling etc. A log file will be recorded that tracks upto 1000 waypoints, this isn't a big file and you could remove the limit but I've kept it fairly conservative here as it's not intended as a main feature, there's already good GPS recorders for the Bangle. The following other items are displayed:
+The GPS and magnetometer will be turned on and after a few moments, when the watch buzzes and the dot turns from red to pink, that means the GPS is fixed. All your movements now will be displayed with a line drawn back to show your position relative to the start. New waypoints will be added based on checking every 10 seconds for at least 5 meters of movement. The map will scale to your distance travelled so the route will always remain within the window, the accelerometer/pedometer is not used - this is a purely GPS and compass solution so can be used for driving/cycling etc. A log file will be recorded that tracks upto 1000 waypoints, this isn't a big file and you could remove the limit, but I've kept it fairly conservative here, as it's not intended as a main feature, there's already good GPS recorders for the Bangle. The following other items are displayed:
1. altitude at origin, this is displayed left of the centre.
2. current altitude, displayed centre right
@@ -16,12 +29,12 @@ The GPS and magnetometer will be turned on and after a few moments, when the wat
4. distance travelled, bottom right (meters)
5. compass heading, at the top
-For the display, the route is kept at a set resolution, so there's no risk of running into memory problems if you run this for long periods or any length of time because the waypoints will be reduced when it reaches a set threshold so you may see the path smooth out slightly at intervals.
+For the display, the route is kept at a set resolution, so there's no risk of running into memory problems if you run this for long periods or any length of time, because the waypoints will be reduced when it reaches a set threshold, so you may see the path smooth out slightly at intervals.
-If you get strange values or dashes for the compass, it just needs calibration so you need to move the watch around briefly for this each time, ideally 360 degrees around itself, which involves taking the watch off. If you don't want to do that you can also just wave your hand around for a few seconds like you're at a rave or Dr Strange making a Sling Ring but often just moving your wrist a bit is enough.
+If you get strange values or dashes for the compass, it just needs calibration so you need to move the watch around briefly for this each time, ideally 360 degrees around itself, which involves taking the watch off. If you don't want to do that you can also just wave your hand around for a few seconds like you're at a rave or Dr Strange making a Sling Ring, but often just moving your wrist a bit is enough.
The buttons do the following:
-BTN1: this will display an 'X' in the bottom of the screen and lock all the buttons, this is to prevent you accidentally pressing either of the below. Remember to press this again to unlock it! soft and hard reset will both still work.
+BTN1: this will display an 'X' in the bottom of the screen and lock all the buttons, this is to prevent you accidentally pressing either of the below. Remember to press this again to unlock it! Soft and hard reset will both still work.
BTN2: this removes all waypoints aside from the origin and your current location; sometimes during smaller journeys and walks, the GPS can give sporadic differences in locations because of the error margins of GPS and this can add noise to your route.
BTN3: this will pause the GPS and magnetometer, useful for saving power for situations where you don't necessarily need to track parts of your route e.g. you're going indoors/shelter for some time. You'll know it's paused because the compass won't update it's reading and all the metrics will be blacked out on the screen.
diff --git a/apps/andark/settings.js b/apps/andark/settings.js
index 708913705..7bbceb2c2 100644
--- a/apps/andark/settings.js
+++ b/apps/andark/settings.js
@@ -25,4 +25,4 @@
};
E.showMenu(appMenu);
-});
+})
diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog
index 11c78588a..f1107fc84 100644
--- a/apps/android/ChangeLog
+++ b/apps/android/ChangeLog
@@ -36,4 +36,7 @@
0.34: Implement API for activity tracks fetching (Recorder app logs).
0.35: Implement API to enable/disable acceleration data tracking.
0.36: Move from wrapper function to {} and let - faster execution at boot
- Allow `calendar-` to take an array of items to remove
\ No newline at end of file
+ Allow `calendar-` to take an array of items to remove
+0.37: Support Gadgetbridge canned responses
+0.38: Don't rewrite settings file on every boot!
+0.39: Move GB message handling into a library to reduce boot time from 40ms->13ms
\ No newline at end of file
diff --git a/apps/android/README.md b/apps/android/README.md
index f322e6a4e..a7a539e38 100644
--- a/apps/android/README.md
+++ b/apps/android/README.md
@@ -49,11 +49,21 @@ The boot code also provides some useful functions:
* `body` the body of the HTTP request
* `headers` an object of headers, eg `{HeaderOne : "headercontents"}`
+`Bangle.http` returns a promise which contains:
+
+```JS
+{
+ t:"http",
+ id: // the ID of this HTTP request
+ resp: "...." // a string containing the response
+}
+```
+
eg:
-```
+```JS
Bangle.http("https://pur3.co.uk/hello.txt").then(data=>{
- console.log("Got ",data);
+ console.log("Got ",data.resp);
});
```
diff --git a/apps/android/boot.js b/apps/android/boot.js
index 729ed2b47..18297d84f 100644
--- a/apps/android/boot.js
+++ b/apps/android/boot.js
@@ -1,350 +1,24 @@
/* global GB */
{
- let gbSend = function(message) {
- Bluetooth.println("");
- Bluetooth.println(JSON.stringify(message));
- }
- let lastMsg; // for music messages - may not be needed now...
- let actInterval; // Realtime activity reporting interval when `act` is true
- let actHRMHandler; // For Realtime activity reporting
- let gpsState = {}; // keep information on GPS via Gadgetbridge
-
- // this settings var is deleted after this executes to save memory
- let settings = require("Storage").readJSON("android.settings.json",1)||{};
- //default alarm settings
- if (settings.rp == undefined) settings.rp = true;
- if (settings.as == undefined) settings.as = true;
- if (settings.vibrate == undefined) settings.vibrate = "..";
- require('Storage').writeJSON("android.settings.json", settings);
+ // settings var is deleted after this executes to save memory
+ let settings = Object.assign({rp:true,as:true,vibrate:".."},
+ require("Storage").readJSON("android.settings.json",1)||{}
+ );
let _GB = global.GB;
- let fetchRecInterval;
- global.GB = (event) => {
+ global.GB = e => {
// feed a copy to other handlers if there were any
- if (_GB) setTimeout(_GB,0,Object.assign({},event));
-
-
- /* TODO: Call handling, fitness */
- var HANDLERS = {
- // {t:"notify",id:int, src,title,subject,body,sender,tel:string} add
- "notify" : function() {
- Object.assign(event,{t:"add",positive:true, negative:true});
- // Detect a weird GadgetBridge bug and fix it
- // For some reason SMS messages send two GB notifications, with different sets of info
- if (lastMsg && event.body == lastMsg.body && lastMsg.src == undefined && event.src == "Messages") {
- // Mutate the other message
- event.id = lastMsg.id;
- }
- lastMsg = event;
- require("messages").pushMessage(event);
- },
- // {t:"notify~",id:int, title:string} // modified
- "notify~" : function() { event.t="modify";require("messages").pushMessage(event); },
- // {t:"notify-",id:int} // remove
- "notify-" : function() { event.t="remove";require("messages").pushMessage(event); },
- // {t:"find", n:bool} // find my phone
- "find" : function() {
- if (Bangle.findDeviceInterval) {
- clearInterval(Bangle.findDeviceInterval);
- delete Bangle.findDeviceInterval;
- }
- if (event.n) // Ignore quiet mode: we always want to find our watch
- Bangle.findDeviceInterval = setInterval(_=>Bangle.buzz(),1000);
- },
- // {t:"musicstate", state:"play/pause",position,shuffle,repeat}
- "musicstate" : function() {
- require("messages").pushMessage({t:"modify",id:"music",title:"Music",state:event.state});
- },
- // {t:"musicinfo", artist,album,track,dur,c(track count),n(track num}
- "musicinfo" : function() {
- require("messages").pushMessage(Object.assign(event, {t:"modify",id:"music",title:"Music"}));
- },
- // {"t":"call","cmd":"incoming/end","name":"Bob","number":"12421312"})
- "call" : function() {
- Object.assign(event, {
- t:event.cmd=="incoming"?"add":"remove",
- id:"call", src:"Phone",
- positive:true, negative:true,
- title:event.name||/*LANG*/"Call", body:/*LANG*/"Incoming call\n"+event.number});
- require("messages").pushMessage(event);
- },
- // {"t":"alarm", "d":[{h:int,m:int,rep:int},... }
- "alarm" : function() {
- //wipe existing GB alarms
- var sched;
- try { sched = require("sched"); } catch (e) {}
- if (!sched) return; // alarms may not be installed
- var gbalarms = sched.getAlarms().filter(a=>a.appid=="gbalarms");
- for (var i = 0; i < gbalarms.length; i++)
- sched.setAlarm(gbalarms[i].id, undefined);
- var alarms = sched.getAlarms();
- var time = new Date();
- var currentTime = time.getHours() * 3600000 +
- time.getMinutes() * 60000 +
- time.getSeconds() * 1000;
- for (var j = 0; j < event.d.length; j++) {
- // prevents all alarms from going off at once??
- var dow = event.d[j].rep;
- var rp = false;
- if (!dow) {
- dow = 127; //if no DOW selected, set alarm to all DOW
- } else {
- rp = true;
- }
- var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0;
- var a = require("sched").newDefaultAlarm();
- a.id = "gb"+j;
- a.appid = "gbalarms";
- a.on = event.d[j].on !== undefined ? event.d[j].on : true;
- a.t = event.d[j].h * 3600000 + event.d[j].m * 60000;
- a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format
- a.rp = rp;
- a.last = last;
- alarms.push(a);
- }
- sched.setAlarms(alarms);
- sched.reload();
- },
- //TODO perhaps move those in a library (like messages), used also for viewing events?
- //add and remove events based on activity on phone (pebble-like)
- // {t:"calendar", id:int, type:int, timestamp:seconds, durationInSeconds, title:string, description:string,location:string,calName:string.color:int,allDay:bool
- "calendar" : function() {
- var cal = require("Storage").readJSON("android.calendar.json",true);
- if (!cal || !Array.isArray(cal)) cal = [];
- var i = cal.findIndex(e=>e.id==event.id);
- if(i<0)
- cal.push(event);
- else
- cal[i] = event;
- require("Storage").writeJSON("android.calendar.json", cal);
- },
- // {t:"calendar-", id:int}
- "calendar-" : function() {
- var cal = require("Storage").readJSON("android.calendar.json",true);
- //if any of those happen we are out of sync!
- if (!cal || !Array.isArray(cal)) cal = [];
- if (Array.isArray(event.id))
- cal = cal.filter(e=>!event.id.includes(e.id));
- else
- cal = cal.filter(e=>e.id!=event.id);
- require("Storage").writeJSON("android.calendar.json", cal);
- },
- //triggered by GB, send all ids
- // { t:"force_calendar_sync_start" }
- "force_calendar_sync_start" : function() {
- var cal = require("Storage").readJSON("android.calendar.json",true);
- if (!cal || !Array.isArray(cal)) cal = [];
- gbSend({t:"force_calendar_sync", ids: cal.map(e=>e.id)});
- },
- // {t:"http",resp:"......",[id:"..."]}
- "http":function() {
- //get the promise and call the promise resolve
- if (Bangle.httpRequest === undefined) return;
- var request=Bangle.httpRequest[event.id];
- if (request === undefined) return; //already timedout or wrong id
- delete Bangle.httpRequest[event.id];
- clearTimeout(request.t); //t = timeout variable
- if(event.err!==undefined) //if is error
- request.j(event.err); //r = reJect function
- else
- request.r(event); //r = resolve function
- },
- // {t:"gps", lat, lon, alt, speed, course, time, satellites, hdop, externalSource:true }
- "gps": function() {
- if (!settings.overwriteGps) return;
- // modify event for using it as Bangle GPS event
- delete event.t;
- if (!isFinite(event.satellites)) event.satellites = NaN;
- if (!isFinite(event.course)) event.course = NaN;
- event.fix = 1;
- if (event.long!==undefined) { // for earlier Gadgetbridge implementations
- event.lon = event.long;
- delete event.long;
- }
- if (event.time){
- event.time = new Date(event.time);
- }
-
- if (!gpsState.lastGPSEvent) {
- // this is the first event, save time of arrival and deactivate internal GPS
- Bangle.moveGPSPower(0);
- } else {
- // this is the second event, store the intervall for expecting the next GPS event
- gpsState.interval = Date.now() - gpsState.lastGPSEvent;
- }
- gpsState.lastGPSEvent = Date.now();
- // in any case, cleanup the GPS state in case no new events arrive
- if (gpsState.timeoutGPS) clearTimeout(gpsState.timeoutGPS);
- gpsState.timeoutGPS = setTimeout(()=>{
- // reset state
- gpsState.lastGPSEvent = undefined;
- gpsState.timeoutGPS = undefined;
- gpsState.interval = undefined;
- // did not get an expected GPS event but have GPS clients, switch back to internal GPS
- if (Bangle.isGPSOn()) Bangle.moveGPSPower(1);
- }, (gpsState.interval || 10000) + 1000);
- Bangle.emit('GPS', event);
- },
- // {t:"is_gps_active"}
- "is_gps_active": function() {
- gbSend({ t: "gps_power", status: Bangle.isGPSOn() });
- },
- // {t:"act", hrm:bool, stp:bool, int:int}
- "act": function() {
- if (actInterval) clearInterval(actInterval);
- actInterval = undefined;
- if (actHRMHandler)
- actHRMHandler = undefined;
- Bangle.setHRMPower(event.hrm,"androidact");
- if (!(event.hrm || event.stp)) return;
- if (!isFinite(event.int)) event.int=1;
- var lastSteps = Bangle.getStepCount();
- var lastBPM = 0;
- actHRMHandler = function(e) {
- lastBPM = e.bpm;
- };
- Bangle.on('HRM',actHRMHandler);
- actInterval = setInterval(function() {
- var steps = Bangle.getStepCount();
- gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM, rt:1 });
- lastSteps = steps;
- }, event.int*1000);
- },
- // {t:"actfetch", ts:long}
- "actfetch": function() {
- gbSend({t: "actfetch", state: "start"});
- var actCount = 0;
- var actCb = function(r) {
- // The health lib saves the samples at the start of the 10-minute block
- // However, GB expects them at the end of the block, so let's offset them
- // here to keep a consistent API in the health lib
- var sampleTs = r.date.getTime() + 600000;
- if (sampleTs >= event.ts) {
- gbSend({
- t: "act",
- ts: sampleTs,
- stp: r.steps,
- hrm: r.bpm,
- mov: r.movement
- });
- actCount++;
- }
- }
- if (event.ts != 0) {
- require("health").readAllRecordsSince(new Date(event.ts - 600000), actCb);
- } else {
- require("health").readFullDatabase(actCb);
- }
- gbSend({t: "actfetch", state: "end", count: actCount});
- },
- //{t:"listRecs", id:"20230616a"}
- "listRecs": function() {
- let recs = require("Storage").list(/^recorder\.log.*\.csv$/,{sf:true}).map(s => s.slice(12, 21));
- if (event.id.length > 2) { // Handle if there was no id supplied. Then we send a list all available recorder logs back.
- let firstNonsyncedIdx = recs.findIndex((logId) => logId > event.id);
- if (-1 == firstNonsyncedIdx) {
- recs = []
- } else {
- recs = recs.slice(firstNonsyncedIdx);
- }
- }
- gbSend({t:"actTrksList", list: recs}); // TODO: split up in multiple transmissions?
- },
- //{t:"fetchRec", id:"20230616a"}
- "fetchRec": function() {
- // TODO: Decide on what names keys should have.
- if (fetchRecInterval) {
- clearInterval(fetchRecInterval);
- fetchRecInterval = undefined;
- }
- if (event.id=="stop") {
- return
- } else {
- let log = require("Storage").open("recorder.log"+event.id+".csv","r");
- let lines = "init";// = log.readLine();
- let pkgcnt = 0;
- gbSend({t:"actTrk", log:event.id, lines:"erase", cnt:pkgcnt}); // "erase" will prompt Gadgetbridge to erase the contents of a already fetched log so we can rewrite it without keeping lines from the previous (probably failed) fetch.
- let sendlines = ()=>{
- lines = log.readLine();
- for (var i = 0; i < 3; i++) {
- let line = log.readLine();
- if (line) lines += line;
- }
- pkgcnt++;
- gbSend({t:"actTrk", log:event.id, lines:lines, cnt:pkgcnt});
- if (!lines && fetchRecInterval) {
- clearInterval(fetchRecInterval);
- fetchRecInterval = undefined;
- }
- }
- fetchRecInterval = setInterval(sendlines, 50)
- }
- },
- "nav": function() {
- event.id="nav";
- if (event.instr) {
- event.t="add";
- event.src="maps"; // for the icon
- event.title="Navigation";
- if (require("messages").getMessages().find(m=>m.id=="nav"))
- event.t = "modify";
- } else {
- event.t="remove";
- }
- require("messages").pushMessage(event);
- },
- "cards" : function() {
- // we receive all, just override what we have
- if (Array.isArray(event.d))
- require("Storage").writeJSON("android.cards.json", event.d);
- },
- "accelsender": function () {
- require("Storage").writeJSON("accelsender.json", {enabled: event.enable, interval: event.interval});
- load();
- }
- };
- var h = HANDLERS[event.t];
- if (h) h(); else console.log("GB Unknown",event);
+ if (_GB) setTimeout(_GB,0,Object.assign({},e));
+ Bangle.emit("GB",e);
+ require("android").gbHandler(e);
};
// HTTP request handling - see the readme
- // options = {id,timeout,xpath}
- Bangle.http = (url,options)=>{
- options = options||{};
- if (!NRF.getSecurityStatus().connected)
- return Promise.reject(/*LANG*/"Not connected to Bluetooth");
- if (Bangle.httpRequest === undefined)
- Bangle.httpRequest={};
- if (options.id === undefined) {
- // try and create a unique ID
- do {
- options.id = Math.random().toString().substr(2);
- } while( Bangle.httpRequest[options.id]!==undefined);
- }
- //send the request
- var req = {t: "http", url:url, id:options.id};
- if (options.xpath) req.xpath = options.xpath;
- if (options.return) req.return = options.return; // for xpath
- if (options.method) req.method = options.method;
- if (options.body) req.body = options.body;
- if (options.headers) req.headers = options.headers;
- gbSend(req);
- //create the promise
- var promise = new Promise(function(resolve,reject) {
- //save the resolve function in the dictionary and create a timeout (30 seconds default)
- Bangle.httpRequest[options.id]={r:resolve,j:reject,t:setTimeout(()=>{
- //if after "timeoutMillisec" it still hasn't answered -> reject
- delete Bangle.httpRequest[options.id];
- reject("Timeout");
- },options.timeout||30000)};
- });
- return promise;
- };
-
+ Bangle.http = (url,options)=>require("android").httpHandler(url,options);
// Battery monitor
- let sendBattery = function() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); }
+ let sendBattery = function() { require("android").gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); }
Bangle.on("charging", sendBattery);
NRF.on("connect", () => setTimeout(function() {
sendBattery();
- gbSend({t: "ver", fw: process.env.VERSION, hw: process.env.HWVERSION});
+ require("android").gbSend({t: "ver", fw: process.env.VERSION, hw: process.env.HWVERSION});
GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process
}, 2000));
NRF.on("disconnect", () => {
@@ -358,81 +32,24 @@
setInterval(sendBattery, 10*60*1000);
// Health tracking - if 'realtime' data is sent with 'rt:1', but let's still send our activity log every 10 mins
Bangle.on('health', h=>{
- gbSend({ t: "act", stp: h.steps, hrm: h.bpm, mov: h.movement });
+ require("android").gbSend({ t: "act", stp: h.steps, hrm: h.bpm, mov: h.movement });
});
// Music control
Bangle.musicControl = cmd => {
// play/pause/next/previous/volumeup/volumedown
- gbSend({ t: "music", n:cmd });
+ require("android").gbSend({ t: "music", n:cmd });
};
// Message response
Bangle.messageResponse = (msg,response) => {
- if (msg.id=="call") return gbSend({ t: "call", n:response?"ACCEPT":"REJECT" });
- if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
+ if (msg.id=="call") return require("android").gbSend({ t: "call", n:response?"ACCEPT":"REJECT" });
+ if (isFinite(msg.id)) return require("android").gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
// error/warn here?
};
Bangle.messageIgnore = msg => {
- if (isFinite(msg.id)) return gbSend({ t: "notify", n:"MUTE", id: msg.id });
+ if (isFinite(msg.id)) return require("android").gbSend({ t: "notify", n:"MUTE", id: msg.id });
};
// GPS overwrite logic
- if (settings.overwriteGps) { // if the overwrite option is set..
- const origSetGPSPower = Bangle.setGPSPower;
- Bangle.moveGPSPower = (state) => {
- if (Bangle.isGPSOn()){
- let orig = Bangle._PWR.GPS;
- delete Bangle._PWR.GPS;
- origSetGPSPower(state);
- Bangle._PWR.GPS = orig;
- }
- };
-
- // work around Serial1 for GPS not working when connected to something
- let serialTimeout;
- let wrap = function(f){
- return (s)=>{
- if (serialTimeout) clearTimeout(serialTimeout);
- origSetGPSPower(1, "androidgpsserial");
- f(s);
- serialTimeout = setTimeout(()=>{
- serialTimeout = undefined;
- origSetGPSPower(0, "androidgpsserial");
- }, 10000);
- };
- };
- Serial1.println = wrap(Serial1.println);
- Serial1.write = wrap(Serial1.write);
-
- // replace set GPS power logic to suppress activation of gps (and instead request it from the phone)
- Bangle.setGPSPower = ((isOn, appID) => {
- let pwr;
- if (!this.lastGPSEvent){
- // use internal GPS power function if no gps event has arrived from GadgetBridge
- pwr = origSetGPSPower(isOn, appID);
- } else {
- // we are currently expecting the next GPS event from GadgetBridge, keep track of GPS state per app
- if (!Bangle._PWR) Bangle._PWR={};
- if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];
- if (!appID) appID="?";
- if (isOn && !Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.push(appID);
- if (!isOn && Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.splice(Bangle._PWR.GPS.indexOf(appID),1);
- pwr = Bangle._PWR.GPS.length>0;
- // stop internal GPS, no clients left
- if (!pwr) origSetGPSPower(0);
- }
- // always update Gadgetbridge on current power state
- gbSend({ t: "gps_power", status: pwr });
- return pwr;
- }).bind(gpsState);
- // allow checking for GPS via GadgetBridge
- Bangle.isGPSOn = () => {
- return !!(Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0);
- };
- // stop GPS on boot if not activated
- setTimeout(()=>{
- if (!Bangle.isGPSOn()) gbSend({ t: "gps_power", status: false });
- },3000);
- }
-
+ if (settings.overwriteGps) require("android").overwriteGPS();
// remove settings object so it's not taking up RAM
delete settings;
}
diff --git a/apps/android/lib.js b/apps/android/lib.js
new file mode 100644
index 000000000..038d154b3
--- /dev/null
+++ b/apps/android/lib.js
@@ -0,0 +1,388 @@
+exports.gbSend = function(message) {
+ Bluetooth.println("");
+ Bluetooth.println(JSON.stringify(message));
+}
+let lastMsg, // for music messages - may not be needed now...
+ gpsState = {}, // keep information on GPS via Gadgetbridge
+ settings = Object.assign({rp:true,as:true,vibrate:".."},
+ require("Storage").readJSON("android.settings.json",1)||{}
+ );
+
+exports.gbHandler = (event) => {
+ var HANDLERS = {
+ // {t:"notify",id:int, src,title,subject,body,sender,tel:string} add
+ "notify" : function() {
+ print("notify",event);
+ Object.assign(event,{t:"add",positive:true, negative:true});
+ // Detect a weird GadgetBridge bug and fix it
+ // For some reason SMS messages send two GB notifications, with different sets of info
+ if (lastMsg && event.body == lastMsg.body && lastMsg.src == undefined && event.src == "Messages") {
+ // Mutate the other message
+ event.id = lastMsg.id;
+ }
+ lastMsg = event;
+ require("messages").pushMessage(event);
+ },
+ // {t:"notify~",id:int, title:string} // modified
+ "notify~" : function() { event.t="modify";require("messages").pushMessage(event); },
+ // {t:"notify-",id:int} // remove
+ "notify-" : function() { event.t="remove";require("messages").pushMessage(event); },
+ // {t:"find", n:bool} // find my phone
+ "find" : function() {
+ if (Bangle.findDeviceInterval) {
+ clearInterval(Bangle.findDeviceInterval);
+ delete Bangle.findDeviceInterval;
+ }
+ if (event.n) // Ignore quiet mode: we always want to find our watch
+ Bangle.findDeviceInterval = setInterval(_=>Bangle.buzz(),1000);
+ },
+ // {t:"musicstate", state:"play/pause",position,shuffle,repeat}
+ "musicstate" : function() {
+ require("messages").pushMessage({t:"modify",id:"music",title:"Music",state:event.state});
+ },
+ // {t:"musicinfo", artist,album,track,dur,c(track count),n(track num}
+ "musicinfo" : function() {
+ require("messages").pushMessage(Object.assign(event, {t:"modify",id:"music",title:"Music"}));
+ },
+ // {"t":"call","cmd":"incoming/end/start/outgoing","name":"Bob","number":"12421312"})
+ "call" : function() {
+ Object.assign(event, {
+ t:event.cmd=="incoming"?"add":"remove",
+ id:"call", src:"Phone",
+ positive:true, negative:true,
+ title:event.name||/*LANG*/"Call", body:/*LANG*/"Incoming call\n"+event.number});
+ require("messages").pushMessage(event);
+ },
+ "canned_responses_sync" : function() {
+ require("Storage").writeJSON("replies.json", event.d);
+ },
+ // {"t":"alarm", "d":[{h:int,m:int,rep:int},... }
+ "alarm" : function() {
+ //wipe existing GB alarms
+ var sched;
+ try { sched = require("sched"); } catch (e) {}
+ if (!sched) return; // alarms may not be installed
+ var gbalarms = sched.getAlarms().filter(a=>a.appid=="gbalarms");
+ for (var i = 0; i < gbalarms.length; i++)
+ sched.setAlarm(gbalarms[i].id, undefined);
+ var alarms = sched.getAlarms();
+ var time = new Date();
+ var currentTime = time.getHours() * 3600000 +
+ time.getMinutes() * 60000 +
+ time.getSeconds() * 1000;
+ for (var j = 0; j < event.d.length; j++) {
+ // prevents all alarms from going off at once??
+ var dow = event.d[j].rep;
+ var rp = false;
+ if (!dow) {
+ dow = 127; //if no DOW selected, set alarm to all DOW
+ } else {
+ rp = true;
+ }
+ var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0;
+ var a = require("sched").newDefaultAlarm();
+ a.id = "gb"+j;
+ a.appid = "gbalarms";
+ a.on = event.d[j].on !== undefined ? event.d[j].on : true;
+ a.t = event.d[j].h * 3600000 + event.d[j].m * 60000;
+ a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format
+ a.rp = rp;
+ a.last = last;
+ alarms.push(a);
+ }
+ sched.setAlarms(alarms);
+ sched.reload();
+ },
+ //TODO perhaps move those in a library (like messages), used also for viewing events?
+ //add and remove events based on activity on phone (pebble-like)
+ // {t:"calendar", id:int, type:int, timestamp:seconds, durationInSeconds, title:string, description:string,location:string,calName:string.color:int,allDay:bool
+ "calendar" : function() {
+ var cal = require("Storage").readJSON("android.calendar.json",true);
+ if (!cal || !Array.isArray(cal)) cal = [];
+ var i = cal.findIndex(e=>e.id==event.id);
+ if(i<0)
+ cal.push(event);
+ else
+ cal[i] = event;
+ require("Storage").writeJSON("android.calendar.json", cal);
+ },
+ // {t:"calendar-", id:int}
+ "calendar-" : function() {
+ var cal = require("Storage").readJSON("android.calendar.json",true);
+ //if any of those happen we are out of sync!
+ if (!cal || !Array.isArray(cal)) cal = [];
+ if (Array.isArray(event.id))
+ cal = cal.filter(e=>!event.id.includes(e.id));
+ else
+ cal = cal.filter(e=>e.id!=event.id);
+ require("Storage").writeJSON("android.calendar.json", cal);
+ },
+ //triggered by GB, send all ids
+ // { t:"force_calendar_sync_start" }
+ "force_calendar_sync_start" : function() {
+ var cal = require("Storage").readJSON("android.calendar.json",true);
+ if (!cal || !Array.isArray(cal)) cal = [];
+ exports.gbSend({t:"force_calendar_sync", ids: cal.map(e=>e.id)});
+ },
+ // {t:"http",resp:"......",[id:"..."]}
+ "http":function() {
+ //get the promise and call the promise resolve
+ if (Bangle.httpRequest === undefined) return;
+ var request=Bangle.httpRequest[event.id];
+ if (request === undefined) return; //already timedout or wrong id
+ delete Bangle.httpRequest[event.id];
+ clearTimeout(request.t); //t = timeout variable
+ if(event.err!==undefined) //if is error
+ request.j(event.err); //r = reJect function
+ else
+ request.r(event); //r = resolve function
+ },
+ // {t:"gps", lat, lon, alt, speed, course, time, satellites, hdop, externalSource:true }
+ "gps": function() {
+ if (!settings.overwriteGps) return;
+ // modify event for using it as Bangle GPS event
+ delete event.t;
+ if (!isFinite(event.satellites)) event.satellites = NaN;
+ if (!isFinite(event.course)) event.course = NaN;
+ event.fix = 1;
+ if (event.long!==undefined) { // for earlier Gadgetbridge implementations
+ event.lon = event.long;
+ delete event.long;
+ }
+ if (event.time){
+ event.time = new Date(event.time);
+ }
+
+ if (!gpsState.lastGPSEvent) {
+ // this is the first event, save time of arrival and deactivate internal GPS
+ Bangle.moveGPSPower(0);
+ } else {
+ // this is the second event, store the intervall for expecting the next GPS event
+ gpsState.interval = Date.now() - gpsState.lastGPSEvent;
+ }
+ gpsState.lastGPSEvent = Date.now();
+ // in any case, cleanup the GPS state in case no new events arrive
+ if (gpsState.timeoutGPS) clearTimeout(gpsState.timeoutGPS);
+ gpsState.timeoutGPS = setTimeout(()=>{
+ // reset state
+ gpsState.lastGPSEvent = undefined;
+ gpsState.timeoutGPS = undefined;
+ gpsState.interval = undefined;
+ // did not get an expected GPS event but have GPS clients, switch back to internal GPS
+ if (Bangle.isGPSOn()) Bangle.moveGPSPower(1);
+ }, (gpsState.interval || 10000) + 1000);
+ Bangle.emit('GPS', event);
+ },
+ // {t:"is_gps_active"}
+ "is_gps_active": function() {
+ exports.gbSend({ t: "gps_power", status: Bangle.isGPSOn() });
+ },
+ // {t:"act", hrm:bool, stp:bool, int:int}
+ "act": function() {
+ if (exports.actInterval) clearInterval(exports.actInterval);
+ exports.actInterval = undefined;
+ if (exports.actHRMHandler)
+ exports.actHRMHandler = undefined;
+ Bangle.setHRMPower(event.hrm,"androidact");
+ if (!(event.hrm || event.stp)) return;
+ if (!isFinite(event.int)) event.int=1;
+ var lastSteps = Bangle.getStepCount();
+ var lastBPM = 0;
+ exports.actHRMHandler = function(e) {
+ lastBPM = e.bpm;
+ };
+ Bangle.on('HRM',exports.actHRMHandler);
+ exports.actInterval = setInterval(function() {
+ var steps = Bangle.getStepCount();
+ exports.gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM, rt:1 });
+ lastSteps = steps;
+ }, event.int*1000);
+ },
+ // {t:"actfetch", ts:long}
+ "actfetch": function() {
+ exports.gbSend({t: "actfetch", state: "start"});
+ var actCount = 0;
+ var actCb = function(r) {
+ // The health lib saves the samples at the start of the 10-minute block
+ // However, GB expects them at the end of the block, so let's offset them
+ // here to keep a consistent API in the health lib
+ var sampleTs = r.date.getTime() + 600000;
+ if (sampleTs >= event.ts) {
+ exports.gbSend({
+ t: "act",
+ ts: sampleTs,
+ stp: r.steps,
+ hrm: r.bpm,
+ mov: r.movement
+ });
+ actCount++;
+ }
+ }
+ if (event.ts != 0) {
+ require("health").readAllRecordsSince(new Date(event.ts - 600000), actCb);
+ } else {
+ require("health").readFullDatabase(actCb);
+ }
+ exports.gbSend({t: "actfetch", state: "end", count: actCount});
+ },
+ //{t:"listRecs", id:"20230616a"}
+ "listRecs": function() {
+ let recs = require("Storage").list(/^recorder\.log.*\.csv$/,{sf:true}).map(s => s.slice(12, 21));
+ if (event.id.length > 2) { // Handle if there was no id supplied. Then we send a list all available recorder logs back.
+ let firstNonsyncedIdx = recs.findIndex((logId) => logId > event.id);
+ if (-1 == firstNonsyncedIdx) {
+ recs = []
+ } else {
+ recs = recs.slice(firstNonsyncedIdx);
+ }
+ }
+ exports.gbSend({t:"actTrksList", list: recs}); // TODO: split up in multiple transmissions?
+ },
+ //{t:"fetchRec", id:"20230616a"}
+ "fetchRec": function() {
+ // TODO: Decide on what names keys should have.
+ if (exports.fetchRecInterval) {
+ clearInterval(exports.fetchRecInterval);
+ exports.fetchRecInterval = undefined;
+ }
+ if (event.id=="stop") {
+ return;
+ } else {
+ let log = require("Storage").open("recorder.log"+event.id+".csv","r");
+ let lines = "init";// = log.readLine();
+ let pkgcnt = 0;
+ exports.gbSend({t:"actTrk", log:event.id, lines:"erase", cnt:pkgcnt}); // "erase" will prompt Gadgetbridge to erase the contents of a already fetched log so we can rewrite it without keeping lines from the previous (probably failed) fetch.
+ let sendlines = ()=>{
+ lines = log.readLine();
+ for (var i = 0; i < 3; i++) {
+ let line = log.readLine();
+ if (line) lines += line;
+ }
+ pkgcnt++;
+ exports.gbSend({t:"actTrk", log:event.id, lines:lines, cnt:pkgcnt});
+ if (!lines && exports.fetchRecInterval) {
+ clearInterval(exports.fetchRecInterval);
+ exports.fetchRecInterval = undefined;
+ }
+ };
+ exports.fetchRecInterval = setInterval(sendlines, 50);
+ }
+ },
+ "nav": function() {
+ event.id="nav";
+ if (event.instr) {
+ event.t="add";
+ event.src="maps"; // for the icon
+ event.title="Navigation";
+ if (require("messages").getMessages().find(m=>m.id=="nav"))
+ event.t = "modify";
+ } else {
+ event.t="remove";
+ }
+ require("messages").pushMessage(event);
+ },
+ "cards" : function() {
+ // we receive all, just override what we have
+ if (Array.isArray(event.d))
+ require("Storage").writeJSON("android.cards.json", event.d);
+ },
+ "accelsender": function () {
+ require("Storage").writeJSON("accelsender.json", {enabled: event.enable, interval: event.interval});
+ load();
+ }
+ };
+ var h = HANDLERS[event.t];
+ if (h) h(); else console.log("GB Unknown",event);
+};
+
+// HTTP request handling - see the readme
+// options = {id,timeout,xpath}
+exports.httpHandler = (url,options) => {
+ options = options||{};
+ if (!NRF.getSecurityStatus().connected)
+ return Promise.reject(/*LANG*/"Not connected to Bluetooth");
+ if (Bangle.httpRequest === undefined)
+ Bangle.httpRequest={};
+ if (options.id === undefined) {
+ // try and create a unique ID
+ do {
+ options.id = Math.random().toString().substr(2);
+ } while( Bangle.httpRequest[options.id]!==undefined);
+ }
+ //send the request
+ var req = {t: "http", url:url, id:options.id};
+ if (options.xpath) req.xpath = options.xpath;
+ if (options.return) req.return = options.return; // for xpath
+ if (options.method) req.method = options.method;
+ if (options.body) req.body = options.body;
+ if (options.headers) req.headers = options.headers;
+ exports.gbSend(req);
+ //create the promise
+ var promise = new Promise(function(resolve,reject) {
+ //save the resolve function in the dictionary and create a timeout (30 seconds default)
+ Bangle.httpRequest[options.id]={r:resolve,j:reject,t:setTimeout(()=>{
+ //if after "timeoutMillisec" it still hasn't answered -> reject
+ delete Bangle.httpRequest[options.id];
+ reject("Timeout");
+ },options.timeout||30000)};
+ });
+ return promise;
+};
+
+exports.overwriteGPS = () => { // if the overwrite option is set, call this on init..
+ const origSetGPSPower = Bangle.setGPSPower;
+ Bangle.moveGPSPower = (state) => {
+ if (Bangle.isGPSOn()){
+ let orig = Bangle._PWR.GPS;
+ delete Bangle._PWR.GPS;
+ origSetGPSPower(state);
+ Bangle._PWR.GPS = orig;
+ }
+ };
+
+ // work around Serial1 for GPS not working when connected to something
+ let serialTimeout;
+ let wrap = function(f){
+ return (s)=>{
+ if (serialTimeout) clearTimeout(serialTimeout);
+ origSetGPSPower(1, "androidgpsserial");
+ f(s);
+ serialTimeout = setTimeout(()=>{
+ serialTimeout = undefined;
+ origSetGPSPower(0, "androidgpsserial");
+ }, 10000);
+ };
+ };
+ Serial1.println = wrap(Serial1.println);
+ Serial1.write = wrap(Serial1.write);
+
+ // replace set GPS power logic to suppress activation of gps (and instead request it from the phone)
+ Bangle.setGPSPower = ((isOn, appID) => {
+ let pwr;
+ if (!this.lastGPSEvent){
+ // use internal GPS power function if no gps event has arrived from GadgetBridge
+ pwr = origSetGPSPower(isOn, appID);
+ } else {
+ // we are currently expecting the next GPS event from GadgetBridge, keep track of GPS state per app
+ if (!Bangle._PWR) Bangle._PWR={};
+ if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];
+ if (!appID) appID="?";
+ if (isOn && !Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.push(appID);
+ if (!isOn && Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.splice(Bangle._PWR.GPS.indexOf(appID),1);
+ pwr = Bangle._PWR.GPS.length>0;
+ // stop internal GPS, no clients left
+ if (!pwr) origSetGPSPower(0);
+ }
+ // always update Gadgetbridge on current power state
+ require("android").gbSend({ t: "gps_power", status: pwr });
+ return pwr;
+ }).bind(gpsState);
+ // allow checking for GPS via GadgetBridge
+ Bangle.isGPSOn = () => {
+ return !!(Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0);
+ };
+ // stop GPS on boot if not activated
+ setTimeout(()=>{
+ if (!Bangle.isGPSOn()) require("android").gbSend({ t: "gps_power", status: false });
+ },3000);
+};
\ No newline at end of file
diff --git a/apps/android/metadata.json b/apps/android/metadata.json
index 7768efb6c..584c071cf 100644
--- a/apps/android/metadata.json
+++ b/apps/android/metadata.json
@@ -2,7 +2,7 @@
"id": "android",
"name": "Android Integration",
"shortName": "Android",
- "version": "0.36",
+ "version": "0.39",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge",
@@ -13,7 +13,8 @@
{"name":"android.app.js","url":"app.js"},
{"name":"android.settings.js","url":"settings.js"},
{"name":"android.img","url":"app-icon.js","evaluate":true},
- {"name":"android.boot.js","url":"boot.js"}
+ {"name":"android.boot.js","url":"boot.js"},
+ {"name":"android","url":"lib.js"}
],
"data": [{"name":"android.settings.json"}, {"name":"android.calendar.json"}, {"name":"android.cards.json"}],
"sortorder": -8
diff --git a/apps/antonclkplus/settings.js b/apps/antonclkplus/settings.js
index 4448c00ed..70851e983 100644
--- a/apps/antonclkplus/settings.js
+++ b/apps/antonclkplus/settings.js
@@ -94,4 +94,4 @@
E.showMenu(mainmenu);
-});
+})
diff --git a/apps/assistedgps/ChangeLog b/apps/assistedgps/ChangeLog
index 13f928f18..89b1c80f8 100644
--- a/apps/assistedgps/ChangeLog
+++ b/apps/assistedgps/ChangeLog
@@ -3,4 +3,5 @@
0.03: Select GNSS systems to use for Bangle.js 2
0.04: Now turns GPS off after upload
0.05: Fix regression in 0.04 that caused AGPS data not to get loaded
-0.06: Auto-set GPS output sentences - newer Bangle.js 2 don't include RMC (GPS direction + time) by default
\ No newline at end of file
+0.06: Auto-set GPS output sentences - newer Bangle.js 2 don't include RMC (GPS direction + time) by default
+0.07: Bangle.js 2 now gets estimated time + lat/lon from the browser (~3x faster fix)
\ No newline at end of file
diff --git a/apps/assistedgps/custom.html b/apps/assistedgps/custom.html
index 994f6d053..a51219346 100644
--- a/apps/assistedgps/custom.html
+++ b/apps/assistedgps/custom.html
@@ -60,6 +60,7 @@