#! /usr/bin/env python """pyduino - A python library to interface with the firmata arduino firmware. Copyright (C) 2007 Joe Turner This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ __version__ = "0.11_dev" import time import serial # Message command bytes - straight outta Pd_firmware.pde DIGITAL_MESSAGE = 0x90 # send data for a digital pin ANALOG_MESSAGE = 0xE0 # send data for an analog pin (or PWM) # PULSE_MESSAGE = 0xA0 # proposed pulseIn/Out message (SysEx) # SHIFTOUT_MESSAGE = 0xB0 # proposed shiftOut message (SysEx) REPORT_ANALOG_PIN = 0xC0 # enable analog input by pin # REPORT_DIGITAL_PORTS = 0xD0 # enable digital input by port pair START_SYSEX = 0xF0 # start a MIDI SysEx message SET_DIGITAL_PIN_MODE = 0xF4 # set a digital pin to INPUT or OUTPUT END_SYSEX = 0xF7 # end a MIDI SysEx message REPORT_VERSION = 0xF9 # report firmware version SYSTEM_RESET = 0xFF # reset from MIDI # Pin modes DIGITAL_INPUT = 0 DIGITAL_OUTPUT = 1 DIGITAL_PWM = 2 PWM_PINS = (9, 10, 11) class Arduino: """Base class for the arduino board""" def __init__(self, port): self.sp = serial.Serial(port, 57600, timeout=0.02) # Allow 2 secs for Diecimila auto-reset to happen time.sleep(2) self.digital = [] for i in range(14): self.digital.append(Digital(self.sp, i)) self.analog = [] for i in range(6): self.analog.append(Analog(self.sp, i)) #Obtain firmata version self.sp.write(chr(REPORT_VERSION)) self.iterate() def __str__(self): return "Arduino: %s"% self.sp.port def iterate(self): """Read and handle a command byte from Arduino's serial port""" data = self.sp.read() if data != "": self._process_input(ord(data)) def _process_input(self, data): """Process a command byte and any additional information bytes""" if data < 0xF0: #Multibyte message = data & 0xF0 pin = data & 0x0F if message == DIGITAL_MESSAGE: #Digital in lsb = "" msb = "" while lsb == "": lsb = self.sp.read() while msb == "": msb = self.sp.read() lsb = ord(lsb) msb = ord(msb) self._set_digital_mask(lsb + (msb << 7)) elif message == ANALOG_MESSAGE: #Analog in lsb = "" msb = "" while lsb == "": lsb = self.sp.read() while msb == "": msb = self.sp.read() lsb = ord(lsb) msb = ord(msb) self.analog[pin].value = msb << 7 | lsb elif data == REPORT_VERSION: major, minor = self.sp.read(2) self.firmata_version = (ord(major), ord(minor)) def _set_digital_mask(self, input_mask): """ Alter the digital mask to reflect changes in the digital inputs""" for pin in self.digital: value = (input_mask & 1 << pin.pin) > 0 if pin.mode == DIGITAL_INPUT and value != pin.read(): Digital.mask ^= 1 << pin.pin def get_firmata_version(self): """Return a (major, minor) version tuple for the firmata firmware""" return self.firmata_version def exit(self): """Exit the application cleanly""" self.sp.close() class Digital: """Digital pin on the arduino board""" mask = 0 def __init__(self, sp, pin): self.sp = sp self.pin = pin self.is_active = 0 self.value = 0 self.mode = DIGITAL_INPUT def __str__(self): return "Digital Port %i"% self.pin def set_active(self, active): """Set the pin to report values""" self.is_active = 1 pin = REPORT_DIGITAL_PORTS + self.pin self.sp.write(chr(pin) + chr(active)) def get_active(self): """Return whether the pin is reporting values""" return self.is_active def set_mode(self, mode): """Set the mode of operation for the pin Argument: mode, takes a value of: - DIGITAL_INPUT - DIGITAL_OUTPUT - DIGITAL_PWM """ if mode == DIGITAL_PWM and self.pin not in PWM_PINS: error_message = "Digital pin %i does not have PWM capabilities" \ % (self.pin) raise IOError, error_message if self.pin < 2: raise IOError, "Cannot set mode for Rx/Tx pins" self.mode = mode command = chr(SET_DIGITAL_PIN_MODE) + chr(self.pin) + chr(mode) self.sp.write(command) def get_mode(self): """Return the pin mode, values explained in set_mode()""" return self.mode def read(self): """Return the output value of the pin, values explained in write()""" if self.mode == DIGITAL_PWM: return self.value def read(self): """Return the output value of the pin, values explained in write()""" if self.mode == DIGITAL_PWM: return self.value else: return (self.__class__.mask & 1 << self.pin) > 0 def write(self, value): """Output a voltage from the pin Argument: value, takes a boolean if the pin is in output mode, or a value from 0 to 255 if the pin is in PWM mode """ if self.mode == DIGITAL_INPUT: error_message = "Digital pin %i is not an output"% self.pin raise IOError, error_message elif value != self.read(): if self.mode == DIGITAL_OUTPUT: #Shorter variable dammit! mask = self.__class__.mask mask ^= 1 << self.pin message = chr(DIGITAL_MESSAGE) + chr(mask % 128) \ + chr(mask >> 7) self.sp.write(message) #Set the attribute to the new mask self.__class__.mask = mask elif self.mode == DIGITAL_PWM: self.value = value pin = ANALOG_MESSAGE + self.pin self.sp.write(chr(pin) + chr(value % 128) + chr(value >> 7)) class Analog: """Analog pin on the arduino board""" def __init__(self, sp, pin): self.sp = sp self.pin = pin self.active = 0 self.value = -1 def __str__(self): return "Analog Input %i"% self.pin def set_active(self, active): """Set the pin to report values""" self.active = active pin = REPORT_ANALOG_PIN + self.pin self.sp.write(chr(pin) + chr(active)) def get_active(self): """Return whether the pin is reporting values""" return self.active def read(self): """Return the input in the range 0-1023""" return self.value