-
-
-
Yes - I updated all apps that said they were out of date, which at the time was those 3.
The issue is indeed with Vertical Clock which appears to be broken now - maybe nothing to do with bootloader or the settings app.
I only noticed as I used it to look at something else and used Vertical Face to test something.
@Gordon - I can see you did a change in commit a4358600a0791e17cc5978c9ae0d08b0a584e6b4
Author: Gordon Williams gw@pur3.co.uk
Date: Mon Jun 28 11:13:47 2021 +0100verticalface 0.09: Fix time when minutes<10 and hours>9 (fix #767)
which introduced
Bangle.setUI("clockupdown", btn=>{
-
I have noticed a stability issue after updrading to bootloader v0.29 and settings v0.29.
I am using v2.09.90 firmwareWhat I observe is a double load of of the default clock.
Here's what I do to provoke the problem.
1) Go into the settings App
2) Change one of the settings, for example set LCD/Wake On/ Twist = On or Off
3) Go Back, Back in the settings app.I then briefly see a message 'updating boot0'
then 'Reloading'.
Then I see the deafult clock appear to draw, immediately by a second redraw.
So it looks like teh App has been loaded twice. (my just be 2 redraws).The same does not occur if you just do a BTN3 reset, you only appear to get the App loaded or redrawn
once.The reason I am reporting this is that prior to updating the bootloader and settings App - my Kitchen Combo app was running fine. But now I can see a repeatable out of memory error being reported. I know that when Kitchen Combo is loaded it uses 80% of memory and it does not allocate further memory. So something has changed.
If I use a BTN3 reset, the Kitchen Combo loads without a low memory error.
-
I updated to the latest Settings App (v0.27), Booloader (v0.27) and Vertical Clock (v0.9)
Whilst looking at another issue I set default clock to Vertical Clock and when I came out of settings
I got the following error.
In this state the Watch is locked, none of the buttons or touch controls will return you to the Launcher or settings App. The clock continues to run buts thats all.> ____ _ | __|___ ___ ___ _ _|_|___ ___ | __|_ -| . | _| | | | | . | |____|___| _|_| |___|_|_|_|___| |_| espruino.com 2v09.90 (c) 2021 G.Williams Uncaught Error: Unknown UI mode at line 31 col 38 throw new Error("Unknown UI mode"); ^ in function called from line 5 col 1952 ...HRM=[];}drawBPM(HRMstate);});Bangle.on('touch',function(button){if(b... ^ at line 6 col 325 ...PSPower(1);}else{eval(clockApp);delete clockApp;} ^ >
-
if you leave the Web IDE set to upload to 'Flash'
Its easy to get that wrong and accidently set to Flash when you mean Ram etc.
I have often wondered if it would be better to have seperate RAM / FLASH buttons in the IDE. When you want to upload to storage use the STORAGE button, when you want to upload to RAM use the RAM button, both of which are dafe. Then to upload to FLASH you can have a warning / are you sure dialogue.
-
-
-
-
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. -
GPS.prototype.loggingStatus
All this is doing is getting the status of whether gps logging is ON or OFF through the settings file.
It returns E-LOG if it cant find the settings file.
Its code I use internal to my App to check for an error condition.
EG - if someone had not installed the GPS logger.Its time I worked out how to state app dependancies for my app.
Until now gpsrec has not been a dependancy.It's good practice now to do Bangle.setGPSPower(on, 'appId') -
Nice one. I was not aware of this. Does this work on current firmware ?
I'll just update my app. -
I'd be happy to update a wiki. Its a lot quicker than managing updates through GIT.
I think with a wiki you get the tools to roll back changes if someone creates havoc.Might be better a good idea to have a few people updating the wiki to start with, ie limit to a set of usernames rather than a free for all. IE if you put your hand up and contribute you get an account.
A page of suggested apps/setups for the Bangle sounds like a good plan. Anyone willing to provide some?
I will write up my setup when I get a moment.
Maybe write a quick app to 'report_my_config' which will list all apps widgets, launchers, firmware installed in a short list. I know you can get this from the App Loader but it takes up a lot of space.
-
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.
-
@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"});
- Exercise tests - measured distance and expected step count against other devices. This code is now 96-99% accurate against a Amazfit GTS2.
-
-
@Gordon would it be possible to get a module interface to the GPSrec App/Widget.
I have just written some code to allow me to turn the recorder on / off through the gps watch face (Kitchen Combo) on a long BTN2 press. When the logger is off I can turn the GPS on through the gps face of kicthen combo. When I turn the logging on the widget will start to flash showing that logging is taking place. But when I turn the logger off through the code - it also turns the GPS off. What I need is a logging off but leave GPS on interface.
This is the code I have used at the moment.
GPS.prototype.toggleGpsLogging = function() { var settings = require("Storage").readJSON("gpsrec.json",1)||{}; if (settings == {}) return false; settings.recording = !settings.recording; require("Storage").write("gpsrec.json", settings); if (WIDGETS["gpsrec"]) WIDGETS["gpsrec"].reload(); return true; } GPS.prototype.loggingStatus = function() { var settings = require("Storage").readJSON("gpsrec.json",1)||{}; if (settings == {}) return "E-LOG"; if (settings.recording) return "ON"; return "OFF"; }
-
Is there a wiki or similar which would help us who don't follow the code figure out our watches?
This is a good start.
https://www.espruino.com/Troubleshooting+Bangle.jsThe docs are mainly aimed at people who want to develop Apps. Ideally every App should have its own README file which you can access through the App Loader but not every developer does this. There are some good Apps that dont have README files that really need them.
I've gone through numerous starts of using my watch but inevitably the config becomes >>unstable/unpredictable
Start with the basic minimal set of Apps. If your watch is not stable at that point them reach out for help in this forum. Then install the Apps that you want to use one by one and check that your watch continues to work.
Which Apps are you consistantly getting trouble with ?
Does loading more apps on the watch affect the memory errors in running apps?
First you need to understand how the watch works.
The code for the apps is stored in Flash storage.
When an App is loaded through the Loader it is loaded into the memory which I think (after the firmware has loaded) is about 40K of RAM which is not a great deal but enough for quite complex apps -like multiclock or Kitchen Combo. In general only 1 App runs at a time unless you have widgets running as well. When you go back to the App Launcher you are effectively just running an App that allows you to select an App to run next. At that point the App loader is no longer running. Most Apps will ue BTN2 to return you to the App Launcher.Can I remove apps to help avoid memory errors?
You can remove Apps through the App Loader, using the App Manager or you can simply delete the files that form the App through the IDE which gives access to the Flash storage file system. The safest way to remove an App is through the App Loader. If you are not sure what you are doing deleting files through the IDE could leave you stuck.
Which apps are always running opposite those which load when you start them?
When you get your basic watch only 1 App will be running at a time.
If you install widgets then these run at the same time as the one App that you have currently running.What base apps are needed for various functionality (e.g. its still unclear to me what role
gps/pedometer widgets/apps play in watch face functionality)?So gps and pedometer is a little confusing. There are a lot of GPS apps that only do one basic thing, like get the time, display your lat/lon etc. The GPS recorder app uses a widget in the background to do the logger and the GPS recorder App allows you change the settings and turn the GPS recorder on or off.
Pedometer is confusing as well as the Bangle firmware does not count steps; it gives an App that is listening an event to say 'I detected a step'. As far as I can tell originally someone wrote the pedometer widget to count the step events. However the step counter in the firmware is not very accurate so someone else produced ActivePedometer which was an attempt to get further accuracy. You should only use either the Pedomintor Widget OR the ActivePedometer widget - both will give you different step count depending on the settings you configure in the ActivePedometer App.
What is quiet mode?
As I understand it is is an attempt to be able to mute Apps from buzzing at inconvenient times. There is a way of setting a quiet period. I personally dont like the way this works as every app has to be modified to obey it but thats the way its been done.
If my watch crashes on startup is there anything I can besides resetting it?
It depends exactly what you mean by crash and start up.
If you are having start up problems my advice would be make sure you have the current firmware (2v09) then reset the watch to the default Apps. The establish that your watch is stable. If it is not stable then shout out to this forum and anyone that can help will respond. @Gordon is oncredibly supportive.
I would really like to see something like the Kitchen Combo be the default face if that can be made stable.
Just read your last line. I'm the creater of Kitchen Combo - glad you like it ?
What problems are you having with it ? Are you using the latest version which is v0.12 which you can tell when you run the App Loader website. Have you read the README file for Kitchen Combo ? https://github.com/espruino/BangleApps/blob/master/apps/kitchen/README.md. I would value your feedback so I can improve it. There is a section on error codes and if you see any of those codes you should follow the instructions to fix. Let me know if the instructions are clear enough.
Older versions of Kitchen Combo did have LOW MEMORY issues as I was trying to squeeze in too much functionality and wanted to use a screen buffer to make certain screens smoother. I recoded parts of the app so that it would use less memory and I sacrificed to a bit more flicker - but I wanted a reliable App. I run kitchen combo as my default clock App every day. It gets a lot of testing and use by me. So if you are having issues with it let me know what they are and I will try and help. Please note that I will be on holiday for a few days next week so I might not respond after Tuesday PM until the following week.
Kitchen Combo is actually one App but there are different (semi seperate modules) to allow you to seemlessly switch from one mini-app to another. Eg I wanted to see my OS grid reference in one face and then switch to the waypointer App and point to a waypoint (eg where's the car). The heart rate app for Kicthen combo does not really work and I have not bothered much to make it work as there are other / better heart rate apps in the apps library.
In time I plan to write a Settings App for Kitchen combo so you can select the Apps that you want to enable / disable. Just have not done it yet as I have been experimenting with seeing if I can work out how to get a more reliable step counter algorithm.
-
@Gordon at the moment I think accelog only records the X,Y,X values but not the magnitude SQRT(X*X + Y*Y + Z*Z). Having the g value is useful, but also agree m/s/s would be useful as well.
-
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 Luo2In 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 #1Worth 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. -
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"});
-
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
-
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.
-
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) 243I 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). -
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"});
Wondering what MicroPython has over Javascript for Bangle JS ?