Commits

Christopher De Vries committed 23e098b

First draft of Arduino communication example.

Comments (0)

Files changed (5)

+syntax: glob
+*~
+*.pyc
+
+syntax: regexp
+(^|/)CVS($|/)
+(^|/)build($|/)
+(^|/)dist($|/)
+(^|/)MANIFEST$
+Artistic License 2.0
+Copyright (c) 2000-2006, The Perl Foundation.
+
+Everyone is permitted to copy and distribute verbatim copies of this license
+document, but changing it is not allowed.
+
+Preamble
+This license establishes the terms under which a given free software Package
+may be copied, modified, distributed, and/or redistributed. The intent is that
+the Copyright Holder maintains some artistic control over the development of
+that Package while still keeping the Package available as open source and free
+software.
+
+You are always permitted to make arrangements wholly outside of this license
+directly with the Copyright Holder of a given Package. If the terms of this
+license do not permit the full use that you propose to make of the Package,
+you should contact the Copyright Holder and seek a different licensing
+arrangement.
+
+Definitions
+"Copyright Holder" means the individual(s) or organization(s) named in the
+copyright notice for the entire Package.
+
+"Contributor" means any party that has contributed code or other material to
+the Package, in accordance with the Copyright Holder's procedures.
+
+"You" and "your" means any person who would like to copy, distribute, or
+modify the Package.
+
+"Package" means the collection of files distributed by the Copyright Holder,
+and derivatives of that collection and/or of those files. A given Package may
+consist of either the Standard Version, or a Modified Version.
+
+"Distribute" means providing a copy of the Package or making it accessible to
+anyone else, or in the case of a company or organization, to others outside of
+your company or organization.
+
+"Distributor Fee" means any fee that you charge for Distributing this Package
+or providing support for this Package to another party. It does not mean
+licensing fees.
+
+"Standard Version" refers to the Package if it has not been modified, or has
+been modified only in ways explicitly requested by the Copyright Holder.
+
+"Modified Version" means the Package, if it has been changed, and such changes
+were not explicitly requested by the Copyright Holder.
+
+"Original License" means this Artistic License as Distributed with the
+Standard Version of the Package, in its current version or as it may be
+modified by The Perl Foundation in the future.
+
+"Source" form means the source code, documentation source, and configuration
+files for the Package.
+
+"Compiled" form means the compiled bytecode, object code, binary, or any other
+form resulting from mechanical transformation or translation of the Source
+form.
+
+Permission for Use and Modification Without Distribution
+(1) You are permitted to use the Standard Version and create and use Modified
+Versions for any purpose without restriction, provided that you do not
+Distribute the Modified Version.
+
+Permissions for Redistribution of the Standard Version
+(2) You may Distribute verbatim copies of the Source form of the Standard
+Version of this Package in any medium without restriction, either gratis or
+for a Distributor Fee, provided that you duplicate all of the original
+copyright notices and associated disclaimers. At your discretion, such
+verbatim copies may or may not include a Compiled form of the Package.
+
+(3) You may apply any bug fixes, portability changes, and other modifications
+made available from the Copyright Holder. The resulting Package will still be
+considered the Standard Version, and as such will be subject to the Original
+License.
+
+Distribution of Modified Versions of the Package as Source
+(4) You may Distribute your Modified Version as Source (either gratis or for a
+Distributor Fee, and with or without a Compiled form of the Modified Version)
+provided that you clearly document how it differs from the Standard Version,
+including, but not limited to, documenting any non-standard features,
+executables, or modules, and provided that you do at least ONE of the
+following:
+
+(a) make the Modified Version available to the Copyright Holder of the
+Standard Version, under the Original License, so that the Copyright Holder may
+include your modifications in the Standard Version.
+(b) ensure that installation of your Modified Version does not prevent the
+user installing or running the Standard Version. In addition, the Modified
+Version must bear a name that is different from the name of the Standard
+Version.
+(c) allow anyone who receives a copy of the Modified Version to make the
+Source form of the Modified Version available to others under
+(i) the Original License or
+(ii) a license that permits the licensee to freely copy, modify and
+redistribute the Modified Version using the same licensing terms that apply to
+the copy that the licensee received, and requires that the Source form of the
+Modified Version, and of any works derived from it, be made freely available
+in that license fees are prohibited but Distributor Fees are allowed.
+
+Distribution of Compiled Forms of the Standard Version or Modified Versions
+without the Source
+(5) You may Distribute Compiled forms of the Standard Version without the
+Source, provided that you include complete instructions on how to get the
+Source of the Standard Version. Such instructions must be valid at the time of
+your distribution. If these instructions, at any time while you are carrying
+out such distribution, become invalid, you must provide new instructions on
+demand or cease further distribution. If you provide valid instructions or
+cease distribution within thirty days after you become aware that the
+instructions are invalid, then you do not forfeit any of your rights under
+this license.
+
+(6) You may Distribute a Modified Version in Compiled form without the Source,
+provided that you comply with Section 4 with respect to the Source of the
+Modified Version.
+
+Aggregating or Linking the Package
+(7) You may aggregate the Package (either the Standard Version or Modified
+Version) with other packages and Distribute the resulting aggregation provided
+that you do not charge a licensing fee for the Package. Distributor Fees are
+permitted, and licensing fees for other components in the aggregation are
+permitted. The terms of this license apply to the use and Distribution of the
+Standard or Modified Versions as included in the aggregation.
+
+(8) You are permitted to link Modified and Standard Versions with other works,
+to embed the Package in a larger work of your own, or to build stand-alone
+binary or bytecode versions of applications that include the Package, and
+Distribute the result without restriction, provided the result does not expose
+a direct interface to the Package.
+
+Items That are Not Considered Part of a Modified Version
+(9) Works (including, but not limited to, modules and scripts) that merely
+extend or make use of the Package, do not, by themselves, cause the Package to
+be a Modified Version. In addition, such works are not considered parts of the
+Package itself, and are not subject to the terms of this license.
+
+General Provisions
+(10) Any use, modification, and distribution of the Standard or Modified
+Versions is governed by this Artistic License. By using, modifying or
+distributing the Package, you accept this license. Do not use, modify, or
+distribute the Package, if you do not accept this license.
+
+(11) If your Modified Version has been derived from a Modified Version made by
+someone other than you, you are nevertheless required to ensure that your
+Modified Version complies with the requirements of this license.
+
+(12) This license does not grant you the right to use any trademark, service
+mark, tradename, or logo of the Copyright Holder.
+
+(13) This license includes the non-exclusive, worldwide, free-of-charge patent
+license to make, have made, use, offer to sell, sell, import and otherwise
+transfer the Package with respect to any patent claims licensable by the
+Copyright Holder that are necessarily infringed by the Package. If you
+institute patent litigation (including a cross-claim or counterclaim) against
+any party alleging that the Package constitutes direct or contributory patent
+infringement, then this Artistic License to you shall terminate on the date
+that such litigation is filed.
+
+(14) Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
+AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE
+IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
+NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW.
+UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY
+OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+Arduino Communication Example
+Copyright 2011 Christopher De Vries
+
+Serial communication with the Arduino is relatively simple, but it can
+sometimes be difficult to organize input and output around the other tasks you
+want the Arduino to accomplish. I use a fairly simple framework for
+two-way communication, but it relies on executing commands which are not going
+to take a whole lot of time or add any delays to each loop.
+
+Basically every time the Arduino runs through loop I do two things:
+
+1. Monitor and or change any I/O pins that require monitoring or changing.
+2. Check for any input, if a full command is received, execute that command.
+
+The framework assumes command sent and received by the Arduino will be text
+terminated by a newline character. On the Arduino side the setup is fairly
+simple. In the setup() function, serial communication should be initialized. I
+tend to use 9600 bps if speed is not a factor, but it seems to work fine at
+115200 bps as well. Use the following statement:
+
+  Serial.begin(9600);
+
+Next, I define a couple of functions. The first is the routine that reads
+commands into the buffer and executes them when they are complete. This code
+does not vary from program to program:
+
+  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;
+        }
+      }
+    }
+  }
+
+The code above reads one character into the buffer each loop (if the character
+is available). If it comes to a newline character it terminates the buffer
+with a NULL character and runs parseAndExecuteCommand(String command). The
+buffer is limited to 128 characters. If too many characters are read into the
+buffer, the Arduino responds with "ERROR Command Overflow".
+
+The parseAndExecuteCommand function will change depending on the commands you
+use. The one in my example responds to two basic commands:
+
+1. If the computer issues CHECK, the Arduino responds with CONNECT, indicating
+that it is ready and the connection is working.
+2. If the computer issues QUERY, the Arduino responds with the state of the
+LED (pin 13, HIGH or LOW). This routine will vary depending on the program.
+
+  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");
+    }
+  }
+
+The main loop is now simple. For any sketch you load on the Arduino, the
+sequence will look like this:
+
+  void loop() {
+    // Do whatever monitoring and work is necessary each loop below
+    <<Code goes here>>
+
+    // Load and parse any commands
+    checkInput();
+  }
+
+The example included with this README is a program that blinks the LED
+attached to pin 13 on and off every 2 seconds (1 second on, 1 second off). If
+it receives a query it will indicate the state of the LED.
+
+I also include a python based user interface with which to interact with the
+Arduino. The python side is a little more complex as I mix the event driven
+style of a user interface with a threaded approach to I/O. A pair of threads
+are created when the python program connects to the serial port (using
+pySerial). The input thread assembles input from the Arduino until it receives
+a newline and then places information in the input queue which is periodically
+polled (every 100 ms) by the main event loop. The UI can then react to the
+information received by the Arduino. Events in the UI can trigger commands to
+go to the Arduino. These commands are placed in an output queue which the
+output thread grabs and then sends down the serial line. 
+
+In the example, the python user interface has a line at the top with a blank
+where you need to put the name of the serial interface to the arduino. On my
+Mac it is typically /dev/tty.usbmodemfd321, but you can look it up using the
+Arduino IDE. Hit the connect button and in about 2 seconds you should receive
+confirmation that the Arduino is connected. Next, just hit the query button
+and the LED state at that instant should be indicated on the UI.
+
+I hope this example helps you get started with 2-way communication with the
+Arduino.
+
+Note: The example code in this distribution is covered by the Artistic License
+2.0, an open source license. This means you are free to modify and distribute
+this code with some restrictions. Please read the LICENSE file for more
+details.

comm_example/comm_example.pde

+unsigned long nextStateChange;
+int ledState = LOW;
+
+void setup() {
+  pinMode(13,OUTPUT);
+  Serial.begin(9600);
+}
+
+void loop() {
+  long int currentTime;
+  int inbyte;
+  
+  // Perform work to be done
+  currentTime = millis();
+  if(currentTime>=nextStateChange) {
+    changeLEDState();
+    nextStateChange = currentTime+1000l;
+  }
+  
+  // Accept and parse input
+  checkInput();
+}
+  
+void changeLEDState() {
+  if(ledState==LOW) {
+    ledState = HIGH;
+    digitalWrite(13,HIGH);
+  }
+  else {
+    ledState = LOW;
+    digitalWrite(13,LOW);
+  }
+}
+
+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;
+      }
+    }
+  }
+}
+
+// This routine is specific to the commands you will be interpreting.
+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");
+  }
+}
+
+
+
+#!/usr/bin/env python
+
+import sys
+import Tkinter
+import threading
+import Queue
+import serial
+
+def 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 0
+
+class GuiClass(object):
+    def __init__(self,inputQ,outputQ,commManager):
+        self.inputQ=inputQ
+        self.outputQ=outputQ
+        self.manager=commManager
+
+        self.root = Tkinter.Tk()
+        
+        text_frame = Tkinter.Frame(self.root)
+        entry_frame = Tkinter.Frame(self.root)
+        
+        scrollbar = Tkinter.Scrollbar(text_frame)
+        self.text = Tkinter.Text(text_frame,height=5,width=80,wrap=Tkinter.WORD,yscrollcommand=scrollbar.set)
+        scrollbar.pack(side=Tkinter.RIGHT,fill=Tkinter.Y)
+        self.text.pack(fill=Tkinter.BOTH,expand=True)
+        scrollbar.config(command=self.text.yview)
+
+        portLabel = Tkinter.Label(entry_frame, text="Arduino Port: ")
+        self.arduinoFile = Tkinter.StringVar()
+        portEntry = Tkinter.Entry(entry_frame, textvariable=self.arduinoFile)
+        portButton = Tkinter.Button(entry_frame, text="Connect", command=self.connect)
+        portLabel.pack(side=Tkinter.LEFT)
+        portEntry.pack(side=Tkinter.LEFT,fill=Tkinter.X,expand=True)
+        portButton.pack(side=Tkinter.LEFT)
+
+        queryButton = Tkinter.Button(self.root,text="Query",command=self.query)
+        quitButton = Tkinter.Button(self.root,text="Quit",command=self.root.quit)
+        entry_frame.pack(fill=Tkinter.X)
+        text_frame.pack(fill=Tkinter.BOTH,expand=True)
+        queryButton.pack(fill=Tkinter.X)
+        quitButton.pack(fill=Tkinter.X)
+        self.text.configure(state=Tkinter.DISABLED)
+        self.firstline=True
+
+        # Menubar
+        menubar = Tkinter.Menu(self.root)
+        filemenu = Tkinter.Menu(menubar,tearoff=False)
+        #filemenu.add_command(label="Open...",command=self.openport)
+        filemenu.add_separator()
+        filemenu.add_command(label="Quit",command=self.root.quit)
+        menubar.add_cascade(label="File",menu=filemenu)
+        self.root.config(menu=menubar)
+        self.root.title("Python UI")
+
+    def go(self):
+        self.root.after(100, self.periodic_check)
+        self.root.mainloop()
+
+    def connect(self):
+        filename = self.arduinoFile.get()
+        self.manager.connect(filename)
+        self.root.after(2000,self.check_connection)
+
+    def check_connection(self):
+        self.outputQ.put("CHECK")
+
+    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]=="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)
+
+    def writeline(self,text):
+        self.text.configure(state=Tkinter.NORMAL)
+        if self.firstline:
+            self.firstline=False
+        else:
+            self.text.insert(Tkinter.END,"\n")
+
+        self.text.insert(Tkinter.END,text)
+        self.text.yview(Tkinter.END)
+        self.text.configure(state=Tkinter.DISABLED)
+    
+    def query(self):
+        self.outputQ.put("QUERY")
+
+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 = False
+
+    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
+        return
+
+    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
+
+        return
+
+    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 = True
+
+    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("All IO Threads stopped")
+
+if __name__ == "__main__":
+    sys.exit(main())