• Hello,

    Here is today's installment of "What Colin did in lockdown today".

    I was teaching someone to program yesterday, and we made a bike computer using a Pixl.js to display speed and distance on the Pixl display using an sensor from an old broken wired Cateye bike computer we had. This sensor is a reed switch and a magnet.

    I thought "I have a Puck.js with Bluetooth and magnetometer" so had a go at making a sensor for my phone using the Puck.

    When running this code the Puck appears as a bluetooth cycling sensor ("CSC profile") to use the jargon and can be read by apps on a mobile phone. I tried "Jepster" on Android and the nRF toolbox app.

    The theory is that the Puck could be mounted on the front fork of a bicycle, with a magnet on a spoke. When you bring the magnet nearby the LED on the Puck lights, and a reading is sent to the phone. When the magnet rotates away the LED goes off.

    There is a commented out bit of code that uses setWatch so the same code could run with a bike computer sensor - this probably gives longer battery life than using the magnetometer.

    I did discover if you alter the time between pulses the app seems quite happy to believe I can cycle at 80,000 km/h - take that, competitive Strava users :-)

    I haven't actually tested this on a bike, but I think it works. Posting here in case it is of interest to anyone!

    Espruino code for Bluetooth Low Energy CSC (Cycling and Cadence Sensor) profile
    Data is returned via the Measurement characteristic:
    flags = 0x01 = Wheel data; 0x02 = Crank data; OR together for both
    uint32 = count of wheel revolutions;
    uint16 = last wheel event time in units of 1/1024 seconds
    uint16 = count of crank revs
    uint16 = crank event time
    At each wheel revolution
    ...each call to this will notify the phone etc
    function create_csc_service() {
      // Dataview and buffer to hold the measurement data
      var buffer = new ArrayBuffer(11);
      var dv = new DataView(buffer, 0);
      var revs=0;
      // Given # wheel revs, time in seconds, format the buffer 
      // for the CSC measurement characteristic.
      function format_csc(revs,wtime) {
        var time_csc_units = wtime*1024; // Convert to units of 1024th of a second
        dv.setInt8(0, 0x01); // Only wheel data is available => 01
        dv.setUint32(1, revs, little_endian=true);
        dv.setUint16(5, time_csc_units, little_endian=true);
        dv.setUint16(7, 0); // Crank revs not used
        dv.setUint16(9, 0); // Crank time not used
        return buffer;
      // Update the characteristic. Call once per revolution passing
      // time in seconds. This is our "public" interface
      function revolution(time_secs) {
        // Now notify...
            0x1816 : { // CSC UUID
            0x2A5B: { // Measurement UUID
              value : buffer,
              readable : true,
              notify : true,
      // Start - advertise that we're a CSC
        0x180F : { // Battery service UUID
            0x2A19: { // Battery value UUID
              value: [90], // Battery level percentage
              readable : true,
        0x1816 : { // CSC UUID
            0x2A5B: { // Measurement UUID
              value : format_csc(0,0),
              readable : true,
              notify : true,
            0x2A5C: { // Feature UUID - need to fully understand this
              value: [ 0x01,0x01],
              readable: true,
      }, { advertise: [ '180F', '1816' ], uart: false});
      return revolution;
    // Version with ex bike computer magnet+reed sensor on GPIO pin D13
    //setWatch(function(e) {
    //  csc(e.time);
    //},D13,{edge: 'falling', repeat:true, debounce:20});
    // Version for Puck.js using its in-built magnetometer
    // Two values for hysteresis. Determine mag reading for magnet near and far.
    // Might need to change this depending on magnet or bicycle positioning
    function on_mag(reading) {
      reading.x -= mag_zero.x;
      reading.y -= mag_zero.y;
      reading.z -= mag_zero.z;
      // Pythagoras theorem, but no need to
      // sqrt as we're only interested in
      // change
      var magnitude = reading.x*reading.x
          + reading.y*reading.y
          + reading.z*reading.z;
      // Magnet detected near?
      if (magnitude>MAGNET_NEAR_VALUE && !magnet_near) {
        csc(getTime()); // Update the bluetooth service
      // Magnet moved away?
      if (magnitude<MAGNET_FAR_VALUE && magnet_near) {
    function onInit() {
      csc = create_csc_service();
      mag_zero = Puck.mag(); // Get base case for magnetometer reading
      magnet_near = false;
      Puck.on('mag', on_mag);
      Puck.magOn(80); // 80Hz sample rate
  • That's amazingly cool - thanks! It's really neat to be able to turn it into something that you can then use with off the shelf apps :)

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview

Mobile phone bike computer sensor from Puck.js - Bluetooth CSC service

Posted by Avatar for ColinP @ColinP