mirror of https://github.com/espruino/BangleApps
widadjust: add other files for v0.01
parent
bef3c32a9a
commit
38d6a95303
|
@ -0,0 +1,52 @@
|
|||
# Adjust Clock
|
||||
|
||||
Adjusts clock continually in the background to counter clock drift.
|
||||
|
||||
## Usage
|
||||
|
||||
First you need to determine the clock drift of your watch in PPM (parts per million).
|
||||
|
||||
For example if you measure that your watch clock is too fast by 5 seconds in 24 hours,
|
||||
then PPM is `5 / (24*60*60) * 1000000 = 57.9`.
|
||||
|
||||
Then set PPM in settings and this widget will continually adjust the clock by that amount.
|
||||
|
||||
## Settings
|
||||
|
||||
See **Basic logic** below for more details.
|
||||
|
||||
- **PPM x 10** - change PPM in steps of 10
|
||||
- **PPM x 1** - change PPM in steps of 1
|
||||
- **PPM x 0.1** - change PPM in steps of 0.1
|
||||
- **Update Interval** - How often to update widget and clock error.
|
||||
- **Threshold** - Threshold for adjusting clock.
|
||||
When clock error exceeds this threshold, clock is adjusted with `setTime`.
|
||||
- **Save State** - If `On` clock error state is saved to file when widget exits, if needed.
|
||||
That is recommended and default setting.
|
||||
If `Off` clock error state is forgotten and reset to 0 whenever widget is restarted,
|
||||
for example when going to Launcher. This can cause significant inaccuracy especially
|
||||
with large **Update Interval** or **Threshold**.
|
||||
- **Debug Log** - If `On` some debug information is logged to file `widadjust.log`.
|
||||
|
||||
## Display
|
||||
|
||||
Widget shows clock error in milliseconds and PPM.
|
||||
|
||||
## Basic logic
|
||||
|
||||
- When widget starts, clock error state is loaded from file `widadjust.state`.
|
||||
- While widget is running, widget display and clock error is updated
|
||||
periodically (**Update Interval**) according to **PPM**.
|
||||
- When clock error exceeds **Threshold** clock is adjusted with `setTime`.
|
||||
- When widget exists, clock error state is saved to file `widadjust.state` if needed.
|
||||
|
||||
## Services
|
||||
|
||||
Other apps/widgets can use `WIDGETS.adjust.now()` to request current adjusted time.
|
||||
To support also case where this widget isn't present, the following code can be used:
|
||||
|
||||
```
|
||||
function adjustedNow() {
|
||||
return WIDGETS.adjust ? WIDGETS.adjust.now() : Date.now();
|
||||
}
|
||||
```
|
|
@ -0,0 +1,120 @@
|
|||
(function(back) {
|
||||
const SETTINGS_FILE = 'widadjust.json';
|
||||
const STATE_FILE = 'widadjust.state';
|
||||
|
||||
const DEFAULT_ADJUST_THRESHOLD = 100;
|
||||
let thresholdV = [ 10, 25, 50, 100, 250, 500, 1000 ];
|
||||
|
||||
const DEFAULT_UPDATE_INTERVAL = 60000;
|
||||
let intervalV = [ 10000, 30000, 60000, 180000, 600000, 1800000, 3600000 ];
|
||||
let intervalN = [ "10 s", "30 s", "1 m", "3 m", "10 m", "30 m", "1 h" ];
|
||||
|
||||
let stateFileErased = false;
|
||||
|
||||
let settings = Object.assign({
|
||||
advanced: false,
|
||||
saveState: true,
|
||||
debugLog: false,
|
||||
ppm: 0,
|
||||
adjustThreshold: DEFAULT_ADJUST_THRESHOLD,
|
||||
updateInterval: DEFAULT_UPDATE_INTERVAL,
|
||||
}, require('Storage').readJSON(SETTINGS_FILE, true) || {});
|
||||
|
||||
if (thresholdV.indexOf(settings.adjustThreshold) == -1) {
|
||||
settings.adjustThreshold = DEFAULT_ADJUST_THRESHOLD;
|
||||
}
|
||||
|
||||
if (intervalV.indexOf(settings.updateInterval) == -1) {
|
||||
settings.updateInterval = DEFAULT_UPDATE_INTERVAL;
|
||||
}
|
||||
|
||||
function onPpmChange(v) {
|
||||
settings.ppm = v;
|
||||
mainMenu['PPM x 10' ].value = v;
|
||||
mainMenu['PPM x 1' ].value = v;
|
||||
mainMenu['PPM x 0.1'].value = v;
|
||||
}
|
||||
|
||||
let mainMenu = {
|
||||
'': { 'title' : 'Adjust Clock' },
|
||||
|
||||
'< Back': () => {
|
||||
require('Storage').writeJSON(SETTINGS_FILE, settings);
|
||||
back();
|
||||
},
|
||||
|
||||
/*
|
||||
// NOT FULLY WORKING YET
|
||||
'Mode': {
|
||||
value: settings.advanced,
|
||||
format: v => v ? 'Advanced' : 'Basic',
|
||||
onchange: () => {
|
||||
settings.advanced = !settings.advanced;
|
||||
}
|
||||
},
|
||||
*/
|
||||
|
||||
'PPM x 10' : {
|
||||
value: settings.ppm,
|
||||
format: v => v.toFixed(1),
|
||||
step: 10,
|
||||
onchange : onPpmChange,
|
||||
},
|
||||
|
||||
'PPM x 1' : {
|
||||
value: settings.ppm,
|
||||
format: v => v.toFixed(1),
|
||||
step: 1,
|
||||
onchange : onPpmChange,
|
||||
},
|
||||
|
||||
'PPM x 0.1' : {
|
||||
value: settings.ppm,
|
||||
format: v => v.toFixed(1),
|
||||
step: 0.1,
|
||||
onchange : onPpmChange,
|
||||
},
|
||||
|
||||
'Update Interval': {
|
||||
value: intervalV.indexOf(settings.updateInterval),
|
||||
min: 0,
|
||||
max: intervalV.length - 1,
|
||||
format: v => intervalN[v],
|
||||
onchange: v => {
|
||||
settings.updateInterval = intervalV[v];
|
||||
},
|
||||
},
|
||||
|
||||
'Threshold': {
|
||||
value: thresholdV.indexOf(settings.adjustThreshold),
|
||||
min: 0,
|
||||
max: thresholdV.length - 1,
|
||||
format: v => thresholdV[v] + " ms",
|
||||
onchange: v => {
|
||||
settings.adjustThreshold = thresholdV[v];
|
||||
},
|
||||
},
|
||||
|
||||
'Save State': {
|
||||
value: settings.saveState,
|
||||
format: v => v ? 'On' : 'Off',
|
||||
onchange: () => {
|
||||
settings.saveState = !settings.saveState;
|
||||
if (!settings.saveState && !stateFileErased) {
|
||||
stateFileErased = true;
|
||||
require("Storage").erase(STATE_FILE);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
'Debug Log': {
|
||||
value: settings.debugLog,
|
||||
format: v => v ? 'On' : 'Off',
|
||||
onchange: () => {
|
||||
settings.debugLog = !settings.debugLog;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
E.showMenu(mainMenu);
|
||||
})
|
|
@ -0,0 +1,244 @@
|
|||
(() => {
|
||||
// ======================================================================
|
||||
// CONST
|
||||
|
||||
const DEBUG_LOG_FILE = 'widadjust.log';
|
||||
const SETTINGS_FILE = 'widadjust.json';
|
||||
const STATE_FILE = 'widadjust.state';
|
||||
|
||||
const DEFAULT_ADJUST_THRESHOLD = 100;
|
||||
const DEFAULT_UPDATE_INTERVAL = 60 * 1000;
|
||||
const MIN_INTERVAL = 10 * 1000;
|
||||
|
||||
const MAX_CLOCK_ERROR_FROM_SAVED_STATE = 2000;
|
||||
|
||||
const SAVE_STATE_CLOCK_ERROR_DELTA_THRESHOLD = 1;
|
||||
const SAVE_STATE_CLOCK_ERROR_DELTA_IN_PPM_THRESHOLD = 1;
|
||||
const SAVE_STATE_PPM_DELTA_THRESHOLD = 1;
|
||||
|
||||
// Widget width.
|
||||
const WIDTH = 22;
|
||||
|
||||
// ======================================================================
|
||||
// VARIABLES
|
||||
|
||||
let settings;
|
||||
let saved;
|
||||
|
||||
let lastClockCheckTime = Date.now();;
|
||||
let lastClockErrorUpdateTime;
|
||||
|
||||
let clockError;
|
||||
let currentUpdateInterval;
|
||||
let lastPpm = null;
|
||||
|
||||
let debugLogFile = null;
|
||||
|
||||
// ======================================================================
|
||||
// FUNCTIONS
|
||||
|
||||
function clockCheck() {
|
||||
let now = Date.now();
|
||||
let elapsed = now - lastClockCheckTime;
|
||||
lastClockCheckTime = now;
|
||||
|
||||
let prevUpdateInterval = currentUpdateInterval;
|
||||
currentUpdateInterval = settings.updateInterval;
|
||||
setTimeout(clockCheck, lastClockCheckTime + currentUpdateInterval - Date.now());
|
||||
|
||||
// If elapsed time differs a lot from expected,
|
||||
// some other app probably used setTime to change clock significantly.
|
||||
// -> reset clock error since elapsed time can't be trusted
|
||||
if (Math.abs(elapsed - prevUpdateInterval) > 10 * 1000) {
|
||||
// RESET CLOCK ERROR
|
||||
|
||||
clockError = 0;
|
||||
lastClockErrorUpdateTime = now;
|
||||
|
||||
debug(
|
||||
'Looks like some other app used setTime, so reset clockError. (elapsed = ' +
|
||||
elapsed.toFixed(0) + ')'
|
||||
);
|
||||
WIDGETS.adjust.draw();
|
||||
|
||||
} else if (!settings.advanced) {
|
||||
// UPDATE CLOCK ERROR WITHOUT TEMPERATURE COMPENSATION
|
||||
|
||||
updateClockError(settings.ppm);
|
||||
} else {
|
||||
// UPDATE CLOCK ERROR WITH TEMPERATURE COMPENSATION
|
||||
|
||||
Bangle.getPressure().then(d => {
|
||||
let temp = d.temperature;
|
||||
updateClockError(settings.ppm0 + settings.ppm1 * temp + settings.ppm2 * temp * temp);
|
||||
}).catch(e => {
|
||||
WIDGETS.adjust.draw();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function debug(line) {
|
||||
console.log(line);
|
||||
if (debugLogFile !== null) {
|
||||
debugLogFile.write(line + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
g.reset().setFont('6x8').setFontAlign(0, 0);
|
||||
g.clearRect(this.x, this.y, this.x + WIDTH - 1, this.y + 23);
|
||||
g.drawString(Math.round(clockError), this.x + WIDTH/2, this.y + 9);
|
||||
|
||||
if (lastPpm !== null) {
|
||||
g.setFont('4x6').setFontAlign(0, 1);
|
||||
g.drawString(lastPpm.toFixed(1), this.x + WIDTH/2, this.y + 23);
|
||||
}
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
settings = Object.assign({
|
||||
advanced: false,
|
||||
saveState: true,
|
||||
debugLog: false,
|
||||
ppm: 0,
|
||||
ppm0: 0,
|
||||
ppm1: 0,
|
||||
ppm2: 0,
|
||||
adjustThreshold: DEFAULT_ADJUST_THRESHOLD,
|
||||
updateInterval: DEFAULT_UPDATE_INTERVAL,
|
||||
}, require('Storage').readJSON(SETTINGS_FILE, true) || {});
|
||||
|
||||
if (settings.debugLog) {
|
||||
if (debugLogFile === null) {
|
||||
debugLogFile = require('Storage').open(DEBUG_LOG_FILE, 'a');
|
||||
}
|
||||
} else {
|
||||
debugLogFile = null;
|
||||
}
|
||||
|
||||
settings.updateInterval = Math.max(settings.updateInterval, MIN_INTERVAL);
|
||||
}
|
||||
|
||||
function onQuit() {
|
||||
let now = Date.now();
|
||||
// WIP
|
||||
let ppm = (lastPpm !== null) ? lastPpm : settings.ppm;
|
||||
let updatedClockError = clockError + (now - lastClockErrorUpdateTime) * ppm / 1000000;
|
||||
let save = false;
|
||||
|
||||
if (! settings.saveState) {
|
||||
debug(new Date(now).toISOString() + ' QUIT');
|
||||
|
||||
} else if (saved === undefined) {
|
||||
save = true;
|
||||
debug(new Date(now).toISOString() + ' QUIT & SAVE STATE');
|
||||
|
||||
} else {
|
||||
let elapsedSaved = now - saved.time;
|
||||
let estimatedClockError = saved.clockError + elapsedSaved * saved.ppm / 1000000;
|
||||
|
||||
let clockErrorDelta = updatedClockError - estimatedClockError;
|
||||
let clockErrorDeltaInPpm = clockErrorDelta / elapsedSaved * 1000000;
|
||||
let ppmDelta = ppm - saved.ppm;
|
||||
|
||||
let debugA = new Date(now).toISOString() + ' QUIT';
|
||||
let debugB =
|
||||
'\n> ' + updatedClockError.toFixed(2) + ' - ' + estimatedClockError.toFixed(2) + ' = ' +
|
||||
clockErrorDelta.toFixed(2) + ' (' +
|
||||
clockErrorDeltaInPpm.toFixed(1) + ' PPM) ; ' +
|
||||
ppm.toFixed(1) + ' - ' + saved.ppm.toFixed(1) + ' = ' + ppmDelta.toFixed(1);
|
||||
|
||||
if ((Math.abs(clockErrorDelta) >= SAVE_STATE_CLOCK_ERROR_DELTA_THRESHOLD
|
||||
&& Math.abs(clockErrorDeltaInPpm) >= SAVE_STATE_CLOCK_ERROR_DELTA_IN_PPM_THRESHOLD
|
||||
) || Math.abs(ppmDelta) >= SAVE_STATE_PPM_DELTA_THRESHOLD
|
||||
)
|
||||
{
|
||||
save = true;
|
||||
debug(debugA + ' & SAVE STATE' + debugB);
|
||||
} else {
|
||||
debug(debugA + debugB);
|
||||
}
|
||||
}
|
||||
|
||||
if (save) {
|
||||
require('Storage').writeJSON(STATE_FILE, {
|
||||
counter: (saved === undefined) ? 1 : saved.counter + 1,
|
||||
time: Math.round(now),
|
||||
clockError: Math.round(updatedClockError * 1000) / 1000,
|
||||
ppm: Math.round(ppm * 1000) / 1000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateClockError(ppm) {
|
||||
let now = Date.now();
|
||||
let elapsed = now - lastClockErrorUpdateTime;
|
||||
let drift = elapsed * ppm / 1000000;
|
||||
clockError += drift;
|
||||
lastClockErrorUpdateTime = now;
|
||||
lastPpm = ppm;
|
||||
|
||||
if (Math.abs(clockError) >= settings.adjustThreshold) {
|
||||
let now = Date.now();
|
||||
// Shorter variables are faster to look up and this part is time sensitive.
|
||||
let e = clockError / 1000;
|
||||
setTime(getTime() - e);
|
||||
debug(
|
||||
new Date(now).toISOString() + ' -> ' + ((now / 1000 - e) % 60).toFixed(3) +
|
||||
' SET TIME (' + clockError.toFixed(2) + ')'
|
||||
);
|
||||
clockError = 0;
|
||||
}
|
||||
|
||||
WIDGETS.adjust.draw();
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// MAIN
|
||||
|
||||
loadSettings();
|
||||
|
||||
WIDGETS.adjust = {
|
||||
area: 'tr',
|
||||
draw: draw,
|
||||
now: () => {
|
||||
let now = Date.now();
|
||||
// WIP
|
||||
let ppm = (lastPpm !== null) ? lastPpm : settings.ppm;
|
||||
let updatedClockError = clockError + (now - lastClockErrorUpdateTime) * ppm / 1000000;
|
||||
return now - updatedClockError;
|
||||
},
|
||||
width: WIDTH,
|
||||
};
|
||||
|
||||
if (settings.saveState) {
|
||||
saved = require('Storage').readJSON(STATE_FILE, true);
|
||||
}
|
||||
|
||||
let now = Date.now();
|
||||
lastClockErrorUpdateTime = now;
|
||||
if (saved === undefined) {
|
||||
clockError = 0;
|
||||
debug(new Date().toISOString() + ' START');
|
||||
} else {
|
||||
clockError = saved.clockError + (now - saved.time) * saved.ppm / 1000000;
|
||||
|
||||
if (Math.abs(clockError) <= MAX_CLOCK_ERROR_FROM_SAVED_STATE) {
|
||||
debug(
|
||||
new Date().toISOString() + ' START & LOAD STATE (' +
|
||||
clockError.toFixed(2) + ')'
|
||||
);
|
||||
} else {
|
||||
debug(
|
||||
new Date().toISOString() + ' START & IGNORE STATE (' +
|
||||
clockError.toFixed(2) + ')'
|
||||
);
|
||||
clockError = 0;
|
||||
}
|
||||
}
|
||||
|
||||
clockCheck();
|
||||
|
||||
E.on('kill', onQuit);
|
||||
|
||||
})()
|
Loading…
Reference in New Issue