Handling two-way communication with an Arduino is something I have had to do with several projects. In this short article, I discuss and demonstrate the method I typically use to communicate from a Python Tkinter graphical user interface to an Arduino which is actively also performing another task besides waiting and responding to serial communication. Please download the example source code through by bitbucket repository. The source code files are available as a gzipped tar file: arduino-comm-example-1.1.tar.gz or as a zip file: arduino-comm-example-1.1.zip. The Arduino SketchCommunication is all about organization. I am assuming that the Arduino has other things to do besides monitor and respond to communication. If that is all the Arduino is doing, then the problem becomes much easier. Generally this is not the case. For the example online, I chose to have the Arduino blink the LED on pin 13 off and on in 1 second intervals. For you application, you may be controlling a motor, reading in sensor information, or even something more complex. In order to handle that task and communication, the Arduino must run through all the tasks it has to perform in every loop. The basic architecture for communication is, for each loop:
In the setup() portion of the Arduino sketch, be sure to turn on serial communication with the command:Serial.begin(9600);I tend to define two functions for dealing with input. The first, void checkInput() is generic to all my sketches which do simple two-way ASCII only I/O over the serial line. This function checks to see if there is available input from the serial line. If there is, then it gets 1 character and places it on the buffer. The buffer is set to be 128 characters long. If the buffer is going to overflow, then it clears the buffer and issues an ERROR response which it sends down the serial line. If the character it receives is a newline character (10 in decimal) then the program has received a complete command. It creates a String holding the command and passes it to the second function, void parseAndExecuteCommand(String command). The second function needs to be rewritten for the commands of the particular Arduino project. Splitting up the input handling this way allows me to split the work into the generic input handling routines and the specific lexical parsing for that project. The void checkInput() function is shown below. /* This routine checks for any input waiting on the serial line. If any is * available it is read in and added to a 128 character buffer. It sends back * an error should the buffer overflow, and starts overwriting the buffer * at that point. It only reads one character per call. If it receives a * newline character is then runs the parseAndExecuteCommand() routine. */void checkInput() { int inbyte; static char incomingBuffer[128]; static char bufPosition=0; if(Serial.available()>0) { // Read only one character per call inbyte = Serial.read(); if(inbyte==10) { // Newline detected incomingBuffer[bufPosition]='\0'; // NULL terminate the string bufPosition=0; // Prepare for next command // Supply a separate routine for parsing the command. This will // vary depending on the task. parseAndExecuteCommand(String(incomingBuffer)); } else { incomingBuffer[bufPosition]=(char)inbyte; bufPosition++; if(bufPosition==128) { Serial.println("ERROR Command Overflow"); bufPosition=0; } } }}An example of a simple void parseAndExecuteCommand(String command) function is shown below. This particular function only recognizes two commands:
/* This routine parses and executes any command received. It will have to be * rewritten for any sketch to use the appropriate commands and arguments for * the program you design. I find it easier to separate the input assembly * from parsing so that I only have to modify this function and can keep the * checkInput() function the same in each sketch. */void parseAndExecuteCommand(String command) { if(command.equals(String("CHECK"))) { // Check connection, respond with CONNECT Serial.println("CONNECT"); } else if(command.equals(String("QUERY"))) { // Query state of the LED if(ledState==LOW) { Serial.println("LOW"); } else { Serial.println("HIGH"); } } else { // Unrecognized command Serial.println("ERROR Unrecognized Command"); }}Given these two routines, we can now write the main loop of the Arduino sketch. It will follow the following pattern: void loop() { /* * Insert non-communication code here. */ checkInput(); // Now handle Serial I/O}For the example sketch, the main loop just checks to see if it is time to change the LED state, and does so if a second has passes since the last state change. This is shown below. /* The loop is set up in two parts. Firs the Arduino does the work it needs to * do for every loop, next is runs the checkInput() routine to check and act on * any input from the serial connection. */void loop() { long int currentTime; int inbyte; // Perform work to be done currentTime = millis(); if(currentTime>=nextStateChange) { changeLEDState(); nextStateChange = currentTime+1000l; // Note that is a lower case L at the end of 1000. } // Accept and parse input checkInput();}Information can also be sent from the Arduino back over the serial line. In order to demonstrate this, the void changeLEDState() function reports back over the serial line how many times its flashed ever 10 flashes using the CYCLE command. void changeLEDState() { if(ledState==LOW) { ledState = HIGH; digitalWrite(13,HIGH); cycle_number++; if(cycle_number%10==0) { // Report every 10th cycle Serial.print("CYCLE "); Serial.println(cycle_number); } } else { ledState = LOW; digitalWrite(13,LOW); }}The Python Graphical User Interface/dev/tty.usbmodemfd321. This is just a file in the filesystem, so I was going to use an open file dialog to select this file, but you cannot open a file in the /dev directory from a standard open file dialog, so I had to leave a text entry blank. I found this annoying. Below that first row is a text area where information is printed. After clicking the connect button, you will receive the message " Arduino Connected" if the CHECK command was sent and a CONNECT response was received from the Arduino. I typically include a short waiting period (2 seconds) because I have noticed that after establishing the connection an immediate query leads to some unpredictable responses. The "Query" button sends the QUERY command to the Arduino. When a response is received it is interpreted and the message "LED is Off" or "LED is On" is printed to the text area. Finally a "Quit" button exits the program.Note: This code requires the PySerial Python Serial Port Extension. Starting UpI divided the code up into two main classes. The CommunicationManager which handles communication back and forth to the Arduino, and the GuiClass which handles the user interface. I also initialize the queues they will use to communicate back and forth. Once I have instantiated and initialized these classes, I enter the Tkinter event loop. The CommunicationManager will take care of establishing communication once the serial port name is given, and will create the threads that handle input and output. The code to do this is below.import sysimport Tkinterimport tkMessageBoximport threadingimport Queueimport serialdef main(argv=None): if argv is None: argv = sys.argv inputQ = Queue.Queue() outputQ = Queue.Queue() manager = CommunicationManager(inputQ,outputQ) gui = GuiClass(inputQ,outputQ,manager) gui.go() return 0The Tkinter Graphical User InterfaceThe GuiClass needs to remember to poll the input queue periodically, I have therefore made a method within the GuiClass which handles periodic polling of the input queue. It splits the input into tokens and then, based on the response, decides how to proceed. Note that after running, it schedules itself for another run in 100 milliseconds.class GuiClass(object): # Download the example to see code I am not showing here def periodic_check(self): try: inp = self.inputQ.get_nowait() tokens = inp.split() if len(tokens)>=1: if tokens[0]=="CONNECT": self.writeline("Arduino Connected") elif tokens[0]=="HIGH": self.writeline("LED is On") elif tokens[0]=="LOW": self.writeline("LED is Off") elif tokens[0]=="CYCLE": self.writeline("tokens[1]+" LED Flashes Completed.") elif tokens[0]=="IOHALT": self.writeline("IO threads halted") elif tokens[0]=="ERROR": self.writeline("Error: "+' '.join(tokens[1:])) else: self.writeline("Unrecognized response:") self.writeline(inp) except Queue.Empty: pass # self.writeline("No Data") self.root.after(100, self.periodic_check)Note that the GUI has to schedule the first periodic check as it enters the main loop. I put that in the GuiClass.go method. def go(self): self.root.after(100, self.periodic_check) self.root.mainloop()For output the GuiClass just places complete commands on the output queue. These will be popped off the queue by the CommunicationManager and sent to the Arduino. For example, the query button executes the following method of GuiClass: def query(self): self.outputQ.put("QUERY")The Communication ManagerThe event driven GUI generates instructions for the Arduino which it places on the output queue, and receives information from the Arduino via the input queue, but it's the communication manager that handles the serial communication. We initialize the CommunicationManager class with only the input and output queues. Initially it is not even connected to the Arduino as it does not know the name of the serial port. I do not create worker threads to send and receive information to and from the Arduino until after a connection. The initialization method of the CommunicationManager class is shown below.class CommunicationManager(object): def __init__(self,inputQ,outputQ): self.inputQ=inputQ self.outputQ=outputQ self.serialPort=None self.inputThread = None self.outputThread = None self.keepRunning = True self.activeConnection = FalseOnce a serial file name is entered and the connect key is pressed, the communication manager establishes a connection and starts the worker threads. Note that if the user decides to connect to a different arduino while the program is still running, the communication manager will have to stop the threads and close the connection. This is possible, and you will note that the connect method checks to see if there is already an active connection and closes it before opening a new one. def connect(self,filename): if self.activeConnection: self.close() self.activeConnection = False try: self.serialPort = serial.Serial(filename,9600) except serial.SerialException: self.inputQ.put("ERROR Unable to Connect to Serial Port") return self.keepRunning=True self.inputThread = threading.Thread(target=self.runInput) self.inputThread.daemon=True self.inputThread.start() self.outputThread = threading.Thread(target=self.runOutput) self.outputThread.daemon=True self.outputThread.start() self.activeConnection = TrueThe worker threads have very simple jobs. The input thread blocks on reading a serial input line until the entire line is received, it then strips the newline (and any other whitespace) off the end of the string and places the information on the input queue. def runInput(self): while self.keepRunning: try: inputline = self.serialPort.readline() self.inputQ.put(inputline.rstrip()) except: # This area is reached on connection closing pass returnThe output thread waits for information to be added to the output queue and then sends that to the Arduino, appending a newline. def runOutput(self): while self.keepRunning: try: outputline = self.outputQ.get() self.serialPort.write("%s\n"%outputline) except: # This area is reached on connection closing pass returnNote that both threads will continue to loop until self.keepRunning is False. When the close method is run, self.keepRunning is set to false, however that is not enough to stop the threads, which are most likely blocking for either input from the serial port or waiting for something to be placed on the output queue. In order to unblock the threads, we first close the serial port, which will cause the input thread to throw an exception. This exception is ignored and the input thread ends. For the output thread we need to place some junk on the queue. This will cause the output thread to try to write to a now closed serial port which will cause an exception which is ignored and then the thread returns. Two join commands wait for those processes to occur, before a signal is placed on the input queue indicating that the connection is closed. The close method is shown below: def close(self): self.keepRunning=False; self.serialPort.close() self.outputQ.put("TERMINATE") # This does not get sent, but stops the outputQ from blocking. self.inputThread.join() self.outputThread.join() self.inputQ.put("IOHALT")SummaryHandling serial input and output from an Arduino is very straightforward. Using the techniques above it is possible for the Arduino to become a great intermediary between your computer and a host of sensors, electric, and mechanical devices outside your computer. Please download the example code by clicking on the appropriate link below.
|

