|
|
|
|
@ -1,18 +1,18 @@
|
|
|
|
|
(function karvonen(hrmSettings, exsHrmStats) {
|
|
|
|
|
//This app is an extra feature implementation for the Run.app of the bangle.js. It's called run+
|
|
|
|
|
//The calculation of the Heart Rate Zones is based on the Karvonen method. It requires to know maximum and minimum heart rates. More precise calculation methods require a lab.
|
|
|
|
|
//Other methods are even more approximative.
|
|
|
|
|
let wu = require("widget_utils");
|
|
|
|
|
wu.hide();
|
|
|
|
|
let R = Bangle.appRect;
|
|
|
|
|
let R;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
g.reset().clearRect(R).setFontAlign(0,0,0);
|
|
|
|
|
const ICON_LOCK = atob("DhABH+D/wwMMDDAwwMf/v//4f+H/h/8//P/z///f/g==");
|
|
|
|
|
const ICON_HEART = atob("Dw4BODj4+fv3///////f/z/+P/g/4D+APgA4ACAA");
|
|
|
|
|
const ICON_LOCATION = atob("CxABP4/7x/B+D8H8e/5/x/D+D4HwHAEAIA==");
|
|
|
|
|
|
|
|
|
|
const x = "x"; const y = "y";
|
|
|
|
|
function Rdiv(axis, divisor) { // Used when placing things on the screen
|
|
|
|
|
return axis=="x" ? (R.x + (R.w-1)/divisor):(R.y + (R.h-1)/divisor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let linePoints = { //Not lists of points, but used to update points in the drawArrows function.
|
|
|
|
|
x: [
|
|
|
|
|
175/40,
|
|
|
|
|
@ -24,10 +24,10 @@
|
|
|
|
|
175/52,
|
|
|
|
|
175/110,
|
|
|
|
|
175/122,
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function drawArrows() {
|
|
|
|
|
g.setColor(g.theme.fg);
|
|
|
|
|
// Upper
|
|
|
|
|
@ -42,27 +42,29 @@
|
|
|
|
|
// HRR = maximum HR - Minimum HR. minhr is minimum hr, maxhr is maximum hr.
|
|
|
|
|
//get the hrr (heart rate reserve).
|
|
|
|
|
// I put random data here, but this has to come as a menu in the settings section so that users can change it.
|
|
|
|
|
let minhr = hrmSettings.min;
|
|
|
|
|
let maxhr = hrmSettings.max;
|
|
|
|
|
|
|
|
|
|
function calculatehrr(minhr, maxhr) {
|
|
|
|
|
return maxhr - minhr;}
|
|
|
|
|
|
|
|
|
|
//test input for hrr (it works).
|
|
|
|
|
let hrr = calculatehrr(minhr, maxhr);
|
|
|
|
|
console.log(hrr);
|
|
|
|
|
let minhr;
|
|
|
|
|
let maxhr;
|
|
|
|
|
let hrr;
|
|
|
|
|
|
|
|
|
|
//Test input to verify the zones work. The following value for "hr" has to be deleted and replaced with the Heart Rate Monitor input.
|
|
|
|
|
let hr = exsHrmStats.getValue();
|
|
|
|
|
let hr;
|
|
|
|
|
// These letiables display next and previous HR zone.
|
|
|
|
|
//get the hrzones right. The calculation of the Heart rate zones here is based on the Karvonen method
|
|
|
|
|
//60-70% of HRR+minHR = zone2. //70-80% of HRR+minHR = zone3. //80-90% of HRR+minHR = zone4. //90-99% of HRR+minHR = zone5. //=>99% of HRR+minHR = serious risk of heart attack
|
|
|
|
|
let minzone2 = hrr * 0.6 + minhr;
|
|
|
|
|
let maxzone2 = hrr * 0.7 + minhr;
|
|
|
|
|
let maxzone3 = hrr * 0.8 + minhr;
|
|
|
|
|
let maxzone4 = hrr * 0.9 + minhr;
|
|
|
|
|
let maxzone5 = hrr * 0.99 + minhr;
|
|
|
|
|
let minzone2;
|
|
|
|
|
let maxzone2;
|
|
|
|
|
let maxzone3;
|
|
|
|
|
let maxzone4;
|
|
|
|
|
let maxzone5;
|
|
|
|
|
|
|
|
|
|
function calculatehrr(minhr, maxhr) {
|
|
|
|
|
hrr = maxhr - minhr;
|
|
|
|
|
minzone2 = hrr * 0.6 + minhr;
|
|
|
|
|
maxzone2 = hrr * 0.7 + minhr;
|
|
|
|
|
maxzone3 = hrr * 0.8 + minhr;
|
|
|
|
|
maxzone4 = hrr * 0.9 + minhr;
|
|
|
|
|
maxzone5 = hrr * 0.99 + minhr;
|
|
|
|
|
}
|
|
|
|
|
// HR data: large, readable, in the middle of the screen
|
|
|
|
|
function drawHR() {
|
|
|
|
|
g.setFontAlign(-1,0,0);
|
|
|
|
|
@ -70,6 +72,7 @@
|
|
|
|
|
g.setColor(g.theme.fg);
|
|
|
|
|
g.setFont("Vector",50);
|
|
|
|
|
g.drawString(hr, Rdiv(x,11/4), Rdiv(y,2)+4);
|
|
|
|
|
drawArrows();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function drawWaitHR() {
|
|
|
|
|
@ -87,21 +90,23 @@
|
|
|
|
|
g.setFont("Vector",20);
|
|
|
|
|
g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/2));
|
|
|
|
|
g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/7));
|
|
|
|
|
|
|
|
|
|
drawArrows();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//These functions call arcs to show different HR zones.
|
|
|
|
|
|
|
|
|
|
//To shorten the code, I'll reference some letiables and reuse them.
|
|
|
|
|
let centreX = R.x + 0.5 * R.w;
|
|
|
|
|
let centreY = R.y + 0.5 * R.h;
|
|
|
|
|
let minRadius = 0.38 * R.h;
|
|
|
|
|
let maxRadius = 0.50 * R.h;
|
|
|
|
|
let centreX;
|
|
|
|
|
let centreY;
|
|
|
|
|
let minRadius;
|
|
|
|
|
let maxRadius;
|
|
|
|
|
|
|
|
|
|
//draw background image (dithered green zones)(I should draw different zones in different dithered colors)
|
|
|
|
|
const HRzones= require("graphics_utils");
|
|
|
|
|
let minRadiusz = 0.44 * R.h;
|
|
|
|
|
let startAngle = HRzones.degreesToRadians(-88.5);
|
|
|
|
|
let endAngle = HRzones.degreesToRadians(268.5);
|
|
|
|
|
let minRadiusz;
|
|
|
|
|
let startAngle;
|
|
|
|
|
let endAngle;
|
|
|
|
|
|
|
|
|
|
function drawBgArc() {
|
|
|
|
|
g.setColor(g.theme.dark==false?0xC618:"#002200");
|
|
|
|
|
@ -136,8 +141,7 @@
|
|
|
|
|
HRzones.fillArc(g, centreX, centreY, minRadius-1, minRadiusz, startAngle, endAngle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getZone(zone) {
|
|
|
|
|
drawBgArc();
|
|
|
|
|
function drawZone(zone) {
|
|
|
|
|
clearCurrentZone();
|
|
|
|
|
if (zone >= 0) {zoning(minzone2, minhr);g.setColor("#00ffff");simplify(-88.5, -45, "Z1", 0, zone);}
|
|
|
|
|
if (zone >= 1) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-43.5, -21.5, "Z2", 1, zone);}
|
|
|
|
|
@ -154,60 +158,190 @@
|
|
|
|
|
if (zone == 12) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(206.5, 227.5, "Z5", 12, zone);}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getZoneAlert() {
|
|
|
|
|
function drawIndicators(){
|
|
|
|
|
drawLockIndicator();
|
|
|
|
|
drawPulseIndicator();
|
|
|
|
|
drawGpsIndicator();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function drawZoneAlert() {
|
|
|
|
|
const HRzonemax = require("graphics_utils");
|
|
|
|
|
drawIndicators();
|
|
|
|
|
g.clearRect(R);
|
|
|
|
|
drawIndicators();
|
|
|
|
|
let minRadius = 0.40 * R.h;
|
|
|
|
|
let startAngle1 = HRzonemax.degreesToRadians(-90);
|
|
|
|
|
let endAngle1 = HRzonemax.degreesToRadians(270);
|
|
|
|
|
g.setFont("Vector",38);g.setColor("#ff0000");
|
|
|
|
|
HRzonemax.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle1, endAngle1);
|
|
|
|
|
g.drawString("ALERT", 26,66);
|
|
|
|
|
g.setFontAlign(0,0).drawString("ALERT", centreX, centreY);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function drawWaitUI(){
|
|
|
|
|
g.clearRect(R);
|
|
|
|
|
drawIndicators();
|
|
|
|
|
drawBgArc();
|
|
|
|
|
drawWaitHR();
|
|
|
|
|
drawArrows();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function drawBase() {
|
|
|
|
|
g.clearRect(R);
|
|
|
|
|
drawIndicators();
|
|
|
|
|
drawBgArc();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function drawZoneUI(full){
|
|
|
|
|
if (full) {
|
|
|
|
|
drawBase();
|
|
|
|
|
}
|
|
|
|
|
drawHR();
|
|
|
|
|
drawZones();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Subdivided zones for better readability of zones when calling the images. //Changing HR zones will trigger the function with the image and previous&next HR zones.
|
|
|
|
|
let subZoneLast;
|
|
|
|
|
function drawZones() {
|
|
|
|
|
if ((hr < maxhr - 2) && subZoneLast==13) {g.clear(); drawArrows(); drawHR();} // Reset UI when coming down from zone alert.
|
|
|
|
|
if (hr <= hrr * 0.6 + minhr) {if (subZoneLast!=0) {subZoneLast=0; getZone(subZoneLast);}} // Z1
|
|
|
|
|
else if (hr <= hrr * 0.64 + minhr) {if (subZoneLast!=1) {subZoneLast=1; getZone(subZoneLast);}} // Z2a
|
|
|
|
|
else if (hr <= hrr * 0.67 + minhr) {if (subZoneLast!=2) {subZoneLast=2; getZone(subZoneLast);}} // Z2b
|
|
|
|
|
else if (hr <= hrr * 0.70 + minhr) {if (subZoneLast!=3) {subZoneLast=3; getZone(subZoneLast);}} // Z2c
|
|
|
|
|
else if (hr <= hrr * 0.74 + minhr) {if (subZoneLast!=4) {subZoneLast=4; getZone(subZoneLast);}} // Z3a
|
|
|
|
|
else if (hr <= hrr * 0.77 + minhr) {if (subZoneLast!=5) {subZoneLast=5; getZone(subZoneLast);}} // Z3b
|
|
|
|
|
else if (hr <= hrr * 0.80 + minhr) {if (subZoneLast!=6) {subZoneLast=6; getZone(subZoneLast);}} // Z3c
|
|
|
|
|
else if (hr <= hrr * 0.84 + minhr) {if (subZoneLast!=7) {subZoneLast=7; getZone(subZoneLast);}} // Z4a
|
|
|
|
|
else if (hr <= hrr * 0.87 + minhr) {if (subZoneLast!=8) {subZoneLast=8; getZone(subZoneLast);}} // Z4b
|
|
|
|
|
else if (hr <= hrr * 0.90 + minhr) {if (subZoneLast!=9) {subZoneLast=9; getZone(subZoneLast);}} // Z4c
|
|
|
|
|
else if (hr <= hrr * 0.94 + minhr) {if (subZoneLast!=10) {subZoneLast=10; getZone(subZoneLast);}} // Z5a
|
|
|
|
|
else if (hr <= hrr * 0.96 + minhr) {if (subZoneLast!=11) {subZoneLast=11; getZone(subZoneLast);}} // Z5b
|
|
|
|
|
else if (hr <= hrr * 0.98 + minhr) {if (subZoneLast!=12) {subZoneLast=12; getZone(subZoneLast);}} // Z5c
|
|
|
|
|
else if (hr >= maxhr - 2) {subZoneLast=13; g.clear();getZoneAlert();} // Alert
|
|
|
|
|
if ((hr < maxhr - 2) && subZoneLast==13) { drawZoneUI(true); } // Reset UI when coming down from zone alert.
|
|
|
|
|
if (hr <= hrr * 0.6 + minhr) {if (subZoneLast!=0) {subZoneLast=0; drawZone(subZoneLast);}} // Z1
|
|
|
|
|
else if (hr <= hrr * 0.64 + minhr) {if (subZoneLast!=1) {subZoneLast=1; drawZone(subZoneLast);}} // Z2a
|
|
|
|
|
else if (hr <= hrr * 0.67 + minhr) {if (subZoneLast!=2) {subZoneLast=2; drawZone(subZoneLast);}} // Z2b
|
|
|
|
|
else if (hr <= hrr * 0.70 + minhr) {if (subZoneLast!=3) {subZoneLast=3; drawZone(subZoneLast);}} // Z2c
|
|
|
|
|
else if (hr <= hrr * 0.74 + minhr) {if (subZoneLast!=4) {subZoneLast=4; drawZone(subZoneLast);}} // Z3a
|
|
|
|
|
else if (hr <= hrr * 0.77 + minhr) {if (subZoneLast!=5) {subZoneLast=5; drawZone(subZoneLast);}} // Z3b
|
|
|
|
|
else if (hr <= hrr * 0.80 + minhr) {if (subZoneLast!=6) {subZoneLast=6; drawZone(subZoneLast);}} // Z3c
|
|
|
|
|
else if (hr <= hrr * 0.84 + minhr) {if (subZoneLast!=7) {subZoneLast=7; drawZone(subZoneLast);}} // Z4a
|
|
|
|
|
else if (hr <= hrr * 0.87 + minhr) {if (subZoneLast!=8) {subZoneLast=8; drawZone(subZoneLast);}} // Z4b
|
|
|
|
|
else if (hr <= hrr * 0.90 + minhr) {if (subZoneLast!=9) {subZoneLast=9; drawZone(subZoneLast);}} // Z4c
|
|
|
|
|
else if (hr <= hrr * 0.94 + minhr) {if (subZoneLast!=10) {subZoneLast=10; drawZone(subZoneLast);}} // Z5a
|
|
|
|
|
else if (hr <= hrr * 0.96 + minhr) {if (subZoneLast!=11) {subZoneLast=11; drawZone(subZoneLast);}} // Z5b
|
|
|
|
|
else if (hr <= hrr * 0.98 + minhr) {if (subZoneLast!=12) {subZoneLast=12; drawZone(subZoneLast);}} // Z5c
|
|
|
|
|
else if (hr >= maxhr - 2) {subZoneLast=13; drawZoneAlert();} // Alert
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initDraw() {
|
|
|
|
|
drawArrows();
|
|
|
|
|
if (hr!=0) updateUI(true); else {drawWaitHR(); drawBgArc();}
|
|
|
|
|
//drawZones();
|
|
|
|
|
let karvonenInterval;
|
|
|
|
|
let hrmstat;
|
|
|
|
|
|
|
|
|
|
function drawLockIndicator() {
|
|
|
|
|
if (Bangle.isLocked())
|
|
|
|
|
g.setColor(g.theme.fg).drawImage(ICON_LOCK, 6, 8);
|
|
|
|
|
else
|
|
|
|
|
g.setColor(g.theme.bg).drawImage(ICON_LOCK, 6, 8);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let gpsTimeout;
|
|
|
|
|
let lastGps;
|
|
|
|
|
function drawGpsIndicator(e) {
|
|
|
|
|
if (e && e.fix){
|
|
|
|
|
if (gpsTimeout)
|
|
|
|
|
clearTimeout(gpsTimeout);
|
|
|
|
|
g.setColor(g.theme.fg).drawImage(ICON_LOCATION, 8, R.y2 - 23);
|
|
|
|
|
lastGps = 0;
|
|
|
|
|
gpsTimeout = setTimeout(()=>{
|
|
|
|
|
g.setColor(g.theme.dark?"#ccc":"#444").drawImage(ICON_LOCATION, 8, R.y2 - 23);
|
|
|
|
|
lastGps = 1;
|
|
|
|
|
gpsTimeout = setTimeout(()=>{
|
|
|
|
|
g.setColor(g.theme.bg).drawImage(ICON_LOCATION, 8, R.y2 - 23);
|
|
|
|
|
lastGps = 2;
|
|
|
|
|
}, 3900);
|
|
|
|
|
}, 1100);
|
|
|
|
|
} else if (lastGps !== undefined){
|
|
|
|
|
switch (lastGps) {
|
|
|
|
|
case 0: g.setColor(g.theme.fg).drawImage(ICON_LOCATION, 8, R.y2 - 23); break;
|
|
|
|
|
case 1: g.setColor(g.theme.dark?"#ccc":"#444").drawImage(ICON_LOCATION, 8, R.y2 - 23); break;
|
|
|
|
|
case 2: g.setColor(g.theme.bg).drawImage(ICON_LOCATION, 8, R.y2 - 23); break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let pulseTimeout;
|
|
|
|
|
function drawPulseIndicator() {
|
|
|
|
|
if (hr){
|
|
|
|
|
if (pulseTimeout)
|
|
|
|
|
clearTimeout(pulseTimeout);
|
|
|
|
|
g.setColor(g.theme.fg).drawImage(ICON_HEART, R.x2 - 21, R.y2 - 21);
|
|
|
|
|
pulseTimeout = setTimeout(()=>{
|
|
|
|
|
g.setColor(g.theme.dark?"#ccc":"#444").drawImage(ICON_HEART, R.x2 - 21, R.y2 - 21);
|
|
|
|
|
pulseTimeout = setTimeout(()=>{
|
|
|
|
|
g.setColor(g.theme.bg).drawImage(ICON_HEART, R.x2 - 21, R.y2 - 21);
|
|
|
|
|
}, 3900);
|
|
|
|
|
}, 1100);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function init(hrmSettings, exsHrmStats) {
|
|
|
|
|
R = Bangle.appRect;
|
|
|
|
|
hrmstat = exsHrmStats;
|
|
|
|
|
hr = hrmstat.getValue();
|
|
|
|
|
minhr = hrmSettings.min;
|
|
|
|
|
maxhr = hrmSettings.max;
|
|
|
|
|
calculatehrr(minhr, maxhr);
|
|
|
|
|
|
|
|
|
|
centreX = R.x + 0.5 * R.w;
|
|
|
|
|
centreY = R.y + 0.5 * R.h;
|
|
|
|
|
minRadius = 0.38 * R.h;
|
|
|
|
|
maxRadius = 0.50 * R.h;
|
|
|
|
|
|
|
|
|
|
minRadiusz = 0.44 * R.h;
|
|
|
|
|
startAngle = HRzones.degreesToRadians(-88.5);
|
|
|
|
|
endAngle = HRzones.degreesToRadians(268.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function start(hrmSettings, exsHrmStats) {
|
|
|
|
|
wu.hide();
|
|
|
|
|
init(hrmSettings, exsHrmStats);
|
|
|
|
|
|
|
|
|
|
g.reset().clearRect(R).setFontAlign(0,0,0);
|
|
|
|
|
|
|
|
|
|
Bangle.on("lock", drawLockIndicator);
|
|
|
|
|
Bangle.on("HRM", drawPulseIndicator);
|
|
|
|
|
Bangle.on("HRM", updateUI);
|
|
|
|
|
Bangle.on("GPS", drawGpsIndicator);
|
|
|
|
|
|
|
|
|
|
setTimeout(updateUI,0,true);
|
|
|
|
|
}
|
|
|
|
|
let waitTimeout;
|
|
|
|
|
let hrLast;
|
|
|
|
|
//h = 0; // Used to force hr update via web ui console field to trigger draws, together with `if (h!=0) hr = h;` below.
|
|
|
|
|
function updateUI(resetHrLast) { // Update UI, only draw if warranted by change in HR.
|
|
|
|
|
hrLast = resetHrLast?0:hr; // Handles correct updating on init depending on if we've got HRM readings yet or not.
|
|
|
|
|
hr = exsHrmStats.getValue();
|
|
|
|
|
//if (h!=0) hr = h;
|
|
|
|
|
if (hr!=hrLast) {
|
|
|
|
|
drawHR();
|
|
|
|
|
drawZones();
|
|
|
|
|
} //g.setColor(g.theme.fg).drawLine(175/2,0,175/2,175).drawLine(0,175/2,175,175/2); // Used to align UI elements.
|
|
|
|
|
if (resetHrLast){
|
|
|
|
|
hrLast = 0; // Handles correct updating on init depending on if we've got HRM readings yet or not.
|
|
|
|
|
subZoneLast = undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initDraw();
|
|
|
|
|
if (waitTimeout) clearTimeout(waitTimeout);
|
|
|
|
|
waitTimeout = setTimeout(() => {drawWaitUI(); waitTimeout = undefined;}, 2000);
|
|
|
|
|
|
|
|
|
|
// check for updates every second.
|
|
|
|
|
karvonenInterval = setInterval(function() {
|
|
|
|
|
if (!isMenuDisplayed && karvonenActive) updateUI();
|
|
|
|
|
}, 1000);
|
|
|
|
|
hr = hrmstat.getValue();
|
|
|
|
|
//if (h!=0) hr = h;
|
|
|
|
|
|
|
|
|
|
return karvonenInterval;
|
|
|
|
|
})
|
|
|
|
|
if (hrLast != hr || resetHrLast){
|
|
|
|
|
if (hr && hrLast != hr){
|
|
|
|
|
drawZoneUI(true);
|
|
|
|
|
} else if (!hr)
|
|
|
|
|
drawWaitUI();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hrLast = hr;
|
|
|
|
|
//g.setColor(g.theme.fg).drawLine(175/2,0,175/2,175).drawLine(0,175/2,175,175/2); // Used to align UI elements.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stop(){
|
|
|
|
|
if (karvonenInterval) clearInterval(karvonenInterval);
|
|
|
|
|
karvonenInterval = undefined;
|
|
|
|
|
if (pulseTimeout) clearTimeout(pulseTimeout);
|
|
|
|
|
pulseTimeout = undefined;
|
|
|
|
|
if (gpsTimeout) clearTimeout(gpsTimeout);
|
|
|
|
|
gpsTimeout = undefined;
|
|
|
|
|
if (waitTimeout) clearTimeout(waitTimeout);
|
|
|
|
|
waitTimeout = undefined;
|
|
|
|
|
Bangle.removeListener("lock", drawLockIndicator);
|
|
|
|
|
Bangle.removeListener("GPS", drawGpsIndicator);
|
|
|
|
|
Bangle.removeListener("HRM", drawPulseIndicator);
|
|
|
|
|
Bangle.removeListener("HRM", updateUI);
|
|
|
|
|
lastGps = undefined;
|
|
|
|
|
wu.show();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
exports.start = start;
|
|
|
|
|
exports.stop = stop;
|