Merge pull request #2322 from storm64/sleeplog

[sleeplog] Improved README + option to add functions triggered by sleeplog
pull/2333/head
Gordon Williams 2022-11-30 09:08:51 +00:00 committed by GitHub
commit 0cc01f584b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 141 additions and 146 deletions

View File

@ -7,3 +7,4 @@
0.10: Complete rework off this app!
0.10beta: Add interface.html to view saved log data, add "View log" function for debugging log, send data for gadgetbridge, change caching for global getStats
0.11: Prevent module not found error
0.12: Improve README, option to add functions triggered by status changes or time periods, remove old log (<0.10) conversion

View File

@ -1,11 +1,19 @@
# Sleep Log
This app logs and displays the following states:
This app logs and displays the following states:
- sleepling status: _unknown, not worn, awake, light sleep, deep sleep_
- consecutive sleep status: _unknown, not consecutive, consecutive_
It is using the built in movement calculation to decide your sleeping state. While charging it is assumed that you are not wearing the watch and if the status changes to _deep sleep_ the internal heartrate sensor is used to detect if you are wearing the watch.
#### Explanations
* __Detection of Sleep__
The movement value of bangle's build in health event that is triggered every 10 minutes is checked against the thresholds for light and deep sleep. If the measured movement is lower or equal to the __Deep Sleep__-threshold a deep sleep phase is detected for the last 10 minutes. If the threshold is exceeded but not the __Light Sleep__-threshold than the last timeperiod is detected as light sleep phase. On exceeding even this threshold it is assumed that you were awake.
* __True Sleep__
The true sleep value is a simple addition of all registered sleeping periods.
* __Consecutive Sleep__
In addition the consecutive sleep value tries to predict the complete time you were asleep, even the very light sleeping periods when an awake period is detected based on the registered movements. All periods after a sleeping period will be summarized until the first following non sleeping period that is longer then the maximal awake duration (__Max Awake__). If this sum is lower than the minimal consecutive sleep duration (__Min Consecutive__) it is not considered, otherwise it will be added to the consecutive sleep value.
Logfiles are not removed on un-/reinstall to prevent data loss.
| Filename (* _example_) | Content | Removeable in |
@ -16,59 +24,73 @@ Logfiles are not removed on un-/reinstall to prevent data loss.
---
### App Usage
### Main App Usage
---
#### On the main app screen:
- __swipe left & right__
#### Controls:
- __swipe left & right__
to change the displayed day
- __touch the "title"__ (e.g. `Night to Fri 20/05/2022`)
- __touch the "title"__ (e.g. `Night to Fri 20/05/2022`)
to enter day selection prompt
- __touch the info area__
to change the displayed information
- __touch the info area__
to change the displayed information
(by default: consecutive & true sleeping)
- __touch the wrench__ (upper right corner)
- __touch the wrench__ (upper right corner)
to enter the settings
- __use back button widget__ (upper left corner)
- __use back button widget__ (upper left corner)
exit the app
#### Inside the settings:
- __Thresholds__ submenu
#### View:
| Status | Color | Height |
|-------------|:------:|----------:|
| unknown | black | 0% |
| not worn | red | 40% |
| awake | green | 60% |
| light sleep | cyan | 80% |
| deep sleep | blue | 100% |
| consecutive | violet | as status |
---
### Settings Usage
---
- __Thresholds__ submenu
Changes take effect from now on, not retrospective!
- __Max Awake__ | maximal awake duration
- __Max Awake__ | maximal awake duration
_10min_ / _20min_ / ... / __60min__ / ... / _120min_
- __Min Consecutive__ | minimal consecutive sleep duration
- __Min Consecutive__ | minimal consecutive sleep duration
_10min_ / _20min_ / ... / __30min__ / ... / _120min_
- __Deep Sleep__ | deep sleep threshold
- __Deep Sleep__ | deep sleep threshold
_30_ / _31_ / ... / __100__ / ... / _200_
- __Light Sleep__ | light sleep threshold
_100_ / _110_ / ... / __200__ / ... / _400_
- __Light Sleep__ | light sleep threshold
_100_ / _110_ / ... / __200__ / ... / _400_
- __Reset to Default__ | reset to bold values above
- __BreakToD__ | time of day to break view
- __BreakToD__ | time of day to break view
_0:00_ / _1:00_ / ... / __12:00__ / ... / _23:00_
- __App Timeout__ | app specific lock timeout
- __App Timeout__ | app specific lock timeout
__0s__ / _10s_ / ... / _120s_
- __Enabled__ | completely en-/disables the background service
- __Enabled__ | completely en-/disables the background service
__on__ / _off_
- __Debugging__ submenu
- __View log__ | display logfile data
Select the logfile by its starting time.
Thresholds are shown as line with its value.
- __swipe left & right__
- __Debugging__ submenu
- __View log__ | display logfile data
Select the logfile by its starting time.
Thresholds are shown as line with its value.
- __swipe left & right__
to change displayed duration
- __swipe up & down__
- __swipe up & down__
to change displayed value range
- __touch the graph__
- __touch the graph__
to change between light & dark colors
- __use back button widget__ (upper left corner)
- __use back button widget__ (upper left corner)
to go back to the logfile selection
- __Enabled__ | en-/disables debugging
- __Enabled__ | en-/disables debugging
_on_ / __off__
- __write File__ | toggles if a logfile is written
- __write File__ | toggles if a logfile is written
_on_ / __off__
- __Duration__ | duration for writing into logfile
_1h_ / _2h_ / ... / __12h__ / _96_
- The following data is logged to a csv-file:
- __Duration__ | duration for writing into logfile
_1h_ / _2h_ / ... / __12h__ / _96_
- The following data is logged to a csv-file:
_timestamp_ (in days since 1900-01-01 00:00 UTC used by office software) _, movement, status, consecutive, asleepSince, awakeSince, bpm, bpmConfidence_
@ -78,48 +100,50 @@ Logfiles are not removed on un-/reinstall to prevent data loss.
Available through the App Loader when your watch is connected.
- __view data__
- __view data__
Display the data to each timestamp in a table.
- __save csv-file__
Download a csv-file with the data to each timestamp.
The time format is chooseable beneath the file list.
- __delete file__
- __save csv-file__
Download a csv-file with the data to each timestamp.
The time format is chooseable beneath the file list.
- __delete file__
Deletes the logfile from the watch. __Please backup your data first!__
---
### Timestamps and files
### Timestamps and Files
---
1. externally visible/usable timestamps (in `global.sleeplog`) are formatted as Bangle timestamps:
1. externally visible/usable timestamps (in `global.sleeplog`) are formatted as Bangle timestamps:
seconds since 1970-01-01 00:00 UTC
2. internally used and logged (to `sleeplog.log (StorageFile)`) is within the highest available resolution:
2. internally used and logged (to `sleeplog.log (StorageFile)`) is within the highest available resolution:
10 minutes since 1970-01-01 00:00 UTC (`Bangle / (10 * 60 * 1000)`)
3. debug .csv file ID (`sleeplog_123456.csv`) has a hourly resolution:
hours since 1970-01-01 00:00 UTC (`Bangle / (60 * 60 * 1000)`)
4. logged timestamps inside the debug .csv file are formatted for office calculation software:
4. logged timestamps inside the debug .csv file are formatted for office calculation software:
days since 1900-01-01 00:00 UTC (`Bangle / (24 * 60 * 60 * 1000) + 25569`)
5. every 14 days the `sleeplog.log (StorageFile)` is reduced and old entries are moved into separat files for each fortnight (`sleeplog_1234.log`) but still accessible though the app:
5. every 14 days the `sleeplog.log (StorageFile)` is reduced and old entries are moved into separat files for each fortnight (`sleeplog_1234.log`) but still accessible though the app:
fortnights since 1970-01-04 12:00 UTC (converted with `require("sleeplog").msToFn(Bangle)` and `require("sleeplog").fnToMs(fortnight)`)
- __Logfiles from before 0.10:__
- __Logfiles from before 0.10:__
timestamps and sleeping status of old logfiles are automatically converted on your first consecutive sleep or manually by `require("sleeplog").convertOldLog()`
- __View logged data:__
if you'd like to view your logged data in the IDE, you can access it with `require("sleeplog").printLog(since, until)` or `require("sleeplog").readLog(since, until)` to view the raw data
- __View logged data:__
if you'd like to view your logged data in the IDE, you can access it with `require("sleeplog").printLog(since, until)` or `require("sleeplog").readLog(since, until)` to view the raw data
since & until in Bangle timestamp, e.g. `require("sleeplog").printLog(Date()-24*60*60*1000, Date())` for the last 24h
---
### Access statistics (developer information)
### Developer Information
---
- Last Asleep Time [Date]:
#### Access statistics
- Last Asleep Time [Date]:
`Date(sleeplog.awakeSince)`
- Last Awake Duration [ms]:
- Last Awake Duration [ms]:
`Date() - sleeplog.awakeSince`
- Last Statistics [object]:
- Last Statistics [object]:
```
// get stats of the last night (period as displayed inside the app)
// as this might be the mostly used function the data is cached inside the global object
// as this might be the mostly used function the data is cached inside the global object
sleeplog.getStats();
// get stats of the last 24h
@ -130,25 +154,50 @@ Available through the App Loader when your watch is connected.
={ calculatedAt: 1653123553810, deepSleep: 250, lightSleep: 150, awakeSleep: 10,
consecSleep: 320, awakeTime: 1030, notWornTime: 0, unknownTime: 0, logDuration: 1440,
firstDate: 1653036600000, lastDate: 1653111600000 }
// to get the start of a period defined by "Break TOD" of any date
var startOfBreak = require("sleeplog").getLastBreak();
// same as
var startOfBreak = require("sleeplog").getLastBreak(Date.now());
// output as date
=Date: Sat May 21 2022 12:00:00 GMT+0200
// get stats of this period as displayed inside the app
require("sleeplog").getStats(require("sleeplog").getLastBreak(), 24*60*60*1000);
// or any other day
require("sleeplog").getStats(require("sleeplog").getLastBreak(Date(2022,4,10)), 24*60*60*1000);
```
- Total Statistics [object]:
- Total Statistics [object]:
```
// use with caution, may take a long time !
require("sleeplog").getStats(0, 0, require("sleeplog").readLog());
```
#### Add functions triggered by status changes or inside a specified time period
With the following code it is possible to add functions that will be called every 10 minutes after new movement data when meeting the specified parameters on each :
```
// first ensure that the sleeplog trigger object is available (sleeplog is enabled)
if (typeof (global.sleeplog || {}).trigger === "object") {
// then add your parameters with the function to call as object into the trigger object
sleeplog.trigger["my app name"] = {
onChange: false, // false as default, if true call fn only on a status change
from: 0, // 0 as default, in ms, first time fn will be called
to: 24*60*60*1000, // 24h as default, in ms, last time fn will be called
// reference time to from & to is rounded to full minutes
fn: function(data) { print(data); } // function to be executed
};
}
```
The passed data object has the following properties:
- timestamp: of the status change as date object,
(should be around 10min. before "now", the actual call of the function)
- status: value of the new status (0-4),
(0 = unknown, 1 = not worn, 2 = awake, 3 = light sleep, 4 = deep sleep)
- consecutive: value of the new status (0-2),
(0 = unknown, 1 = no consecutive sleep, 2 = consecutive sleep)
- prevStatus: if changed the value of the previous status (0-4) else undefined,
- prevConsecutive: if changed the value of the previous status (0-2) else undefined
---
### Worth Mentioning
@ -156,14 +205,14 @@ Available through the App Loader when your watch is connected.
#### To do list
- Check translations.
- Add more functionallities to interface.html.
- Enable recieving data on the Gadgetbridge side + testing.
- Enable receiving data on the Gadgetbridge side + testing.
__Help appreciated!__
#### Requests, Bugs and Feedback
Please leave requests and bug reports by raising an issue at [github.com/storm64/BangleApps](https://github.com/storm64/BangleApps) (or send me a [mail](mailto:banglejs@storm64.de)).
#### Creator
Storm64 ([Mail](mailto:banglejs@storm64.de), [github](https://github.com/storm64))
Storm64 ([mail](mailto:banglejs@storm64.de), [github](https://github.com/storm64))
#### Contributors
myxor ([github](https://github.com/myxor))

View File

@ -124,7 +124,7 @@ if (sleeplog.conf.enabled) {
if (!sleeplog.info.saveUpToDate || force) {
// save status, consecutive status and info timestamps to restore on reload
var save = [sleeplog.info.lastCheck, sleeplog.info.awakeSince, sleeplog.info.asleepSince];
// add debuging status if active
// add debuging status if active
if (sleeplog.debug) save.push(sleeplog.debug.writeUntil, sleeplog.debug.fileid);
// stringify entries
@ -253,11 +253,38 @@ if (sleeplog.conf.enabled) {
}
}
// check if the status has changed
var changed = data.status !== this.status || data.consecutive !== this.consecutive;
// read and check trigger entries
var triggers = Object.keys(this.trigger) || [];
if (triggers.length) {
// calculate time from timestamp in ms on full minutes
var time = new Date();
time = (time.getHours() * 60 + time.getMinutes()) * 60 * 1000;
// go through all triggers
triggers.forEach(key => {
// read entry to key
var entry = this.trigger[key];
// check if the event matches the entries requirements
if (typeof entry.fn === "function" && (changed || !entry.onChange) &&
(entry.from || 0) <= time && (entry.to || 24 * 60 * 60 * 1000) >= time)
// and call afterwards with status data
setTimeout(entry.fn, 100, {
timestamp: new Date(data.timestamp),
status: data.status,
consecutive: data.consecutive,
prevStatus: data.status === this.status ? undefined : this.status,
prevConsecutive: data.consecutive === this.consecutive ? undefined : this.consecutive
});
});
}
// cache change into a known consecutive state
var changeIntoConsec = data.consecutive;
// check if the status has changed
if (data.status !== this.status || data.consecutive !== this.consecutive) {
// actions on a status change
if (changed) {
// append status
this.appendStatus(data.timestamp, data.status, data.consecutive);
@ -268,7 +295,7 @@ if (sleeplog.conf.enabled) {
// reset saveUpToDate status
delete this.info.saveUpToDate;
}
// send status to gadgetbridge
var gb_kinds = "unknown,not_worn,activity,light_sleep,deep_sleep";
Bluetooth.println(JSON.stringify({
@ -319,7 +346,10 @@ if (sleeplog.conf.enabled) {
}
// return stats cache
return this.statsCache;
}
},
// define trigger object
trigger: {}
}, sleeplog);
// initial starting

View File

@ -149,14 +149,6 @@ exports = {
// define move log function, move StorageFile content into files seperated by fortnights
moveLog: function(force) {
/** convert old logfile (< v0.10) if present **/
if (require("Storage").list("sleeplog.log", {
sf: false
}).length) {
convertOldLog();
}
/** may be removed in later versions **/
// first day of this fortnight period
var thisFirstDay = this.fnToMs(this.msToFn(Date.now()));
@ -384,82 +376,5 @@ exports = {
"unknown,not worn,awake,light sleep,deep sleep".split(",")[entry[1]].padEnd(12) +
"for" + (duration + "min").padStart(8));
});
},
/** convert old (< v0.10) to new logfile data **/
convertOldLog: function() {
// read old logfile
var oldLog = require("Storage").read("sleeplog.log") || "";
// decode data if needed
if (!oldLog.startsWith("[")) oldLog = atob(oldLog);
// delete old logfile and return if it is empty or corrupted
if (!oldLog.startsWith("[[") || !oldLog.endsWith("]]")) {
require("Storage").erase("sleeplog.log");
return;
}
// transform into StorageFile and clear oldLog to have more free ram accessable
require("Storage").open("sleeplog_old.log", "w").write(JSON.parse(oldLog).reverse().join("\n"));
oldLog = undefined;
// calculate fortnight from now
var fnOfNow = this.msToFn(Date.now());
// open StorageFile with old log data
var file = require("Storage").open("sleeplog_old.log", "r");
// define active fortnight and file cache
var activeFn = true;
var fileCache = [];
// loop through StorageFile entries
while (activeFn) {
// define fortnight for this entry
var thisFn = false;
// cache new line
var line = file.readLine();
// check if line is filled
if (line) {
// parse line
line = line.substr(0, 15).split(",").map(e => parseInt(e));
// calculate fortnight for this entry
thisFn = this.msToFn(line[0]);
// convert timestamp into 10min steps
line[0] = line[0] / 6E5 | 0;
// set consecutive to unknown
line.push(0);
}
// check if active fortnight and file cache is set, fortnight has changed and
// active fortnight is not fortnight from now
if (activeFn && fileCache.length && activeFn !== thisFn && activeFn !== fnOfNow) {
// write file cache into new file according to fortnight
require("Storage").writeJSON("sleeplog_" + activeFn + ".log", fileCache);
// clear file cache
fileCache = [];
}
// add line to file cache if it is filled
if (line) fileCache.push(line);
// set active fortnight
activeFn = thisFn;
}
// check if entries are leftover
if (fileCache.length) {
// format fileCache entries into a string
fileCache = fileCache.map(e => e.join(",")).join("\n");
// read complete new log StorageFile as string
file = require("Storage").open("sleeplog.log", "r");
var newLogString = file.read(file.getLength());
// add entries at the beginning of the new log string
newLogString = fileCache + "\n" + newLogString;
// rewrite new log StorageFile
require("Storage").open("sleeplog.log", "w").write(newLogString);
}
// free ram
file = undefined;
fileCache = undefined;
// clean up old files
require("Storage").erase("sleeplog.log");
require("Storage").open("sleeplog_old.log", "w").erase();
}
/** may be removed in later versions **/
};

View File

@ -2,7 +2,7 @@
"id":"sleeplog",
"name":"Sleep Log",
"shortName": "SleepLog",
"version": "0.11",
"version": "0.12",
"description": "Log and view your sleeping habits. This app is using the built in movement calculation.",
"icon": "app.png",
"type": "app",