Sunday, September 22, 2013

Arduino Round III: Trying to Listen to the Computer

Having read data from the Arduino to a computer, the next step was to figure out how to send data from a computer to an Arduino. Again, the Arduino framework made this a much easier task than I thought. The mini project that I chose to implement this was a simple GUI which controls the RGB values of a three colour LED. Really, this is identical to independently controlling three LEDs and would work the same way.

Given that I don't know Python worth a dang, the most challenging part was the Python. First, I searched google for Python GUI, and found a plethora of packages. After a brief browse I went for the "native package" TkInter. Starting with a few lines of example code, I got the buttons, slider bars and everything on the screen. Writing to serial was as simple as writing to a console via serial.write().

The GUI, show above, was to have buttons to connect to the Arduino via the serial port, three sliding scrollbars for red green and blue LED intensity, and a 'disco mode' button which cycles through the colours. The program window is shown here:


There were a few nuisances. One of them was in naming. What I wanted was a sliding widget which was a scrollbar returning an interger withing a specific range. In Java, it's called a Scrollbar. In tKinter, a Scrollbar is a separate object, which is attached to other objects. What I really wanted was a "Scale". Next, in Java, there is a function called whenever the Scrollbar is updated, so that when the user slides from 12 to 25, you can automatically run code. As far as I could tell, this is not the case in TkInter. There are several quick workarounds: one is to have a button run a function which polls the Scales. A slightly better solution is binding which allows you to run code every time some pre-determined thing (releasing the right mouse-button for example) in a widget.

The code I used is here:

from Tkinter import *
import serial
import time

class Application(Frame):
# Store LED brightness, stored as integers, sent as bytes
    rBrightness = -1
    gBrightness = 0
    bBrightness = 0
# Slight delay between sending serial data so as not to miss a char
# Possibly unnecessary.    
    delay  = 0.01
# Strictly for keeping track of what label to put on buttons
    on = False;
    discoOn = False;
# GUI action Function definitions        

# Open or close the serial port connection. For now hardwired to COM3
    def toggle_connect(self):
        if self.ser.isOpen():
            self.ser.close()
            print("Closed COM 3")
            self.COM["text"] = "Open",
        else:
            self.ser = serial.Serial('COM3', 9600, timeout=0)
            print("Opened COM 3")
            self.COM["text"] = "Close",
# Shuts down the python interpreter. Last resort for port closing
    def shut_down(self):
        self.ser.close()
        exit()
# Send a code to Arduino to switch LED off.        
    def send_on_off(self):        
        self.ser.write('0')
        if self.on:
            self.on = False
            self.LED["text"] = "Turn On LED"
        else:
            self.on = True
            self.LED["text"] = "Turn Off LED"
# Disco light show baby.            
    def disco_on_off(self):
        self.ser.write('d')
        if self.discoOn:
            self.discoOn = False
            self.disco["text"] = "Start Party"
        else:
            self.discoOn = True
            self.disco["text"] = "Stop Party"        
# Sends RGB values to the arduino encoded as a 'char' 
# Method is bound to mouse clicks on the slider        
    def update_LED(self,event):
        self.rBrightness = self.scaleRed.get()
        self.gBrightness = self.scaleGreen.get()
        self.bBrightness = self.scaleBlue.get()
        print("(R,G,B) = (%i,%i,%i)") %(self.rBrightness,self.gBrightness,self.bBrightness)
        self.ser.write('1')
        time.sleep(self.delay)
        self.ser.write(str(unichr(self.rBrightness)))
        time.sleep(self.delay)
        self.ser.write('2')
        time.sleep(self.delay)
        self.ser.write(str(unichr(self.gBrightness)))
        time.sleep(self.delay)
        self.ser.write('3')        
        time.sleep(self.delay)
        self.ser.write(str(unichr(self.bBrightness)))
# GUI stuff        
    def createWidgets(self):
# First, create buttons        
        self.QUIT = Button(self)
        self.QUIT["text"] = "QUIT"
        self.QUIT["fg"]   = "red"
        self.QUIT["command"] =  self.shut_down

        self.COM = Button(self)
        self.COM["text"] = "Open",
        self.COM["command"] = self.toggle_connect

        self.LED = Button(self)
        self.LED["text"] = "Turn On LED"
        self.LED["command"] = self.send_on_off
        
        self.disco = Button(self)
        self.disco["text"] = "Start Party"
        self.disco["command"] = self.disco_on_off        
# Sliders (if this were java, I'd say scroll bars. In python, scrollbar is another thing altogether)        
        self.scaleRed = Scale(self,from_=0, to=255)
        self.scaleRed["bg"] = "red"        
        
        self.scaleGreen = Scale(self,from_=0, to=255)
        self.scaleGreen["bg"] = "green"        
        
        self.scaleBlue = Scale(self,from_=0, to=255)
        self.scaleBlue["bg"] = "blue"
# Add widgets to the screen                        
        self.QUIT.pack({"side": "left"})
        self.COM.pack({"side": "left"})        
        self.LED.pack({"side": "left"})
        self.scaleRed.pack({"side": "left"})
        self.scaleGreen.pack({"side": "left"})
        self.scaleBlue.pack({"side": "left"})
        self.disco.pack({"side": "left"})    
# Bind scrollbars to mouseclicks        
        self.scaleGreen.bind('',self.update_LED)
        self.scaleBlue.bind('',self.update_LED)
        self.scaleRed.bind('',self.update_LED)
# initialize GUI
    def __init__(self, master=None):
        Frame.__init__(self, master)
        self.pack()
        self.createWidgets()
        self.ser = serial.Serial('COM3', 9600, timeout=0)
        self.ser.close()

root = Tk()
app = Application(master=root)
app.mainloop()
root.destroy()

The Arduino code was next. The idea was to have have the Arduino constantly looking for a new char sent through the COM port. If it receives a particular character, it will change state accordingly. For example, a '0' will toggle the LED output on or off. A 'd' will start disco party mode. A '1', '2', or '3' will put the chip in a listening state for red, green or blue respectively. In this state, the next char received is assigned to the corresponding LED's brightness. The code shown here:


// Accept commands from serialComm.py Python script
// Controls the RGB values of a 3-color LED.
// Optional 'disco mode' which cycles through the colours
// Author: Andrew MacRae (macrae@berkeley.edu)

// PWM output ports
const int RLED = 9;
const int GLED = 10;
const int BLED = 11;
// Device states
const int LISTEN = 0;    // Accepting data.
const int READ_RED = 1;  // next char read assigned to RED.
const int READ_GREEN = 2;// Same, but for green.
const int READ_BLUE = 3; // ditto, for blue.

boolean on = false;     // Outputing to the LED?
boolean disco = false;  // Disco mode
// Stores the RGB values recieved from the GUI
int rBright = 28;        
int gBright = 128;
int bBright = 68;
// For disco mode. These will be automatically adjusted
int iR = 0;
int iG = 0;
int iB = 0;

byte byteRead = 'x'; // current byte read from serial
int state = LISTEN;  // Initially, wait for instructions

// Setup ports for input and start the serial connection
void setup()
{
  pinMode(RLED, OUTPUT);
  pinMode(GLED, OUTPUT);
  pinMode(BLED, OUTPUT);
  Serial.begin(9600);
}
// main loop
void loop()
{
  if (Serial.available())
  {
// is a byte has been sent, store it    
    byteRead = Serial.read();
// If previously recieved instruction to read in colour,
// set the value to that of the byte and reset state to LISTEN
    if(state == READ_RED)
    {
      rBright = byteRead;
      state = LISTEN;
    }
    else if(state == READ_GREEN)
    {
      gBright = byteRead;
      state = LISTEN;
    }
    else if(state == READ_BLUE)
    {
      bBright = byteRead;
      state = LISTEN;
    }
// Otherwaise, check if state change is needed:

// 'd' means toggle disco mode
    else
    {
      if(byteRead == 'd')
      {
        disco = !disco;
      }
// '0' means toggle output on/off
      if(byteRead == '0')
      {
        on = !on;
        byteRead= 'x';
      }
// '1'/'2'/'3' means prepare to read in red/green/blue
      if(byteRead == '1')
      {
        state = READ_RED;
      }
      if(byteRead == '2') // Toggle On/Off
      {
        state = READ_GREEN;
      }
      if(byteRead == '3') // Toggle On/Off
      {
        state = READ_BLUE;
      }      
    }        
  }
// if the LED is on  
  if(on)
  {
// loop disco mode variables    
    iR+=3;
    iG+=5;
    iB+=7;    
    if(iR>255) iR=0;
    if(iG>255) iG=0;
    if(iB>255) iB=0;
// and use these variables if in disco mode    
    if(disco)
    {
      analogWrite(RLED,iR);
      analogWrite(GLED,iG);
      analogWrite(BLED,iB);
      delay(30);
    }
// otherwaise use the variables set by the user
    else
    {
      analogWrite(RLED,rBright);
      analogWrite(GLED,gBright);
      analogWrite(BLED,bBright);
    }
  }
// if off, set all outputs low  
  else
  {
    digitalWrite(RLED,LOW);
    digitalWrite(GLED,LOW);
    digitalWrite(BLED,LOW);
  }
}

The circuit is simplicity itself. Three separate PWM outputs are sent to the led which is grounded through a 100 Ohm resistor. The green and blue chanels get an extra 220 Ohms to set the relative brightness of each LED about equal. A sketch is included below:



Finally, here's a  very crappily shot video of the circuit in action! To reduce the brightness and display the colour more uniformly, I used a high-tech solution known as a Perforated Fibrous Diffusive Membrane. Paper towel to the layperson.



http://www.youtube.com/watch?v=MZN09ancg_U

No comments:

Post a Comment