2 Jan 2012

Control Arduino Through Serial Connection (USB) VB Visual Basic program

Content Has been moved to my new blog


http://blog.oscarliang.net/control-arduino-through-serial-connection-usb-vb-visual-basic-program/





LED Example:




Hexapod Example:


Why?

When we are debugging and testing our circuits and codings, most of the times we reset the Arduino board and upload the new program. But the thing is everything has a finite lifetime, and by doing harmful things to the body would even reduce life quicker. 

Just like smoking could kill you, repetitively uploading could kill your arduino too! I still remember I read from somewhere, and someone said an Arduino board has an average uploading limit of about 1000 times... I don't know if this is true, but if we could avoid doing something that could harm the arduino, then why not?

So here we are, I was looking for a way to test and develop new moves and gaits for my hexapod robot. But frequently uploading new codes really cost me time and risking killing the arduino, so I thought it would be nice to test it just by sending the arduino a command through USB connection (Serial communication), therefore the possibility of program uploading could be minimized.

This is also useful when we use bluetooth to control arduino.




How?

Arduino has already provided a serial communication class, and there are built in examples of how to use them. Here is the official doc:

I will first try to establish a basic class and working Arduino program in the Arduino development IDE, and then move on to write a VB program to provide better and more user friendly interface. I will also publish my source code in the near future.





=======================================================

1/1/2012

Happy new year!
I will divide this tutorial into 2 sections: Receiver and Sender ends.

Receiver side coding:

So the command I designed would look like this. 
MxPxDx/

x - arbitrary number
M - mode (0 - write, 1 - read, 2 - servo control)
P - pin (0 - 13), or servo number (specified in arduino program)
D - data (0 - LOW, 1 - HIGH, or (600 to 2400) - servo position)
/ - indicate end of command (to execute)

for example, 
M0P2D0/ - means to make pin 2 LOW
M2P1D900/ - means to make servo number 1 to 900 ms (1500 = 90 degree)



The idea is that the command get sent to the arduino as a array of charaters one by one, which has delay between each charaters. 
notice that the function Serial.read() can only read one byte (one charater) at a time. So if you write something like this:

a = Serial.read()
b = Serial.read()

you can get the first character, but the second one might not be read as it hasn't arrived at the serial receiving buffer yet.
So I use this statement:

void loop(){

     if (if (Serial.available() > 0) {

          //...Read one byte and store in array...

          //...if the last character read was '/' then...
               //...execute the command...

         //... reset the command array, ...
          //...keep listening to serial port...

     }

}


Serial.available() indicates how many bytes are available to be read. So the arduino just keeps running, and reads data when it becomes available and that's how commands are received. Here is how the command is decoded and executed:

1. When I received a character 'M', I remember all the char after 'M', before I encounter 'P', and store that in variable 'mode'
2. same, store characters after 'P' and before 'D', in variable 'pin'
3. store characters before '/', in variable 'data'

Then convert these char into int type (you can also do it during above process), by subtracting '0', which has an ASCII value of 48.
So now we have 3 variables, which is basically the commands. To execute the command, we can write something like this:

switch (mode){
      case 0:
        digitalWrite(pin,data);
        break;

      case 1:
        //...Read digital pin...
         // ... Read analogue pin...
        break;

      case 2:
        // ...Control Servo...
        // ... set position, read position so on...
        break;
        
}

To execute read and servo commands, we will need more complicated coding, but that's roughly how it would work.

Sender side Coding:

To use this serial command system, no coding is needed when using arduino serial monitor. just type in the command and it just works!

But that isn't even 5% of the amazing things it could achieve! If you could write a program and introduce graphical interface, controlling the arduino would be so intuitive and clear! also command can be sent more rapidly through computer programs, especially useful when testing robot moves, or LED lighting effects!

I think the most popular choices for serial programming would be Python, C/C++, or Basic. I know C/C++ the best here, but the thing is C/C++ is not easy to write the graphical interface. So I went for Basic, which I realized its simplicity and fast development time. I spent 2 hours learning the basics, and got my program working 3,4 hours later. Surely most of us could do better.

In the example video (the LED control program), when the ON/OFF button is pressed, it goes to a 'switch - case' statement, depending on what LED is selected, corresponding LED is switched on or off. The Display function is even easier. it sends out a command, delay for sometime, before sends out another one. So the LED flashes and creates some kind of fabulous visual effect.


That's it for the basics. I will carry on completing the code, especially the servo control for my hexapod robot. Finally will put it in a header file.


=====================================================
02/01/2012

Servo Control coding:

Receiver Side (arduino):


     command example:
     M2P0D1500/     ---------   set servo number 1 to middle position (1500 ms).

This command works the best with my way of updating servos' positions (described here, search for "26/12/2011" update. )

Basically what it does is, there is a variable for the position of each servo, when command is received and executed, the variable will be updated rather than writing the servo position straight away. The position is only written at the end of the loop process (or when the timer has reach certain point if you want delay).

for example:



void loop(){

  // ... command receiving
  // ... command executing (update servo position and store in variable 'pos')

  servo.writeMicroseconds(pos);


}



So in the execution coding block, we add:





 // ============ Execute command ======
    switch (mode){
      case 0:
        digitalWrite(pin,data);
        break;

      case 1:
        digitalRead(pin);
        break;

      case 2:
        switch (pin){
          case 0:
            pos0 = data; //
            break;
          case 1:
            //pos1 = data;
            break;
          case 2:
            //pos2 = data;
            break;


        }
        break;

    }






 


Sender side:

    


When we are testing using arduino serial monitor, no coding is needed.

In VB (basic), it's best to use track bar. 



Create events when trackbar is scrolled, this is an example 

    Private Sub TrackBar1_Scroll(ByVal sender As System.ObjectByVal e As System.EventArgsHandles TrackBar1.Scroll

        Dim valueInStr As String = CStr(TrackBar1.Value)
        Dim command As String = "M2P0D" + valueInStr + "/"

        SerialPort1.Write(command)
        LB_ServoPos.Text = valueInStr

        System.Threading.Thread.Sleep(100)

    End Sub

it works when you only have one servo, if there are more servos need to be deal with, has to write more codes to determine 'P' (which servo to control).




================================================================

03/01/2012

Tested the serial command system, one big drawback is that between each command, the delay is quite obvious. When I intend to move 2 servo positions at the at same, one servo is actually moving before the other.

That's because it uses this algorithm:

(waiting and listening to USB) ---> (receive command 1) --> (Execute command 1) --> (waiting and listening to USB) --->   (receive command 2) ---> (Execute command 2) --->.......

Although command 1 and 2 are intended to be executed at the same time (sent at the same time), there will be a delay.

I re-consider the system, decided to fix this synchronization problem by adding an additional command:

SxCa...SyCb/

which contains all the servos and changes of position at the same time, and execute them all together, so delay could be reduced between servo moves.

The length of this command is indefinite, but can be determined by how many servos wants to be controlled. E.g. max length of command = (1+2+1+4)*number of servos

assuming we control 18 servos at most, the command buffer length would be 8*18 = 144 (this can be modified easily when needed)

for example:
S0C-200S4C600/    --- would turn Servo 0 backward 200us, Servo 4 forward 600us.

You might notice, when 2 or more servos are moved at the same time, if the changes of positions are different, it will also result in a different duration (where some servo reach destination faster than others).

To address this, I added one more string of data at the end of the new command, 

Lxxx, 

which means Loops, or steps, required to finish transition. So ideally, when we have a command like the example above, "S0C-200S4C600L50/"    Servo 0 will have to spend 50 loops in the arduino program to finish the transition, which divides servo 0 to move -4 each loop, and servo 4 to move 12. 

You might think it's unnecessary, but one big good thing it brings is smoothness! let image you want to change a servo position from 1500 to 800, you could do it at once (within 1 loop), but it introduces instability to your robot and circuit (draw of current). Also when motion gets too big, it won't look pretty. with additional loop control, big motion can be broken down into small, digestible pieces.  

Now we have the final template servo command:

SxCa...SyCbLc/

--------------------------------------------------
As we have a new command, additional algorithm in the arduino programm will be needed to code with it. What I will do is:

1. create a function that generate a new position for each servo at each loop. 

2. make arduino to talk back! Sending the computer a signal, when we have complete a command, and available for another one. (so we can have a collection of commands waiting on the PC side, and are sent when arduino finishes the last one)



====================================================
05/01/2012

This will be the final update on this topic.


new idea of the command analysis process:




first of all we need to have a fixed length of command, by making unused its zero, and use '1' to represent negative in servo command e.g.

     M00P05D0235/   --- write pin 5 a PWM value of 235 
     S10C00300S02C10700L0100/   ---  turn servo 10 300um, turn servo 2 -700um after 100loops

in which case, instead of having a switch, we can now just loop through the command buffer char by char, to store the variables and data.

I want to do this is because

1. cleaner code block
2. less likely to have bug.
3. commands are easier to handle when have fixed length.



==============================================================
Source Code:


LED- VB (visual basic) program and command files:
https://dl.dropbox.com/u/457167/Blog_Download_Resources/PC_Arduino_Controller_VB.7z


Hexapd - VB (visual basic) program and command files:
https://dl.dropbox.com/u/457167/Blog_Download_Resources/Hexapod_Controller_VB.7z


Arduino Main program (An example on hexapod robot, to just make you understand):



// Oscar's Project
#include <Servo.h>
#include "serial_command.h"

// ========== Position Table ===========
SerialCommand command;

// ========= Servo ===================
// 12 servos
// first 6 - legs    last 6 - sholders
Servo servo[12];
int servoPos[12];

// ========== Pins ============
// first 6 legs, last 6 sholder
const byte pin[12] = {8,9,10,11,12,13,2,3,4,5,6,7};

void setup()
{
                Serial.begin(9600);

                // == Setup Servos ==

                for (int i=0; i<12; i++){
                                servo[i].attach(pin[i]);
                                servoPos[i] = 1400;
                                servo[i].writeMicroseconds(servoPos[i]);
                                delay(100);
                }

                delay(2000);

                command.Reset();
}


void loop()
{
                if (command.ReceiveCommand()){
                                if (command.DecodeCommand()){
                                                if (command.ExecuteCommand()) {
                                                                for(int curPos=0; curPos<command.loops; curPos++){
                                                                                for (int i=0; i<12; i++){

                                                                                                // UPDATE POSITIONS 
                                                                                                servoPos[i] += command.servoPosAdjust[i];
                                                                                                servoPos[i] = constrain(servoPos[i], 600, 2400);

                                                                                                // WRITE POSITIONS
                                                                                                servo[i].writeMicroseconds(servoPos[i]);
                                                                                }
                                                                                delay(40);
                                                                }

                                                                // Finish, send signal to PC
                                                                Serial.write('1');
                                                }
                                                else
                                                                Serial.println("execution failed"); // execution failed
                                }
                                else
                                                Serial.println("decoding failed"); // decoding failed

                                //if (command.executed && command.listening)
                                command.Reset();
                }
}


Command class (this is the class for the command system, it's a general purpose class)



#include "WProgram.h"

class SerialCommand{

private:

                static const int MAX_SERVO_NUM = 18;
                static const int SINGLE_COMMAND_LENGTH = 3+5;
                static const int MAX_COMMAND_LENGTH = MAX_SERVO_NUM * SINGLE_COMMAND_LENGTH + 5;

                int index;
                int command[MAX_COMMAND_LENGTH]; // longest command is 12 char long

public:

                boolean executed; // true - ready for new command
                boolean listening; // true - recieved part of the command but not '/' yet
                // false - still listening to command
                int pin;    // pin number, OR servo number
                int mode;   // 0 - write, 1 - read, 2 - servo control
                int data;

                // Servo data

                int loops;
                int servoPos[MAX_SERVO_NUM];
                int servoPos_LastUpdated;
                int servoPosChange[MAX_SERVO_NUM];
                float servoPosAdjust[MAX_SERVO_NUM];

                // ============ Functions ===============

private:
                long GetSerialByte();
                int Char2Int(char chr);

public:
                SerialCommand();
                void Reset();
                boolean ReceiveCommand();
                boolean DecodeCommand();
                boolean ExecuteCommand();


};


(this is the command class functions, goes with the class)
 #include "serial_command.h"


SerialCommand::SerialCommand(){

                servoPos_LastUpdated = 1500;
                for (int i=0; i<MAX_SERVO_NUM; i++)
                                servoPos[i] = 1500; // longest command is 12 char long

                Reset();

}

long SerialCommand::GetSerialByte(){

                return Serial.read();

}

int SerialCommand::Char2Int(char chr) {
                if ((chr < '0') || (chr > '9'))
                                return -1;
                else
                                return chr - '0';
}

// =========================================================
// ===================== Main functions ====================
// =========================================================

boolean SerialCommand::ReceiveCommand(){

                if (Serial.available() > 0) {

                                // determine
                                if (index >= MAX_COMMAND_LENGTH)    return false;

                                // Receiving
                                command[index] = GetSerialByte();
                                listening = false;
                                if (command[index] == '/')                                    return true;
                                listening = true;
                                index++;

                }

                // no signal available
                return false;

}

boolean SerialCommand::DecodeCommand(){

                if (command[index] != '/')     return false;

                boolean negative = false;
                int indexCur = 0;

                // Decoding

                // differentiate type of command
                if (command[indexCur] == 'M') {
                                // pin mode
                                // ... more code...
                }
                else if (command[indexCur] == 'S'){

                                // servo mode
                                while (indexCur < index) {
                                                int tempServoNum = 0;
                                                int tempServoPos = 0;

                                                // extracting servo number (2 integers)
                                                for(int i=0; i<2; i++){
                                                                indexCur++;
                                                                int temp = Char2Int(command[indexCur]);
                                                                if (temp < 0)           return false;
                                                                tempServoNum = tempServoNum*10 + temp;
                                                }

                                                // extracting pos number
                                                indexCur++;

                                                if (command[indexCur] != 'C')             return false;

                                                indexCur++;
                                                boolean negative = Char2Int(command[indexCur]);      
                                                for (int i=0; i<4; i++) {
                                                                indexCur++;
                                                                int temp = Char2Int(command[indexCur]);
                                                                if (temp < 0)           return false;
                                                                tempServoPos = tempServoPos*10 + temp;
                                                }
                                                if (negative) tempServoPos = -tempServoPos;
                                                servoPosChange[tempServoNum] = tempServoPos;

                                                // extracting loop number
                                                indexCur++;
                                                if (command[indexCur] == 'L'){

                                                                loops = 0;
                                                                for (int i=0; i<4; i++){
                                                                                indexCur++;
                                                                                int temp = Char2Int(command[indexCur]);
                                                                                if (temp < 0)           return false;
                                                                                loops = loops*10 + temp;
                                                                }

                                                                for (int i=0; i<MAX_SERVO_NUM; i++)
                                                                                servoPosAdjust[i] = (float)servoPosChange[i]/(float)loops;

                                                                indexCur++;

                                                }
                                } // end of while indexCur < index

                                return true;
                } // end of if 'S'

                else {
                                ;// unknown command
                                // ... more code...
                }
}

boolean SerialCommand::ExecuteCommand(){
                /*
                if ((pin < 0) || (mode < 0) || (data < 0))
                return false;
                */
                switch (mode){
                case 0:
                                // do noting.
                                break;
                case 1:
                                // ... more code...
                                digitalWrite(pin,data);
                                break;

                case 2:
                                // ... more code...
                                digitalRead(pin);
                                break;

                case 3:
                                // ... more code...
                                servoPos[pin] = data;
                                servoPos_LastUpdated = data;
                                break;
                }

                executed = true;
                return true;
}

void SerialCommand::Reset(){

                // initialize variables and parameters
                executed = false;
                listening = false;

                index = 0;
                //loops = 0;
                mode = 0;
                pin = 0;
                data = 0;

                for (int i=0; i<MAX_COMMAND_LENGTH; i++){
                                command[i] = 0;
                }

                for (int i=0; i<MAX_SERVO_NUM; i++){
                                servoPosChange[i] = 0;
                                servoPosAdjust[i] = 0;
                }

}