new app: heatsuite
parent
f6b8aa5942
commit
97e55d22cb
|
|
@ -15,3 +15,4 @@ _site
|
|||
Desktop.ini
|
||||
.sync_*.db*
|
||||
*.swp
|
||||
apps/heatsuite/heatsuite.5sts.js
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
0.01: New App!
|
||||
0.05: Added functionality for incorporating BTHRM
|
||||
0.06: Modified BLE Broadcasting; Includes Optical HR and Wrist Temperature
|
||||
0.07: Fixing storage overloaded issue
|
||||
0.08: Added scrolling to Surveys. CORESensor app added.
|
||||
0.09: Added High Temporal Accelerometry logging (x,y,z per second)
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
# HeatSuite Watch Application
|
||||
|
||||
This is the HeatSuite Watch Application which allows for seemless integration into the HeatSuite platform ([read the docs](https://heatsuitelabs.github.io/HeatSuiteDocs/) and our [research](#research-using-heatsuite)). You may use this watch application independent of the full(er) HeatSuite platform.
|
||||
|
||||
## What is HeatSuite?
|
||||
|
||||
HeatSuite is a comprehensive all in one solution for researchers to monitor the physiological, behavioural, and perceptual responses of individuals and their personal environmental exposure. Learn more on the details of the HeatSuite platform by [reading the docs](https://heatsuitelabs.github.io/HeatSuiteDocs/).
|
||||
|
||||
## Why do we need this?
|
||||
|
||||
Consumer and research-based wearables have largely determined what researchers can measure in the field, and/or have proprietary postprocessing embedded into the hardware/software stack which limits transparency and transferrability of data collected. HeatSuite challenges this one-sided relationship, offering a solution for researchers who desire access and awareness of how and what data they are collecting from *their* participants.
|
||||
|
||||
## Watch Specific Features
|
||||
|
||||
This is a list of current features available when using the HeatSuite Watch Application:
|
||||
|
||||
+ Per minute averaging and/or sum of onboard watch sensor data (Heart rate, barometer temperature and pressure, accelerometer, battery)
|
||||
+ High temporal resolution accelerometer logging (x,y,z per second)
|
||||
+ Can connect external bluetooth devices for added physiological monitoring (e.g. Bluetooth Heart Rate, CORE Sensor) - more being added
|
||||
+ Connect and store data from other devices including:
|
||||
+ Blood Pressure Monitor (A&D Medical UA651-BLE)
|
||||
+ Oral Temperature using custom dongle - Contact [Nicholas Ravanelli, PhD](emailto:nick.ravanelli@gmail.com)
|
||||
+ Body Mass Scale (Xiaomi Composition Scale 2)
|
||||
+ Collect perceptions and behaviour using ecological momentary assessments with onboard questionnaires
|
||||
+ Mictruition frequency and color analysis for index of hydration status
|
||||
+ Create study schedules for participants to receive programmatic nudges daily, specific to each task
|
||||
+ Programmatic GPS monitoring, with adaptive power switching for battery optimization
|
||||
+ Fall Detection and bluetooth broadcasting (beta)
|
||||
|
||||
## Can I request or add a feature?
|
||||
|
||||
Certainly!! To help ensure that the release on BangleApps is always functional and works with the other devices within the HeatSuite ecosystem, please make any feature requests or code updates to the forked version within the [HeatSuiteLabs repository](https://github.com/HeatSuiteLabs/BangleApps). That is our testing bed before we push to the official Bangle Apps git.
|
||||
|
||||
## Watch accelerometer data
|
||||
|
||||
The current interation of the HeatSuite application provides the option to average the accelerometer `x,z,y` every second or as needed (known as High Temporal Resolution Accelerometer Logging), and/or magnitude per minute. Magnitude is calculated in the Espruino firmware as:
|
||||
|
||||
```
|
||||
sqrt(x^2 + y^2 + z^2)
|
||||
```
|
||||
|
||||
To transform this to [Euclidean norm minus one (ENMO)](https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0061691) format:
|
||||
|
||||
```
|
||||
ENMO = acc_avg - 1
|
||||
```
|
||||
Where `acc_avg` is the average acceleration magnitude per minute, available in the CSV file. While this may be satisfactory for offline long term monitoring (+2 weeks), it is recommended to use per second `mag` data.
|
||||
|
||||
Previous research has demonstrated strong agreement between the onboard accelerometer of the Bangle.js2 and a research grade ActiGraph GT9X;
|
||||
|
||||
Van Laerhoven, K., Hoelzemann, A., Pahmeier, I. et al. Validation of an open-source ambulatory assessment system in support of replicable activity studies. *Ger J Exerc Sport Res* 52, 262–272 (2022). https://doi.org/10.1007/s12662-022-00813-2
|
||||
|
||||
## Applications/modules that HeatSuite integrates:
|
||||
|
||||
* [BTHRM](https://banglejs.com/apps/#bthrm)
|
||||
* [coretemp](https://banglejs.com/apps/#coretemp)
|
||||
* [gpssetup](https://banglejs.com/apps/#gpssetup)
|
||||
* [recorder](https://banglejs.com/apps/#recorder) (modified in HeatSuite code to incorporate per minute averaging)
|
||||
|
||||
## Icons
|
||||
|
||||
HeatSuite uses icons from [Flaticon.com](https://www.flaticon.com) & [Freepik.com](https://www.freepik.com)
|
||||
|
||||
## Research Using HeatSuite
|
||||
|
||||
A full list of peer-reviewed research and conference preceedings using HeatSuite can be found [here](https://heatsuitelabs.github.io/HeatSuiteDocs/research/)
|
||||
|
||||
## To Do
|
||||
|
||||
* Finish Download All and Delete All functions on custom.html
|
||||
* Graphical User Interface for EMA and Task Development
|
||||
|
||||
## Creator
|
||||
|
||||
[Nicholas Ravanelli, PhD](https://github.com/nravaneli)
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("ksqwcBkmSpICCmVCAoYCGyckyALIAQMmochDROSskmFJeZkmEBZEAhMmpFJkEABwtAgECpgaBMIMSBwmApEEyMJkkCoApFgGAhMiiVIAoILCAoIjCgFAggDBggIBEwICByEJB4UEDQJHBhAFCI4QaBAoZcCFIJHEAogjCI4QjBKYJoCgAODNAJKCBYQABRIY1BAoZTCNwhTBNARNEAQUBSQeQAoILC5LXBF4WAhADBXIVMEQJQBRgcJAoNkyUZhOQNAoFC5AaBiYpBI4cBAoWQeQOSoVJgmQFIQFByZJBBwK2CgEQKAMADoPJEAbdCa4ZuHkBuDXgiwCgS2FAoaqDgQFBWwQACiC/CEwSwDDoQyBI4i2DAoQsBEwIIBCgIFDJoIpBEYIFBGQQdCOISDBBQRWCJQjdFojdGAQUCoEkxBQBJoiSDFgLUBoApCBwhHBXgJxCBYgCDmVJNwYCGychmRNDAQ/IsgLJAQJJBDRWSsmEBAo"))
|
||||
|
|
@ -0,0 +1,553 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
|
||||
<style>
|
||||
.jsoneditor-container {
|
||||
height: 500px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.editor-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ul class="tab tab-block" id="tab-navigate">
|
||||
<li class="tab-item active" id="tab-settingsContainer">
|
||||
<a href="javascript:showTab('settingsContainer')">Settings</a>
|
||||
</li>
|
||||
<li class="tab-item" id="tab-tasksContainer">
|
||||
<a href="javascript:showTab('tasksContainer')">Tasks</a>
|
||||
</li>
|
||||
<li class="tab-item" id="tab-surveyContainer">
|
||||
<a href="javascript:showTab('surveyContainer')">EMA</a>
|
||||
</li>
|
||||
<li class="tab-item" id="tab-downloadData">
|
||||
<a href="javascript:showTab('downloadData')">
|
||||
<div id="downloadData-tab-label" class="" data-badge="0">Download
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="container apploader-tab" id="settingsContainer">
|
||||
<form id="heatsuiteSettings">
|
||||
<div class="row pt-2"><strong>Recorder Options:</strong>
|
||||
<br>Select what sensor/data you want to record and average each minute:
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="steps">
|
||||
<i class="form-icon"></i> Steps
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="hrm">
|
||||
<i class="form-icon"></i> Optical Heart Rate
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="acc">
|
||||
<i class="form-icon"></i> Accelerometry (per minute magnitude)
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="bat">
|
||||
<i class="form-icon"></i> Battery
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="movement">
|
||||
<i class="form-icon"></i> Movement
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="baro">
|
||||
<i class="form-icon"></i> Temperature/Pressure
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="bthrm">
|
||||
<i class="form-icon"></i> Bluetooth HRM (Uses BTHRM app/module)
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="CORESensor">
|
||||
<i class="form-icon"></i> CORE Sensor (Uses coretemp app/module)
|
||||
</label>
|
||||
</div>
|
||||
<div class="row pt-2"><strong>High Resolution Accelerometer Data:</strong>
|
||||
<br>If you want to record high temporal resolution accelerometer data (magnitude of x,y,z; can store up to 4 days of data at 1 second, will rollover).
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="highAcc">
|
||||
<i class="form-icon"></i> High Temporal Accelerometry √(x² + y² + z²)
|
||||
</label>
|
||||
<label class="form-label" for="input-AccLogInt">Interval (seconds)</label>
|
||||
<input class="form-input" type="number" name="AccLogInt" id="input-AccLogInt" value=1 min=1>
|
||||
<p class="form-input-hint mb-0">Interval for logging averaged magnitude and sum from accelerometer - default is 1 second
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pt-2"><strong>GPS PSMOO Options:</strong>
|
||||
<br>Option to have GPS work in PSMOO (uses GPSSetup app/module)
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="GPS">
|
||||
<i class="form-icon"></i> Turn GPS On
|
||||
</label>
|
||||
<label class="form-label" for="input-GPSScanTime">Scan Time (mins)</label>
|
||||
<input class="form-input" type="number" name="GPSScanTime" id="input-GPSScanTime" value=2 min=0>
|
||||
<p class="form-input-hint mb-0">The time spent scanning for a GPS signal.</strong></p>
|
||||
<label class="form-label" for="input-GPSInterval">Scan Interval (mins)</label>
|
||||
<input class="form-input" type="number" name="GPSInterval" id="input-GPSInterval" value=10 min=0>
|
||||
<p class="form-input-hint mb-0">The time between scans when a <strong>signal is not acquired.</strong></p>
|
||||
<label class="form-label" for="input-GPSAdaptiveTime">Adaptive Time (mins)</label>
|
||||
<input class="form-input" type="number" name="GPSAdaptiveTime" id="input-GPSAdaptiveTime" value=2 min=0>
|
||||
<p class="form-input-hint mb-0">The time between scans when a <strong>signal is acquired.</strong></p>
|
||||
</div>
|
||||
<div class="row pt-2"><strong>Extras:</strong>
|
||||
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="fallDetect">
|
||||
<i class="form-icon"></i> Fall Detection
|
||||
</label>
|
||||
<p class="form-input-hint mb-0">Will detect falls.</p>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="surveyRandomize">
|
||||
<i class="form-icon"></i> Randomize EMA questions
|
||||
</label>
|
||||
<p class="form-input-hint mb-0">Enable this if you want your questions to be shuffled at random each time.</p>
|
||||
<label class="form-label" for="input-studyID">Study ID:</label>
|
||||
<input class="form-input" type="text" name="studyID" id="input-studyID" value="" minlength="1" maxlength="4">
|
||||
<p class="form-input-hint mb-0">For communicating with HeatSuite Nodes. Maximum of 4 (no special) characters.
|
||||
</p>
|
||||
<label class="form-label" for="input-filePrefix">File Prefix</label>
|
||||
<input class="form-input" type="text" name="filePrefix" id="input-filePrefix" value="htst" minlength="1"
|
||||
maxlength="5">
|
||||
<p class="form-input-hint mb-0">ONLY CHANGE IF YOU KNOW WHAT YOU ARE DOING.</p>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="container apploader-tab pb-2" id="tasksContainer" style="display:none;">
|
||||
<div class="row pt-2"><strong>Edit your Task JSON File Editor:</strong>
|
||||
<br><a href="https://heatsuitelabs.github.io/HeatSuiteDocs/" target="_blank">Read the Docs</a> on how to properly format the JSON file. GUI coming soon to BangleApps.
|
||||
</div>
|
||||
|
||||
<div id="heatsuite_taskFile_editor" class="jsoneditor-container"></div>
|
||||
<button class="btn btn-primary" id="heatsuite_taskFile_resetBtn">Restore Default</button>
|
||||
</div>
|
||||
<div class="container apploader-tab pb-2" id="surveyContainer" style="display:none;">
|
||||
<div class="row pt-2"><strong>Ecological Momentary Assessment (EMA) JSON File Editor:</strong>
|
||||
<br><a href="https://heatsuitelabs.github.io/HeatSuiteDocs/" target="_blank">Read the Docs</a> on how to properly format the JSON file. GUI coming soon to BangleApps.
|
||||
</div>
|
||||
<div id="heatsuite_surveyFile_editor" class="jsoneditor-container"></div>
|
||||
<button class="btn btn-primary" id="heatsuite_surveyFile_resetBtn">Restore Default</button>
|
||||
</div>
|
||||
<div class="container apploader-tab pb-2" id="downloadData" style="display:none;">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File name</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="downloadData-tab-content">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="p-2 m-2"><button id="upload" class="btn btn-success">Upload</button></p>
|
||||
|
||||
<script src="../../core/lib/customize.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/jsoneditor@latest/dist/jsoneditor.min.css" rel="stylesheet" type="text/css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsoneditor@latest/dist/jsoneditor.min.js"></script>
|
||||
|
||||
<script>
|
||||
//default Schema
|
||||
let heatsuite__taskFile_defaultSchema = [
|
||||
{
|
||||
"id": "survey",
|
||||
"icon": 'require("heatshrink").decompress(atob("lEo4kA///6H7BIP2m9hjEpyQLBxeq0UAp3js1Z5F59GB9nChvL+83E4cCkQABkA6NgUq293rVCChsq6MRAAOcoQmM4ISCAAPqFBclCQkRjJpBExOxCYsR8goJkmRB4XuAYUVCZMsJYdEFgUZoATIpwTC1XnooFC2QTIoIhCC4OqCYVyMRGhBoXhjNLAoXSCZkR2mpAgXilGIwRnFCYlKPgd40czmeoCglJCYd5AgdYn////zCgknBwfZAgeqCQIUCwQTClQOD3oDCj1DCYf/nATHAAcawYTE/Q8CkoTHvE/+c6mY8FMYgACi9Kn84xAqDmSfC7euCYnE0f/1CKBPQRQBgWqkUk3zeCvWKBwJ4BnATFKQUipWq0koBoSyBxATDZYsCkUAOgk4AoYTGAAUjCYgmCUAgAFlQTDRQQTGgUowQuBJ4f/nQEDRYIiD81u0UAhAODmYECeAkCq1ms1nkECbIoACD4ImCCQIAB2QGBFAYADHYchs3nv1msguBY4IAD+bhBCYVGs261YTCMovz1GKnSfCCYIACtRXCxTHBeAJYBxGACYek8wnDSYWIxAjCdocls0eE4PiXhAAEhEWCQNhoATNgFOCYPkCRxABpdqIQQUP2gSQCYMiCaMAHRw="))',
|
||||
"cbBtn": "Bangle.load('heatsuite.survey.js');",
|
||||
"tod": [
|
||||
900,
|
||||
1200,
|
||||
1500,
|
||||
1800
|
||||
],
|
||||
"debounce": 300
|
||||
},
|
||||
{
|
||||
"id": "bloodPressure",
|
||||
"icon": 'require("heatshrink").decompress(atob("lEo4kA///zND//3BYOe98ggHGhEllM533ssW5odBhFOud642DhE10omCnczHSO3s3jmATP3MRjNrCh+2s0ZzNrCJkDn1mv1382X8YlMyMRi1xy1hzwSKhdxs12CYIWBu5RKm8WtMeCYfp8ATJ2MRiITDjPpsCUCu4ADv2w24TBjITCiOZE4MLCQgABve+MQMXCYeW2ATBv2ZAAeeve28w7Ey3mE4dCkQACx172INBtd+s1m81xJ4ITBpH/wn//oTB29pjIRBs1nAgITD7l3zGXv4nDsJRBiORz1hCYdCogBCCYWhiNusNutIWByA7EKIXXCYOq1QPBAAIZBiA7DvITCp4TDAAYTBHYp3CpgTNxvdAAX+CZl+9wAECZjDBbIQEBCZwlC9ITOy98zOcFAITOzl+vgTQvJSCJ6QWBCYMaCZED852CAAXj21pCgkRswTBgEzmc78/rAgMwm1usLvDz1m2ATBAAMD5+TAodpiIVBt3njN+CYvn8YGDnwnEu18uAMDgHpDQkAnfms1mtc+/4TFvngAwgxCAQV3EAt8AwoWFvIMF5gTKgF/HYv5mATKnPLAoc3+4TLh3J8ayCvieDKBN85LVBznJJxaSCvPM5nJuYSMCgW73ezBIoA="))',
|
||||
"tod": [
|
||||
830,
|
||||
1330,
|
||||
1930
|
||||
],
|
||||
"debounce": 3600,
|
||||
"btPair": true,
|
||||
"btInfo": {
|
||||
"service": "1810",
|
||||
"supported": [
|
||||
"A&D_UA-651"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "coreTemperature",
|
||||
"icon": 'require("heatshrink").decompress(atob("lEo4kA///A4Pf99fx3YtnjjEkkN/7+X0ujoFK/VusdBhH0ykIoXY2ozPjnBIyEBr2piATPiuq1WREx9aCYIoPjmaCYIoOgNo9ITC1gTMi3yvwoC9gmNw8nFAVRExmL5dyFAOlMZcBt+2223FAImN+9Emlru/cExlo6kzme8x9hJptkmYnBwwmN21DmdG3Am/TZF28YmPgvyl9rEx0AjsikX28wmNgFnkVyFAImNgHnl7pBvAmNgOyk/3u/2ExsM6UiEx8Ajd3u9/6omNgO3u/b8wSNgEeu974IRNEwPvu+xCRzrB+/+HBwABrF/8ISPhnXYBwAC5t9qImQ2/cCR8Ajm32ASPgPr5YmQj3eMCEB9qHQEycA5wmROoImRgAA=="))',
|
||||
"tod": [
|
||||
830,
|
||||
1330,
|
||||
1930
|
||||
],
|
||||
"TaskDesc": "coreTemperature_TaskDesc",
|
||||
"debounce": 3600,
|
||||
"btPair": true,
|
||||
"btInfo": {
|
||||
"service": "1809",
|
||||
"supported": [
|
||||
"BLEThermistorPod"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bodyMass",
|
||||
"icon": "require('heatshrink').decompress(atob('lEo4kB3//AAIJB7//wUQjnn485t97vvvoMIhlj9dasspptbz33kshiIApkUpylP/nGswAB5n/pOSkQSEl+3utVAAOI93uxAGCq96+QSCil1gczABdXyITBo4SDmt6ut61Wq09TBQU+ukRiVwDgcK5t1vvd7vc0ALD88hjgbDmcLvxOBAAOH2ALDgvBk5EEq1+wpfBrGHsoLDh1yyouEtN+q3M5llw+WvwMCgF5CYMFu9V3l2E4tn5dVu9Qh1ZCYN0/9E2s65phBMYWjrdE/9HCYOXmv2u9kqaLBRIKLDmtLu9vqF5y83o930lzWA83+4MCvMnmtKCZYgB2lQuQTBtmr5ijEAAc15mr+1euUqTYNESwgAFBgUO0Urgc+uwmIFAVn8fu2UW8czu2DCZM4s8z9FhCYVcE5fFmflsMcCAITP4MfCaPxigQB8yeIUAVumHloMZCYMLCZewCYORCYU32YTJndzCYUSEgM34p7BAA0+rgTBg8hCYM+ulE2oTHrdEo/g84TCmvLvdHgYSFmF009kuYTEu93tiNGDwN3vm+CYWjm9nCZvqkMR2E3pV6pQ7H1932lw9cRiNonGk+llMY9Wp9KwFWCYNFZgN3HQflrw8Du9emFUCYMqG41cdIIAEh2yCYMUbI1VqpSF89BCYMRo7FIAAc4ugSCiMZ2tY9wAGn3uwt7yITDiMp+1mAAtqrVmt+SCQgAl'))",
|
||||
"tod": [
|
||||
800
|
||||
],
|
||||
"debounce": 3600,
|
||||
"btPair": false,
|
||||
"btInfo": {
|
||||
"service": "181b",
|
||||
"supported": [
|
||||
"MIBFS"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urine",
|
||||
"icon": "require('heatshrink').decompress(atob('lEo4kA///7/3oMInGVtdC//3rvn5lS7/Gx1rksZDYIXB7edkAFB4wqGhcukXgHp0Lkkz6eaCheykWq9Ux5nM42elWilwjH0c97VG5lVqtcjOp7s3JwIAEgU4xF6tgSBAAPBzXf/FACYsK7GD1vFCYdcjuIx+bCY/azgSDAANRyePzwTHzQ6EAAWax87CY2Z1I6EHgVqvNyMY0q1ATGqsX1XrRg1JoITH4dJRY0Ao5iGAAPN1MkogVFogTI42p1uIUItEngTJ0lE0AnQle7J59d1JjHoemO4/JoYTHpGjWZGTCY9BzWMCYvBzVjCZFn1tc5gADtOtqewCY2py/qvOZAAeu1OUCY0LSYN3ogAEo90Yo0AgU4xAAJYooTBzvdABLFFAAMq1QAJMY+0ExMyCQ0AgkRjiKEAAQTIhVhirbGrmeCY8C6MRCY1VvYTHhwTI4x2HAANhCY/Ndw4AB0YTG5mERQ5QCods5nFrnM4Np1YSIAAOy1PWiMRtE31wSKgEL0mq0lEAQJNJHgc96c5zM9xFLCZcE7GPAAP4x7FIAAe5xGP/4AC7QTLlMzAAk0RRIAB8UiAAkuCYwA=='))",
|
||||
"cbBtn": "Bangle.load('heatsuite.urine.js');",
|
||||
"debounce": 0,
|
||||
"tod": [],
|
||||
"btPair": false,
|
||||
"btInfo": null,
|
||||
"notify": false
|
||||
},
|
||||
{
|
||||
"id": "sleep",
|
||||
"icon": "require('heatshrink').decompress(atob('lEo4kA///v3nBIPDjE968CqOzxlX/1m0VBhGBkHb61qpV4/0978Y2xZ1gQTmkQ9VkA9llJKMJZMjmRADlM85gAC4eSBYUCCIOW23Mmc281v30RAAXr+wLBmfMt9py0Rj27CAgAEBYIMCtMsB5AAE///33u4UD2ISLjfMnNu9cwhPxE5nr5m+9+QgVhBYhpBAAVrA4Mcn/ui0gCYsWnVVAANa0YLBsfxCZFjrGIAAWFm0fRQP+CY0fmoSDAANT/dmswTCMYkc1ATFxXBUIPuMYMDCYfsquqAAOlxGqqs7CYcwT4mzqfGGgNmu9onQTB93uT4ITFzHdAAd3xM/CYo7D2eWCYtpCYY7BMYgTNY4vzCY1jz4TCT4r5Bs92v996/3AoPM2J3BCYsfm/duvDtvWnk3u9z+ITIm3d63IE4Nss48BCYhjE5l9J4t35jHFT4fzr/dJoJRBu9lO4ITBT4MsNAPhHgPM7vfnnD/t3wf+8ITB93Cy0R9e//exspNBtGGHQQLC30RtOW+3Mmcz5/ztpOC692ngLC5lvtMjmUggAABhh5BWQciBYUCCIIQCAAcjs4SBuwMHAA+V42MrISOAAMikQSQAD4='))",
|
||||
"cbBtn": 'modHS.saveDataToFile("sleep", "event", {"marker":"sleep"});Bangle.showClock();',
|
||||
"debounce": 0,
|
||||
"tod": [],
|
||||
"btPair": false,
|
||||
"btInfo": null,
|
||||
"notify": false
|
||||
}
|
||||
];
|
||||
let heatsuite__surveyFile_defaultSchema = {
|
||||
"supported":{
|
||||
"en_GB":"English (GB)",
|
||||
"fr_CA":"Francais (CA)"
|
||||
},
|
||||
"questions":[{
|
||||
"key":"comfort",
|
||||
"text": {
|
||||
"en_GB":"Thermal comfort?",
|
||||
"fr_CA":"Confort thermique?"
|
||||
},
|
||||
"tod":[[0,2359]],
|
||||
"oncePerDay": true,
|
||||
"orderFix":false,
|
||||
"options": [{
|
||||
"text":{
|
||||
"en_GB":"Comfortable",
|
||||
"fr_CA":"Confortable"
|
||||
},
|
||||
"value":0,
|
||||
"color":"#ffffff",
|
||||
"btnColor":"#38ed35"
|
||||
},{
|
||||
"text":{
|
||||
"en_GB":"Uncomfortable",
|
||||
"fr_CA": "Inconfortable"
|
||||
},
|
||||
"value":1,
|
||||
"color":"#ffffff",
|
||||
"btnColor":"#ff0019"
|
||||
}]
|
||||
}]
|
||||
};
|
||||
//function from original index.js of BangleApps - required to bring here as the custom.html is loaded in an iframe
|
||||
function showTab(tabname) {
|
||||
document.querySelectorAll("#tab-navigate .tab-item").forEach(tab => {
|
||||
tab.classList.remove("active");
|
||||
});
|
||||
document.querySelectorAll(".apploader-tab").forEach(tab => {
|
||||
tab.style.display = "none";
|
||||
});
|
||||
document.getElementById("tab-" + tabname).classList.add("active");
|
||||
document.getElementById(tabname).style.display = "inherit";
|
||||
}
|
||||
function formDataToJson(form) {
|
||||
const formData = new FormData(form);
|
||||
const data = {};
|
||||
const forceArrayFields = ['record']; // force these to always be arrays
|
||||
const checkboxHandled = new Set();
|
||||
for (let [name, value] of formData.entries()) {
|
||||
const field = form.querySelector(`[name="${name}"]`);
|
||||
if (field && field.type === 'checkbox') {
|
||||
if (checkboxHandled.has(name)) continue;
|
||||
checkboxHandled.add(name);
|
||||
const checkboxes = form.querySelectorAll(`input[type="checkbox"][name="${name}"]`);
|
||||
const values = Array.from(checkboxes)
|
||||
.filter(cb => cb.checked)
|
||||
.map(cb => cb.value);
|
||||
|
||||
data[name] = forceArrayFields.includes(name) ? values : (values.length ? values[0] : false);
|
||||
} else {
|
||||
if (data.hasOwnProperty(name)) {
|
||||
if (!Array.isArray(data[name])) {
|
||||
data[name] = [data[name]];
|
||||
}
|
||||
data[name].push(value);
|
||||
} else {
|
||||
if (name === "studyID" && !value.length) continue;
|
||||
data[name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
forceArrayFields.forEach(field => {
|
||||
if (!Array.isArray(data[field])) {
|
||||
data[field] = data[field] !== undefined ? [data[field]] : [];
|
||||
}
|
||||
});
|
||||
return data;
|
||||
}
|
||||
function fillFormFromJson(form, data) {
|
||||
for (let [name, value] of Object.entries(data)) {
|
||||
const fields = form.querySelectorAll(`[name="${name}"]`);
|
||||
if (!fields.length) continue;
|
||||
fields.forEach(field => {
|
||||
if (field.type === 'checkbox') {
|
||||
if (Array.isArray(value)) {
|
||||
field.checked = value.includes(field.value);
|
||||
} else {
|
||||
field.checked = value === true || value === field.value || value === "true";
|
||||
}
|
||||
} else if (field.type === 'radio') {
|
||||
field.checked = (field.value === value);
|
||||
} else {
|
||||
field.value = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//autosave form data to localstorage
|
||||
function autosaveSettings(){
|
||||
const form = document.getElementById('heatsuiteSettings');
|
||||
const formjson = formDataToJson(form);
|
||||
localStorage.setItem("heatuite__settings", JSON.stringify(formjson));
|
||||
}
|
||||
function readStorageJSONAsync(filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Util.readStorageJSON(filename, function (data) {
|
||||
try {
|
||||
resolve(data || {});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function readStorageAsync(filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Util.readStorageFile(filename, function (data) {
|
||||
try {
|
||||
resolve(data);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function DownloadSingleFile(filename){
|
||||
Util.readStorageFile(filename, (c)=>{
|
||||
let url;
|
||||
const a = document.createElement('a');
|
||||
fnArr = filename.split('_');
|
||||
if(fnArr[1] !== 'accel'){
|
||||
const blob = new Blob([c], { type: 'text/plain' });
|
||||
url = URL.createObjectURL(blob);
|
||||
}else{
|
||||
|
||||
function secondsToClock(seconds) {
|
||||
let h = Math.floor(seconds / 3600);
|
||||
let m = Math.floor((seconds % 3600) / 60);
|
||||
let s = seconds % 60;
|
||||
return [h, m, s].map(v => v.toString().padStart(2, '0')).join(':');
|
||||
}
|
||||
let csv = "time,seconds,mag_avg,mag_sum\n"; // Header
|
||||
let lines = c.trim().split("\n");
|
||||
for (let line of lines) {
|
||||
let parts = line.split(",").map(v => parseInt(v, 10));
|
||||
if (parts.length !== 3) continue;
|
||||
let seconds = parts[0];
|
||||
let avg = parts[1]/8192;
|
||||
let sum = parts[2]/1024; //scaled differently
|
||||
let time = secondsToClock(seconds);
|
||||
csv += `${time},${seconds},${avg},${sum}\n`;
|
||||
}
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
url = URL.createObjectURL(blob);
|
||||
}
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
return;
|
||||
});
|
||||
}
|
||||
function downloadAllFiles(){
|
||||
|
||||
}
|
||||
function deleteFile(filename){
|
||||
if (confirm(`Are you sure you want to delete ${filename}?`)) {
|
||||
Util.eraseStorageFile(filename,(c) =>{
|
||||
var filePrefix = settings.filePrefix || 'htst';
|
||||
return Puck.eval('require("Storage").list(/^'+filePrefix+'/)',renderDownloadTab);
|
||||
});
|
||||
}
|
||||
}
|
||||
function renderDownloadTabLogs(e){
|
||||
var element = document.getElementById("downloadData-tab-content");
|
||||
if(e.length > 0){
|
||||
|
||||
}else{
|
||||
e.innerHTML = "No files to download.";
|
||||
}
|
||||
}
|
||||
function renderDownloadTab(e){
|
||||
var element = document.getElementById("downloadData-tab-content");
|
||||
var tab_badge = document.getElementById("downloadData-tab-label");
|
||||
if(e.length > 0){
|
||||
tab_badge.classList.add("badge");
|
||||
tab_badge.dataset.badge = e.length;
|
||||
element.innerHTML = "";
|
||||
e.forEach((file)=>{
|
||||
var filename = file.replace(/\x01$/, '');
|
||||
element.insertAdjacentHTML('beforeend', `<tr id="file-${filename}"><td>${filename}</td><td><i class="icon icon-download" onclick="DownloadSingleFile('${filename}');"></i> <i class="icon icon-delete" onclick="deleteFile('${filename}');"></i></td></tr>`);
|
||||
});
|
||||
//element.insertAdjacentHTML('beforeend',`<tr id="end" class="text-bold"><td><button class="btn">Download All <i class="icon icon-download" onclick="DownloadAllFiles(JSON.parse("${e}"));"></i></button></td><td><button class="btn">Delete All <i class="icon icon-delete" onclick=""></i></button> </td></tr>`);
|
||||
}else{
|
||||
tab_badge.classList.remove("badge");
|
||||
tab_badge.dataset.badge = 0;
|
||||
element.innerHTML = '<tr class="active"><td>No Files</td></tr>';
|
||||
}
|
||||
}
|
||||
function onInit(device) {
|
||||
let settings = {};
|
||||
let promise = Promise.resolve();
|
||||
promise = promise.then(() => {
|
||||
return readStorageJSONAsync("heatsuite.default.json");
|
||||
});
|
||||
promise = promise.then(defaults => {
|
||||
return readStorageJSONAsync("heatsuite.settings.json").then(user => {
|
||||
settings = Object.assign({}, defaults, user);
|
||||
});
|
||||
});
|
||||
promise = promise.then(() => {
|
||||
const settingsForm = document.getElementById('heatsuiteSettings');
|
||||
fillFormFromJson(settingsForm, settings);
|
||||
});
|
||||
promise = promise.then(() =>{
|
||||
document.getElementById("downloadData-tab-content").innerHTML = '<div class="loading"></div>';;
|
||||
var filePrefix = settings.filePrefix || 'htst';
|
||||
return Puck.eval('require("Storage").list(/^'+filePrefix+'/)',renderDownloadTab);
|
||||
});
|
||||
//promise = promise.then(()=>{
|
||||
// return Puck.eval('require("Storage").list(heatsuite.log)',renderDownloadTabLogs);
|
||||
//});
|
||||
promise.catch(error => {
|
||||
console.error("Error loading settings:", error);
|
||||
});
|
||||
}
|
||||
|
||||
const editors = {};
|
||||
function initJsonEditor({ elementId, storageKey, defaultSchema, resetBtnId }) {
|
||||
const container = document.getElementById(elementId);
|
||||
const savedContent = localStorage.getItem(storageKey);
|
||||
const initialContent = savedContent ? JSON.parse(savedContent) : defaultSchema;
|
||||
const options = {
|
||||
modes: ['tree', 'form', 'code', 'text'],
|
||||
mode: 'code',
|
||||
onChange: function () {
|
||||
try {
|
||||
const currentContent = editor.get();
|
||||
localStorage.setItem(storageKey, JSON.stringify(currentContent));
|
||||
} catch (err) {
|
||||
console.error(`[${storageKey}] Invalid JSON not saved.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const editor = new JSONEditor(container, options);
|
||||
editor.set(initialContent);
|
||||
editors[storageKey] = { editor, defaultSchema };
|
||||
if (resetBtnId) {
|
||||
const resetBtn = document.getElementById(resetBtnId);
|
||||
resetBtn.addEventListener('click', () => {
|
||||
editor.set(defaultSchema);
|
||||
localStorage.setItem(storageKey, JSON.stringify(defaultSchema));
|
||||
});
|
||||
}
|
||||
}
|
||||
window.onload = function () {
|
||||
const studyIDInput = document.getElementById('input-studyID');
|
||||
studyIDInput.addEventListener("input", () => {
|
||||
studyIDInput.value = studyIDInput.value.replace(/[^a-zA-Z0-9]/g, '');
|
||||
});
|
||||
const filePrefixInput = document.getElementById('input-filePrefix');
|
||||
filePrefixInput.addEventListener("input", () => {
|
||||
filePrefixInput.value = filePrefixInput.value.replace(/[^a-zA-Z0-9]/g, '');
|
||||
});
|
||||
const settingsForm = document.getElementById('heatsuiteSettings');
|
||||
settingsForm.addEventListener('input', autosaveSettings);
|
||||
settingsForm.addEventListener('change', autosaveSettings);
|
||||
const storedSettings = localStorage.getItem("heatuite__settings");
|
||||
if (storedSettings) { //lets fill form:
|
||||
fillFormFromJson(settingsForm, JSON.parse(storedSettings));
|
||||
}
|
||||
//initialize both JsonEditors for tasks and surveys
|
||||
initJsonEditor({
|
||||
elementId: 'heatsuite_taskFile_editor',
|
||||
storageKey: 'heatsuite__taskFile',
|
||||
defaultSchema: heatsuite__taskFile_defaultSchema,
|
||||
resetBtnId: 'heatsuite_taskFile_resetBtn'
|
||||
});
|
||||
initJsonEditor({
|
||||
elementId: 'heatsuite_surveyFile_editor',
|
||||
storageKey: 'heatsuite__surveyFile',
|
||||
defaultSchema: heatsuite__surveyFile_defaultSchema,
|
||||
resetBtnId: 'heatsuite_surveyFile_resetBtn'
|
||||
});
|
||||
};
|
||||
|
||||
// When the 'upload' button is clicked...
|
||||
document.getElementById("upload").addEventListener("click", function () {
|
||||
const form = document.getElementById('heatsuiteSettings');
|
||||
//TO DO VALIDATE jsonEditors!
|
||||
sendCustomizedApp({
|
||||
storage: [
|
||||
{ 'name': "heatsuite.settings.json", content: JSON.stringify(formDataToJson(form)) },
|
||||
{ "name": "heatsuite.tasks.json", content: JSON.stringify(editors['heatsuite__taskFile'].editor.get()) },
|
||||
{ "name": "heatsuite.survey.json", content: JSON.stringify(editors['heatsuite__surveyFile'].editor.get()) },
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
{
|
||||
let studyTasksJSON = "heatsuite.tasks.json";
|
||||
let studyTasks = require('Storage').readJSON(studyTasksJSON, true) || {};
|
||||
|
||||
let Layout = require("Layout");
|
||||
let modHS = require("HSModule");
|
||||
let layout;
|
||||
let NRFFindDeviceTimeout, TaskScreenTimeout;
|
||||
|
||||
let settings = modHS.getSettings();
|
||||
|
||||
let appCache = modHS.getCache();
|
||||
|
||||
function queueNRFFindDeviceTimeout() {
|
||||
if (NRFFindDeviceTimeout) clearTimeout(NRFFindDeviceTimeout);
|
||||
NRFFindDeviceTimeout = setTimeout(function () {
|
||||
NRFFindDeviceTimeout = undefined;
|
||||
findBtDevices();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function findBtDevices() {
|
||||
NRF.setScan(); //clear any scans running!
|
||||
NRF.findDevices(function (devices) {
|
||||
let found = false;
|
||||
if (devices.length !== 0) {
|
||||
devices.every((d) => {
|
||||
modHS.log("Found device", d);
|
||||
let services = d.services;
|
||||
modHS.log("Services: ", services);
|
||||
if (services !== undefined && services.includes('1810') && d.id === settings.bt_bloodPressure_id) {
|
||||
//Blood Pressure
|
||||
found = true;
|
||||
layout.msg.label = "BP Found";
|
||||
layout.render();
|
||||
if (NRFFindDeviceTimeout) clearTimeout(NRFFindDeviceTimeout);
|
||||
return Bangle.load('heatsuite.bp.js');
|
||||
} else if (services !== undefined && services.includes('181b') && studyTasks.filter(task => task.id === "bodyMass")) {
|
||||
let data = d.serviceData[services];
|
||||
let ctlByte = data[1];
|
||||
let weightRemoved = ctlByte & (1 << 7);
|
||||
modHS.log(weightRemoved);
|
||||
if (weightRemoved === 0) {
|
||||
//Mass found
|
||||
found = true;
|
||||
layout.msg.label = "Scale Found";
|
||||
layout.render();
|
||||
if (NRFFindDeviceTimeout) clearTimeout(NRFFindDeviceTimeout);
|
||||
return Bangle.load('heatsuite.mass.js');
|
||||
}
|
||||
modHS.log("No weight on scale");
|
||||
} else if (services !== undefined && services.includes('1809') && d.id === settings.bt_coreTemperature_id) {
|
||||
//Core Temperature
|
||||
found = true;
|
||||
layout.msg.label = "Temp Found";
|
||||
layout.render();
|
||||
if (NRFFindDeviceTimeout) clearTimeout(NRFFindDeviceTimeout);
|
||||
return Bangle.load('heatsuite.bletemp.js');
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!found) {
|
||||
modHS.log("Search Complete, No Devices Found");
|
||||
queueNRFFindDeviceTimeout();
|
||||
} else {
|
||||
if (TaskScreenTimeout) clearTimeout(TaskScreenTimeout);
|
||||
if (NRFFindDeviceTimeout) clearTimeout(NRFFindDeviceTimeout);
|
||||
}
|
||||
}, { timeout: 3000, active: true});
|
||||
}
|
||||
|
||||
function taskButtonInterpretter(string) {
|
||||
//turn off FindDeviceHandler whenever we navigate off task screen
|
||||
let command = 'if (NRFFindDeviceTimeout){clearTimeout(NRFFindDeviceTimeout);}' + string;
|
||||
return eval(command);
|
||||
}
|
||||
|
||||
function queueTaskScreenTimeout() {
|
||||
if (TaskScreenTimeout) clearTimeout(TaskScreenTimeout);
|
||||
if (TaskScreenTimeout === undefined) {
|
||||
TaskScreenTimeout = setTimeout(function () {
|
||||
if (NRFFindDeviceTimeout) clearTimeout(NRFFindDeviceTimeout);
|
||||
Bangle.load();
|
||||
}, 180000);
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
g.clear();
|
||||
g.reset();
|
||||
if (studyTasks.length === 0) {
|
||||
if(require("Storage").list().includes("heatsuite.survey.json")){ //likely just using for EMA survey
|
||||
return Bangle.load('heatsuite.survey.js'); //go right to survey!
|
||||
}
|
||||
modHS.log('No Study Tasks loaded...');
|
||||
layout = new Layout({
|
||||
type: "v",
|
||||
c: [
|
||||
{
|
||||
type: "txt",
|
||||
font: "Vector:30",
|
||||
label: "No Study Tasks Loaded.",
|
||||
wrap: true,
|
||||
fillx: 1,
|
||||
filly: 1
|
||||
}
|
||||
]
|
||||
});
|
||||
layout.render();
|
||||
return;
|
||||
}
|
||||
let taskArr = appCache.taskQueue;
|
||||
let taskID = [];
|
||||
if (taskArr !== undefined) {
|
||||
taskID = taskArr.filter(function (taskArr) {
|
||||
return taskArr.id;
|
||||
}).map(function (taskArr) {
|
||||
return taskArr.id;
|
||||
});
|
||||
}
|
||||
let layoutOut = { type: "v", c: [] };
|
||||
let row = { type: "h", c: [] };
|
||||
let rowCount = 2;
|
||||
if( studyTasks.length > 4){
|
||||
rowCount = 3; //so we can include up to 9 tasks on the screen at once
|
||||
}
|
||||
studyTasks.forEach(task => {
|
||||
let btn = { type: "btn", fillx: 1, filly: 1 };
|
||||
btn.id = task.id;
|
||||
btn.src = eval(task.icon);
|
||||
//callback on button press
|
||||
if (task.cbBtn) {
|
||||
btn.cb = l => taskButtonInterpretter(task.cbBtn);
|
||||
}
|
||||
//back color determination
|
||||
btn.btnFaceCol = "#90EE90";
|
||||
//a to do!!
|
||||
if (taskID.includes(task.id)) {
|
||||
btn.btnFaceCol = "#FFFF00";
|
||||
}
|
||||
//no bt paired
|
||||
if (task.btPair === true) {
|
||||
if (settings["bt_" + task.id + "_id"] === undefined || !settings["bt_" + task.id + "_id"]) {
|
||||
//make it clickable so we can go to settings and pair something
|
||||
btn.btnFaceCol = "#FF0000";
|
||||
btn.cb = l => taskButtonInterpretter("Bangle.load('heatsuite.settings.js');");
|
||||
}
|
||||
}
|
||||
//builder for each icon in taskScreen
|
||||
if (row.c.length >= rowCount) {
|
||||
layoutOut.c.push(row);
|
||||
row = { type: "h", c: [] };
|
||||
}
|
||||
row.c.push(btn);
|
||||
});
|
||||
//push that last row in if needed
|
||||
if (row.c.length > 0) {
|
||||
layoutOut.c.push(row);
|
||||
}
|
||||
//Final
|
||||
layoutOut.c.push({ type: "txt", font: "6x8:2", label: "Searching...", id: "msg", fillx: 1 });
|
||||
let options = {
|
||||
lazy: true,
|
||||
btns:[{label:"Exit", cb: l=>Bangle.showClock() }],
|
||||
remove: () => {
|
||||
NRF.setScan(); //clear scan
|
||||
if (TaskScreenTimeout) clearTimeout(TaskScreenTimeout);
|
||||
if (NRFFindDeviceTimeout) clearTimeout(NRFFindDeviceTimeout);
|
||||
NRFFindDeviceTimeout = undefined;
|
||||
TaskScreenTimeout = undefined;
|
||||
require("widget_utils").show();
|
||||
}
|
||||
};
|
||||
layout = new Layout(layoutOut, options);
|
||||
layout.render();
|
||||
queueNRFFindDeviceTimeout();
|
||||
queueTaskScreenTimeout();
|
||||
}
|
||||
|
||||
Bangle.setLocked(false); //unlock screen!
|
||||
Bangle.loadWidgets();
|
||||
require("widget_utils").hide();
|
||||
draw();
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
var Layout = require("Layout");
|
||||
const modHS = require('HSModule');
|
||||
var layout;
|
||||
|
||||
var settings = modHS.getSettings();
|
||||
//var appCache = modHS.getCache();
|
||||
function log(msg) {
|
||||
if (!settings.DEBUG) {
|
||||
return;
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
//Schema for the message coming from the BLE ThermistorPod:
|
||||
const Schema_ThermistorPodBLE = {
|
||||
msgType: 'int32',
|
||||
ta: 'float32',
|
||||
rh: 'float32',
|
||||
batP: 'int32',
|
||||
temp: 'float32',
|
||||
tempAvg: 'float32',
|
||||
adc: 'int32',
|
||||
resistance: 'float32',
|
||||
ambLight: 'int32'
|
||||
};
|
||||
|
||||
function getTcore(id) {
|
||||
layout = new Layout({
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: "Oral Temp", fillx: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: "Waiting...", fillx: 1 },
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
g.clear();
|
||||
layout.render();
|
||||
var gatt;
|
||||
var startTime;
|
||||
var complete = false;
|
||||
var TCoreData = {
|
||||
"temp": null,
|
||||
"ta": null,
|
||||
"rh": null,
|
||||
"measures": []
|
||||
};
|
||||
NRF.connect(id).then(function (g) {
|
||||
gatt = g;
|
||||
startTime = parseInt((getTime()).toFixed(0));
|
||||
gatt.device.on('gattserverdisconnected', function (reason) {
|
||||
gatt = null;
|
||||
Bangle.load();
|
||||
log("Disconnected ", reason);
|
||||
});
|
||||
return gatt.getPrimaryService("1809");
|
||||
}).then(function (s) {
|
||||
return s.getCharacteristic("00002A1F-0000-1000-8000-00805F9B34FB");
|
||||
}).then(function (c) {
|
||||
c.on('characteristicvaluechanged', function (event) {
|
||||
const receivedData = modHS.parseBLEData(event.target.value, Schema_ThermistorPodBLE);
|
||||
TCoreData.temp = receivedData.tempAvg;
|
||||
TCoreData.ta = receivedData.ta;
|
||||
TCoreData.rh = receivedData.rh;
|
||||
TCoreData.measures.push(receivedData.adc);
|
||||
var timeNow = parseInt((getTime()).toFixed(0));
|
||||
var diff = timeNow - startTime;
|
||||
var display;
|
||||
if (diff > 90 && !complete) { // time to save the data and disconnect
|
||||
complete = true;
|
||||
if (modHS.saveDataToFile('coreTemp', 'coreTemperature', TCoreData)) {
|
||||
display = {
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: "Saved!", fillx: 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
}
|
||||
} else {
|
||||
var remaining = 90 - diff;
|
||||
display = {
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: remaining + " secs", fillx: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "4x6:2", label: receivedData.adc + " " + receivedData.temp.toFixed(2) + "C", fillx: 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
layout = new Layout(display);
|
||||
g.clear();
|
||||
layout.render();
|
||||
if (complete) {
|
||||
if(gatt){
|
||||
gatt.disconnect();
|
||||
}
|
||||
setTimeout(() => { Bangle.load() }, 2000);
|
||||
}
|
||||
});
|
||||
return c.startNotifications();
|
||||
}).then(function (d) {
|
||||
}).catch(function (e) {
|
||||
E.showAlert("error! " + e).then(function () { Bangle.load(); });
|
||||
});
|
||||
}
|
||||
|
||||
let macID = settings.bt_coreTemperature_id.split(" ");
|
||||
//so you can see timeout
|
||||
Bangle.setOptions({backlightTimeout: 0}) // turn off the timeout
|
||||
Bangle.setBacklight(1); // keep screen on
|
||||
getTcore(macID[0]);
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatsuite").enable();
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
var Layout = require("Layout");
|
||||
const modHS = require('HSModule');
|
||||
var layout;
|
||||
var settings = modHS.getSettings();
|
||||
//var appCache = modHS.getCache();
|
||||
function log(msg) {
|
||||
if (!settings.DEBUG) {
|
||||
return;
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
//Schema for the message coming from the A&D Medical UA651BLE:
|
||||
function analyzeBPData(data) {
|
||||
const flags = data.getUint8(0, 1);
|
||||
const buf = data.buffer;
|
||||
let result = { //Schema for BP measures
|
||||
"sbp" : null,
|
||||
"dbp" : null,
|
||||
"map" : null,
|
||||
"hr" : null,
|
||||
"moved" : null,
|
||||
"cuffLoose" : null,
|
||||
"irregularPulse" : null,
|
||||
"improperMeasure" : null,
|
||||
"year" : null,
|
||||
"month" : null,
|
||||
"day" : null,
|
||||
"hour" : null,
|
||||
"minute" : null,
|
||||
"second" : null
|
||||
};
|
||||
let index = 1;
|
||||
result.sbp = buf[index];
|
||||
index += 2;
|
||||
result.dbp = buf[index];
|
||||
index += 2;
|
||||
result.map = buf[index];
|
||||
index += 2;
|
||||
if (flags & 0x02) {
|
||||
result.year = buf[index] + (buf[index + 1] << 8),
|
||||
result.month = buf[index + 2],
|
||||
result.day = buf[index + 3],
|
||||
result.hour = buf[index + 4],
|
||||
result.minute = buf[index + 5],
|
||||
result.second = buf[index + 6],
|
||||
index += 7;
|
||||
}
|
||||
if (flags & 0x04) {
|
||||
result.hr = buf[index];
|
||||
index += 2;
|
||||
}
|
||||
if (flags & 0x08) {
|
||||
index += 1;
|
||||
}
|
||||
if (flags & 0x10) {
|
||||
const ms = buf[index];
|
||||
result.moved = (ms & 0b1) ? 1 : 0;
|
||||
result.cuffLoose = (ms & 0b10) ? 1 : 0;
|
||||
result.irregularPulse = (ms & 0b100) ? 1 : 0;
|
||||
result.improperMeasure = (ms & 0b100000) ? 1 : 0;
|
||||
index += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getBP(id) {
|
||||
layout = new Layout({
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "6x8:2", label: "Blood Pressure", fillx: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "6x8:2", label: "Waiting...", fillx: 1 },
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
g.clear();
|
||||
layout.render();
|
||||
var device;
|
||||
var service;
|
||||
log("connecting to ", id);
|
||||
NRF.connect(id).then(function (d) {
|
||||
device = d;
|
||||
return new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}).then(function () {
|
||||
log("connected");
|
||||
if (device.getSecurityStatus().bonded) {
|
||||
log("Already bonded");
|
||||
return true;
|
||||
} else {
|
||||
log("Start bonding");
|
||||
return device.startBonding();
|
||||
}
|
||||
}).then(function () {
|
||||
device.device.on('gattserverdisconnected', function (reason) {
|
||||
Bangle.load();
|
||||
log("Disconnected ", reason);
|
||||
});
|
||||
return device.getPrimaryService("1810");
|
||||
}).then(function (s) {
|
||||
service = s;
|
||||
return service.getCharacteristic("2A08");
|
||||
}).then(function (characteristic) {
|
||||
//set time on device during pairing
|
||||
var date = new Date();
|
||||
var b = new ArrayBuffer(7);
|
||||
var v = new DataView(b);
|
||||
v.setUint16(0, date.getFullYear(), true);
|
||||
v.setUint8(2, date.getMonth() + 1);
|
||||
v.setUint8(3, date.getDate());
|
||||
v.setUint8(4, date.getHours());
|
||||
v.setUint8(5, date.getMinutes());
|
||||
v.setUint8(5, date.getSeconds());
|
||||
var arr = [];
|
||||
for (let i = 0; i < v.buffer.length; i++) {
|
||||
arr[i] = v.buffer[i];
|
||||
}
|
||||
return characteristic.writeValue(arr);
|
||||
}).then(function () {
|
||||
return service.getCharacteristic("2A35");
|
||||
}).then(function (c) {
|
||||
c.on('characteristicvaluechanged', function (event) {
|
||||
//log("-> "); // this is a DataView
|
||||
//log(event.target.value);
|
||||
const receivedData = analyzeBPData(event.target.value);
|
||||
modHS.saveDataToFile('bpres', 'bloodPressure', receivedData);
|
||||
layout = new Layout({
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: receivedData.sbp, fillx: 1 },
|
||||
{ type: "txt", font: "12x20:2", label: "/", fillx: 1 },
|
||||
{ type: "txt", font: "12x20:2", label: receivedData.dbp, fillx: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: receivedData.hr, fillx: 1 },
|
||||
{ type: "txt", font: "12x20:2", label: "BPM", fillx: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: "Saved!", fillx: 1 }
|
||||
]
|
||||
},
|
||||
]
|
||||
});
|
||||
g.clear();
|
||||
layout.render();
|
||||
});
|
||||
return c.startNotifications();
|
||||
}).then(function (d) {
|
||||
log("Setting Notification Interval");
|
||||
log("Waiting for notifications");
|
||||
}).catch(function (e) {
|
||||
log("GATT ", device);
|
||||
layout = new Layout({
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "6x8:2", label: "ERROR!", fillx: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "6x8:1", label: e, fillx: 1 },
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
g.clear();
|
||||
layout.render();
|
||||
if (!device.connected) {
|
||||
getBP(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
var macID = settings.bt_bloodPressure_id.split(" ");
|
||||
setTimeout(() => { getBP(macID[0]) }, 2000);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"DEBUG" : false,
|
||||
"SAVE_DEBUG": false,
|
||||
"notifications" : true,
|
||||
"record" : ["bat","steps","hrm","baro","acc"],
|
||||
"filePrefix": "htst",
|
||||
"GPS" : true,
|
||||
"GPSAdaptiveTime" : 2,
|
||||
"GPSInterval" : 30,
|
||||
"GPSScanTime" : 5
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
var Layout = require("Layout");
|
||||
var modHS = require("HSModule");
|
||||
var layout;
|
||||
|
||||
/** --------- MI SCALE --------------------------- */
|
||||
function getMass(service) {
|
||||
var datareceived = [];
|
||||
layout = new Layout({
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: "Body Mass", fillx: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: "Waiting...", fillx: 1 },
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
g.clear();
|
||||
layout.render();
|
||||
|
||||
NRF.setScan();//clear other scans
|
||||
|
||||
NRF.setScan(function (devices) {
|
||||
var data = devices.serviceData[service];
|
||||
datareceived.push(data);
|
||||
var ctlByte = data[1];
|
||||
var stabilized = ctlByte & (1 << 5);
|
||||
var weight = ((data[12] << 8) + data[11]) / 200;
|
||||
var impedance = (data[10] << 8) + data[9];
|
||||
if (stabilized && datareceived.length > 1 && impedance > 0 && impedance < 65534) {
|
||||
NRF.setScan();
|
||||
datareceived = [];
|
||||
var dataOut ={
|
||||
'mass' : weight,
|
||||
'impedance' : impedance
|
||||
};
|
||||
modHS.saveDataToFile('mass', 'bodyMass', dataOut);
|
||||
layout = new Layout({
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: weight, fillx: 1 },
|
||||
{ type: "txt", font: "12x20:2", label: "kg", fillx: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "6x8:2", label: impedance, fillx: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: "Saved!", fillx: 1 }
|
||||
]
|
||||
},
|
||||
]
|
||||
});
|
||||
g.clear();
|
||||
layout.render();
|
||||
setTimeout(function () { Bangle.load(); }, 3000);
|
||||
}
|
||||
}, { timeout: 2000, filters: [{ services: [service] }] });
|
||||
}
|
||||
|
||||
//init
|
||||
getMass('181b');
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
function _getSettings() {
|
||||
var out = Object.assign(
|
||||
require('Storage').readJSON("heatsuite.default.json", true) || {},
|
||||
require('Storage').readJSON("heatsuite.settings.json", true) || {}
|
||||
);
|
||||
out.StudyTasks = require('Storage').readJSON("heatsuite.tasks.json", true) || {};
|
||||
return out;
|
||||
}
|
||||
function _checkFileHeaders(filename,header){
|
||||
var storageFile = require("Storage").open(filename, "r");
|
||||
var headers = storageFile.readLine().trim();
|
||||
var headerString = header.join(",");
|
||||
if(headers === headerString){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function _renameOldFile(file){
|
||||
var rename = false;
|
||||
var i = 1;
|
||||
while(!rename){
|
||||
var filename = file+"_"+String(i);
|
||||
if(require('Storage').list(filename).length == 0){
|
||||
var newFile = require("Storage").open(filename, "w");
|
||||
var oldFile = require("Storage").open(file, "r");
|
||||
var l = oldFile.readLine();
|
||||
while (l!==undefined) {
|
||||
newFile.write(l);
|
||||
l = oldFile.readLine();
|
||||
}
|
||||
oldFile.erase(); //erase old file
|
||||
require("Storage").compact(); //compact memory
|
||||
rename = true;
|
||||
}else{
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
function _getRecordFile(type, headers) {
|
||||
var settings = _getSettings();
|
||||
var dt = new Date();
|
||||
var hour = dt.getHours();
|
||||
if (hour < 10) hour = '0' + hour;
|
||||
var month = dt.getMonth() + 1;
|
||||
if (month < 10) month = '0' + month;
|
||||
var day = dt.getDate();
|
||||
if (day < 10) day = '0' + day;
|
||||
var date = dt.getFullYear() + "" + month + "" + day;
|
||||
var fileName = settings.filePrefix + "_" + type + "_";
|
||||
fileName = fileName + date;
|
||||
if (require('Storage').list(fileName).length > 0 && type !== "accel") {
|
||||
if(_checkFileHeaders(fileName,headers)){
|
||||
return require('Storage').open(fileName, 'a');
|
||||
}else{ // need to rename the old file as headers have changed
|
||||
_renameOldFile(fileName);
|
||||
}
|
||||
}
|
||||
if (type !== "accel") {
|
||||
var storageFile = require("Storage").open(fileName, "w");
|
||||
storageFile.write(headers.join(",") + "\n");
|
||||
}
|
||||
return require("Storage").open(fileName, "a");
|
||||
}
|
||||
function _checkStorageFree(type) {
|
||||
var settings = _getSettings();
|
||||
var freeSpace = require("Storage").getFree();
|
||||
var filePrefix = settings.filePrefix + type;
|
||||
var csvList = require("Storage").list(filePrefix);
|
||||
if (freeSpace < 500000) {
|
||||
if(csvList.length > 0){
|
||||
require("Storage").open(csvList[0],"r").erase();
|
||||
}
|
||||
require("Storage").compact();
|
||||
}
|
||||
}
|
||||
function _saveDataToFile(type, task, arr) {
|
||||
var newArr = {
|
||||
'unix' : parseInt((getTime()).toFixed(0)),
|
||||
'tz' : (new Date()).getTimezoneOffset() * -60
|
||||
}
|
||||
for (var key in arr) {
|
||||
newArr[key] = arr[key];
|
||||
}
|
||||
var data = [];
|
||||
var headers = [];
|
||||
for (var key in newArr) {
|
||||
if(Array.isArray(newArr[key])){
|
||||
newArr[key] = newArr[key].join(';');
|
||||
}
|
||||
data.push(newArr[key]);
|
||||
headers.push(key);
|
||||
}
|
||||
var currFile = _getRecordFile(type, headers);
|
||||
if (currFile) {
|
||||
var String = data.join(',') + '\n';
|
||||
currFile.write(String);
|
||||
_updateTaskQueue(task, newArr);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
function _updateTaskQueue(task, arr) {
|
||||
var appCache = _getCache();
|
||||
var taskQueue = appCache.taskQueue;
|
||||
var tasktime = parseInt((getTime()).toFixed(0));
|
||||
if (taskQueue !== undefined) {
|
||||
var newTaskQueue = taskQueue.filter(function (taskQueue) {
|
||||
return taskQueue.id !== task;
|
||||
});
|
||||
appCache.taskQueue = newTaskQueue;
|
||||
}
|
||||
if (appCache[task] === undefined) appCache[task] = {};
|
||||
if (task === 'survey') { //we will refactor the value to be an object with keys
|
||||
var key = arr.key;
|
||||
if(appCache.survey[key] === undefined) appCache.survey[key] = {};
|
||||
appCache.survey[key] = {
|
||||
unix: tasktime,
|
||||
resp: arr.value
|
||||
};
|
||||
}else{
|
||||
appCache[task] = arr;
|
||||
}
|
||||
appCache[task].unix = tasktime;
|
||||
//lets always store cache so we can restore values if needed
|
||||
_writeCache(appCache);
|
||||
}
|
||||
function _getCache() {
|
||||
return require('Storage').readJSON("heatsuite.cache.json", true) || {};
|
||||
}
|
||||
function _writeCache(cache) {
|
||||
var oldCache = _getCache();
|
||||
if (oldCache !== cache) require('Storage').writeJSON("heatsuite.cache.json", cache);
|
||||
return _getCache();
|
||||
}
|
||||
function _clearCache() {
|
||||
require('Storage').writeJSON("heatsuite.cache.json", {});
|
||||
return _getCache();
|
||||
}
|
||||
function _parseBLEData(buffer, dataSchema) {
|
||||
let offset = 0;
|
||||
let result = {};
|
||||
for (let field in dataSchema) {
|
||||
const dataType = dataSchema[field];
|
||||
let value;
|
||||
switch (dataType) {
|
||||
case 'uint8':
|
||||
value = buffer.getUint8(offset,true);
|
||||
offset += 1; // 1 byte for uint8
|
||||
break;
|
||||
case 'uint16':
|
||||
value = buffer.getUint16(offset,true); // Assuming little-endian format
|
||||
offset += 4; // 2 bytes for uint16
|
||||
break;
|
||||
case 'int32':
|
||||
value = buffer.getInt32(offset,true); // Assuming little-endian format
|
||||
offset += 4; // 4 bytes for int32
|
||||
break;
|
||||
case 'float32':
|
||||
value = buffer.getFloat32(offset,true); // Assuming little-endian format
|
||||
offset += 4; // 4 bytes for float32
|
||||
break;
|
||||
case 'float64':
|
||||
value = buffer.getFloat64(offset,true); // Assuming little-endian format
|
||||
offset += 8; // 8 bytes for float64
|
||||
break;
|
||||
case 'array':
|
||||
value = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
value.push(buffer.getUint8(offset,true));
|
||||
offset += 1; // 1 byte for each uint8
|
||||
}
|
||||
break;
|
||||
case 'float16':{
|
||||
const b0 = buffer.getUint8(offset, true);
|
||||
const b1 = buffer.getUint8(offset + 1, true);
|
||||
const mantissa = (b1 << 8) | b0;
|
||||
const sign = mantissa & 0x8000 ? -1 : 1;
|
||||
const exponent = (mantissa >> 11) & 0x0F;
|
||||
const fraction = mantissa & 0x7FF;
|
||||
value = sign * (1 + fraction / 2048) * Math.pow(2, exponent - 15);
|
||||
offset += 2;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown data type: ${dataType}`);
|
||||
}
|
||||
result[field] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function _log(msg) {
|
||||
var settings = _getSettings();
|
||||
if(settings.SAVE_DEBUG){
|
||||
var file = require('Storage').open('heatsuite.log', 'a');
|
||||
var string = String(parseInt((new Date().getTime() / 1000).toFixed(0)))+": "+msg+"\n";
|
||||
file.write(string);
|
||||
return;
|
||||
}
|
||||
else if (!settings.DEBUG) {
|
||||
return;
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
exports = {
|
||||
getSettings: _getSettings,
|
||||
getRecordFile: _getRecordFile,
|
||||
saveDataToFile: _saveDataToFile,
|
||||
checkStorageFree : _checkStorageFree,
|
||||
getCache: _getCache,
|
||||
writeCache: _writeCache,
|
||||
clearCache: _clearCache,
|
||||
updateTaskQueue: _updateTaskQueue,
|
||||
parseBLEData: _parseBLEData,
|
||||
log: _log,
|
||||
};
|
||||
|
|
@ -0,0 +1,350 @@
|
|||
(function (back) {
|
||||
|
||||
var settingsJSON = "heatsuite.settings.json";
|
||||
var studyTasksJSON = "heatsuite.tasks.json";
|
||||
|
||||
function log(msg) {
|
||||
if (!settings.DEBUG) {
|
||||
return;
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function writeSettings(key, value) {
|
||||
var s = require('Storage').readJSON(settingsJSON, true) || {};
|
||||
s[key] = value;
|
||||
require('Storage').writeJSON(settingsJSON, s);
|
||||
settings = readSettings();
|
||||
}
|
||||
|
||||
function readSettings() {
|
||||
var out = Object.assign(
|
||||
require('Storage').readJSON("heatsuite.default.json", true) || {},
|
||||
require('Storage').readJSON(settingsJSON, true) || {}
|
||||
);
|
||||
out.StudyTasks = require('Storage').readJSON(studyTasksJSON, true) || {};
|
||||
return out;
|
||||
}
|
||||
var settings = readSettings();
|
||||
|
||||
/*---- PAIRING FUNCTIONS FOR DEVICES ----*/
|
||||
function BPPair(id) {
|
||||
var device;
|
||||
E.showMessage(`Pairing /n ${id}`, "Bluetooth");
|
||||
NRF.connect(id).then(function (d) {
|
||||
device = d;
|
||||
return new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}).then(function () {
|
||||
log("connected");
|
||||
if (device.getSecurityStatus().bonded) {
|
||||
log("Already bonded");
|
||||
return true;
|
||||
} else {
|
||||
log("Start bonding");
|
||||
return device.startBonding();
|
||||
}
|
||||
}).then(function () {
|
||||
device.device.on('gattserverdisconnected', function (reason) {
|
||||
log("Disconnected ", reason);
|
||||
});
|
||||
return device.getPrimaryService("1810");
|
||||
}).then(function (service) {
|
||||
log(service);
|
||||
return service.getCharacteristic("2A08");
|
||||
}).then(function (characteristic) {
|
||||
//set time on device during pairing
|
||||
var date = new Date();
|
||||
var b = new ArrayBuffer(7);
|
||||
var v = new DataView(b);
|
||||
v.setUint16(0, date.getFullYear(), true);
|
||||
v.setUint8(2, date.getMonth() + 1);
|
||||
v.setUint8(3, date.getDate());
|
||||
v.setUint8(4, date.getHours());
|
||||
v.setUint8(5, date.getMinutes());
|
||||
v.setUint8(5, date.getSeconds());
|
||||
var arr = [];
|
||||
for (let i = 0; i < v.buffer.length; i++) {
|
||||
arr[i] = v.buffer[i];
|
||||
}
|
||||
return characteristic.writeValue(arr);
|
||||
}).then(function () {
|
||||
writeSettings("bt_bloodPressure_id", id);
|
||||
// Store the name for displaying later. Will connect by ID
|
||||
if (device.name) {
|
||||
writeSettings("bt_bloodPressure_name", device.name);
|
||||
}
|
||||
E.showAlert("Paired!").then(function () { E.showMenu(deviceSettings()) });
|
||||
log("Device ID paired, time set, Done!");
|
||||
return device.disconnect();
|
||||
}).catch(function (e) {
|
||||
log(e);
|
||||
E.showAlert("error! " + e).then(function () { E.showMenu(deviceSettings()) });
|
||||
});
|
||||
}
|
||||
function PairTcore(id) {
|
||||
E.showMessage(`Pairing /n ${id}`, "Bluetooth");
|
||||
var gatt;
|
||||
NRF.connect(id).then(function (g) {
|
||||
gatt = g;
|
||||
console.log("connected!!!");
|
||||
// return gatt.startBonding();
|
||||
//}).then(function() {
|
||||
console.log("bonded", gatt.getSecurityStatus());
|
||||
writeSettings("bt_coreTemperature_id", id);
|
||||
E.showAlert("Paired!").then(function () { E.showMenu(deviceSettings()) });
|
||||
log("Device ID paired, Done!");
|
||||
gatt.disconnect();
|
||||
}).catch(function (e) {
|
||||
log("ERROR: " + e);
|
||||
E.showAlert("error! " + e).then(function () { E.showMenu(deviceSettings()) });
|
||||
});
|
||||
}
|
||||
|
||||
function deviceSettings() {
|
||||
var menu = { '< Back': function () { E.showMenu(mainMenuSettings()); } };
|
||||
menu[''] = { 'title': 'Devices' };
|
||||
Object.keys(settings.StudyTasks).forEach(key => {
|
||||
var details = settings.StudyTasks[key];
|
||||
if (details.btPair === undefined || !details.btPair) return;
|
||||
let id = "bt_" + key + "_id";
|
||||
if (settings[id] !== undefined) {
|
||||
menu["Clear " + key] = function () {
|
||||
E.showPrompt("Clear " + key + " device?").then((r) => {
|
||||
if (r) {
|
||||
writeSettings("bt_" + key + "_id", undefined);
|
||||
writeSettings("bt_" + key + "_name", undefined);
|
||||
}
|
||||
E.showMenu(mainMenuSettings());
|
||||
});
|
||||
};
|
||||
} else {
|
||||
menu["Pair " + key] = () => createMenuFromScan(key, settings.StudyTasks[key].btInfo.service);
|
||||
}
|
||||
});
|
||||
return menu;
|
||||
}
|
||||
|
||||
function recordMenu(){
|
||||
var updateRecorder = function(name,v){
|
||||
var r = settings.record;
|
||||
r = r.filter(item => item !== name);
|
||||
if(v){
|
||||
r.push(name);
|
||||
}
|
||||
writeSettings("record",r);
|
||||
}
|
||||
var menu = { '< Back': function () { E.showMenu(mainMenuSettings()); } };
|
||||
menu[''] = { 'title': 'Recorder' };
|
||||
var recorderOptions = {
|
||||
'hrm' : 'Optical HR',
|
||||
'steps' : "Steps",
|
||||
'bat' : 'Battery',
|
||||
'movement': 'Movement',
|
||||
'acc':'Accelerometry',
|
||||
'baro':'Temp/Pressure',
|
||||
'bthrm': 'BT HRM',
|
||||
'CORESensor':'CORE Sensor'
|
||||
}
|
||||
for (let key in recorderOptions) {
|
||||
let name = recorderOptions['key'];
|
||||
menu[name] = {
|
||||
value: settings.record.includes(key),
|
||||
onchange: v => {updateRecorder(key,v);}
|
||||
};
|
||||
}
|
||||
menu['High Acc'] = {
|
||||
value: settings.highAcc || false,
|
||||
onchange: v => {
|
||||
settings.highAcc = v;
|
||||
writeSettings("highAcc", v);
|
||||
}
|
||||
};
|
||||
return menu;
|
||||
}
|
||||
|
||||
function mainMenuSettings() {
|
||||
var menu = {
|
||||
'': { 'title': 'Main' },
|
||||
'< Back': back
|
||||
};
|
||||
|
||||
menu['Recorders'] = function () {E.showMenu(recordMenu()) };
|
||||
menu['Devices'] = function () { E.showMenu(deviceSettings()) };
|
||||
menu['GPS'] = function () { E.showMenu(gpsSettings()) };
|
||||
menu['Language'] = function () { E.showMenu(languageMenu()) };
|
||||
menu['Survey Random'] = {
|
||||
value: settings.surveyRandomize || false,
|
||||
onchange: v => {
|
||||
settings.GPS = v;
|
||||
writeSettings("surveyRandomize", v);
|
||||
}
|
||||
};
|
||||
menu['HRM Interval'] = {
|
||||
value: settings.HRMInterval || 0,
|
||||
min: 0, max: 60,
|
||||
onchange: v => {
|
||||
settings.HRMInterval = v;
|
||||
writeSettings("HRMInterval", v);
|
||||
}
|
||||
};
|
||||
menu['Restart BLE'] = function () {
|
||||
E.showPrompt("Restart Bluetooth?").then((r) => {
|
||||
if (r) {
|
||||
NRF.disconnect()
|
||||
NRF.restart();
|
||||
}
|
||||
E.showMenu(mainMenuSettings());
|
||||
});
|
||||
};
|
||||
menu['Clear Cache'] = function () {
|
||||
E.showPrompt("Clear Cache?").then((r) => {
|
||||
if (r) {
|
||||
require('Storage').writeJSON("heatsuite.cache.json", {});
|
||||
}
|
||||
E.showMenu(mainMenuSettings());
|
||||
});
|
||||
}
|
||||
menu['Clear Study ID'] = function () {
|
||||
E.showPrompt("Clear study ID (includes ignored)?").then((r) => {
|
||||
if (r) {
|
||||
writeSettings("studyID", undefined);
|
||||
writeSettings("studyIDIgnore", []);
|
||||
}
|
||||
E.showMenu(mainMenuSettings());
|
||||
});
|
||||
}
|
||||
menu['Notifications'] = {
|
||||
value: settings.notifications || false,
|
||||
onchange: v => {
|
||||
settings.notifications = v;
|
||||
writeSettings("notifications", v);
|
||||
}
|
||||
};
|
||||
menu['Debug'] = function () { E.showMenu(debugMenu()) };
|
||||
return menu;
|
||||
}
|
||||
function debugMenu(){
|
||||
var menu = {
|
||||
'': { 'title': 'Debug' },
|
||||
'< Back': function () { E.showMenu(mainMenuSettings()); }
|
||||
};
|
||||
menu['Console'] = {
|
||||
value: settings.DEBUG || false,
|
||||
onchange: v => {
|
||||
settings.DEBUG = v;
|
||||
writeSettings("DEBUG", v);
|
||||
}
|
||||
};
|
||||
menu['Log (file)'] = {
|
||||
value: settings.SAVE_DEBUG || false,
|
||||
onchange: v => {
|
||||
settings.SAVE_DEBUG = v;
|
||||
writeSettings("SAVE_DEBUG", v);
|
||||
}
|
||||
};
|
||||
return menu;
|
||||
}
|
||||
function gpsSettings() {
|
||||
var menu = {
|
||||
'': { 'title': 'GPS' },
|
||||
'< Back': function () { E.showMenu(mainMenuSettings()); }
|
||||
};
|
||||
menu['GPS'] = {
|
||||
value: settings.GPS || false,
|
||||
onchange: v => {
|
||||
settings.GPS = v;
|
||||
writeSettings("GPS", v);
|
||||
}
|
||||
};
|
||||
menu['Scan Time (min)'] = {
|
||||
value: settings.GPSScanTime || 1,
|
||||
min: 0, max: 60,
|
||||
onchange: v => {
|
||||
settings.GPSScanTime = v;
|
||||
writeSettings("GPSScanTime", v);
|
||||
}
|
||||
};
|
||||
menu['Interval (min)'] = {
|
||||
value: settings.GPSInterval || 10,
|
||||
min: 0, max: 180,
|
||||
onchange: v => {
|
||||
settings.GPSinterval = v;
|
||||
writeSettings("GPSInterval", v);
|
||||
}
|
||||
};
|
||||
menu['Adaptive (min)'] = {
|
||||
value: settings.GPSAdaptiveTime || 2,
|
||||
min: 0, max: 60,
|
||||
onchange: v => {
|
||||
settings.GPSAdaptiveTime = v;
|
||||
writeSettings("GPSAdaptiveTime", v);
|
||||
}
|
||||
};
|
||||
return menu;
|
||||
}
|
||||
|
||||
function languageMenu() {
|
||||
var menu = { '< Back': function () { E.showMenu(mainMenuSettings()); } };
|
||||
menu[''] = { 'title': 'Language' };
|
||||
var surveySettings = require('Storage').readJSON("heatsuite.survey.json", true) || {};
|
||||
|
||||
Object.keys(surveySettings.supported).forEach(key => {
|
||||
//var id = surveySettings.supported[key];
|
||||
menu[key] = function () {
|
||||
E.showPrompt("Set " + key + "?").then((r) => {
|
||||
if (r) {
|
||||
writeSettings('lang', key);
|
||||
}
|
||||
E.showMenu(mainMenuSettings());
|
||||
});
|
||||
};
|
||||
});
|
||||
return menu;
|
||||
}
|
||||
|
||||
function createMenuFromScan(type, service) {
|
||||
E.showMenu();
|
||||
E.showMessage("Scanning for 4 seconds");
|
||||
var submenu_scan = {
|
||||
'< Back': function () { E.showMenu(deviceSettings()); }
|
||||
};
|
||||
NRF.findDevices(function (devices) {
|
||||
submenu_scan[''] = { title: `Scan (${devices.length} found)` };
|
||||
if (devices.length === 0) {
|
||||
E.showAlert("No " + type + " devices found")
|
||||
.then(() => E.showMenu(deviceSettings()));
|
||||
return;
|
||||
} else {
|
||||
devices.forEach((d) => {
|
||||
print("Found device", d);
|
||||
var shown = (d.name || d.id.substr(0, 17));
|
||||
submenu_scan[shown] = function () {
|
||||
E.showPrompt("Set " + shown + "?").then((r) => {
|
||||
if (r) {
|
||||
switch (type) {
|
||||
case "bloodPressure":
|
||||
BPPair(d.id);
|
||||
break;
|
||||
case "coreTemperature":
|
||||
PairTcore(d.id);
|
||||
break;
|
||||
case "bthrm":
|
||||
|
||||
break;
|
||||
default:
|
||||
E.showMenu(deviceSettings());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
E.showMenu(deviceSettings());
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
E.showMenu(submenu_scan);
|
||||
}, { timeout: 4000, active: true, filters: [{ services: [service] }] });
|
||||
}
|
||||
E.showMenu(mainMenuSettings());
|
||||
})
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
var surveyFileJSON = "heatsuite.survey.json";
|
||||
var Layout = require("Layout");
|
||||
const modHS = require('HSModule');
|
||||
var layout;
|
||||
var TaskScreenTimeout;
|
||||
Bangle.setOptions({
|
||||
'backlightTimeout':30000,
|
||||
'lockTimeout':30000
|
||||
});
|
||||
|
||||
var settings = modHS.getSettings();
|
||||
//randomize question order
|
||||
function shuffle(array) {
|
||||
const result = [];
|
||||
var itemsLeft = array;
|
||||
while (itemsLeft.length) {
|
||||
var Item;
|
||||
if (itemsLeft[0].orderFix !== undefined && itemsLeft[0].orderFix == true) {
|
||||
Item = itemsLeft.splice(0, 1)[0];
|
||||
} else {
|
||||
var randomIndex = Math.floor(Math.random() * itemsLeft.length);
|
||||
Item = itemsLeft.splice(randomIndex, 1)[0];
|
||||
}
|
||||
result.push(Item); // ...and add it to the result
|
||||
}
|
||||
return result;
|
||||
}
|
||||
var surveyFile = require('Storage').readJSON(surveyFileJSON, true) || {"questions":[{"text":{"en_GB":"Thermal Comfort?"},"options":[{"text":{"en_GB":"Comfortable"},"value":0,"color":"#ffffff","btnColor":"#38ed35"},{"text":{"en_GB":"Uncomfortable"},"value":1,"color":"#ffffff","btnColor":"#ff0019"}],"tod":[[0,2359]],"key":"comfort"}],"supported":{"en_GB":"English (GB)"}};
|
||||
var QArr = surveyFile.questions;
|
||||
if (settings.surveyRandomize !== undefined && settings.surveyRandomize) {
|
||||
QArr = shuffle(QArr);
|
||||
}
|
||||
|
||||
|
||||
function log(msg) {
|
||||
if (!settings.DEBUG) {
|
||||
return;
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
var appCache = modHS.getCache();
|
||||
var lang = settings.lang || require("locale").name || "en_GB";
|
||||
|
||||
function queueTaskScreenTimeout() {
|
||||
if (TaskScreenTimeout) clearTimeout(TaskScreenTimeout);
|
||||
if (TaskScreenTimeout === undefined) {
|
||||
TaskScreenTimeout = setTimeout(function () {
|
||||
Bangle.load();
|
||||
}, 180000);
|
||||
}
|
||||
}
|
||||
|
||||
/** -----------==== SURVEYS ====---------------- */
|
||||
var scrollInterval;
|
||||
function drawScrollingText(text,height) {
|
||||
Bangle.appRect = { x: 0, y: height, w: g.getWidth(), h: g.getHeight() - height, x2: g.getWidth()-1, y2: g.getHeight()-1 };
|
||||
let stringWidth = g.stringWidth(text);
|
||||
let textX = (stringWidth > g.getWidth())? (stringWidth/2) : 0;
|
||||
g.setColor("#000");
|
||||
g.setBgColor("#FFF");
|
||||
g.setFont("Vector:20", 2);
|
||||
g.clearRect(0, 0, g.getWidth(), height);
|
||||
function QuestionText() {
|
||||
g.setColor("#000");
|
||||
g.setBgColor("#FFF");
|
||||
g.setFont("Vector:20", 2);
|
||||
g.clearRect(0, 0, g.getWidth(), height);
|
||||
g.drawString(text, textX, height/2);
|
||||
textX -= 5;
|
||||
if (textX < (-(stringWidth/2)+g.getWidth())) textX = (stringWidth/2);
|
||||
g.flip();
|
||||
}
|
||||
if(scrollInterval) clearTimeout(scrollInterval);
|
||||
if(stringWidth > g.getWidth()){
|
||||
scrollInterval = setInterval(QuestionText, 60); //will need to scroll as its too long
|
||||
}else{
|
||||
QuestionText();
|
||||
}
|
||||
}
|
||||
function drawResponseOpts(ind){
|
||||
//force scrolling of question at the top
|
||||
var question = QArr[ind];
|
||||
drawScrollingText(question.text[lang].replace(/\\n/g, " "),30);
|
||||
var height = 30;
|
||||
var options = question.options;
|
||||
if(options.length < 4){
|
||||
height = Math.floor(Bangle.appRect.h / options.length);
|
||||
}
|
||||
let drawItem = function (idx,r){
|
||||
var optionText = options[idx].value;
|
||||
if (options[idx].text !== undefined) {
|
||||
optionText = options[idx].text[lang];
|
||||
}
|
||||
g.setColor((options[idx].color)?options[idx].color:"#000");
|
||||
g.setBgColor((options[idx].btnColor)?options[idx].btnColor:"#CCC").clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1);
|
||||
g.setFontAlign(0, 0, 0);
|
||||
g.setFont("Vector:20").drawString(optionText,r.x+(g.getWidth()/2),r.y+(height/2));
|
||||
};
|
||||
let selectItem = function(id) {
|
||||
var resp = (options[id] && options[id].text && lang in options[id].text) ? options[id].text[lang] : options[id].value;
|
||||
const cbString = ind + "," + question.key + "," + resp + "," + options[id].value;
|
||||
return surveyResponse(cbString);
|
||||
};
|
||||
E.showScroller({
|
||||
h : height,
|
||||
c : options.length,
|
||||
draw : drawItem,
|
||||
select : selectItem
|
||||
});
|
||||
}
|
||||
function drawSurveyLayout(index) {
|
||||
if(scrollInterval) clearTimeout(scrollInterval);
|
||||
if (surveyFile === undefined) {
|
||||
log('No Survey File');
|
||||
E.showAlert("No Survey File Found.").then(function () {
|
||||
Bangle.showClock();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (index == QArr.length) {
|
||||
//at the end, so we can show a saved image and redirect to time screen
|
||||
g.clear();
|
||||
g.reset();
|
||||
g.setBgColor("#FFF");
|
||||
Bangle.buzz(150);
|
||||
layout = new Layout({
|
||||
type: "v",
|
||||
c: [{
|
||||
type: "img",
|
||||
pad: 4,
|
||||
src: require("heatshrink").decompress(atob("ikUwYFCgVJkgMDhMkyVJAwQFCAQNAgESAoQCBwEBBwlIgAFDpNkyAjDkm/5MEBwdf+gUEl/6AoVZkmX/oLClv6pf+DQn1/4+E3//0gFBkACBv/SBYI7D5JiDLJx9CBAR4CAoWQQ4Z9DgAA=="))
|
||||
},
|
||||
{
|
||||
type: "txt",
|
||||
font: "Vector:30",
|
||||
label: "Done!"
|
||||
}]
|
||||
});
|
||||
layout.render();
|
||||
setTimeout(function () {
|
||||
Bangle.load();
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
var question = QArr[index];
|
||||
var dateN = new Date();
|
||||
if (question.tod !== undefined && question.tod.length > 0) {
|
||||
//Now we need to see if we are in window of the day that we are eligible to ask the question
|
||||
var windowOfDay = false;
|
||||
var currMT = parseInt(dateN.getHours() + "" + dateN.getMinutes());
|
||||
for (let d = 0; d < question.tod.length; d++) {
|
||||
if (currMT > question.tod[d][0] && currMT < question.tod[d][1]) {
|
||||
windowOfDay = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!windowOfDay) {
|
||||
drawSurveyLayout(index + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (appCache.survey !== undefined){ //first time we are asking this question
|
||||
if(appCache.survey[question.key] !== undefined) {
|
||||
//lets just check if this survey question can be shown right now, otherwise we will skip it
|
||||
var lastS = new Date(appCache.survey[question.key].unix * 1000);
|
||||
if (question.oncePerDay !== undefined && question.oncePerDay) { // check if we can only show survey once a day and if we already have
|
||||
//if (dateN.getFullYear() + dateN.getMonth() + dateN.getDate() === lastS.getFullYear() + lastS.getMonth() + lastS.getDate()) {
|
||||
if(Math.floor(dateN.getTime() / 86400000) === Math.floor(lastS.getTime() / 86400000)){
|
||||
drawSurveyLayout(index + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Bangle.buzz(100);
|
||||
g.clear();
|
||||
g.reset();
|
||||
var out = {
|
||||
type: "v",
|
||||
c: []
|
||||
};
|
||||
//default to English if question isn't translated
|
||||
if (!question.text[lang]) {
|
||||
lang = "en_GB";
|
||||
}
|
||||
var questionText = question.text[lang].replace(/\\n/g, "\n");
|
||||
var q = {
|
||||
type: "txt",
|
||||
wrap: true,
|
||||
fillx: 1,
|
||||
filly: 1,
|
||||
font: "Vector:20",
|
||||
label: questionText,
|
||||
id: "label"
|
||||
};
|
||||
out.c.push(q);
|
||||
var optFont = 'Vector:30';
|
||||
if (question.optFont !== undefined) optFont = question.optFont;
|
||||
var opt = {
|
||||
type: "btn",
|
||||
font: optFont,
|
||||
label: ">>",
|
||||
pad: 1,
|
||||
btnFaceCol: "#0f0",
|
||||
cb: l => drawResponseOpts(index)
|
||||
};
|
||||
out.c.push(opt);
|
||||
layout = new Layout(out);
|
||||
layout.render();
|
||||
return;
|
||||
}
|
||||
|
||||
function surveyResponse(text) {
|
||||
var arr = text.split(',');
|
||||
var nextSurvey = parseInt(arr[0]) + 1;
|
||||
let newArr = {
|
||||
"key": arr[1],
|
||||
"resp": arr[2],
|
||||
"value": arr[3]
|
||||
}
|
||||
modHS.saveDataToFile('survey', 'survey', newArr);
|
||||
drawSurveyLayout(nextSurvey);
|
||||
}
|
||||
|
||||
drawSurveyLayout(0);
|
||||
queueTaskScreenTimeout();
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
var Layout = require("Layout");
|
||||
const modHS = require('HSModule');
|
||||
var layout;
|
||||
|
||||
//var settings = modHS.getSettings();
|
||||
var appCache = modHS.getCache();
|
||||
|
||||
var results = {
|
||||
color: 0,
|
||||
volume: null,
|
||||
colorAssessment: appCache.urine && appCache.urine.colorAssessment ? appCache.urine.colorAssessment : 0
|
||||
}
|
||||
|
||||
function YMDInt(date) {
|
||||
var year = date.getFullYear().toString();
|
||||
var month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
var day = date.getDate().toString().padStart(2, '0');
|
||||
var concatenatedDate = year + month + day;
|
||||
var concatenatedInteger = parseInt(concatenatedDate);
|
||||
return concatenatedInteger;
|
||||
}
|
||||
|
||||
function saveUrineData(color) {
|
||||
if (color > 0) {
|
||||
var d = new Date();
|
||||
results.color = color;
|
||||
results.colorAssessment = YMDInt(d);
|
||||
}
|
||||
Bangle.buzz(150);
|
||||
modHS.saveDataToFile('urine', 'urine', results);
|
||||
g.clear();
|
||||
g.reset();
|
||||
layout = new Layout({
|
||||
type: "v",
|
||||
c: [{
|
||||
type: "img",
|
||||
pad: 4,
|
||||
src: require("heatshrink").decompress(atob("ikUwYFCgVJkgMDhMkyVJAwQFCAQNAgESAoQCBwEBBwlIgAFDpNkyAjDkm/5MEBwdf+gUEl/6AoVZkmX/oLClv6pf+DQn1/4+E3//0gFBkACBv/SBYI7D5JiDLJx9CBAR4CAoWQQ4Z9DgAA=="))
|
||||
},
|
||||
{
|
||||
type: "txt",
|
||||
font: "Vector:30",
|
||||
label: "Saved!"
|
||||
}]
|
||||
});
|
||||
layout.render();
|
||||
setTimeout(function () {
|
||||
Bangle.load();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function drawColorAssessment(){
|
||||
var dateNow = new Date();
|
||||
var lastUrineColorDate = appCache.urine && appCache.urine.colorAssessment ? appCache.urine.colorAssessment : 0;
|
||||
var hourCurrent = dateNow.getHours();
|
||||
var currentDay = YMDInt(dateNow);
|
||||
if (hourCurrent >= 16 && currentDay > lastUrineColorDate) {
|
||||
var layout = new Layout({
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "btn", font: "6x8:2", label: " ", btnFaceCol: E.HSBtoRGB(0.3, 0.99, 1), cb: l => saveUrineData(2), fillx: 1, filly: 1, pad: 1 },
|
||||
{ type: "btn", font: "6x8:2", label: " ", btnFaceCol: E.HSBtoRGB(0.2, 1, 1), cb: l => saveUrineData(1), fillx: 1, filly: 1, pad: 1 }
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "btn", font: "6x8:2", label: " ", btnFaceCol: E.HSBtoRGB(0.38, 1, 1), cb: l => saveUrineData(3), fillx: 1, filly: 1, pad: 1 },
|
||||
{ type: "btn", font: "6x8:2", label: " ", btnFaceCol: E.HSBtoRGB(0.44, 1, 0.9), cb: l => saveUrineData(4), fillx: 1, filly: 1, pad: 1 },
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
g.clear();
|
||||
g.reset();
|
||||
layout.render();
|
||||
} else {
|
||||
saveUrineData(0);
|
||||
}
|
||||
}
|
||||
|
||||
drawColorAssessment();
|
||||
/*
|
||||
//Urine Colours adapted NSW chart and from the Hillmen Urine Chart to includes blood presence
|
||||
//https://www.health.nsw.gov.au/environment/beattheheat/Pages/urine-colour-chart.aspx
|
||||
|
||||
*/
|
||||
|
|
@ -0,0 +1,811 @@
|
|||
(() => {
|
||||
const modHS = require('HSModule');
|
||||
var settings = modHS.getSettings();
|
||||
var cache = modHS.getCache();
|
||||
var hrmInterval = 0;
|
||||
var appName = "heatsuite";
|
||||
var bleAdvertGen = 0xE9D0;
|
||||
var lastBLEAdvert = [];
|
||||
var recorders;
|
||||
var activeRecorders = [];
|
||||
var dataLog = [];
|
||||
var lastGPSFix = 0;
|
||||
var gpsLog = [];
|
||||
var connectionLock = false;
|
||||
var processQueue = [];
|
||||
var processQueueTimeout = null;
|
||||
let initHandlerTimeout = null;
|
||||
let BTHRM_ConnectCheck = null;
|
||||
//high Accelerometry data
|
||||
var perSecAccHandler = null;
|
||||
var highAccTimeout = null;
|
||||
var highAccWriteTimeout = null;
|
||||
//Fall Detection
|
||||
var fallTime = 0;
|
||||
var fallDetected = false;
|
||||
|
||||
Bangle.setOptions({
|
||||
"hrmSportMode": -1,
|
||||
});
|
||||
//function for setting timeouts to the nearest second or minute
|
||||
function timeoutAligned(periodMs, callback) {
|
||||
var now = new Date();
|
||||
var millisPassed = (now.getSeconds() * 1000) + now.getMilliseconds();
|
||||
if (periodMs < 1000) periodMs = 1000; //nothing less than a second is allowed
|
||||
var millisLeft = periodMs - (millisPassed % periodMs);
|
||||
return setTimeout(() => { callback(); }, millisLeft);
|
||||
}
|
||||
function secondsSinceMidnight() {//valuable for compact storage of time
|
||||
let d = new Date();
|
||||
return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
|
||||
}
|
||||
function queueProcess(func, arg) {
|
||||
processQueue.push((next) => func(next, arg));
|
||||
if (!connectionLock) {
|
||||
processNextInQueue();
|
||||
} else {
|
||||
if (!processQueueTimeout) {
|
||||
processQueueTimeout = setTimeout(processNextInQueue, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
function processNextInQueue() {
|
||||
clearTimeout(processQueueTimeout); // Clear the timeout when processing starts
|
||||
processQueueTimeout = null;
|
||||
if (processQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (!connectionLock && processQueue.length > 0) {
|
||||
const task = processQueue.shift();
|
||||
task(() => {
|
||||
modHS.log("[ProcessQueue] Processing queued Task");
|
||||
processNextInQueue();
|
||||
});
|
||||
} else {
|
||||
if (!processQueueTimeout) {
|
||||
processQueueTimeout = setTimeout(processNextInQueue, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
function stringToBytes(str) {
|
||||
const byteArray = new Uint8Array(str.length);
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
byteArray[i] = str.charCodeAt(i);
|
||||
}
|
||||
return byteArray;
|
||||
}
|
||||
function xorEncryptWithSalt(payload, key, salt) {
|
||||
const encrypted = [];
|
||||
const keyLen = key.length;
|
||||
const saltLen = salt.length;
|
||||
|
||||
for (let i = 0; i < payload.length; i++) {
|
||||
encrypted[i] = payload[i] ^ key.charCodeAt(i % keyLen) ^ salt.charCodeAt(i % saltLen);
|
||||
}
|
||||
return encrypted;
|
||||
}
|
||||
function shuffleArray(array) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
let temp = array[i];
|
||||
array[i] = array[j];
|
||||
array[j] = temp;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
function flattenArray(array) {
|
||||
let flattened = [];
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (Array.isArray(array[i])) {
|
||||
// If the element is an array, concatenate it recursively
|
||||
flattened = flattened.concat(flattenArray(array[i]));
|
||||
} else {
|
||||
// If it's not an array, add the element to the result
|
||||
flattened.push(array[i]);
|
||||
}
|
||||
}
|
||||
return flattened;
|
||||
}
|
||||
function createRandomizedPayload(studyid, battery, temperature, heartRate, hr_loc, movement, pingFlag) {
|
||||
let textBytes = stringToBytes(studyid);
|
||||
if (textBytes.length < 4) {
|
||||
const paddedArray = new Uint8Array(4); // Create a new Uint8Array with 4 bytes (default 0x00)
|
||||
paddedArray.set(textBytes); // Copy the textBytes into the paddedArray
|
||||
textBytes = paddedArray; // Replace textBytes with the padded version
|
||||
}
|
||||
const dataBlocks = [];
|
||||
dataBlocks.push([0x00, textBytes[0], textBytes[1], textBytes[2], textBytes[3]]); // Study Code
|
||||
var alert = false;
|
||||
if (cache.hasOwnProperty('alert') && Object.keys(cache.alert).length > 0) {
|
||||
var HEALTH_EVENTS = {
|
||||
fall: 1,
|
||||
custom: 99
|
||||
};
|
||||
var HEALTH_EVENT_TYPE = HEALTH_EVENTS[cache.alert.type] || HEALTH_EVENTS.custom;
|
||||
dataBlocks.push([0x07, HEALTH_EVENT_TYPE]);
|
||||
}
|
||||
if (studyid !== "####") {
|
||||
if (battery != null) {
|
||||
dataBlocks.push([0x01, battery]); // Battery level
|
||||
}
|
||||
if (temperature != null && !isNaN(temperature)) {
|
||||
dataBlocks.push([0x02, temperature & 255, (temperature >> 8) & 255]); // Temperature
|
||||
}
|
||||
if (heartRate != null && !isNaN(heartRate)) {
|
||||
dataBlocks.push([0x03, heartRate]);
|
||||
}
|
||||
if (hr_loc != null && !isNaN(hr_loc) && !alert) { //sub this for an alert flag if needed!
|
||||
dataBlocks.push([0x04, hr_loc]);
|
||||
}
|
||||
|
||||
if (movement != null && !isNaN(movement)) {
|
||||
dataBlocks.push([
|
||||
0x05,
|
||||
movement & 255,
|
||||
(movement >> 8) & 255,
|
||||
(movement >> 16) & 255,
|
||||
(movement >> 24) & 255
|
||||
]);
|
||||
}
|
||||
}
|
||||
if (!isNaN(pingFlag)) {
|
||||
let statusByte = (+Bangle.isCharging() << 1) | pingFlag;
|
||||
dataBlocks.push([0x06, statusByte]);
|
||||
}
|
||||
modHS.log(JSON.stringify(dataBlocks));
|
||||
const randomizedDataBlocks = shuffleArray(dataBlocks);
|
||||
const payload = flattenArray(randomizedDataBlocks);
|
||||
return studyid !== "####" ? xorEncryptWithSalt(payload, "heatsuite", studyid) : payload;
|
||||
}
|
||||
//from: https://github.com/espruino/BangleApps/tree/master/apps/recorder
|
||||
//adapted to minute average data
|
||||
let getRecorders = function () {
|
||||
var recorders = {
|
||||
hrm: function () {
|
||||
var bpm = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null }, bpmConfidence = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null }, src = "";
|
||||
function onHRM(h) {
|
||||
if (h.confidence !== 0) bpmConfidence = newValueHandler(bpmConfidence, h.confidence);
|
||||
if (h.bpm !== 0) bpm = newValueHandler(bpm, h.bpm);
|
||||
src = h.src;
|
||||
}
|
||||
return {
|
||||
name: "HR",
|
||||
fields: ["hr", "hr_conf", "hr_src"],
|
||||
getValues: () => {
|
||||
var r = [bpm.avg === null ? null : bpm.avg.toFixed(0), bpmConfidence.avg === null ? null : bpmConfidence.avg.toFixed(0), src];
|
||||
bpm = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
bpmConfidence = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
src = "";
|
||||
return r;
|
||||
},
|
||||
start: () => {
|
||||
Bangle.on('HRM', onHRM);
|
||||
Bangle.setHRMPower(1, appName);
|
||||
},
|
||||
stop: () => {
|
||||
Bangle.removeListener('HRM', onHRM);
|
||||
Bangle.setHRMPower(0, appName);
|
||||
},
|
||||
draw: (x, y) => g.setColor(Bangle.isHRMOn() ? "#f00" : "#f88").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"), x, y)
|
||||
};
|
||||
},
|
||||
bthrm: function () {
|
||||
var bt_bpm = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
var bt_bat = "";
|
||||
var bt_energy = "";
|
||||
var bt_contact = "";
|
||||
var bt_rr = [];
|
||||
function onBTHRM(h) {
|
||||
//modHS.log(JSON.stringify(h));
|
||||
if (h.bpm === 0) return;
|
||||
bt_bpm = newValueHandler(bt_bpm, h.bpm);
|
||||
bt_bat = h.bat;
|
||||
bt_energy = h.energy;
|
||||
bt_contact = h.contact;
|
||||
if (h.rr) bt_rr.push(h.rr);
|
||||
}
|
||||
return {
|
||||
name: "BT HR",
|
||||
fields: ["bt_bpm", "bt_bat", "bt_energy", "bt_contact", "bt_rr"],
|
||||
getValues: () => {
|
||||
const result = [bt_bpm.avg === null ? null : bt_bpm.avg.toFixed(0), bt_bat, bt_energy, bt_contact, bt_rr.join(";")];
|
||||
bt_bpm = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
bt_rr = [];
|
||||
bt_bat = "";
|
||||
bt_energy = "";
|
||||
bt_contact = "";
|
||||
return result;
|
||||
},
|
||||
start: () => {
|
||||
Bangle.on('BTHRM', onBTHRM);
|
||||
if (Bangle.setBTHRMPower) Bangle.setBTHRMPower(1, appName);
|
||||
},
|
||||
stop: () => {
|
||||
Bangle.removeListener('BTHRM', onBTHRM);
|
||||
if (Bangle.setBTHRMPower) Bangle.setBTHRMPower(0, appName);
|
||||
}
|
||||
}
|
||||
},
|
||||
CORESensor: function () {
|
||||
var core = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
var skin = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
var core_hr = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
var hsi = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
var core_bat = null;
|
||||
var unit = null;
|
||||
function onCORE(h) {
|
||||
core = newValueHandler(core, h.core);
|
||||
skin = newValueHandler(skin, h.skin);
|
||||
if (core_hr > 0) {
|
||||
core_hr = newValueHandler(core_hr, h.hr);
|
||||
}
|
||||
hsi = newValueHandler(hsi, h.hsi);
|
||||
core_bat = h.battery;
|
||||
unit = h.unit;
|
||||
}
|
||||
return {
|
||||
name: "CORESensor",
|
||||
fields: ["core", "skin", "unit", "core_hr", "hsi", "core_bat"],
|
||||
getValues: () => {
|
||||
const result = [core.avg === null ? null : core.avg.toFixed(2), skin.avg === null ? null : skin.avg.toFixed(2), unit, core_hr.avg === null ? null : core_hr.avg.toFixed(0), hsi.avg === null ? null : hsi.avg.toFixed(1), core_bat];
|
||||
core = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
skin = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
core_hr = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
hsi = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
core_bat = null;
|
||||
unit = null;
|
||||
return result;
|
||||
},
|
||||
start: () => {
|
||||
Bangle.on('CORESensor', onCORE);
|
||||
if (Bangle.setCORESensorPower) Bangle.setCORESensorPower(1, appName);
|
||||
},
|
||||
stop: () => {
|
||||
Bangle.removeListener('CORESensor', onCORE);
|
||||
if (Bangle.setCORESensorPower) Bangle.setCORESensorPower(0, appName);
|
||||
}
|
||||
}
|
||||
},
|
||||
bat: function () {
|
||||
return {
|
||||
name: "BAT",
|
||||
fields: ["batt_p", "batt_v", "charging"],
|
||||
getValues: () => {
|
||||
return [E.getBattery(), NRF.getBattery().toFixed(2), Bangle.isCharging()];
|
||||
},
|
||||
start: () => {
|
||||
},
|
||||
stop: () => {
|
||||
},
|
||||
draw: (x, y) => g.setColor(Bangle.isCharging() ? "#0f0" : "#ff0").drawImage(atob("DAwBAABgH4G4EYG4H4H4H4GIH4AA"), x, y)
|
||||
};
|
||||
},
|
||||
steps: function () {
|
||||
var lastSteps = 0;
|
||||
return {
|
||||
name: "steps",
|
||||
fields: ["steps"],
|
||||
getValues: () => {
|
||||
var c = Bangle.getStepCount(), r = [c - lastSteps];
|
||||
lastSteps = c;
|
||||
return r;
|
||||
},
|
||||
start: () => { lastSteps = Bangle.getStepCount(); },
|
||||
stop: () => { },
|
||||
draw: (x, y) => g.reset().drawImage(atob("DAwBAAMMeeeeeeeecOMMAAMMMMAA"), x, y)
|
||||
};
|
||||
},
|
||||
movement: function () {
|
||||
return {
|
||||
name: "movement",
|
||||
fields: ["movement"],
|
||||
getValues: () => {
|
||||
return [Bangle.getHealthStatus().movement];
|
||||
},
|
||||
start: () => { },
|
||||
stop: () => { },
|
||||
draw: (x, y) => g.reset().drawImage(atob("DAwBAAMMeeeeeeeecOMMAAMMMMAA"), x, y)
|
||||
};
|
||||
},
|
||||
acc: function () {
|
||||
var accMagArray = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
function accelHandler(accel) {
|
||||
// magnitude is computed as: sqrt(x*x + y*y + z*z)
|
||||
// to compute Elucidean Norm Minus One, simply run: mag - 1
|
||||
// (https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0061691)
|
||||
accMagArray = newValueHandler(accMagArray, accel.mag);
|
||||
}
|
||||
return {
|
||||
name: "Accelerometer",
|
||||
fields: ["acc_min", "acc_max", "acc_avg", "acc_sum"],
|
||||
getValues: () => {
|
||||
var r = [accMagArray.min === null ? null : accMagArray.min.toFixed(4), accMagArray.max === null ? null : accMagArray.max.toFixed(4), accMagArray.avg === null ? null : accMagArray.avg.toFixed(4), accMagArray.sum === null ? null : accMagArray.sum.toFixed(4)];
|
||||
accMagArray = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
return r;
|
||||
},
|
||||
start: () => {
|
||||
//Bangle.setPollInterval(80); // This will allow it to be dynamic and save battery
|
||||
Bangle.on('accel', accelHandler);
|
||||
},
|
||||
stop: () => {
|
||||
Bangle.removeListener('accel', accelHandler);
|
||||
},
|
||||
draw: (x, y) => g.setColor(Bangle.isHRMOn() ? "#f00" : "#f88").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"), x, y)
|
||||
};
|
||||
},
|
||||
};
|
||||
if (Bangle.getPressure) {
|
||||
recorders['baro'] = function () {
|
||||
var temp = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
var press = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
var alt = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
function onPress(c) {
|
||||
if (c.temperature !== 0) temp = newValueHandler(temp, c.temperature);
|
||||
if (c.pressure !== 0) press = newValueHandler(press, c.pressure);
|
||||
if (c.altitude !== 0) alt = newValueHandler(alt, c.altitude);
|
||||
}
|
||||
return {
|
||||
name: "Baro",
|
||||
fields: ["baro_temp", "baro_press", "baro_alt"],
|
||||
getValues: () => {
|
||||
var r = [temp.avg === null ? null : temp.avg.toFixed(2), press.avg === null ? null : press.avg.toFixed(2), alt.avg === null ? null : alt.avg.toFixed(2)];
|
||||
var temp = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
var press = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
var alt = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
|
||||
return r;
|
||||
},
|
||||
start: () => {
|
||||
Bangle.setBarometerPower(1, appName);
|
||||
Bangle.on('pressure', onPress);
|
||||
},
|
||||
stop: () => {
|
||||
Bangle.setBarometerPower(0, appName);
|
||||
Bangle.removeListener('pressure', onPress);
|
||||
},
|
||||
draw: (x, y) => g.setColor("#0f0").drawImage(atob("DAwBAAH4EIHIEIHIEIHIEIEIH4AA"), x, y)
|
||||
};
|
||||
}
|
||||
}
|
||||
return recorders;
|
||||
}
|
||||
function newValueHandler(arr, value) {//a way to keep resource use down for each sensor since this could grow large!
|
||||
arr.count = (arr.count === null) ? 1 : arr.count + 1;
|
||||
if (arr.count === 1) arr.min = value;
|
||||
arr.last = value;
|
||||
arr.avg = (value + (arr.avg * (arr.count - 1))) / arr.count;
|
||||
arr.sum = arr.sum + value;
|
||||
arr.min = (value < arr.min) ? value : arr.min;
|
||||
arr.max = (value > arr.max) ? value : arr.max;
|
||||
return arr;
|
||||
}
|
||||
//increased accelerometer data storage for higher resolution activity tracking
|
||||
function perSecAcc(status) {
|
||||
if(!status){
|
||||
if (perSecAccHandler) Bangle.removeListener('accel', perSecAccHandler);
|
||||
if (highAccTimeout) clearTimeout(highAccTimeout);
|
||||
if (highAccWriteTimeout) clearTimeout(highAccWriteTimeout);
|
||||
return;
|
||||
}
|
||||
function arrayAcc() {
|
||||
return { count: 0, sum: 0 };
|
||||
}
|
||||
function updateArray(acc, value) {
|
||||
acc.sum += value;
|
||||
acc.count++;
|
||||
}
|
||||
function getAvg(acc) {
|
||||
return acc.count ? acc.sum / acc.count : 0;
|
||||
}
|
||||
let mag = arrayAcc();
|
||||
let accTemp = [];
|
||||
perSecAccHandler = function(accel){
|
||||
updateArray(mag, accel.mag);
|
||||
};
|
||||
Bangle.on('accel', perSecAccHandler);
|
||||
|
||||
function writeAccLog(buf) {
|
||||
if (!buf || !buf.length) return;
|
||||
let f = modHS.getRecordFile("accel", []);
|
||||
if (!f) return;
|
||||
let line = "";
|
||||
function processArrayChunk() {
|
||||
let chunkSize = 10;
|
||||
for (let i = 0; i < chunkSize && buf.length; i++) {
|
||||
let data = buf.shift();
|
||||
if (!data || data.length !== 8) continue;
|
||||
let dv = new DataView(data.buffer);
|
||||
let t = dv.getUint32(0, true);
|
||||
let mag = dv.getUint16(4, true);
|
||||
let sum = dv.getUint16(6, true);
|
||||
line += t + "," + mag + "," + sum + "\n";
|
||||
}
|
||||
if (buf.length) {
|
||||
setTimeout(processArrayChunk, 10);
|
||||
} else {
|
||||
f.write(line);
|
||||
f = null;
|
||||
}
|
||||
}
|
||||
processArrayChunk();
|
||||
}
|
||||
|
||||
function writeHSAccelSetTimeout() {
|
||||
if (accTemp.length > 0) {
|
||||
queueProcess((next, buf) => {
|
||||
writeAccLog(buf);
|
||||
next();
|
||||
},accTemp);
|
||||
accTemp = [];
|
||||
}
|
||||
highAccWriteTimeout = timeoutAligned(10000, writeHSAccelSetTimeout); //check every 10 seconds
|
||||
}
|
||||
function tempAccLog() {
|
||||
let secondsSM = secondsSinceMidnight();
|
||||
let scaledMagAvg = Math.round(getAvg(mag) * 8192);
|
||||
let scaledMagSum = Math.round(mag.sum * 1024);
|
||||
let b = new Uint8Array(8);
|
||||
let dv = new DataView(b.buffer);
|
||||
dv.setUint32(0, secondsSM, true);
|
||||
dv.setUint16(4, scaledMagAvg, true);
|
||||
dv.setUint16(6, scaledMagSum, true);
|
||||
accTemp.push(b); // Push Uint8Array
|
||||
mag = arrayAcc();
|
||||
highAccTimeout = timeoutAligned(1000, tempAccLog);
|
||||
}
|
||||
let rawAccLogInt = (settings.AccLogInt ? settings.AccLogInt * 1000 : 1000);
|
||||
let accLogInt = Math.max(1000, Math.round(rawAccLogInt / 1000) * 1000);
|
||||
highAccTimeout = timeoutAligned(accLogInt, tempAccLog);
|
||||
highAccWriteTimeout = timeoutAligned(30000, writeHSAccelSetTimeout);
|
||||
}
|
||||
|
||||
function updateBLEAdvert(data) {
|
||||
var unix = parseInt((new Date().getTime() / 1000).toFixed(0));
|
||||
var batt = null,
|
||||
rawTemp = null,
|
||||
temperature = null,
|
||||
heartRate = null,
|
||||
hr_loc = null,
|
||||
rawMovement = null,
|
||||
movement = null;
|
||||
if (data.length > 0) {
|
||||
var headers = ['unix', 'tz'];
|
||||
activeRecorders.forEach(recorder => headers.push.apply(headers, recorder.fields));
|
||||
const safeGet = (field) => {
|
||||
const index = headers.indexOf(field);
|
||||
return index !== -1 ? data[index] : null;
|
||||
};
|
||||
unix = data[0]; // Unix timestamp is always first
|
||||
batt = safeGet('batt_p', data, headers);
|
||||
rawTemp = safeGet('baro_temp', data, headers);
|
||||
temperature = (rawTemp != null) ? Math.round(rawTemp * 100) : null;
|
||||
heartRate = safeGet('hr', data, headers);
|
||||
hr_loc = 1;
|
||||
if (headers.includes('bt_hrm')) {
|
||||
hr_loc = 2;
|
||||
heartRate = safeGet('bt_hrm', data, headers);
|
||||
}
|
||||
rawMovement = safeGet('acc_sum', data, headers);
|
||||
movement = (rawMovement != null) ? Math.round(rawMovement * 10000) : null;
|
||||
}
|
||||
var studyid = settings.studyID || "####";
|
||||
if (studyid.length > 4) {
|
||||
studyid = studyid.substring(0, 4);
|
||||
}
|
||||
var lastNodePing = cache.lastNodePing || 0;
|
||||
var nodePing = (Math.abs(unix - lastNodePing) > 360) ? 1 : 0;
|
||||
let advert = createRandomizedPayload(studyid, batt, temperature, heartRate, hr_loc, movement, nodePing);
|
||||
modHS.log(advert);
|
||||
require("ble_advert").set(bleAdvertGen, advert);
|
||||
}
|
||||
|
||||
function fallDetectFunc(acc) {
|
||||
if (!fallDetected) {
|
||||
let d = new Date().getTime();
|
||||
if (fallTime != 0) {
|
||||
modHS.log("acc", acc.mag);
|
||||
}
|
||||
if (fallTime != 0 && d - fallTime > 200) {
|
||||
fallTime = 0; fallDetected = false;
|
||||
} else if (acc.mag < 0.3 && fallTime === 0) {
|
||||
fallTime = d;
|
||||
modHS.log("FALLING", fallTime);
|
||||
} else if (acc.mag > 2.1 && d - fallTime < 200) {
|
||||
//IMPACT
|
||||
Bangle.buzz(400);
|
||||
E.showPrompt("Did you fall?", { title: "FALL DETECTION", img: atob("FBQBAfgAf+Af/4P//D+fx/n+f5/v+f//n//5//+f//n////3//5/n+P//D//wf/4B/4AH4A=") }).then((r) => {
|
||||
if (r) {
|
||||
fallDetected = true;
|
||||
modHS.saveDataToFile('alert', 'alert', { 'type': 'fall' });
|
||||
Bangle.showClock();
|
||||
} else {
|
||||
Bangle.showClock(); //no fall, so just return to clock
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function storeTempLog(unix) {
|
||||
var fields = [unix, ((new Date()).getTimezoneOffset() * -60)];
|
||||
activeRecorders.forEach(recorder => fields.push.apply(fields, recorder.getValues()));
|
||||
dataLog.push(fields);
|
||||
lastBLEAdvert = fields;
|
||||
updateBLEAdvert(lastBLEAdvert);
|
||||
}
|
||||
|
||||
function writeLog() {
|
||||
var headers = ["unix", "tz"];
|
||||
activeRecorders.forEach(recorder => headers.push.apply(headers, recorder.fields));
|
||||
var storageFile = modHS.getRecordFile('minData', headers);
|
||||
try {
|
||||
if (storageFile) {
|
||||
while (dataLog.length > 0) {
|
||||
let item = dataLog.shift();
|
||||
storageFile.write(item.join(',') + '\n');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
modHS.log(e);
|
||||
}
|
||||
modHS.checkStorageFree('minData');
|
||||
return true;
|
||||
}
|
||||
|
||||
function toggleHRM() {
|
||||
var recHRM = recorders['hrm'];
|
||||
var hrm = recHRM();
|
||||
if (Bangle.isHRMOn()) {
|
||||
hrm.stop();
|
||||
} else {
|
||||
hrm.start();
|
||||
}
|
||||
}
|
||||
|
||||
function writeGPS() {
|
||||
var storageFile = modHS.getRecordFile('gps', ["unix", "tz", 'lat', "lon", "alt", "speed", "course", "fix", "satellites"]);
|
||||
if (storageFile) {
|
||||
while (gpsLog.length > 0) { //store it
|
||||
let item = gpsLog.shift();
|
||||
storageFile.write(item.join(',') + '\n');
|
||||
cache["lastGPSSignal"] = item[0];
|
||||
queueProcess((next, cache) => {
|
||||
modHS.writeCache(cache);
|
||||
next();
|
||||
}, cache);
|
||||
}
|
||||
}
|
||||
}
|
||||
function gpsHandler() {
|
||||
if (settings.GPS) {
|
||||
function logGPS(f) {
|
||||
if (!isNaN(f.lat)) {
|
||||
const unix = parseInt((new Date().getTime() / 1000).toFixed(0));
|
||||
if (unix > lastGPSFix + 60) {
|
||||
var fields = [unix, ((new Date()).getTimezoneOffset() * -60), f.lat, f.lon, f.alt, f.speed, f.course, f.fix, f.satellites];
|
||||
gpsLog.push(fields);
|
||||
lastGPSFix = unix;
|
||||
}
|
||||
}
|
||||
}
|
||||
Bangle.on('GPS', logGPS);
|
||||
Bangle.setGPSPower(1, appName);
|
||||
var updateTime = settings.GPSInterval !== undefined ? settings.GPSInterval * 60 : 600;
|
||||
var searchTime = settings.GPSScanTime !== undefined ? settings.GPSScanTime * 60 : 60;
|
||||
var adaptiveTime = settings.GPSAdaptiveTime !== undefined ? settings.GPSAdaptiveTime * 60 : 120;
|
||||
require("gpssetup").setPowerMode({ power_mode: "PSMOO", update: updateTime, search: searchTime, adaptive: adaptiveTime, appName: appName });
|
||||
} else {
|
||||
Bangle.setGPSPower(0, appName); //just make sure its off
|
||||
}
|
||||
}
|
||||
|
||||
function studyTaskCheck(timenow) {
|
||||
modHS.log("[StudyTask] Func init at " + timenow);
|
||||
let notifications = false;
|
||||
const tasks = settings.StudyTasks;
|
||||
tasks.forEach(task => {
|
||||
let key = task.id;
|
||||
modHS.log(`[StudyTask] Processing task: ${JSON.stringify(task)}`);
|
||||
if (!cache[key]) {
|
||||
cache[key] = {};
|
||||
}
|
||||
const d = new Date(timenow * 1000);
|
||||
const hours = d.getHours();
|
||||
const minutes = d.getMinutes().toString().padStart(2, "0");
|
||||
const tod = parseInt(`${hours}${minutes}`);
|
||||
const lastTaskTime = cache[key].unix || 0;
|
||||
const debounceTime = (timenow - lastTaskTime) >= task.debounce;
|
||||
if (task.tod !== undefined && Array.isArray(task.tod) && task.tod.includes(tod) && debounceTime) {
|
||||
modHS.log(`[StudyTask] Time to notify: ${task}`);
|
||||
const taskID = { id: key, time: timenow };
|
||||
cache.taskQueue = cache.taskQueue || []; // Ensure taskQueue exists
|
||||
cache.taskQueue.push(taskID);
|
||||
var seen = {};
|
||||
var newTaskQueue = [];
|
||||
for (var i = 0; i < cache.taskQueue.length; i++) {
|
||||
var obj = cache.taskQueue[i];
|
||||
if (!seen[obj.id]) {
|
||||
seen[obj.id] = true;
|
||||
newTaskQueue.push(obj);
|
||||
}
|
||||
}
|
||||
cache.taskQueue = newTaskQueue;
|
||||
modHS.log(`[StudyTask] ${JSON.stringify(cache)}`);
|
||||
if (task.notify) {
|
||||
notifications = true;
|
||||
}
|
||||
modHS.writeCache(cache);
|
||||
WIDGETS["heatsuite"].draw();
|
||||
}
|
||||
});
|
||||
if (notifications) {
|
||||
Bangle.buzz(200);
|
||||
setTimeout(() => Bangle.buzz(200), 300);
|
||||
}
|
||||
if (notifications && Bangle.CLOCK && settings.notifications) {
|
||||
const desc = `Tasks: ${cache.taskQueue.length}`;
|
||||
E.showPrompt(desc, {
|
||||
title: "NOTIFICATION",
|
||||
img: atob("FBQBAfgAf+Af/4P//D+fx/n+f5/v+f//n//5//+f//n////3//5/n+P//D//wf/4B/4AH4A="),
|
||||
buttons: {
|
||||
" X ": false,
|
||||
" >> ": true
|
||||
}
|
||||
}).then(v => {
|
||||
if (v) {
|
||||
Bangle.load("heatsuite.app.js");
|
||||
} else {
|
||||
Bangle.load();
|
||||
}
|
||||
});
|
||||
}
|
||||
modHS.log("[StudyTask] Func end");
|
||||
}
|
||||
|
||||
//heart beat of the backend
|
||||
function init() {
|
||||
cache = modHS.getCache(); //update cache each minute
|
||||
var unix = parseInt((new Date().getTime() / 1000).toFixed(0));
|
||||
if (storeTempLog(unix)) {
|
||||
modHS.log("Data Logged to RAM");
|
||||
}
|
||||
queueProcess((next, unix) => {
|
||||
modHS.log("[HRM + StudyTask]");
|
||||
try {
|
||||
// HRM interval check
|
||||
if (settings.HRMInterval > 0) {
|
||||
if (hrmInterval >= settings.HRMInterval) {
|
||||
toggleHRM();
|
||||
hrmInterval = 0;
|
||||
}
|
||||
hrmInterval++;
|
||||
}
|
||||
if (settings.StudyTasks.length > 0) {
|
||||
studyTaskCheck(unix); // This might also need to be queued if async
|
||||
}
|
||||
} catch (error) {
|
||||
modHS.log("Error in StudyTaskCheck:", error);
|
||||
} finally {
|
||||
next(); // Ensure next() is called even if an error occurs
|
||||
}
|
||||
}, unix);
|
||||
}
|
||||
function initHandler() {
|
||||
function callback() {
|
||||
init(); initHandler();
|
||||
}
|
||||
initHandlerTimeout = timeoutAligned(60000, callback);
|
||||
}
|
||||
|
||||
function startRecorder() {
|
||||
settings = modHS.getSettings();
|
||||
if (initHandlerTimeout) clearTimeout(initHandlerTimeout);
|
||||
if (BTHRM_ConnectCheck) clearInterval(BTHRM_ConnectCheck);
|
||||
activeRecorders = []; //clear active recorders
|
||||
recorders = getRecorders();
|
||||
settings.record.forEach(r => {
|
||||
var recorder = recorders[r];
|
||||
if (!recorder) {
|
||||
return;
|
||||
}
|
||||
var activeRecorder = recorder();
|
||||
activeRecorder.start();
|
||||
activeRecorders.push(activeRecorder);
|
||||
});
|
||||
if (settings.hasOwnProperty('fallDetect') && settings.fallDetect) {
|
||||
Bangle.on('accel', fallDetectFunc);
|
||||
} else {
|
||||
Bangle.removeListener('accel', fallDetectFunc);
|
||||
}
|
||||
//BTHRM Additions
|
||||
if (settings.record.includes('bthrm') && Bangle.hasOwnProperty("isBTHRMConnected")) {
|
||||
var BTHRMStatus = 0;
|
||||
BTHRM_ConnectCheck = setInterval(function () {
|
||||
if (Bangle.isBTHRMConnected() != BTHRMStatus) {
|
||||
BTHRMStatus = Bangle.isBTHRMConnected();
|
||||
WIDGETS["heatsuite"].draw();
|
||||
}
|
||||
}, 10000); //runs every 10 seconds
|
||||
}
|
||||
updateBLEAdvert(lastBLEAdvert);
|
||||
initHandler();
|
||||
if (settings.highAcc !== undefined && settings.highAcc) {
|
||||
perSecAcc(settings.highAcc);
|
||||
}
|
||||
}
|
||||
|
||||
startRecorder();
|
||||
|
||||
gpsHandler();
|
||||
|
||||
function writeSetTimeout() {
|
||||
if (dataLog.length > 0) {
|
||||
queueProcess((next, unix) => {
|
||||
writeLog();
|
||||
next();
|
||||
},0);
|
||||
}
|
||||
if (gpsLog.length > 0) {
|
||||
//writeGPS();
|
||||
queueProcess((next, unix) => {
|
||||
writeGPS();
|
||||
next();
|
||||
},0);
|
||||
}
|
||||
setTimeout(writeSetTimeout, 5000);
|
||||
}
|
||||
|
||||
//log writer/checker
|
||||
writeSetTimeout();
|
||||
|
||||
//widget stuff
|
||||
var iconWidth = 44;
|
||||
function draw() {
|
||||
g.reset();
|
||||
if (cache.taskQueue !== undefined) (cache.taskQueue.length > 0 ? g.setColor("#f00") : g.setColor("#0f0"));
|
||||
g.setFontAlign(0, 0);
|
||||
g.fillRect({ x: this.x, y: this.y, w: this.x + iconWidth - 1, h: this.y + 23, r: 8 });
|
||||
g.setColor(-1);
|
||||
g.setFont("Vector", 12);
|
||||
g.drawImage(atob("FBfCAP//AADk+kPKAAAoAAAAAKoAAAAAKAAAAFQoFQAAVTxVAFQVVVQVRABVABFVEBQEVQBUABUAAFUAVQCoFVVUKogAVQAiqBVVVCoAVQBVAABUABUAVRAUBFVEAFUAEVQVVVQVAFU8VQAAVCgVAAAAKAAAAACqAAAAACgAAA=="), this.x + 1, this.y + 1);
|
||||
g.setColor((Bangle.hasOwnProperty("isBTHRMConnected") && Bangle.isBTHRMConnected()) ? "#00F" : "#0f0");
|
||||
g.drawImage(atob("EhCCAAKoAqgCqqiqqCqqqqqqqqqqqqqqqqqqqqqqqqqqqqqiqqqqqAqqqqoAKqqqgACqqqAAAqqoAAAKqgAAACqAAAAAoAAAAAoAAA=="), this.x + 22, this.y + 3);
|
||||
}
|
||||
WIDGETS.heatsuite = {
|
||||
area: 'tr',
|
||||
width: iconWidth,
|
||||
draw: draw,
|
||||
changed: function () {
|
||||
startRecorder();
|
||||
WIDGETS["heatsuite"].draw();
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.on('tap', function(data) {
|
||||
if(data.double && data.dir === "front" && !Bangle.isCharging()){
|
||||
require("widget_utils").hide();
|
||||
Bangle.load('heatsuite.app.js');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//Diagnosing BLUETOOTH Connection Issues
|
||||
if (NRF.getSecurityStatus().connected) { //if widget starts while a bluetooth connection exits, need to force connection flag
|
||||
connectionLock = true;
|
||||
}
|
||||
NRF.on('error', function (msg) {
|
||||
modHS.log("[NRF][ERROR] " + msg);
|
||||
});
|
||||
NRF.on('connect', function (addr) {
|
||||
connectionLock = true;
|
||||
modHS.log("[NRF][CONNECTED] " + JSON.stringify(addr));
|
||||
});
|
||||
NRF.on('disconnect', function (reason) {
|
||||
connectionLock = false;
|
||||
cache = modHS.getCache(); //update cache each disconnect
|
||||
if (lastBLEAdvert) {
|
||||
updateBLEAdvert(lastBLEAdvert); //update this if its changed with cache update
|
||||
}
|
||||
processNextInQueue();
|
||||
modHS.log("[NRF][DISCONNECT] " + JSON.stringify(reason));
|
||||
});
|
||||
})();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -0,0 +1,31 @@
|
|||
{ "id": "heatsuite",
|
||||
"name": "HeatSuite",
|
||||
"shortName":"HeatSuite",
|
||||
"version":"0.09",
|
||||
"description": "The smartwatch software to integrate with HeatSuite",
|
||||
"icon": "icon.png",
|
||||
"type": "app",
|
||||
"tags": "health,tool",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"dependencies" : { "bthrm":"app", "gpssetup" : "app", "coretemp":"app"},
|
||||
"customConnect": true,
|
||||
"custom": "custom.html",
|
||||
"storage": [
|
||||
{"name":"heatsuite.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"HSModule","url":"heatsuite.module.js"},
|
||||
{"name":"heatsuite.app.js","url":"heatsuite.app.js"},
|
||||
{"name":"heatsuite.boot.js","url":"heatsuite.boot.js"},
|
||||
{"name":"heatsuite.settings.js","url":"heatsuite.settings.js"},
|
||||
{"name":"heatsuite.wid.js","url":"heatsuite.wid.js"},
|
||||
{"name":"heatsuite.survey.js","url":"heatsuite.survey.js"},
|
||||
{"name":"heatsuite.bletemp.js","url":"heatsuite.bletemp.js"},
|
||||
{"name":"heatsuite.bp.js","url":"heatsuite.bp.js"},
|
||||
{"name":"heatsuite.mass.js","url":"heatsuite.mass.js"},
|
||||
{"name":"heatsuite.urine.js","url":"heatsuite.urine.js"}
|
||||
],
|
||||
"data": [
|
||||
{"name":"heatsuite.settings.json"},
|
||||
{"name":"heatsuite.cache.json"}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue