Motion based PuckJS Media remote control

Posted on
  • Hi, I have a few puckjs and am looking at how make them into useful household devices. I am currently trying to build a simple to use HID BLE media remote control

    Features

    1. Turn the puck clockwise to raise the volume
    2. Tune the puck anticlockwise to turn down the volume
    3. Tune the puck on its back (black side facing up) then turn to change the channel or track
    4. Click to pause and resume (or Mute /UnMute)

      console.log('Compass heading test');
      Puck.magOn();
      Puck.on('mag', function(c) {
      console.log('Raw mag data: ',c);
      var xGa = c.x * 0.48828125;
      var yGa = c.y * 0.48828125;
      var d = 0;
      if (xGa == 0) {
      if ( yGa <= 0) {
        d = 90;
      } else {
        d = 0;
      }
      } else {
      console.log('Corrected X,YL ',xGa,yGa);
      d = Math.atan(yGa/xGa)*(180/Math.PI);
      }
      if (d > 360) {
      d = d - 360; 
      }
      if (d < 0) {
      d = d + 360;
      }
      console.log('Direction: ', d);
      });
      

    My first attempt is to convert mag x, y, z reading to a compass heading to then take changes to measure left and right rotation. I have completely failed to get this to work any idea what I am doing wrong?

  • Fri 2019.06.28

    Hi @user101436,

    any idea what I am doing wrong?

    Hard to guess what the code is doing, or what you mean by 'doing wrong'. Please post the output as seen by the WebIDE, and the result of process.env for current flash version.

    Although I don't believe this should be an issue, but the examples define the response function, then turn on the mag detection.

    http://www.espruino.com/Puck.js+Door+Lig­ht

  • Your readings will be impacted by the environment, and will fluctuate, so the first thing you want to do is zero/rebase your readings. I don't think you can get a heading, but not withstanding that, once rebased you should be able to detect incremental changes. Obviously moving the puck (to another location) or introducing magnetic things near to the Puck will impact the reading again, so you'd need to beware of this.

  • Basically what @Ollie says - you need to take a zero reading :)

    To use it as a compass you can maintain a min/max reading (http://www.espruino.com/modules/Vec3.js should make this easier) and then subtract the average of those 2 and then use the offset like a compass - so you'd have to upload the code, then rotate it every angle you can think of, and it should then be ok.

    Math.atan2 will save you a bunch of work too - unlike atan it can work over a whole 360 degrees, not just 90/180.

    But... Moving location, having magnets near, etc will all throw it off and it may need recalibrating.

    One thing you could do is make your own little jig/stand that has a magnet on it. That way it'll be powerful enough that the surrounding magnetic field won't be a big issue.

  • I'd start try to use it as compass, before implementing anything else.
    The compass on my smartphone (and an so many others, cheap and expensive) is not accurate at all, not moving when turning the smartphone and suddenly jumping back. Maybe it's just overcompensated, or whatever. I wouldn't use my smartphone compass to control volume up and down. Maybe puckjs is better.

  • Thanks for all the good advice.

    I think I need to lean some vector math!

    my next steps are:

    Use Vec3.js to calibrate my mag sensor and surroundings.

    by adding a hold down the button for 4 seconds to calibrate feature:

    To calibrate rotate the puck in all directions while sampling the mag data at a high rate, save the min and max value of each x,y,z axis.

    Press button to exit calibrate

    Take the average of the min/max for x,y,z and store these as JSON to a file in flash mem.

    Sample at a lower rate and subtract the average x,y,z from each reading

    use vector maths to calculate which way up the puck is and any clockwise or anticlockwise rotation steps.

    Then I need to work out how to send HID command via BT

    I will post an update as soon as I have made progress or if I have more problems. With some debug output this time.

  • I have made some progress.

    The following code calculates the min and max value for the mag x,y,z axis using Vec3 module. Can you test and show what you get on your puck. The to do at the end of the code shows my next steps.

    To use hold down the button for 5 seconds then rotate the puck in all directions until you only see green flashes (no blue). Then press the button to end calibration. The min and max values are then logged on your debug screen.

    var Vec3 = require('Vec3');
    var vMin = new Vec3();
    var vMax = new Vec3();
    var calibrating = false;
    var count = 0;
    
    
    // call a function if button is held down for 4 seconds
    function b4Seconds(callback) {
      setWatch(function(e) {
        console.log("Button down");
        t4s = setTimeout(function() {
          callback();
        },4000);
        setWatch(function(e) {
          clearTimeout(t4s);
          console.log("Button up");
        }, BTN, { repeat: false, edge: 'falling', debounce: 50 });
      }, BTN, { repeat: true, edge: 'rising', debounce: 50 });
    }
    
    
    function calibrate () {
      console.log('calibrating...');
      calibrating = true;
      var xyz = Puck.mag();
      var sMax = 0; 
      var sMin = 0;
      count = 0;
      vMin = new Vec3(xyz);
      vMax = new Vec3(xyz);
      Puck.magOn(10);
      Puck.on('mag', function(xyz) {
        var vMag = new Vec3(xyz);
        vMin = vMin.min(vMag);
        vMax = vMax.max(vMag);
        if ((sMax !=  vMax.mag()) || (sMin != vMin.mag())) {
          sMin = vMin.mag();
          sMax = vMax.mag();
          count++;
          console.log('new min or max: ',count);
          // flash the blue LED when calibrating
          digitalWrite(LED3, count % 2 == 0);
        } else { 
        // flash the green LED when not calibrating
        digitalWrite(LED2, Math.random()>0.5);
        digitalWrite(LED3, false);
        }
      });
      //press the buton to stop calibrating
      setWatch(function(e) {
        Puck.magOff();
        calibrating = false;
        console.log("calibarion completed");
        console.log('max: ',vMax);
        console.log('min: ',vMin);
        digitalWrite(LED3, false);
        digitalWrite(LED2, false);
        // TO DO 
        // get the zero position when the end calibrate button was pressed
        // calculate the average of vMin and vMax to get the nuteral position
        // calculate the spread of vMin, vMax to scale future readings
        // use the above to find out if the puck is rotating clockwise or anti
        // use the above to find out if the puck is upside down
        // send BT HID key presses
      }, BTN, { repeat: false, edge: 'rising', debounce: 50 });
    }
    
    //calibrate by holding button down for 4 seconds
    b4Seconds(calibrate);
    
    
    

    | |_ ___ ___ _ ||___ ___
    | |_ -| . | _| | | | | . |
    |
    |_| || |_|||_|_|

         |_| espruino.com
    

    2v03 (c) 2018 G.Williams

    Button down
    calibrating...
    new min or max: 1
    new min or max: 2
    new min or max: 3
    new min or max: 4
    new min or max: 5
    new min or max: 6
    Button up
    new min or max: 7
    new min or max: 8
    new min or max: 9
    new min or max: 10
    new min or max: 11
    new min or max: 12
    new min or max: 13
    new min or max: 14
    new min or max: 15
    new min or max: 16
    new min or max: 17
    new min or max: 18
    new min or max: 19
    new min or max: 20
    new min or max: 21
    new min or max: 22
    new min or max: 23
    new min or max: 24
    new min or max: 25
    new min or max: 26
    new min or max: 27
    new min or max: 28
    new min or max: 29
    new min or max: 30
    new min or max: 31
    new min or max: 32
    new min or max: 33
    new min or max: 34
    new min or max: 35
    new min or max: 36
    new min or max: 37
    new min or max: 38
    Button down
    calibarion completed
    max: Vec3: { "x": 794, "y": 527, "z": 124 }
    min: Vec3: { "x": 104, "y": -179, "z": -857 }
    Button up

  • Sat 2019.06.29

    Hi @user101436, just happen to have a Puck lying around, desperately waiting to do something useful. ;-)

    Results of two separate tests, about 300 degrees each axis:

    new min or max:  68
    new min or max:  69
    new min or max:  70
    Button up
    Button down
    calibarion completed
    max:  Vec3: { "x": -2493, "y": 2277, "z": 493 }
    min:  Vec3: { "x": -3198, "y": 1564, "z": -1226 }
    Button up
    >
    


    new min or max:  108
    new min or max:  109
    new min or max:  110
    Button up
    Button down
    calibarion completed
    max:  Vec3: { "x": -2654, "y": 2234, "z": 100 }
    min:  Vec3: { "x": -3308, "y": 1540, "z": -1256 }
    Button up
    >
    

    After giving this a whirl, and re-reading the objective in #1, I have to agree you are on to a pretty cool idea here. I really like the idea of inverting the Puck to get yet another feature.

    Just a minute, . . . I have to get up, walk over to the telly, change the channel and turn down the volume!!

    If it hasn't been thought of yet, put the Puck to sleep to save battery.

  • Thanks for your test. Your calibration numbers are both similar which is good but are also very different to my numbers.

    You are right I will need to add some sort of sleep mode where the puck reduces or stops its mag sensor scanning to save power.

    next step is to calculate the average of the min and max and use to re-base the sensor.

  • I have calculated the average of the min max and subtracted this from the raw. The calibration data now looks good. Now i can see when the puck is upside down as the z axis changes it sign. I am also calculating the heading and correcting for negative numbers .

    Can you test on your device and see if the puck now works as a digital compass.

    You will have to calibrate by holding down the button for 5 seconds then rotating the puck in all directions until the is only green flashes no blue. then press to go back to calibrated readings.

    var Vec3 = require('Vec3');
    var vMin = new Vec3();
    var vMax = new Vec3();
    var vAve = new Vec3();
    var vZero = new Vec3(); 
    var calibrating = false;
    var calibrated = false;
    var count = 0;
    var h = 0;
    var oldh = 0;
    
    
    // call a function if button is held down for 4 seconds
    function b4Seconds(callback) {
      setWatch(function(e) {
        if (calibrating) {return;}
        console.log("Button down");
        t4s = setTimeout(function() {
          callback();
        },4000);
        setWatch(function(e) {
          clearTimeout(t4s);
          console.log("Button up");
        }, BTN, { repeat: false, edge: 'falling', debounce: 50 });
      }, BTN, { repeat: true, edge: 'rising', debounce: 50 });
    }
    
    
    function calibrate () {
      if (calibrated) {
        console.log('re-calibrating...');
      } else {
        console.log('calibrating...');
      }
      calibrated = false;
      calibrating = true;
      var xyz = Puck.mag();
      var sMax = 0; 
      var sMin = 0;
      count = 0;
      vMin = new Vec3(xyz);
      vMax = new Vec3(xyz);
      Puck.magOn(10);
      Puck.on('mag', function(xyz) {
        if (calibrated) {return;}
        var vMag = new Vec3(xyz);
        vMin = vMin.min(vMag);
        vMax = vMax.max(vMag);
        if ((sMax !=  vMax.mag()) || (sMin != vMin.mag())) {
          sMin = vMin.mag();
          sMax = vMax.mag();
          count++;
          console.log('new min or max: ',count);
          // flash the blue LED when calibrating
          digitalWrite(LED3, count % 2 == 0);
        } else { 
        // flash the green LED when not calibrating
        digitalWrite(LED2, Math.random()>0.5);
        digitalWrite(LED3, false);
        }
      });
      //press the buton to stop calibrating
      setWatch(function(e) {
        Puck.magOn(5);
        calibrating = false;
        console.log("calibarion completed");
        console.log('max: ',vMax);
        console.log('min: ',vMin);
        digitalWrite(LED3, false);
        digitalWrite(LED2, false);
        // calculate the average of vMin and vMax to get the nuteral position
        vAve = new Vec3((vMin.x + vMax.x)/2,(vMin.y + vMax.y)/2,(vMin.z + vMax.z)/2);
        console.log('ave: ',vAve);
        // get the zero position when the end calibrate button was pressed
        vZero = new Vec3(Puck.mag()).sub(vAve);
        console.log('zero:',vZero);
        calibrated = true;
      }, BTN, { repeat: false, edge: 'rising', debounce: 50 });
    }
    
    
    function calibratedMag() {
      Puck.magOn();
      Puck.on('mag', function(xyz) {
        if(calibrating) {return;}
        var vMag = new Vec3(xyz).sub(vAve);
        //console.log('mag: ',vMag);
        h = (Math.atan2(vMag.y, vMag.x) * 180) / Math.PI;
        if (h > 360) {
          h = h - 360; 
        }
        if (h < 0) {
          h = h + 360;
        }
        if (Math.abs(Math.round(oldh) - Math.round(h)) >4) {
          console.log('--------------------');
          if (!calibrated) {console.log('uncalibrated!');}
          console.log('heading: ',Math.round(h));
          console.log('rotate: ', Math.round(oldh - h));
          if (vMag.z > 0) {
            console.log('upwards');
          } else {
            console.log('downwards');
          }
        }
        oldh = h;
      });
    }
    
    
    //calibrate by holding button down for 4 seconds
    b4Seconds(calibrate);
    calibratedMag();
    
    

    The out

    --------------------
    heading:  140
    rotate:  -20
    downwards
    --------------------
    heading:  209
    rotate:  -69
    downwards
    --------------------
    heading:  201
    rotate:  9
    upwards
    --------------------
    heading:  186
    rotate:  13
    upwards
    --------------------
    heading:  161
    rotate:  25
    upwards
    --------------------
    heading:  128
    rotate:  29
    upwards
    --------------------
    heading:  114
    rotate:  14
    upwards
    --------------------
    heading:  103
    rotate:  11
    upwards
    --------------------
    

    put after calibration

  • I have added the HID media keys but only volume up is working.??? I am debugging

  • I found the problem. I was still using console.log() while connected as a HID which caused the puckjs to hang. I need to work out how to re-direct console.log somewhere safe. I have volume up and down working well. I will do a bit more work and post an update.

  • I will do some optimisation and tidying up then post the final code

  • IT WORKS!

    To use:

    1. Upload code via webIDE
    2. Disconnect the IDE
    3. Calibrate the mag sensor by rotating puck in all directions until no red flashes (only green)
    4. Press button to stop calibrating
    5. Connect by BLE from a laptop or phone
    6. run music software
    7. rotate for volume
    8. press for pause/play
    9. Turn over and rotate for next/prev track

    Let me know your results.
    Current things to improve are next/prev un- pauses, the is no power saving mode, and tidy up my code and create some low level functions for calibrate and power management.

    Code:

    var Vec3 = require('Vec3');
    var vMin = new Vec3();
    var vMax = new Vec3();
    var vAve = new Vec3();
    var vZero = new Vec3(); 
    var calibrating = false;
    var calibrated = false;
    var count = 0;
    var h = 0;
    var oldh = 0;
    var r = 0;
    var controls = require("ble_hid_controls");
    var nrf = NRF.setServices(undefined, { hid : controls.report });
    
    /*
    // use this for power managment
    // call a function if button is held down for 4 seconds
    function b4Seconds(callback) {
      setWatch(function(e) {
        if (calibrating) {return;}
        console.log("Button down");
        t4s = setTimeout(function() {
          callback();
        },4000);
        setWatch(function(e) {
          clearTimeout(t4s);
          console.log("Button up");
        }, BTN, { repeat: false, edge: 'falling', debounce: 50 });
      }, BTN, { repeat: true, edge: 'rising', debounce: 50 });
    }
    */
    
    function calibrate () {
      if (calibrated) {
        //console.log('re-calibrating...');
      } else {
        //console.log('calibrating...');
      }
      calibrated = false;
      calibrating = true;
      var xyz = Puck.mag();
      var sMax = 0; 
      var sMin = 0;
      count = 0;
      vMin = new Vec3(xyz);
      vMax = new Vec3(xyz);
      Puck.magOn(10);
      Puck.on('mag', function(xyz) {
        if (calibrated) {return;}
        var vMag = new Vec3(xyz);
        vMin = vMin.min(vMag);
        vMax = vMax.max(vMag);
        if ((sMax !=  vMax.mag()) || (sMin != vMin.mag())) {
          sMin = vMin.mag();
          sMax = vMax.mag();
          count++;
          //console.log('new min or max: ',count);
          // flash the red LED when calibrating
          digitalPulse(LED1,1,100);
        } else { 
        // flash the green LED when not calibrating
        digitalPulse(LED2,1,10);
        }
      });
      //press the buton to stop calibrating
      setWatch(function(e) {
        Puck.magOn(5);
        calibrating = false;
        //console.log("calibarion completed");
        //console.log('max: ',vMax);
        //console.log('min: ',vMin);
        // calculate the average of vMin and vMax to get the nuteral position
        vAve = new Vec3((vMin.x + vMax.x)/2,(vMin.y + vMax.y)/2,(vMin.z + vMax.z)/2);
       // console.log('ave: ',vAve);
        // get the zero position when the end calibrate button was pressed
        vZero = new Vec3(Puck.mag()).sub(vAve);
       // console.log('zero:',vZero);
        calibrated = true;
      }, BTN, { repeat: false, edge: 'rising', debounce: 50 });
    }
    
    
    function calibratedMag() {
      var comQueue = [];
      var coms = false;
      // use the button for play/pause
      setWatch(function(e) {
        if(calibrating) {return;}
        comQueue.push('S');
        digitalPulse(LED1,1,10);
        digitalPulse(LED2,1,10);
        digitalPulse(LED3,1,10);
      }, BTN, { repeat: true, edge: 'rising', debounce: 50 });
      Puck.magOn(10);
      Puck.on('mag', function(xyz) {
        if(calibrating) {return;}
        if ((comQueue.length > 0) && (coms == false)){
          coms = true;
          //console.log(comQueue,coms);
          var comand = comQueue.pop();
          if (comand == 'U') {
            digitalPulse(LED1,1,3);
            try { 
              controls.volumeUp(function(){
                controls.volumeUp(function(){
                  coms = false;
                });});
            } catch (e) {coms = false;}
          }
          if (comand == 'D') {
            digitalPulse(LED3,1,3);
            try { 
              controls.volumeDown(function(){
                controls.volumeDown(function(){
                  coms = false;
                });});
            } catch (e) {coms = false;}
          }
          if (comand == 'N') {
            digitalPulse(LED2,1,3);
            try { 
              controls.next(function(){
                  coms = false;
              });
            } catch (e) {coms = false;}
          }
          if (comand == 'P') {
            digitalPulse(LED2,1,3);
            try { 
              controls.prev(function(){
                  coms = false;
              });
            } catch (e) {coms = false;}
          }
          if (comand == 'S') {
            try { 
              controls.playpause(function(){
                  coms = false;
              });
            } catch (e) {coms = false;}
          }
        }
        // correct mag reading using calibration data
        var vMag = new Vec3(xyz).sub(vAve);
        //console.log('mag: ',vMag);
        h = (Math.atan2(vMag.y, vMag.x) * 180) / Math.PI;
        if (h > 360) {
          h = h - 360; 
        }
        if (h < 0) {
          h = h + 360;
        }
        
        r = Math.round(h - oldh);
        if ((r > 300)) {r = r - 360;} 
        if ((r < -300)) {r = r + 360;} 
        
        if (Math.abs(r) >4) {
          if (vMag.z > 0) {
            if (r>0) {
              comQueue.push('U');
              //try { controls.volumeUp();} catch (e) { }
            } else {
              comQueue.push('D');
            }
          } else {
            if (r>0) {
              comQueue.push('P');
            } else {
              comQueue.push('N');
            }
          }
          //console.log('-------------');
          if (!calibrated) {console.log('uncalibrated!');}
            //console.log('heading: ',Math.round(h));
            //console.log('rotate: ',r);
          if (vMag.z > 0) {
            //console.log('upwards');
          } else {
            //console.log('downwards');
          }
        }
        oldh = h;
      });
    }
    
    
    //calibrate by holding button down for 4 seconds
    calibrate()
    calibratedMag();
    
    
    
    
    
    

    Reset to remove stop code

  • Tue 2019.07.02

    'IT WORKS!'

    Fantastic! . . . and to put on the final polish, If these haven't been discovered yet:

    Quick Start Best Practices - Puck tips - increasing battery life

    Sleep Wake setup and example snippet:

    Follow embedded links - Gordons #2

    http://forum.espruino.com/conversations/­297381/
    http://forum.espruino.com/conversations/­297050/

    Save on start examples - My post #2

    http://forum.espruino.com/conversations/­335391/

    MaBe and allObjects discussion starting at #4

    http://forum.espruino.com/conversations/­334253/#comment14748678



    Adding a comment block at the code block top, personalized with the author, date, copyright year and project concept/purpose and how to calibrate/use for braggin' rights, ;-) will be a nice touch!

  • Nice. I've been thinking of doing a BT remote control car, and using Puck for the steering. If I get to it, I'll be using this code as a jumping off point. Thanks for posting up.

  • Oh, time for some bot-check on registration? :/

  • Now the code is working I am going to use it to write a module that can calibrate the Mag sensor and then convert mag readings into compass heading and rotation angel since last measurement.

  • @AkosLukacs - post deleted - I'm pretty sure these are actually people making the posts though. It's just reaching the point where it's easier for companies to pay someone a few pence per post :(

  • hello
    im rodrigo and pretty new to the puck.js, i started using this thread, and stumbled with an issue, i have no idea how to disable te next-previous track
    i tried commenting and removing what i thought was the lines of code regarding that matter, but after i finished calibrating the puck it just doesnt do anything else
    anyone know how to do this? thanks in advance, and great project :D

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

Motion based PuckJS Media remote control

Posted by Avatar for user101436 @user101436

Actions