Now while it is fun to make lights blink on and off, and with the right electronics there are lots of things you can do which don't require the Arduino to be connected to a PC (other than to upload the code to it) I'm more interested (at least at the moment) on controlling the Arduino and any connected electronic components from within software running on my computer. This allows me to move some of the logic etc. off the Arduino, and means I can interface with it using any language I like; or at least any language that can talk to the USB port. The Arduino is programmed using C or C++ both of which are languages which I have only slight experience of (I now remember just how much I hate pointers) and so I can write the computer side of things in Java where I'm a lot more comfortable.
Given how I wanted to use the Arduino, one of the first main issues I've come across is finding a sensible way of passing messages backwards and forwards. I can easily read and write text to the USB port (actually a sequence of bytes but for this description saying text will suffice), the problem I had was figuring out a sensible way of converting the text into commands that I could easily interpret on the Arduino. There are a few libraries available for doing this (CmdMessenger for example) and a number of complex state machine based approaches but they all seemed either overly complex or over engineered for what I wanted. As I'm going to be in control of the code running on both the Arduino and the PC I can cut down on some of the error handling, and I will also know the full syntax of all possible commands. This allows me to reduce the code down to just the functional aspects I'll need. While it would certainly have been quicker and easier to use an existing library I a) wouldn't have (re)learnt as much as I did and b) it would have resulted in a larger code size. This second point is important as the ATmega328 PIC at the heart of the Arduino only has 32K of RAM. In comparison to modern computers this is almost nothing; although it is twice as much as the Acorn Electron on which I learnt to program and the same as the BBC Model B. My point is, that with such limited memory available (to hold both the program and the data) using a library with lots of functions I'll never need will reduce the amount of space I have available for no good reason. So let's dive in and I'll explain the approach I'm using.
The first part of any message passing system has to involve deciding on the format of the messages. For this example I'm assuming that all messages that we want to pass to the Arduino meet the following restrictions:
- the message type is encoded as a single character
- the command and any parameters are separated by the
:
character - each message is terminated by the new line character
- each message can be at most 64 characters in length
A
to represent the message type. This would then give us the simple message A:3:5
to send (with a new line at the end) to the Arduino which should respond with 8
. We could do something similar for subtraction, S:8:3
giving a response of 5
.So now that we know what a message will look like we need code to read one in:
//The maximum length (in characters) of a command const int MAX_LEN = 64; //the buffer that holds incoming data char buffer[MAX_LEN+1]; //the offset in the buffer where the next byte should be placed int offset = 0; //true if there is a new command to be processed boolean cmdReady = false; /** * A SerialEvent occurs whenever a new data comes in the * hardware serial RX, and this method is called between * successive calls of loop() */ void serialEvent() { while (Serial.available() && !cmdReady) { //if there are bytes available and there isn't a completed //command waiting to be processed... //get the new byte: char inChar = (char)Serial.read(); //if the incoming character is a newline, set a flag //so the main loop can do something about it: if (inChar == '\n') { //a new line char signifies the end of the command so... //null terminate the string buffer[offset] = NULL; //reset the offset ready for next time offset = 0; //flag up that there is a command ready to process cmdReady = true; } else { //add the character to the end of the command buffer[offset] = inChar; //move the offset on ready for the next character ++offset; } } }Lines 1 to 11 define the maximum message size, create a buffer to hold a message, and set some flags for recording how much message we have read in so far and if we have read in a complete command. The method defined in the rest of the snippet is called frequently when running on the Arduino (between successive invocations of the main loop), and it is responsible for reading in the actual message. Firstly it checks to see if there is any data to read and even if there is it only continues if there isn't a completed command waiting to be processed (the loop condition on line 19). It then reads in the next available character. If the character is the new line character then we have read in a complete message, so we NULL terminate the message (line 32; this is a C thing so that we know where in the buffer the message finishes), reset the offset counter ready for the next message (line 35), and then flag that there is a command waiting to be processed (line 38). If it wasn't a new line character then we add it to the message we are building up (line 41) and then record where we should put the next character (line 44).
Now that we can read in a message we need to actually do something with it. Chances are that you will want to do this in the main
loop
method, probably with code that fits the following template:void loop() { if (cmdReady) { //there is a command waiting to be processed... //so process it in here } //do other stuff that has to be done no matter what }There are probably many ways you could set about processing each message but what follows is an example showing an implementation of the add message described above.
//The set of commands we understand const char ADD = 'A'; //the set of token separators that split the cmd into sections const char separator_tokens[] = { ':' }; void loop() { if (cmdReady) { //there is a command waiting to be processed... //get the actual command (i.e. the first token) char* current = strtok(buffer,separator_tokens); //switch based on the command switch (*current) { case ADD: { //get and convert to ints the two numbers to add int x = atoi(strtok(NULL,separator_tokens)); int y = atoi(strtok(NULL,separator_tokens)); //do the addition int z = x+y; //return the result Serial.println(z); break; } default: //this only happens if the command is unknown //which should never happen! Serial.print("Unknown Command!"); } //we have processed the command cmdReady = false; } //do other stuff that has to be done no matter what }So let's work through this to make it clear what's happening. Firstly, while the message types are encoded as single characters it's nicer to be able to refer to them by name, and so line 2 maps message type
A
to a constant called ADD
. We also know that the sections of a command are separated by a colon, so we record this in line 5; note that we could have multiple separators if we wanted to, but the most important thing to remember is not to use a character you ever want to use in a message. Once we know we have a command to process the first thing we need to know is the type of the command. To do this, on line 16, we get the first section of the message (everything before the first colon) using the strtok
method. Essentially this splits the message into sections, given the separator character, and returns the first section. Now as we are using single characters for the message type we can simply switch
based upon the character (line 19). If we were using multiple characters for the message type then we would need to use a set of if-then-else
statements instead to check the type. Once we know what the message type is we can then process the arguments (if it has any). In this example you can see that lines 21-34 deal with the ADD
message. You don't really need to understand the example, other than to see that we can retrieve the arguments by calling strtok
again (note that we pass in NULL
instead of buffer
to essentially tell the method to carry on from where it was last time). Once we have dealt with the current message we can get ready for the next one by resetting the cmdReady
flag (line 43).I've put together a slightly longer example which also includes messages for subtraction and echoing arguments back to the PC and which shows how the snippets I've presented go together to produce the full Arduino sketch. Hopefully someone else will find this useful, but please do let me know if I've missed something obvious or if there is a simpler way of achieving a similar result.
0 comments:
Post a Comment