-
I have just tested a garmin heartrate strap compared to the Heart Rate Monitor app of Bangle2. The readings are Way off, so far that the strap was reading 120 and the app 60. This was whilst being on a gym bike. So I don't know what to call the current HR reliability, its almost non-existent I would imagine.
It has also shown 120, whilst the strap shows 80. Considering the watch is fed accel data, would the faulty readings(spikes noticed with onTap) affect it?
Part of the reason the first test was super bad for me was because riding a stationary gym bike doesnt' trigger accel changes, so automatic sportsMode didnt' kick in. After using
Bangle.setOptions({hrmSportMode: 1})
, it improved, but sometimes in a bad way, like above.I will try hrmSportMode 2, for bike. Ok results: I was actually surprised how
well
it keeps up with the hrm in sportsMode 2, seriously. It seems to struggle going above 100, if eg. the strap says 130, it can remain at 100.
But its accuracy within 70-100 is reallygood
. So its doing something right!But something is also wrong with it :D, like it gets stuck often. Or wanders into weird hrm territory for no reason.
Btw its fair to assume hrmSportMode 1==2, as gordon pointed out previously, haven't noticed a difference between the two yet.
-
-
This has a rolling average windows of size 8. And includes the hrm band pass filter from heartrate.c
https://gist.github.com/d3nd3/0f15665e2dfe03c949449bfae9a50ae5
-
To set constraints of graph with min/max...
let nonCircVals = []; let min = 40000; let max = -40000; //circular to non-circular. for ( let i = 0; i < windowSize; i++ ) { let v = vals.obj[(tick + 1 + i) % windowSize]; if ( v < min ) min = v; if ( v > max ) max = v; nonCircVals.push(v); } print(`min is ${min}, max is ${max}`); //black require("graph").drawLine(g, nonCircVals,{axes:false,miny:min,maxy:max,gridy:(Math.abs(max-min))/8});
The heartrate.c implementation also applies a band pass filter, which greatly helped before.
-
This code can be used to view the data, might be useful. It is using analogRead from old method of reading pin directly instead of talk through i2c. Could be interesting to compare outputs. Pass hrm.raw into instead of val to c.onHRM to see difference. (You have to adjust graph constraints too, hrm.raw are huge numbers between 0 - 10000 ).
EDIT:It is using wrong pin (29), its not hrm data
var c = E.compiledC(` // void onHRM(int,int,int) const unsigned char windowSize = 25; typedef struct inputs { short * vals; } inputs_t; void onHRM(unsigned int &tick,int val,inputs_t *argss){ argss->vals[tick % windowSize] = val; tick++; } `); let newUint32FlatArray = len => { let fs = E.toFlatString((new Uint32Array(len)).buffer); if ( !fs ) throw new Error("Required to be a flatstring"); let obj = new Uint32Array(E.toArrayBuffer(fs)); return {"obj" : obj, "ref" : E.getAddressOf(obj,true) }; }; let newInt16FlatArray = len => { let fs = E.toFlatString((new Int16Array(len)).buffer); if ( !fs ) throw new Error("Required to be a flatstring"); let obj = new Int16Array(E.toArrayBuffer(fs)); return {"obj" : obj, "ref" : E.getAddressOf(obj,true) }; }; Bangle.setLCDPower(1); Bangle.setHRMPower(1); Bangle.setOptions({ backlightTimeout:0, powerSave:false, hrmGreenAdjust: false }); Bangle.setPollInterval(80); let windowSize = 25; var vals = newInt16FlatArray(windowSize); var inputs = newUint32FlatArray(6); inputs.obj[0] = vals.ref; let tickFlat = newUint32FlatArray(1); tickFlat.obj[0] = 0; let tickCountOld = 0; //This is called by idleLoop. Not an interrupt. Bangle.on('HRM-raw', function(hrm) { let val = Math.round(analogRead(29)* 16383); c.onHRM(tickFlat.ref,val,inputs.ref); }); //25Hz, Bangle.js 2 //50Hz, Bangle.js 1 setInterval(function hmm(){ let tick = tickFlat.obj[0]; tickCountOld = tick; g.clear(true); g.setFont("4x6:0.5"); let nonCircVals = []; //circular to non-circular. for ( let i = 0; i < windowSize; i++ ) { nonCircVals.push(vals.obj[(tick + 1 + i) % windowSize]); } //black require("graph").drawLine(g, nonCircVals,{axes:false,miny:-50,maxy:50,gridy:25}); },5000);
-
Bangle.setOptions({ hrmGreenAdjust: false });
Bangle.hrmWr(0x17,15)
Adjust 15, to change intensity.
===================
Regarding the data, I agree and have same results. I assume the old way of read pin directly with analogRead produced better results for raw ppg manipulation. You would have to uncomment:# Standard open-source heart rate algorithm:
# 'SOURCES += libs/misc/heartrate.c',Line 74 of boards/BANGLEJS2.py, and recomment the current one.
Then it doesn't read from
Register 0x80 via I2C (HEARTRATE_PIN_SDA)
(hrm_vc31.c @vc31b_readfifo) , but insteadHEARTRATE_PIN_ANALOG 29
(hrm_analog.c @hrm_sensor_on).That is as much as I know atm. However the actual readings of BPM are considered quite good even though its routed through the binary blob, but not sure why it forces the raw values to be bad.
EDIT: HEARTRATE_PIN_ANALOG 29 is for
Bangle.js 1 only
!! -
let r = E.getAddressOf(f.buffer,true); print(`addr aligned 4: ${r % 4}`);
I can now confirm that alignment is the thing that affect all cases. I was just lucky before.
so
Somehow code loaded at boot.js stage is more aligned? crazy.
is false and just based on luck.
Still haven't figured out the size confusion yet though.
-
That is interesting information. So my theory about it working from storage isn't so complete. I further compared upload as "test.js" and used load("test.js") to call it. It doesnt' work there, the example where it works is when I load the file by renaming it to
test.boot.js
.I checked the alignment of the address, and it seems related to what you describe. Somehow code loaded at boot.js stage is more aligned? crazy.
Unrelated...
>let f = new Float32Array(1); =new Float32Array(1) >f[0] = 0.5 =0.5 >E.getAddressOf(f.buffer,true) =195582 >peek32(E.getAddressOf(f.buffer,true)).toString(16) ="4"
This is similar to your code above, but in the Emulator. I get weird output always "4".
Also this might sound stupid, but how can it store the float in 1 block(16bits). I print the float with :
>trace(f) #4452[r1,l1] Float32Array (offs 0, len 1) #5210[r1,l0] ArrayBuffer (offs 0, len 4) #2358[r1,l0] String [1 blocks] "\0>\x1CF" =undefined >peek32(r).toString(16) ="461c3e00" >f[0] =9999.5
Its using 24 bits, 0x46, 0x1c,0x3e. Yet 1 block? How is that possible?
process.memory().blocksize
prints14
E.getSizeOf()
for the flat string was 4. ( One block for the Float32Array view, One block for the ArrayBuffer view, 2 block for the backing flatstring data )for the normal string backed example, it was 3. ( One block for the Flaot32Array view, One block for the ArrayBuffer view, 1 block for the backing string data ).
Is my understanding correct of the above?
-
It could be that
getAddressOf(var,true)
does not properly do what its described it does? Because it does not return 0 when an apparent non flatstring based flat ArrayBuffer is given to it.Mb its wrong to assume that all Int32Array etc are flat, cos if you give small length, its not?
["flatAddress","bool","(boolean) If `true` and a Flat String or Flat ArrayBuffer is supplied, return the address of the data inside it - otherwise 0. If `false` (the default) return the address of the JsVar itself."]
Unless I am missing some information about flat array buffers, perhaps it does not refer to the underlying buffer...
That is why I didn't think about flatstring being the culprit for so longer, was so happy checking the return value of E.getAddressOf(). Perhaps it only applies to strings?
E.toString() notes -> You can still check if the returned value is a Flat string using E.getAddressOf(str, true)!=0, or can use E.toFlatString instead.
The reason it was important for this to work with
RAM Upload
was because that is how I test most of my development, I didn't want to make LOADS of writes to the flash just when i am debugging my code. -
-
Thanks for that heads up. I have tested that the return value from E.getAddressOf is not 0, and I still have to upload to file for it to work. Mb its related to that old post I made about the interpreter uploading line by line idk, I'm trying to think how it can go wrong here, its essentially a call to
E.NativeCall(offsetInBlob, funcTemplate, binaryBlob)
. Thinking...Btw: Whats really annoying/strange is that if you add some random lines of code, like eg. adding
print("a")
into the function, it magically starts working. So such tiny irrelevant edits contribute to the crash (when running via ram)... -
So when I wrap stuff in functions, it crashes, when the code is not inside a function call, it works. This must be a bug...?
Nevermind, so when I save this as a file on the watch its self. It doesn't have an issue running it. Why is this? I always get confused with the
prompt (save to ram)
and its execution differences to file-based running. -
This code is a simpler layout of the above by reference. It could be there is an error in the wrapper stuff in cFloat object.
This mb works? Not sure yet.
var c = E.compiledC(` // void fadd_byref(int,int,int) typedef union { float f; int i; } floatint; void fadd_byref(floatint* arg1, floatint* arg2, floatint* arg3) { floatint farg1,farg2; farg1.i=arg1->i; farg2.i=arg2->i; floatint out; out.f = farg1.f + farg2.f; arg3->i = out.i; } `); let f = new Float32Array(1); let r = E.getAddressOf(f,true); print(`a : ${r}`); f[0] = 0.3; let f1 = new Float32Array(1); let r1 = E.getAddressOf(f1,true); print(`a : ${r1}`); f1[0] = 0.63; let f2 = new Float32Array(1); let r2 = E.getAddressOf(f2,true); print(`a : ${r2}`); c.fadd_byref(r,r1,r2); print(f[0]); print(f1[0]); print(f2[0]);
Edit: Yes this works, there is something wrong with the cFloat wrapper function I created. Yet I dont know what it is. So good news for the float ref working I guess.
Make sure to use this:
let fs = E.toFlatString(new Float32Array(1).buffer); if ( !fs ) throw new Error("Required to be a flatstring"); let f = new Float32Array(E.toArrayBuffer(fs));
-
It took me a while to get working, so thought I'd share what worked. Can be useful for off-loading some heavy duty calculations to c, to increase throughput.
var c = E.compiledC(` // int fadd_byval(int,int) // void fadd_byref(int,int,int) typedef union { float f; int i; } floatint; int fadd_byval(int arg1,int arg2) { floatint farg1,farg2,farg3; farg1.i=arg1;farg2.i=arg2; farg3.f = farg1.f + farg2.f; return farg3.i; } void fadd_byref(floatint* arg1, floatint* arg2, floatint* arg3) { floatint farg1,farg2; farg1.i=arg1->i; farg2.i=arg2->i; floatint out; out.f = farg1.f + farg2.f; arg3->i = out.i; } `); { let cFloatArray = (val,len)=>{ val = typeof val !== 'undefined' ? val : 0.0; len = typeof len !== 'undefined' ? len : 1; let fs = E.toFlatString((new Float32Array(len)).buffer); if ( !fs ) throw new Error("Required to be a flatstring"); let f = new Float32Array(E.toArrayBuffer(fs)); let i = new Int32Array(f.buffer); let r = E.getAddressOf(f.buffer,true); if ( r % 4 != 0 ) throw new Error("Required to be a aligned"); f.fill(val); return { f: f, i: i, ref: r, store: s=>{ i[0]=s; }, get: ()=>f[0] }; }; //pass by val. let f1 = cFloatArray(0.9); let f2 = cFloatArray(1.2); let fSum = cFloatArray(); fSum.store( c.fadd_byval(f1.i[0],f2.i[0]) ); print(`pass by val answer : ${fSum.get()}`); //pass by reference. f1 = cFloatArray(0.65); f2 = cFloatArray(3.4); let fOut = cFloatArray(); c.fadd_byref(f1.ref,f2.ref,fOut.ref); print(`pass by ref answer : ${fOut.get()}`);
1) Use string literal `` in E.compiledC().
2) Keep it declared in var = global scope.
4) Flatstring + 4 byte Alignment (Thanks fanoush)Edit: Getting the ref version to work was extremely time-consuming because of some strange crash of reading the variables directly + performing floating math on them. Yet copying the inputs off of the stack seems to solve it. Also it is necessary to write to the pointers in int form ,not float.
Edit: Fixed the crashes by forcing flat string. (cos alignment) -
The accel increase due to HRM can be anywhere between 1.5x -> 15x . Thats why increasing threshold does not eliminate it completely but you can increase the threshold further ::
//bit is changed to update settings var c = Bangle.accelRd(0x18); // CNTL1 Bangle.accelWr(0x18, c &~0x80); //adjust threshold, where 0x25 is the threshold value (increase/decrease) Bangle.accelWr(0x27, 0x25); //restore Bangle.accelWr(0x18, c);
0x25 is hex, so 0x20 is lower, 0x30 is higher
-
I wanna just clear up that my previous statements were based on false evidence. The scripts I used to collect the data were causing the bias. I didn't have time to double check. So paranoia say bye bye.
But today, I did double check and there was no differences across firmware v18 and v19. So all guchi!.
-
EDIT: Ignore this post. False.
@Gordon I am still trying to wrap my head around it.
I will do continue investigation.
My latest findings point to toggling health timer to 3 mins in health settings. This enables the 'power issue', yet strangely when you disable it, it still remains. I'll keep you posted.I mean the above, in the context of using your setHRMPower script with accel events.
I also have to test this on 2v18 too.
-
EDIT: This response is based on false evidence.
I tested every firmware from 2v11 to 2v19. Using gordon's script printing accel values.Results are that v19 has anomalies which are not present in v18. No where near as high. Still, I am unable to explain OP's condition or find a test-case that screen turn on without
wakeOnTouch
. -
@user147320 it'd do us a favour if you ran
console.log(Bangle.getOptions())
on left hand side of ide whilst experiencing the bug and pasting output here. To clear that confusion up. -
Bangle 2 and the accel wakeOnTouch has been around for a while, yet we didn't have those issues around?. Has the hr usage always been having this influence on other components or is it related to the third-party algorithm?
Whats the new variable here to cause it?
@Gordon I did forget that it uses accel, was assuming it has another touch sensor similar to bangle 1.
Is there an older firmware version of b2 that does
not
have this problem? -
I do not. How to recreate? Edit: I just experienced it, I set only
wake on touch
.I made further testing and requires :
- wake on touch
Bangle.setHRMPower(true)
some timestamps in ms between each wake, with watch on desk face down to keep hr light on:
62529 58397 11837 32309 13753 5924 29351 6560 27113 796 11840 8795
Bangle.setOptions({hrmPushEnv:true}); console.log(Bangle.getOptions()); Bangle.setHRMPower(true); console.log(Bangle.getOptions());
It seems calling
setHRMPower(true)
forcefully sets/overrides my own values,hrmWearDetect
is set totrue
, andhrmPushEnv
is set tofalse
. So I to have to set those options after callingsetHRMPower()
Bangle.on("HRM-env",(e)=> { console.log(e); });
My readings are all
0
for this event. Is that expected? The documentation doesn't describe whatenvironment
means either. - wake on touch
-
-
-
https://strawpoll.com/GPgV6Lr74ga
I noticed there are some inconsistences with scroll. When you first install the watch the
welcome
app wants you to scrollnon-inversely
. I personally prefer inverted scroll because its more common on phones and feels like I am rotating a dial. Another example is thelauncher
which usesinverse scroll
, and feels awesome, another example is the vertical scroll insettings
menu for changing a setting value and it usesnon-inverse scroll
.I recommend all scrolls try to use the same type of scroll for consistency, then an option can be required in settings to change it. Or hard-code what people prefer, using the poll above.
I think hrmSportsMode is the algorithm that becomes dependent on Accel data, because in the example source it takes x,y,z input. hrmSportsMode 0 also worked well for me too at times. I think its either slow and has to take a while to follow true heartrate or the signal is sometimes too noisy for the current alg.
Because at times, I am very impressed, but its short-lived. Does anyone know if its best to measure time between 2 consecutive beats, or to count peaks over time? I'm starting to think the time-measuring is the way to go.