From 38d6a95303cd72e56f9e041d4da1f571b1198851 Mon Sep 17 00:00:00 2001 From: Markus Laire Date: Fri, 25 Feb 2022 17:53:04 +0200 Subject: [PATCH] widadjust: add other files for v0.01 --- apps/widadjust/README.md | 52 ++++++++ apps/widadjust/settings.js | 120 ++++++++++++++++++ apps/widadjust/widget.js | 244 +++++++++++++++++++++++++++++++++++++ 3 files changed, 416 insertions(+) create mode 100644 apps/widadjust/README.md create mode 100644 apps/widadjust/settings.js create mode 100644 apps/widadjust/widget.js diff --git a/apps/widadjust/README.md b/apps/widadjust/README.md new file mode 100644 index 000000000..1b51440f5 --- /dev/null +++ b/apps/widadjust/README.md @@ -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(); +} +``` diff --git a/apps/widadjust/settings.js b/apps/widadjust/settings.js new file mode 100644 index 000000000..5791d763b --- /dev/null +++ b/apps/widadjust/settings.js @@ -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); +}) diff --git a/apps/widadjust/widget.js b/apps/widadjust/widget.js new file mode 100644 index 000000000..ce1fee022 --- /dev/null +++ b/apps/widadjust/widget.js @@ -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); + +})()