Help for writing an app (running zones using Karvonnen method)

Posted on
Page
of 3
/ 3
Last Next
  • Yop, I just had a few javascript classes and decided to write the app I'm waiting for :) ! Unfortunately, I have lots to learn.
    I would like to implement my code as an extra feature of the run.app. It would be called run+
    The missing features for which I need help are:

    • to have it running in the run.app as an extra screen, accessible by swipe right/swipe left.
    • the hr input is not tied to hrm
    • the minimum and maximum heart rate have to be set up in the settings menu (so that other people can use it).
    • Also, the refresh rate, for the HR (in the middle of the screen) has to be every second, but the refresh rate for the graphics could be every 4 seconds, to save memory.
      Here's the thing:

      //This app is an extra feature implementation for the Run.app of the bangle.js. It's called run+
      //I plan to have it running in the run.app, as an extra screen, accessible by swipe right/swipe left.
      //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();
      g.drawLine(44,58,88,40);
      g.drawLine(88,40,132,58);
      g.drawLine(44,116,88,134);
      g.drawLine(88,134,132,116);
      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 = 176;
      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 in the middle of the screen
      g.setFont("Vector",46);
      g.drawString(hr1, 72,66);
      //these functions call background images (>6kb each) that show HRzones graphically. Flash was too memory hungry, has to be uploaded in storage.
      function getzone1() {
      return
      require("heatshrink").decompress(atob("y­OR4cA///gEB/8H0EAkEBsARCkAsZzMIhMkyVIL9k­DGIVJwIDCAQI0pgguDAQI1EAQOAGkpoDAQeEAwqn­lhArFAQOQGoxukiQrHzAJIyVAakwCCycCGpACBGr­woJMII1KyRqdFBDMChI1KpI1bFBAMDgI1LkhGCGr­6uQUjiONgg1MUjEID4tAB441NQIwAQgQcEyAPIiQ­1NUiw1Fe5IPFARNIGqlCDgcwdSikaGokgCp6nLXp­IfNpgXSNxVAGtLgLGqrxVgLaaGrIABbTKIDXCQAF­hJuHDKY1YgA1HjahPDIOTGjDAFAQUa3gWOagI1bU­g0eyXwJh0ggY1bUgo1BpfAQR2AGrkAGotJm4mLoQ­TCGrsAGoskUZZrCa7gACgJXCuQmCNhUCJIVAGz0E­GoMPEwVOGtqQBAQM9EwNJvg1tAAfMGoMnUZA1ogc­yFIOcBpA1CpA1jgEMFIX4GpVgGskAnIpBp4LHhJr­ngEHsjaBnA1wgEZGoMm+A1Ipg1mgFyZwOcBIsBGo­NJGs8O5IsB/A1wgExFgNPBIo1CkA2o0gsBvg1xjI­sBk+ABAcEBAMkGtEAuQsB3wHDiA1sh3JkmTNglCG­oIHEAEs5kmX+AIEkI1rg9k3vAA4cSgECiA1pgEe9­0AwgGCgVJNVQABgeAgKbDGoMkOYgAngMJkmYAwQ1­ByY1rF4WSoAFBHYIFDNlQvBpA1EAoQAqF4NJkA7G­AFY1Bkg7EGtsSGoOAgEEGoIEBAFcCGoOQGoanBAF­kJkmTU4wArgLUBpCnFAFg1BpKnD4A1tagbdEAFqe­DAQOSGtyeDhI1BoA2uNAUBAQNIGtxoDAQNMGt0EN­AUEyVJwA2uGoNJiACBkA1ugAyBmACBkg1viQyB4Q­CBUV8CGomQNl4yBUIWSGt8JkmTAQOSoA1ugJrCyG­SpBsvNYMhGV5wIGWFJAQKkBG2ESGQI1CwA1ugQ1E­yCiwGoeSGt8JGolAGt0BGolINl4yBpmEHAI1vggy­BwACBmA1xkESpMmGt0CGQMkgQCByA2uGoWAAQOSN­l5oChI1BoA2uNAUBAQNIGtxoDgmSpOAGuMAGoMkG­tqeEiQ1BNlw1BpMAgQ1ByA1tagUgU4WTGtsQGoan­EAFg1BkinFAFkSGoOAOIY7CGtvAHYwAqgQvByAFE­HYQApgMJagIGCAoJrsAAOEGocBmEAhA0rkCeBoB0­FBIIApkMkcBDYrkmZA4kSGoOQGteSpIIEGoLgDAE­41HhIIBpA1skAINAEeEFg8EBAMkGtAsJiQIByA1n­iA1IgQIByRsoGoWABIsJGoNIGuMBGoLjGAEECZwX­AcaAAgGoWQBaY1pgQLByVAGs+YBhEJbQQ1koRfCB­pI1CbUhrCGpUQGoXANczLKIhoAYgQ1NgAOCpA1xg­I1kgEJGpsAwg1CwBrggBcPBwRHNACVCEYVgCJkIC­IVJGr9JkECGpo2BGoUwGrsSpMkwEgCh41BkmQGr+­SdaAUDbTggDUJyjCCgS5BGrSMCyT+BAB8JGoS5BG­rprQAAMEC4WTGjEQGqy5EpJtXwg1DfCg1DUi4aDR­KsBDTMEDTLyFpMgDCUJGoeSGq0CGocmDCUhGrakF­wBuPgUSoAXDyA1XYAIdC4CPCChmEyVIiAXDGrCkD­yDFDwgSJdgkSXKikLU4mSpgPFMoYCCJoQ1bAAOSY­ogCEwRiBBYzsQACA1JmA1IQAI0eAAMEGo8kwQ1Io­A1ggDFCFguQGpA0hAAQsHhIIFkw0kAAMIGomTgg1­FwA1mbo1gwg7DGdAAEyGSpkQAQJoYA=")) ;
      }
      
      function getzone2a() {
      return
      require("heatshrink").decompress(atob("y­OR4cA///gEg8E4kEh/8HgkggEB8EHFa+27dtiVJk­mAL9lbto1C0Q1ByQCBGlIyCAQW0GomSpA0mlo1F2­2JGoinmhYyEAQXRGoxujgQyFAQXaGo5uiiw1I7dE­GpGSoA1eGQ4CCoA1JpMgGjkFGpCwGHA+QGrctGo3­QCRJuGpA0ZgJoG2gULhJuGGr9oCxykehY1E7QXQN­wqkXGorUKAA8ENwkgGqtbGomADKUCUjQ1E2jyVUg­lAGrGgXyykEGrA0WAAKkEGq1oGrCkFGt6kFpA1Ue­CgAGgSkDkAVPhY1eUgg1QwEtGr0Agg1Cj+AW50AG­r8AiQ1C/wSOpECGr8AhI1B//4JB8LGr8AGoX/8A1­NkizOACY1C/4mLGoTaBGsnwGpyhgGon/4A10/
      

    1 Attachment

    • signal-2023-01-20-121526.jpg
  • The whole code doesn't fit in the post because of the pics. I'm going to attach the whole code to this post (I modified to .odt, so that there's no security issue with posting it). Still, the end of the code (after the pictures) is here:

    g.setFont("Vector",20);
    //Subdivided zones for better readability of zones when calling the background images. //Changing HR zones will trigger the change of the background image and the HR to change zone.
    
    if (hr <= minhr) {
      console.log("HR too low");
    } else if (hr <= hrr*0.6 + minhr) {
      g.drawImage(getzone1(),0,0,{scale:1.2});­g.drawString("Z1", 32,78);g.drawString(minzone2, 62,20);
    } else if (hr <= hrr*0.64 + minhr) {
      g.drawImage(getzone2a(),0,0,{scale:1.2})­;g.drawString("Z2", 32,78);g.drawString(maxzone2, 62,20);g.drawString(minzone2, 62,136);
    } else if (hr <= hrr*0.67+minhr) {
      g.drawImage(getzone2b(),0,0,{scale:1.2})­;g.drawString("Z2", 32,78);g.drawString(maxzone2, 62,20);g.drawString(minzone2, 62,136);
    } else if (hr <= hrr * 0.7 + minhr) {
     g.drawImage(getzone2c(),0,0,{scale:1.2})­;g.drawString("Z2", 32,78);g.drawString(maxzone2, 62,20);g.drawString(minzone2, 62,136);
    } else if (hr <= hrr * 0.74 + minhr) {
      g.drawImage(getzone3a(),0,0,{scale:1.2})­;g.drawString("Z3", 32,78);g.drawString(maxzone3, 62,20);g.drawString(maxzone2, 62,136);
    } else if (hr <= hrr * 0.77 + minhr) {
      g.drawImage(getzone3b(),0,0,{scale:1.2})­;g.drawString("Z3", 32,78);g.drawString(maxzone3, 62,20);g.drawString(maxzone2, 62,136);
    } else if (hr <= hrr * 0.8 + minhr) {
      g.drawImage(getzone3c(),0,0,{scale:1.2})­;g.drawString("Z3", 32,78);g.drawString(maxzone3, 62,20);g.drawString(maxzone2, 62,136);
    } else if (hr <= hrr * 0.84 + minhr) {
      g.drawImage(getzone4a(),0,0,{scale:1.2})­;g.drawString("Z4", 32,78);g.drawString(maxzone4, 62,20);g.drawString(maxzone3, 62,136);
    } else if (hr <= hrr * 0.87 + minhr) {
      g.drawImage(getzone4b(),0,0,{scale:1.2})­;g.drawString("Z4", 32,78);g.drawString(maxzone4, 62,20);g.drawString(maxzone3, 62,136);
    } else if (hr <= hrr * 0.9 + minhr) {
      g.drawImage(getzone4c(),0,0,{scale:1.2})­;g.drawString("Z4", 32,78);g.drawString(maxzone4, 62,20);g.drawString(maxzone3, 62,136);
    } else if (hr <= hrr * 0.94 + minhr) {
      g.drawImage(getzone5a(),0,0,{scale:1.2})­;g.drawString("Z5", 32,78);g.drawString(maxzone5, 62,20);g.drawString(maxzone4, 62,136);
    } else if (hr <= hrr * 0.96 + minhr) {
      g.drawImage(getzone5b(),0,0,{scale:1.2})­;g.drawString("Z5", 32,78);g.drawString(maxzone5, 62,20);g.drawString(maxzone4, 62,136);
    } else if (hr <= hrr * 0.98 + minhr) {
      g.drawImage(getzone5c(),0,0,{scale:1.2})­;g.drawString("Z5", 32,78);g.drawString(maxzone5, 62,20);g.drawString(maxzone4, 62,136);
    } else if (hr >= maxhr - 2) {
      g.clear();g.drawImage(getzonealert(),0,0­,{scale:1.2});g.setFont("Vector",38);g.d­rawString("ALERT", 20,53);g.drawString("HR limit", 18,84);
    }
    

    1 Attachment

  • ok, I think we should concentrate on the swipe switch layout thing first to get your code on github as soon as possible. It is better readable there.

    These are the first steps I recommend:

    1. Read http://www.espruino.com/Bangle.js+App+Lo­ader on how to fork and locally checkout the Bangle Repository.
    2. Place your code in a file inside apps/run/ in a file named for example karvonnen.js and upload it to github.

    Once this is done we can add http://www.espruino.com/Reference#l_Bang­le_swipe to listen to swipes to switch layout.

  • That looks great! One thing I'd say is maybe rather than using images, you could just draw the individual zones as polygons. There's no function built in but it's easy enough to do. For instance:

    const R = Bangle.appRect, CX = R.x+R.w/2, CY = R.y+R.h/2, CR = (R.h/2)-8;
    
    g.drawSlice = function(pa, pb, size) {
      pa = (pa+0.05)*Math.PI/8;
      pb = (pb-0.05)*Math.PI/8;
      let a = (pb-pa)/6.01;
      let poly = [CX,CY], ir = CR-size;
      for (let r = pa; r <= pb; r += a) {
        poly.unshift(
          CX + ir * Math.sin(r),
          CY - ir * Math.cos(r)
        );
        poly.push(
          CX + CR * Math.sin(r),
          CY - CR * Math.cos(r)
        );
      }
      return this.fillPoly(poly);
    }
    
    g.clear();
    
    // top 2 big slices
    g.setColor(E.HSBtoRGB(0,1,1,16)).drawSli­ce(0,2, 10);
    g.setColor(E.HSBtoRGB(0.9,1,1,16)).drawS­lice(14,16, 10);
    // other slices
    for (var i=2;i<14;i++) {
      g.setColor(E.HSBtoRGB(i/16,1,1,16)).draw­Slice(i,i+1, 10+Math.pow(Math.random(),4)*20);
    

    It'd likely be faster, would save a bunch of memory, and is a bit more flexible too as you can choose exactly how big and what color each slice is.


    1 Attachment

    • ex_zones.png
  • This could also use fillArc() from graphics_utils lib for drawing the segments.

  • Thanks a bunch guys ! Waow, that's incredible what you can do with code. Basically, I can decypher 20% of Gordon's code, the rest is like 3d hieroglyphs.
    fillArc() seems closer to my abilities, but still, quite a huge gap to fill (starting with figuring out how sin, cos, radians work...)
    It took me 4 days to write this code and do the graphics, which lead to running late on major projects (studies, moving interstate,...). I need 2 weeks to catch up, at least.
    If you feel like finishing that thing, feel free to use the concept and the code I posted (if there's anything worth keeping).
    If it's just me, I will just use the heavy weight code whenever I have time, as I can't do any better.
    Still, thanks ! Now I see what code wizardry can do !

  • You can just use the library to draw an arc like this:

    g.clear();
    const GU = require("graphics_utils");
    let centreX = 0.5 * g.getWidth();
    let centreY = 0.5 * g.getWidth();
    let startAngle = GU.degreesToRadians(20);
    let endAngle = GU.degreesToRadians(135);
    let minRadius = 0.3 * g.getWidth();
    let maxRadius = 0.45 * g.getWidth();
    GU.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle, endAngle);
    

    Gordons code does very similar things by constructing a polygon that approximates the arc.


    1 Attachment

    • arc.png
  • I'd forgotten about require("graphics_utils"); - that'd be much easier! :)

  • Hello guys,
    my app is on github, finally: https://github.com/f-teacher/karvonnen . Feel free to do what's needed to have it run. You can reuse and publish, just make it work, please !

  • Also, I tried to send Halemmerich's code in the watch and in the emulator with the same result: module graphics_utils not found. I don't know what I'm doing wrong there.

  • Check this comment out :) Or more specifically this readme.

  • Thank you @Ganblejs. Now that I've followed these instructions, I can make it work. I'm restarting the creative (but so tedious for beginners) process of coding.

  • Ok, I've done something cleaner thanks to your comments :).
    I'm now using the require("graphics_utils") , very efficient. I reduced the number of lines to 109 and I made the code a bit less repetitive thanks to some functions. It's definitively much faster. It's in Github.
    I'm still up for advices or help. I checked the swipe and I don't get how the swipe would take me from the running run.js to karvonnen.js (or if I have to merge it in one app). Also the HR input and the settings menu are not done at all. Some improvements are needed on the new graphical layout, that's the most doable part for me.
    Thanks again for your help and encouragement.

  • I checked the swipe and I don't get how the swipe would take me from the running run.js to karvonnen.js (or if I have to merge it in one app).

    With regards to separate files vs. merging in your code in run.app.js both approaches are doable. EDIT: But after considering your specification in the OP I think you should try to merge into one file.

    I think it may be easiest to define a function handling swipe events in the run app, I usually call them swipeHandler, where you call load('karvonnen.js'). Then doing Bangle.on('swipe', swipeHandler) to make the Bangle listen for swipes and execute the function.

    Check out the Espruino Hardware Reference for load(), event swipe, and maybe Bangle.setUI().

    If you want to swipe between the regular run app and korvonnen frequently it's maybe better to implement it as one (or several) functions to call inside run.app.js. If you want the run app to work simultaneously as well this will probably be the way to go. EDIT: I believe this is what you want.

  • Why do you want to implement it inside the run app? Wouldn't it be easier to make it a separate app? I think the functionality could stand on it's own when it's done.

    That way you also get to practice the basics of creating the required metadata, settings.js and icons etc, utilizing the tutorials along the way :)

  • I'm going to describe what I need. I usually run 50 to 80 minutes. I'm almost satisfied with the run app's live data (HR, duration, distance, avg speed) and being able to extract a gpx after the run is critical to keep track of my runs. Basically, the run app covers the performance and the Karvonnen would allow me to check the intensity of my effort. So I need the Karvonnen app for most of my run and the run app just a few times during the run to check how I'm doing in terms of performance. Also, as soon as I'm running faster (increasing the intensity), I struggle to read the small numbers of the run app, I needed something bigger. For my personnal use, I will add a buzz when changing HR zone. I definitively want to swipe between run and Karvonnen and have the benefits of both.
    I'm very happy to learn coding and I'm glad I have help to know where to look to implement what I need. Still, I have little spare time for coding and the process is very long for me. It took me 10 hours to get this code running. Now that I think about it, I think I should have written it differently... I will restart the process in a few weeks.

  • I definitively want to swipe between run and Karvonnen and have the benefits of both.

    Ok!

    I started merging the code here just to show how it can be done, it's not perfect in any way :) There are some intervals in the run app code that must be stopped (or modified, I don't really understand the code yet :P) and started again when switching UI, and there are significant ram leaks right now.

    What works is swiping back and forth to display the different UI's.

    EDIT: Can be installed via my app loader.

  • @Fteacher, I like your comment

    I'm restarting the creative (but so tedious for beginners) process of coding.

    a lot, especially this part: ...so tedious for beginners

    I can tell you that without Espruino / @Gordon's platform, you would not even find a word in the whole universe to describe it...

    just saying.

    Enjoy the creative writing process... Espruino likes to read your stories and follow them to the tee... so watch what you are asking it for!

  • Every useful line of code has dozens to hundreds deleted ones standing behind it ;)

  • I have tweaked the merged code and it seems to work pretty well now! I am sure there are things that can be done better and bugs will be found. Some cleanup should be done as well. But I don't get large ram leaks at least, and the run app functionality runs in the background when the karvonnen UI is displayed. Use the links in my previous reply.

    EDIT: I did a pull request into your master branch in case you want to continue with the karvonnen code accessible from app.js :)

  • Good job, just one question: Wouldn't it be better to jeep the code in a separate file for a.) clarity and b.) Easier extensibility in case of an alternative "zone-method"?

    oh another question, was this tested on Bangle 1 & 2?

  • Wouldn't it be better to jeep the code in a separate file for a.) clarity and b.) Easier extensibility in case of an alternative "zone-method"?

    I agree with both your points, I just didn't know how to achieve both having them run concurrently and have them in separate files. Maybe have them in different files on the app loader but require karvonnen into run.app.js when loading it to the clock? The Karvonnen code is pretty much intact so something like that could maybe work?

    was this tested on Bangle 1 & 2?

    Unfortunately no, only on my Bangle 2. I just checked ram usage, and it seems to be higher than before by a value of 100 across the different screens, with peak 3444 (27% of full RAM on Bangle 2) when the app asks to overwrite or start a new log.

    EDIT: Could it be problematic somehow that I changed all 'var' declarations to 'let' declarations? I did it as part of tracking down RAM leaks, but don't know if it did any difference or not.

  • Separation: I was aiming for something simple, like putting everything in karvonnen.js, adding this to metadata

        {
          "name": "runkarvonnen",
          "url": "karvonnen.js"
        },
    

    and loading it in app.js:

    function swipeHandler(LR,_) {
      if (LR==-1 && karvonnenActive && !isMenuDisplayed) run();
      if (LR==1 && !karvonnenActive && !isMenuDisplayed) require("runkarvonnen").karvonnen();
    }
    

    see attached run.diff.txt

    Bangle.js 1: I have never used the run app, but if wanted I might be able to test later together with https://banglejs.com/apps/?id=sensortool­s as gps provider.


    1 Attachment

  • Thanks! I have done changes like you suggested and it works.

    I used naming like for the modules (run_karvonnen.js and run_karvonnen), should I use the name you suggested instead as this is maybe not a module in the same sense as the ones in the module folder?

    Use the same links as before. I rebased the code on my app loader on the PR to f-teacher:master.

  • I'm not sure here (maybe this is for gordon?), for me at is ok as long as the name starts with "run" so in the storage the new file is sorted near all the other run files.

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

Help for writing an app (running zones using Karvonnen method)

Posted by Avatar for Fteacher @Fteacher

Actions