• 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
    https://www.bluetooth.com/wp-content/upl­oads/Sitecore-Media-Library/Gatt/Xml/Cha­racteristics/org.bluetooth.characteristi­c.csc_measurement.xml
    
    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
    
    Usage:
    
    csc=create_csc_service()
    
    At each wheel revolution
    
    csc(time_in_seconds)
    
    ...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) {
        revs+=1;
        format_csc(revs,time_secs);
        // Now notify...
        NRF.updateServices({
            0x1816 : { // CSC UUID
            0x2A5B: { // Measurement UUID
              value : buffer,
              readable : true,
              notify : true,
            }
          }
        });
      }
    
      // Start - advertise that we're a CSC
      NRF.setServices({
        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
    //pinMode(D13,"input_pullup");
    //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
    MAGNET_NEAR_VALUE=150000;
    MAGNET_FAR_VALUE=130000;
    
    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) {
        magnet_near=true;
        LED.write(1);
        csc(getTime()); // Update the bluetooth service
      }
    
      // Magnet moved away?
      if (magnitude<MAGNET_FAR_VALUE && magnet_near) {
        magnet_near=false;
        LED.write(0);
      }
    }
    
    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
    }
    
About

Avatar for ColinP @ColinP started