středa 2. října 2013

Remote controlled robot

Reason, why I developed my little input library were that I wanted remotely control robots and other things with a computer. My idea was sending commands via serial line to the one Arduino and transmit them wirelessly into another. Twistedinput was designed as layer between input device and serial output.

I decided for Twisted framework because I very like it and because I can make very complex things with it. For example control robots over the Internet. Maybe build something like this :)

Wireless module

I'm using a cheap radio module from ebay. It is base on nRF24L01 chip. If you are interested in arduino, you are probably already familiar with it.

There is very nice library for this module called RF24. You can google out many tutorials and lot of other useful stuff for it.

Wireless transmitter

For sending commands into robot, I designed its own communication protocol. It is based on very similar data structure as input_event from linux/input.h header file.

Although transmitter has defined this structure, it doesn't actually use it. It only needs to know, how long this structure is. That's because it reads data from serial line byte by byte. When it receives all bytes needed for building one event structure, it sends them out.

Transmitter code

#include <SPI.h>
#include "RF24.h"

struct event {
  unsigned char type;
  unsigned char code;
  short value;
};

// buffer for received data
char eventBuffer[sizeof(struct event)];
int bufferIndex;

RF24 radio(9, 10);
const uint64_t pipes[2] = { 0xF0F0F0F0E1LL, 0xF0F0F0F0D2LL };

/**
 * routine for sending events
 */
void sendEvent(struct event * event) {
  radio.stopListening();
  
  // send event to robot
  radio.write(event, sizeof(struct event));
  
  radio.startListening();
}

void setup() {
  Serial.begin(19200);
  
  bufferIndex = 0;
  
  radio.begin();
  radio.setRetries(15, 15) ;
  radio.setPayloadSize(sizeof(struct event));
  
  radio.openWritingPipe(pipes[0]);
  radio.openReadingPipe(1,pipes[1]);
  
  radio.startListening();
}

void loop() {
  if(Serial.available() > 0) {
    Serial.readBytes(eventBuffer + bufferIndex, 1);
    bufferIndex++;
    if(!(bufferIndex < sizeof(struct event))) {
      bufferIndex = 0;
      sendEvent((struct event *)eventBuffer);
    }
  }
}

Wireless receiver

Receiver is my robot. It doesn't reads data byte by byte, it receives complete event structure instead. After that it needs to handle it.

Handling algorithm is little bit odd. I wanted to calculate, which pins should be LOW an which should by HIGH. This solution can do this but it isn't ideal way I thing.

The code is splitted into two parts. Header file with defines and second file with code. In the Arduino IDE you can create second file by clicking at small triangle on the right side, just below serial monitor button.

robot.h header file:

// define some commands
#define EV_MOVE         0x01

#define MOVE_FORWARD    0x01
#define MOVE_BACKWARD   0x02
#define MOVE_STOP       0x03
#define MOVE_LEFT       0x04
#define MOVE_RIGHT      0x05
#define MOVE_STRAIGHT   0x06

#define MOTOR_PIN_COUNT          4
#define LEFT_WHEEL_FORWARD       6
#define RIGHT_WHEEL_FORWARD      3
#define LEFT_WHEEL_BACKWARD      9
#define RIGHT_WHEEL_BACKWARD     5

struct event {
  unsigned char type;
  unsigned char code;
  short value;
};

// define moving states
enum Move {
  E_MOVE_STOP,
  E_MOVE_FORWARD,
  E_MOVE_BACKWARD
};

enum Turn {
  E_TURN_STRAIGHT,
  E_TURN_LEFT,
  E_TURN_RIGHT
};

// function prototypes
unsigned char getMoveMask(Move eMove);
unsigned char getTurnMask(Turn eTurn);
unsigned char getPinMask(Move eMove, Turn eTurn);
void update();
void doMove(struct event * event);
void handleEvent(struct event * event);
struct event * bytesToEvent(char * buffer);

robot code:

#include <SPI.h>
#include "RF24.h"
#include "nRF24L01.h"
#include "robot.h"

// buffer for received data
char eventBuffer[sizeof(struct event)];

RF24 radio(8, 10);
const uint64_t pipes[2] = { 0xF0F0F0F0E1LL, 0xF0F0F0F0D2LL };

// store current move direction
Move currentMove;
Turn currentTurn;

// store current pin mask for motors
unsigned char currentPinMask;

int motorPins[] = {
  RIGHT_WHEEL_BACKWARD,
  RIGHT_WHEEL_FORWARD,
  LEFT_WHEEL_BACKWARD,
  LEFT_WHEEL_FORWARD
};

/**
 * get mask for move
 */
unsigned char getMoveMask(Move eMove) {
  switch(eMove) {
    case E_MOVE_STOP:      return B0000; break;
    case E_MOVE_FORWARD:   return B1010; break;
    case E_MOVE_BACKWARD:  return B0101; break;
  }
}

/**
 * get mask for turn
 */
unsigned char getTurnMask(Turn eTurn) {
  switch(eTurn) {
    case E_TURN_STRAIGHT:  return B0000; break;
    case E_TURN_LEFT:      return B0110; break;
    case E_TURN_RIGHT:     return B1001; break;
  }
}

unsigned char getPinMask(Move eMove, Turn eTurn) {
  unsigned char moveMask = getMoveMask(eMove);
  unsigned char turnMask = getTurnMask(eTurn);
  if((moveMask == 0) || (turnMask == 0)) {
    return moveMask | turnMask;
  }
  if(eMove == E_MOVE_BACKWARD) {
    turnMask = ~turnMask;
  }
  return moveMask & turnMask;
}

/**
 * update motor pins
 */
void update() {
  int i;
  unsigned char pinMask = getPinMask(currentMove, currentTurn);
  
  // create differential pin mask by xoring current mask and new mask
  // differential mask is used for handling only those pins, which really needs
  // to change its state
  unsigned char differentialPinMask = currentPinMask ^ pinMask;
  
  // handle pins which needs to be set LOW at first
  for(i = 0; i < MOTOR_PIN_COUNT; i++) {
    if(((differentialPinMask & (0x1 << i)) != 0) &&
      ((pinMask & (0x1 << i)) == 0)) {
      digitalWrite(motorPins[i], LOW);
    } 
  }
  
  // handle pins which needs to be set HIGH
  for(i = 0; i < MOTOR_PIN_COUNT; i++) {
    if(((differentialPinMask & (0x1 << i)) != 0) &&
      ((pinMask & (0x1 << i)) != 0)) {
      digitalWrite(motorPins[i], HIGH);
    } 
  }
  
  // assign new mask
  currentPinMask = pinMask;
}

void doMove(struct event * event) {
  switch(event->code) {
    case MOVE_FORWARD:   currentMove = E_MOVE_FORWARD;     break;
    case MOVE_BACKWARD:  currentMove = E_MOVE_BACKWARD;    break;
    case MOVE_STOP:      currentMove = E_MOVE_STOP;        break;
    case MOVE_LEFT:      currentTurn = E_TURN_LEFT;        break;
    case MOVE_RIGHT:     currentTurn = E_TURN_RIGHT;       break;
    case MOVE_STRAIGHT:  currentTurn = E_TURN_STRAIGHT;    break;
  }
  update();
}

void handleEvent(struct event * event) {
  switch(event->type) {
    case EV_MOVE:        doMove(event);               break;
  }
}

/**
 * convert big-endian bytes pointed by buffer into
 * native endian event structure
 */
struct event * bytesToEvent(char * buffer) {
  short val = *(short *)(((void *)buffer) + (2 * sizeof(unsigned char)));
  *((short *)(((void *)buffer) + (2 * sizeof(unsigned char)))) = (val << 8) | ((val >> 8) & 0xff);
  return (struct event *)buffer;
}

void setup() {
  
  // debug only
  Serial.begin(19200);
  
  // set motor pins to OUTPUT
  int i;
  for(i = 0; i < MOTOR_PIN_COUNT; i++) {
    pinMode(motorPins[i], OUTPUT);
  }
  
  // initialize pin mask
  currentPinMask = 0x0;
  
  // initialize current move direction
  currentMove = E_MOVE_STOP;
  currentTurn = E_TURN_STRAIGHT;
  
  radio.begin();
  radio.setRetries(15, 15);
  radio.setPayloadSize(sizeof(struct event));
  
  radio.openWritingPipe(pipes[1]);
  radio.openReadingPipe(1,pipes[0]);
  
  radio.startListening();
}

void loop() {
  if(radio.available() > 0) {
    bool done = false;
    while(! done) {
      done = radio.read(eventBuffer, sizeof(struct event));
    }
    handleEvent(bytesToEvent(eventBuffer));
  }
}

Endianness

Endianness is a way of storing data types, which are more than one byte long. If you are not familiar with it, please read wikipedia.

It's known convention, that all data send across network should be encoded in big-endian byte order. For that reason my robot actually doesn't receive event structure. It receive some bytes which needs to be encoded into right order. This is what bytesToEvent routine do. Unfortunately, I don't now about some elegant solution, my implementation is little bit dirty. If Arduino were based on some big-endian architecture, this routine will return an event structure in wrong byte order.

Controlling with computer

Finally we need last piece of software, which runs on the computer. A computer acts like interface between transmitting Arduino and in input device. But it can do more things. It can save all events into file and reproduce them later or it can be some kind of autopilot. This software hasn't send data into transmitter directly, but over computer network. What ever you want.

You can download my radiocontrol script

Žádné komentáře:

Okomentovat