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