diff --git a/apps/run/run.app.karvonnen.js b/apps/run/run.app.karvonnen.js index 8d6918a4e..8dabfe60d 100644 --- a/apps/run/run.app.karvonnen.js +++ b/apps/run/run.app.karvonnen.js @@ -1,28 +1,120 @@ -Modules.addCached("Layout",ª(){ªp(b,k){ªd(h){h.idž(f[h.id]=h);h.type (h.type="");h.cžh.c.forEach(d)}¯._l=¯.l=b;¯.physBtns=2Šprocess.env.HWVERSION?1:3;¯.options=k {};¯.lazy=¯.options.lazy !1;­a;£(2Œprocess.env.HWVERSION){a=[];ªh(m){"btn"Šm.typeža.push(m);m.cžm.c.forEach(h)}h(b);a.lengthž(¯.physBtns=0,¯.buttons=a,¯.selectedButton=-1)}£(¯.options.btns)£(b=¯.options.btns,¯.physBtns‘b.length){¯.b=b;­h=Math.floor(Bangle.appRect.h/¯.physBtns);§(2<¯.physBtnsž1Šb.lengthžb.unshift({label:""});¯.physBtns>b.length;)b.push({label:""});¯._l.width=g.getWidth()-8;¯._l={type:"h",filly:1,c:[¯._l,{type:"v",pad:1,filly:1,c:b.map(m¢(m.type="txt",m.font="6x8",m.height=h,m.r=1,m))}]}}¤¯._l.width=g.getWidth()-32,¯._l={type:"h",c:[¯._l,{type:"v",c:b.map(h¢(h.type="btn",h.filly=1,h.width=32,h.r=1,h))}]},aža.push.apply(a,¯._l.c[1].c);¯.setUI();¬f=¯;d(¯._l);¯.updateNeeded=!0}ªr(b,k,d,a,f){¬h=¶Šb.bgCol?f:g.toColor(b.bgCol);£(hŒf "txt"Šb.type "btn"Šb.type "img"Šb.type "custom"Šb.type){¬m=b.c;¾b.c;¬c="H"+E.CRC32(E.toJS(b));mž(b.c=m);¾k[c] ((a[c]=[b.x,b.y,b.x+b.w-1,b.y+b.h-1]).bg=¶Šf?g.theme.bg:f,dž(d.push(b),d=¶))}£(b.c)§(¬lÆb.c)r(l,k,d,a,h)}p.prototype.setUI=ª(){Bangle.setUI();­b;¯.buttonsž(Bangle.setUI({mode:"updown",back:¯.options.back},k¢{¬d=¯.selectedButton,a=¯.buttons.length;£(À0‹kž¯.buttons[d])«¯.buttons[d].cb();¯.buttons[d]ž(¾¯.buttons[d].selected,¯.render(¯.buttons[d]));d=(d+a+k)%a;¯.buttons[d]ž(¯.buttons[d].selected=1,¯.render(¯.buttons[d]));¯.selectedButton=d}),b=!0);¯.options.backž!bžBangle.setUI({mode:"custom",back:¯.options.back});£(¯.b){ªk(d,a){.750žstate.notify.dist.nextŽstate.distance){stats["dist"].emit("notify",stats["dist"]);state.notify.dist.next=state.notify.dist.next+state.notify.dist.increment;}});Bangle.on("step",ª(steps){£(!state.active)«;£(stats["step"])stats["step"].emit("changed",stats["step"]);state.stepHistory[0]–steps-state.lastStepCount;state.lastStepCount=steps;£(state.notify.step.increment>0žstate.notify.step.nextŽsteps){stats["step"].emit("notify",stats["step"]);state.notify.step.next=state.notify.step.next+state.notify.step.increment;}});Bangle.on("HRM",ª(h){£(h.confidence‘60){state.BPM=h.bpm;state.BPMage=0;£(state.maxBPM60){state.BPM=0;£(stats["bpm"])stats["bpm"].emit("changed",stats["bpm"]);}£(state.notify.time.increment>0žstate.notify.time.nextŽnow){stats["time"].emit("notify",stats["time"]);state.notify.time.next=state.notify.time.next+state.notify.time.increment;}},1000);ªreset(){state.startTime=Date.now();state.startSteps=state.lastSteps=Bangle.getStepCount();state.lastSteps=0;state.stepHistory.fill(0);state.stepsPerMin=0;state.distance=0;state.avrSpeed=0;state.curSpeed=0;state.BPM=0;state.BPMage=0;state.maxBPM=0;state.alt=·;state.alti=0;state.notify=options.notify;£(options.notify.dist.increment>0){state.notify.dist.next=state.distance+options.notify.dist.increment;}£(options.notify.step.increment>0){state.notify.step.next=state.startSteps+options.notify.step.increment;}£(options.notify.time.increment>0){state.notify.time.next=state.startTime+options.notify.time.increment;}}reset();«{stats:stats,state:state,start:ª(){state.active=´;reset();},stop:ª(){state.active=µ;}};};exports.appendMenuItems=ª(menu,settings,saveSettings){¬paceNames=["1000m","1 mile","1/2 Mthn","Marathon",];¬paceAmts=[1000,1609,21098,42195];menu['Pace']={min:0,max:paceNames.length-1,value:Math.max(paceAmts.indexOf(settings.paceLength),0),format:v¢paceNames[v],onchange:v¢{settings.paceLength=paceAmts[v];saveSettings();},};}exports.appendNotifyMenuItems=ª(menu,settings,saveSettings){¬distNames=['Off',"1000m","1 mile","1/2 Mthn","Marathon",];¬distAmts=[0,1000,1609,21098,42195];menu['Ntfy Dist']={min:0,max:distNames.length-1,value:Math.max(distAmts.indexOf(settings.notify.dist.increment),0),format:v¢distNames[v],onchange:v¢{settings.notify.dist.increment=distAmts[v];saveSettings();},};¬stepNames=['Off','100','500','1000','5000','10000'];¬stepAmts=[0,100,500,1000,5000,10000];menu['Ntfy Steps']={min:0,max:stepNames.length-1,value:Math.max(stepAmts.indexOf(settings.notify.step.increment),0),format:v¢stepNames[v],onchange:v¢{settings.notify.step.increment=stepAmts[v];saveSettings();},};¬timeNames=['Off','30s','1min','2min','5min','10min','30min','1hr'];¬timeAmts=[0,30000,60000,120000,300000,600000,1800000,3600000];menu['Ntfy Time']={min:0,max:timeNames.length-1,value:Math.max(timeAmts.indexOf(settings.notify.time.increment),0),format:v¢timeNames[v],onchange:v¢{settings.notify.time.increment=timeAmts[v];saveSettings();},};};}); -¬ExStats=require("exstats"); -¬B2=process.env.HWVERSION‹2; -¬Layout=require("Layout"); -¬locale=require("locale"); -¬fontHeading="6x8:2"; -¬fontValue=B2?"6x15:2":"6x8:3"; -¬headingCol="#888"; -¬fixCount=0; -¬isMenuDisplayed=µ; +//The calculation of the Heart Rate Zones is based on the Karvonnen method. It requires to know maximum and minimum heart rates. More precise calculation methods require a lab. +//Other methods are even more approximative. g.clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -­settings=Object.assign({record:´,B1:"dist",B2:"time",B3:"pacea",B4:"bpm",B5:"step",B6:"caden",paceLength:1000,notify:{dist:{value:0,notifications:[],},step:{value:0,notifications:[],},time:{value:0,notifications:[],},},},require("Storage").readJSON("run.json",1) {}); -¬statIDs=[settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s¢s""); -¬exs=ExStats.getStats(statIDs,settings); -ªonStartStop(){¬running=!exs.state.active;¬prepPromises=[];£(settings.recordžWIDGETS["recorder"]){£(running){isMenuDisplayed=´;prepPromises.push(WIDGETS["recorder"].setRecording(´).then(()¢{isMenuDisplayed=µ;layout.setUI();layout.forgetLazyState();layout.render();}));}¤{prepPromises.push(WIDGETS["recorder"].setRecording(µ));}}£(!prepPromises.length)prepPromises.push(Promise.resolve());Promise.all(prepPromises).then(()¢{£(running){exs.start();}¤{exs.stop();}layout.button.label=running?"STOP":"START";layout.status.label=running?"RUN":"STOP";layout.status.bgCol=running?"#0f0":"#f00";layout.render();});} -¬lc=[]; -§(¬i=0;i0žexs.stats[statType]){configureNotification(exs.stats[statType]);}}); -Bangle.on("GPS",ª(fix){layout.gps.bgCol=fix.fix?"#0f0":"#f00";£(!fix.fix)«;£(fixCount˜‹0){Bangle.buzz();}}); -setInterval(ª(){layout.clock.label=locale.time(¸Date(),1);£(!isMenuDisplayed)layout.render();},1000); \ No newline at end of file +// ########### The settings section ? How does this work ? ############# +let settings = Object.assign({ + minhr: { + value: 0} + maxhr: { + value: 0} +}, require("Storage").readJSON("run.json", 1) || {}); +// ######## end of settings section + +// ########## listening to swipes ? How does this work ? ######### +//to go back and forth between Run and karvonnen +Bangle.on('swipe', function(directionLR, directionUD) { ... }); + +// ###### The app itself ############ +g.drawLine(40,64,88,52,136,64); +g.drawLine(88,52,136,64); +g.drawLine(40,112,88,124); +g.drawLine(88,124,132,112); +g.setFont("Vector",20); + +//To calculate Heart rate zones, we need to know the heart rate reserve (HRR) +// 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 = 48; +let maxhr = 187; + +function calculatehrr(minhr, maxhr) { + return maxhr - minhr;} + +//test input for hrr (it works). +let hrr = calculatehrr(minhr, maxhr); +console.log(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 = 174; +var hr1 = hr; +// These variables display next and previous HR zone. +//get the hrzones right. The calculation of the Heart rate zones here is based on the Karvonnen 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 +var minzone2 = hrr * 0.6 + minhr; +var maxzone2 = hrr * 0.7 + minhr; +var maxzone3 = hrr * 0.8 + minhr; +var maxzone4 = hrr * 0.9 + minhr; +var maxzone5 = hrr * 0.99 + minhr; + +// HR data: large, readable, in the middle of the screen +g.setFont("Vector",50); +g.drawString(hr1, 62,66); + +//To shorten the code, I'll reference some variables and reuse them. +let centreX = 0.5 * g.getWidth(); +let centreY = 0.5 * g.getWidth(); +let minRadius = 0.38 * g.getWidth(); +let maxRadius = 0.50 * g.getWidth(); + +//####### A function to simplify a bit the code ###### +function simplify (sA, eA, Z) { +const zone= require("graphics_utils"); +let startAngle = zone.degreesToRadians(sA); +let endAngle = zone.degreesToRadians(eA); +zone.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle, endAngle); +g.drawString(Z, 29,80);} + +//####### A function to simplify next&previous zones ###### +function zoning (max, min) { +g.drawString(max, 56,28);g.drawString(min, 60,128);} + +//Thenext functions call arcs to display different HR zones. +//draw background image (dithered green zones)(I will draw different zones in different dithered colors) + const HRzones= require("graphics_utils"); + let minRadiusz = 0.44 * g.getWidth(); + let startAngle = HRzones.degreesToRadians(-88.5); + let endAngle = HRzones.degreesToRadians(268.5); + g.setColor("#002200"); + HRzones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); + g.setFont("Vector",24); + +function getzone1() {g.setColor("#00ffff");{(simplify (-88.5, -20, "Z1"));}zoning(minzone2, minhr);} +function getzone2a() {g.setColor("#00ff00");{(simplify (-43.5, -21.5, "Z2"));}zoning(maxzone2, minzone2);} +function getzone2b() {g.setColor("#00ff00");{(simplify (-20, 1.5, "Z2"));}zoning(maxzone2, minzone2);} +function getzone2c() {g.setColor("#00ff00");{(simplify (3, 24, "Z2"));}zoning(maxzone2, minzone2);} +function getzone3a() {g.setColor("#ffff00");{(simplify (25.5, 46.5, "Z3"));}zoning(maxzone3, maxzone2);} +function getzone3b() {g.setColor("#ffff00");{(simplify (48, 69, "Z3"));}zoning(maxzone3, maxzone2);} +function getzone3c() {g.setColor("#ffff00");{(simplify (70.5, 91.5, "Z3"));}zoning(maxzone3, maxzone2);} +function getzone4a() {g.setColor("#ff8000");{(simplify (91, 114.5, "Z4"));}zoning(maxzone4, maxzone3);} +function getzone4b() {g.setColor("#ff8000");{(simplify (116, 137.5, "Z4"));}zoning(maxzone4, maxzone3);} +function getzone4c() {g.setColor("#ff8000");{(simplify (139, 160, "Z4"));}zoning(maxzone4, maxzone3);} +function getzone5a() {g.setColor("#ff0000");{(simplify (161.5, 182.5, "Z5"));}zoning(maxzone5, maxzone4);} +function getzone5b() {g.setColor("#ff0000");{(simplify (184, 205, "Z5"));}zoning(maxzone5, maxzone4);} +function getzone5c() {g.setColor("#ff0000");{(simplify (206.5, 227.5, "Z5"));}zoning(maxzone5, maxzone4);} + +function getzonealert() { + const HRzonemax = require("graphics_utils"); + var centreX1,centreY1,maxRadius1 = 1; + let minRadius = 0.40 * g.getWidth(); + 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);} +//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. + + if (hr <= hrr*0.6 + minhr) {(getzone1()); +} else if (hr <= hrr*0.64 + minhr) {(getzone2a()); +} else if (hr <= hrr*0.67+minhr) {(getzone2b()); +} else if (hr <= hrr * 0.7 + minhr) {(getzone2c()); +} else if (hr <= hrr * 0.74 + minhr) {(getzone3a()); +} else if (hr <= hrr * 0.77 + minhr) {(getzone3b()); +} else if (hr <= hrr * 0.8 + minhr) {(getzone3c()); +} else if (hr <= hrr * 0.84 + minhr) {(getzone4a()); +} else if (hr <= hrr * 0.87 + minhr) {(getzone4b()); +} else if (hr <= hrr * 0.9 + minhr) {(getzone4c()); +} else if (hr <= hrr * 0.94 + minhr) {(getzone5a()); +} else if (hr <= hrr * 0.96 + minhr) {(getzone5b()); +} else if (hr <= hrr * 0.98 + minhr) {(getzone5c()); +} else if (hr >= maxhr - 2) {g.clear();(getzonealert());}