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"});
Espruino is a JavaScript interpreter for low-power Microcontrollers. This site is both a support community for Espruino and a place to share what you are working on.
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.
1 Attachment