Merge branch 'master' into event_date_repeat
|
@ -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
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "gitsubmodule"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
|
@ -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
|
||||
|
|
|
@ -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; }
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
})();
|
||||
})();
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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"}],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Actually upload correct code
|
||||
0.03: Display sea-level pressure, too, and allow calibration
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Don't fire if the app uses swipes already.
|
||||
|
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4ARkQAHBwsBiIACiAHBgQXIkAXJiIuKGAwWEC4cjmYABn//AAMyC63yC653FC6HwC5aQBC5ybIC44WChGAWxMgC44rCxGIZxYXFIoYXBGAQNCAAQXILYYXBGAUDBoK0EC5AsBC4QwEC5wAEC853BhAWDI6CPCFwp3OX4ouCC8xHXCAJ3VX94XCwBHVGIiPTU4oNCAAQWBX5gDBgQRCAAoXGGAUIFwQXHkAXHJIgABCw4IBC5sAiIAEiAgHAAQXLHBAYIC+6wJQYIADgIXGGBJ3FC4iOBAH4A/ACAA=="))
|
|
@ -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);
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -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}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 4.4 KiB |
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New app!
|
|
@ -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
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwZC/gECoARQpARQpIRRkARQkgRRwBrPkmQBpIvDCIMEyQQIgIvDR4WSSRIvDCIUSSRIvDCISSCJRAvCCIoaDCIgvCGooaCiRNEDoRZFBwQRFgDCBPooOCOI0JkihDhApBHARxDgiSCyTFCHYQRGUIQRDHYIRCHYIRBiChDBAJKBHYICBpIRDyQyCSQQgBCJNBCIbCDCIZNDF4R0DEYwRCIIa5BI5ARDyAdCNZIFCCIKYBR5QRBVoJ6BWZY7CTwTXJWYQRFfZYRFRgQRCAoT4DCIgICCIQpCHARlCfYRBDCIhlDCIZuDGor1BCIgCBLgZZEAAiABEYIGCPooALUIYRQVQYRLdIQRPKAQROCBzjELJ4RPAHoA=="))
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -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}
|
||||
]
|
||||
}
|
|
@ -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 = "--";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -11,6 +11,7 @@ sub-items simply swipe up/down. To run an action (e.g. trigger home assistant),
|
|||
|
||||

|
||||
|
||||
Note: Check out the settings to change different themes.
|
||||
|
||||
## Settings
|
||||
- Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden).
|
||||
|
|
|
@ -132,6 +132,7 @@ clockInfoItems[0].items.unshift({ name : "nop",
|
|||
|
||||
|
||||
let clockInfoMenu = clock_info.addInteractive(clockInfoItems, {
|
||||
app: "bwclk",
|
||||
x : 0,
|
||||
y: 135,
|
||||
w: W,
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
0.01: First version
|
||||
0.02: Support BangleJS2
|
||||
0.03: Added threshold
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)})})()
|
|
@ -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"}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New clkinfo!
|
|
@ -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
|
After Width: | Height: | Size: 3.7 KiB |
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
})
|
|
@ -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"}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
0.01: Moved from modules/clock_info.js
|
||||
0.02: Fix settings page
|
|
@ -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!
|
||||
```
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA=="))
|
After Width: | Height: | Size: 390 B |
|
@ -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;
|
||||
};
|
||||
|
|
@ -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"}
|
||||
]
|
||||
}
|
|
@ -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);
|
||||
})
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App based on dragboard, but with a U shaped drag area
|
|
@ -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
|
After Width: | Height: | Size: 9.0 KiB |
|
@ -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();
|
||||
});
|
||||
};
|
|
@ -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"}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 16 KiB |
|
@ -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);
|
||||
});
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Add lightning
|
||||
0.03: Convert Yes/No On/Off in settings to checkboxes
|
||||
|
|
|
@ -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" }],
|
||||
|
|
|
@ -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'),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|