Merge branch 'master' into feat/messages-no-show-1st-unread

Conflicts:
	apps/messagegui/ChangeLog
pull/3622/head
Rob Pilling 2024-11-07 17:55:44 +00:00
commit 03b1519648
203 changed files with 2637 additions and 905 deletions

View File

@ -29,5 +29,4 @@
}
E.showMenu(buildMainMenu());
});
})

View File

@ -109,4 +109,4 @@
},
};
E.showMenu(menu);
});
})

View File

@ -68,4 +68,4 @@ function buildMainMenu() {
}
E.showMenu(buildMainMenu());
});
})

View File

@ -48,4 +48,4 @@
};
E.showMenu(appMenu);
});
})

View File

@ -25,4 +25,4 @@
};
E.showMenu(appMenu);
});
})

View File

@ -38,4 +38,5 @@
0.36: Move from wrapper function to {} and let - faster execution at boot
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.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

View File

@ -1,349 +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
// 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);
},
"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 = [];
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", () => {
@ -357,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;
}

388
apps/android/lib.js Normal file
View File

@ -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);
};

View File

@ -2,7 +2,7 @@
"id": "android",
"name": "Android Integration",
"shortName": "Android",
"version": "0.38",
"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

View File

@ -94,4 +94,4 @@
E.showMenu(mainmenu);
});
})

View File

@ -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
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)

View File

@ -60,6 +60,7 @@
<script>
var isB1; // is Bangle.js 1?
var isB2; // is Bangle.js 2?
var currentPosition;
// When the 'upload' button is clicked...
document.getElementById("upload").addEventListener("click", function() {
@ -120,6 +121,7 @@
}
// =================================================== Bangle.js 2 CASIC
// https://www.espruino.com/Bangle.js2+Technical#gps
function CASIC_CHECKSUM(cmd) {
var cs = 0;
@ -128,6 +130,61 @@
return cmd+"*"+cs.toString(16).toUpperCase().padStart(2, '0');
}
// Send a binary CASIC packet, eg: {classId:6, messageId:0, payload:[]}
function CASIC_PKT(pkt) {
pkt.payload = pkt.payload || [];
var plen = pkt.payload.length;
var msg = new Uint8Array(10+pkt.payload.length);
msg.set([0xBA,0xCE,
plen, // LENGTH
0x00,
pkt.classId, // CLASS ID
pkt.messageId]); // MESSAGE ID
msg.set(pkt.payload, 6);
var dv = new DataView(msg.buffer);
// checksum
var ckSum = 0;
for (i = -4; i < plen; i+=4)
ckSum = 0|(ckSum+dv.getUint32(6+i, true));
dv.setUint32(6+plen, ckSum, true);
return msg;
}
// Send AID_INI message, {lat,lon,alt}
function AID_INI(pos) {
var msg = new Uint8Array(56);
var dv = new DataView(msg.buffer);
/*
double xOrLat, yOrLon, zOrAlt;
double tow; // 24
float df; // 32
float posAcc; // 36
float tAcc; // 40
float fAcc; // 44
unsigned int res; // 48
unsigned short int wn; // 52
unsigned char timeSource; // 54
unsigned char flags; // 55
*/
var ms = Date.now();
var wk = (ms-new Date("1980-01-06T00:00:00Z")) / 604800000;
var wn = Math.floor(wk); // week number
var tow = (wk-wn) * 604800; // seconds in week
dv.setFloat64(0, pos.lat, true); // xOrLat
dv.setFloat64(8, pos.lon, true); // yOrLon
dv.setFloat64(16, pos.alt, true); // zOrAlt
dv.setFloat64(24, tow, true); // tow
dv.setFloat32(32, 0, true); // df
dv.setFloat32(36, 0, true); // posAcc
dv.setFloat32(40, 0, true); // tAcc
dv.setFloat32(44, 0, true); // fAcc
dv.setUint32(48, 0, true); // res
dv.setUint16(52, wn, true); // wn
dv.setUint8(54,0); // timeSource
dv.setUint8(55, 0x23); // flags ( lat/lon and clock valid, no drift data )
return CASIC_PKT({classId:0x0B, messageId:0x01, payload:msg});
}
// ===================================================
function jsFromBase64(b64) {
@ -140,7 +197,6 @@
js += `\x10Serial1.write(atob("${btoa(String.fromCharCode.apply(null,UBX_MGA_INI_TIME_UTC()))}"))\n`; // set GPS time
}
if (isB2) { // CASIC
// Select what GNSS System to use for decreased fix time.
var radios = document.getElementsByName('gnss_select');
var gnss_select="1";
@ -150,11 +206,11 @@
js += `\x10var t=getTime()+0.5;while (getTime()<t);\n`; // This is nasty - but we just wait here until the GPS has had time to boot
js += `\x10Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnss_select)}")\n`; // set GNSS mode
js += `\x10Serial1.println("${CASIC_CHECKSUM("$PCAS03,1,0,0,1,1,0,0,0")}")\n`; // enable GGA,GSV,RMC packets (new Bangle.js 2 GPS firmwares don't include RMC by default!)
// If the browser let us have the current location, give it to the GPS chip to get a faster fix
if (currentPosition) {
js += `\x10Serial1.write([${AID_INI(currentPosition).join(",")}])\n`;
}
// Serial1.println("$PCAS06,0*1B") gets the current firmware version
// What about:
// NAV-TIMEUTC (0x01 0x10)
// NAV-PV (0x01 0x03)
// or AGPS.zip uses AID-INI (0x0B 0x01)
}
for (var i=0;i<bin.length;i+=chunkSize) {
@ -184,8 +240,20 @@
document.getElementById("banglejs1-info").style = isB1?"":"display:none";
document.getElementById("banglejs2-info").style = isB2?"":"display:none";
document.getElementById("upload-wrap").style = "";
if (isB2) {
// get current position for AGPS improvement
navigator.geolocation.getCurrentPosition(function(position) {
currentPosition = {
lat : position.coords.latitude,
lon : position.coords.longitude,
alt : 0|position.coords.altitude
};
});
}
}
</script>
</body>
</html>

View File

@ -2,7 +2,7 @@
"id": "assistedgps",
"name": "Assisted GPS Updater (AGPS)",
"shortName": "AGPS",
"version": "0.06",
"version": "0.07",
"description": "Downloads assisted GPS (AGPS) data to Bangle.js for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
"sortorder": -1,
"icon": "app.png",

View File

@ -1,2 +1,3 @@
0.01: New app!
0.02: Minor code improvements
0.03: Remove clearing of the screen (will break running apps) and fix lint errors

View File

@ -2,8 +2,8 @@
"id": "banglebridge",
"name": "BangleBridge",
"shortName": "BangleBridge",
"version": "0.02",
"description": "Widget that allows Bangle Js to record pair and end data using Bluetooth Low Energy in combination with the BangleBridge Android App",
"version": "0.03",
"description": "Widget that allows Bangle.js to record pair and end data using Bluetooth Low Energy in combination with the BangleBridge Android App (**Note:** this has nothing to do with Gadgetbridge)",
"icon": "widget.png",
"type": "widget",
"tags": "widget",

View File

@ -1,10 +1,10 @@
(() => {
/**
* Widget measurements
* Description:
* Description:
* name: connection.wid.js
*icon: conectionIcon.icon
*
*
*/
//Font
@ -24,7 +24,7 @@
//Sensors code
/**
*
*
* @author Jorge
*/
function accel() {
@ -35,8 +35,7 @@
});
setInterval(function () {
acclS = accelN.x + "##" + accelN.y + "##" + accelN.z + "\n" + accelN.diff + "##" + accelN.mag;
//acclS = accelN.x + "##" + accelN.y + "##" + accelN.z + "\n" + accelN.diff + "##" + accelN.mag;
data[3] = accelN;
}, 2 * 1000);
@ -45,8 +44,7 @@
function btt() {
setInterval(function () {
bttS = E.getBattery(); //return String
//bttS = E.getBattery(); //return String
data[2] = E.getBattery();
}, 15 * 1000);
@ -65,9 +63,9 @@
setInterval(function () {
compssS = "A: " + compssN.x + " ## " + compssN.y + " ## " + compssN.z + "\n" +
/*compssS = "A: " + compssN.x + " ## " + compssN.y + " ## " + compssN.z + "\n" +
"B: " + compssN.dx + " ## " + compssN.dy + " ## " + compssN.dz + " ## " + "\n" +
"C: " + compssN.heading; //return String
"C: " + compssN.heading; *///return String
data[4] = compssN;
}, 2 * 1000);
@ -86,8 +84,8 @@
setInterval(function () {
gpsS = "A: " + gpsN.lat + " ## " + gpsN.lon + " ## " + gpsN.alt + "\n" + "B: " + gpsN.speed + " ## " + gpsN.course + " ## " + gpsN.time + "\n" +
"C: " + gpsN.satellites + " ## " + gpsN.fix; //return String
/*gpsS = "A: " + gpsN.lat + " ## " + gpsN.lon + " ## " + gpsN.alt + "\n" + "B: " + gpsN.speed + " ## " + gpsN.course + " ## " + gpsN.time + "\n" +
"C: " + gpsN.satellites + " ## " + gpsN.fix; *///return String
// work out how to display the current time
var d = new Date();
var year = d.getFullYear();
@ -150,7 +148,7 @@
//console.log("Index ==> "+ index);
msr[indexFinal] = nueva;
item = nueva;
//item = nueva;
lastInsert = indexFinal;
}
@ -180,7 +178,7 @@
hrmN = normalize(hrmN);
var roundedRate = parseFloat(hrmN).toFixed(2);
hrmS = String.valueOf(roundedRate); //return String
//hrmS = String.valueOf(roundedRate); //return String
//console.log("array----->" + msr);
data[0] = roundedRate;
@ -205,7 +203,7 @@
setInterval(function () {
stepS = String.valueOf(stepN); //return String
//stepS = String.valueOf(stepN); //return String
data[1] = stepN;
}, 2 * 1000);
@ -240,12 +238,11 @@
g.setFont("Vector", 45);
g.drawString(prueba,100,200);*/
if (flip == 1) { //when off
flip = 0;
//Bangle.buzz(1000);
g.clear();
} else { //when on
flip = 1;
g.setFont("Vector", 30);
g.drawString(data[0], 65, 180);
@ -283,7 +280,7 @@
com: data[4],
gps: data[5]
};
/* g.clear();
/*
g.drawString(compssS,100,200);
*/
@ -293,7 +290,7 @@
//draw();
}, 5 * 1000);
WIDGETS["banglebridge"]={
area: "tl",
width: 10,

View File

@ -17,4 +17,4 @@
}
}
});
});
})

View File

@ -32,4 +32,4 @@
}
require("ClockFace_menu").addItems(menu, save, items);
E.showMenu(menu);
});
})

View File

@ -23,4 +23,4 @@
};
E.showMenu(mainmenu);
});
})

View File

@ -68,4 +68,4 @@
E.showMenu(appMenu);
});
})

View File

@ -69,4 +69,4 @@
//};
E.showMenu(mainmenu);
});
})

View File

@ -70,5 +70,3 @@
};
return ci;
})

View File

@ -72,3 +72,7 @@
0.61: Instead of breaking execution with an Exception when updating boot, just use if..else (fix 'Uncaught undefined')
0.62: Handle setting for configuring BLE privacy
0.63: Only set BLE `display:1` if we have a passkey
0.64: Automatically create .widcache and .clkinfocache to speed up loads
Bangle.loadWidgets overwritten with fast version on success
Refuse to work on firmware <2v16 and remove old polyfills
0.65: Only display interpreter errors if log is nonzero

View File

@ -12,14 +12,13 @@ if (DEBUG) {
boot += "var _tm=Date.now()\n";
bootPost += "delete _tm;";
}
if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed
let CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT);
boot += `if(E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
} else {
let CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT);
boot += `if(E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
if (FWVERSION < 216) {
E.showMessage(/*LANG*/"Please update Bangle.js firmware\n\nCurrent = "+process.env.VERSION,{title:"ERROR"});
throw new Error("Old firmware");
}
boot += `{eval(require('Storage').read('bootupdate.js'));print("Storage Updated!")}else{\n`;
let CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.js$/)+E.CRC32(process.env.GIT_COMMIT);
boot += `if(E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.js$/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
boot += `{eval(require('Storage').read('bootupdate.js'));}else{\n`;
boot += `E.setFlags({pretokenise:1});\n`;
boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`;
bootPost += `NRF.setServices(bleServices,bleServiceOptions);delete bleServices,bleServiceOptions;\n`; // executed after other boot code
@ -44,7 +43,7 @@ LoopbackA.setConsole(true);\n`;
boot += `
Bluetooth.line="";
Bluetooth.on('data',function(d) {
var l = (Bluetooth.line + d).split(/[\\n\\r]/);
let l = (Bluetooth.line + d).split(/[\\n\\r]/);
Bluetooth.line = l.pop();
l.forEach(n=>Bluetooth.emit("line",n));
});
@ -67,12 +66,12 @@ if (s.ble===false) boot += `if (!NRF.getSecurityStatus().connected) NRF.sleep();
if (s.timeout!==undefined) boot += `Bangle.setLCDTimeout(${s.timeout});\n`;
if (!s.timeout) boot += `Bangle.setLCDPower(1);\n`;
boot += `E.setTimeZone(${s.timezone});`;
// Draw out of memory errors onto the screen
boot += `E.on('errorFlag', function(errorFlags) {
// Draw out of memory errors onto the screen if logging enabled
if (s.log) boot += `E.on('errorFlag', function(errorFlags) {
g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip();
print("Interpreter error:", errorFlags);
E.getErrorFlags(); // clear flags so we get called next time
});\n`;
E.getErrorFlags();
});\n`;// E.getErrorFlags() -> clear flags so we get called next time
// stop users doing bad things!
if (global.save) boot += `global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }\n`;
// Apply any settings-specific stuff
@ -86,30 +85,18 @@ if (s.bleprivacy || (s.passkey!==undefined && s.passkey.length==6)) {
if (s.blename === false) boot+=`NRF.setAdvertising({},{showName:false});\n`;
if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist) { let whitelist = (require('Storage').readJSON('setting.json',1)||{}).whitelist; if (NRF.resolveAddress !== undefined) { let resolvedAddr = NRF.resolveAddress(addr); if (resolvedAddr !== undefined) addr = resolvedAddr + " (resolved)"; } if (!whitelist.includes(addr)) NRF.disconnect(); }});\n`;
if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation
boot+=`Bangle.loadWidgets=function(){if(!global.WIDGETS)eval(require("Storage").read(".widcache"))};\n`;
// ================================================== FIXING OLDER FIRMWARES
if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted.
boot += `Bangle.on('mag',e=>{if(!isNaN(e.heading))e.heading=360-e.heading;});
Bangle.getCompass=(c=>(()=>{e=c();if(!isNaN(e.heading))e.heading=360-e.heading;return e;}))(Bangle.getCompass);`;
// deleting stops us getting confused by our own decl. builtins can't be deleted
// this is a polyfill without fastloading capability
delete Bangle.showClock;
if (!Bangle.showClock) boot += `Bangle.showClock = ()=>{load(".bootcde")};\n`;
delete Bangle.load;
if (!Bangle.load) boot += `Bangle.load = load;\n`;
let date = new Date();
delete date.toLocalISOString; // toLocalISOString was only introduced in 2v15
if (!date.toLocalISOString) boot += `Date.prototype.toLocalISOString = function() {
var o = this.getTimezoneOffset();
var d = new Date(this.getTime() - o*60000);
var sign = o>0?"-":"+";
o = Math.abs(o);
return d.toISOString().slice(0,-1)+sign+Math.floor(o/60).toString().padStart(2,0)+(o%60).toString().padStart(2,0);
};\n`;
// show timings
if (DEBUG) boot += `print(".boot0",0|(Date.now()-_tm),"ms");_tm=Date.now();\n`
// ================================================== BOOT.JS
// ================================================== .BOOT0
// Append *.boot.js files.
// Name files with a number - eg 'foo.5.boot.js' to enforce order (lowest first). Numbered files get placed before non-numbered
// These could change bleServices/bleServiceOptions if needed
@ -128,17 +115,47 @@ let bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
});
// precalculate file size
bootPost += "}";
let fileSize = boot.length + bootPost.length;
bootFiles.forEach(bootFile=>{
// match the size of data we're adding below in bootFiles.forEach
if (DEBUG) fileSize += 2+bootFile.length+1; // `//${bootFile}\n` comment
fileSize += require('Storage').read(bootFile).length+2; // boot code plus ";\n"
if (DEBUG) fileSize += 48+E.toJS(bootFile).length; // `print(${E.toJS(bootFile)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n`
});
// write file in chunks (so as not to use up all RAM)
require('Storage').write('.boot0',boot,0,fileSize);
let fileOffset = boot.length;
bootFiles.forEach(bootFile=>{
let fileOffset,fileSize;
/* code to output a file, plus preable and postable
when called with dst==undefined it just increments
fileOffset so we can see ho wbig the file has to be */
let outputFile = (dst,src,pre,post) => {"ram";
if (DEBUG) {
if (dst) require('Storage').write(dst,`//${src}\n`,fileOffset);
fileOffset+=2+src.length+1;
}
if (pre) {
if (dst) require('Storage').write(dst,pre,fileOffset);
fileOffset+=pre.length;
}
let f = require('Storage').read(src);
if (src.endsWith("clkinfo.js") && f[0]!="(") {
/* we shouldn't have to do this but it seems sometimes (sched 0.28) folks have
used libraries which get added into the clockinfo, and we can't use them directly
to we have to revert back to eval */
f = `eval(require('Storage').read(${E.toJS(src)}))`;
}
if (dst) {
// we can't just write 'f' in one go because it can be too big
let len = f.length;
let offset = 0;
while (len) {
let chunk = Math.min(len, 2048);
require('Storage').write(dst,f.substr(offset, chunk),fileOffset);
fileOffset+=chunk;
offset+=chunk;
len-=chunk;
}
} else
fileOffset+=f.length;
if (dst) require('Storage').write(dst,post,fileOffset);
fileOffset+=post.length;
if (DEBUG) {
if (dst) require('Storage').write(dst,`print(${E.toJS(src)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n`,fileOffset);
fileOffset += 48+E.toJS(src).length;
}
};
let outputFileComplete = (dst,fn) => {
// we add a semicolon so if the file is wrapped in (function(){ ... }()
// with no semicolon we don't end up with (function(){ ... }()(function(){ ... }()
// which would cause an error!
@ -146,31 +163,48 @@ bootFiles.forEach(bootFile=>{
// "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n";
// but we need to do this without ever loading everything into RAM as some
// boot files seem to be getting pretty big now.
if (DEBUG) {
require('Storage').write('.boot0',`//${bootFile}\n`,fileOffset);
fileOffset+=2+bootFile.length+1;
}
let bf = require('Storage').read(bootFile);
// we can't just write 'bf' in one go because at least in 2v13 and earlier
// Espruino wants to read the whole file into RAM first, and on Bangle.js 1
// it can be too big (especially BTHRM).
let bflen = bf.length;
let bfoffset = 0;
while (bflen) {
let bfchunk = Math.min(bflen, 2048);
require('Storage').write('.boot0',bf.substr(bfoffset, bfchunk),fileOffset);
fileOffset+=bfchunk;
bfoffset+=bfchunk;
bflen-=bfchunk;
}
require('Storage').write('.boot0',";\n",fileOffset);
fileOffset+=2;
if (DEBUG) {
require('Storage').write('.boot0',`print(${E.toJS(bootFile)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n`,fileOffset);
fileOffset += 48+E.toJS(bootFile).length
}
});
outputFile(dst,fn,"",";\n");
};
fileOffset = boot.length + bootPost.length;
bootFiles.forEach(fn=>outputFileComplete(undefined,fn)); // just get sizes
fileSize = fileOffset;
require('Storage').write('.boot0',boot,0,fileSize);
fileOffset = boot.length;
bootFiles.forEach(fn=>outputFileComplete('.boot0',fn));
require('Storage').write('.boot0',bootPost,fileOffset);
delete boot,bootPost,bootFiles;
// ================================================== .WIDCACHE for widgets
let widgetFiles = require("Storage").list(/\.wid\.js$/);
let widget = `// Made by bootupdate.js\nglobal.WIDGETS={};`, widgetPost = `var W=WIDGETS;WIDGETS={};
Object.keys(W).sort((a,b)=>(0|W[b].sortorder)-(0|W[a].sortorder)).forEach(k=>WIDGETS[k]=W[k]);`; // sort
if (DEBUG) widget+="var _tm=Date.now();";
outputFileComplete = (dst,fn) => {
outputFile(dst,fn,"try{",`}catch(e){print(${E.toJS(fn)},e,e.stack)}\n`);
};
fileOffset = widget.length + widgetPost.length;
widgetFiles.forEach(fn=>outputFileComplete(undefined,fn)); // just get sizes
fileSize = fileOffset;
require('Storage').write('.widcache',widget,0,fileSize);
fileOffset = widget.length;
widgetFiles.forEach(fn=>outputFileComplete('.widcache',fn));
require('Storage').write('.widcache',widgetPost,fileOffset);
delete widget,widgetPost,widgetFiles;
// ================================================== .clkinfocache for clockinfos
let ciFiles = require("Storage").list(/\.clkinfo\.js$/);
let ci = `// Made by bootupdate.js\n`;
if (DEBUG) ci+="var _tm=Date.now();";
outputFileComplete = (dst,fn) => {
outputFile(dst,fn,"try{let fn=",`;let a=fn(),b=menu.find(x=>x.name===a.name);if(b)b.items=b.items.concat(a.items)else menu=menu.concat(a);}catch(e){print(${E.toJS(fn)},e,e.stack)}\n`);
};
fileOffset = ci.length;
ciFiles.forEach(fn=>outputFileComplete(undefined,fn)); // just get sizes
fileSize = fileOffset;
require('Storage').write('.clkinfocache',ci,0,fileSize);
fileOffset = ci.length;
ciFiles.forEach(fn=>outputFileComplete('.clkinfocache',fn));
delete ci,ciFiles;
// test with require("clock_info").load()
// ================================================== END
E.showMessage(/*LANG*/"Reloading...");
}
// .bootcde should be run automatically after if required, since

View File

@ -1,7 +1,7 @@
{
"id": "boot",
"name": "Bootloader",
"version": "0.63",
"version": "0.65",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",
@ -11,6 +11,9 @@
{"name":".boot0","url":"boot0.js"},
{"name":".bootcde","url":"bootloader.js"},
{"name":"bootupdate.js","url":"bootupdate.js"}
],"data": [
{"name":".widcache"},
{"name":".clkinfocache"}
],
"sortorder": -10
}

View File

@ -91,4 +91,4 @@
});
E.showMenu(menu);
});
})

View File

@ -371,4 +371,4 @@
};
E.showMenu(buildMainMenu());
});
})

View File

@ -17,4 +17,4 @@
}
};
E.showMenu(menu);
});
})

View File

@ -91,4 +91,4 @@
settings = readSettings();
showMainMenu();
});
})

View File

@ -92,4 +92,4 @@
}
showMainMenu();
});
})

View File

@ -47,4 +47,4 @@
},
'< Back': back,
});
});
})

View File

@ -34,4 +34,4 @@
}
});
});
})

View File

@ -124,4 +124,4 @@
};
return info;
});
})

View File

@ -58,4 +58,4 @@
};
return info;
});
})

View File

@ -74,4 +74,4 @@
}
]
};
});
})

View File

@ -80,4 +80,4 @@
}
]
};
}) satisfies ClockInfoFunc
}) satisfies ClockInfoFunc // FIXME: semi-colon added automatically when Typescript generates Javascript file?

View File

@ -10,4 +10,6 @@
0.09: Save clkinfo settings on kill and remove
0.10: Fix focus bug when changing focus between two clock infos
0.11: Prepend swipe listener if possible
0.12: Add drawFilledImage to allow drawing icons with a separately coloured middle
0.12: Add drawFilledImage to allow drawing icons with a separately coloured middle
0.13: Cache loaded ClockInfos so if we have clockInfoWidget and a clock, we don't load them twice (saves ~300ms)
0.14: Check for .clkinfocache and use that if exists (from boot 0.64)

View File

@ -14,6 +14,8 @@ if (stepGoal == undefined) {
exports.loadCount = 0;
/// A list of all the instances returned by addInteractive
exports.clockInfos = [];
/// A list of loaded clockInfos
exports.clockInfoMenus = undefined;
/// Load the settings, with defaults
exports.loadSettings = function() {
@ -29,6 +31,8 @@ exports.loadSettings = function() {
/// Load a list of ClockInfos - this does not cache and reloads each time
exports.load = function() {
if (exports.clockInfoMenus)
return exports.clockInfoMenus;
var settings = exports.loadSettings();
delete settings.apps; // keep just the basic settings in memory
// info used for drawing...
@ -131,10 +135,14 @@ exports.load = function() {
hide : function() { clearInterval(this.interval); delete this.interval; },
});
}
var clkInfoCache = require('Storage').read('.clkinfocache');
if (clkInfoCache!==undefined) {
// note: code below is included in clkinfocache by bootupdate.js
// we use clkinfocache if it exists as it's faster
eval(clkInfoCache);
} else require("Storage").list(/clkinfo\.js$/).forEach(fn => {
// In case there exists already a menu object b with the same name as the next
// object a, we append the items. Otherwise we add the new object a to the list.
require("Storage").list(/clkinfo.js$/).forEach(fn => {
try{
var a = eval(require("Storage").read(fn))();
var b = menu.find(x => x.name === a.name);
@ -146,6 +154,7 @@ exports.load = function() {
});
// return it all!
exports.clockInfoMenus = menu;
return menu;
};
@ -345,6 +354,9 @@ exports.addInteractive = function(menu, options) {
menuHideItem(menu[options.menuA].items[options.menuB]);
exports.loadCount--;
delete exports.clockInfos[options.index];
// If nothing loaded now, clear our list of loaded menus
if (exports.loadCount==0)
exports.clockInfoMenus = undefined;
};
options.redraw = function() {
drawItem(menu[options.menuA].items[options.menuB]);

View File

@ -1,7 +1,7 @@
{ "id": "clock_info",
"name": "Clock Info Module",
"shortName": "Clock Info",
"version":"0.12",
"version":"0.14",
"description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)",
"icon": "app.png",
"type": "module",

View File

@ -4,4 +4,5 @@
0.04: More options for different background colors
'Plasma' generative background
Add a 'view' option in settings menu to view the current background
0.05: Random square+plasma speed improvements (~2x faster)
0.05: Random square+plasma speed improvements (~2x faster)
0.06: 25% speed improvement if Math.randInt exists (2v25 fw)

View File

@ -1,6 +1,7 @@
let settings;
exports.reload = function() {
//let t = Date.now();
settings = Object.assign({
style : "randomcolor",
colors : ["#F00","#0F0","#00F"]
@ -17,7 +18,8 @@ exports.reload = function() {
let bpp = (settings.colors.length>4)?4:2;
let bg = Graphics.createArrayBuffer(11,11,bpp,{msb:true});
let u32 = new Uint32Array(bg.buffer); // faster to do 1/4 of the ops of E.mapInPlace(bg.buffer, bg.buffer, ()=>Math.random()*256);
E.mapInPlace(u32, u32, function(r,n){"ram";return r()*n}.bind(null,Math.random,0x100000000)); // random pixels
if (Math.randInt) E.mapInPlace(u32, u32, Math.randInt); // random pixels
else E.mapInPlace(u32, u32, function(r,n){"ram";return r()*n}.bind(null,Math.random,0x100000000)); // random pixels
bg.buffer[bg.buffer.length-1]=Math.random()*256; // 11x11 isn't a multiple of 4 bytes - we need to set the last one!
bg.palette = new Uint16Array(1<<bpp);
bg.palette.set(settings.colors.map(c=>g.toColor(c)));
@ -28,7 +30,8 @@ exports.reload = function() {
settings.style = "image";
let bg = Graphics.createArrayBuffer(16,16,4,{msb:true});
let u32 = new Uint32Array(bg.buffer); // faster to do 1/4 of the ops of E.mapInPlace(bg.buffer, bg.buffer, ()=>Math.random()*256);
E.mapInPlace(u32, u32, function(r,n){"ram";return r()*n}.bind(null,Math.random,0x100000000)); // random pixels
if (Math.randInt) E.mapInPlace(u32, u32, Math.randInt); // random pixels
else E.mapInPlace(u32, u32, function(r,n){"ram";return r()*n}.bind(null,Math.random,0x100000000)); // random pixels
bg.filter([ // a gaussian filter to smooth out
1, 4, 7, 4, 1,
4,16,26,16, 4,
@ -42,6 +45,7 @@ exports.reload = function() {
settings.imgOpt = {scale:11};
delete settings.colors;
}
//console.log("bg",Date.now()-t);
};
exports.reload();

View File

@ -1,7 +1,7 @@
{ "id": "clockbg",
"name": "Clock Backgrounds",
"shortName":"Backgrounds",
"version": "0.05",
"version": "0.06",
"description": "Library that allows clocks to include a custom background (generated on demand or uploaded).",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}],

View File

@ -114,4 +114,4 @@
};
// Show the menu
E.showMenu(menu);
});
})

View File

@ -7,4 +7,4 @@
"showDate", "hideWidgets"
]);
E.showMenu(menu);
});
})

View File

@ -52,4 +52,4 @@
};
// Show the menu
E.showMenu(menu);
});
})

View File

@ -61,4 +61,4 @@
}
};
E.showMenu(menu);
});
})

View File

@ -56,4 +56,3 @@
}
});
})

View File

@ -54,4 +54,4 @@
},
'< Back': back
});
});
})

View File

@ -45,4 +45,4 @@
};
E.showMenu(appMenu);
});
})

View File

@ -41,4 +41,4 @@
};
E.showMenu(appMenu);
});
})

View File

@ -5,3 +5,4 @@
0.04: Enhance menu: enable bluetooth, visit settings & visit recovery
0.05: Enhance menu: permit toggling bluetooth
0.06: Display clock in green when charging, with "charging" text
0.07: Correctly restore full power when the charged threshold is reached

View File

@ -88,7 +88,7 @@ var reload = function () {
};
reload();
Bangle.emit("drained", E.getBattery());
var _a = require("Storage").readJSON("".concat(app, ".setting.json"), true) || {}, _b = _a.keepStartup, keepStartup = _b === void 0 ? true : _b, _c = _a.restore, restore = _c === void 0 ? 20 : _c, _d = _a.exceptions, exceptions = _d === void 0 ? ["widdst.0"] : _d;
var _a = require("Storage").readJSON("".concat(app, ".setting.json"), true) || {}, _b = _a.keepStartup, keepStartup = _b === void 0 ? true : _b, _c = _a.restore, restore = _c === void 0 ? 20 : _c, _d = _a.exceptions, exceptions = _d === void 0 ? ["widdst.0"] : _d, _e = _a.interval, interval = _e === void 0 ? 10 : _e;
function drainedRestore() {
if (!keepStartup) {
try {
@ -110,8 +110,10 @@ var checkCharge = function () {
if (Bangle.isCharging())
checkCharge();
Bangle.on("charging", function (charging) {
if (drainedInterval)
drainedInterval = clearInterval(drainedInterval);
if (charging)
checkCharge();
drainedInterval = setInterval(checkCharge, interval * 60 * 1000);
});
if (!keepStartup) {
var storage = require("Storage");

View File

@ -115,7 +115,7 @@ reload();
Bangle.emit("drained", E.getBattery());
// restore normal boot on charge
const { keepStartup = true, restore = 20, exceptions = ["widdst.0"] }: DrainedSettings
const { keepStartup = true, restore = 20, exceptions = ["widdst.0"], interval = 10 }: DrainedSettings
= require("Storage").readJSON(`${app}.setting.json`, true) || {};
// re-enable normal boot code when we're above a threshold:
@ -142,7 +142,10 @@ if (Bangle.isCharging())
checkCharge();
Bangle.on("charging", charging => {
if(charging) checkCharge();
if(drainedInterval)
drainedInterval = clearInterval(drainedInterval) as undefined;
if(charging)
drainedInterval = setInterval(checkCharge, interval * 60 * 1000);
});
if(!keepStartup){

View File

@ -1,7 +1,7 @@
{
"id": "drained",
"name": "Drained",
"version": "0.06",
"version": "0.07",
"description": "Switches to displaying a simple clock when the battery percentage is low, and disables some peripherals",
"readme": "README.md",
"icon": "icon.png",

View File

@ -87,4 +87,4 @@
});
};
updateAndRedraw();
});
})

View File

@ -55,4 +55,4 @@
E.showMenu(mainmenu);
});
})

View File

@ -30,3 +30,4 @@ when moving pages. Add caching for faster startups.
0.23: Bangle 1: Fix issue with missing icons, added touch screen interactions
0.24: Add buzz-on-interaction setting
0.25: Minor code improvements
0.26: Bangle 2: Postpone loading icons that are not needed initially.

View File

@ -33,10 +33,10 @@
s.writeJSON("launch.cache.json", launchCache);
}
let apps = launchCache.apps;
apps.forEach(app=>{
if (app.icon)
app.icon = s.read(app.icon); // should just be a link to a memory area
});
for (let i = 0; i < 4; i++) { // Initially only load icons for the current page.
if (apps[i].icon)
apps[i].icon = s.read(apps[i].icon); // should just be a link to a memory area
}
let Napps = apps.length;
let Npages = Math.ceil(Napps/4);
@ -101,6 +101,11 @@
Bangle.loadWidgets();
drawPage(0);
for (let i = 4; i < apps.length; i++) { // Load the rest of the app icons that were not initially.
if (apps[i].icon)
apps[i].icon = s.read(apps[i].icon); // should just be a link to a memory area
}
let swipeListenerDt = function(dirLeftRight, dirUpDown){
updateTimeoutToClock();
selected = -1;

View File

@ -1,7 +1,7 @@
{
"id": "dtlaunch",
"name": "Desktop Launcher",
"version": "0.25",
"version": "0.26",
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
"icon": "icon.png",

View File

@ -65,4 +65,4 @@
}
},
});
});
})

View File

@ -28,4 +28,4 @@
});
E.showMenu(menu);
});
})

View File

@ -248,4 +248,4 @@
});
};
showMainMenu();
});
})

View File

@ -29,4 +29,4 @@
}
// Initially show the menu
showMenu();
});
})

View File

@ -38,4 +38,4 @@
};
E.showMenu(menu);
});
})

View File

@ -45,4 +45,4 @@
},
};
E.showMenu(menu);
});
})

View File

@ -99,4 +99,4 @@
}
}
});
});
})

View File

@ -1,4 +1,4 @@
(function(back) {
// just go right to our app - we need all the memory
// just go right to our app - we need all the memory */
load("gpsrec.app.js");
})();
})()

View File

@ -1,4 +1,4 @@
(function(back) {
// just go right to our app
/* just go right to our app*/
load("gpssetup.app.js");
})();
})()

View File

@ -21,3 +21,4 @@
0.34: Fix 'fast load' so clock doesn't always redraw when screen unlocked/locked
0.35: Minor code improvements
0.36: Minor code improvements
0.37: Fix settings (show default as '90' not 'off')

View File

@ -2,7 +2,7 @@
"id": "hworldclock",
"name": "Hanks World Clock",
"shortName": "Hanks World Clock",
"version": "0.36",
"version": "0.37",
"description": "Current time zone plus up to three others",
"allow_emulator":true,
"icon": "app.png",

View File

@ -2,6 +2,7 @@
var FILE = "hworldclock.json";
var settings = Object.assign({
secondsOnUnlock: false,
rotationTarget: "90",
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
@ -55,4 +56,4 @@
E.showMenu(mainmenu);
});
})

View File

@ -50,4 +50,4 @@
},
};
E.showMenu(appMenu);
});
})

View File

@ -12,3 +12,4 @@
Broke out config loading into separate file to avoid duplicating a whole bunch of code
Added support for fast loading
0.10: Minor code improvements
0.11: Make sure variables are properly defined in settings.js

View File

@ -1,7 +1,7 @@
{
"id": "infoclk",
"name": "Informational clock",
"version": "0.10",
"version": "0.11",
"description": "A configurable clock with extra info and shortcuts when unlocked, but large time when locked",
"readme": "README.md",
"icon": "icon.png",
@ -41,4 +41,4 @@
"dependencies": {
"weather": "app"
}
}
}

View File

@ -17,6 +17,9 @@
} else return '' + hour;
}
let minute; // Is used in onchange functions. Defined here to appease the linter.
let hour; // Is used in onchange functions. Defined here to appease the linter.
// The menu for configuring when the seconds are shown
function showSecondsMenu() {
E.showMenu({
@ -744,4 +747,4 @@
}
showMainMenu();
});
})

View File

@ -1 +1,3 @@
0.01: New App!
0.02: Accents and extended mode characters
0.03: Bugfix - draw initial text after the back button has been removed

View File

@ -12,7 +12,14 @@ To display the in app character chart, long press the screen; you can scroll thr
For a full character chart see [EwChart.pdf](EwChart.pdf)
**Supported:** Letters (including capitals), numbers, backspace, word backspace, space, punctuation, new line, and some cursor controls (left, right, word left/right, home, end).
**Supported:** Letters (including capitals), numbers, backspace, word backspace, space, punctuation, new line, accents, extended mode (if characters are supported by the vector font), and some cursor controls (left, right, word left/right, home, end).
**Unsupported:** Extended mode, accents, and word-level stroking.
**Unsupported:** Word-level stroking.
## Settings
Font size can be selected in Settings app > "Apps" > "EdgeWrite Keyboard"
## Author
Woogal [github](https://github.com/retcurve)

View File

@ -193,17 +193,93 @@
"2425": "`",
"31": " \n",
"24": " ",
"46": "\xb7",
"432146": "\xb0",
"412346": "\xb0",
"123246": "\xae",
"2123246": "\xae",
"123146": "\xae",
"2123146": "\xae",
"2346": "\xac",
"32146": "\xa9",
"41236": "\xa2",
"24316": "\xd7",
"31246": "\xd7",
"316": "\xf7",
"136": "\xf7",
"232146": "\x80",
"23246": "\x80",
"132146": "\x80",
"412316": "\x80",
"323146": "\x80",
"324146": "\x80",
"24346": "\xa5",
"243416": "\xa5",
"2143416": "\xa5",
"34146": "\xf0",
"342146": "\xf0",
"343146": "\xf0",
"4214346": "\xf0",
"414346": "\xf0",
"434146": "\xf0",
"123416": "\xf0",
"2123416": "\xf0",
"1346": "\xe6",
"1246": "\xe6",
"13416": "\xe6",
"12416": "\xe6",
"3214346": "\xe6",
"21416": "\xdf",
"213416": "\xdf",
"212416": "\xdf",
"141216": "\xdf",
"1341216": "\xdf",
"121416": "\xdf",
"1232416": "\xdf",
"1231416": "\xdf",
"21232416": "\xdf",
"21231416": "\xdf",
"2321416": "\xdf",
"2146": "\xa3",
"21436": "\xb5",
"214346": "\xb5",
"121436": "\xb5",
"1214346": "\xb5",
"3214316": "\xf8",
"3412316": "\xf8",
"4126": "\xbf",
"216": "\xa1",
"346": "\xa6",
"21236": "\xb1",
"212326": "\xb1",
"31426": "\xa4",
"24136": "\xa4",
"3146": "\xab",
"2416": "\xbb",
"32": "#bs",
"41": "#wbs",
"12": "#pu-on",
"43": "#pu-on",
"325": "#pu-off",
"415": "#pu-off",
"42": "#ex-on",
"326": "#ex-off",
"416": "#ex-off",
"323": "#cur-left",
"232": "#cur-right",
"414": "#cur-word-left",
"141": "#cur-word-right",
"4141": "#cur-home",
"1414": "#cur-end"
"1414": "#cur-end",
"242": "#grave",
"313": "#acute",
"431": "#circumflex",
"421": "#circumflex",
"3421": "#tilde",
"43412": "#umlaut",
"43214": "#ring",
"41234": "#ring",
"142": "#cedilla",
"143": "#cedilla"
}

View File

@ -7,10 +7,20 @@ exports.input = function(options) {
let chartX = 0;
let chartY = 0;
let settings = Object.assign({
fontSize: 32,
}, require('Storage').readJSON("kbedgewrite.json", true));
let shouldShowWidgetBar = Bangle.appRect.y > 0;
options = options||{};
let text = options.text;
// Substring doesn't play well with UTF8
if (E.isUTF8(text)) {
text = E.decodeUTF8(text);
}
let wrappedText = '';
if ('string' != typeof text) text='';
// Colours for number of corner occurrences
@ -18,40 +28,140 @@ exports.input = function(options) {
const cornerSize = g.getWidth() / 3;
let punctuationMode = false;
let extendedMode = false;
let path = '';
let cursorPos = text.length;
let chartShown = false;
let characterSet = Object.assign({}, require('Storage').readJSON('kbedgewrite.charset.json', true) || {});
function draw() {
g.clearRect(Bangle.appRect).setClipRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2);
const accentedCharacters = {
'#grave': {
'a': String.fromCharCode(0xE0),
'A': String.fromCharCode(0xC0),
'e': String.fromCharCode(0xE8),
'E': String.fromCharCode(0xC8),
'i': String.fromCharCode(0xEC),
'I': String.fromCharCode(0xCC),
'o': String.fromCharCode(0xF2),
'O': String.fromCharCode(0xD2),
'u': String.fromCharCode(0xF9),
'U': String.fromCharCode(0xD9)
},
'#acute': {
'a': String.fromCharCode(0xE1),
'A': String.fromCharCode(0xC1),
'e': String.fromCharCode(0xE9),
'E': String.fromCharCode(0xC9),
'i': String.fromCharCode(0xED),
'I': String.fromCharCode(0xCD),
'o': String.fromCharCode(0xF3),
'O': String.fromCharCode(0xD3),
'u': String.fromCharCode(0xFA),
'U': String.fromCharCode(0xDA),
'y': String.fromCharCode(0xFD),
'Y': String.fromCharCode(0xDD)
},
'#circumflex': {
'a': String.fromCharCode(0xE2),
'A': String.fromCharCode(0xC2),
'e': String.fromCharCode(0xEA),
'E': String.fromCharCode(0xCA),
'i': String.fromCharCode(0xEE),
'I': String.fromCharCode(0xCE),
'o': String.fromCharCode(0xF4),
'O': String.fromCharCode(0xD4),
'u': String.fromCharCode(0xFB),
'U': String.fromCharCode(0xDB)
},
'#umlaut': {
'a': String.fromCharCode(0xE4),
'A': String.fromCharCode(0xC4),
'e': String.fromCharCode(0xEB),
'E': String.fromCharCode(0xCB),
'i': String.fromCharCode(0xEF),
'I': String.fromCharCode(0xCF),
'o': String.fromCharCode(0xF6),
'O': String.fromCharCode(0xD6),
'u': String.fromCharCode(0xFC),
'U': String.fromCharCode(0xDC),
'y': String.fromCharCode(0xFF)
},
'#tilde': {
'a': String.fromCharCode(0xE3),
'A': String.fromCharCode(0xC3),
'n': String.fromCharCode(0xF1),
'N': String.fromCharCode(0xD1),
'o': String.fromCharCode(0xF5),
'O': String.fromCharCode(0xD5)
},
'#ring': {
'a': String.fromCharCode(0xE5),
'A': String.fromCharCode(0xC5)
},
'#cedilla': {
'c': String.fromCharCode(0xE7),
'C': String.fromCharCode(0xC7)
},
// Draw the text string
let l = g.setFont('6x8:4').wrapString(text.substring(0, cursorPos) + '_' + text.substring(cursorPos), g.getWidth());
if (!l) l = [];
if (l.length>5) {
};
function wrapText() {
let stringToWrap = text.substring(0, cursorPos) + '_' + text.substring(cursorPos);
let l = [];
let startPos = 0;
g.setFont("Vector", settings.fontSize); // set the font so we can calculate a string width
// Wrap the string into array of lines that will fit the screen width
for (let i = 0; i < stringToWrap.length; i++) {
// wrap if string is too long or we hit a line break
if (stringToWrap.charCodeAt(i) == 10 || g.stringWidth(stringToWrap.substring(startPos, i+1)) > 176) {
l.push(stringToWrap.substring(startPos, i));
// skip the line break
if (stringToWrap.charCodeAt(i) == 10) {
i++;
}
startPos = i;
}
}
// Add the final line
l.push(stringToWrap.substring(startPos));
// Number of lines that can fit on the screen
let numLines = Math.floor(g.getHeight() / g.getFontHeight());
// If too many lines, reposition so the cursor can be seen
if (l.length > numLines) {
let textPos = 0;
let lineNum;
for (lineNum = 0; lineNum < l.length; lineNum++) {
textPos = textPos + l[lineNum].length - 1;
textPos = textPos + l[lineNum].length;
if (textPos >= cursorPos) break;
}
l=l.slice(lineNum - l.length - 5);
l=l.slice(lineNum - l.length - numLines + 1);
}
g.setColor(g.theme.fg);
g.setFontAlign(-1, -1, 0);
g.drawString(l.join('\n'), Bangle.appRect.x, Bangle.appRect.y);
// Draw punctuation flag
if (punctuationMode > 0) {
wrappedText = l.join('\n');
}
function draw() {
g.clearRect(Bangle.appRect).setClipRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2);
g.setColor(g.theme.fg);
g.setFont("Vector", settings.fontSize);
g.setFontAlign(-1, -1, 0);
g.drawString(wrappedText, Bangle.appRect.x, Bangle.appRect.y);
// Draw punctuation or extended flags
if (punctuationMode || extendedMode) {
let x = (g.getWidth() / 2) - 12;
let y = g.getHeight() - 32;
g.setColor('#F00');
g.setColor(punctuationMode ? '#F00' : '#0F0');
g.fillRect(x,y,x+24,y+32);
g.setColor('#FFF');
g.setFont('6x8:4');
g.drawString('P', x+4, y+4, false);
g.drawString(punctuationMode ? 'P' : 'E', x+4, y+4, false);
}
// Draw corners
@ -69,30 +179,27 @@ exports.input = function(options) {
}
function processPath() {
let capital = false;
// Punctuation paths end in 5
if (punctuationMode) {
path = path + '5';
}
// Capital letters end in 2, remove that and set a capital flag
// but only if the path isn't 232 (cursor right)
if (path != '232' && path.length > 2 && path.slice(-1) == '2') {
path = path.slice(0,-1);
capital = true;
// Extended paths end in 6
if (extendedMode) {
path = path + '6';
}
// Find character from path
let char = characterSet[path];
// Handle capitals
if (capital && char != 'undefined') {
if (char.charCodeAt(0)>96 && char.charCodeAt(0)<123) {
char = char.toUpperCase();
} else {
// Anything that can't be capitalised is an invalid path
char = undefined;
// Unknown character, but ends in a 2 so may be a capital letter
if (char == 'undefined' && path.slice(-1) == '2') {
// Remove the 2 and look for a letter
char = characterSet[path.slice(0,-1)];
// Handle capitals
if (char != 'undefined') {
if (char.charCodeAt(0)>96 && char.charCodeAt(0)<123) {
char = char.toUpperCase();
}
}
}
@ -129,6 +236,17 @@ exports.input = function(options) {
punctuationMode = false;
break;
}
// Enable extended mode
case '#ex-on': {
extendedMode = true;
break;
}
// Disable extended mode
case '#ex-off': {
extendedMode = false;
break;
}
// Cursor controls
case '#cur-left': {
if (cursorPos > 0) {
cursorPos--;
@ -168,6 +286,22 @@ exports.input = function(options) {
cursorPos = text.length;
break;
}
// Accents
case '#grave':
case '#acute':
case '#circumflex':
case '#umlaut':
case '#tilde':
case '#ring':
case '#cedilla':
// If the previous character can be accented, replace it with the accented version
if (cursorPos > 0) {
char = accentedCharacters[char][text.substring(cursorPos-1, cursorPos)];
if (char != 'undefined') {
text = text.substring(0, cursorPos-1) + char + text.substring(cursorPos);
}
}
break;
// Append character
default: {
text = text.substring(0, cursorPos) + char + text.substring(cursorPos);
@ -184,6 +318,7 @@ exports.input = function(options) {
if (!chartShown) {
if (e.b == 0) { // Finger lifted, process completed path
processPath();
wrapText();
draw();
} else {
let corner = 0;
@ -217,10 +352,6 @@ exports.input = function(options) {
}
};
// Draw initial string
require("widget_utils").hide();
g.setBgColor(g.theme.bg);
draw();
return new Promise((resolve,reject) => {
Bangle.setUI({
@ -249,6 +380,13 @@ exports.input = function(options) {
}
}
});
// Draw initial string
require("widget_utils").hide();
g.setBgColor(g.theme.bg);
wrapText();
draw();
});

View File

@ -1,14 +1,19 @@
{ "id": "kbedgewrite",
"name": "EdgeWrite keyboard",
"version":"0.01",
"version":"0.03",
"description": "A library for text input via EdgeWrite swipe gestures",
"icon": "app.png",
"type":"textinput",
"tags": "keyboard",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"screenshots" : [ { "url":"screenshot.png" } ],
"storage": [
{"name":"textinput","url":"lib.js"},
{"name":"kbedgewrite.charset.json","url":"characterset.json"}
{"name":"kbedgewrite.charset.json","url":"characterset.json"},
{"name":"kbedgewrite.settings.js","url":"settings.js"}
],
"data": [
{"name":"kbedgewrite.json"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,40 @@
(function(back) {
var FILE = 'kbedgewrite.json';
// Load settings
var settings = Object.assign({
fontSize: 32
}, require('Storage').readJSON(FILE, true) || {});
function setSetting(key,value) {
settings[key] = value;
require('Storage').writeJSON(FILE, settings);
}
// Helper method which uses int-based menu item for set of string values and their labels
function stringItems(key, startvalue, values, labels) {
return {
value: (startvalue === undefined ? 0 : values.indexOf(startvalue)),
format: v => labels[v],
min: 0,
max: values.length - 1,
wrap: true,
step: 1,
onchange: v => {
setSetting(key,values[v]);
}
};
}
// Helper method which breaks string set settings down to local settings object
function stringInSettings(name, values, labels) {
return stringItems(name,settings[name], values, labels);
}
// Show the menu
E.showMenu({
'' : { 'title' : 'EdgeWrite' },
'< Back' : () => back(),
'Font Size': stringInSettings('fontSize', [24, 32, 48], ['Small', 'Medium', 'Large'])
});
})

View File

@ -82,4 +82,4 @@
};
E.showMenu(mainMenu);
});
})

View File

@ -34,4 +34,4 @@
}
};
E.showMenu(appMenu);
});
})

View File

@ -515,12 +515,6 @@ module.exports = {
"no-undef"
]
},
"apps/widbt_notify/widget.js": {
"hash": "16372ffcbc6bd1419ca326c7da40c2195f82a4bfceb6f123c15872624c4f0adf",
"rules": [
"no-undef"
]
},
"apps/widbgjs/widget.js": {
"hash": "9852ce9aafb0a1ca3029d497282c8cdf07438ea36a3323313bad5b7569b1081b",
"rules": [
@ -534,7 +528,7 @@ module.exports = {
]
},
"apps/usgs/settings.js": {
"hash": "af1b7bc7e041c1e6988b407b6c8ee66dbd6a0e181a20caf102d2abdb6dbd5ac0",
"hash": "00ee672a6920f5667bfbd2988fd2853cfd579895a843ae036a00028dcb13878d",
"rules": [
"no-undef"
]
@ -1157,12 +1151,6 @@ module.exports = {
"no-undef"
]
},
"apps/banglebridge/widget.js": {
"hash": "4ee8d6749e1d0e28c58ad871fd9f6ccbca2d716bb4fbd3511ba4c34a6a5897e1",
"rules": [
"no-undef"
]
},
"apps/bad/bad.app.js": {
"hash": "d1354613102818190dd4e6e28fd715db7dc4d51b8e618cae61a3135529cc97eb",
"rules": [

View File

@ -177,7 +177,7 @@ var locales = {
speed: "kmh",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d. %b %Y", "1": "%d.%m.%Y" }, // 1. Mär 2020 // 01.03.20
abmonth: "Jan,Feb,Mär,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Dez",
@ -194,7 +194,7 @@ var locales = {
speed: "kmh",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%Y/%m/%d", 1: "%y/%m/%d" },
abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",
@ -210,7 +210,7 @@ var locales = {
speed: "kmh",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d %b %Y", 1: "%d-%m-%Y" }, // 28 feb 2020 // 28-02-2020
abday: "zo,ma,di,wo,do,vr,za",
@ -258,7 +258,7 @@ var locales = {
speed: "km/h",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d %B %Y", "1": "%d/%m/%Y" }, // 1 mars 2020 // 01/03/2020
abmonth: "janv,févr,mars,avril,mai,juin,juil,août,sept,oct,nov,déc",
@ -290,7 +290,7 @@ var locales = {
speed: 'km/h',
distance: { "0": "m", "1": "km" },
temperature: '°C',
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%B %d %Y", "1": "%Y-%m-%d" }, // March 1 2020 // 2020-03-01
abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",
@ -306,7 +306,7 @@ var locales = {
speed: "km/t",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d. %b. %Y", 1: "%d/%m %Y" }, // 1. feb. 2020 // 01/02 2020 // a better short ver. is 1/2 2020 but its not supported
abmonth: "jan,feb,mar,apr,maj,jun,jul,aug,sep,okt,nov,dec",
@ -322,7 +322,7 @@ var locales = {
speed: "km/h",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d. %b. %Y", 1: "%d/%m %Y" }, // 1. feb. 2020 // 01/02 2020 // a better short ver. is 1/2 2020 but its not supported
abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",
@ -370,7 +370,7 @@ var locales = {
speed: "kmh",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%A, %d. %B %Y", "1": "%d.%m.%y" }, // Sonntag, 1. März 2020 // 01.03.20
abmonth: "Jän,Feb,März,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Dez",
@ -403,7 +403,7 @@ var locales = {
speed: "kmh",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%A, %d de %B de %Y", "1": "%d/%m/%y" }, // domingo, 1 de marzo de 2020 // 01/03/20
abmonth: "ene,feb,mar,abr,may,jun,jul,ago,sept,oct,nov,dic",
@ -420,7 +420,7 @@ var locales = {
speed: "kmh",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%A %B %d %Y", "1": "%d/%m/%y" }, // dimanche 1 mars 2020 // 01/03/20
abmonth: "janv.,févr.,mars,avril,mai,juin,juil.,août,sept.,oct.,nov.,déc.",
@ -484,7 +484,7 @@ var locales = {
speed: 'kmh',
distance: { "0": "m", "1": "km" },
temperature: '°C',
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM.%SS", 1: "%HH:%MM" }, // 17:00.00 // 17:00
datePattern: { 0: "%d %b %Y", "1": "%d/%m/%Y" }, // 1 marzo 2020 // 01/03/2020
abmonth: "gen,feb,mar,apr,mag,giu,lug,ago,set,ott,nov,dic",
@ -500,7 +500,7 @@ var locales = {
speed: 'kmh',
distance: { "0": "m", "1": "km" },
temperature: '°C',
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM.%SS", 1: "%HH:%MM" }, // 17:00.00 // 17:00
datePattern: { 0: "%d %b %Y", "1": "%d/%m/%Y" }, // 1 marzo 2020 // 01/03/2020
abmonth: "gen,feb,mar,apr,mag,giu,lug,ago,set,ott,nov,dic",
@ -516,7 +516,7 @@ var locales = {
speed: 'kmh',
distance: { "0": "m", "1": "km" },
temperature: '°C',
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH.%MM.%SS", 1: "%HH.%MM" }, // 17.00.00 // 17.00
datePattern: { 0: "%A, %d. %B %Y", "1": "%Y-%m-%d" }, // Sunntag, 1. Märze 2020 // 2020-03-01
abmonth: "Jen,Hor,Mär,Abr,Mei,Brá,Hei,Öig,Her,Wím,Win,Chr",
@ -564,7 +564,7 @@ var locales = {
speed: "km/h",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%A %d %B de %Y", "1": "%d/%m/%Y" }, // dimenge 1 de març de 2020 // 01/03/2020
abmonth: "gen.,febr.,març,abril,mai,junh,julh,ago.,set.,oct.,nov.,dec.",
@ -660,7 +660,7 @@ var locales = {
speed: "kmh",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d. %b %Y", "1": "%d.%m.%Y" }, // 1. Mar 2021 // 01.03.2021
abmonth: "Sty,Lut,Mar,Kwi,Maj,Cze,Lip,Sie,Wrz,Paź,Lis,Gru",
@ -676,7 +676,7 @@ var locales = {
speed: "kmh",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d. %b %Y", "1": "%d.%m.%Y" }, // 1. Mar 2020 // 01.03.20
abmonth: "Jan,Feb,Mar,Apr,Mai,Jūn,Jūl,Aug,Sep,Okt,Nov,Dec",
@ -692,7 +692,7 @@ var locales = {
speed: "kmt",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d. %b %Y", "1": "%d.%m.%Y" }, // 1. Mar 2020 // 01.03.20
abmonth: "Jan,Feb,Mar,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Des",
@ -708,7 +708,7 @@ var locales = {
speed: "kmh",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d. %b %Y", "1": "%d.%m.%Y" }, // 1. Mar 2020 // 01.03.20
abmonth: "Jan,Feb,Mar,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Des",
@ -725,7 +725,7 @@ var locales = {
speed: "kmh",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d %B %Y", "1": "%d/%m/%y" },
abmonth: "gen.,febr.,març,abr.,maig,juny,jul.,ag.,set.,oct.,nov.,des.",

View File

@ -4,15 +4,15 @@
*/
const datetime_length_map = {
// %A, %a, %B, %b vary depending on the locale, so they are calculated later
"%Y": [4, 4],
"%y": [2, 2],
"%m": [2, 2],
"%-m": [1, 2],
"%d": [2, 2],
"%-d": [1, 2],
"%HH": [2, 2],
"%MM": [2, 2],
"%SS": [2, 2],
"%Y": [4, 4, "2024", "2024"],
"%y": [2, 2, "24", "24"],
"%m": [2, 2, "10", "10"],
"%-m": [1, 2, "1", "10"],
"%d": [2, 2, "10", "10"],
"%-d": [1, 2, "1", "10"],
"%HH": [2, 2, "10", "10"],
"%MM": [2, 2, "10", "10"],
"%SS": [2, 2, "10", "10"],
};
/**
@ -30,20 +30,21 @@ function getLengthOfDatetimeFormat(name, datetimeEspruino, locale, errors) {
["%a", locale.abday],
["%B", locale.month],
["%b", locale.abmonth],
]){
]) {
const length = [Infinity, 0];
for(const value of values.split(",")){
if(length[0] > value.length) length[0] = value.length;
if(length[1] < value.length) length[1] = value.length;
if(length[0] > value.length) { length[0] = value.length; length[2] = value; }
if(length[1] < value.length) { length[1] = value.length; length[3] = value; }
}
length_map[symbol] = length;
}
// Find the length of the output
let formatLength = [0, 0];
let formatLength = [0, 0, "", ""];
let i = 0;
while (i < datetimeEspruino.length) {
if (datetimeEspruino[i] === "%") {
let ch = datetimeEspruino[i];
if (ch === "%") {
let match;
for(const symbolLength of [2, 3]){
const length = length_map[datetimeEspruino.substring(i, i+symbolLength)];
@ -57,16 +58,22 @@ function getLengthOfDatetimeFormat(name, datetimeEspruino, locale, errors) {
if(match){
formatLength[0] += match.length[0];
formatLength[1] += match.length[1];
formatLength[2] += match.length[2];
formatLength[3] += match.length[3];
i += match.symbolLength;
}else{
errors.push({name, value: datetimeEspruino, lang: locale.lang, error: `uses an unsupported format symbol: ${datetimeEspruino.substring(i, i+3)}`});
formatLength[0]++;
formatLength[1]++;
formatLength[2]+=" ";
formatLength[3]+=" ";
i++;
}
} else {
formatLength[0]++;
formatLength[1]++;
formatLength[2]+=ch;
formatLength[3]+=ch;
i++;
}
}
@ -154,10 +161,10 @@ function checkLocale(locale, {speedUnits, distanceUnits, codePages, CODEPAGE_CON
function checkFormatLength(name, value, min, max) {
const length = getLengthOfDatetimeFormat(name, value, locale, errors);
if (min && length[0] < min) {
errors.push({name, value, lang: locale.lang, error: `output must be longer than ${min-1} characters`});
errors.push({name, value, lang: locale.lang, error: `output must be longer than ${min-1} characters (${length[2]} -> ${length[0]})`});
}
if (max && length[1] > max) {
errors.push({name, value, lang: locale.lang, error: `output must be shorter than ${max+1} characters`});
errors.push({name, value, lang: locale.lang, error: `output must be shorter than ${max+1} characters (${length[3]} -> ${length[1]})`});
}
}
function checkIsIn(name, value, listName, list) {

View File

@ -110,4 +110,5 @@
0.81: Fix issue stopping Music message for being marked as read
Make sure play button image is transparent
Add top-right menu to music playback to allow message to be deleted
0.82: Add option to not open the first unread message
0.82: Stop buzzing when a message is removed (e.g. call answered)
0.83: Add option to not open the first unread message

View File

@ -18,6 +18,10 @@ exports.listener = function(type, msg) {
clearTimeout(exports.messageTimeout);
delete exports.messageTimeout;
}
if (type==="clearAll") {
require("messages").stopBuzz();
return;
}
if (msg.t==="remove") {
// we won't open the UI for removed messages, so make sure to delete it from flash
if (Bangle.MESSAGES) {
@ -25,6 +29,8 @@ exports.listener = function(type, msg) {
require("messages").apply(msg, Bangle.MESSAGES);
if (!Bangle.MESSAGES.length) delete Bangle.MESSAGES;
}
if(type!=="music")
require("messages").stopBuzz();
return require("messages").save(msg); // always write removal to flash
}

View File

@ -2,7 +2,7 @@
"id": "messagegui",
"name": "Message UI",
"shortName": "Messages",
"version": "0.82",
"version": "0.83",
"description": "Default app to display notifications from iOS and Gadgetbridge/Android",
"icon": "app.png",
"type": "app",

View File

@ -136,4 +136,4 @@
}
showMainMenu();
});
})

View File

@ -102,4 +102,4 @@
},
};
E.showMenu(mainmenu);
});
})

View File

@ -23,5 +23,4 @@
},
};
E.showMenu(mainmenu);
});
})

View File

@ -63,4 +63,4 @@
}
E.showMenu(buildMainMenu());
});
})

View File

@ -45,4 +45,4 @@
},
};
E.showMenu(menu);
});
})

View File

@ -29,4 +29,4 @@
}
},
});
});
})

View File

@ -39,4 +39,4 @@
E.showMenu(mainmenu);
});
})

View File

@ -22,4 +22,4 @@
}
},
});
});
})

View File

@ -81,4 +81,4 @@
}
E.showMenu(buildMainMenu());
});
})

View File

@ -54,4 +54,4 @@
}
},
});
});
})

Some files were not shown because too many files have changed in this diff Show More