widadjust: add other files for v0.01

pull/1512/head
Markus Laire 2022-02-25 17:53:04 +02:00 committed by GitHub
parent bef3c32a9a
commit 38d6a95303
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 416 additions and 0 deletions

52
apps/widadjust/README.md Normal file
View File

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

120
apps/widadjust/settings.js Normal file
View File

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

244
apps/widadjust/widget.js Normal file
View File

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