-
• #2
Hi Joost,
That looks really promising - and it's great that the Wahoo sensor uses a standard Bluetooth LE format for its data as well!
I think you just want to start notifications on the characteristic - check out http://www.espruino.com/Reference#l_BluetoothRemoteGATTCharacteristic_startNotifications - there's some example code there that should do exactly what you want.
var gatt; NRF.requestDevice({ filters: [{ namePrefix: 'Wahoo' }] }).then(function(device) { console.log(device); return device.gatt.connect(); }).then(function(g) { gatt = g; return gatt.getPrimaryService("1816"); }).then(function(service) { console.log("Service:" + service); return service.getCharacteristic("0x2A5B"); }).then(function(characteristic) { characteristic.on('characteristicvaluechanged', function(event) { console.log("-> "+event.target.value); }); return characteristic.startNotifications(); }).then(function() { console.log("Done!"); // Then call gatt.disconnect(); if/when you want to disconnect });
event.target.value
should be a DataView so you should be able to pull out the relevant bits of the characteristic pretty easily. -
• #3
Cool, thanks a lot! Will try this later tonight, J
-
• #4
As a quick follow up, this works!
var last_wheel_event_time = 0, actual_wheel_event_time; var last_cumulative_wheel_revolutions = 0, cumulative_wheel_revolutions; var delta_revolutions, delta_event_time; var wheel_circumference = 2340; var delta_time_in_ms; function OnNotify(event) { //console.log("-> "+(event.target.value.getUint8(0, true)>>> 0).toString(2)); //characteristics flags cumulative_wheel_revolutions = event.target.value.getUint32(1, true); actual_wheel_event_time = event.target.value.getUint16(5, true); console.log("Cumulative wheel Revolutions: "+ cumulative_wheel_revolutions); console.log("Last Wheel Event time: "+ actual_wheel_event_time + " ms"); delta_revolutions = cumulative_wheel_revolutions - last_cumulative_wheel_revolutions; //console.log("-> "+event.target.value.getUint16(7, true)); //crank //console.log("-> "+event.target.value.getUint16(9, true)); //crank if (actual_wheel_event_time >= last_wheel_event_time) { delta_event_time = actual_wheel_event_time - last_wheel_event_time; } else { delta_event_time = 65536 - last_wheel_event_time + actual_wheel_event_time; } delta_time_in_ms =(delta_event_time / 1.024); console.log(delta_event_time); console.log("delta_time_in_ms:" + delta_time_in_ms); last_wheel_event_time = actual_wheel_event_time; last_cumulative_wheel_revolutions = cumulative_wheel_revolutions; } var gatt; NRF.requestDevice({ timeout:20000, filters: [{ namePrefix: 'Wahoo' }] }).then(function(device) { console.log(device); return device.gatt.connect(); }).then(function(g) { gatt = g; return gatt.getPrimaryService("1816"); }).then(function(service) { //console.log("Service:" + service); return service.getCharacteristic("0x2A5B"); }).then(function(characteristic) { characteristic.on('characteristicvaluechanged', OnNotify); return characteristic.startNotifications(); }).then(function() { console.log("Done!"); // Then call gatt.disconnect(); if/when you want to disconnect });
Thanks a lot, J
-
• #5
That's great! Thanks for posting the code up.
I'd be really interested to see what you make with it!
-
• #6
Hey Joost! (Thanks Gordon for pointing me here!)
This looks like a brilliant bit of kit - and making your own head unit sounds like an awesome project. I'd be really interested in hearing updates too.
I'm planning to make a tracker which would only check values every hour (I'm planning to capture bike usage over several months). Do the cumulative revolutions increase whilst you're not connected? If so, I'm definitely going to pick up one of these.
-
• #7
Hi Benjamin,
the Wahoo Speed Sensor resets its "cumulative wheel revolutions" counter when switching off, so you will have to monitor the transmitted data continously with enabling the notifications for the service.
As it seems, we have similar goals with creating an unobtrusive device. Right now I did not advance, as I am awaiting the delivery of the MDBT42 Breakout board, which I ordered just vey recently. I think it will arrive next week (international shipping). The code above was tested with a Puck.js by the way.
My plan is to add a small LiPo battery (around 300-500mAh), a small LiPo charger and an ePaper display. My device will be switched on/active when cycling, I also want it to update the displayed data every couple of seconds. Perhaps it might be an option to add an IMU or vibration sensor to let it sleep when the bike is parked, and awaken when moving the bike - that'd be cool. I guess I will check with Gordonif he has any comments/hints for a very low power state with the bluetooth radio switched off, just monitoring a GPIO. -
• #8
Hi! Just sitting there monitoring a GPIO with setWatch is pretty low-power - your idea of using a vibration sensor is nice and easy. There are some Power consumption figures for Puck.js here - http://www.espruino.com/Puck.js#power-consumption
Only thing to watch out for is you want a vibration sensor that is normally not shorted. If it's shorted then just the current across the pullup resistor will probably make up the majority of your power draw!
-
• #9
Thanks Gordon, I ordered one of those SW-18020P (e.g. https://www.sunrom.com/p/vibration-sensor ), hopeful it'll work!
-
• #10
Hi,
anyone knows why this code
function setDisplayMode(displaymode) { print("setDisplayMode: ", displaymode); switch (displaymode) { case "switchToConnected": Pixl.menu(); break; case "connecting": if (timer!==-1) {clearInterval(timer); timer = -1;} Pixl.menu(); connect(); break; case "livedata": print("livedata"); Pixl.menu(); if (timer==-1) { print("Set timer"); timer = setInterval( function () {showLiveData(); }, 1000); print("timer = ", timer); } break; case "menu": if (timer!==-1) {clearInterval(timer); timer = -1;} m=Pixl.menu(mainmenu); g.setFont8x12(); m.draw(); } }
gives me this runtime error?
>setDisplayMode: menu =undefined setDisplayMode: connecting Starting scan... Uncaught SyntaxError: BREAK statement outside of SWITCH, FOR or WHILE loop at line 119 col 12 break; ^ in function "setDisplayMode" called from line 1 col 28 setDisplayMode("connecting"); ^ in function "b" called from line 1 col 48 var b=d[c[a.selected]];"function"==typeof b&&b() ^ in function "select" called from line 1 col 10 b.select() ^ in function called from system
I looked it up a hundred times, this should be correct switch/case syntax, or is it not?
-
• #11
Can you show a little bit more of the code? I wonder about the connect being some async/callback thing that messes with you... Worst case - comes push to shove - it may be that the (async) code executing 'in'
connect(...
makes the JS interpreter loose the context and returns unable to match thebreak
with theswitch
it looks to be in... (if we can trust the console output - time sync correctly...). Try this far shot by wrapping connect() into a setTimeout(...:setTimeout(connect,1);
(connect() must be a globally / level 0 defined function of yours, is it?).Btw, you can simply say
if (timer) timer = clearInterval(timer);
(and converselyif (!timer) { ...
- in line 15) with globally declaringvar timer;
.What are you using the timer for? ...update the display every second?
-
• #12
Thanks for your answer!
Here is the complete code:
var actual_menu, gatt; var last_wheel_event_time = 0, actual_wheel_event_time; var last_cumulative_wheel_revolutions = 0, actual_cumulative_wheel_revolutions = 0, track_cumulative_revolutions = 0; var delta_revolutions, delta_event_time, timer = -1; var wheel_circumference = 2340; var delta_time_in_ms, connected = false; function connect() { print("Starting scan..."); g.clear(); var s = "Connecting..."; g.drawString(s ,0 ,0); g.flip(); try { NRF.requestDevice({ timeout:20000, filters: [{ namePrefix: 'Wahoo' }] }).then(function(device) { console.log(device); return device.gatt.connect(); }).then(function(gatt_object) { gatt = gatt_object; s = "Looking for service..."; g.clear(); g.drawString(s ,0 ,0); g.flip(); return gatt.getPrimaryService("1816"); }).then(function(service) { s = "Looking for char..."; g.clear(); g.drawString(s ,0 ,0); g.flip(); return service.getCharacteristic("0x2A5B"); }).then(function(characteristic) { characteristic.on('characteristicvaluechanged', OnNotify); return characteristic.startNotifications(); }).then(function() { connected = true; setDisplayMode("livedata"); console.log("Done!"); // Then call gatt.disconnect(); if/when you want to disconnect }); } catch(error) { print("Sensor not found", error); g.clear(); g.drawString("No sensor found..." ,0 ,0); g.flip(); } } function disconnect() { if (gatt) { gatt.disconnect(); } connected = false; print("Connected: ", connected); setDisplayMode("menu"); } function OnNotify(event) { //console.log("-> "+(event.target.value.getUint8(0, true)>>> 0).toString(2)); //characteristics flags actual_cumulative_wheel_revolutions = event.target.value.getUint32(1, true); actual_wheel_event_time = event.target.value.getUint16(5, true); //console.log("Cumulative wheel Revolutions: " + actual_cumulative_wheel_revolutions); //console.log("Last Wheel Event time: "+ actual_wheel_event_time + " ms"); delta_revolutions = actual_cumulative_wheel_revolutions - last_cumulative_wheel_revolutions; //console.log("-> "+event.target.value.getUint16(7, true)); //crank //console.log("-> "+event.target.value.getUint16(9, true)); //crank if (actual_wheel_event_time >= last_wheel_event_time) { delta_event_time = actual_wheel_event_time - last_wheel_event_time; } else { delta_event_time = 65536 - last_wheel_event_time + actual_wheel_event_time; } delta_time_in_ms =(delta_event_time / 1.024); //console.log("delta_time_in_ms:" + delta_time_in_ms); last_wheel_event_time = actual_wheel_event_time; last_cumulative_wheel_revolutions = actual_cumulative_wheel_revolutions; track_cumulative_revolutions += delta_revolutions; } var mainmenu = { "" : { "title" : "-- Main Menu --", "fontHeight" : 10 }, "Connect" : function () { setDisplayMode("connecting"); }, "Disconnect" : function () { disconnect(); }, "Live Data" : function() {setDisplayMode("livedata"); }, "New Track" : function () {}, "Toggle Backlight" : function() { LED1.toggle(); }, "-> Settings" : function () {Pixl.menu(SettingsMenu); }, //"Exit" : function() { Pixl.menu(); }, }; var SettingsMenu = { "" : { "title" : "-- Settings --", "fontHeight" : 10 }, "Set Wheel Circumference" : undefined, // do nothing "Set Total Distance Counter" : undefined, // do nothing "< Back" : function() { Pixl.menu(mainmenu); }, }; function showLiveData() { //print("showlivedata"); g.clear(); var s = "Cum. Wheel Revolutions: "; g.drawString(s ,0 ,0); //95 - g.stringWidth(s) g.getWidth() g.drawString(actual_cumulative_wheel_revolutions, 0, 11); var speed = delta_revolutions * wheel_circumference / delta_time_in_ms * 60 * 60 / 1000; s = "Speed: " + String(speed.toFixed(2)); g.drawString(s, 0, 22); s = "Track distance: " + (track_cumulative_revolutions * wheel_circumference / 1000 / 1000).toFixed(2); g.drawString(s, 0, 33); g.flip(); } function onInit () { require("Font8x12").add(Graphics); setDisplayMode("menu"); setWatch(function () { setDisplayMode("menu"); }, BTN2, {repeat:true, debounce:50}); } function setDisplayMode(displaymode) { print("setDisplayMode: ", displaymode); switch (displaymode) { case "switchToConnected": Pixl.menu(); break; case "connecting": if (timer!==-1) {clearInterval(timer); timer = -1;} Pixl.menu(); setTimeout(function() {connect();},1); break; case "livedata": print("livedata"); Pixl.menu(); if (timer==-1) { print("Set timer"); timer = setInterval( function () {showLiveData(); }, 2000); print("timer = ", timer); } break; case "menu": if (timer!==-1) {clearInterval(timer); timer = -1;} m=Pixl.menu(mainmenu); g.setFont8x12(); m.draw(); } } onInit();
Though sadly your idea with
setTimeout(connect, 1);
did not work out; returning the same error. Thanks again!
-
• #13
Thanks - I just checked on this and it's a bug in
Pixl.menu
in the Pixl.js firmware (it's slightly special as it is implemented in JS, but the act of executing it seems to mess with the execution state).I'll try and get a fix in for it, but in the mean time, add this code up the top of what you upload and it'll fix it:
Pixl._menu = Pixl.menu; Pixl.menu = function(m) { Pixl._menu(m); };
-
• #14
Hi Gordon, thanks a lot!
The switch/break error seems fixed; now something creeped in giving me
____ _ | __|___ ___ ___ _ _|_|___ ___ | __|_ -| . | _| | | | | . | |____|___| _|_| |___|_|_|_|___| |_| espruino.com 1v99 (c) 2018 G.Williams >setDisplayMode: menu Uncaught Error: Cannot read property 'draw' of undefined at line 143 col 20 pixlmenu.draw(); ^ in function "setDisplayMode" called from line 115 col 24 setDisplayMode("menu"); ^ in function "onInit" called from line 1 col 8 onInit(); ^
(also not working if renaming pixlmenu back to "m", as I had in my code before).
This destroys the larger menu font you helped me with yesterday.This being the code right now:
var actual_menu, gatt; var last_wheel_event_time = 0, actual_wheel_event_time; var last_cumulative_wheel_revolutions = 0, actual_cumulative_wheel_revolutions = 0, track_cumulative_revolutions = 0; var delta_revolutions, delta_event_time, timer = -1; var wheel_circumference = 2340; var delta_time_in_ms, connected = false; Pixl._menu = Pixl.menu; //Bug Fix von Gordon, s. Forum Pixl.menu = function(m) { Pixl._menu(m); }; //Bug Fix function connect() { print("Starting scan..."); g.clear(); var s = "Connecting..."; g.drawString(s ,0 ,0); g.flip(); try { NRF.requestDevice({ timeout:20000, filters: [{ namePrefix: 'Wahoo' }] }).then(function(device) { console.log(device); return device.gatt.connect(); }).then(function(gatt_object) { gatt = gatt_object; s = "Looking for service..."; g.clear(); g.drawString(s ,0 ,0); g.flip(); return gatt.getPrimaryService("1816"); }).then(function(service) { s = "Looking for char..."; g.clear(); g.drawString(s ,0 ,0); g.flip(); return service.getCharacteristic("0x2A5B"); }).then(function(characteristic) { characteristic.on('characteristicvaluechanged', OnNotify); return characteristic.startNotifications(); }).then(function() { connected = true; setDisplayMode("livedata"); console.log("Done!"); // Then call gatt.disconnect(); if/when you want to disconnect }); } catch(error) { print("Sensor not found", error); g.clear(); g.drawString("No sensor found..." ,0 ,0); g.flip(); } } function disconnect() { ... } function OnNotify(event) { ... } var mainmenu = { "" : { "title" : "-- Main Menu --", "fontHeight" : 10 }, "Connect" : function () { setDisplayMode("connecting"); }, "Disconnect" : function () { disconnect(); }, "Live Data" : function() {setDisplayMode("livedata"); }, "New Track" : function () {}, "Toggle Backlight" : function() { LED1.toggle(); }, "-> Settings" : function () {Pixl.menu(SettingsMenu); }, //"Exit" : function() { Pixl.menu(); }, }; var SettingsMenu = { "" : { "title" : "-- Settings --", "fontHeight" : 10 }, "Set Wheel Circumference" : undefined, // do nothing "Set Total Distance Counter" : undefined, // do nothing "< Back" : function() { Pixl.menu(mainmenu); }, }; function showLiveData() { } function onInit () { require("Font8x12").add(Graphics); setDisplayMode("menu"); setWatch(function () { setDisplayMode("menu"); }, BTN2, {repeat:true, debounce:50}); } function setDisplayMode(displaymode) { print("setDisplayMode: ", displaymode); switch (displaymode) { case "switchToConnected": Pixl.menu(); break; case "connecting": if (timer!==-1) {clearInterval(timer); timer = -1;} Pixl.menu(); connect(); break; case "livedata": print("livedata"); Pixl.menu(); if (timer==-1) { print("Set timer"); timer = setInterval( function () {showLiveData(); }, 2000); print("timer = ", timer); } break; case "menu": if (timer!==-1) {clearInterval(timer); timer = -1;} pixlmenu=Pixl.menu(mainmenu); g.setFont8x12(); pixlmenu.draw(); } } onInit();
-
• #15
Ahh, ok - try:
Pixl._menu = Pixl.menu; Pixl.menu = function(m) { return Pixl._menu(m); };
-
• #16
That worked, great! Thanks a lot, J
-
• #17
Hi, I'm an engineering student trying to get an output from the wahoo cadence sensor but I am using an Arduino Uno and an Adafruit Bluefruit LE SPI Friend.
I'd love some help in getting the code you have here working or adapted for use with arduino.
Any help would be great! (I'm running mac too by the way, judge me as necessary...)Kieran
-
• #18
@kierankay, hat's a tough task you take onto you... the way that Arduino works - polling loop - versus Espruino - interrupt driven - puts itself in way of adapting this code. Furthermore, check on the implementation of Promises in Arduino world. Depending what the adafruit libs give you (setting some flags on filtered reception of data and buffer thereof) and availability of a lib or your on implementation of Promises you should be able to lean on the main line and structure of the Espruino code...
Hi everybody and esp. Gordon (thanks for Espruino!!),
I'm looking into receiving sensor readings from the commercially sold CSC sensor Wahoo Speed (https://www.amazon.com/Wahoo-Cycling-Speed-Sensor-Bluetooth/dp/B01DIE7LUG/ref=sr_1_3?s=sporting-goods&ie=UTF8&qid=1532458958&sr=1-3&keywords=wahoo+speed) which came with my bike computer Wahoo ELEMNT Mini, of which I want to replace the cumbersome headunit.
The sensors sends data via BLE with the CSC service:
https://blog.bluetooth.com/part-2-the-wheels-on-the-bike-are-bluetooth-smart-bluetooth-smart-bluetooth-smart
https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.csc_measurement.xml
Using NRF connect on Android, the sensor readings are shown easily after enabling the notification.
Now to the crucial part (for me):
This code:
gives me this output:
Can anyone point me in the right direction how I can enable and receive the CSC measurement? Sorry I'm a dummy with programming/JavaScript...
The path to go I imagine would probably consist of writing to 0x2902 characteristic to enable notifications and have some kind of callback receive and interpret the data.
Later on I plan on using the new MDBT42Q Breakout, connecting a Waveshare epaper display and 3d print a small case and have it running on a 18650 battery. Another option would be to use Pixl.js with a custom case and some kind of battery managment.
Thanks for any help!!
Best regards,
Joost