1
0
Fork 0

Merge branch 'master' into event_date_repeat

master
Gordon Williams 2023-03-07 11:23:13 +00:00 committed by GitHub
commit 00980611e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
390 changed files with 18315 additions and 10392 deletions

View File

@ -6,3 +6,6 @@ apps/qrcode/qr-scanner.umd.min.js
apps/gipy/pkg/gpconv.js
apps/health/chart.min.js
*.test.js
# typescript/generated files
apps/btadv/*.js

7
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "gitsubmodule"
directory: "/"
schedule:
interval: "daily"

View File

@ -3,3 +3,4 @@
0.03: Exit as first menu option, dont show decimal places for seconds
0.04: Localisation, change Exit->Back to allow back-arrow to appear on 2v13 firmware
0.05: Add max G values during recording, record actual G values and magnitude to CSV
0.06: Convert Yes/No On/Off in settings to checkboxes

View File

@ -26,8 +26,7 @@ function showMenu() {
viewLogs();
},
/*LANG*/"Log raw data" : {
value : logRawData,
format : v => v?/*LANG*/"Yes":/*LANG*/"No",
value : !!logRawData,
onchange : v => { logRawData=v; }
},
};

View File

@ -2,7 +2,7 @@
"id": "accellog",
"name": "Acceleration Logger",
"shortName": "Accel Log",
"version": "0.05",
"version": "0.06",
"description": "Logs XYZ acceleration data to a CSV file that can be downloaded to your PC",
"icon": "app.png",
"tags": "outdoor",

View File

@ -8,3 +8,4 @@
0.08: Use default Bangle formatter for booleans
0.09: New app screen (instead of showing settings or the alert) and some optimisations
0.10: Add software back button via setUI
0.11: Add setting to unlock screen

View File

@ -26,6 +26,12 @@
if (!(storage.readJSON('setting.json', 1) || {}).quiet) {
Bangle.buzz(400);
}
if ((storage.readJSON('activityreminder.s.json', 1) || {}).unlock) {
Bangle.setLocked(false);
Bangle.setLCDPower(1);
}
setTimeout(load, 20000);
}
@ -34,4 +40,4 @@
Bangle.drawWidgets();
run();
})();
})();

View File

@ -3,7 +3,7 @@
"name": "Activity Reminder",
"shortName":"Activity Reminder",
"description": "A reminder to take short walks for the ones with a sedentary lifestyle",
"version":"0.10",
"version":"0.11",
"icon": "app.png",
"type": "app",
"tags": "tool,activity",

View File

@ -75,7 +75,14 @@
settings.tempThreshold = v;
activityreminder.writeSettings(settings);
}
}
},
'Unlock on alarm': {
value: !!settings.unlock,
onchange: v => {
settings.unlock = v;
activityreminder.writeSettings(settings);
}
},
};
return mainMenu;

View File

@ -12,3 +12,4 @@
0.11: Setting to use "Today" and "Yesterday" instead of dates
Added dynamic, short and range fields to clkinfo
0.12: Added color field and updating clkinfo periodically (running events)
0.13: Show day of the week in date

View File

@ -34,8 +34,9 @@ function getDate(timestamp) {
return new Date(timestamp*1000);
}
function formatDay(date) {
let formattedDate = Locale.dow(date,1) + " " + Locale.date(date).replace(/\d\d\d\d/,"");
if (!settings.useToday) {
return Locale.date(date);
return formattedDate;
}
const dateformatted = date.toISOString().split('T')[0]; // yyyy-mm-dd
const today = new Date(Date.now()).toISOString().split('T')[0]; // yyyy-mm-dd
@ -46,7 +47,7 @@ function formatDay(date) {
if (dateformatted == tomorrow) {
return /*LANG*/"Tomorrow ";
}
return Locale.date(date);
return formattedDate;
}
}
function formatDateLong(date, includeDay, allDay) {
@ -58,7 +59,7 @@ function formatDateLong(date, includeDay, allDay) {
return shortTime;
}
function formatDateShort(date, allDay) {
return formatDay(date).replace(/\d\d\d\d/,"")+(allDay?"":Locale.time(date,1)+Locale.meridian(date));
return formatDay(date)+(allDay?"":Locale.time(date,1)+Locale.meridian(date));
}
var lines = [];
@ -75,25 +76,29 @@ function showEvent(ev) {
if (titleCnt) lines.push(""); // add blank line after title
if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth())
includeDay = false;
if(includeDay || ev.allDay) {
if(includeDay && ev.allDay) {
//single day all day (average to avoid getting previous day)
lines = lines.concat(
/*LANG*/"Start:",
g.wrapString(formatDateLong(new Date((start+end)/2), includeDay, ev.allDay), g.getWidth()-10));
} else if(includeDay || ev.allDay) {
lines = lines.concat(
/*LANG*/"Start"+":",
g.wrapString(formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10),
/*LANG*/"End:",
/*LANG*/"End"+":",
g.wrapString(formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10));
} else {
lines = lines.concat(
g.wrapString(Locale.date(start), g.getWidth()-10),
g.wrapString(formatDateShort(start,true), g.getWidth()-10),
g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10),
g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10));
}
if(ev.location)
lines = lines.concat(/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10));
if(ev.description)
lines = lines.concat("",/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10));
if(ev.description && ev.description.trim())
lines = lines.concat("",g.wrapString(ev.description, g.getWidth()-10));
if(ev.calName)
lines = lines.concat(/*LANG*/"Calendar"+": ", g.wrapString(ev.calName, g.getWidth()-10));
lines = lines.concat(["",/*LANG*/"< Back"]);
lines = lines.concat("",/*LANG*/"Calendar"+": ", g.wrapString(ev.calName, g.getWidth()-10));
lines = lines.concat("",/*LANG*/"< Back");
E.showScroller({
h : g.getFontHeight(), // height of each menu item in pixels
c : lines.length, // number of menu items
@ -120,7 +125,7 @@ function showList() {
CALENDAR = CALENDAR.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000);
}
if(CALENDAR.length == 0) {
E.showMessage("No events");
E.showMessage(/*LANG*/"No events");
return;
}
E.showScroller({

View File

@ -1,7 +1,7 @@
{
"id": "agenda",
"name": "Agenda",
"version": "0.12",
"version": "0.13",
"description": "Simple agenda",
"icon": "agenda.png",
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],

View File

@ -4,4 +4,5 @@
0.04: Use widget_utils module.
0.05: Support for clkinfo.
0.06: ClockInfo Fix: Use .get instead of .show as .show is not implemented for weather etc.
0.07: Use clock_info.addInteractive instead of a custom implementation
0.07: Use clock_info.addInteractive instead of a custom implementation
0.08: Use clock_info module as an app

View File

@ -3,9 +3,10 @@
"name": "AI Clock",
"shortName":"AI Clock",
"icon": "aiclock.png",
"version":"0.07",
"version":"0.08",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"dependencies" : { "clock_info":"module" },
"description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.",
"type": "clock",
"tags": "clock",

View File

@ -38,4 +38,7 @@
0.35: Add automatic translation of more strings
0.36: alarm widget moved out of app
0.37: add message input and dated Events
0.38: Dated event repeat option
0.38: Display date in locale
When switching 'repeat' from 'Workdays', 'Weekends' to 'Custom' preset Custom menu with previous selection
Display alarm label in delete prompt
0.39: Dated event repeat option

View File

@ -42,6 +42,14 @@ function handleFirstDayOfWeek(dow) {
// Check the first day of week and update the dow field accordingly (alarms only!)
alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow));
function getLabel(e) {
const dateStr = e.date && require("locale").date(new Date(e.date), 1);
return (e.timer
? require("time_utils").formatDuration(e.timer)
: (e.date ? `${e.date.substring(5,10)}${e.rp?"*":""} ${require("time_utils").formatTime(e.t)}` : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeRepeat(e)}` : ""))
) + (e.msg ? ` ${e.msg}` : "");
}
function showMainMenu() {
const menu = {
"": { "title": /*LANG*/"Alarms & Timers" },
@ -50,12 +58,7 @@ function showMainMenu() {
};
alarms.forEach((e, index) => {
var label = (e.timer
? require("time_utils").formatDuration(e.timer)
: (e.date ? `${e.date.substring(5,10)}${e.rp?"*":""} ${require("time_utils").formatTime(e.t)}` : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeRepeat(e)}` : ""))
) + (e.msg ? ` ${e.msg}` : "");
menu[label] = {
menu[getLabel(e)] = {
value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff),
onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index)
};
@ -185,7 +188,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
if (!isNew) {
menu[/*LANG*/"Delete"] = () => {
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => {
E.showPrompt(getLabel(alarm) + "\n" + /*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => {
if (confirm) {
alarms.splice(alarmIndex, 1);
saveAndReload();
@ -272,7 +275,7 @@ function showEditRepeatMenu(repeat, day, dowChangeCallback) {
},
/*LANG*/"Custom": {
value: isCustom ? decodeRepeat({ rp: true, dow: dow }) : false,
onchange: () => setTimeout(showCustomDaysMenu, 10, isCustom ? dow : EVERY_DAY, dowChangeCallback, originalRepeat, originalDow)
onchange: () => setTimeout(showCustomDaysMenu, 10, dow, dowChangeCallback, originalRepeat, originalDow)
}
};
} else {
@ -401,7 +404,7 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
if (!keyboard) delete menu[/*LANG*/"Message"];
if (!isNew) {
menu[/*LANG*/"Delete"] = () => {
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => {
E.showPrompt(getLabel(timer) + "\n" + /*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => {
if (confirm) {
alarms.splice(timerIndex, 1);
saveAndReload();

View File

@ -1,2 +1,3 @@
0.01: New App!
0.02: Actually upload correct code
0.03: Display sea-level pressure, too, and allow calibration

View File

@ -1,4 +1,4 @@
Bangle.setBarometerPower(true, "app");
Bangle.setBarometerPower(true, "altimeter");
g.clear(1);
Bangle.loadWidgets();
@ -10,21 +10,62 @@ var MEDIANLENGTH = 20;
var avr = [], median;
var value = 0;
function getStandardPressure(altitude) {
const P0 = 1013.25; // standard pressure at sea level in hPa
const T0 = 288.15; // standard temperature at sea level in K
const g0 = 9.80665; // standard gravitational acceleration in m/s^2
const R = 8.31432; // gas constant in J/(mol*K)
const M = 0.0289644; // molar mass of air in kg/mol
const L = -0.0065; // temperature lapse rate in K/m
const temperature = T0 + L * altitude; // temperature at the given altitude
const pressure = P0 * Math.pow((temperature / T0), (-g0 * M) / (R * L)); // pressure at the given altitude
return pressure;
}
function convertToSeaLevelPressure(pressure, altitude) {
return 1013.25 * (pressure / getStandardPressure(altitude));
}
Bangle.on('pressure', function(e) {
while (avr.length>MEDIANLENGTH) avr.pop();
avr.unshift(e.altitude);
median = avr.slice().sort();
g.reset().clearRect(0,y-30,g.getWidth()-10,y+30);
g.reset().clearRect(0,y-30,g.getWidth()-10,R.h);
if (median.length>10) {
var mid = median.length>>1;
value = E.sum(median.slice(mid-4,mid+5)) / 9;
g.setFont("Vector",50).setFontAlign(0,0).drawString((value-zero).toFixed(1), g.getWidth()/2, y);
t = value-zero;
if ((t > -100) && (t < 1000))
t = t.toFixed(1);
else
t = t.toFixed(0);
g.setFont("Vector",50).setFontAlign(0,0).drawString(t, g.getWidth()/2, y);
sea = convertToSeaLevelPressure(e.pressure, value-zero);
t = sea.toFixed(1) + " " + e.temperature.toFixed(1);
if (0) {
print("alt raw:", value.toFixed(1));
print("temperature:", e.temperature);
print("pressure:", e.pressure);
print("sea pressure:", sea);
print("std pressure:", getStandardPressure(value-zero));
}
g.setFont("Vector",25).setFontAlign(-1,0).drawString(t,
10, R.y+R.h - 35);
}
});
print(g.getFonts());
g.reset();
g.setFont("6x8").setFontAlign(0,0).drawString(/*LANG*/"ALTITUDE (m)", g.getWidth()/2, y-40);
g.setFont("Vector:15");
g.setFontAlign(0,0);
g.drawString(/*LANG*/"ALTITUDE (m)", g.getWidth()/2, y-40);
g.drawString(/*LANG*/"SEA L (hPa) TEMP (C)", g.getWidth()/2, y+62);
g.flip();
g.setFont("6x8").setFontAlign(0,0,3).drawString(/*LANG*/"ZERO", g.getWidth()-5, g.getHeight()/2);
setWatch(function() {
zero = value;
}, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat:true});
Bangle.setUI("updown", btn=> {
if (!btn) zero=value;
if (btn<0) zero-=5;
if (btn>0) zero+=5;
});

View File

@ -1,6 +1,6 @@
{ "id": "altimeter",
"name": "Altimeter",
"version":"0.02",
"version":"0.03",
"description": "Simple altimeter that can display height changed using Bangle.js 2's built in pressure sensor.",
"icon": "app.png",
"tags": "tool,outdoors",

View File

@ -20,3 +20,5 @@
0.19: Add automatic translation for a couple of strings.
0.20: Fix wrong event used for forwarded GPS data from Gadgetbridge and add mapper to map longitude value correctly.
0.21: Fix broken 'Messages' button in menu
0.22: Handle connection events for GPS forwarding from phone
0.23: Handle 'act' Gadgetbridge messages for realtime activity monitoring

View File

@ -3,8 +3,11 @@
Bluetooth.println("");
Bluetooth.println(JSON.stringify(message));
}
var lastMsg;
var lastMsg; // for music messages - may not be needed now...
var actInterval; // Realtime activity reporting interval when `act` is true
var actHRMHandler; // For Realtime activity reporting
// this settings var is deleted after this executes to save memory
var settings = require("Storage").readJSON("android.settings.json",1)||{};
//default alarm settings
if (settings.rp == undefined) settings.rp = true;
@ -60,6 +63,7 @@
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;
@ -92,6 +96,7 @@
},
//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 = [];
@ -102,6 +107,7 @@
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!
@ -110,11 +116,13 @@
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;
@ -127,21 +135,44 @@
else
request.r(event); //r = resolve function
},
// {t:"gps", lat, lon, alt, speed, course, time, satellites, hdop, externalSource:true }
"gps": function() {
const settings = require("Storage").readJSON("android.settings.json",1)||{};
if (!settings.overwriteGps) return;
delete event.t;
event.satellites = NaN;
event.course = NaN;
if (!isFinite(event.course)) event.course = NaN;
event.fix = 1;
if (event.long!==undefined) {
if (event.long!==undefined) { // for earlier Gadgetbridge implementations
event.lon = event.long;
delete event.long;
}
Bangle.emit('GPS', event);
},
// {t:"is_gps_active"}
"is_gps_active": function() {
gbSend({ t: "gps_power", status: Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0 });
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 });
lastSteps = steps;
}, event.int*1000);
}
};
var h = HANDLERS[event.t];
@ -178,21 +209,28 @@
},options.timeout||30000)};
});
return promise;
}
};
// Battery monitor
function sendBattery() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); }
Bangle.on("charging", sendBattery);
NRF.on("connect", () => setTimeout(function() {
sendBattery();
GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process
}, 2000));
Bangle.on("charging", sendBattery);
if (!settings.keep)
NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect
NRF.on("disconnect", () => {
// disable HRM/activity monitoring ('act' message)
GB({t:"act",stp:0,hrm:0,int:0}); // just call the handler to save duplication
// remove all messages on disconnect (if enabled)
var settings = require("Storage").readJSON("android.settings.json",1)||{};
if (!settings.keep)
require("messages").clearAll();
});
setInterval(sendBattery, 10*60*1000);
// Health tracking
Bangle.on('health', health=>{
gbSend({ t: "act", stp: health.steps, hrm: health.bpm });
if (actInterval===undefined) // if 'realtime' we do it differently
gbSend({ t: "act", stp: health.steps, hrm: health.bpm });
});
// Music control
Bangle.musicControl = cmd => {
@ -207,13 +245,39 @@
};
// GPS overwrite logic
if (settings.overwriteGps) { // if the overwrite option is set../
// Save current logic
const originalSetGpsPower = Bangle.setGPSPower;
const origSetGPSPower = Bangle.setGPSPower;
// migrate all GPS clients to the other variant on connection events
let handleConnection = (state) => {
if (Bangle.isGPSOn()){
let orig = Bangle._PWR.GPS;
delete Bangle._PWR.GPS;
origSetGPSPower(state);
Bangle._PWR.GPS = orig;
}
};
NRF.on('connect', ()=>{handleConnection(0);});
NRF.on('disconnect', ()=>{handleConnection(1);});
// Work around Serial1 for GPS not working when connected to something
let serialTimeout;
let wrap = function(f){
return (s)=>{
if (serialTimeout) clearTimeout(serialTimeout);
handleConnection(1);
f(s);
serialTimeout = setTimeout(()=>{
serialTimeout = undefined;
if (NRF.getSecurityStatus().connected) handleConnection(0);
}, 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) => {
// if not connected, use old logic
if (!NRF.getSecurityStatus().connected) return originalSetGpsPower(isOn, appID);
// Emulate old GPS power logic
// if not connected use internal GPS power function
if (!NRF.getSecurityStatus().connected) return origSetGPSPower(isOn, appID);
if (!Bangle._PWR) Bangle._PWR={};
if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];
if (!appID) appID="?";
@ -222,11 +286,15 @@
let pwr = Bangle._PWR.GPS.length>0;
gbSend({ t: "gps_power", status: pwr });
return pwr;
}
// Replace check if the GPS is on to check the _PWR variable
};
// Allow checking for GPS via GadgetBridge
Bangle.isGPSOn = () => {
return Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0;
}
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);
}
// remove settings object so it's not taking up RAM

View File

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

View File

@ -12,7 +12,7 @@
var mainmenu = {
"" : { "title" : "Android" },
"< Back" : back,
/*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" },
/*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?/*LANG*/"Yes":/*LANG*/"No" },
/*LANG*/"Find Phone" : () => E.showMenu({
"" : { "title" : /*LANG*/"Find Phone" },
"< Back" : ()=>E.showMenu(mainmenu),

126
apps/android/test.js Normal file
View File

@ -0,0 +1,126 @@
let result = true;
function assertTrue(condition, text) {
if (!condition) {
result = false;
print("FAILURE: " + text);
} else print("OK: " + text);
}
function assertFalse(condition, text) {
assertTrue(!condition, text);
}
function assertUndefinedOrEmpty(array, text) {
assertTrue(!array || array.length == 0, text);
}
function assertNotEmpty(array, text) {
assertTrue(array && array.length > 0, text);
}
let internalOn = () => {
return getPinMode((process.env.HWVERSION==2)?D30:D26) == "input";
};
let sec = {
connected: false
};
NRF.getSecurityStatus = () => sec;
setTimeout(() => {
// add an empty starting point to make the asserts work
Bangle._PWR={};
print("Not connected, should use internal GPS");
assertTrue(!NRF.getSecurityStatus().connected, "Not connected");
assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS");
assertFalse(Bangle.isGPSOn(), "isGPSOn");
assertTrue(Bangle.setGPSPower(1, "test"), "Switch GPS on");
assertNotEmpty(Bangle._PWR.GPS, "GPS");
assertTrue(Bangle.isGPSOn(), "isGPSOn");
assertTrue(internalOn(), "Internal GPS on");
assertFalse(Bangle.setGPSPower(0, "test"), "Switch GPS off");
assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS");
assertFalse(Bangle.isGPSOn(), "isGPSOn");
assertFalse(internalOn(), "Internal GPS off");
print("Connected, should use GB GPS");
sec.connected = true;
assertTrue(NRF.getSecurityStatus().connected, "Connected");
assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS");
assertFalse(Bangle.isGPSOn(), "isGPSOn");
assertFalse(internalOn(), "Internal GPS off");
assertTrue(Bangle.setGPSPower(1, "test"), "Switch GPS on");
assertNotEmpty(Bangle._PWR.GPS, "GPS");
assertTrue(Bangle.isGPSOn(), "isGPSOn");
assertFalse(internalOn(), "Internal GPS off");
assertFalse(Bangle.setGPSPower(0, "test"), "Switch GPS off");
assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS");
assertFalse(Bangle.isGPSOn(), "isGPSOn");
assertFalse(internalOn(), "Internal GPS off");
print("Connected, then reconnect cycle");
sec.connected = true;
assertTrue(NRF.getSecurityStatus().connected, "Connected");
assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS");
assertFalse(Bangle.isGPSOn(), "isGPSOn");
assertFalse(internalOn(), "Internal GPS off");
assertTrue(Bangle.setGPSPower(1, "test"), "Switch GPS on");
assertNotEmpty(Bangle._PWR.GPS, "GPS");
assertTrue(Bangle.isGPSOn(), "isGPSOn");
assertFalse(internalOn(), "Internal GPS off");
NRF.emit("disconnect", {});
print("disconnect");
sec.connected = false;
setTimeout(() => {
assertNotEmpty(Bangle._PWR.GPS, "GPS");
assertTrue(Bangle.isGPSOn(), "isGPSOn");
assertTrue(internalOn(), "Internal GPS on");
print("connect");
sec.connected = true;
NRF.emit("connect", {});
setTimeout(() => {
assertNotEmpty(Bangle._PWR.GPS, "GPS");
assertTrue(Bangle.isGPSOn(), "isGPSOn");
assertFalse(internalOn(), "Internal GPS off");
assertFalse(Bangle.setGPSPower(0, "test"), "Switch GPS off");
assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS");
assertFalse(Bangle.isGPSOn(), "isGPSOn");
assertFalse(internalOn(), "Internal GPS off");
setTimeout(() => {
print("Test disconnect without gps on");
assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS");
assertFalse(Bangle.isGPSOn(), "isGPSOn");
assertFalse(internalOn(), "Internal GPS off");
print("Result Overall is " + (result ? "OK" : "FAIL"));
}, 0);
}, 0);
}, 0);
}, 5000);

View File

@ -2,3 +2,4 @@
0.02: Update to work with Bangle.js 2
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

View File

@ -158,7 +158,7 @@
var chunk = bin.substr(i,chunkSize);
js += `\x10Serial1.write(atob("${btoa(chunk)}"))\n`;
}
js = "\x10setTimeout(() => Bangle.setGPSPower(0,'agps'), 1000);\n"; // turn GPS off after a delay
js += "\x10setTimeout(() => Bangle.setGPSPower(0,'agps'), 1000);\n"; // turn GPS off after a delay
return js;
}

View File

@ -1,7 +1,7 @@
{
"id": "assistedgps",
"name": "Assisted GPS Updater (AGPS)",
"version": "0.04",
"version": "0.05",
"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 +1,2 @@
0.01: New App!
0.02: Don't fire if the app uses swipes already.

23
apps/backswipe/README.md Normal file
View File

@ -0,0 +1,23 @@
Service that allows you to use an app's back button using left to right swipe gesture.
## Settings
Mode: Blacklist/Whitelist/Always On/Disabled
App List: Black-/whitelisted apps
Standard # of swipe handlers: 0-10 (Default: 0, must be changed for backswipe to work at all)
Standard # of drag handlers: 0-10 (Default: 0, must be changed for backswipe to work at all)
Standard # of handlers settings are used to fine tune when backswipe should trigger the back function. E.g. when using a keyboard that works on drags, we don't want the backswipe to trigger when we just wanted to select a letter. This might not be able to cover all cases however.
To get an indication for standard # of handlers `Bangle["#onswipe"]` and `Bangle["#ondrag"]` can be entered in the [Espruino Web IDE](https://www.espruino.com/ide) console field. They return `undefined` if no handler is active, a function if one is active, or a list of functions if multiple are active. Calling this on the clock app is a good start.
## TODO
- Possibly add option to tweak standard # of handlers on per app basis.
## Creator
Kedlub
## Contributors
thyttan

View File

@ -15,18 +15,28 @@
var currentFile = global.__FILE__ || "";
if(global.BACK) delete global.BACK;
if (global.BACK) delete global.BACK;
if (options && options.back && enabledForApp(currentFile)) {
global.BACK = options.back;
}
setUI(mode, cb);
};
function goBack(lr, ud) {
function countHandlers(eventType) {
if (Bangle["#on"+eventType] === undefined) {
return 0;
} else if (Bangle["#on"+eventType] instanceof Array) {
return Bangle["#on"+eventType].length;
} else if (Bangle["#on"+eventType] !== undefined) {
return 1;
}
}
function goBack(lr, _) {
// if it is a left to right swipe
if (lr === 1) {
// if we're in an app that has a back button, run the callback for it
if (global.BACK) {
if (global.BACK && countHandlers("swipe")<=settings.standardNumSwipeHandlers && countHandlers("drag")<=settings.standardNumDragHandlers) {
global.BACK();
}
}

View File

@ -1,7 +1,7 @@
{ "id": "backswipe",
"name": "Back Swipe",
"shortName":"BackSwipe",
"version":"0.01",
"version":"0.02",
"description": "Service that allows you to use an app's back button using left to right swipe gesture",
"icon": "app.png",
"tags": "back,gesture,swipe",

View File

@ -4,19 +4,21 @@
// Apps is an array of app info objects, where all the apps that are there are either blocked or allowed, depending on the mode
var DEFAULTS = {
'mode': 0,
'apps': []
'apps': [],
'standardNumSwipeHandlers': 0,
'standardNumDragHandlers': 0
};
var settings = {};
var loadSettings = function() {
settings = require('Storage').readJSON(FILE, 1) || DEFAULTS;
}
};
var saveSettings = function(settings) {
require('Storage').write(FILE, settings);
}
};
// Get all app info files
var getApps = function() {
var apps = require('Storage').list(/\.info$/).map(appInfoFileName => {
@ -35,8 +37,8 @@
return 0;
});
return apps;
}
};
var showMenu = function() {
var menu = {
'': { 'title': 'Backswipe' },
@ -55,11 +57,31 @@
},
'App List': () => {
showAppSubMenu();
},
'Standard # of swipe handlers' : { // If more than this many handlers are present backswipe will not go back
value: 0|settings.standardNumSwipeHandlers,
min: 0,
max: 10,
format: v=>v,
onchange: v => {
settings.standardNumSwipeHandlers = v;
saveSettings(settings);
},
},
'Standard # of drag handlers' : { // If more than this many handlers are present backswipe will not go back
value: 0|settings.standardNumDragHandlers,
min: 0,
max: 10,
format: v=>v,
onchange: v => {
settings.standardNumDragHandlers = v;
saveSettings(settings);
},
}
};
E.showMenu(menu);
}
};
var showAppSubMenu = function() {
var menu = {
@ -101,4 +123,4 @@
loadSettings();
showMenu();
})
})

View File

@ -0,0 +1 @@
0.01: New App!

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4ARkQAHBwsBiIACiAHBgQXIkAXJiIuKGAwWEC4cjmYABn//AAMyC63yC653FC6HwC5aQBC5ybIC44WChGAWxMgC44rCxGIZxYXFIoYXBGAQNCAAQXILYYXBGAUDBoK0EC5AsBC4QwEC5wAEC853BhAWDI6CPCFwp3OX4ouCC8xHXCAJ3VX94XCwBHVGIiPTU4oNCAAQWBX5gDBgQRCAAoXGGAUIFwQXHkAXHJIgABCw4IBC5sAiIAEiAgHAAQXLHBAYIC+6wJQYIADgIXGGBJ3FC4iOBAH4A/ACAA=="))

41
apps/blescanner/app.js Normal file
View File

@ -0,0 +1,41 @@
E.showMessage("Scanning...");
var devices = [];
setInterval(function() {
NRF.findDevices(function(devs) {
devs.forEach(dev=>{
var existing = devices.find(d=>d.id==dev.id);
if (existing) {
existing.timeout = 0;
existing.rssi = (existing.rssi*3 + dev.rssi)/4;
} else {
dev.timeout = 0;
dev.new = 0;
devices.push(dev);
}
});
devices.forEach(d=>{d.timeout++;d.new++});
devices = devices.filter(dev=>dev.timeout<8);
devices.sort((a,b)=>b.rssi - a.rssi);
g.clear(1).setFont("12x20");
var wasNew = false;
devices.forEach((d,y)=>{
y*=20;
var n = d.name;
if (!n) n=d.id.substr(0,22);
if (d.new<4) {
g.fillRect(0,y,g.getWidth(),y+19);
g.setColor(g.theme.bg);
if (d.rssi > -70) wasNew = true;
} else {
g.setColor(g.theme.fg);
}
g.setFontAlign(-1,-1);
g.drawString(n,0,y);
g.setFontAlign(1,-1);
g.drawString(0|d.rssi,g.getWidth()-1,y);
});
g.flip();
Bangle.setLCDBrightness(wasNew);
}, 1200);
}, 1500);

BIN
apps/blescanner/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,14 @@
{ "id": "blescanner",
"name": "BLE Scanner",
"shortName":"BLE Scan",
"version":"0.01",
"description": "Scans for bluetooth devices nearby and shows their names on the screen ordered by signal strength. The most recently discovered items are highlighted.",
"icon": "app.png",
"screenshots" : [ { "url":"screenshot.png" } ],
"tags": "tool,bluetooth",
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"blescanner.app.js","url":"app.js"},
{"name":"blescanner.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -64,3 +64,5 @@
0.55: Add toLocalISOString polyfill for pre-2v15 firmwares
Only add boot info comments if settings.bootDebug was set
If settings.bootDebug is set, output timing for each section of .boot0
0.56: Settings.log = 0,1,2,3 for off,display, log, both
0.57: Handle the whitelist being disabled

View File

@ -32,14 +32,12 @@ if (s.ble!==false) {
boot += `bleServiceOptions.hid=Bangle.HID;\n`;
}
}
if (s.log==2) { // logging to file
boot += `_DBGLOG=require("Storage").open("log.txt","a");
`;
} if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth
if (s.log==2) boot += `_DBGLOG=require("Storage").open("log.txt","a");
LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);});
// settings.log 0-off, 1-display, 2-log, 3-both
if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth
if (s.log>=2) { boot += `_DBGLOG=require("Storage").open("log.txt","a");
LoopbackB.on('data',function(d) {_DBGLOG.write(d);${(s.log==3)?"Terminal.write(d);":""}});
LoopbackA.setConsole(true);\n`;
else if (s.log) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal
} else if (s.log==1) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal
else boot += `E.setConsole(null,{force:true});\n`; // on new (2v05+) firmware we have E.setConsole which allows a 'null' console
/* If not programmable add our own handler for Bluetooth data
to allow Gadgetbridge commands to be received*/
@ -56,10 +54,10 @@ Bluetooth.on('line',function(l) {
try { global.GB(JSON.parse(l.slice(3,-1))); } catch(e) {}
});\n`;
} else {
if (s.log==2) boot += `_DBGLOG=require("Storage").open("log.txt","a");
LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);});
if (s.log>=2) boot += `_DBGLOG=require("Storage").open("log.txt","a");
LoopbackB.on('data',function(d) {_DBGLOG.write(d);${(s.log==3)?"Terminal.write(d);":""}});
if (!NRF.getSecurityStatus().connected) LoopbackA.setConsole();\n`;
else if (s.log) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection)
else if (s.log==1) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection)
else boot += `Bluetooth.setConsole(true);\n`; // else if no debug, force REPL to Bluetooth
}
// we just reset, so BLE should be on.
@ -81,7 +79,7 @@ if (global.save) boot += `global.save = function() { throw new Error("You can't
if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`;
if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`;
if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`;
if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`;
if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation
// ================================================== FIXING OLDER FIRMWARES
if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted.

View File

@ -1,7 +1,7 @@
{
"id": "boot",
"name": "Bootloader",
"version": "0.55",
"version": "0.57",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",

1
apps/btadv/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New app!

16
apps/btadv/README.md Normal file
View File

@ -0,0 +1,16 @@
# Bluetooth Advert
This app advertises and exports (over Bluetooth) live data from the bangle's sensors:
- Heart Rate
- Accelerometer readings
- Pressure
- GPS information
- Magnetic flux
Swipe in any direction to access settings, and tap a setting to toggle it.
Hit back to return to the details screen, which shows sensor data being exported.
# TypeScript
This app is written in TypeScript, see [typescript/README.md](/typescript/README.md) for more info

412
apps/btadv/app.js Normal file
View File

@ -0,0 +1,412 @@
"use strict";
var __assign = Object.assign;
var Layout = require("Layout");
Bangle.loadWidgets();
Bangle.drawWidgets();
var HRM_MIN_CONFIDENCE = 75;
var services = ["0x180d", "0x181a", "0x1819"];
var acc;
var bar;
var gps;
var hrm;
var hrmAny;
var mag;
var btnsShown = false;
var prevBtnsShown = undefined;
var hrmAnyClear;
var settings = {
bar: false,
gps: false,
hrm: false,
mag: false,
};
var idToName = {
acc: "Acceleration",
bar: "Barometer",
gps: "GPS",
hrm: "HRM",
mag: "Magnetometer",
};
var infoFont = "6x8:2";
var colour = {
on: "#0f0",
off: "#fff",
};
var makeToggle = function (id) { return function () {
settings[id] = !settings[id];
var entry = btnLayout[id];
var col = settings[id] ? colour.on : colour.off;
entry.btnBorder = entry.col = col;
btnLayout.update();
btnLayout.render();
enableSensors();
}; };
var btnStyle = {
font: "Vector:14",
fillx: 1,
filly: 1,
col: g.theme.fg,
bgCol: g.theme.bg,
btnBorder: "#fff",
};
var btnLayout = new Layout({
type: "v",
c: [
{
type: "h",
c: [
__assign({ type: "btn", label: idToName.bar, id: "bar", cb: makeToggle('bar') }, btnStyle),
__assign({ type: "btn", label: idToName.gps, id: "gps", cb: makeToggle('gps') }, btnStyle),
]
},
{
type: "h",
c: [
__assign({ type: "btn", label: idToName.hrm, id: "hrm", cb: makeToggle('hrm') }, btnStyle),
__assign({ type: "btn", label: idToName.mag, id: "mag", cb: makeToggle('mag') }, btnStyle),
]
},
{
type: "h",
c: [
__assign(__assign({ type: "btn", label: idToName.acc, id: "acc", cb: function () { } }, btnStyle), { col: colour.on, btnBorder: colour.on }),
__assign({ type: "btn", label: "Back", cb: function () {
setBtnsShown(false);
} }, btnStyle),
]
}
]
}, {
lazy: true,
back: function () {
setBtnsShown(false);
},
});
var setBtnsShown = function (b) {
btnsShown = b;
hook(!btnsShown);
setIntervals();
redraw();
};
var drawInfo = function (force) {
var _a = Bangle.appRect, y = _a.y, x = _a.x, w = _a.w;
var mid = x + w / 2;
var drawn = false;
if (!force && !bar && !gps && !hrm && !mag)
return;
g.reset()
.clearRect(Bangle.appRect)
.setFont(infoFont)
.setFontAlign(0, -1);
if (bar) {
g.drawString("".concat(bar.altitude.toFixed(1), "m"), mid, y);
y += g.getFontHeight();
g.drawString("".concat(bar.pressure.toFixed(1), " hPa"), mid, y);
y += g.getFontHeight();
g.drawString("".concat(bar.temperature.toFixed(1), "C"), mid, y);
y += g.getFontHeight();
drawn = true;
}
if (gps) {
g.drawString("".concat(gps.lat.toFixed(4), " lat, ").concat(gps.lon.toFixed(4), " lon"), mid, y);
y += g.getFontHeight();
g.drawString("".concat(gps.alt, "m (").concat(gps.satellites, " sat)"), mid, y);
y += g.getFontHeight();
drawn = true;
}
if (hrm) {
g.drawString("".concat(hrm.bpm, " BPM (").concat(hrm.confidence, "%)"), mid, y);
y += g.getFontHeight();
drawn = true;
}
else if (hrmAny) {
g.drawString("~".concat(hrmAny.bpm, " BPM (").concat(hrmAny.confidence, "%)"), mid, y);
y += g.getFontHeight();
drawn = true;
if (!settings.hrm && !hrmAnyClear) {
hrmAnyClear = setTimeout(function () {
hrmAny = undefined;
hrmAnyClear = undefined;
}, 10000);
}
}
if (mag) {
g.drawString("".concat(mag.x, " ").concat(mag.y, " ").concat(mag.z), mid, y);
y += g.getFontHeight();
g.drawString("heading: ".concat(mag.heading.toFixed(1)), mid, y);
y += g.getFontHeight();
drawn = true;
}
if (!drawn) {
if (!force || Object.values(settings).every(function (x) { return !x; })) {
g.drawString("swipe to enable", mid, y);
}
else {
g.drawString("events pending", mid, y);
}
y += g.getFontHeight();
}
};
var onTap = function () {
setBtnsShown(true);
};
var redraw = function () {
if (btnsShown) {
if (!prevBtnsShown) {
prevBtnsShown = btnsShown;
Bangle.removeListener("swipe", onTap);
btnLayout.setUI();
btnLayout.forgetLazyState();
g.clearRect(Bangle.appRect);
}
btnLayout.render();
}
else {
if (prevBtnsShown) {
prevBtnsShown = btnsShown;
Bangle.setUI();
Bangle.on("swipe", onTap);
drawInfo(true);
}
else {
drawInfo();
}
}
};
var encodeHrm = function (hrm) {
return [0, hrm.bpm];
};
encodeHrm.maxLen = 2;
var encodePressure = function (data) {
return toByteArray(Math.round(data.pressure * 10), 4, false);
};
encodePressure.maxLen = 4;
var encodeElevation = function (data) {
return toByteArray(Math.round(data.altitude * 100), 3, true);
};
encodeElevation.maxLen = 3;
var encodeTemp = function (data) {
return toByteArray(Math.round(data.temperature * 10), 2, true);
};
encodeTemp.maxLen = 2;
var encodeGps = function (data) {
var speed = toByteArray(Math.round(1000 * data.speed / 36), 2, false);
var lat = toByteArray(Math.round(data.lat * 10000000), 4, true);
var lon = toByteArray(Math.round(data.lon * 10000000), 4, true);
var elevation = toByteArray(Math.round(data.alt * 100), 3, true);
var heading = toByteArray(Math.round(data.course * 100), 2, false);
return [
157,
2,
speed[0], speed[1],
lat[0], lat[1], lat[2], lat[3],
lon[0], lon[1], lon[2], lon[3],
elevation[0], elevation[1], elevation[2],
heading[0], heading[1]
];
};
encodeGps.maxLen = 17;
var encodeGpsHeadingOnly = function (data) {
var heading = toByteArray(Math.round(data.heading * 100), 2, false);
return [
16,
16,
heading[0], heading[1]
];
};
encodeGpsHeadingOnly.maxLen = 17;
var encodeMag = function (data) {
var x = toByteArray(data.x, 2, true);
var y = toByteArray(data.y, 2, true);
var z = toByteArray(data.z, 2, true);
return [x[0], x[1], y[0], y[1], z[0], z[1]];
};
encodeMag.maxLen = 6;
var toByteArray = function (value, numberOfBytes, isSigned) {
var byteArray = new Array(numberOfBytes);
if (isSigned && (value < 0)) {
value += 1 << (numberOfBytes * 8);
}
for (var index = 0; index < numberOfBytes; index++) {
byteArray[index] = (value >> (index * 8)) & 0xff;
}
return byteArray;
};
var enableSensors = function () {
Bangle.setBarometerPower(settings.bar, "btadv");
if (!settings.bar)
bar = undefined;
Bangle.setGPSPower(settings.gps, "btadv");
if (!settings.gps)
gps = undefined;
Bangle.setHRMPower(settings.hrm, "btadv");
if (!settings.hrm)
hrm = hrmAny = undefined;
Bangle.setCompassPower(settings.mag, "btadv");
if (!settings.mag)
mag = undefined;
};
var haveServiceData = function (serv) {
switch (serv) {
case "0x180d": return !!hrm;
case "0x181a": return !!(bar || mag);
case "0x1819": return !!(gps && gps.lat && gps.lon || mag);
}
};
var serviceToAdvert = function (serv, initial) {
var _a, _b, _c;
if (initial === void 0) { initial = false; }
switch (serv) {
case "0x180d":
if (hrm || initial) {
var o = {
maxLen: encodeHrm.maxLen,
readable: true,
notify: true,
};
if (hrm) {
o.value = encodeHrm(hrm);
hrm = undefined;
}
return _a = {}, _a["0x2a37"] = o, _a;
}
return {};
case "0x1819":
if (gps || initial) {
var o = {
maxLen: encodeGps.maxLen,
readable: true,
notify: true,
};
if (gps) {
o.value = encodeGps(gps);
gps = undefined;
}
return _b = {}, _b["0x2a67"] = o, _b;
}
else if (mag) {
var o = {
maxLen: encodeGpsHeadingOnly.maxLen,
readable: true,
notify: true,
value: encodeGpsHeadingOnly(mag),
};
return _c = {}, _c["0x2a67"] = o, _c;
}
return {};
case "0x181a": {
var o = {};
if (bar || initial) {
o["0x2a6c"] = {
maxLen: encodeElevation.maxLen,
readable: true,
notify: true,
};
o["0x2A1F"] = {
maxLen: encodeTemp.maxLen,
readable: true,
notify: true,
};
o["0x2a6d"] = {
maxLen: encodePressure.maxLen,
readable: true,
notify: true,
};
if (bar) {
o["0x2a6c"].value = encodeElevation(bar);
o["0x2A1F"].value = encodeTemp(bar);
o["0x2a6d"].value = encodePressure(bar);
bar = undefined;
}
}
if (mag || initial) {
o["0x2aa1"] = {
maxLen: encodeMag.maxLen,
readable: true,
notify: true,
};
if (mag) {
o["0x2aa1"].value = encodeMag(mag);
}
}
return o;
}
}
};
var getBleAdvert = function (map, all) {
if (all === void 0) { all = false; }
var advert = {};
for (var _i = 0, services_1 = services; _i < services_1.length; _i++) {
var serv = services_1[_i];
if (all || haveServiceData(serv)) {
advert[serv] = map(serv);
}
}
mag = undefined;
return advert;
};
var updateServices = function () {
var newAdvert = getBleAdvert(serviceToAdvert);
NRF.updateServices(newAdvert);
};
var onAccel = function (newAcc) { return acc = newAcc; };
var onPressure = function (newBar) { return bar = newBar; };
var onGPS = function (newGps) { return gps = newGps; };
var onHRM = function (newHrm) {
if (newHrm.confidence >= HRM_MIN_CONFIDENCE)
hrm = newHrm;
hrmAny = newHrm;
};
var onMag = function (newMag) { return mag = newMag; };
var hook = function (enable) {
if (enable) {
Bangle.on("accel", onAccel);
Bangle.on("pressure", onPressure);
Bangle.on("GPS", onGPS);
Bangle.on("HRM", onHRM);
Bangle.on("mag", onMag);
}
else {
Bangle.removeListener("accel", onAccel);
Bangle.removeListener("pressure", onPressure);
Bangle.removeListener("GPS", onGPS);
Bangle.removeListener("HRM", onHRM);
Bangle.removeListener("mag", onMag);
}
};
var setIntervals = function (locked, connected) {
if (locked === void 0) { locked = Bangle.isLocked(); }
if (connected === void 0) { connected = NRF.getSecurityStatus().connected; }
changeInterval(redrawInterval, locked ? 15000 : 5000);
if (connected) {
var interval = btnsShown ? 5000 : 1000;
if (bleInterval) {
changeInterval(bleInterval, interval);
}
else {
bleInterval = setInterval(updateServices, interval);
}
}
else if (bleInterval) {
clearInterval(bleInterval);
bleInterval = undefined;
}
};
var redrawInterval = setInterval(redraw, 1000);
Bangle.on("lock", function (locked) { return setIntervals(locked); });
var bleInterval;
NRF.on("connect", function () { return setIntervals(undefined, true); });
NRF.on("disconnect", function () { return setIntervals(undefined, false); });
setIntervals();
setBtnsShown(true);
enableSensors();
{
var ad = getBleAdvert(function (serv) { return serviceToAdvert(serv, true); }, true);
var adServices = Object
.keys(ad)
.map(function (k) { return k.replace("0x", ""); });
NRF.setServices(ad, {
advertise: adServices,
uart: false,
});
}

715
apps/btadv/app.ts Normal file
View File

@ -0,0 +1,715 @@
// ts helpers:
const __assign = Object.assign;
const Layout = require("Layout") as Layout_.Layout;
Bangle.loadWidgets();
Bangle.drawWidgets();
const enum Intervals {
// BLE_ADVERT = 60 * 1000,
BLE = 1000, // info screen
BLE_BACKGROUND = 5000, // button screen
UI_INFO = 5 * 1000, // info refresh, wake
UI_INFO_SLEEP = 15 * 1000, // info refresh, asleep
}
type Hrm = { bpm: number, confidence: number };
const HRM_MIN_CONFIDENCE = 75;
// https://github.com/sputnikdev/bluetooth-gatt-parser/blob/master/src/main/resources/gatt/
const enum BleServ {
// org.bluetooth.service.heart_rate
// contains: HRM
HRM = "0x180d",
// org.bluetooth.service.environmental_sensing
// contains: Elevation, Temp(Celsius), Pressure, Mag
EnvSensing = "0x181a",
// org.bluetooth.service.location_and_navigation
// contains: LocationAndSpeed
LocationAndNavigation = "0x1819",
// Acc // none known for this
}
const services = [BleServ.HRM, BleServ.EnvSensing, BleServ.LocationAndNavigation];
const enum BleChar {
// org.bluetooth.characteristic.heart_rate_measurement
// <see encode function>
HRM = "0x2a37",
// org.bluetooth.characteristic.elevation
// s24, meters 0.01
Elevation = "0x2a6c",
// org.bluetooth.characteristic.temperature
// s16 *10^2
Temp = "0x2a6e",
// org.bluetooth.characteristic.temperature_celsius
// s16 *10^2
TempCelsius = "0x2A1F",
// org.bluetooth.characteristic.pressure
// u32 *10
Pressure = "0x2a6d",
// org.bluetooth.characteristic.location_and_speed
// <see encodeGps>
LocationAndSpeed = "0x2a67",
// org.bluetooth.characteristic.magnetic_flux_density_3d
// s16: x, y, z, tesla (10^-7)
MagneticFlux3D = "0x2aa1",
}
type BleCharAdvert = {
value?: Array<number>,
readable?: true,
notify?: true,
indicate?: true, // notify + ACK
maxLen?: number,
};
type BleServAdvert = {
[key in BleChar]?: BleCharAdvert;
};
type LenFunc<T> = {
(_: T): Array<number>,
maxLen: number,
}
let acc: undefined | AccelData;
let bar: undefined | PressureData;
let gps: undefined | GPSFix;
let hrm: undefined | Hrm;
let hrmAny: undefined | Hrm;
let mag: undefined | CompassData;
let btnsShown = false;
let prevBtnsShown: boolean | undefined = undefined;
let hrmAnyClear: undefined | number;
type BtAdvType<IncludeAcc = false> = "bar" | "gps" | "hrm" | "mag" | (IncludeAcc extends true ? "acc" : never);
type BtAdvMap<T, IncludeAcc = false> = { [key in BtAdvType<IncludeAcc>]: T };
const settings: BtAdvMap<boolean> = {
bar: false,
gps: false,
hrm: false,
mag: false,
};
const idToName: BtAdvMap<string, true> = {
acc: "Acceleration",
bar: "Barometer",
gps: "GPS",
hrm: "HRM",
mag: "Magnetometer",
};
// 15 characters per line
const infoFont: FontNameWithScaleFactor = "6x8:2";
const colour = {
on: "#0f0",
off: "#fff",
} as const;
const makeToggle = (id: BtAdvType) => () => {
settings[id] = !settings[id];
const entry = btnLayout[id]!;
const col = settings[id] ? colour.on : colour.off;
entry.btnBorder = entry.col = col;
btnLayout.update();
btnLayout.render();
//require('Storage').writeJSON(SETTINGS_FILENAME, settings);
enableSensors();
};
const btnStyle: {
font: FontNameWithScaleFactor,
fillx?: 1,
filly?: 1,
col: ColorResolvable,
bgCol: ColorResolvable,
btnBorder: ColorResolvable,
} = {
font: "Vector:14",
fillx: 1,
filly: 1,
col: g.theme.fg,
bgCol: g.theme.bg,
btnBorder: "#fff",
};
const btnLayout = new Layout(
{
type: "v",
c: [
{
type: "h",
c: [
{
type: "btn",
label: idToName.bar,
id: "bar",
cb: makeToggle('bar'),
...btnStyle,
},
{
type: "btn",
label: idToName.gps,
id: "gps",
cb: makeToggle('gps'),
...btnStyle,
},
]
},
{
type: "h",
c: [
// hrm, mag
{
type: "btn",
label: idToName.hrm,
id: "hrm",
cb: makeToggle('hrm'),
...btnStyle,
},
{
type: "btn",
label: idToName.mag,
id: "mag",
cb: makeToggle('mag'),
...btnStyle,
},
]
},
{
type: "h",
c: [
{
type: "btn",
label: idToName.acc,
id: "acc",
cb: () => {},
...btnStyle,
col: colour.on,
btnBorder: colour.on,
},
{
type: "btn",
label: "Back",
cb: () => {
setBtnsShown(false);
},
...btnStyle,
},
]
}
]
},
{
lazy: true,
back: () => {
setBtnsShown(false);
},
},
);
const setBtnsShown = (b: boolean) => {
btnsShown = b;
hook(!btnsShown);
setIntervals();
redraw();
};
const drawInfo = (force?: true) => {
let { y, x, w } = Bangle.appRect;
const mid = x + w / 2
let drawn = false;
if (!force && !bar && !gps && !hrm && !mag)
return;
g.reset()
.clearRect(Bangle.appRect)
.setFont(infoFont)
.setFontAlign(0, -1);
if (bar) {
g.drawString(`${bar.altitude.toFixed(1)}m`, mid, y);
y += g.getFontHeight();
g.drawString(`${bar.pressure.toFixed(1)} hPa`, mid, y);
y += g.getFontHeight();
g.drawString(`${bar.temperature.toFixed(1)}C`, mid, y);
y += g.getFontHeight();
drawn = true;
}
if (gps) {
g.drawString(
`${gps.lat.toFixed(4)} lat, ${gps.lon.toFixed(4)} lon`,
mid,
y,
);
y += g.getFontHeight();
g.drawString(
`${gps.alt}m (${gps.satellites} sat)`,
mid,
y,
);
y += g.getFontHeight();
drawn = true;
}
if (hrm) {
g.drawString(`${hrm.bpm} BPM (${hrm.confidence}%)`, mid, y);
y += g.getFontHeight();
drawn = true;
} else if (hrmAny) {
g.drawString(`~${hrmAny.bpm} BPM (${hrmAny.confidence}%)`, mid, y);
y += g.getFontHeight();
drawn = true;
if (!settings.hrm && !hrmAnyClear) {
// hrm is erased, but hrmAny will remain until cleared (or reset)
// if it runs via health check, we reset it here
hrmAnyClear = setTimeout(() => {
hrmAny = undefined;
hrmAnyClear = undefined;
}, 10000);
}
}
if (mag) {
g.drawString(
`${mag.x} ${mag.y} ${mag.z}`,
mid,
y
);
y += g.getFontHeight();
g.drawString(
`heading: ${mag.heading.toFixed(1)}`,
mid,
y
);
y += g.getFontHeight();
drawn = true;
}
if (!drawn) {
if (!force || Object.values(settings).every((x: boolean) => !x)) {
g.drawString(`swipe to enable`, mid, y);
} else {
g.drawString(`events pending`, mid, y);
}
y += g.getFontHeight();
}
};
const onTap = (/* _: { ... } */) => {
setBtnsShown(true);
};
const redraw = () => {
if (btnsShown) {
if (!prevBtnsShown) {
prevBtnsShown = btnsShown;
Bangle.removeListener("swipe", onTap);
btnLayout.setUI();
btnLayout.forgetLazyState();
g.clearRect(Bangle.appRect); // in case btnLayout isn't full screen
}
btnLayout.render();
} else {
if (prevBtnsShown) {
prevBtnsShown = btnsShown;
Bangle.setUI(); // remove all existing input handlers
Bangle.on("swipe", onTap);
drawInfo(true);
} else {
drawInfo();
}
}
};
const encodeHrm: LenFunc<Hrm> = (hrm: Hrm) =>
// {
// flags: u8,
// bytes: [u8...]
// }
// flags {
// 1 << 0: 16bit bpm
// 1 << 1: sensor contact available
// 1 << 2: sensor contact boolean
// 1 << 3: energy expended, next 16 bits
// 1 << 4: "rr" data available, u16s, intervals
// }
[0, hrm.bpm];
encodeHrm.maxLen = 2;
const encodePressure: LenFunc<PressureData> = (data: PressureData) =>
toByteArray(Math.round(data.pressure * 10), 4, false);
encodePressure.maxLen = 4;
const encodeElevation: LenFunc<PressureData> = (data: PressureData) =>
toByteArray(Math.round(data.altitude * 100), 3, true);
encodeElevation.maxLen = 3;
const encodeTemp: LenFunc<PressureData> = (data: PressureData) =>
toByteArray(Math.round(data.temperature * 10), 2, true);
encodeTemp.maxLen = 2;
const encodeGps: LenFunc<GPSFix> = (data: GPSFix) => {
// flags: 16 bits
// bit 0: Instantaneous Speed Present
// bit 1: Total Distance Present
// bit 2: Location Present
// bit 3: Elevation Present
// bit 4: Heading Present
// bit 5: Rolling Time Present
// bit 6: UTC Time Present
//
// bit 7-8: position status
// 0 (0b00): no position
// 1 (0b01): position ok
// 2 (0b10): estimated position
// 3 (0b11): last known position
//
// bit 9: speed & distance format
// 0: 2d
// 1: 3d
//
// bit 10-11: elevation source
// 0: Positioning System
// 1: Barometric Air Pressure
// 2: Database Service (or similiar)
// 3: Other
//
// bit 12: Heading Source
// 0: Heading based on movement
// 1: Heading based on magnetic compass
//
// speed: u16 (m/s), 1/100
// distance: u24, 1/10
// lat: s32, 1/10^7
// lon: s32, 1/10^7
// elevation: s24, 1/100
// heading: u16 (deg), 1/100
// rolling time: u8 (s)
// utc time: org.bluetooth.characteristic.date_time
const speed = toByteArray(Math.round(1000 * data.speed / 36), 2, false);
const lat = toByteArray(Math.round(data.lat * 10000000), 4, true);
const lon = toByteArray(Math.round(data.lon * 10000000), 4, true);
const elevation = toByteArray(Math.round(data.alt * 100), 3, true);
const heading = toByteArray(Math.round(data.course * 100), 2, false);
return [
0b10011101, // speed, location, elevation, heading [...]
0b00000010, // position ok, 3d speed/distance
speed[0]!, speed[1]!,
lat[0]!, lat[1]!, lat[2]!, lat[3]!,
lon[0]!, lon[1]!, lon[2]!, lon[3]!,
elevation[0]!, elevation[1]!, elevation[2]!,
heading[0]!, heading[1]!
];
};
encodeGps.maxLen = 17;
const encodeGpsHeadingOnly: LenFunc<CompassData> = (data: CompassData) => {
// see encodeGps()
const heading = toByteArray(Math.round(data.heading * 100), 2, false);
return [
0b00010000, // heading present
0b00010000, // heading source: mag
heading[0]!, heading[1]!
];
};
encodeGpsHeadingOnly.maxLen = 17;
const encodeMag: LenFunc<CompassData> = (data: CompassData) => {
const x = toByteArray(data.x, 2, true);
const y = toByteArray(data.y, 2, true);
const z = toByteArray(data.z, 2, true);
return [ x[0]!, x[1]!, y[0]!, y[1]!, z[0]!, z[1]! ];
};
encodeMag.maxLen = 6;
const toByteArray = (value: number, numberOfBytes: number, isSigned: boolean) => {
const byteArray: Array<number> = new Array(numberOfBytes);
if(isSigned && (value < 0)) {
value += 1 << (numberOfBytes * 8);
}
for(let index = 0; index < numberOfBytes; index++) {
byteArray[index] = (value >> (index * 8)) & 0xff;
}
return byteArray;
};
const enableSensors = () => {
Bangle.setBarometerPower(settings.bar, "btadv");
if (!settings.bar)
bar = undefined;
Bangle.setGPSPower(settings.gps, "btadv");
if (!settings.gps)
gps = undefined;
Bangle.setHRMPower(settings.hrm, "btadv");
if (!settings.hrm)
hrm = hrmAny = undefined;
Bangle.setCompassPower(settings.mag, "btadv");
if (!settings.mag)
mag = undefined;
};
// ----------------------------
const haveServiceData = (serv: BleServ): boolean => {
switch (serv) {
case BleServ.HRM: return !!hrm;
case BleServ.EnvSensing: return !!(bar || mag);
case BleServ.LocationAndNavigation: return !!(gps && gps.lat && gps.lon || mag);
}
};
const serviceToAdvert = (serv: BleServ, initial = false): BleServAdvert => {
switch (serv) {
case BleServ.HRM:
if (hrm || initial) {
const o: BleCharAdvert = {
maxLen: encodeHrm.maxLen,
readable: true,
notify: true,
};
if (hrm) {
o.value = encodeHrm(hrm);
hrm = undefined;
}
return { [BleChar.HRM]: o };
}
return {};
case BleServ.LocationAndNavigation:
if (gps || initial) {
const o: BleCharAdvert = {
maxLen: encodeGps.maxLen,
readable: true,
notify: true,
};
if (gps) {
o.value = encodeGps(gps);
gps = undefined;
}
return { [BleChar.LocationAndSpeed]: o };
} else if (mag) {
const o: BleCharAdvert = {
maxLen: encodeGpsHeadingOnly.maxLen,
readable: true,
notify: true,
value: encodeGpsHeadingOnly(mag),
};
return { [BleChar.LocationAndSpeed]: o };
}
return {};
case BleServ.EnvSensing: {
const o: BleServAdvert = {};
if (bar || initial) {
o[BleChar.Elevation] = {
maxLen: encodeElevation.maxLen,
readable: true,
notify: true,
};
o[BleChar.TempCelsius] = {
maxLen: encodeTemp.maxLen,
readable: true,
notify: true,
};
o[BleChar.Pressure] = {
maxLen: encodePressure.maxLen,
readable: true,
notify: true,
};
if (bar) {
o[BleChar.Elevation]!.value = encodeElevation(bar);
o[BleChar.TempCelsius]!.value = encodeTemp(bar);
o[BleChar.Pressure]!.value = encodePressure(bar);
bar = undefined;
}
}
if (mag || initial) {
o[BleChar.MagneticFlux3D] = {
maxLen: encodeMag.maxLen,
readable: true,
notify: true,
};
if (mag) {
o[BleChar.MagneticFlux3D]!.value = encodeMag(mag);
}
}
return o;
}
}
};
const getBleAdvert = <T>(map: (s: BleServ) => T, all = false) => {
const advert: { [key in BleServ]?: T } = {};
for (const serv of services) {
if (all || haveServiceData(serv)) {
advert[serv] = map(serv);
}
}
// clear mag only after both EnvSensing and LocationAndNavigation have run
mag = undefined;
return advert;
};
// done via advertise in setServices()
//const updateBleAdvert = () => {
// let bleAdvert: ReturnType<typeof getBleAdvert<undefined>>;
//
// if (!(bleAdvert = (Bangle as any).bleAdvert)) {
// bleAdvert = getBleAdvert(_ => undefined);
//
// (Bangle as any).bleAdvert = bleAdvert;
// }
//
// try {
// NRF.setAdvertising(bleAdvert);
// } catch (e) {
// console.log("couldn't setAdvertising():", e);
// }
//};
const updateServices = () => {
const newAdvert = getBleAdvert(serviceToAdvert);
NRF.updateServices(newAdvert);
};
const onAccel = (newAcc: NonNull<typeof acc>) => acc = newAcc;
const onPressure = (newBar: NonNull<typeof bar>) => bar = newBar;
const onGPS = (newGps: NonNull<typeof gps>) => gps = newGps;
const onHRM = (newHrm: NonNull<typeof hrm>) => {
if (newHrm.confidence >= HRM_MIN_CONFIDENCE)
hrm = newHrm;
hrmAny = newHrm;
};
const onMag = (newMag: NonNull<typeof mag>) => mag = newMag;
const hook = (enable: boolean) => {
// need to disable for perf reasons, when buttons are shown
if (enable) {
Bangle.on("accel", onAccel);
Bangle.on("pressure", onPressure);
Bangle.on("GPS", onGPS);
Bangle.on("HRM", onHRM);
Bangle.on("mag", onMag);
} else {
Bangle.removeListener("accel", onAccel);
Bangle.removeListener("pressure", onPressure);
Bangle.removeListener("GPS", onGPS);
Bangle.removeListener("HRM", onHRM);
Bangle.removeListener("mag", onMag);
}
}
// --- intervals ---
const setIntervals = (
locked: boolean = Bangle.isLocked(),
connected: boolean = NRF.getSecurityStatus().connected,
) => {
changeInterval(
redrawInterval,
locked ? Intervals.UI_INFO_SLEEP : Intervals.UI_INFO,
);
if (connected) {
const interval = btnsShown ? Intervals.BLE_BACKGROUND : Intervals.BLE;
if (bleInterval) {
changeInterval(bleInterval, interval);
} else {
bleInterval = setInterval(updateServices, interval);
}
} else if (bleInterval) {
clearInterval(bleInterval);
bleInterval = undefined;
}
};
const redrawInterval = setInterval(redraw, /*replaced*/1000);
Bangle.on("lock", locked => setIntervals(locked));
let bleInterval: undefined | number;
NRF.on("connect", () => setIntervals(undefined, true));
NRF.on("disconnect", () => setIntervals(undefined, false));
setIntervals();
// turn things on
setBtnsShown(true);
enableSensors();
// set services/advert once at startup:
{
// must have fixed services from the start:
const ad = getBleAdvert(serv => serviceToAdvert(serv, true), /*all*/true);
const adServices = Object
.keys(ad)
.map((k: string) => k.replace("0x", ""));
NRF.setServices(
ad,
{
advertise: adServices,
uart: false,
},
);
}

1
apps/btadv/icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwZC/gECoARQpARQpIRRkARQkgRRwBrPkmQBpIvDCIMEyQQIgIvDR4WSSRIvDCIUSSRIvDCISSCJRAvCCIoaDCIgvCGooaCiRNEDoRZFBwQRFgDCBPooOCOI0JkihDhApBHARxDgiSCyTFCHYQRGUIQRDHYIRCHYIRBiChDBAJKBHYICBpIRDyQyCSQQgBCJNBCIbCDCIZNDF4R0DEYwRCIIa5BI5ARDyAdCNZIFCCIKYBR5QRBVoJ6BWZY7CTwTXJWYQRFfZYRFRgQRCAoT4DCIgICCIQpCHARlCfYRBDCIhlDCIZuDGor1BCIgCBLgZZEAAiABEYIGCPooALUIYRQVQYRLdIQRPKAQROCBzjELJ4RPAHoA=="))

BIN
apps/btadv/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

14
apps/btadv/metadata.json Normal file
View File

@ -0,0 +1,14 @@
{
"id": "btadv",
"name": "btadv",
"shortName": "btadv",
"version": "0.01",
"description": "Advertise & export live heart rate, accel, pressure, GPS & mag data over bluetooth",
"icon": "icon.png",
"tags": "health,tool,sensors,bluetooth",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"btadv.app.js","url":"app.js"},
{"name":"btadv.img","url":"icon.js","evaluate":true}
]
}

View File

@ -96,7 +96,7 @@ function draw(){
if (!isNaN(bt.battery)) layout.btBattery.label = bt.battery + "%";
if (bt.rr) layout.btRR.label = bt.rr.join(",");
if (!isNaN(bt.location)) layout.btLocation.label = BODY_LOCS[bt.location];
if (bt.contact !== undefined) layout.btContact.label = bt.contact ? "Yes":"No";
if (bt.contact !== undefined) layout.btContact.label = bt.contact ? /*LANG*/"Yes":/*LANG*/"No";
if (!isNaN(bt.energy)) layout.btEnergy.label = bt.energy.toFixed(0) + "kJ";
} else {
layout.bt.label = "--";

View File

@ -29,4 +29,5 @@
clkinfo.addInteractive that would cause ReferenceError.
0.28: Option to show (1) time only and (2) week of year.
0.29: use setItem of clockInfoMenu to change the active item
0.30: Use widget_utils.
0.30: Use widget_utils
0.31: Use clock_info module as an app

View File

@ -11,6 +11,7 @@ sub-items simply swipe up/down. To run an action (e.g. trigger home assistant),
![](screenshot_3.png)
Note: Check out the settings to change different themes.
## Settings
- Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden).

View File

@ -132,6 +132,7 @@ clockInfoItems[0].items.unshift({ name : "nop",
let clockInfoMenu = clock_info.addInteractive(clockInfoItems, {
app: "bwclk",
x : 0,
y: 135,
w: W,

View File

@ -1,7 +1,7 @@
{
"id": "bwclk",
"name": "BW Clock",
"version": "0.30",
"version": "0.31",
"description": "A very minimalistic clock.",
"readme": "README.md",
"icon": "app.png",
@ -9,6 +9,7 @@
"type": "clock",
"tags": "clock,clkinfo",
"supports": ["BANGLEJS2"],
"dependencies" : { "clock_info":"module" },
"allow_emulator": true,
"storage": [
{"name":"bwclk.app.js","url":"app.js"},

View File

@ -11,3 +11,5 @@
0.10: Use default Bangle formatter for booleans
0.11: Fix off-by-one-error on next year
0.12: Mark dated events on a day
0.13: Switch to swipe left/right for month and up/down for year selection
Display events for current month on touch

View File

@ -4,11 +4,13 @@ Basic calendar
## Usage
- Use `BTN4` (left screen tap) to go to the previous month
- Use `BTN5` (right screen tap) to go to the next month
- Swipe left to go to the previous month
- Swipe right to go to the next month
- Swipe up (Bangle.js 2 only) to go to the previous year
- Swipe down (Bangle.js 2 only) to go to the next year
- Touch to display events for current month
- Press the button (button 3 on Bangle.js 1) to exit
## Settings
- Starts Sunday: whether the calendar should start on Sunday (default is Monday).
- B2 Colors: use non-dithering colors (default, recommended for Bangle 2) or the original color scheme.

View File

@ -24,11 +24,21 @@ let fgOtherMonth = gray1;
let fgSameMonth = white;
let bgEvent = blue;
const eventsPerDay=6; // how much different events per day we can display
const date = new Date();
const timeutils = require("time_utils");
let settings = require('Storage').readJSON("calendar.json", true) || {};
let startOnSun = ((require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0) === 0;
const events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date); // all alarms that run on a specific date
// all alarms that run on a specific date
const events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => {
const date = new Date(a.date);
const time = timeutils.decodeTime(a.t);
date.setHours(time.h);
date.setMinutes(time.m);
date.setSeconds(time.s);
return {date: date, msg: a.msg};
});
events.sort((a,b) => a.date - b.date);
if (settings.ndColors === undefined) {
settings.ndColors = !g.theme.dark;
@ -192,6 +202,12 @@ function drawCalendar(date) {
}
}
const weekBeforeMonth = new Date(date.getTime());
weekBeforeMonth.setDate(weekBeforeMonth.getDate() - 7);
const week2AfterMonth = new Date(date.getFullYear(), date.getMonth()+1, 0);
week2AfterMonth.setDate(week2AfterMonth.getDate() + 14);
const eventsThisMonth = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth);
let i = 0;
for (y = 0; y < rowN - 1; y++) {
for (x = 0; x < colN; x++) {
@ -215,18 +231,20 @@ function drawCalendar(date) {
);
}
// Display events for this day
const eventsCurDay = events.filter(ev => ev.date === curDay.toLocalISOString().substr(0, 10));
if (eventsCurDay.length > 0) {
if (eventsThisMonth.length > 0) {
// Display events for this day
g.setColor(bgEvent);
eventsCurDay.forEach(ev => {
const time = timeutils.decodeTime(ev.t);
const hour = time.h + time.m/60.0;
const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59
const height = (y2-2) - (y1+2); // height of a cell
const sliceHeight = height/eventsPerDay;
const ystart = (y1+2) + slice*sliceHeight;
g.fillRect(x1+1, ystart, x2-2, ystart+sliceHeight);
eventsThisMonth.forEach((ev, idx) => {
if (sameDay(ev.date, curDay)) {
const hour = ev.date.getHours() + ev.date.getMinutes()/60.0;
const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59
const height = (y2-2) - (y1+2); // height of a cell
const sliceHeight = height/eventsPerDay;
const ystart = (y1+2) + slice*sliceHeight;
g.fillRect(x1+1, ystart, x2-2, ystart+sliceHeight);
eventsThisMonth.splice(idx, 1); // this event is no longer needed
}
});
}
@ -242,23 +260,51 @@ function drawCalendar(date) {
}
}
const date = new Date();
drawCalendar(date);
clearWatch();
Bangle.on("touch", area => {
const month = date.getMonth();
if (area == 1) {
let prevMonth = month > 0 ? month - 1 : 11;
if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1);
date.setMonth(prevMonth);
} else {
let nextMonth = month < 11 ? month + 1 : 0;
if (nextMonth === 0) date.setFullYear(date.getFullYear() + 1);
date.setMonth(nextMonth);
}
drawCalendar(date);
});
function setUI() {
Bangle.setUI({
mode : "custom",
swipe: (dirLR, dirUD) => {
if (dirLR<0) { // left
const month = date.getMonth();
let prevMonth = month > 0 ? month - 1 : 11;
if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1);
date.setMonth(prevMonth);
drawCalendar(date);
} else if (dirLR>0) { // right
const month = date.getMonth();
let nextMonth = month < 11 ? month + 1 : 0;
if (nextMonth === 0) date.setFullYear(date.getFullYear() + 1);
date.setMonth(nextMonth);
drawCalendar(date);
} else if (dirUD<0) { // up
date.setFullYear(date.getFullYear() - 1);
drawCalendar(date);
} else if (dirUD>0) { // down
date.setFullYear(date.getFullYear() + 1);
drawCalendar(date);
}
},
btn: (n) => n === (process.env.HWVERSION === 2 ? 1 : 3) && load(),
touch: (n,e) => {
const menu = events.filter(ev => ev.date.getFullYear() === date.getFullYear() && ev.date.getMonth() === date.getMonth()).map(e => {
const dateStr = require("locale").date(e.date, 1);
const timeStr = require("locale").time(e.date, 1);
return { title: `${dateStr} ${timeStr}` + (e.msg ? " " + e.msg : "") };
});
if (menu.length === 0) {
menu.push({title: /*LANG*/"No events"});
}
menu[""] = { title: require("locale").month(date) + " " + date.getFullYear() };
menu["< Back"] = () => {
E.showMenu();
drawCalendar(date);
setUI();
};
E.showMenu(menu);
}
});
}
// Show launcher when button pressed
setWatch(() => load(), process.env.HWVERSION === 2 ? BTN : BTN3, { repeat: false, edge: "falling" });
drawCalendar(date);
setUI();
// No space for widgets!

View File

@ -1,11 +1,11 @@
{
"id": "calendar",
"name": "Calendar",
"version": "0.12",
"version": "0.13",
"description": "Simple calendar",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],
"tags": "calendar",
"tags": "calendar,tool",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,

View File

@ -1 +1,3 @@
0.01: First version
0.02: Support BangleJS2
0.03: Added threshold

View File

@ -6,6 +6,8 @@ The first stage of charging Li-ion ends at ~80% capacity when the charge voltage
This app has no UI and no configuration. To disable the app, you have to uninstall it.
New in v0.03: before the very first buzz, the average value after the peak is written to chargent.json and used as threshold for future charges. This reduces the time spent in the second charge stage.
Side notes
- Full capacity is reached after charge current drops to an insignificant level. This is quite some time after charge voltage reached its peak / `E.getBattery()` returns 100.
- This app starts buzzing some time after `E.getBattery()` returns 100 (~15min on my watch), and at least 5min after the peak to account for noise.

View File

@ -1,22 +1,32 @@
(() => {
const pin = process.env.HWVERSION === 2 ? D3 : D30;
var id;
Bangle.on('charging', (charging) => {
if (charging) {
if (!id) {
var max = 0;
var count = 0;
var cnt = 0;
var sum = 0;
var lim = (require('Storage').readJSON('chargent.json', true) || {}).limit || 0;
id = setInterval(() => {
var d30 = analogRead(D30);
if (max < d30) {
max = d30;
count = 0;
var val = analogRead(pin);
if (max < val) {
max = val;
cnt = 1;
sum = val;
} else {
count++;
if (10 <= count) { // 10 * 30s == 5 min // TODO ? customizable
// TODO ? customizable
Bangle.buzz(500);
setTimeout(() => Bangle.buzz(500), 1000);
cnt++;
sum += val;
}
if (10 < cnt || (lim && lim <= max)) { // 10 * 30s == 5 min // TODO ? customizable
if (!lim) {
lim = sum / cnt;
require('Storage').writeJSON('chargent.json', {limit: lim});
}
// TODO ? customizable
Bangle.buzz(500);
setTimeout(() => Bangle.buzz(500), 1000);
}
}, 30*1000);
}

View File

@ -1 +0,0 @@
(function(){var a;Bangle.on("charging",function(e){if(e){if(!a){var c=0,b=0;a=setInterval(function(){var d=analogRead(D30);c<d?(c=d,b=0):(b++,10<=b&&(Bangle.buzz(500),setTimeout(function(){return Bangle.buzz(500)},1E3)))},3E4)}}else a&&(clearInterval(a),a=void 0)})})()

View File

@ -1,13 +1,16 @@
{ "id": "chargent",
"name": "Charge Gently",
"version": "0.01",
"version": "0.03",
"description": "When charging, reminds you to disconnect the watch to prolong battery life.",
"icon": "icon.png",
"type": "bootloader",
"tags": "battery",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name": "chargent.boot.js", "url": "boot.min.js"}
{"name": "chargent.boot.js", "url": "boot.js"}
],
"data": [
{"name": "chargent.json"}
]
}

View File

@ -41,3 +41,4 @@
0.22: Fixed crash if item has no image and cutting long overflowing text
0.23: Setting circles colours per clkinfo and not position
0.24: Using suggested color from clock_info if set as default and available
0.25: Use clock_info module as an app

View File

@ -356,6 +356,7 @@ for(var i=0;i<circleCount; i++) {
let w = circlePosX[i];
let y = h3-radiusBorder;
clockInfoMenu[i] = require("clock_info").addInteractive(clockInfoItems, {
app:"circlesclock",
x:w-radiusBorder, y:y, w:radiusBorder*2, h:g.getHeight()-(y+1),
draw : clockInfoDraw, circlePosition : i+1
});

View File

@ -1,14 +1,14 @@
{ "id": "circlesclock",
"name": "Circles clock",
"shortName":"Circles clock",
"version":"0.24",
"version":"0.25",
"description": "A clock with three or four circles for different data at the bottom in a probably familiar style",
"icon": "app.png",
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}],
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator":true,
"dependencies" : { "clock_info":"module" },
"readme": "README.md",
"storage": [
{"name":"circlesclock.app.js","url":"app.js"},

View File

@ -0,0 +1 @@
0.01: New clkinfo!

View File

@ -0,0 +1,15 @@
# StopW
A simple stopwatch widget
## Usage
Tap to start, tap again to pause. Tap again to restart
## Requests
[Contact Rob](https://www.github.com/bobrippling) for features/bugs
# TypeScript
This app is written in TypeScript, see [typescript/README.md](/typescript/README.md) for more info

BIN
apps/clkinfostopw/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,58 @@
"use strict";
(function () {
var durationOnPause = "---";
var redrawInterval;
var startTime;
var unqueueRedraw = function () {
if (redrawInterval)
clearInterval(redrawInterval);
redrawInterval = undefined;
};
var queueRedraw = function () {
var _this = this;
unqueueRedraw();
redrawInterval = setInterval(function () { return _this.emit('redraw'); }, 100);
};
var pad2 = function (s) { return ('0' + s.toFixed(0)).slice(-2); };
var duration = function (start) {
var seconds = (Date.now() - start) / 1000;
if (seconds < 60)
return seconds.toFixed(1);
var mins = seconds / 60;
seconds %= 60;
if (mins < 60)
return "".concat(pad2(mins), "m").concat(pad2(seconds), "s");
var hours = mins / 60;
mins %= 60;
return "".concat(Math.round(hours), "h").concat(pad2(mins), "m").concat(pad2(seconds), "s");
};
var img = function () { return atob("GBiBAAAAAAB+AAB+AAAAAAB+AAH/sAOB8AcA4A4YcAwYMBgYGBgYGBg8GBg8GBgYGBgAGAwAMA4AcAcA4AOBwAH/gAB+AAAAAAAAAA=="); };
return {
name: "timer",
img: img(),
items: [
{
name: "stopw",
get: function () { return ({
text: startTime
? duration(startTime)
: durationOnPause,
img: img(),
}); },
show: queueRedraw,
hide: unqueueRedraw,
run: function () {
if (startTime) {
durationOnPause = duration(startTime);
startTime = undefined;
unqueueRedraw();
}
else {
queueRedraw.call(this);
startTime = Date.now();
}
}
}
]
};
});

View File

@ -0,0 +1,65 @@
((): ClockInfo.Menu => {
let durationOnPause = "---";
let redrawInterval: number | undefined;
let startTime: number | undefined;
const unqueueRedraw = () => {
if (redrawInterval) clearInterval(redrawInterval);
redrawInterval = undefined;
};
const queueRedraw = function(this: ClockInfo.MenuItem) {
unqueueRedraw();
redrawInterval = setInterval(() => this.emit('redraw'), 100);
};
const pad2 = (s: number) => ('0' + s.toFixed(0)).slice(-2);
const duration = (start: number) => {
let seconds = (Date.now() - start) / 1000;
if (seconds < 60)
return seconds.toFixed(1);
let mins = seconds / 60;
seconds %= 60;
if (mins < 60)
return `${pad2(mins)}m${pad2(seconds)}s`;
let hours = mins / 60;
mins %= 60;
return `${Math.round(hours)}h${pad2(mins)}m${pad2(seconds)}s`;
};
const img = () => atob("GBiBAAAAAAB+AAB+AAAAAAB+AAH/sAOB8AcA4A4YcAwYMBgYGBgYGBg8GBg8GBgYGBgAGAwAMA4AcAcA4AOBwAH/gAB+AAAAAAAAAA==");
return {
name: "timer",
img: img(),
items: [
{
name: "stopw",
get: () => ({
text: startTime
? duration(startTime)
: durationOnPause,
img: img(),
}),
show: queueRedraw,
hide: unqueueRedraw,
run: function() { // tapped
if (startTime) {
durationOnPause = duration(startTime);
startTime = undefined;
unqueueRedraw();
} else {
queueRedraw.call(this);
startTime = Date.now();
}
}
}
]
};
})

View File

@ -0,0 +1,14 @@
{
"id": "clkinfostopw",
"name": "Stop Watch Clockinfo",
"version":"0.01",
"description": "A simple stopwatch, shown via clockinfo",
"icon": "app.png",
"type": "clkinfo",
"tags": "clkinfo,timer",
"supports" : ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"stopw.clkinfo.js","url":"clkinfo.js"}
]
}

View File

@ -0,0 +1,2 @@
0.01: Moved from modules/clock_info.js
0.02: Fix settings page

99
apps/clock_info/README.md Normal file
View File

@ -0,0 +1,99 @@
# Clock Info module
Module that allows for loading of clock 'info' displays
that can be scrolled through on the clock face.
## Usage
In most clocks that use Clock Info, you can interact with it the following way:
* Tap on an info menu to 'focus' it (this will highlight it in some way)
* Swipe up/down to change which info is displayed within the category
* Tap to activate (if supported), eg for a Stopwatch, Home Assistant, etc
* Swipe left/right to change between categories (Bangle.js/Agenda/etc)
* Tap outside the area of the Clock Info to 'defocus' it
## Extensions
By default Clock Info provides:
* Battery
* Steps
* Heart Rate (HRM)
* Altitude
But by installing other apps that are tagged with the type `clkinfo` you can
add extra features. For example [Sunrise Clockinfo](http://banglejs.com/apps/?id=clkinfosunrise)
A full list is available at https://banglejs.com/apps/?q=clkinfo
## Settings
Available from `Settings -> Apps -> Clock Info`
* `Defocus on Lock` - (default=on) when the watch screen auto-locks, defocus
and previously focussed Clock Infos
* `HRM` - (default=always) when does the HRM stay on?
* `Always` - When a HRM ClockInfo is shown, keep the HRM on
* `Tap` - When a HRM ClockInfo is shown, turn HRM on for 1 minute. Turn on again when tapped.
* `Max Altitude` - on clocks like [Circles Clock](https://banglejs.com/apps/?id=circlesclock) a
progress/percent indicator may be shown. The percentage for altitude will be how far towards
the Max Altitude you are. If you go higher than `Max Altitude` the correct altitude will still
be shown - the percent indicator will just read 100%
## API (Software development)
See http://www.espruino.com/Bangle.js+Clock+Info for details on using
this module inside your apps (or generating your own Clock Info
extensions).
`load()` returns an array of menu objects, where each object contains a list of menu items:
* `name` : text to display and identify menu object (e.g. weather)
* `img` : a 24x24px image
* `dynamic` : if `true`, items are not constant but are sorted (e.g. calendar events sorted by date)
* `items` : menu items such as temperature, humidity, wind etc.
Note that each item is an object with:
* `item.name` : friendly name to identify an item (e.g. temperature)
* `item.hasRange` : if `true`, `.get` returns `v/min/max` values (for progress bar/guage)
* `item.get` : function that returns an object:
```JS
{
'text' // the text to display for this item
'short' // optional: a shorter text to display for this item (at most 6 characters)
'img' // optional: a 24x24px image to display for this item
'color' // optional: a color string (like "#f00") to color the icon in compatible clocks
'v' // (if hasRange==true) a numerical value
'min','max' // (if hasRange==true) a minimum and maximum numerical value (if this were to be displayed as a guage)
}
```
* `item.show` : called when item should be shown. Enables updates. Call BEFORE 'get'
* `item.hide` : called when item should be hidden. Disables updates.
* `.on('redraw', ...)` : event that is called when 'get' should be called again (only after 'item.show')
* `item.run` : (optional) called if the info screen is tapped - can perform some action. Return true if the caller should feedback the user.
See the bottom of `lib.js` for example usage...
example.clkinfo.js :
```JS
(function() {
return {
name: "Bangle",
img: atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==") }),
items: [
{ name : "Item1",
get : () => ({ text : "TextOfItem1", v : 10, min : 0, max : 100,
img : atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==")
}),
show : () => {},
hide : () => {}
// run : () => {} optional (called when tapped)
}
]
};
}) // must not have a semi-colon!
```

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA=="))

BIN
apps/clock_info/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

View File

@ -1,54 +1,4 @@
/* Module that allows for loading of clock 'info' displays
that can be scrolled through on the clock face.
`load()` returns an array of menu objects, where each object contains a list of menu items:
* `name` : text to display and identify menu object (e.g. weather)
* `img` : a 24x24px image
* `dynamic` : if `true`, items are not constant but are sorted (e.g. calendar events sorted by date)
* `items` : menu items such as temperature, humidity, wind etc.
Note that each item is an object with:
* `item.name` : friendly name to identify an item (e.g. temperature)
* `item.hasRange` : if `true`, `.get` returns `v/min/max` values (for progress bar/guage)
* `item.get` : function that returns an object:
{
'text' // the text to display for this item
'short' // optional: a shorter text to display for this item (at most 6 characters)
'img' // optional: a 24x24px image to display for this item
'color' // optional: a color string (like "#ffffff") to color the icon in compatible clocks
'v' // (if hasRange==true) a numerical value
'min','max' // (if hasRange==true) a minimum and maximum numerical value (if this were to be displayed as a guage)
}
* `item.show` : called when item should be shown. Enables updates. Call BEFORE 'get'
* `item.hide` : called when item should be hidden. Disables updates.
* `.on('redraw', ...)` : event that is called when 'get' should be called again (only after 'item.show')
* `item.run` : (optional) called if the info screen is tapped - can perform some action. Return true if the caller should feedback the user.
See the bottom of this file for example usage...
example.clkinfo.js :
(function() {
return {
name: "Bangle",
img: atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==") }),
items: [
{ name : "Item1",
get : () => ({ text : "TextOfItem1", v : 10, min : 0, max : 100,
img : atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==")
}),
show : () => {},
hide : () => {}
// run : () => {} optional (called when tapped)
}
]
};
}) // must not have a semi-colon!
*/
/* See the README for more info... */
let storage = require("Storage");
let stepGoal = undefined;
@ -60,7 +10,21 @@ if (stepGoal == undefined) {
stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000;
}
// Load the settings, with defaults
exports.loadSettings = function() {
return Object.assign({
hrmOn : 0, // 0(Always), 1(Tap)
defocusOnLock : true,
maxAltitude : 3000,
apps : {}
},
require("Storage").readJSON("clock_info.json",1)||{}
);
};
exports.load = function() {
var settings = exports.loadSettings();
delete settings.apps; // keep just the basic settings in memory
// info used for drawing...
var hrm = 0;
var alt = "--";
@ -111,8 +75,31 @@ exports.load = function() {
text : (hrm||"--") + " bpm", v : hrm, min : 40, max : 200,
img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAADAAADAAAHAAAHjAAHjgAPngH9n/n82/gA+AAA8AAA8AAAcAAAYAAAYAAAAAAAAAAAAAAAAAAAAAA==")
}},
show : function() { Bangle.setHRMPower(1,"clkinfo"); Bangle.on("HRM", hrmUpdateHandler); hrm = Math.round(Bangle.getHealthStatus().bpm||Bangle.getHealthStatus("last").bpm); hrmUpdateHandler(); },
hide : function() { Bangle.setHRMPower(0,"clkinfo"); Bangle.removeListener("HRM", hrmUpdateHandler); hrm = 0; },
run : function() {
Bangle.setHRMPower(1,"clkinfo");
if (settings.hrmOn==1/*Tap*/) {
/* turn off after 1 minute. If Health HRM monitoring is
enabled we will still get HRM events every so often */
this.timeout = setTimeout(function() {
this.timeout = undefined;
Bangle.setHRMPower(0,"clkinfo");
}, 60000);
}
},
show : function() {
Bangle.on("HRM", hrmUpdateHandler);
hrm = Math.round(Bangle.getHealthStatus().bpm||Bangle.getHealthStatus("last").bpm); hrmUpdateHandler();
this.run(); // start HRM
},
hide : function() {
Bangle.setHRMPower(0,"clkinfo");
Bangle.removeListener("HRM", hrmUpdateHandler);
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = undefined;
}
hrm = 0;
},
}
],
}];
@ -123,7 +110,7 @@ exports.load = function() {
hasRange : true,
get : () => ({
text : alt, v : parseInt(alt),
min : 0, max : 3000,
min : 0, max : settings.maxAltitude,
img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAAACAAAGAAAPAAEZgAOwwAPwQAZgYAwAMBgAGBAACDAADGAABv///////wAAAAAAAAAAAAAAAAAAAA==")
}),
show : function() { this.interval = setInterval(altUpdateHandler, 60000); alt = "--"; altUpdateHandler(); },
@ -148,9 +135,18 @@ exports.load = function() {
return menu;
};
/** Adds an interactive menu that could be used on a clock face by swiping.
Simply supply the menu data (from .load) and a function to draw the clock info.
options = {
app : "str", // optional: app ID used when saving clock_info positions
// if defined, your app will remember its own positions,
// otherwise all apps share the same ones
x : 20, y: 20, w: 80, h:80, // dimensions of area used for clock_info
draw : (itm, info, options) // draw function
}
For example:
let clockInfoItems = require("clock_info").load();
@ -181,7 +177,7 @@ clockInfoMenu is the 'options' parameter, with the following added:
* `redraw` : function - force a redraw
* `focus` : function - bool to show if menu is focused or not
You can have more than one clock_info at once as well, sfor instance:
You can have more than one clock_info at once as well, for instance:
let clockInfoDraw = (itm, info, options) => {
g.reset().setBgColor(options.bg).setColor(options.fg).clearRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1);
@ -201,19 +197,19 @@ exports.addInteractive = function(menu, options) {
options.index = 0|exports.loadCount;
exports.loadCount = options.index+1;
options.focus = options.index==0 && options.x===undefined; // focus if we're the first one loaded and no position has been defined
const appName = "default:"+options.index;
const appName = (options.app||"default")+":"+options.index;
{ // load the currently showing clock_infos
let settings = require("Storage").readJSON("clock_info.json",1)||{};
if (settings[appName]) {
let a = settings[appName].a|0;
let b = settings[appName].b|0;
if (menu[a] && menu[a].items[b]) { // all ok
options.menuA = a;
options.menuB = b;
}
// load the currently showing clock_infos
let settings = exports.loadSettings()
if (settings.apps[appName]) {
let a = settings.apps[appName].a|0;
let b = settings.apps[appName].b|0;
if (menu[a] && menu[a].items[b]) { // all ok
options.menuA = a;
options.menuB = b;
}
}
if (options.menuA===undefined) options.menuA = 0;
if (options.menuB===undefined) options.menuB = Math.min(exports.loadCount, menu[options.menuA].items.length)-1;
function drawItem(itm) {
@ -262,41 +258,46 @@ exports.addInteractive = function(menu, options) {
menuShowItem(menu[options.menuA].items[options.menuB]);
}
// save the currently showing clock_info
let settings = require("Storage").readJSON("clock_info.json",1)||{};
settings[appName] = {a:options.menuA,b:options.menuB};
let settings = exports.loadSettings();
settings.apps[appName] = {a:options.menuA,b:options.menuB};
require("Storage").writeJSON("clock_info.json",settings);
}
Bangle.on("swipe",swipeHandler);
var touchHandler;
var lockHandler;
let touchHandler, lockHandler;
if (options.x!==undefined && options.y!==undefined && options.w && options.h) {
lockHandler = function() {
if(options.focus) {
options.focus=false;
options.redraw();
}
};
touchHandler = function(_,e) {
if (e.x<options.x || e.y<options.y ||
e.x>(options.x+options.w) || e.y>(options.y+options.h)) {
if (options.focus) {
options.focus=false;
delete Bangle.CLKINFO_FOCUS;
options.redraw();
}
return; // outside area
}
if (!options.focus) {
options.focus=true; // if not focussed, set focus
Bangle.CLKINFO_FOCUS=true;
options.redraw();
} else if (menu[options.menuA].items[options.menuB].run) {
Bangle.buzz(100, 0.7);
menu[options.menuA].items[options.menuB].run(); // allow tap on an item to run it (eg home assistant)
} else {
options.focus=true;
Bangle.CLKINFO_FOCUS=true;
}
};
Bangle.on("touch",touchHandler);
Bangle.on("lock", lockHandler);
if (settings.defocusOnLock) {
lockHandler = function() {
if(options.focus) {
options.focus=false;
delete Bangle.CLKINFO_FOCUS;
options.redraw();
}
};
Bangle.on("lock", lockHandler);
}
}
// draw the first item
menuShowItem(menu[options.menuA].items[options.menuB]);
@ -305,6 +306,7 @@ exports.addInteractive = function(menu, options) {
Bangle.removeListener("swipe",swipeHandler);
if (touchHandler) Bangle.removeListener("touch",touchHandler);
if (lockHandler) Bangle.removeListener("lock", lockHandler);
delete Bangle.CLKINFO_FOCUS;
menuHideItem(menu[options.menuA].items[options.menuB]);
exports.loadCount--;
};
@ -328,6 +330,8 @@ exports.addInteractive = function(menu, options) {
return true;
}
delete settings; // don't keep settings in RAM - save space
return options;
};

View File

@ -0,0 +1,18 @@
{ "id": "clock_info",
"name": "Clock Info Module",
"shortName": "Clock Info",
"version":"0.02",
"description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)",
"icon": "app.png",
"type": "module",
"tags": "clkinfo",
"supports" : ["BANGLEJS2"],
"provides_modules" : ["clock_info"],
"readme": "README.md",
"storage": [
{"name":"clock_info","url":"lib.js"},
{"name":"clock_info.settings.js","url":"settings.js"}
], "data": [
{"name":"clock_info.json","url":"lib.js"}
]
}

View File

@ -0,0 +1,30 @@
(function(back) {
let settings = require("clock_info").loadSettings();
function save(key, value) {
settings[key] = value;
require('Storage').write("clock_info.json", settings);
}
let menu ={
'': { 'title': 'Clock Info' },
/*LANG*/'< Back': back,
/*LANG*/'Defocus on Lock': {
value: !!settings.defocusOnLock,
onchange: x => save('defocusOnLock', x),
},
/*LANG*/'HRM': {
value: settings.hrmOn,
min: 0, max: 1, step: 1,
format: v => ["Always","Tap"][v],
onchange: x => save('hrmOn', x),
},
/*LANG*/'Max Altitude': {
value: settings.maxAltitude,
min: 500, max: 10000, step: 500,
format: v => v+"m",
onchange: x => save('maxAltitude', x),
}
};
E.showMenu(menu);
})

View File

@ -4,3 +4,5 @@
0.04: Use default Bangle formatter for booleans
0.05: Improved colors (connected vs disconnected)
0.06: Tell clock widgets to hide.
0.07: Convert Yes/No On/Off in settings to checkboxes
0.08: Fixed typo in settings.js for DRAGDOWN to make option work

View File

@ -1,7 +1,7 @@
{
"id": "clockcal",
"name": "Clock & Calendar",
"version": "0.06",
"version": "0.08",
"description": "Clock with Calendar",
"readme":"README.md",
"icon": "app.png",

View File

@ -16,7 +16,7 @@
actions = ["[ignore]","[calend.]","[AI:music]","[AI:messg]"];
require("Storage").list(RegExp(".app.js")).forEach(element => actions.push(element.replace(".app.js","")));
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
@ -93,7 +93,7 @@
value: actions.indexOf(settings.DRAGDOWN),
format: v => actions[v],
onchange: v => {
settings.DRGDOWN = actions[v];
settings.DRAGDOWN = actions[v];
writeSettings();
}
},
@ -106,18 +106,11 @@
writeSettings();
}
},
'Load deafauls?': {
value: 0,
min: 0, max: 1,
format: v => ["No", "Yes"][v],
onchange: v => {
if (v == 1) {
settings = defaults;
writeSettings();
load();
}
}
},
'Load defaults': () => {
settings = defaults;
writeSettings();
load();
}
};
// Show the menu
E.showMenu(menu);

View File

@ -6,3 +6,4 @@
0.06: Now read wheel rev as well as cadence sensor
Improve connection code
0.07: Make Bangle.js 2 compatible
0.08: Convert Yes/No On/Off in settings to checkboxes

View File

@ -2,7 +2,7 @@
"id": "cscsensor",
"name": "Cycling speed sensor",
"shortName": "CSCSensor",
"version": "0.07",
"version": "0.08",
"description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch",
"icon": "icons8-cycling-48.png",
"tags": "outdoors,exercise,ble,bluetooth",

View File

@ -23,17 +23,17 @@
}
}
const menu = {
'': { 'title': 'Cycle speed sensor' },
'': { 'title': /*LANG*/'Cycle speed sensor' },
'< Back': back,
'Wheel circ.(mm)': {
/*LANG*/'Wheel circ.(mm)': {
value: s.wheelcirc,
min: 800,
max: 2400,
step: 5,
onchange: save('wheelcirc'),
},
'Reset total distance': function() {
E.showPrompt("Zero total distance?", {buttons: {"No":false, "Yes":true}}).then(function(v) {
/*LANG*/'Reset total distance': function() {
E.showPrompt(/*LANG*/"Zero total distance?", {buttons: {/*LANG*/"No":false, /*LANG*/"Yes":true}}).then(function(v) {
if (v) {
s['totaldist'] = 0;
storage.write(SETTINGS_FILE, s);

View File

@ -0,0 +1 @@
0.01: New App based on dragboard, but with a U shaped drag area

View File

@ -0,0 +1,8 @@
Swipe along the drag bars and release to select a letter, number or punctuation.
Tap on left for backspace or right for space.
Settings:
- ABC Color: color of the characters row
- Num Color: color of the digits and symbols row
- Highlight Color: color of the currently shown character

BIN
apps/draguboard/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

156
apps/draguboard/lib.js Normal file
View File

@ -0,0 +1,156 @@
exports.input = function(options) {
options = options||{};
var text = options.text;
if ("string"!=typeof text) text="";
let settings = require('Storage').readJSON('draguboard.json',1)||{};
var R;
const paramToColor = (param) => g.toColor(`#${settings[param].toString(16).padStart(3,0)}`);
var BGCOLOR = g.theme.bg;
var HLCOLOR = settings.Highlight ? paramToColor("Highlight") : g.theme.fg;
var ABCCOLOR = settings.ABC ? paramToColor("ABC") : g.toColor(1,0,0);//'#FF0000';
var NUMCOLOR = settings.Num ? paramToColor("Num") : g.toColor(0,1,0);//'#00FF00';
var BIGFONT = '6x8:3';
var SMALLFONT = '6x8:1';
var LEFT = "IJKLMNOPQ";
var MIDDLE = "ABCDEFGH";
var RIGHT = "RSTUVWXYZ";
var NUM = ' 1234567890!?,.-@';
var rectHeight = 40;
var vLength = LEFT.length;
var MIDPADDING;
var NUMPADDING;
var showCharY;
var middleWidth;
var middleStart;
var topStart;
function drawAbcRow() {
g.clear();
try { // Draw widgets if they are present in the current app.
if (WIDGETS) Bangle.drawWidgets();
} catch (_) {}
g.setColor(ABCCOLOR);
g.setFont('6x8:2x1');
g.setFontAlign(-1, -1, 0);
g.drawString(RIGHT.split("").join("\n\n"), R.x2-28, topStart);
g.drawString(LEFT.split("").join("\n\n"), R.x+22, topStart);
g.setFont('6x8:1x2');
var spaced = MIDDLE.split("").join(" ");
middleWidth = g.stringWidth(spaced);
middleStart = (R.x2-middleWidth)/2;
g.drawString(spaced, (R.x2-middleWidth)/2, (R.y2)/2);
g.fillRect(MIDPADDING, (R.y2)-26, (R.x2-MIDPADDING), (R.y2));
// Draw left and right drag rectangles
g.fillRect(R.x, R.y, 12, R.y2);
g.fillRect(R.x2, R.y, R.x2-12, R.y2);
}
function drawNumRow() {
g.setFont('6x8:1x2');
g.setColor(NUMCOLOR);
NUMPADDING = (R.x2-g.stringWidth(NUM))/2;
g.setFontAlign(-1, -1, 0);
g.drawString(NUM, NUMPADDING, (R.y2)/4);
g.drawString("<-", NUMPADDING+10, showCharY+5);
g.drawString("->", R.x2-(NUMPADDING+20), showCharY+5);
g.fillRect(NUMPADDING, (R.y2)-rectHeight*4/3, (R.x2)-NUMPADDING, (R.y2)-rectHeight*2/3);
}
function updateTopString() {
g.setFont(SMALLFONT);
g.setColor(BGCOLOR);
g.fillRect(R.x,R.y,R.x2,R.y+9);
var rectLen = text.length<27? text.length*6:27*6;
g.setColor(0.7,0,0);
//draw cursor at end of text
g.fillRect(R.x+rectLen+5,R.y,R.x+rectLen+10,R.y+9);
g.setColor(HLCOLOR);
g.setFontAlign(-1, -1, 0);
g.drawString(text.length<=27? text : '<- '+text.substr(-24,24), R.x+5, R.y+1);
}
function showChars(chars) {
"ram";
// clear large character
g.setColor(BGCOLOR);
g.fillRect(R.x+65,showCharY,R.x2-65,showCharY+28);
// show new large character
g.setColor(HLCOLOR);
g.setFont(BIGFONT);
g.setFontAlign(-1, -1, 0);
g.drawString(chars, (R.x2 - g.stringWidth(chars))/2, showCharY+4);
}
var charPos;
var char;
var prevChar;
function moveCharPos(list, select, posPixels) {
charPos = Math.min(list.length-1, Math.max(0, Math.floor(posPixels)));
char = list.charAt(charPos);
if (char != prevChar) showChars(char);
prevChar = char;
if (select) {
text += char;
updateTopString();
}
}
return new Promise((resolve,reject) => {
// Interpret touch input
Bangle.setUI({
mode: 'custom',
back: ()=>{
Bangle.setUI();
g.clearRect(Bangle.appRect);
resolve(text);
},
drag: function(event) {
"ram";
// drag on middle bottom rectangle
if (event.x > MIDPADDING - 2 && event.x < (R.x2-MIDPADDING + 2) && event.y >= ( (R.y2) - 12 )) {
moveCharPos(MIDDLE, event.b == 0, (event.x-middleStart)/(middleWidth/MIDDLE.length));
}
// drag on left or right rectangle
else if (event.y > R.y && (event.x < MIDPADDING-2 || event.x > (R.x2-MIDPADDING + 2))) {
moveCharPos(event.x<MIDPADDING-2 ? LEFT : RIGHT, event.b == 0, (event.y-topStart)/((R.y2 - topStart)/vLength));
}
// drag on top rectangle for number or punctuation
else if ((event.y < ( (R.y2) - 12 )) && (event.y > ( (R.y2) - 52 ))) {
moveCharPos(NUM, event.b == 0, (event.x-NUMPADDING)/6);
}
// Make a space or backspace by tapping right or left on screen above green rectangle
else if (event.y > R.y && event.b == 0) {
if (event.x < (R.x2)/2) {
showChars('<-');
text = text.slice(0, -1);
} else {
//show space sign
showChars('->');
text += ' ';
}
prevChar = null;
updateTopString();
}
}
});
R = Bangle.appRect;
MIDPADDING = R.x + 35;
showCharY = (R.y2)/3;
topStart = R.y+12;
drawAbcRow();
drawNumRow();
updateTopString();
});
};

View File

@ -0,0 +1,15 @@
{ "id": "draguboard",
"name": "DragUboard",
"version":"0.01",
"description": "A library for text input via swiping U-shaped keyboard.",
"icon": "app.png",
"type":"textinput",
"tags": "keyboard",
"supports" : ["BANGLEJS2"],
"screenshots": [{"url":"screenshot.png"}],
"readme": "README.md",
"storage": [
{"name":"textinput","url":"lib.js"},
{"name":"draguboard.settings.js","url":"settings.js"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,44 @@
(function(back) {
let settings = require('Storage').readJSON('draguboard.json',1)||{};
const colors = {
4095: /*LANG*/"White",
4080: /*LANG*/"Yellow",
3840: /*LANG*/"Red",
3855: /*LANG*/"Magenta",
255: /*LANG*/"Cyan",
240: /*LANG*/"Green",
15: /*LANG*/"Blue",
0: /*LANG*/"Black",
'-1': /*LANG*/"Default"
};
const save = () => require('Storage').write('draguboard.json', settings);
function colorMenu(key) {
let menu = {'': {title: key}, '< Back': () => E.showMenu(appMenu)};
Object.keys(colors).forEach(color => {
var label = colors[color];
menu[label] = {
value: settings[key] == color,
onchange: () => {
if (color >= 0) {
settings[key] = color;
} else {
delete settings[key];
}
save();
setTimeout(E.showMenu, 10, appMenu);
}
};
});
return menu;
}
const appMenu = {
'': {title: 'draguboard'}, '< Back': back,
/*LANG*/'ABC Color': () => E.showMenu(colorMenu("ABC")),
/*LANG*/'Num Color': () => E.showMenu(colorMenu("Num")),
/*LANG*/'Highlight Color': () => E.showMenu(colorMenu("Highlight"))
};
E.showMenu(appMenu);
});

View File

@ -1,2 +1,3 @@
0.01: New App!
0.02: Add lightning
0.03: Convert Yes/No On/Off in settings to checkboxes

View File

@ -1,7 +1,7 @@
{ "id": "f9lander",
"name": "Falcon9 Lander",
"shortName":"F9lander",
"version":"0.02",
"version":"0.03",
"description": "Land a rocket booster",
"icon": "f9lander.png",
"screenshots" : [ { "url":"f9lander_screenshot1.png" }, { "url":"f9lander_screenshot2.png" }, { "url":"f9lander_screenshot3.png" }],

View File

@ -2,7 +2,6 @@
/**
* @param {function} back Use back() to return to settings menu
*/
const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off";
(function(back) {
const SETTINGS_FILE = 'f9settings.json'
// initialize with default settings...
@ -27,8 +26,7 @@ const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off";
'': { 'title': 'OpenWind' },
'< Back': back,
'Lightning': {
value: settings.lightning,
format: boolFormat,
value: !!settings.lightning,
onchange: save('lightning'),
}
}

View File

@ -7,7 +7,7 @@ var m;
var files;
function delete_file(fn) {
E.showPrompt("Delete\n"+fn+"?", {buttons: {"No":false, "Yes":true}}).then(function(v) {
E.showPrompt(/*LANG*/"Delete\n"+fn+"?", {buttons: {/*LANG*/"No":false, /*LANG*/"Yes":true}}).then(function(v) {
if (v) {
if (fn.charCodeAt(fn.length-1)==1) {
var fh = STOR.open(fn.substr(0, fn.length-1), "r");

View File

@ -1,3 +1,4 @@
0.01: First release
0.02: Move translations to locale module (removed watch settings, now pick language in Bangle App Loader, More..., Settings)
0.03: Change for fast loading, use widget_utils to hide widgets
0.03: Change for fast loading, use widget_utils to hide widgets
0.04: Add animation when display changes

View File

@ -16,7 +16,6 @@ Most translations are taken from the original Fuzzy Text International code.
## TODO
* Bold hour word (as the pebble version has)
* Animation when changing time?
## References
Based on Pebble app Fuzzy Text International: https://github.com/hallettj/Fuzzy-Text-International

View File

@ -33,13 +33,17 @@
]
};
let text_scale = 3.5;
let text_scale = 4;
let timeout = 2.5*60;
let drawTimeout;
let animInterval;
let time_string = "";
let time_string_old = "";
let time_string_old_wrapped = "";
let loadSettings = function() {
settings = require("Storage").readJSON(SETTINGS_FILE,1)|| {'showWidgets': false};
}
};
let queueDraw = function(seconds) {
let millisecs = seconds * 1000;
@ -48,10 +52,7 @@ let queueDraw = function(seconds) {
drawTimeout = undefined;
draw();
}, millisecs - (Date.now() % millisecs));
}
const h = g.getHeight();
const w = g.getWidth();
};
let getTimeString = function(date) {
let segment = Math.round((date.getMinutes()*60 + date.getSeconds() + 1)/300);
@ -63,18 +64,47 @@ let getTimeString = function(date) {
f_string = f_string.replace('$2', fuzzy_string.hours[(hour + 1) % 12]);
}
return f_string;
}
};
let draw = function() {
let time_string = getTimeString(new Date()).replace('*', '');
// print(time_string);
g.setFont('Vector', (h-24*2)/text_scale);
g.setFontAlign(0, 0);
g.clearRect(0, 24, w, h-24);
g.setColor(g.theme.fg);
g.drawString(g.wrapString(time_string, w).join("\n"), w/2, h/2);
time_string = getTimeString(new Date()).replace('*', '');
//print(time_string);
if (time_string != time_string_old) {
g.setFont('Vector', R.h/text_scale).setFontAlign(0, 0);
animate(3);
}
queueDraw(timeout);
}
};
let animate = function(step) {
if (animInterval) clearInterval(animInterval);
let time_string_new_wrapped = g.wrapString(time_string, R.w).join("\n");
slideX = 0;
animInterval = setInterval(function() {
let time_start = getTime()
//blank old time
g.setColor(g.theme.bg);
g.drawString(time_string_old_wrapped, R.x + R.w/2 + slideX, R.y + R.h/2);
g.drawString(time_string_new_wrapped, R.x - R.w/2 + slideX, R.y + R.h/2);
g.setColor(g.theme.fg);
slideX += step;
let stop = false;
if (slideX>=R.w) {
slideX=R.w;
stop = true;
}
//draw shifted new time
g.drawString(time_string_old_wrapped, R.x + R.w/2 + slideX, R.y + R.h/2);
g.drawString(time_string_new_wrapped, R.x - R.w/2 + slideX, R.y + R.h/2);
if (stop) {
time_string_old = time_string;
clearInterval(animInterval);
animInterval=undefined;
time_string_old_wrapped = time_string_new_wrapped;
}
print(Math.round((getTime() - time_start)*1000))
}, 30);
};
g.clear();
loadSettings();
@ -95,6 +125,8 @@ Bangle.setUI({
// Called to unload all of the clock app
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
if (animInterval) clearInterval(animInterval);
animInterval = undefined;
require('widget_utils').show(); // re-show widgets
}
});
@ -106,5 +138,6 @@ if (settings.showWidgets) {
require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
}
R = Bangle.appRect;
draw();
}

View File

@ -2,7 +2,7 @@
"id":"fuzzyw",
"name":"Fuzzy Text Clock",
"shortName": "Fuzzy Text",
"version": "0.03",
"version": "0.04",
"description": "An imprecise clock for when you're not in a rush",
"readme": "README.md",
"icon":"fuzzyw.png",

View File

@ -6,3 +6,4 @@
Add CRC checks for common bootloaders that we know don't work
0.04: Include a precompiled bootloader for easy bootloader updates
0.05: Rename Bootloader->DFU and add explanation to avoid confusion with Bootloader app
0.06: Lower chunk size to 1024 (from 2048) to make firmware updates more reliable

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