Introduction
Hellofriend,
Let's write some C++ today.
I want to show you how you can communicate with Arduino (or other MCs) over your usb port.
Serial communication usually uses RX and TX pins (for receive and transmit), and works at baud rates between 9600 and 115200 bd. [ Pls don't confuse bit- and
baudrate ]
This is usefull because if you have your own program for communication, you can create handlers for specific messages.
You could post twitter messages on specific triggers, for example. Or send the input of your custom-controller to your emulator. Or check the status of your underground weed plantage and initiate watering, whatever you want.
Also, I like to have full control over my programs, and a serial monitor program like Arduino's IDE or Putty, would not be able to call stuff outside it's scope.
[the complete code can be found here]
Specifications and headerfile
What should our program be able to do?
Well, the most obvious thing is to
open a serial connection at a port of our choice.
If you really want to dive into this, you should try to build a
demon that waits for an Arduino to connect to the bus system and then start the program with the correct port automaticly.
The program should also show any incoming communication and correctly flush the bus and port to avoid data-"gridlock".
Furthermore, we want a function to send data ( although we will keep this as a prototype - feel free to expand the program to your needs )
Here's the header file for you:
#ifndef _SerialClass_H
#define _SerialClass_H
#include <iostream>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#define ARDUINO_TIMEOUT 2000
using namespace std;
class mySerial
{
public:
mySerial(const char *port,int baudRate);
~mySerial();
//read buffer, return -1 if buffer empty
int readData(char *buffer, unsigned int dataLength);
//write from buffer to serial connection, true on success
bool writeData(const char *buffer, unsigned int payload);
bool connectionUp();
private:
//serial comm handler
HANDLE serialHandle;
bool isConnected;
COMSTAT status; //gets information on connection
DWORD lastError;
};
#endif // _SerialClass_H
Let's look into some parts of this file together:
If you didn't knew, the first two lines are include-guards. I will not explain what they do exactly here,
just keep in mind they provide some safety. Also keep in mind that we are using windows.h include,
which makes us quiete platform-dependant, but since UNIX uses file-like I/O, we had
to design the program in a different matter.
Line 10: timeout 2000
This is the time we provide our Arduino to boot and get the serial working. Without it,
we have a chance of data loss and could accidently interpret Arduino boot-rubbish for paydata.
Line 15-22: public members
The public parts of our class can be called from anywhere as long as you have the permission to call.
[ For example: The driver of a car checks the fuel status of the car ]
We have constructor and destructor here, also a send and receive function.
For safety issues we have a connection status function aswell.
Line 23-30: private members
The access of private members is obviously limited to the instanciated object.
[ Like, for example, only you can make your heart beat, nobody else ]
We have a
HANDLE there. A HANDLE is kind of a pointer to a specific resource (more on that later).
We have some variables to check errors and connection status, and we have a COMSTAT object
to manage the connections.
So much to our headerfile, next let's implement the class itself, omae
The class
So, we finished our header, let's code down the class now. Because the individual parts of the class can be rather long, we will examine them one after another, starting with the most notorious one, the
constructor.
But first we have to do a little excursion to understand the purpose of a
HANDLE and a
DCB object.
If you're generally interested in serial communication,
Wikipedia provides some good base knowledge.
What is a HANDLE?
So, we already know that to read/write in junction with the serial port, we need kind of a pointer to a resource. A handle is exactly that. Where in older days, you had to crawl through memory tables, you now create a HANDLE to the file that is the represantation of the hardware resource.
A pointer is one kind of Handle, but not all Handles are pointers. If you are curious about this, here is
a good thread to begin reading.
Our Handle is managing
a file he receives from the Windows API. [later more]
What is a DCB object?
A dcb structure is a control mechanism for serial communiacation devices. It has many switches, for example to control the baud rate, parity- and stop bits.
Instead of explaining every single aspect of this structure,
here is a link to the Microsoft Docs.
1 - Constructor
mySerial::mySerial(const char *port, int baudRate)
{
this->isConnected = false;
//handler to the comm port
this->serialHandle = CreateFile(port,GENERIC_READ|GENERIC_WRITE,
0,NULL,OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,NULL);
//check if everything went ok and we are connected
if(this->serialHandle == INVALID_HANDLE_VALUE)
{
int errorcode = GetLastError();
//windows api function
switch(errorcode)
{
case ERROR_FILE_NOT_FOUND:
cout << "COMMPORT " << port <<" NOT AVAILABLE;\nMAYBE WRONG PORTNUMBER?\n";
exit (1);
break;
case ERROR_ACCESS_DENIED:
cout << "ACCESS TO COMMPORT " << port<< " WAS DENIED!\n(maybe port is busy/locked?)\n";
exit (2);
break;
default:
cout << "UNKNOWN ERROR OCCURED\nTRY RESTARTING\n";
exit (-1);
break;
}
}
//define control setting for serial com device
DCB serialConnection = {0};
if(!GetCommState(serialHandle,&serialConnection))
{
cout << "ERROR GETTING COMMSTATE FOR COMMPORT " << port << endl;
exit(3);
}
//define parameters for arduino boards, note baudrate!
serialConnection.BaudRate = baudRate; //from argument 2
serialConnection.ByteSize = 8;
serialConnection.StopBits = ONESTOPBIT;
serialConnection.Parity = NOPARITY;
//make sure the arduino boots up properly after (re)start
serialConnection.fDtrControl = DTR_CONTROL_ENABLE;
//set parameters for serial port
if(!SetCommState(serialHandle, &serialConnection))
{
cout << "SETTING UP SERIAL PARAMETERS FAILED\n";
exit(4);
}
this->isConnected = true;
//flush rx and tx buffers from old characters
PurgeComm(this->serialHandle, PURGE_RXCLEAR | PURGE_TXCLEAR);
Sleep(ARDUINO_TIMEOUT);
}
Lines 6-8
At the beginning of our code, we are creating the serial Handle. As you can see in the parameter list, we want the Handle to read and write stuff. Also, we need to specify the target port. We will look into this later on.
The next lines are for error checking. If we get an invalid handle-value, we call the windows api function
GetLastError().
As you can see, the switchcase already covers up some basic error cases, represented through integer-constants and returned by getlasterror-function.
Lines 33-34
Here we are creating our DCB object. This object contains kind of an array representing the various values for our serial connection and is filled wih values throughout the lines 41-46.
One of the most important values is the baudrate, as a wrong value will render our whole communication invalid. The de facto standart for baud rates was 9600 over the years, but many Arduinos and derivates used 76800 as standart. Whichever value you wanna use, keep in mind to use the same on every device in the serial network.
Line 57
Next we are purging RX and TX pins. This is an important step, because there might be old data still lying around on that port, and we wanna have a fresh, clean connection.
Well, that was way too much work. Time to sleep for a little while, like 2 seconds, to give the Arduino time to boot up.
2 - Destructor and ReadData function
The
destructor for our class is rather easy:
mySerial::~mySerial()
{
//close open connections before disconnect
if(this->isConnected)
{
this->isConnected = false;
CloseHandle(this->serialHandle);
}
}
We check whether a connection was established, and if so, we close it's Handle.
Next, the
ReadData function:
//return 0 on Error or empty port for easy if-usage
int mySerial::readData(char *buffer, unsigned int dataLength)
{
//unsigned int, os dependant
DWORD byteCount;
DWORD toRead;
//read status of port with api function
ClearCommError(this->serialHandle,&this->lastError,&this->status);
//check if data arrives and weather it fits our buffer
if(this->status.cbInQue != 0)
{
//get number of bytes to read and return bytes from port
this->status.cbInQue > dataLength ? toRead = dataLength : toRead = this->status.cbInQue;
return ReadFile(serialHandle,buffer,toRead,&byteCount,NULL) ? byteCount : 0;
}
return 0; //empty port or error
}
This function receives payloads from the Arduino, thus it needs a buffer of some kind and a delimiter, as function parameters.
At first, we are creating two unsigned ints. A
Double
Word the OS-dependant represantation of an Integer, and on most computers referes to a 32bit data type. The reason we are using a DWORD and not simply an int for this purpose is that we can pass memory management and pointer arithmetic to the underlying operation system. If we had just used an
int here, and in 5 years somebody decides "hey, why don't we expand every operand and memory cell to 128bit, cause fukk yeah, well, then our program would be pretty wasted (which is the destiny of many oldschool programs nowadays and partly a reason MS brought so much compalibility modes to the table).
Also, this is kind of a convention, so "When in Rome, do as the Romans do".
Next, we are clearing any comm errors, and after that we check if we got any data in the buffer.
Our comstat object returns the number of bytes received but not yet read as "cbInQue".
Now let's check how much data we got: is there more data on the line than size of our buffer?
Depending on the outcome of this check, the DWORD toRead holds either the maximum length of our char array, or the count of the remaining bytes in the buffer. This is important because we are filling a char array, thus we don't want any overflow, and we want the exact remaining count of bytes so we won't get any nullpointers.
If there were more bytes in the pipe than we can store, the remaining bytes are split, and the process is repeated until the buffer is completely cleared.
3 - WriteData and connectionUP
We almost got it, cybermonkeys, one final push and our class is finished.
If we can receive data, there naturally has to be a way to send them aswell, also we could use some sort of check routine that tells us whether we already got a connection up or not.
bool mySerial::writeData(const char *payload, unsigned int dataLength)
{
DWORD bytecount;
//try writing to serial
if(!WriteFile(this->serialHandle,(void *)payload,dataLength,&bytecount,0))
{
//return false and get comm error on FALSE
ClearCommError(serialHandle,&this->lastError,&this->status);
return false;
}
return true;
}
bool mySerial::connectionUp()
{
//Simply return the connection status
return this->isConnected;
}
Ok, these two fellas are easy to explain:
writeData needs two parameters: a Buffer(char*) and an int called dataLength.
As you should already know, we need to handle the bytecount, thus creating another DWORD here.
The write function itself needs a
handle, our
payload, the
dataLength, a
pointer to bytecount and an
overlapped parameter, that is set to 0, so we can ignore it here.
The writeFile function itself returns a boolean, so we can use it to check for errors.
If an error occures, we clear all remaining error messages and return false, if everything went well, we return true. Pretty much straight forward, eh?
The other function is
connectionUp, which simply tells us the status of our connection. Remember how we only set this boolean to true if the connection really was established without any errors?
That's it, we finished the class. Yeah! Now, let's implement it into a real program.
4 - Implementation and testing
After all this fuzz I feel we should do something rather ez, so let us implement the class together and try it's functionality.
Just grab whichever Arduino you can find, and plug it into your usb slot (the Arduino should actually communicate with the serial interface, lol).
Important
Don't get confused by the name of our main function. It is called int _tmain because it should be able to accept 16bit unicode parameters (greetings from europe, where every country has their own shitty set of unique characters).
It is basicly an expansion of the normal int main function made by Microsoft, and we need to use it in order to work with the Windows API.
Here's the code:
#include
#include
#include
#include "SerialClass.h"
#include
using namespace std;
// application reads from the specified serial port and reports the collected data
int _tmain(int argc, _TCHAR* argv[])
{
printf("Welcome to the serial test app!\n\n");
//char *portNumber = argv[1];
//When addressing ports larger than COM9 in Windows you will have to specify the port like this: "COM10" becomes "\\\\.\\COM10"
mySerial* SP = new mySerial("\\\\.\\COM10"); // adjust as needed
if (SP->connectionUp())
printf("We're connected\n");
char incomingData[256] = ""; // don't forget to pre-allocate memory
//printf("%s\n",incomingData);
int dataLength = 255;
int readResult = 0;
while(SP->connectionUp())
{
readResult = SP->readData(incomingData,dataLength);
// printf("Bytes read: (0 means no data available) %i\n",readResult);
incomingData[readResult] = 0;
printf("%s",incomingData);
Sleep(500);
}
return 0;
}
Forgive me, but I have only so much time, so we focus on receiving stuff from serial here, but if you really are curious about sending and can't manage to do it, feel free to write me and I will see if I can help you.
Yeah, we received some data from our Arduino, and depending on what our goals are we can now start programs, make calls, print out stuff, threaten NK on twitter or anything.
There is but one important thing left
For some mysterious reason Microsoft decided that port numbers greater 9 have to be specified with many escape symbols, so instead of com10 we have to write
\\\\.\\COM10.
If the port number is 9 or smaller, you can just write it down.
Remember to check in your device manager which port your Arduino is bound.
[This is the one thing that could be improved with this main class, if I was willing, that is]
I hope you liked this rather long coding session, cybermonkeys.
I had fun creating this, and if you feel like I do, go and share your shit with others!
Information has to flow, and those who seal away information are to be crushed!
Let's part with a word from Mr. Kisaragi:
Strength without determination means nothing,
and determination without strength is equally useless.
- numb.3rs
Comments
Post a Comment