Better Pedometer - HELP NEEDED!

Posted on
Page
of 12
  • Trying to understand your code example - have added questions as comments.

    // values for debug plotting
    var lastAccel = 120;      // BLUE
    var lastAccelFilt = 120;  // YELLOW
    var lastThresh = 120;     // RED
    
    // question: where do these values come from ? what does tap mean ?
    // question: is this a low pass 3Hz filter ?
    var filter_taps = new Int8Array([ -2, 4, 4, 1, -1, 0, 2, -3, -12, -13, 2, 24, 29, 6, -25, -33, -13, 10, 11, -1, 3, 29, 41, 4, -62, -89, -34, 62, 110, 62, -34, -89, -62, 4, 41, 29, 3, -1, 11, 10, -13, -33, -25, 6, 29, 24, 2, -13, -12, -3, 2, 0, -1, 1, 4, 4, -2 ]);
    
    // create a history buffer and populate it with the tap values ?
    var history = new Int8Array(filter_taps.length);
    
    // what units is this in ? 
    const stepCounterThresholdMin = 1500;
    const stepCounterAvr = 1;
    /// Theshold in filtered acceleration for detecting a step
    var stepCounterThreshold = stepCounterThresholdMin;
    
    /// has filtered acceleration passed stepCounterThresholdLow?
    var stepWasLow = false;
    
    
    function onAccel(a) {
    
      // question - onAccel is event driven, so events could be irregular, papers seem to suggest 20Hz sampling frequency
      // will this event driven approach provide regular sampling of approx 20Hz or will it be subject to CPU performance etc
    
    
      // scale to fit and clip
      var v = ((a.mag-1)*8192)>>5;
    
      // create a new Int8Array from the existing but starting from pos 1 of the existing history
      // this drops off index 0 and moves everything up, leaving the last entry to be filled
      // with the new value
      history.set(new Int8Array(history.buffer,1));
      // set last value to the clipped value from the accel
      history[history.length-1] = E.clip(v, -128, 127);
    
      console.log("history");
      console.log(history);
    
      
      // do filtering
      //  what is convolution ??
      //https://homepages.inf.ed.ac.uk/rbf/HIPR2/convolve.htm#:~:text=Convolution%20provides%20a%20way%20of,numbers%20of%20the%20same%20dimensionality.&text=In%20an%20image%20processing%20context,normally%20just%20a%20graylevel%20image.
    
    
      // question: - is this a low pass filter at 3Hz to get rid of noise ?
      // question: - why is accFiltered much larger than v , why >>2 to make it bigger ? 
      var accFiltered = E.convolve(filter_taps, history, 0) >> 2;
    
      console.log("accFiltered");
      console.log(accFiltered);
      
      /*
      // Simple average-based threshold
      if (stepCounterAvr) {
        var a = accFiltered;
        if (a<0) a=-a;
        stepCounterThreshold = (stepCounterThreshold*(32-stepCounterAvr) + a*stepCounterAvr) >> 5;
        if (stepCounterThreshold < stepCounterThresholdMin)
          stepCounterThreshold = stepCounterThresholdMin;
      }*/
      
      // Set threshold based on the 'middle' history item - the one making the big spikes.
      // Try and scale it appropriately
    
    
      /*
        question:  why would middle history item represent a big spike ?
        the history starts out blank and samples shuffle along in a FIFO manner
        initially history[32] will be 0 until the 32nd accel event has been captured ?
    
        question: having filtered why are we using the raw data again and not a scalled form of accFiltered ?
       */
      
      var a = history[32] * 55;
      if (a<0) a=-a;
      if (a > stepCounterThreshold) 
        stepCounterThreshold = a;//(stepCounterThreshold+a) >> 1;
      
      stepCounterThreshold -= 48;  // question: why -= 48 ??
      
      if (stepCounterThreshold < stepCounterThresholdMin)
        stepCounterThreshold = stepCounterThresholdMin;  
      
      // check for steps, a bottom, followed by top threshold crossing = a step
      var hadStep = false;
      if (accFiltered < -stepCounterThreshold) 
        stepWasLow = true;
      else if ((accFiltered > stepCounterThreshold) && stepWasLow) {
        stepWasLow = false;
        hadStep = true;
      }
      // output data
      g.scroll(0,1);
      var n;
    
    
      // question: cyan markers on the edge of the screen mean that
      // we crossed the threshold ??
      if (accFiltered < -stepCounterThreshold)
        g.setColor("#0ff").fillRect(0,0,8,0);
      if (accFiltered > stepCounterThreshold)
        g.setColor("#0ff").fillRect(232,0,240,0);
      
      n = 120+v;
      g.setColor("#00f").fillRect(lastAccel,0,n,0);
      lastAccel = n;
      n = 120+(accFiltered>>6);
      g.setColor("#ff0").fillRect(lastAccelFilt,0,n,0);
      lastAccelFilt = n;
      n = 120+(stepCounterThreshold>>6);
      g.setColor("#f00").fillRect(lastThresh,0,n,0);
      lastThresh = n;
    
      if (hadStep) {
        g.setColor(-1).drawString("STEP",60,0);
      }
    }
    Bangle.on('accel',onAccel);
    Bangle.setLCDTimeout(0);
    
  • It's worth looking at the actual C code as it's documented a lot better there (and also it's slightly different now), but here are my comments on the JS:

    // values for debug plotting
    var lastAccel = 120;      // BLUE
    var lastAccelFilt = 120;  // YELLOW
    var lastThresh = 120;     // RED
    
    // question: where do these values come from ? what does tap mean ?
    // question: is this a low pass 3Hz filter ?
    // GW If you look at the actual C code there's a link to where the filter comes from and what it is: https://github.com/espruino/Espruino/blob/master/libs/misc/stepcount.c
    // GW Basically it keeps 1.3-2.5Hz which is what stuff online seems to think is a normal step rate.
    var filter_taps = new Int8Array([ -2, 4, 4, 1, -1, 0, 2, -3, -12, -13, 2, 24, 29, 6, -25, -33, -13, 10, 11, -1, 3, 29, 41, 4, -62, -89, -34, 62, 110, 62, -34, -89, -62, 4, 41, 29, 3, -1, 11, 10, -13, -33, -25, 6, 29, 24, 2, -13, -12, -3, 2, 0, -1, 1, 4, 4, -2 ]);
    // create a history buffer and populate it with the tap values ?
    // GW The history just needs to be the same length as the filter. It's not pre-populated
    var history = new Int8Array(filter_taps.length);
    
    // what units is this in ? 
    // GW: It's not in any in particular - it's what comes out the filter
    const stepCounterThresholdMin = 1500;
    const stepCounterAvr = 1;
    /// Theshold in filtered acceleration for detecting a step
    var stepCounterThreshold = stepCounterThresholdMin;
    /// has filtered acceleration passed stepCounterThresholdLow?
    var stepWasLow = false;
    function onAccel(a) {
      // question - onAccel is event driven, so events could be irregular, papers seem to suggest 20Hz sampling frequency
      // will this event driven approach provide regular sampling of approx 20Hz or will it be subject to CPU performance etc
    //GW it's 12.5Hz as that's what comes out the accelerometer - it will be regular, yes.
      // scale to fit and clip
      var v = ((a.mag-1)*8192)>>5;
      // create a new Int8Array from the existing but starting from pos 1 of the existing history
      // this drops off index 0 and moves everything up, leaving the last entry to be filled
      // with the new value
      history.set(new Int8Array(history.buffer,1));
      // set last value to the clipped value from the accel
      history[history.length-1] = E.clip(v, -128, 127);
      console.log("history");
      console.log(history);
      
      // do filtering
      //  what is convolution ??
      //https://homepages.inf.ed.ac.uk/rbf/HIPR2/convolve.htm#:~:text=Convolution%20provides%20a%20way%20of,numbers%20of%20the%20same%20dimensionality.&text=In%20an%20image%20processing%20context,normally%20just%20a%20graylevel%20image.
      // question: - is this a low pass filter at 3Hz to get rid of noise ?
    // GW: answered this above
      // question: - why is accFiltered much larger than v , why >>2 to make it bigger ? 
    // GW: The filter amplifies it because we're doing everything in integer arithmetic not floats (for speed in C code)
      var accFiltered = E.convolve(filter_taps, history, 0) >> 2;
      console.log("accFiltered");
      console.log(accFiltered);
      
      /*
      // Simple average-based threshold
      if (stepCounterAvr) {
        var a = accFiltered;
        if (a<0) a=-a;
        stepCounterThreshold = (stepCounterThreshold*(32-stepCounterAvr) + a*stepCounterAvr) >> 5;
        if (stepCounterThreshold < stepCounterThresholdMin)
          stepCounterThreshold = stepCounterThresholdMin;
      }*/
      
      // Set threshold based on the 'middle' history item - the one making the big spikes.
      // Try and scale it appropriately
      /*
        question:  why would middle history item represent a big spike ?
        the history starts out blank and samples shuffle along in a FIFO manner
        initially history[32] will be 0 until the 32nd accel event has been captured ?
        question: having filtered why are we using the raw data again and not a scalled form of accFiltered ?
       */
    // GW: Check out that filter design website. It might make a bit more sense when you see the shape of the filter.
    // GW: Personally I'd ignore this code - adjusting the threshold never actually worked that well - a fixed threshold seemed to be better
      
      var a = history[32] * 55;
      if (a<0) a=-a;
      if (a > stepCounterThreshold) 
        stepCounterThreshold = a;//(stepCounterThreshold+a) >> 1;
      
      stepCounterThreshold -= 48;  // question: why -= 48 ??
      
      if (stepCounterThreshold < stepCounterThresholdMin)
        stepCounterThreshold = stepCounterThresholdMin;  
      
      // check for steps, a bottom, followed by top threshold crossing = a step
      var hadStep = false;
      if (accFiltered < -stepCounterThreshold) 
        stepWasLow = true;
      else if ((accFiltered > stepCounterThreshold) && stepWasLow) {
        stepWasLow = false;
        hadStep = true;
      }
      // output data
      g.scroll(0,1);
      var n;
      // question: cyan markers on the edge of the screen mean that
      // we crossed the threshold ??
      // GW:  yes
      if (accFiltered < -stepCounterThreshold)
        g.setColor("#0ff").fillRect(0,0,8,0);
      if (accFiltered > stepCounterThreshold)
        g.setColor("#0ff").fillRect(232,0,240,0);
      
      n = 120+v;
      g.setColor("#00f").fillRect(lastAccel,0,n,0);
      lastAccel = n;
      n = 120+(accFiltered>>6);
      g.setColor("#ff0").fillRect(lastAccelFilt,0,n,0);
      lastAccelFilt = n;
      n = 120+(stepCounterThreshold>>6);
      g.setColor("#f00").fillRect(lastThresh,0,n,0);
      lastThresh = n;
      if (hadStep) {
        g.setColor(-1).drawString("STEP",60,0);
      }
    }
    Bangle.on('accel',onAccel);
    Bangle.setLCDTimeout(0);
    

    It's a shame the new step counting hasn't worked out - is it substantially worse than it was before, or only marginally better?

    I guess making a widget is a good idea, although it won't be great for battery. You could actually run multiple widgets at once I guess and see which ones gave better values.

    I wouldn't add your widget to the main app store though - the last thing we need is another slightly different pedometer widget to confuse people. If you can get the step counting substantially better then we can look at pulling those changes back into the Bangle.js firmware

  • Thanks for filling in the gaps in my knowledge. I spent a good couple of hours looking at the code.

    is it substantially worse than it was before, or only marginally better?

    Gut feel ...about the same. Thats not bery scientific I know. I have have generally got used to measuring activepedom and then using the latest firmware through activepedom. That felt better. I have previously only tested walks and done comparisons against my AmizFit Bip. OR I have tested sitting still or watching TV to watch out for false step detecting. This was the first time I decided to do a midnight to 6pm test.

    I wouldn't add your widget to the main app store

    I think we need a javascript implementation that people can tweak. Its a lot easier than having to flash the firmware and means you can test multiple iterations on the same firmware. For me to go back to the previous version I will have to reflash older cutting edge firmware.

    I have started reading a few papers that I have downloaded. A number of people have tested multiple approaches BUT they never show any code that you can look at. One thought I had was about writing to all the authors of papers in the last 5 years and asking if they were aware of Bangle and it they would like to attempt a step counter in javascript. If we provide a basic template App or Widget then it is easier to get started. Might not get much engagement but you never know. There is a substantial amount of time and effort needed for these kind of things as you need to repeat multiple tests many times with different people and different versions of software and algorithms.

    Another idea might be to crowd fund for a cash prize for someome to write an accurate step counter that will be open source for Bangle. A free Bangle is not going to attract that many to have a go otherwise. I think there might be enough of us wanting a good step counter that we could each chip in £20 to get it done. 20 such donations would make a cash prize of £400.

    Ref the accelerometer being 12.5Hz - has anyone done any tests to validate that it is regular ?

    Also the papers I read suggested 20Hz as an ideal sampling freq. Thinking outloud - I reckon I generally walk at 2 steps per second. Thats only 6 samples per step at 12.5Hz, where as 20Hz would give 10 samples per step. Granted you only need to see the crossing of the thresholds.

    It feels like there is an assumption somewehere that is not correct in practice. Again not very scientific.

    Is there any accuracy data on the how the stock firmware step counter performs ? Could it be limited by the hardware ?

  • here's the filter screenshot I got when I tried to enter the values you put in the code.


    1 Attachment

    • Screenshot 2021-06-25 19.12.42.png
  • This paper suggests that you get a 10% error rate when using a 10Hz sampling rate, this reduces to 2.5% for a 50Hz sampling rate. In the chart below magnitude is the algorithm Bangle is using.

    https://www.researchgate.net/publication/321413174_Sampling_Frequency_for_Step_Detection_Based_on_Smartphone_Accelerometry

    Having said that the problem is still down to the detection of steps when basically sat still.
    So its not filtering out the one off movements whens sleeping, sat at a desk etc.


    1 Attachment

    • Screenshot 2021-06-25 21.12.04.png
  • Had a look at the firmware code and translated it to a javascript version.
    @Gordon - could you give it a quick eyeball to see if I have got it right.

    /**
     * javascript version of the Bangle firmware step counter 2v09.90
     * 
     * See: https://github.com/espruino/Espruino/blob/master/libs/misc/stepcount.c
     *
     */
    
    // values for debug plotting
    var lastAccel = 120;      // BLUE    - raw
    var lastAccelFilt = 120;  // GREEN   - filtered
    var lastThresh = 120;     // RED
    
    /**
     * FIR filter designed with http://t-filter.appspot.com
     * sampling frequency: 12.5 Hz
     * fixed point precision: 10 bits
     *
     * 0   Hz - 1.1 Hz,  gain = 0  desired attenuation = -40 dB
     * 1.3 Hz - 2.5 Hz,  gain = 1  desired ripple = 5 dB
     * 2.7 Hz - 6.25 Hz, gain = 0  desired attenuation = -40 dB
     */
    var filter_taps = new Int8Array([ -2, 4, 4, 1, -1, 0, 2, -3, -12, -13, 2, 24, 29, 6, -25, -33, -13, 10, 11, -1, 3, 29, 41, 4, -62, -89, -34, 62, 110, 62, -34, -89, -62, 4, 41, 29, 3, -1, 11, 10, -13, -33, -25, 6, 29, 24, 2, -13, -12, -3, 2, 0, -1, 1, 4, 4, -2 ]);
    
    // create a history buffer the same lenght as the filter taps array
    var history = new Int8Array(filter_taps.length);
    
    // value used in the f/m by default
    const stepCounterThreshold = 1000;
    /// has filtered acceleration passed stepCounterThreshold
    var stepWasLow = false;
    
    const STEPCOUNTERHISTORY = 5;        // keep a history of last 5 steps
    const STEPCOUNTERHISTORY_TIME = 75;  // 6*12.5Hz = 75 values samples ~ 6 seconds
    var stepHistory = new Int16Array(STEPCOUNTERHISTORY);
    var i;
    
    // initialise the history with values 255 (timedout)
    for (i=0;i<STEPCOUNTERHISTORY;i++)
      stepHistory[i]=255;
    
    
    // acceleromter operates at 12.5Hz
    function onAccel(a) {
    
      // scale to fit and clip
      var v = ((a.mag-1)*8192)>>5;
    
      /**
       *  create a new Int8Array from the existing but starting from pos 1 of the existing history
       *  this drops off index 0 and moves everything up, leaving the last entry to be filled
       *  with the new value
       */
      history.set(new Int8Array(history.buffer,1));
      // set last value to the clipped value from the accel
      var raw_clipped = E.clip(v, -128, 127);
      history[history.length-1] = raw_clipped;
    
      // digital filter, output has to be scaled down as the calculation is integer based, no floating point
      var accFiltered = E.convolve(filter_taps, history, 0) >> 2;
      
      // increment step count history counters, each slot records how many samples ago the
      // step was from the currently detected step
      for (var i = 0 ;i < STEPCOUNTERHISTORY; i++)
        if (stepHistory[i] < 255)
          stepHistory[i]++;
      
      // check for steps, a bottom, followed by top threshold crossing = a step
      var hadStep = false;
      if (accFiltered < -stepCounterThreshold) {
        stepWasLow = true;
      } else if ((accFiltered > stepCounterThreshold) && stepWasLow) {
        stepWasLow = false;
        // We now have something resembling a step!
        // Don't register it unless we've already had X steps within Y time period
        if (stepHistory[0] < STEPCOUNTERHISTORY_TIME) {
          hadStep = true;
        }
        
        // Add it to our history anyway so we can keep track of how many steps we have
        for (i=0;i<STEPCOUNTERHISTORY-1;i++)
          stepHistory[i] = stepHistory[i+1];
        stepHistory[STEPCOUNTERHISTORY-1] = 0;
      }
    
      // output data
      g.scroll(0,1);
      var n;
    
      // cyan markers on the edge of the screen mean that we crossed the threshold
      if (accFiltered < -stepCounterThreshold)
        g.setColor("#0ff").fillRect(0,0,8,0);
      if (accFiltered > stepCounterThreshold)
        g.setColor("#0ff").fillRect(232,0,240,0);
      
      // plot raw value BLUE
      n = 120+v;
      g.setColor("#00f").fillRect(lastAccel,0,n,0);
      lastAccel = n;
      
      // plot filtered value, in GREEN
      n = 120+(accFiltered>>6);
      g.setColor("#0f0").fillRect(lastAccelFilt,0,n,0);
      lastAccelFilt = n;
      
      // plot threshold in RED
      n = 120+(stepCounterThreshold>>6);
      g.setColor("#f00").fillRect(lastThresh,0,n,0);
      lastThresh = n;
    
      if (hadStep) {
        g.setColor(-1).drawString("STEP",60,0);
      }
    }
    
    g.clear();
    Bangle.on('accel',onAccel);
    Bangle.setLCDTimeout(0);
    
    
    
  • I think we need a javascript implementation that people can tweak.

    Well, I did spend time doing that 2 months ago, but then nobody bothered to do anything with it!

    For developing, absolutely - but in the end we need it baked into the firmware or it'll kill battery life.

    Your JS version looks good though. I think it's possible that just raising stepCounterThreshold to maybe 1500, lowering STEPCOUNTERHISTORY to 2 and STEPCOUNTERHISTORY_TIME to 40ish might actually have a pretty good effect.

    The accelerometer regularity should be absolutely fine unless you have an app/clock that's blocking execution for more than around 100ms - in which case you could end up with some duplicated samples.

    In terms of sample rate - we're currently 200% out - so I don't think that is our main problem. If we increase the sample rate then that drains battery quicker (especially if we're processing in JS) and does cause some issues in other areas, so I'd rather not do it unless it turns out to be absolutely required. You can however tweak it yourself from JS if you want to try: https://github.com/espruino/BangleApps/blob/master/apps/accelrec/app.js#L35-L38

  • Well, I did spend time doing that 2 months ago, but then nobody bothered to do anything with it!

    Appreciated. It was me that requested it then I had other stuff to do. I'm making use of it now though.

    I've built the App below. It has allowed me to get a much better understanding of what is going on. The main ISSUE IS THE RINGING produced by the filter. One impulse (eg raising your hand to scratch the back of your neck and lowering it back) can cause a ring that will produce 7 steps. The step counting is fairly accurate - the big issue is the ringing. This is why I think it counts 1200 steps when you sleep for 8 hours. A few shuffles every hour or so adds up over that 8 hour period.

    Maybe a simpler single stage filter would produce less ringing. Maybe we dont actually need a filter. The act of walking produces a strong enough up/down cycle in the magnitude of the accelerometer to drown out noise etc.

    Not sure if I understood how the approach to X steps in Y seconds is working. It took a while for me to figure out what is going on. I think what is happening is 1) step comes in 2) did it occur within 75 samples (6 seconds, 6x12.5=75) Yes/No record it as a step in the history, 3) the 5 element step history fills up with steps that could each have occured between 0-X samples. When it fills up you check the OLDEST entry in the step history step that it arrived within 75 samples ago (ie within 6 seconds ago). Hmm - ok - I think I go it.

    I have done it as a state machine. First step is detected move to STEP_1 state. If the next step is within 1 second move to STEP_225 state otherwise go back to STEP_1 state. When you get to 5 steps each arriving within 1 second of each other then you are in STEPPING state. After that 2 seconds of in activity means go back to STEP_1. When you get to STEPPING state the 5 held back steps are released to be counted. The state machine works fine. The problem is with the filter ringing.

    To test BTN1 to start. Walk 10 steps across the room, stop. You will over count by 2,3,4 steps maybe. More if you are walking fast / stomping.

    Here's my test code:

    /**
     * javascript step counting app, with state machine to ensure
     * we only start counting after 5 steps in 5 seconds
     *
     * FIR filter designed with http://t-filter.appspot.com
     * sampling frequency: 12.5 Hz
     * fixed point precision: 10 bits
     *
     * 0   Hz - 1.1 Hz,  gain = 0  desired attenuation = -40 dB
     * 1.3 Hz - 2.5 Hz,  gain = 1  desired ripple = 5 dB
     * 2.7 Hz - 6.25 Hz, gain = 0  desired attenuation = -40 dB
     */
    var filter_taps = new Int8Array([ -2, 4, 4, 1, -1, 0, 2, -3, -12, -13, 2, 24, 29, 6, -25, -33, -13, 10, 11, -1, 3, 29, 41, 4, -62, -89, -34, 62, 110, 62, -34, -89, -62, 4, 41, 29, 3, -1, 11, 10, -13, -33, -25, 6, 29, 24, 2, -13, -12, -3, 2, 0, -1, 1, 4, 4, -2 ]);
    
    // create a history buffer the same lenght as the filter taps array
    var history = new Int8Array(filter_taps.length);
    
    // value used in the f/m by default
    const stepCounterThreshold = 1000;
    /// has filtered acceleration passed stepCounterThreshold
    var stepWasLow = false;
    var step_count = 0;  // total steps since app start
    
    // acceleromter operates at 12.5Hz
    function onAccel(a) {
    
      // scale to fit and clip
      var m = a.mag;
      var v = ((m-1)*8192)>>5;
    
      /**
       *  create a new Int8Array from the existing but starting from pos 1 of the existing history
       *  this drops off index 0 and moves everything up, leaving the last entry to be filled
       *  with the new value
       */
      history.set(new Int8Array(history.buffer,1));
      // set last value to the clipped value from the accel
      var raw_clipped = E.clip(v, -128, 127);
      history[history.length-1] = raw_clipped;
      //console.log("history: " + history);
    
      // digital filter, output has to be scaled down as the calculation is integer based, no floating point
      var accFiltered = E.convolve(filter_taps, history, 0) >> 2;
      console.log("m: " + m + " r: " + v + " rc: " + raw_clipped + " f: " + accFiltered);
      
      // check for steps, a bottom, followed by top threshold crossing = a step
      var hadStep = false;
      if (accFiltered < -stepCounterThreshold) {
        stepWasLow = true;
        console.log(" LOW");
      } else if ((accFiltered > stepCounterThreshold) && stepWasLow) {
        stepWasLow = false;
        console.log(" HIGH");
        // We now have something resembling a step
        // now call state machine to ensure we only count steps when we have done 5 steps in 5 seconds
        // 2s of silence will reset the state to STEP1 state
        hadStep = true;
        step_count += step_machine.step_state();
        console.log(" *STEP*  " + step_count);
      }
    }
    
    function STEP_STATE() {
      this.S_STILL = 0;       // just created state m/c no steps yet
      this.S_STEP_1 = 1;      // first step recorded
      this.S_STEP_225 = 2;    // counting 2-5 steps
      this.S_STEPPING = 3;    // we've had 5 steps in 5 seconds
      
      this.state = this.S_STILL;
      this.hold_steps = 0;
      this.t_prev = getTime();
    }
    
    STEP_STATE.prototype.step_state = function() {
      var st = this.state;
      var t;
      
      switch (st) {
      case this.S_STILL:
        console.log("S_STILL");
        this.state = this.S_STEP_1;
        this.t_prev = getTime();
        this.hold_steps = 1;
        return 0;
        
      case this.S_STEP_1:
        t = Math.round((getTime() - this.t_prev)*1000);
        console.log(t + " S_STEP_1");
        this.t_prev = getTime();
        
        // we got a step within 1 second
        if (t <= 1000) {
          this.state = this.S_STEP_225;
          this.hold_steps = 2;
        } else {
          // we stay in STEP_1 state
        }
        return 0;
    
      case this.S_STEP_225:
        t = Math.round((getTime() - this.t_prev)*1000);
        console.log(t + " S_STEP_225");
        this.t_prev = getTime();
        
        // we got a step within 1 second
        if (t <= 1000) {
          this.hold_steps++;
    
          if (this.hold_steps >= 5) {
            this.state = this.S_STEPPING;
            return 5;
          }
        } else {
          // we did not get the step in time, back to STEP_1
          this.state = this.S_STEP_1;
          this.hold_steps = 1;
        }
        return 0;
    
      case this.S_STEPPING:
        t = Math.round((getTime() - this.t_prev)*1000);
        console.log(t + " S_STEPPING");
        this.t_prev = getTime();
        
        // we got a step within 2 seconds, otherwise we stopped stepping
        if (t <= 2000) {
          this.state = this.S_STEPPING;
          return 1;
        } else {
          // we did not get the step in time, back to STEP_1
          this.state = this.S_STEP_1;
          this.hold_steps = 1;
        }
        return 0;
      }
    
      // should never get here
      return 0;
    };
    
    
    STEP_STATE.prototype.get_state = function() {
      switch(this.state) {
      case this.S_STILL: return "S_STILL";
      case this.S_STEP_1: return "S_STEP_1";
      case this.S_STEP_225: return "S_STEP_255";
      case this.S_STEPPING: return "S_STEPPING";
      default: return "ERROR";
      }
    }
    
    let step_machine = new STEP_STATE();
    
    function draw() {
      g.clear();
      g.setColor(0);
      g.setColor(1,1,1);
      g.setFont("Vector",20);
      g.setFontAlign(0,-1);
      g.drawString("  " + step_machine.get_state() + "  ", 120, 30, true);
    
      if (running) {
        g.drawString(step_count, 120, 100, true);
      } else {
        g.drawString("(" + step_count + ") BTN1 to START", 120, 100, true);
      }
    }
    
    var running = false;
    
    function onStartStop() {
    
      running = !running;
      
      if (running) {
        step_count = 0; // reset
        Bangle.on('accel',onAccel);
      } else {
        Bangle.removeListener('accel', onAccel);
        console.log("STOP  -------------------------------------");
      }
    }
    
    // handle switch display on by pressing BTN1
    Bangle.on('lcdPower', function(on) {
      if (on) draw();
    });
    
    g.clear();
    Bangle.loadWidgets();
    Bangle.drawWidgets();
    setInterval(draw, 1000); // refresh every second
    draw();
    setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
    setWatch(onStartStop, BTN1, {repeat:true,edge:"rising"});
    

    Attached is a test log of me doing 10 steps, stopping and then watching steps 11,12 and 13 count themselves by ringing.


    1 Attachment

  • I think it's possible that just raising stepCounterThreshold to maybe 1500, lowering >>STEPCOUNTERHISTORY to 2 and STEPCOUNTERHISTORY_TIME to 40ish might actually have a >>pretty good effect.

    I'm not convinced based on my observations about the ringing.

    Tuning the thresholds is quite tricky as I briefly experimented with taking out the filter.

    It would be good to know how much amplication the convolve() function gives. If the input is -128 to +127, what is the expected output range. How do you scale it back to the -128 to +127 range.

    Another thought I have had is PEAK detection. You then just need 3 points A B C where B > C AND and B > A to detect a PEAK. You would have to take off the clipping though otherwise you get a plateau. The advantage of PEAK detection is that you dont need to worry about setting a threshold for that part. However you do need to determine a band in which you say any magnitude M between -X and +X is considered to be too weak to be walking. I would suggest a good start for that band is -50 to +50. These are the max/min values that I observe for raw_clipped when I lift my hand to rub my hair and drop it back to my side. These are clearly not working values for walking which will be much higher.

      var m = a.mag;
      var v = ((m-1)*8192)>>5;
       var raw_clipped = E.clip(v, -128, 127);
    

    I might try PEAK detection and removing the filter once I understand how to scale the output of convolve().

  • Does my code for X steps in Y time actually work though? It's pretty basic - you're just storing how long ago each step occurred. If the second from last step occurred less than 3 seconds ago then it's good. It feels like a state machine is overkill.

    Yes, the ringing is a pain. I'd originally hoped that the filter would have the effect of significantly amplifying repeated steps (which it does) but the flipside is it tends to make instantaneous peaks oscillate. It's why I though raising the threshold would be a good help, as you have to have really strong movements to get peaks anywhere near as high as you'd get when you start doing repeated movements.

    If you look at the step count tester I'd posted up at https://github.com/gfwilliams/step-count then it actually produces nice graphs where you can see the behaviour in relation to the threshold.

    It's entirely possible that a simple high/low pass filter (rather than bandpass) wouldn't ring though - and since we're counting step frequency probably all we care about is cutting out high frequency.

  • Agree - looked up ringing on wikipedia and the upshot is that ringing is enevitable and complex to get rid of.

    https://en.wikipedia.org/wiki/Ringing_artifacts

    BUT - Eureka - I think I have worked out a way to deal with this - but will need to do the experiments.

    1) the filter rings. But it is fairly consistant and does a good job in ampliying the walking cadence.

    2) Using STEPO (one of my apps in the repo) with trip counter I can see that rolling over or moving my arm over to the bedside table and back to switch the light on will cause on averge between 6-9 steps. This suggests to me that making the threshold for starting step counting should be 10 steps in 10 seconds. Your X in Y algorithm works. The only difference with the state machine is that after we get to step counting (STEPPING) state - that the criteria is removed and only a gap of 2 seconds will put you back into STEP1 state (waiting for X steps in Y seconds). With your C code 5 stepHistory[] approach (much simpler and very elegant) - the constraint continues for all steps and if not met will drop the odd step. In reality the difference may not be significant or matter. Will have to measure it. Your 5 line approach just took a lot longer to understand - but it is a lot less code, which is usually a good thing.

    3) I have observed after stopping walking on average I see 2-3 extra steps due to the ringing. So the solution is that when you come out of 10 steps in 10 seconds you count 7 steps to start with knowing that the ringing will provide the other 3 steps at the end of the STEPPING period. This may sound a bit hacky at first but the more I think about it the more I like it.

    Will build an App tonight and test it.

  • That sounds really promising - thanks! However I do wonder about the ringing issue if we're having to manually account for the extra rings it adds (I bet it'll be different for different people).

    I guess one benefit of the state machine if you can actually add the missing 10 steps back in once you reach that point. My code doesn't do that (but maybe the ringing at the end makes up for it slightly!)

  • I bet it'll be different for different people

    I think once you actually get walking the ringing will be more consistant. I think it might also depend on a persons weight and momentum. Once I have a prototype that does not record many steps whilst sleeping I will think up a set of tests. My gold standard is a 1.03 mile circuit I have used many times that FitBit used to consistantly come back with 2000 steps. I would do a test at 2mph 3mph,4pm and jogging. I'm afraid all my distances are in miles - I know what an 8 minute mile feels like to run :) Another test will be a standard working day with odd walks round the house etc, will have to wear another tracker to compare.

    If this line of attack does not come up with improved accuracy then will have to start looking at how other filter specs perform in terms of ringing. Its interesting to note that I have not seen mention of ringing in the various papers I have read. They just say a low pass filter was used.

  • Here is the test App. I am just loading it through the filemanager or IDE.
    When loaded through the IDE it prints out a lot of useful rows in CSV format.

    Only done cursory tests so far - but I beleive this is significantly better than anything done so far. The main improvement is screening out counting steps when you are not on your feet - but I think this is the best we are going to get with this filter.

    Key Result 1: Walked 0.31mile round the block:
    Amizfit GTS2 - 450, Bangle steps_mc2.app.js - 446 !! Great result.

    Key Result 2: Sat on the sofa, scratch my head, typing for 1 hour - Zero steps. Never been seen before with any of the previous iterrations.

    Key result 3: Will count 10 steps from standing still - with varying results (sometimes less or more). But for continuous counting its pretty accurate.

    The key problem this is trying to solve is counting steps when you are not actually walking.
    This App gets a lot closer than I have seen to date BUT it is still possible to trigger it into Walking mode just by doing something that takes more than 10 seconds - like putting a coat on standing on the spot. Ultimately I think we need a better filter, one that rings a lot less, posssibly much higher sampling frequency which I think will reduce the ringing.

    Attached is one of the better papers I have found so far. This suggests a LPF with 20Hz cut off. This would not be possible using 12.5Hz as the sampling frequency. There is 6 sample formula for the LPF which looks like it would be simple to implement. Is it worth experimenting with 40Hz sample and a 20Hz cut off LPF ? OR after you have tried this out and others have tested it you could just translate it into the next cutting edge version of the Firmware. As I say - I think this is significantly less susceptable to counting when you are sat down typing etc.

    Do you have a Bangle with Stock Firmware that could be tested ? I'd like to see if the commercial product was any good on the step counting front.

    /**
     * javascript step counting app, with state machine to ensure
     * we only start counting after X steps in Y seconds
     *
     * FIR filter designed with http://t-filter.appspot.com
     * sampling frequency: 12.5 Hz
     * fixed point precision: 10 bits
     *
     * 0   Hz - 1.1 Hz,  gain = 0  desired attenuation = -40 dB
     * 1.3 Hz - 2.5 Hz,  gain = 1  desired ripple = 5 dB
     * 2.7 Hz - 6.25 Hz, gain = 0  desired attenuation = -40 dB
     *
     * V2 8 steps in 8 seconds to start counting, count 7 and rely on 'ringing' to give back 3 steps
     *
     *
     *
     */
    
    
    const X_STEPS = 9;         // we need to see X steps in X seconds to move to STEPPING state
    const X_STEPS_COUNT = 4;   // count Y steps once in STEPPING state, let 'ringing' give back 5 steps when stop
    
    
    var filter_taps = new Int8Array([ -2, 4, 4, 1, -1, 0, 2, -3, -12, -13, 2, 24, 29, 6, -25, -33, -13, 10, 11, -1, 3, 29, 41, 4, -62, -89, -34, 62, 110, 62, -34, -89, -62, 4, 41, 29, 3, -1, 11, 10, -13, -33, -25, 6, 29, 24, 2, -13, -12, -3, 2, 0, -1, 1, 4, 4, -2 ]);
    
    // create a history buffer the same lenght as the filter taps array
    var history = new Int8Array(filter_taps.length);
    
    // value used in the f/m by default
    const stepCounterThreshold = 1000;
    /// has filtered acceleration passed stepCounterThreshold
    var stepWasLow = false;
    var step_count = 0;  // total steps since app start
    
    // acceleromter operates at 12.5Hz
    function onAccel(a) {
    
      // scale to fit and clip
      var m = a.mag;
      var v = ((m-1)*8192)>>5;
    
      /**
       *  create a new Int8Array from the existing but starting from pos 1 of the existing history
       *  this drops off index 0 and moves everything up, leaving the last entry to be filled
       *  with the new value
       */
      history.set(new Int8Array(history.buffer,1));
      // set last value to the clipped value from the accel
      var raw_clipped = E.clip(v, -128, 127);
      history[history.length-1] = raw_clipped;
    
      // digital filter, output has to be scaled down as the calculation is integer based, no floating point
      var accFiltered = E.convolve(filter_taps, history, 0) >> 2;
      
      // check for steps, a bottom, followed by top threshold crossing = a step
      var hadStep = false;
      var t = 0;
      if (accFiltered < -stepCounterThreshold) {
        stepWasLow = true;
        //console.log(" LOW");
        t = Math.round((getTime() - t_start)*1000);
        console.log(t + "," +  m + "," + raw_clipped + "," + accFiltered + "," +
                    step_count + "," + step_machine.get_state() + "," + step_machine.get_hold_steps());
      } else if ((accFiltered > stepCounterThreshold) && stepWasLow) {
        stepWasLow = false;
        //console.log(" HIGH");
        // We now have something resembling a step
        // now call state machine to ensure we only count steps when we have done X steps in X seconds
        // 2s of silence will reset the state to STEP1 state
        hadStep = true;
        step_count += step_machine.step_state();
        t = Math.round((getTime() - t_start)*1000);
        console.log(t + "," +  m + "," + raw_clipped + "," + accFiltered + "," +
                    step_count + "," + step_machine.get_state() + "," + step_machine.get_hold_steps());
      }
    }
    
    function STEP_STATE() {
      this.S_STILL = 0;       // just created state m/c no steps yet
      this.S_STEP_1 = 1;      // first step recorded
      this.S_STEP_22N = 2;    // counting 2-X steps
      this.S_STEPPING = 3;    // we've had X steps in X seconds
      
      this.state = this.S_STILL;
      this.hold_steps = 0;
      this.t_prev = getTime();
    };
    
    STEP_STATE.prototype.get_hold_steps = function() {
      return this.hold_steps;
    };
    
    STEP_STATE.prototype.step_state = function() {
      var st = this.state;
      var t;
      
      switch (st) {
      case this.S_STILL:
        //console.log("S_STILL");
        this.state = this.S_STEP_1;
        this.t_prev = getTime();
        this.hold_steps = 1;
        return 0;
        
      case this.S_STEP_1:
        t = Math.round((getTime() - this.t_prev)*1000);
        //console.log(t + " S_STEP_1");
        this.t_prev = getTime();
        
        // we got a step within 1 second
        if (t <= 1000) {
          this.state = this.S_STEP_22N;
          this.hold_steps = 2;
        } else {
          // we stay in STEP_1 state
        }
        return 0;
    
      case this.S_STEP_22N:
        t = Math.round((getTime() - this.t_prev)*1000);
        //console.log(t + " S_STEP_22N");
        this.t_prev = getTime();
        
        // we got a step within 1 second
        if (t <= 1000) {
          this.hold_steps++;
    
          if (this.hold_steps >= X_STEPS) {
            this.state = this.S_STEPPING;
            this.hold_steps = 1;
            return X_STEPS_COUNT;
          }
        } else {
          // we did not get the step in time, back to STEP_1
          this.state = this.S_STEP_1;
          this.hold_steps = 1;
        }
        return 0;
    
      case this.S_STEPPING:
        t = Math.round((getTime() - this.t_prev)*1000);
        //console.log(t + " S_STEPPING");
        this.t_prev = getTime();
        
        // we got a step within 2 seconds, otherwise we stopped stepping
        if (t <= 2000) {
          this.state = this.S_STEPPING;
          return 1;
        } else {
          // we did not get the step in time, back to STEP_1
          this.state = this.S_STEP_1;
          this.hold_steps = 1;
        }
        return 0;
      }
    
      // should never get here
      return 0;
    };
    
    
    STEP_STATE.prototype.get_state = function() {
      switch(this.state) {
      case this.S_STILL: return "S_STILL";
      case this.S_STEP_1: return "S_STEP_1";
      case this.S_STEP_22N: return "S_STEP_22N";
      case this.S_STEPPING: return "S_STEPPING";
      default: return "ERROR";
      }
    };
    
    let step_machine = new STEP_STATE();
    
    function draw() {
      g.clear();
      g.setColor(0);
      g.setColor(1,1,1);
      g.setFont("Vector",20);
      g.setFontAlign(0,-1);
      g.drawString("  " + step_machine.get_state() + "  ", 120, 40, true);
      g.drawString(" Hold " + step_machine.get_hold_steps() + "  ", 120, 70, true);
      g.drawString("  BATT: " + E.getBattery() + "%", 120, 100, true);
    
      if (running) {
        g.drawString("Steps: " + step_count, 120, 130, true);
      } else {
        g.drawString("(" + step_count + ") BTN1 to START", 120, 120, true);
      }
    }
    
    var running = true;
    var t_start = 0;
    
    function onStartStop() {
    
      running = !running;
      
      if (running) {
        step_count = 0; // reset
        t_start = getTime();
        Bangle.on('accel',onAccel);
      } else {
        Bangle.removeListener('accel', onAccel);
      }
    }
    
    // handle switch display on by pressing BTN1
    Bangle.on('lcdPower', function(on) {
      if (on) draw();
    });
    
    g.clear();
    //Bangle.loadWidgets();
    //Bangle.drawWidgets();
    setInterval(draw, 1000); // refresh every second
    draw();
    
    // test2 - use these options through a sleep period
    // uncomment the 2 lines below
    running = false;  // will get negated by onStartStop()
    onStartStop();
    
    // test1 - START / STOP
    // uncomment to experiment using BTN1 for START / STOP
    //setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
    //setWatch(onStartStop, BTN1, {repeat:true,edge:"rising"});
    

    1 Attachment

  • Great!

    The main improvement is screening out counting steps when you are not on your feet

    Did you do anything special for that? Or you mean just with your X steps in Y seconds implementation?

    With the filtering, I believe you're getting a certain amount of filtering from the accelerometer. I believe it takes more than 12.5 samples/sec, but then averages them down. So if you're looking at a 20 Hz low pass filter, maybe it's worth just trying with no filtering whatsoever and seeing what it behaves like?

  • Yes - so changes were:
    Count 9 Steps in 9 Seconds to go into STEPPING mode = but report 4 steps counted - relying on ringing to give back the other 4 steps.

    Overnight sleep test result is good (should not be counting steps when asleep)
    AmizFit GTS2 25 , Bangle JS (v2 state mc) 243

    I have seen Fitbit and AmizFit Bip report 200 steps overnight so thats a good result.

    I did briefly try no filtering BUT thresholds then all have to be rebalanced and I have not tinkered with the C code to run your automated threshold calculator.

    I would like to try a simpler LPF if possible. Maybe 5 or 6Hz cut off only.
    Would ideally like to use the 6 sample LPF in the paper I quoted - but that has a 20Hz cut off.
    Will maybe try the filter site and see if I can get results out of it for a single LPF filer (not 3 bands).

  • So all day test result is not so good and it is this test that matters.
    These figures include steps counted from going to bed until 18:20pm.

    Amizfit GTS2 902 , Bangle State M/C V2 - 2814

    Not done a lot of walking etc, been in the house working at my desk, going up and down stairs and a short walk in the morning for 10 minutes.

    So going to have to adjust the X steps to 11. But I do suspect that I'm fighting a loosing battle with the ringing.

    I'm not entirely convinced that a filter is needed if you detect PEAKs or threshold crossing. It should not matter but most of the papers I have read say you should use a LPF.

  • Thanks for trying this out!

    I'm not entirely convinced that a filter is needed

    I seem to recall that earlier you felt you had a lot of success with just the active pedometer on earlier firmwares?

    In that case, we could look at removing all the filtering code, going with just a standard threshold, and doing the X steps in Y seconds thing?

  • I seem to recall that earlier you felt you had a lot of success with just the active
    pedometer on earlier firmwares?

    I think you are referring to comment #37 a couple of months back on Firmware 2v09.9 which I think had the filter. The test was only a single walk - which I have now established repeatedly will come out quite accurely. You then added the X steps in Y implementation a while later which is what I have tested. Its when the bangle is at rest (when sleeping or sittting / typing) that the false counting happens. But I probably should retest one of the v208 firmwares with the X steps in Y idea.

    Do you have the C implementation I can look at for the pre-filter step counter ? I grabbed the git repo for Espruino but lib/misc/stepcount.c only goes as far back as April 2021. It must have been somewhere else before that ?

    earliest commit I can find on stepcount.c is:

    commit d338bfbf8a045362fcf07bbbc52996b457a94d8a
    Author: Gordon Williams <gw@pur3.co.uk>
    Date:   Thu Apr 29 11:00:05 2021 +0100
    
  • Here's my latest attempt. I came up the idea of keeping a rolling average of the ABS values coming out of the raw a.mag value. Once the values had dropped below an at rest value I force the average to -1. That allows me to check if steps have shutdown before the ringing has stopped. This works well for stopping counting immediately - but its not the right end of the problem to be solved.

    I tested it overnight and during the day and the counts were over by 2000+ at the end of the day. So bad result.

    Just logging my experiments / results here in case I need to look them up later.

    
    /**
     * javascript step counting app, with state machine to ensure
     * we only start counting after X steps in Y seconds
     *
     * FIR filter designed with http://t-filter.appspot.com
     * sampling frequency: 12.5 Hz
     * fixed point precision: 10 bits
     *
     * 0   Hz - 1.1 Hz,  gain = 0  desired attenuation = -40 dB
     * 1.3 Hz - 2.5 Hz,  gain = 1  desired ripple = 5 dB
     * 2.7 Hz - 6.25 Hz, gain = 0  desired attenuation = -40 dB
     *
     * V3 using rolling average to gate steps
     *
     */
    
    
    const X_STEPS = 7;         // we need to see X steps in X seconds to move to STEPPING state
    const X_STEPS_COUNT = 7;   // count Y steps once in STEPPING state, let 'ringing' give back 5 steps when stop
    const RAW_HISTORY_SIZE = 75;
    const V_REGISTER = 15;
    const V_PUNISH = -1;
    const V_REWARD = 3;
    const V_KILL_AVG = 15;  // below threshold values and we stop stepping
    
    var kill_count = 0;
    
    var filter_taps = new Int8Array([ -2, 4, 4, 1, -1, 0, 2, -3, -12, -13, 2, 24, 29, 6, -25, -33, -13, 10, 11, -1, 3, 29, 41, 4, -62, -89, -34, 62, 110, 62, -34, -89, -62, 4, 41, 29, 3, -1, 11, 10, -13, -33, -25, 6, 29, 24, 2, -13, -12, -3, 2, 0, -1, 1, 4, 4, -2 ]);
    
    // create a history buffer the same lenght as the filter taps array
    var raw_history = new Int8Array(RAW_HISTORY_SIZE);
    //var raw_history = new Int8Array(filter_taps.length);
    var history = new Int8Array(filter_taps.length);
    
    var raw_total = 0;
    var raw_avg = 0;
    
    // value used in the f/m by default
    const stepCounterThreshold = 1000;
    /// has filtered acceleration passed stepCounterThreshold
    var stepWasLow = false;
    var step_count = 0;  // total steps since app start
    
    // acceleromter operates at 12.5Hz
    function onAccel(a) {
    
      // scale to fit and clip
      var m = a.mag;
      var v = ((m-1)*8192)>>5;
    
      /**
       *  create a new Int8Array from the existing but starting from pos 1 of the existing history
       *  this drops off index 0 and moves everything up, leaving the last entry to be filled
       *  with the new value
       */
      history.set(new Int8Array(history.buffer,1));
      // set last value to the clipped value from the accel
      var raw_clipped = E.clip(v, -128, 127);
      history[history.length-1] = raw_clipped;
    
      /**
       *
       * add the sample to our rolling average history
       * when rolling raw average is above 15 - we are walking 
       *
       */
      raw_history.set(new Int8Array(raw_history.buffer,1));
      // set last value to the clipped value from the accel
    
      if (v > V_REGISTER || v < -V_REGISTER) {
        raw_history[raw_history.length-1] = V_REWARD;
        kill_count = 0;
      } else {
        raw_history[raw_history.length-1] = V_PUNISH;
        kill_count++;
      }
    
      // calculate raw rolling avg
      raw_total = raw_history.reduce((a, b) => a + b, 0);
      raw_avg = raw_total / raw_history.length || 0;
    
      if (kill_count >= V_KILL_AVG)
        raw_avg = -1;
      
      // digital filter, output has to be scaled down as the calculation is integer based, no floating point
      var accFiltered = E.convolve(filter_taps, history, 0) >> 2;
    
      console.log(v + "   ," +raw_avg);
    
      
      // check for steps, a bottom, followed by top threshold crossing = a step
      var hadStep = false;
      var t = 0;
      if (accFiltered < -stepCounterThreshold) {
        stepWasLow = true;
        //console.log(" LOW");
        t = Math.round((getTime() - t_start)*1000);
        /*
        console.log(t + "," +  m + "," + raw_clipped + "," + accFiltered + "," +
                    step_count + "," + step_machine.get_state() + "," + step_machine.get_hold_steps());
        */
      } else if ((accFiltered > stepCounterThreshold) && stepWasLow) {
        stepWasLow = false;
        //console.log(" HIGH");
        // We now have something resembling a step
        // now call state machine to ensure we only count steps when we have done X steps in X seconds
        // 2s of silence will reset the state to STEP1 state
        hadStep = true;
        step_count += step_machine.step_state();
        t = Math.round((getTime() - t_start)*1000);
        /*
        console.log(t + "," +  m + "," + raw_clipped + "," + accFiltered + "," +
                    step_count + "," + step_machine.get_state() + "," + step_machine.get_hold_steps());
        */
      }
    }
    
    function STEP_STATE() {
      this.S_STILL = 0;       // just created state m/c no steps yet
      this.S_STEP_1 = 1;      // first step recorded
      this.S_STEP_22N = 2;    // counting 2-X steps
      this.S_STEPPING = 3;    // we've had X steps in X seconds
      this.reset();
    };
    
    STEP_STATE.prototype.reset = function() {
      this.state = this.S_STILL;
      this.hold_steps = 0;
      this.t_prev = getTime();
    };
    
    STEP_STATE.prototype.get_hold_steps = function() {
      return this.hold_steps;
    };
    
    STEP_STATE.prototype.step_state = function() {
      var st = this.state;
      var t;
      
      switch (st) {
      case this.S_STILL:
        //console.log("S_STILL");
        this.state = this.S_STEP_1;
        this.t_prev = getTime();
        this.hold_steps = 1;
        return 0;
        
      case this.S_STEP_1:
        t = Math.round((getTime() - this.t_prev)*1000);
        //console.log(t + " S_STEP_1");
        this.t_prev = getTime();
        
        // we got a step within 0.167s (6 min/mile) and 1 second
        if (t <= 1000 && t >= 167) {
          this.state = this.S_STEP_22N;
          this.hold_steps = 2;
        } else {
          // we stay in STEP_1 state
        }
        return 0;
    
      case this.S_STEP_22N:
        t = Math.round((getTime() - this.t_prev)*1000);
        //console.log(t + " S_STEP_22N");
        this.t_prev = getTime();
        
        // we got a step within 0.167s (6min/mile) and 1 second
        if (t <= 1000 && t >= 167) {
          this.hold_steps++;
    
          if (raw_avg > 0 && this.hold_steps >= X_STEPS) {
            this.state = this.S_STEPPING;
            this.hold_steps = 1;
            return X_STEPS_COUNT;
          }
        } else {
          // we did not get the step in time, back to STEP_1
          this.state = this.S_STEP_1;
          this.hold_steps = 1;
        }
        return 0;
    
      case this.S_STEPPING:
        this.hold_steps = 1;
        t = Math.round((getTime() - this.t_prev)*1000);
        //console.log(t + " S_STEPPING");
        this.t_prev = getTime();
        
        // we got a step within 2 seconds, otherwise we stopped stepping
        if (t <= 2000 && raw_avg > 0) {
          this.state = this.S_STEPPING;
          return 1;
        } else {
          // we did not get the step in time, back to STEP_1
          this.state = this.S_STEP_1;
        }
        return 0;
      }
    
      // should never get here
      return 0;
    };
    
    
    STEP_STATE.prototype.get_state = function() {
      switch(this.state) {
      case this.S_STILL: return "S_STILL";
      case this.S_STEP_1: return "S_STEP_1";
      case this.S_STEP_22N: return "S_STEP_22N";
      case this.S_STEPPING: return "S_STEPPING";
      default: return "ERROR";
      }
    };
    
    let step_machine = new STEP_STATE();
    
    function draw() {
      g.clear();
      g.setColor(0);
      g.setColor(1,1,1);
      g.setFont("Vector",20);
      g.setFontAlign(0,-1);
      g.drawString("  " + step_machine.get_state() + "  ", 120, 40, true);
      g.drawString(" Hold " + step_machine.get_hold_steps() + "  ", 120, 70, true);
      g.drawString("  BATT: " + E.getBattery() + "%", 120, 100, true);
    
      if (running) {
        g.drawString("Steps: " + step_count, 120, 130, true);
      } else {
        g.drawString("(" + step_count + ") BTN1 to START", 120, 120, true);
      }
    }
    
    var running = false;
    var t_start = 0;
    
    function onStartStop() {
    
      running = !running;
      
      if (running) {
        step_count = 0; // reset
        step_machine.reset();
        t_start = getTime();
        Bangle.on('accel',onAccel);
      } else {
        Bangle.removeListener('accel', onAccel);
      }
    }
    
    // handle switch display on by pressing BTN1
    Bangle.on('lcdPower', function(on) {
      if (on) draw();
    });
    
    g.clear();
    //Bangle.loadWidgets();
    //Bangle.drawWidgets();
    setInterval(draw, 1000); // refresh every second
    draw();
    
    // test2 - use these options through a sleep period
    // uncomment the 2 lines below
    running = false;  // will get negated by onStartStop()
    onStartStop();
    
    // test1 - START / STOP
    // uncomment to experiment using BTN1 for START / STOP
    //setWatch(onStartStop, BTN1, {repeat:true,edge:"rising"});
    
    
  • This warrants experimentation:

    Through thousands of steps testing, it can be found that
    the time for one step is between T0=0.41 s and T1=1 s [12] and the threshold
    of the difference between peak and valley is set to 1.1 m/s2. If the difference
    between peak and valley is less than the threshold or the period of the most
    recent process from C0 to C6 is not in the [T0, T1], this waveform is determined
    to be caused by the accidental acts.

    From:
    An Adaptive Step Detection Algorithm Based on the State Machine
    Fang Zhao1, Xinran Li1(B), Jiabao Yin1, and Haiyong Luo2

    In other words:
    max(a.mag) - min(a.mag) should be greater than 1.1 if its a step and not a wrist or arm movement whilst at rest.

    Just need to find the equivalent values for:
    var v = (m*8192)>>0; or a simlar integer scale. NEXT STEP #1

    Worth a try to see if I can use this to gate stepping.
    I should also look at the period range [0.41s, 1s] - my state machine does this for 1s part already.
    I already attempted to add a lower limit of 0.167s as I estimated that as 6min mile pace.
    NEXT STEP #2 to replace with 0.41s for the lower period limit.

  • of the difference between peak and valley is set to 1.1 m/s2.

    So this translates to approximately 0.1g (g is 9.8m/s/s) which is not distinguishable from moving your arm up to scratch the back of your neck. So this was a red herring.

  • @Gordon - I think I have cracked the major problems. I am down to the level of improvements that require much tighter testing metholodoly (average of multiple tests, calibration of the device that it is being compared against).

    The code below is the current best. And I think you could implement this in the firmware without too much disruption to memory requirements or the existing code.

    NEXT STEP is to implement this in firmware (ideally with X in X being configurable) so that I can continue testing.

    I have introduced 2 improvements that have been tested over the last 3 days with lots of success.

    1) Gating the filter to cut the ringing- using the raw signal. Avoids over counting by 5-8 additional steps every time you stop walking. Also avoids openning up the State Machine due to a single impulse.

    2) The State Machine - which is easy to configure and will have no memory impact if we later decide that 8 in 8 or 9 in 9 is a better setting.

    I have got the counting while the person is sleeping problem down from 1200 steps to 46 steps. 1000 steps in a day is a 10% error over 10K steps.

    There are multiple tests that have to be done in different scenarios.

    • Exercise tests - measured distance and expected step count against other devices. This code is now 96-99% accurate against a Amazfit GTS2.
    • Sleep Tests * - the step counter should not count while sleeping. All do to a lesser or greater extent. Record the steps counted while sleeping overnights.
    • Driving test - we ideally should not count 100s of steps while driving. I have got this effect down but not fully. It might be improved by moving from 7 Steps in 7 Seconds to 8 in 8.

      
      /**
      * Javascript step counting app, with state machine to ensure we only
      * start counting after X steps in X seconds we use the state machine
      * to check each step is within the expected period for that step To
      * reduce the impact of the Low Pass Filter ringing we use the raw
      * accelerometer data to gate the output from the filter.  This leads
      * to better cut off within 1 second when a step sequence has
      * stopped.
      *
      * FIR filter designed with http://t-filter.appspot.com
      * sampling frequency: 12.5 Hz
      * fixed point precision: 10 bits
      *
      * 0   Hz - 1.1 Hz,  gain = 0  desired attenuation = -40 dB
      * 1.3 Hz - 2.5 Hz,  gain = 1  desired ripple = 5 dB
      * 2.7 Hz - 6.25 Hz, gain = 0  desired attenuation = -40 dB
      *
      *   --------     ------------------    --------------------   ---------------------   -----------
      *   | accel |    | raw thresold   |    |                  |   |                   |   |  state  |
      *   |       |----| to gate filter |----| Low Pass Filter  |---| Step Up/Down      |---| machine |-- increment
      *   |1      |    | output        2|    |                 3|   | cycle detection 4 |   |        5|   step count
      *    ---------    ------------------    --------------------   ---------------------   -----------
      *
      *  2,5 improvements identified and tested by Hugh Barney, July 2021
      *
      * V3.1 bypass the filter if the accelerometer is not over threshold for more than 1 second of samples
      * V3.2 fixed assignment issue in if statement
      * v3.3 over counting by 1 step after 5 reached, could be the last 1 second before we bypass the filter
      * v3.4 set X_STEPS_COUNT=5 (dont change again), use OFFSET and set V_REGISTER = 17
      * v3.5 X_STEPS_COUNT removed, now uses X_STEPS which is set to 7 following tests driving, OFFSET removed
      *
      *
      */
      
      const version = "3.5";
      
      
      const X_STEPS = 7;            // we need to see X steps in X seconds to move to STEPPING state
      const V_REGISTER = 17;        // raw threshold we must cross to start registering raw acceleration values
      const N_ACTIVE_SAMPLES = 12;  // number of samples raw acceleration must be over threshold before pass to filter
      const T_MAX_STEP = 1000;      // upper limit for time for 1 step (ms)
      const T_MIN_STEP = 167;       // lower limit for time for 1 step (ms)
      
      
      var bypass_filter = true;
      var active_sample_count = 0;
      
      // (3) Low Pass filter 
      var filter_taps = new Int8Array([ -2, 4, 4, 1, -1, 0, 2, -3, -12, -13, 2, 24, 29, 6, -25, -33, -13, 10, 11, -1, 3, 29, 41, 4, -62, -89, -34, 62, 110, 62, -34, -89, -62, 4, 41, 29, 3, -1, 11, 10, -13, -33, -25, 6, 29, 24, 2, -13, -12, -3, 2, 0, -1, 1, 4, 4, -2 ]);
      
      // create a history buffer the same lenght as the filter taps array
      var history = new Int8Array(filter_taps.length);
      
      /**
      * stepCounterThreshold of the filtered data used by the f/w by
      * default.  In practice increasing this threshold does not make a
      * lot of difference as the range of the filtered output is approx
      * -4000 to +4000.  This value ensures consistant Up / Down step edge
      * detection.  Any two places on a sine wave 180 degress apart will
      * do for detecting a full up / down cycle.
      *
      */
      const stepCounterThreshold = 1000;
      var stepWasLow = false;  // has filtered acceleration passed stepCounterThreshold
      var step_count = 0;      // total steps since app start
      var pass_count = 0;      // number of seperate passes through the STATE machine that lead to STEPPING
      var reject_count = 0;    // number of rejections through the STATE machine
      
      // acceleromter operates at 12.5Hz
      function onAccel(a) {
      
      // scale to fit and clip
      var m = a.mag;
      var v = ((m-1)*8192)>>5;
      
      /**
      * create a new Int8Array from the existing but starting from pos 1
      * of the existing history this drops off index 0 and moves
      * everything up, leaving the last entry to be filled with the new
      * value
      */
      history.set(new Int8Array(history.buffer,1));
      /**
      * (1) Accelerometer input data
      *
      * Set last value to the clipped value from the accel. The clipping
      * is needed otherwise the history will need to be stored in an
      * In16Array which will double the memory requirements of the
      * filter in the C code.
      */
      var raw_clipped = E.clip(v, -128, 127);
      history[history.length-1] = raw_clipped;
      
      
      /**
      * (2) Gating output of the filter
      *
      * The filter rings 5-6 cycles after 1 impulse which look like 5 or
      * 6 additional steps, when in fact we stopped 5 steps ago.
      *
      * The accelerometer responds to movement or lack of movement very
      * quickly and does not ring (unlike the filter). So we can rely on
      * the accelerometer telling us when we have stopped moving.
      *
      * We wait for N_ACTIVE_SAMPLES over threshold before we take the
      * output of the filter.  Likewise we stop taking the output of the
      * filter within N_ACTIVE_SAMPLES of having stopped moving.
      *
      * This reduces the impact of the ringing on the filter to within
      * +/- 1 second almost a perfect filter. It means we might over
      * count by 1 step due to late shutting off BUT this is blanced by
      * late switching on when we first detected acceleration.
      *
      */
      
      if ( v > V_REGISTER || v < -1*V_REGISTER ) {
      if (active_sample_count < N_ACTIVE_SAMPLES)
        active_sample_count++;
      if (active_sample_count == N_ACTIVE_SAMPLES)
        bypass_filter = false;
      } else {
      if (active_sample_count > 0)
        active_sample_count--;
      if (active_sample_count == 0)
        bypass_filter = true;
      }
        
      var accFiltered;
        
      if (bypass_filter) {
      accFiltered = 0;
      } else {
      // (3) Low Pass filter output - gated by (2)
      // digital filter, output has to be scaled down as the calculation is integer based, no floating point
      accFiltered = E.convolve(filter_taps, history, 0) >> 2;
      }
      
      var t = Math.round((getTime() - t_start)*1000);
      // this is useful to generate a CSV file of TIME, RAW, FILTERED values
      console.log(t + "," + raw_clipped + "," + accFiltered);
      
      // (4) check for steps, a bottom, followed by top threshold crossing = a step
      var hadStep = false;
      var t = 0;
      if (accFiltered < -stepCounterThreshold) {
      stepWasLow = true;
      } else if ((accFiltered > stepCounterThreshold) && stepWasLow) {
      stepWasLow = false;
      // We now have something resembling a step
      // now call state machine to ensure we only count steps when we have done X steps in X seconds
      hadStep = true;
      // (5) state machine
      step_count += step_machine.step_state();
      }
      }
      
      /**
      * (5) State Machine
      *
      * The state machine ensure all steps are checked that they fall
      * between T_MIN_STEP and T_MAX_STEP. The 2v9.90 firmare uses X steps
      * in Y seconds but this just enforces that the step X steps ago was
      * within 6 seconds (75 samples).  It is possible to have 4 steps
      * within 1 second and then not get the 5th until T5 seconds.  This
      * could mean that the F/W would would be letting through 2 batches
      * of steps that actually would not meet the threshold as the step at
      * T5 could be the last.  The F/W version also does not give back the
      * X steps detected whilst it is waiting for X steps in Y seconds.
      * After 100 cycles of the algorithm this would amount to 500 steps
      * which is a 5% error over 10K steps.  In practice the number of
      * passes through the step machine from STEP_1 state to STEPPING
      * state can be as high as 500 events.  So using the state machine
      * approach avoids this source of error.
      *
      */
      
      function STEP_STATE() {
      this.S_STILL = 0;       // just created state m/c no steps yet
      this.S_STEP_1 = 1;      // first step recorded
      this.S_STEP_22N = 2;    // counting 2-X steps
      this.S_STEPPING = 3;    // we've had X steps in X seconds
      this.reset();
      };
      
      STEP_STATE.prototype.reset = function() {
      this.state = this.S_STILL;
      this.hold_steps = 0;
      this.t_prev = getTime();
      };
      
      STEP_STATE.prototype.get_hold_steps = function() {
      return this.hold_steps;
      };
      
      STEP_STATE.prototype.step_state = function() {
      var st = this.state;
      var t;
        
      switch (st) {
      case this.S_STILL:
      //console.log("S_STILL");
      this.state = this.S_STEP_1;
      this.t_prev = getTime();
      this.hold_steps = 1;
      return 0;
          
      case this.S_STEP_1:
      t = Math.round((getTime() - this.t_prev)*1000);
      //console.log(t + " S_STEP_1");
      this.t_prev = getTime();
          
      // we got a step within 0.167s (6 min/mile) and 1 second
      if (t <= T_MAX_STEP && t >= T_MIN_STEP) {
        this.state = this.S_STEP_22N;
        this.hold_steps = 2;
      } else {
        // we stay in STEP_1 state
        reject_count++;
      }
      return 0;
      
      case this.S_STEP_22N:
      t = Math.round((getTime() - this.t_prev)*1000);
      //console.log(t + " S_STEP_22N");
      this.t_prev = getTime();
          
      // we got a step within 0.167s (6min/mile) and 1 second
      if (t <= T_MAX_STEP && t >= T_MIN_STEP) {
        this.hold_steps++;
      
        if (this.hold_steps >= X_STEPS) {
          this.state = this.S_STEPPING;
          this.hold_steps = 1;
          pass_count++;  // we are going to STEPPING STATE
          return X_STEPS;
        }
      } else {
        // we did not get the step in time, back to STEP_1
        this.state = this.S_STEP_1;
        this.hold_steps = 1;
        reject_count++;
      }
      return 0;
      
      case this.S_STEPPING:
      this.hold_steps = 1;
      t = Math.round((getTime() - this.t_prev)*1000);
      //console.log(t + " S_STEPPING");
      this.t_prev = getTime();
          
      // we got a step within T_MAX_STEP
      if (t <= T_MAX_STEP) {
        this.state = this.S_STEPPING;
        return 1;
      } else {
        // we did not get the step in time, back to STEP_1
        this.state = this.S_STEP_1;
        reject_count++;
      }
      return 0;
      }
      
      // should never get here
      return 0;
      };
      
      
      STEP_STATE.prototype.get_state = function() {
      switch(this.state) {
      case this.S_STILL: return "S_STILL";
      case this.S_STEP_1: return "S_STEP_1";
      case this.S_STEP_22N: return "S_STEP_22N";
      case this.S_STEPPING: return "S_STEPPING";
      default: return "ERROR";
      }
      };
      
      let step_machine = new STEP_STATE();
      
      /**
      * standard UI code for the App, not part of the algorithm
      */
      
      function draw() {
      g.clear();
      g.setColor(0);
      g.setColor(1,1,1);
      g.setFont("Vector",20);
      g.setFontAlign(0,-1);
      g.drawString(version + " " + step_machine.get_state() + "  ", 120, 40, true);
      g.drawString("Hold " + step_machine.get_hold_steps() + "  ", 120, 70, true);
      g.drawString("BATT: " + E.getBattery() + "%", 120, 100, true);
      g.drawString("Ps: " + pass_count + "  Rj: " + reject_count, 120, 130, true);
        
      if (running) {
      g.setColor(0xFFC0); // yellow
      g.setFont("Vector",60);
      g.drawString("" + step_count, 120, 160, true);
      } else {
      g.drawString("(" + step_count + ") BTN1 to START", 120, 170, true);
      }
      }
      
      var running = false;
      var t_start = 0;
      
      function onStartStop() {
      
      running = !running;
        
      if (running) {
      step_count = 0; // reset
      step_machine.reset();
      t_start = getTime();
      Bangle.on('accel',onAccel);
      } else {
      Bangle.removeListener('accel', onAccel);
      }
      }
      
      // handle switch display on by pressing BTN1
      Bangle.on('lcdPower', function(on) {
      if (on) draw();
      });
      
      
      // test2 - use these options through a sleep period
      // uncomment the 2 lines below
      running = false;  // will get negated by onStartStop()
      onStartStop();
      
      g.clear();
      setInterval(draw, 1000); // refresh every second
      draw();
      
      // test1 - START / STOP
      // uncomment to experiment using BTN1 for START / STOP
      // running = false;  // will get negated by onStartStop()
      //setWatch(onStartStop, BTN1, {repeat:true,edge:"rising"});
      
      

    1 Attachment

  • Will supply some more test results by the end of the day.
    I have attcahed the code above as it looks like the indenting got messed up.

    BTW - THE FILTER IS NEEDED. It does a good job at normalising the raw input into a sine wave that crosses the boundaries consistantly. Without it the detection of the UP/DOWN thresholds would be a nightmare and so would accurate detection of the period of a steps. The only issue with the filter is the ringing which I have provided some good solutions to take that out of the problem.

  • Here's my log of test results. This would indicate increasing X steps in X seconds reduces the over counting when doing every day tasks like sitting, sleeping driving. But that this is at the expense of 1% per step accuracy when comparing the counts on 1-2 miles of walking. The sweet spot would be around 7.

    Key:
    SLEEP - record count when wake up - how many steps did it count in your sleep
    WALK - a walk with step counts noted at start and end so that count during walking can be compared
    OVERALL - from getting up until end of day when you want to stop. The overall step count including sleep, work, walking, sitting etc.
    DRIVING - the step count while driving for a known period.


    1 Attachment

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

Better Pedometer - HELP NEEDED!

Posted by Avatar for Gordon @Gordon

Actions