From 0ec1d57addd5205bd1be7b1b053630681f1a97fb Mon Sep 17 00:00:00 2001 From: storm64 Date: Sun, 13 Feb 2022 20:54:26 +0100 Subject: [PATCH] sleeplog: Complete power saving mode + move all read/write log actions into lib.js * Complete new power saving mode after tests * Move all read/write log operations into lib/module Update app.js - update `readLog(...)` call Update boot.js - update maxmove default value - delete `log(..)`, merging into `writeLog(...)` from lib.js - replace all read/write actions to the logfile with `readLog(...)`/`writeLog(...)` from lib.js - add restoring the status after a restart (<5 min ago) to last known state - add deletion of unused settings on power saving mode - update log timestamp on power saving mode to always be 10min before now Update ChangeLog Update lib.js - update `readLog(...)`, prevent errors on reading, minimize workload if unfiltered - add `writeLog(...)` to append or replace log depending on input with plausibility checks on input - merging `log(...)` from boot.js into `writeLog(...)` - replace all read/write actions to the logfile with `readLog(...)`/`writeLog(...)` Update metadata.json - update version number - add power saving mode to description Update README.md - add power saving mode description - move sleeping/not worn decision description into its own section - add description for timestamp values to Logging section - update Global Object and Module Functions section Update settings.js - update maxmove setting and default value --- apps/sleeplog/ChangeLog | 1 + apps/sleeplog/README.md | 84 ++++++++++++++++++---------- apps/sleeplog/app.js | 2 +- apps/sleeplog/boot.js | 73 ++++++++----------------- apps/sleeplog/lib.js | 106 +++++++++++++++++++++++++----------- apps/sleeplog/metadata.json | 4 +- apps/sleeplog/settings.js | 6 +- 7 files changed, 160 insertions(+), 116 deletions(-) diff --git a/apps/sleeplog/ChangeLog b/apps/sleeplog/ChangeLog index 7dee1a116..dae137b19 100644 --- a/apps/sleeplog/ChangeLog +++ b/apps/sleeplog/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Fix crash on start +0.03: Added power saving mode + move all read/write log actions into lib/module diff --git a/apps/sleeplog/README.md b/apps/sleeplog/README.md index 24f47c23c..d5c706940 100644 --- a/apps/sleeplog/README.md +++ b/apps/sleeplog/README.md @@ -2,20 +2,26 @@ This app logs and displays the four following states: _unknown, not worn, awake, sleeping_ -It derived from the [SleepPhaseAlarm](https://banglejs.com/apps/#sleepphasealarm) and uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments ([ESS](https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en)) and the internal temperature to decide _sleeping_ or _not worn_ when the watch is resting. +It derived from the [SleepPhaseAlarm](https://banglejs.com/apps/#sleepphasealarm) and uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments ([ESS](https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en)) and +also provides a power saving mode using the built in movement calculation. The internal temperature is used to decide if the status is _sleeping_ or _not worn_. #### Operating Principle * __ESS calculation__ The accelerometer polls values with 12.5Hz. On each poll the magnitude value is saved. When 13 values are collected, every 1.04 seconds, the standard deviation over this values is calculated. - Is the calculated standard deviation lower than the "no movement" threshold (__NoMoThresh__) a "no movement" counter is incremented. Each time the "no movement" threshold is reached the "no movement" counter will be reset. - When the "no movement" counter reaches the sleep threshold the watch is considered as resting. (The sleep threshold is calculated from the __MinDuration__ setting, Example: _sleep threshold = MinDuration * 60 / calculation interval => 10min * 60s/min / 1.04s ~= 576,9 rounded up to 577_) - To check if a resting watch indicates as sleeping, the internal temperature must be greater than the temperature threshold (__TempThresh__). Otherwise the watch is considered as not worn. + Is the calculated standard deviation lower than the "no movement" threshold (__NoMoThresh__) a "no movement" counter is incremented. Each time the "no movement" threshold is reached the "no movement" counter will be reset. The first time no movement is detected the actual timestamp is cached (in _sleeplog.firstnomodate_) for logging. + When the "no movement" counter reaches the sleep threshold the watch is considered as resting. (The sleep threshold is calculated from the __MinDuration__ setting, Example: _sleep threshold = MinDuration * 60 / calculation interval => 10min * 60s/min / 1.04s ~= 576,9 rounded up to 577_) +* __Power Saving Mode__ + On power saving mode the movement value of bangle's build in health event is checked against the maximal movement threshold (__MaxMove__). The event is only triggered every 10 minutes which decreases the battery impact but also reduces accurracy. +* ___Sleeping___ __or__ ___Not Worn___ + To check if a resting watch indicates a sleeping status, the internal temperature must be greater than the temperature threshold (__TempThresh__). Otherwise the watch is considered as not worn. * __True Sleep__ The true sleep value is a simple addition of all registert sleeping periods. * __Consecutive Sleep__ In addition the consecutive sleep value tries to predict the complete time you were asleep, even the light sleeping phases with registered movements. All periods after a sleeping period will be summarized til the first following non sleeping period that is longer then the maximal awake duration (__MaxAwake__). If this sum is lower than the minimal consecutive sleep duration (__MinConsec__) it is not considered, otherwise it will be added to the consecutive sleep value. * __Logging__ - To minimize the log size only a changed state is logged. + To minimize the log size only a changed state is logged. The logged timestamp is matching the beginning of its measurement period. + When not on power saving mode a movement is detected nearly instantaneous and the detection of a no movement period is delayed by the minimal no movement duration. To match the beginning of the measurement period a cached timestamp (_sleeplog.firstnomodate_) is logged. + On power saving mode the measurement period is fixed to 10 minutes and all logged timestamps are also set back 10 minutes. --- ### Control @@ -28,28 +34,34 @@ It derived from the [SleepPhaseAlarm](https://banglejs.com/apps/#sleepphasealarm --- ### Settings --- -* __BreakTod__ break at time of day +* __BreakTod__ | break at time of day _0_ / _1_ / _..._ / __10__ / _..._ / _12_ Change time of day on wich the lower graph starts and the upper graph ends. -* __MaxAwake__ maximal awake duration +* __MaxAwake__ | maximal awake duration _15min_ / _20min_ / _..._ / __60min__ / _..._ / _120min_ Adjust the maximal awake duration upon the exceeding of which aborts the consecutive sleep period. -* __MinConsec__ minimal consecutive sleep duration +* __MinConsec__ | minimal consecutive sleep duration _15min_ / _20min_ / _..._ / __30min__ / _..._ / _120min_ Adjust the minimal consecutive sleep duration that will be considered for the consecutive sleep value. -* __TempThresh__ temperature threshold +* __TempThresh__ | temperature threshold _20°C_ / _20.5°C_ / _..._ / __25°C__ / _..._ / _40°C_ The internal temperature must be greater than this threshold to log _sleeping_, otherwise it is _not worn_. -* __NoMoThresh__ no movement threshold +* __PowerSaving__ + _on_ / __off__ + En-/Disable power saving mode. _Saves battery, but might decrease accurracy._ +* __MaxMove__ | maximal movement threshold | only available when on power saving mode + _50_ / _51_ / _..._ / __100__ / _..._ / _200_ + On power saving mode the watch is considered resting if this threshold is lower or equal to the movement value of bangle's health event. +* __NoMoThresh__ | no movement threshold | only available when not on power saving mode _0.006_ / _0.007_ / _..._ / __0.012__ / _..._ / _0.020_ The standard deviation over the measured values needs to be lower then this threshold to count as not moving. The defaut threshold value worked best for my watch. A threshold value below 0.008 may get triggert by noise. -* __MinDuration__ minimal no movement duration +* __MinDuration__ | minimal no movement duration | only available when not on power saving mode _5min_ / _6min_ / _..._ / __10min__ / _..._ / _15min_ If no movement is detected for this duration, the watch is considered as resting. * __Enabled__ __on__ / _off_ - En-/Disable the service (all background activities). _Saves battery, but might make this app useless._ + En-/Disable the service (all background activities). _Saves the most battery, but might make this app useless._ * __Logfile__ __default__ / _off_ En-/Disable logging by setting the logfile to _sleeplog.log_ / _undefined_. @@ -66,7 +78,7 @@ For easy access from the console or other apps the following parameters, values logfile: "sleeplog.log", // string / used logfile resting: false, // bool / indicates if the watch is resting status: 2, // int / actual status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping - firstnomodate: 1644435877595, // number / Date.now() from first recognised no movement + firstnomodate: 1644435877595, // number / Date.now() from first recognised no movement, not available in power saving mode stop: function () { ... }, // funct / stops the service until the next load() start: function () { ... }, // funct / restarts the service ... @@ -74,42 +86,54 @@ For easy access from the console or other apps the following parameters, values >require("sleeplog") ={ - setEnabled: function (enable, logfile) { ... }, - // en-/disable the service and/or logging - // * enable / bool / service status to change to - // * logfile / bool or string + setEnabled: function (enable, logfile, powersaving) { ... }, + // restarts the service with changed settings + // * enable / bool / new service status + // * logfile / bool or string // - true = enables logging to "sleeplog.log" // - "some_file.log" = enables logging to "some_file.log" // - false = disables logging - // returns: bool or undefined - // - true = changes executed - // - false = no changes needed + // * (powersaving) / bool / new power saving status, default: false + // returns: true or undefined + // - true = service restart executed // - undefined = no global.sleeplog found - readLog: function (since, until) { ... }, + readLog: function (logfile, since, until) { ... }, // read the raw log data for a specific time period - // * since / Date or number / startpoint of period - // * until / Date or number / endpoint of period + // * logfile / string / on no string uses logfile from global object or "sleeplog.log" + // * (since) / Date or number / startpoint of period, default: 0 + // * (until) / Date or number / endpoint of period, default: 1E14 // returns: array // * [[number, int, string], [...], ... ] / sorting: latest first // - number // timestamp in ms // - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping // - string // additional information - // * [] = no data available or global.sleeplog found - getReadableLog: function (printLog, since, until) { ... } + // * [] = no data available or global.sleeplog not found + writeLog: function (logfile, input) { ... }, + // append or replace log depending on input + // * logfile / string / on no string uses logfile from global object or default + // * input / array + // - append input if array length >1 and element[0] >9E11 + // - replace log with input if at least one entry like above is inside another array + // returns: true or undefined + // - true = changest written to storage + // - undefined = wrong input + getReadableLog: function (printLog, since, until, logfile) { ... } // read the log data as humanreadable string for a specific time period - // * since / Date or number / startpoint of period - // * until / Date or number / endpoint of period + // * (printLog) / bool / direct print output with additional information, default: false + // * (since) / Date or number / see readLog(..) + // * (until) / Date or number / see readLog(..) + // * (logfile) / string / see readLog(..) // returns: string // * "{substring of ISO date} - {status} for {duration}min\n...", sorting: latest last // * undefined = no data available or global.sleeplog found restoreLog: function (logfile) { ... } // eliminate some errors inside a specific logfile - // * logfile / string / name of the logfile that will be restored + // * (logfile) / string / see readLog(..) // returns: int / number of changes that were made reinterpretTemp: function (logfile, tempthresh) { ... } // reinterpret worn status based on given temperature threshold - // * logfile / string / name of the logfile - // * tempthresh / float / new temperature threshold + // * (logfile) / string / see readLog(..) + // * (tempthresh) / float / new temperature threshold, on default uses tempthresh from global object or 27 // returns: int / number of changes that were made } ``` diff --git a/apps/sleeplog/app.js b/apps/sleeplog/app.js index 19ef52ef8..c89b37267 100644 --- a/apps/sleeplog/app.js +++ b/apps/sleeplog/app.js @@ -25,7 +25,7 @@ function drawLog(topY, viewUntil) { var y = topY + graphHeight; // read 12h wide log - var log = require("sleeplog").readLog(timestamp0, viewUntil.valueOf()); + var log = require("sleeplog").readLog(0, timestamp0, viewUntil.valueOf()); // format log array if not empty if (log.length) { diff --git a/apps/sleeplog/boot.js b/apps/sleeplog/boot.js index 1e52497a3..623463d69 100644 --- a/apps/sleeplog/boot.js +++ b/apps/sleeplog/boot.js @@ -12,7 +12,7 @@ global.sleeplog = Object.assign({ winwidth: 13, // 13 values, read with 12.5Hz = every 1.04s nomothresh: 0.012, // values lower than 0.008 getting triggert by noise sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min - maxmove: 44, // movement threshold on power saving mode + maxmove: 100, // movement threshold on power saving mode tempthresh: 27, // every temperature above ist registered as worn }, require("Storage").readJSON("sleeplog.json", true) || {}); @@ -28,41 +28,6 @@ if (sleeplog.enabled) { resting: undefined, status: undefined, - // define logging function - log: function(date, status, temperature, info) { - // exit on wrong this - if (this.enabled === undefined) return; - // skip logging if logfile is undefined or does not end with ".log" - if (!this.logfile || !this.logfile.endsWith(".log")) return; - // prevent logging on implausible date - if (date < 9E11 || Date() < 9E11) return; - - // set default value for status - status = status || 0; - - // define storage - var storage = require("Storage"); - - // read previous logfile - var log = storage.read(this.logfile) || ""; - log = log ? JSON.parse(atob(log)) || [] : []; - - // remove last state if it was unknown and is less then 10min ago - if (log.length > 0 && log[0][1] === 0 && - Math.floor(Date.now()) - log[0][0] < 600000) log.shift(); - - // add actual status at the first position if it has changed - if (log.length === 0 || log[0][1] !== status) - log.unshift(info ? [date, status, temperature, info] : temperature ? [date, status, temperature] : [date, status]); - - // write log to storage - storage.write(this.logfile, btoa(JSON.stringify(log))); - - // clear variables - log = undefined; - storage = undefined; - }, - // define stop function (logging will restart if enabled and boot file is executed) stop: function() { // remove all listeners @@ -72,7 +37,7 @@ if (sleeplog.enabled) { // exit on missing global object if (!global.sleeplog) return; // write log with undefined sleeping status - sleeplog.log(Math.floor(Date.now())); + require("sleeplog").writeLog(0, [Math.floor(Date.now()), 0]); // reset always used cached values sleeplog.resting = undefined; sleeplog.status = undefined; @@ -91,14 +56,20 @@ if (sleeplog.enabled) { if (sleeplog.accel) Bangle.on('accel', sleeplog.accel); // add kill listener E.on('kill', sleeplog.stop); - // set status to unknown - sleeplog.status = 0; + // read log since 5min ago and restore status to last known state or unknown + sleeplog.status = (require("sleeplog").readLog(0, Date.now() - 3E5)[1] || [0, 0])[1] + // update resting according to status + sleeplog.resting = sleeplog.status % 2; + // write restored status to log + require("sleeplog").writeLog(0, [Math.floor(Date.now()), sleeplog.status]); } }); // check for power saving mode if (sleeplog.powersaving) { // power saving mode using build in movement detection + // delete unused settings + ["winwidth", "nomothresh", "sleepthresh"].forEach(property => delete sleeplog[property]); // add cached values and functions to global object sleeplog = Object.assign(sleeplog, { // define health listener function @@ -107,24 +78,27 @@ if (sleeplog.enabled) { var gObj = global.sleeplog; if (!gObj) return; + // calculate timestamp for this measurement + var timestamp = Math.floor(Date.now() - 6E5); + // check for non-movement according to the threshold if (data.movement <= gObj.maxmove) { // check resting state - if (gObj.resting !== true) { + if (true || gObj.resting !== true) { // log always for testing // change resting state gObj.resting = true; // set status to sleeping or worn gObj.status = E.getTemperature() > gObj.tempthresh ? 3 : 1; - // write status to log, correct timestamp by health interval in ms - gObj.log(Math.floor(Date.now() - 6E5), gObj.status, E.getTemperature()); + // write status to log, + require("sleeplog").writeLog(0, [timestamp, gObj.status, E.getTemperature(), data.movement]); } } else { // check resting state - if (gObj.resting !== false) { - // change resting state, set status and write to log as awake + if (true || gObj.resting !== false) { // log always for testing + // change resting state, set status and write status to log gObj.resting = false; gObj.status = 2; - gObj.log(Math.floor(Date.now()), 2); + require("sleeplog").writeLog(0, [timestamp, 2, E.getTemperature(), data.movement]); } } } @@ -164,18 +138,19 @@ if (sleeplog.enabled) { this.resting = true; // set status to sleeping or worn this.status = E.getTemperature() > this.tempthresh ? 3 : 1; - // write status to log, correct timestamp by health interval in ms - this.log(this.firstnomodate, this.status, E.getTemperature()); + // write status to log, with first no movement timestamp + require("sleeplog").writeLog(0, [this.firstnomodate, this.status, E.getTemperature()]); } } else { // reset non-movement sections count this.nomocount = 0; // check resting state if (this.resting !== false) { - // change resting state, set status and write to log as awake + // change resting state and set status this.resting = false; this.status = 2; - this.log(Math.floor(Date.now()), 2); + // write status to log + require("sleeplog").writeLog(0, [Math.floor(Date.now()), 2]); } } } diff --git a/apps/sleeplog/lib.js b/apps/sleeplog/lib.js index 8db3e8600..6fd6d2ba9 100644 --- a/apps/sleeplog/lib.js +++ b/apps/sleeplog/lib.js @@ -20,7 +20,7 @@ exports = { storage.writeJSON(filename, Object.assign(storage.readJSON(filename, true) || {}, { enabled: enable, logfile: logfile, - powersaving: powersaving + powersaving: powersaving || false })); // force changes to take effect by executing the boot script @@ -39,33 +39,77 @@ exports = { // - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping // - float // internal temperature // - string // additional information - readLog: function(since, until) { - // set logfile - var logfile = (global.sleeplog || {}).logfile || "sleeplog.log"; + readLog: function(logfile, since, until) { + // check/set logfile + logfile = typeof logfile === "string" && logfile.endsWith(".log") ? logfile : + (global.sleeplog || {}).logfile || "sleeplog.log"; // check if since is in the future if (since > Date()) return []; - // read log json to array - var log = storage.read(this.logfile) || ""; - log = log ? JSON.parse(atob(log)) || [] : []; + // read logfile + var log = require("Storage").read(logfile); + // return empty log + if (!log) return []; + // decode data if needed + if (log[0] !== "[") log = atob(log); + // do a simple check before parsing + if (!log.startsWith("[[") || !log.endsWith("]]")) return []; + log = JSON.parse(log) || []; - // search for latest entry befor since - since = (log.find(element => element[0] <= since) || [0])[0]; - - // filter selected time period - log = log.filter(element => (element[0] >= since) && (element[0] <= (until || 1E14))); + // check if filtering is needed + if (since || until) { + // search for latest entry befor since + if (since) since = (log.find(element => element[0] <= since) || [0])[0]; + // filter selected time period + log = log.filter(element => (element[0] >= since) && (element[0] <= (until || 1E14))); + } // output log return log; }, + // define write log function, append or replace log depending on input + // append input if array length >1 and element[0] >9E11 + // replace log with input if at least one entry like above is inside another array + writeLog: function(logfile, input) { + // check/set logfile + logfile = typeof logfile === "string" && logfile.endsWith(".log") ? logfile : + (global.sleeplog || {}).logfile || "sleeplog.log"; + + // check if input is an array + if (typeof input !== "object" || typeof input.length !== "number") return; + + // check for entry plausibility + if (input.length > 1 && input[0] * 1 > 9E11) { + // read log + var log = this.readLog(logfile); + + // remove last state if it was unknown and less then 5min ago + if (log.length > 0 && log[0][1] === 0 && + Math.floor(Date.now()) - log[0][0] < 3E5) log.shift(); + + // add entry at the first position if it has changed + if (log.length === 0 || input.some((e, index) => index > 0 && input[index] !== log[0][index])) log.unshift(input); + + // map log as input + input = log; + } + + // simple check for log plausibility + if (input[0].length > 1 && input[0][0] * 1 > 9E11) { + // write log to storage + require("Storage").write(logfile, btoa(JSON.stringify(input))); + return true; + } + }, + // define log to humanreadable string function // sorting: latest last, format: // "{substring of ISO date} - {status} for {duration}min\n..." - getReadableLog: function(printLog, since, until) { + getReadableLog: function(printLog, since, until, logfile) { // read log and check - var log = this.readLog(since, until); + var log = this.readLog(logfile, since, until); if (!log.length) return; // reverse array to set last timestamp to the end log.reverse(); @@ -79,8 +123,11 @@ exports = { logString[index] = "" + Date(element[0] - Date().getTimezoneOffset() * 6E4).toISOString().substr(0, 19).replace("T", " ") + " - " + statusText[element[1]] + - (index === log.length - 1 ? "" : " for " + Math.round((log[index + 1][0] - element[0]) / 60000) + "min") + - (element[2] ? " | Temp: " + element[2] + "°C" : "") + + (index === log.length - 1 ? + element.length < 3 ? "" : " ".repeat(12) : + " for " + ("" + Math.round((log[index + 1][0] - element[0]) / 60000)).padStart(4) + "min" + ) + + (element[2] ? " | Temp: " + ("" + element[2]).padEnd(5) + "°C" : "") + (element[3] ? " | " + element[3] : ""); }); logString = logString.join("\n"); @@ -98,12 +145,9 @@ exports = { // define function to eliminate some errors inside the log restoreLog: function(logfile) { - // define storage - var storage = require("Storage"); - - // read log json to array - var log = storage.read(this.logfile) || ""; - log = log ? JSON.parse(atob(log)) || [] : []; + // read log and check + var log = this.readLog(logfile); + if (!log.length) return; // define output variable to show number of changes var output = log.length; @@ -111,8 +155,8 @@ exports = { // remove non decremental entries log = log.filter((element, index) => log[index][0] >= (log[index + 1] || [0])[0]); - // write log to storage - storage.write(logfile, btoa(JSON.stringify(log))); + // write log + this.writeLog(logfile, log); // return difference in length return output - log.length; @@ -120,12 +164,12 @@ exports = { // define function to reinterpret worn status based on given temperature threshold reinterpretTemp: function(logfile, tempthresh) { - // define storage - var storage = require("Storage"); + // read log and check + var log = this.readLog(logfile); + if (!log.length) return; - // read log json to array - var log = storage.read(this.logfile) || ""; - log = log ? JSON.parse(atob(log)) || [] : []; + // set default tempthresh + tempthresh = tempthresh || (global.sleeplog ? sleeplog.tempthresh : 27); // define output variable to show number of changes var output = 0; @@ -140,8 +184,8 @@ exports = { return element; }); - // write log to storage - storage.write(logfile, btoa(JSON.stringify(log))); + // write log + this.writeLog(logfile, log); // return output return output; diff --git a/apps/sleeplog/metadata.json b/apps/sleeplog/metadata.json index f4590f7c0..1d098cc72 100644 --- a/apps/sleeplog/metadata.json +++ b/apps/sleeplog/metadata.json @@ -2,8 +2,8 @@ "id":"sleeplog", "name":"Sleep Log", "shortName": "SleepLog", - "version": "0.02", - "description": "Log and view your sleeping habits. This app derived from SleepPhaseAlarm and uses also the principe of Estimation of Stationary Sleep-segments (ESS).", + "version": "0.03", + "description": "Log and view your sleeping habits. This app derived from SleepPhaseAlarm and uses also the principe of Estimation of Stationary Sleep-segments (ESS). It also provides a power saving mode using the built in movement calculation.", "icon": "app.png", "type": "app", "tags": "tool,boot", diff --git a/apps/sleeplog/settings.js b/apps/sleeplog/settings.js index 09d2eb28e..6363dfd8d 100644 --- a/apps/sleeplog/settings.js +++ b/apps/sleeplog/settings.js @@ -9,7 +9,7 @@ minconsec: 18E5, // 30min in ms tempthresh: 27, // every temperature above ist registered as worn powersaving: false, // disables ESS and uses build in movement detection - maxmove: 44, // movement threshold on power saving mode + maxmove: 100, // movement threshold on power saving mode nomothresh: 0.012, // values lower than 0.008 getting triggert by noise sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min winwidth: 13, // 13 values, read with 12.5Hz = every 1.04s @@ -97,9 +97,9 @@ }, "MaxMove": { value: settings.maxmove, - step: 44, + step: 1, onchange: function(v) { - this.value = v = circulate(40, 100, v); + this.value = v = circulate(50, 200, v); writeSetting("maxmove", v); } },