Pinoccio Demo Project - the Web Rover


We hooked up Pinoccio to a Pololu 3pi Robot to create an unmanned rover that can be driven via the Web. Then we drove our little Web Rover in Nevada from 10,000 km away in Brazil.


Pololu 3pi plus Pinoccio equals cutest robot overlords ever!

Some of us are afraid of flying. Like me. And like our buddy, Web Rover. Sure, his cousin QuadCopter is known for her aerial derring-do. But Rover, he prefers to keep his treads planted firmly on the ground. However, just like his unmanned vehicular brethren, Web Rover is happy to take orders from you remotely. In fact, he takes his orders via the Web! Check out our video demonstration:

Watch the Video

Test Drive Your Accelerometer

If you have an iPhone, iPad, iPod touch, or a Mac laptop with an accelerometer, try the following:

  1. Open this page in Chrome or Safari.
  2. Click the Enable Driving button.
  3. Tilt your device and you should see the Position measurements update on-screen.

If you were connected to the Rover right now, this information from your accelerometer would be fed to the Web, and then to the Web Rover via Pinoccio.

Position:
  • Forward: -
  • Turn: -
Messages from Rover:
Waiting for the 3pi Scout connection...

How It Works

The Web Rover is an example of how easily Pinoccios glue the physical to the virtual.

The Set Up
  • We built the Web Rover using a Pololu 3pi Robot, a Pinoccio Scout, and a Pinoccio Lead Scout.
  • The Scout is connected to the 3pi via a few wires — two for communication, and two for power, so no battery is necessary.
  • The Lead Scout is nearby on a table and is communicating wirelessly with the Scout via mesh radio.
  • The Lead Scout is also connected to the Web via WiFi, and is subscribed to a Pinoccio API topic called 3pi-control.
  • Any messages published to that topic are received by the Lead Scout and are passed along to the Scout that’s on the 3pi.
The Code
  • We wrote a quick Javascript app that grabs accelerometer data from an iPhone, and translates it into motor controls for the 3pi.
  • The browser grabs this data via WebSockets, so it’s realtime, and doesn’t require a page refresh.
  • This information is published to the 3pi-control Pinoccio API topic.
  • Because the Lead Scout is subscribed to this topic, it receives the information, and passes it along to the Scout.
  • The Scout then translates this information into simple serial commands, telling the 3pi’s left and right motor how fast to go, and in what direction.
Why It’s Awesome
  • This demo is not limited to our local wireless network. We were able to drive the Web Rover in real-time from 10,000 km away.
  • Anyone with an internet connection could theoretically drive the 3pi around, regardless of where they are in the world.
  • Though we don't show it in this example, the data can go in both directions.
  • For example, we plan on adding the ability for the Rover to read its own battery voltage and publish that data to an API topic called 3pi-telemetry. A web app would subscribe to that topic and display the Rover’s battery life on a web page. The battery life info would be updated in real-time in the browser while the Rover is being driven around.
  • Sensors could be added to the Web Rover and that data could be published in real-time to the Web. Imagine adding a Passive Infrared sensor that detect motion, now you have your own web-controlled security bot.
  • The topics 3pi-control and 3pi-telemetry are publicly available. This means that any device that can publish or subscribe to those topics could interact with the robot as well
  • That means that mobile apps, third-party web apps, and even other Pinoccios elsewhere in the world could interact with the Web Rover — sending and receiving info via the API topics.

Build Your Own

Time Required: 1 hour

Hardware

Skill Level: Intermediate
Just solder two resistors to the 3pi, and plug in a couple of wires.

Ingredients:

Software

Skill Level: Beginner
Load our code sketch onto your Pinoccio Scout via the Arduino IDE. Change your WiFi access point and password, then load up the public web page in a browser.

Ingredients:
  • Code Sketch - Uploaded to Pinoccio via the Arduino IDE
  • MQTT Server
  • WebSocket
  • JavaScript - See code snippet below

The Code

JavaScript - demo.js
Sketch Arduino IDE - webrover.ino
  •   var mq = new Mosquitto();
      var url = 'ws://test.mosquitto.org/ws';
      var controlTopic = 'pinoccio/3pi-control';
      var telemetryTopic = 'pinoccio/3pi-telemetry';
    
      $(function() {
        var online = false;
        var armed = false;
        var leftMotorArray = new Array();
        var rightMotorArray = new Array();
        var motorArrayLength = 5;
        var leftMotor = 0;
        var rightMotor = 0;
        var betaSensitivity = 0.5;
        var gammaSensitivity = 0.3;
    
        // mqtt connection and message-recieve handling
        mq.connect(url, true);
        mq.subscribe(telemetryTopic, 0);
    
        mq.onmessage = function(topic, payload, qos) {
          if (payload == 'Still alive') {
            if ($('#alive-ping').html() == ' ') {
              if (!online) {
                online = true;
                $('#rover-messages-container').html('Pinoccio 3pi online');
              }
              $('#alive-ping').html('•');
            } else {
              $('#alive-ping').html(' ');
            }
          } else {
            $('#rover-messages-container').append(payload);
          }
        };
    
        // device accelerometer handling
        if (window.DeviceOrientationEvent) {
          window.addEventListener('deviceorientation', function() {
            tilt(event.beta, event.gamma);
          }, true);
        }
    
        var tilt = function(forwardBack, leftRight) {
          if (!armed) {
            return;
          }
          forwardBack = forwardBack * -1;
          var forward = (forwardBack * betaSensitivity).toFixed(0);
          var turn = (leftRight * gammaSensitivity).toFixed(0);
    
          $('#forward-amount').html(forward);
          $('#turn-amount').html(turn);
    
          leftMotorArray.push((+forward) + (+turn));
          if (leftMotorArray.length >= motorArrayLength) {
            leftMotorArray.shift();
          }
          rightMotorArray.push((+forward) + (+turn * -1));
          if (rightMotorArray.length >= motorArrayLength) {
            rightMotorArray.shift();
          }
        }
    
        // interface-button handling
        $('.arm-button').click(function(e) {
          if (!armed) {
            armed = true;
            $('.arm-button').html("Driving enabled!");
          } else {
            armed = false;
            $('.arm-button').html("Enable driving");
            $('#forward-amount').html('-');
            $('#turn-amount').html('-');
          }
        });
    
        // only send control messages twice a second (2Hz), and use smoothing via averaging
        setInterval(function() {
          if (!armed) {
            return;
          }
          for (var i=0; i < leftMotorArray.length; i++) {
            leftMotor += leftMotorArray[i];
          }
          for (var j=0; j < rightMotorArray.length; j++) {
            rightMotor += rightMotorArray[j];
          }
    
          leftMotor = (+leftMotor) / leftMotorArray.length;
          rightMotor = (+rightMotor) / rightMotorArray.length;
    
          leftMotor = +(leftMotor.toFixed());
          rightMotor = +(rightMotor.toFixed());
    
          mq.publish(controlTopic, leftMotor + ':' + rightMotor, 0);
          console.log('leftMotor: ' + leftMotor + ' - rightMotor: ' + rightMotor);
        }, 500);
      });
  •   #include "config.h"
      #include <Pinoccio.h>
    
      // http://www.pololu.com/docs/0J21/10.a
      enum {
        PI_SIGNATURE           = 0x81,
        PI_RAW_SENSORS         = 0x86,
        PI_CALIBRATED_SENSORS  = 0x87,
        PI_TRIMPOT             = 0xB0,
        PI_BATTERY_MV          = 0xB1,
        PI_PLAY_MUSIC          = 0xB3,
        PI_CALIBRATE           = 0xB4,
        PI_RESET_CALIBRATION   = 0xB5,
        PI_LINE_POSITION       = 0xB6,
        PI_CLEAR_LCD           = 0xB7,
        PI_PRINT               = 0xB8,
        PI_LCD_GOTO_XY         = 0xB9,
        PI_AUTOCALIBRATE       = 0xBA,
        PI_START_PID           = 0xBB,
        PI_STOP_PID            = 0xBC,
        PI_M1_FORWARD          = 0xC1,
        PI_M1_BACKWARD         = 0xC2,
        PI_M2_FORWARD          = 0xC5,
        PI_M2_BACKWARD         = 0xC6
      };
    
      int leftMotor = 0;
      int rightMotor = 0;
      unsigned int safetyTimer = 50000; // start in a disabled state
    
      void setup() {
        Pinoccio.init();
        initialize3Pi();
    
        RgbLed.blinkRed(200);
        RgbLed.blinkGreen(200);
        RgbLed.blinkBlue(200);
        Serial.println("3pi Scout ready for duty");
    
        Pinoccio.SetAddr(APP_MESH_ADDR);
        Pinoccio.SetPanId(APP_MESH_PANID);
        Pinoccio.SetChannel(APP_MESH_CHANNEL);
        Pinoccio.SetRxState(true);
    
        Pinoccio.listenForMessage(receiveMessage);
      }
    
      void loop() {
        Pinoccio.loop();
    
        if (safetyTimer > 30000) {
          leftMotor = rightMotor = 0;
          Serial1.write(PI_STOP_PID); // stop all motors
        } else {
          safetyTimer++;
        }
      }
    
      void initialize3Pi() {
        Serial1.begin(115200);
        Serial1.write(PI_SIGNATURE);
        RgbLed.blinkRed(2000);
        Serial1.write(PI_CLEAR_LCD);
        Serial1.write(PI_PRINT);
        Serial1.write(8);
        Serial1.print("Pinoccio");
        Serial1.write(PI_LCD_GOTO_XY);
        Serial1.write(0);
        Serial1.write(1);
        Serial1.write(PI_PRINT);
        Serial1.write(8);
        Serial1.print("Lets go!");
      }
    
      static bool receiveMessage(meshFrame *ind) {
        safetyTimer = 0;
    
        getMotorPayload(ind->data, ind->size);
    
        if (leftMotor >= 0) {
          Serial1.write(PI_M1_FORWARD);
        } else {
          Serial1.write(PI_M1_BACKWARD);
        }
        Serial1.write(abs(leftMotor));
        if (rightMotor >= 0) {
          Serial1.write(PI_M2_FORWARD);
        } else {
          Serial1.write(PI_M2_BACKWARD);
        }
        Serial1.write(abs(rightMotor));
      }
    
      void getMotorPayload(byte* payload, unsigned int length) {
        int i=0, j=0;
        char buf[10];
        int forward = 0;
        int turn = 0;
    
        while (payload[i] != ':') {
          buf[j++] = payload[i++];
        }
        buf[i++] = '\0';
        leftMotor = atoi(buf);
        j = 0;
    
        while (i < length) {
          buf[j++] = payload[i++];
        }
        buf[j++] = '\0';
        rightMotor = atoi(buf);
      }
              

Log In


×

Choose your username


×

Create an Account


With an account, you can:

  • Join in community discussions
  • Stay up to date with our email newsletter

And soon, you’ll be able to:
  • Create a public profile
  • Share your projects & code with others
×