├── requirements.txt ├── MANIFEST.in ├── screenshot.png ├── .gitignore ├── examples ├── screenshot_realtime_scope.png ├── scan.py ├── README.md ├── print_analog_data.py ├── pwm.py ├── servo.py ├── realtime_1kHz_scope.py ├── realtime_scope.py ├── digital-in.py ├── realtime_two_channel_scope.py └── blink.py ├── setup.py ├── pyfirmata2 ├── boards.py ├── __init__.py ├── mockup.py ├── util.py └── pyfirmata2.py ├── LICENSE ├── README_py.rst ├── README.md └── tests.py /requirements.txt: -------------------------------------------------------------------------------- 1 | pyserial -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include requirements.txt 3 | include LICENSE 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/berndporr/pyFirmata2/HEAD/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | .coverage 4 | build 5 | dist 6 | MANIFEST 7 | docs/_build 8 | 9 | *.sublime-* 10 | -------------------------------------------------------------------------------- /examples/screenshot_realtime_scope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/berndporr/pyFirmata2/HEAD/examples/screenshot_realtime_scope.png -------------------------------------------------------------------------------- /examples/scan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import serial 4 | import serial.tools.list_ports 5 | 6 | print("Serial devices available:") 7 | l = serial.tools.list_ports.comports() 8 | for ll in l: 9 | print(ll.device, ll.description) 10 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ![alt tag](screenshot_realtime_scope.png) 4 | 5 | - Realtime oscilloscope. This script shows how to process data at a given sampling rate. 6 | - Printing data on the screen using an event handler 7 | - Digital in reads from a digital pin using an event handler 8 | - Flashing LED using a timer 9 | - PWM 10 | - Servo 11 | 12 | There is also a scanner for COM ports. 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from setuptools import setup 4 | 5 | with open('README_py.rst') as f: 6 | long_description = f.read() 7 | 8 | setup( 9 | name='pyFirmata2', 10 | version='2.5.1', 11 | description="Use your Arduino as a data acquisition card under Python", 12 | long_description=long_description, 13 | author='Bernd Porr', 14 | author_email='mail@berndporr.me.uk', 15 | packages=['pyfirmata2'], 16 | include_package_data=True, 17 | install_requires=['pyserial'], 18 | zip_safe=False, 19 | url='https://github.com/berndporr/pyFirmata2', 20 | classifiers=[ 21 | 'Intended Audience :: Developers', 22 | 'License :: OSI Approved :: MIT License', 23 | 'Operating System :: OS Independent', 24 | 'Programming Language :: Python :: 3', 25 | 'Topic :: Utilities', 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /pyfirmata2/boards.py: -------------------------------------------------------------------------------- 1 | BOARDS = { 2 | 'arduino': { 3 | 'digital': tuple(x for x in range(14)), 4 | 'analog': tuple(x for x in range(6)), 5 | 'pwm': (3, 5, 6, 9, 10, 11), 6 | 'use_ports': True, 7 | 'disabled': (0, 1) # Rx, Tx, Crystal 8 | }, 9 | 'arduino_mega': { 10 | 'digital': tuple(x for x in range(54)), 11 | 'analog': tuple(x for x in range(16)), 12 | 'pwm': tuple(x for x in range(2, 14)), 13 | 'use_ports': True, 14 | 'disabled': (0, 1) # Rx, Tx, Crystal 15 | }, 16 | 'arduino_due': { 17 | 'digital': tuple(x for x in range(54)), 18 | 'analog': tuple(x for x in range(12)), 19 | 'pwm': tuple(x for x in range(2, 14)), 20 | 'use_ports': True, 21 | 'disabled': (0, 1) # Rx, Tx, Crystal 22 | }, 23 | 'arduino_nano': { 24 | 'digital': tuple(x for x in range(14)), 25 | 'analog': tuple(x for x in range(8)), 26 | 'pwm': (3, 5, 6, 9, 10, 11), 27 | 'use_ports': True, 28 | 'disabled': (0, 1) # Rx, Tx, Crystal 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Tino de Bruijn 2 | Copyright (c) 2018-2020, Bernd Porr 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /examples/print_analog_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from pyfirmata2 import Arduino 4 | import time 5 | 6 | PORT = Arduino.AUTODETECT 7 | # PORT = '/dev/ttyACM0' 8 | 9 | # prints data on the screen at the sampling rate of 50Hz 10 | # can easily be changed to saving data to a file 11 | 12 | # It uses a callback operation so that timing is precise and 13 | # the main program can just go to sleep. 14 | # Copyright (c) 2018-2020, Bernd Porr 15 | # see LICENSE file. 16 | 17 | 18 | class AnalogPrinter: 19 | 20 | def __init__(self): 21 | # sampling rate: 10Hz 22 | self.samplingRate = 10 23 | self.timestamp = 0 24 | self.board = Arduino(PORT) 25 | 26 | def start(self): 27 | self.board.analog[0].register_callback(self.myPrintCallback) 28 | self.board.samplingOn(1000 / self.samplingRate) 29 | self.board.analog[0].enable_reporting() 30 | 31 | def myPrintCallback(self, data): 32 | print("%f,%f" % (self.timestamp, data)) 33 | self.timestamp += (1 / self.samplingRate) 34 | 35 | def stop(self): 36 | self.board.exit() 37 | 38 | print("Let's print data from Arduino's analogue pins for 10secs.") 39 | 40 | # Let's create an instance 41 | analogPrinter = AnalogPrinter() 42 | 43 | # and start DAQ 44 | analogPrinter.start() 45 | 46 | # let's acquire data for 10secs. We could do something else but we just sleep! 47 | time.sleep(10) 48 | 49 | # let's stop it 50 | analogPrinter.stop() 51 | 52 | print("finished") 53 | -------------------------------------------------------------------------------- /pyfirmata2/__init__.py: -------------------------------------------------------------------------------- 1 | from .boards import BOARDS 2 | from .pyfirmata2 import * # NOQA 3 | 4 | # shortcut classes 5 | 6 | class Arduino(Board): 7 | """ 8 | A board that will set itself up as a normal Arduino. 9 | """ 10 | def __init__(self, *args, **kwargs): 11 | args = list(args) 12 | args.append(BOARDS['arduino']) 13 | super(Arduino, self).__init__(*args, **kwargs) 14 | 15 | def __str__(self): 16 | return "Arduino {0.name} on {0.sp.port}".format(self) 17 | 18 | 19 | class ArduinoMega(Board): 20 | """ 21 | A board that will set itself up as an Arduino Mega. 22 | """ 23 | def __init__(self, *args, **kwargs): 24 | args = list(args) 25 | args.append(BOARDS['arduino_mega']) 26 | super(ArduinoMega, self).__init__(*args, **kwargs) 27 | 28 | def __str__(self): 29 | return "Arduino Mega {0.name} on {0.sp.port}".format(self) 30 | 31 | 32 | class ArduinoDue(Board): 33 | """ 34 | A board that will set itself up as an Arduino Due. 35 | """ 36 | def __init__(self, *args, **kwargs): 37 | args = list(args) 38 | args.append(BOARDS['arduino_due']) 39 | super(ArduinoDue, self).__init__(*args, **kwargs) 40 | 41 | def __str__(self): 42 | return "Arduino Due {0.name} on {0.sp.port}".format(self) 43 | 44 | 45 | class ArduinoNano(Board): 46 | """ 47 | A board that will set itself up as an Arduino Nano. 48 | """ 49 | def __init__(self, *args, **kwargs): 50 | args = list(args) 51 | args.append(BOARDS['arduino_nano']) 52 | super(ArduinoNano, self).__init__(*args, **kwargs) 53 | 54 | def __str__(self): 55 | return "Arduino Nano {0.name} on {0.sp.port}".format(self) 56 | -------------------------------------------------------------------------------- /README_py.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | pyFirmata2 3 | ========== 4 | 5 | PyFirmata2 turns your Arduino into a data acquisition card controlled by Python. 6 | 7 | Up to 1kHz precise sampling at the analogue ports for digital filtering. 8 | 9 | Just upload the default firmata sketch into your Arduino and you are all set. 10 | 11 | pyFirmata2 is an updated version of pyFirmata which *replaces loops 12 | with callbacks*. Instead of unreliable "sleep" commands in a loop the 13 | Python application registers callbacks which are then called every 14 | time after new data has arrived. This means for the analogue 15 | channels the callbacks are called at the specified sampling rate 16 | while the digital ports call the callback functions after 17 | a state change at the port (from 0 to 1 or 1 to 0). 18 | 19 | This API has been used in my Digital Signal Processing (DSP) class to 20 | practise realtime filtering of analogue sensor 21 | data. Examples can be viewed on the YouTube channel of the 22 | class: https://www.youtube.com/user/DSPcourse 23 | 24 | 25 | Installation 26 | ============ 27 | 28 | 29 | Upload firmata 30 | -------------- 31 | 32 | Install the Arduino IDE on your computer: https://www.arduino.cc/en/Main/Software 33 | 34 | Start the IDE and upload the standard firmata sketch into your Arduino with:: 35 | 36 | File -> Examples -> Firmata -> Standard Firmata 37 | 38 | 39 | 40 | Install pyfirmata2 41 | ------------------ 42 | 43 | The preferred way to install is with `pip` / `pip3`. Under Linux:: 44 | 45 | pip3 install pyfirmata2 [--user] [--upgrade] 46 | 47 | 48 | and under Windows/Mac type:: 49 | 50 | pip install pyfirmata2 [--user] [--upgrade] 51 | 52 | 53 | You can also install from source with:: 54 | 55 | git clone https://github.com/berndporr/pyFirmata2 56 | cd pyFirmata2 57 | 58 | Under Linux type:: 59 | 60 | python3 setup.py install 61 | 62 | Under Windows / Mac:: 63 | 64 | python setup.py install 65 | 66 | 67 | Usage 68 | ===== 69 | 70 | Please go to https://github.com/berndporr/pyFirmata2 for the 71 | documentation and in particular the example code. 72 | 73 | 74 | Example code 75 | ============ 76 | 77 | It's strongly recommended to check out the example code at 78 | https://github.com/berndporr/pyFirmata2/tree/master/examples 79 | to see how the callbacks work. 80 | 81 | 82 | Credits 83 | ======= 84 | 85 | The original pyFirmata was written by Tino de Bruijn. 86 | The realtime sampling / callback has been added by Bernd Porr. 87 | -------------------------------------------------------------------------------- /examples/pwm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Copyright (c) 2018-2021, Bernd Porr 4 | # 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # * Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # * Neither the name of the pyfirmata team nor the names of its contributors 16 | # may be used to endorse or promote products derived from this software 17 | # without specific prior written permission. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY 20 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY 23 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | import pyfirmata2 31 | 32 | # PWM demo on port 5. The default PWM frequency is 1kHz. 33 | 34 | # Adjust that the port match your system, see samples below: 35 | # On Linux: /dev/tty.usbserial-A6008rIF, /dev/ttyACM0, 36 | # On Windows: \\.\COM1, \\.\COM2 37 | # PORT = '/dev/ttyACM0' 38 | PORT = pyfirmata2.Arduino.AUTODETECT 39 | 40 | # Creates a new board 41 | board = pyfirmata2.Arduino(PORT) 42 | print("Setting up the connection to the board ...") 43 | 44 | # Setup the digital pin for PWM 45 | pwm_5 = board.get_pin('d:5:p') 46 | 47 | v = float(input("PWM duty cycle from 0 to 100: ")) / 100.0 48 | 49 | # Set the duty cycle (0..1) 50 | pwm_5.write(v) 51 | 52 | # just idle here 53 | input("Press enter to exit") 54 | 55 | # pwm off 56 | pwm_5.write(0) 57 | 58 | # Close the serial connection to the Arduino 59 | board.exit() 60 | -------------------------------------------------------------------------------- /examples/servo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Copyright (c) 2018-2021, Bernd Porr 4 | # 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # * Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # * Neither the name of the pyfirmata team nor the names of its contributors 16 | # may be used to endorse or promote products derived from this software 17 | # without specific prior written permission. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY 20 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY 23 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | import pyfirmata2 31 | 32 | # Servo demo on port 5. The default frequency is 50Hz approx. 33 | # Lowest dutycycle (1ms) at 0 degrees and highest (2ms) at 180 degrees. 34 | 35 | # Adjust that the port match your system, see samples below: 36 | # On Linux: /dev/tty.usbserial-A6008rIF, /dev/ttyACM0, 37 | # On Windows: \\.\COM1, \\.\COM2 38 | # PORT = '/dev/ttyACM0' 39 | PORT = pyfirmata2.Arduino.AUTODETECT 40 | 41 | # Creates a new board 42 | board = pyfirmata2.Arduino(PORT) 43 | print("Setting up the connection to the board ...") 44 | 45 | # Setup the digital pin as servo 46 | servo_5 = board.get_pin('d:5:s') 47 | 48 | v = float(input("Servo angle from 0 to 180 degrees: ")) 49 | 50 | # Set the duty cycle 51 | servo_5.write(v) 52 | 53 | # just idle here 54 | input("Press enter to exit") 55 | 56 | # Close the serial connection to the Arduino 57 | board.exit() 58 | -------------------------------------------------------------------------------- /examples/realtime_1kHz_scope.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """ 3 | Plots channel zero at 1kHz. Requires pyqtgraph. 4 | 5 | Copyright (c) 2018-2021, Bernd Porr 6 | see LICENSE file. 7 | 8 | """ 9 | 10 | import sys 11 | 12 | import pyqtgraph as pg 13 | from pyqtgraph.Qt import QtCore, QtWidgets 14 | 15 | import numpy as np 16 | 17 | from pyfirmata2 import Arduino 18 | 19 | PORT = Arduino.AUTODETECT 20 | # sampling rate: 1kHz 21 | samplingRate = 1000 22 | 23 | class QtPanningPlot: 24 | 25 | def __init__(self,layout,title): 26 | self.pw = pg.PlotWidget() 27 | layout.addWidget(self.pw) 28 | self.pw.setYRange(-1,1) 29 | self.pw.setXRange(0,500/samplingRate) 30 | self.plt = self.pw.plot() 31 | self.data = [] 32 | # any additional initalisation code goes here (filters etc) 33 | self.timer = QtCore.QTimer() 34 | self.timer.timeout.connect(self.update) 35 | self.timer.start(100) 36 | 37 | def update(self): 38 | self.data=self.data[-500:] 39 | if self.data: 40 | self.plt.setData(x=np.linspace(0,len(self.data)/samplingRate,len(self.data)),y=self.data) 41 | 42 | def addData(self,d): 43 | self.data.append(d) 44 | 45 | app = pg.mkQApp() 46 | mw = QtWidgets.QMainWindow() 47 | mw.setWindowTitle('1kHz PlotWidget') 48 | mw.resize(800,800) 49 | cw = QtWidgets.QWidget() 50 | mw.setCentralWidget(cw) 51 | 52 | # Vertical arrangement 53 | l = QtWidgets.QVBoxLayout() 54 | cw.setLayout(l) 55 | 56 | # Let's create a plot window 57 | qtPanningPlot1 = QtPanningPlot(l,"Arduino 1st channel") 58 | label = QtWidgets.QLabel("This label show how to add another Widget to the layout.") 59 | l.addWidget(label) 60 | 61 | # called for every new sample at channel 0 which has arrived from the Arduino 62 | # "data" contains the new sample 63 | def callBack(data): 64 | # filter your channel 0 samples here: 65 | # data = self.filter_of_channel0.dofilter(data) 66 | # send the sample to the plotwindow 67 | qtPanningPlot1.addData(data) 68 | 69 | # Get the Ardunio board. 70 | board = Arduino(PORT,debug=True) 71 | 72 | # Set the sampling rate in the Arduino 73 | board.samplingOn(1000 / samplingRate) 74 | 75 | # Register the callback which adds the data to the animated plot 76 | # The function "callback" (see above) is called when data has 77 | # arrived on channel 0. 78 | board.analog[0].register_callback(callBack) 79 | 80 | # Enable the callback 81 | board.analog[0].enable_reporting() 82 | 83 | # Show the window 84 | mw.show() 85 | 86 | # showing all the windows 87 | pg.exec() 88 | 89 | # needs to be called to close the serial port 90 | board.exit() 91 | 92 | print("Finished") 93 | -------------------------------------------------------------------------------- /examples/realtime_scope.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from pyfirmata2 import Arduino 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | import matplotlib.animation as animation 7 | 8 | # Realtime oscilloscope at a sampling rate of 100Hz 9 | # It displays analog channel 0. 10 | # You can plot multiple chnannels just by instantiating 11 | # more RealtimePlotWindow instances and registering 12 | # callbacks from the other channels. 13 | # Copyright (c) 2018-2020, Bernd Porr 14 | # see LICENSE file. 15 | 16 | PORT = Arduino.AUTODETECT 17 | # PORT = '/dev/ttyUSB0' 18 | 19 | # Creates a scrolling data display 20 | class RealtimePlotWindow: 21 | 22 | def __init__(self): 23 | # create a plot window 24 | self.fig, self.ax = plt.subplots() 25 | # that's our plotbuffer 26 | self.plotbuffer = np.zeros(500) 27 | # create an empty line 28 | self.line, = self.ax.plot(self.plotbuffer) 29 | # axis 30 | self.ax.set_ylim(0, 1.5) 31 | # That's our ringbuffer which accumluates the samples 32 | # It's emptied every time when the plot window below 33 | # does a repaint 34 | self.ringbuffer = [] 35 | # add any initialisation code here (filters etc) 36 | # start the animation 37 | self.ani = animation.FuncAnimation(self.fig, self.update, interval=100) 38 | 39 | # updates the plot 40 | def update(self, data): 41 | # add new data to the buffer 42 | self.plotbuffer = np.append(self.plotbuffer, self.ringbuffer) 43 | # only keep the 500 newest ones and discard the old ones 44 | self.plotbuffer = self.plotbuffer[-500:] 45 | self.ringbuffer = [] 46 | # set the new 500 points of channel 9 47 | self.line.set_ydata(self.plotbuffer) 48 | return self.line, 49 | 50 | # appends data to the ringbuffer 51 | def addData(self, v): 52 | self.ringbuffer.append(v) 53 | 54 | 55 | # Create an instance of an animated scrolling window 56 | # To plot more channels just create more instances and add callback handlers below 57 | realtimePlotWindow = RealtimePlotWindow() 58 | 59 | # sampling rate: 100Hz 60 | samplingRate = 100 61 | 62 | # called for every new sample which has arrived from the Arduino 63 | def callBack(data): 64 | # send the sample to the plotwindow 65 | # add any filtering here: 66 | # data = self.myfilter.dofilter(data) 67 | realtimePlotWindow.addData(data) 68 | 69 | # Get the Ardunio board. 70 | board = Arduino(PORT) 71 | 72 | # Set the sampling rate in the Arduino 73 | board.samplingOn(1000 / samplingRate) 74 | 75 | # Register the callback which adds the data to the animated plot 76 | board.analog[0].register_callback(callBack) 77 | 78 | # Enable the callback 79 | board.analog[0].enable_reporting() 80 | 81 | # show the plot and start the animation 82 | plt.show() 83 | 84 | # needs to be called to close the serial port 85 | board.exit() 86 | 87 | print("finished") 88 | -------------------------------------------------------------------------------- /examples/digital-in.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Copyright (c) 2012, Fabian Affolter 4 | # Copyright (c) 2018-2021, Bernd Porr 5 | # 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright notice, this 12 | # list of conditions and the following disclaimer. 13 | # * Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # * Neither the name of the pyfirmata team nor the names of its contributors 17 | # may be used to endorse or promote products derived from this software 18 | # without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY 21 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | # DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY 24 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 27 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | import pyfirmata2 32 | 33 | # The program monitors the digital pin 6. 34 | # Connect a switch between pin 6 and GND. 35 | # Whenever there is a change of the state 36 | # at pin 6 the callback function "pinCallback" 37 | # is called. 38 | 39 | # Adjust that the port match your system, see samples below: 40 | # On Linux: /dev/tty.usbserial-A6008rIF, /dev/ttyACM0, 41 | # On Windows: \\.\COM1, \\.\COM2 42 | # PORT = '/dev/ttyACM0' 43 | PORT = pyfirmata2.Arduino.AUTODETECT 44 | 45 | 46 | # Callback function which is called whenever there is a 47 | # change at the digital port 6. 48 | def pinCallback(value): 49 | if value: 50 | print("Button released") 51 | else: 52 | print("Button pressed") 53 | 54 | 55 | # Creates a new board 56 | board = pyfirmata2.Arduino(PORT) 57 | print("Setting up the connection to the board ...") 58 | 59 | # default sampling interval of 19ms 60 | board.samplingOn() 61 | 62 | # Setup the digital pin with pullup resistor: "u" 63 | digital_0 = board.get_pin('d:6:u') 64 | 65 | # points to the callback 66 | digital_0.register_callback(pinCallback) 67 | 68 | # Switches the callback on 69 | digital_0.enable_reporting() 70 | 71 | print("To stop the program press return.") 72 | 73 | # Do nothing here. Just preventing the program from reaching the 74 | # exit function. 75 | input() 76 | 77 | # Close the serial connection to the Arduino 78 | board.exit() 79 | -------------------------------------------------------------------------------- /examples/realtime_two_channel_scope.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """ 3 | Plots channels zero and one at 100Hz. Requires pyqtgraph. 4 | 5 | Copyright (c) 2018-2022, Bernd Porr 6 | see LICENSE file. 7 | 8 | """ 9 | 10 | import sys 11 | 12 | import pyqtgraph as pg 13 | from pyqtgraph.Qt import QtCore, QtWidgets 14 | 15 | import numpy as np 16 | 17 | from pyfirmata2 import Arduino 18 | 19 | PORT = Arduino.AUTODETECT 20 | # sampling rate: 100Hz 21 | samplingRate = 100 22 | 23 | class QtPanningPlot: 24 | 25 | def __init__(self,title): 26 | self.pw = pg.PlotWidget() 27 | self.pw.setYRange(-1,1) 28 | self.pw.setXRange(0,500/samplingRate) 29 | self.plt = self.pw.plot() 30 | self.data = [] 31 | # any additional initalisation code goes here (filters etc) 32 | self.timer = QtCore.QTimer() 33 | self.timer.timeout.connect(self.update) 34 | self.timer.start(100) 35 | 36 | def getWidget(self): 37 | return self.pw 38 | 39 | def update(self): 40 | self.data=self.data[-500:] 41 | if self.data: 42 | self.plt.setData(x=np.linspace(0,len(self.data)/samplingRate,len(self.data)),y=self.data) 43 | 44 | def addData(self,d): 45 | self.data.append(d) 46 | 47 | app = pg.mkQApp() 48 | mw = QtWidgets.QMainWindow() 49 | mw.setWindowTitle('100Hz dual PlotWidget') 50 | mw.resize(800,800) 51 | cw = QtWidgets.QWidget() 52 | mw.setCentralWidget(cw) 53 | 54 | # Let's arrange the two plots horizontally 55 | layout = QtWidgets.QHBoxLayout() 56 | cw.setLayout(layout) 57 | 58 | # Let's create two instances of plot windows 59 | qtPanningPlot1 = QtPanningPlot("Arduino 1st channel") 60 | layout.addWidget(qtPanningPlot1.getWidget()) 61 | 62 | qtPanningPlot2 = QtPanningPlot("Arduino 2nd channel") 63 | layout.addWidget(qtPanningPlot2.getWidget()) 64 | 65 | # called for every new sample at channel 0 which has arrived from the Arduino 66 | # "data" contains the new sample 67 | def callBack1(data): 68 | # filter your channel 0 samples here: 69 | # data = self.filter_of_channel0.dofilter(data) 70 | # send the sample to the plotwindow 71 | qtPanningPlot1.addData(data) 72 | 73 | # called for every new sample at channel 1 which has arrived from the Arduino 74 | # "data" contains the new sample 75 | def callBack2(data): 76 | # filter your channel 1 samples here: 77 | # data = self.filter_of_channel1.dofilter(data) 78 | # send the sample to the plotwindow 79 | qtPanningPlot2.addData(data) 80 | 81 | # Get the Ardunio board. 82 | board = Arduino(PORT) 83 | 84 | # Set the sampling rate in the Arduino 85 | board.samplingOn(1000 / samplingRate) 86 | 87 | # Register the callback which adds the data to the animated plot 88 | board.analog[0].register_callback(callBack1) 89 | board.analog[1].register_callback(callBack2) 90 | 91 | # Enable the callback 92 | board.analog[0].enable_reporting() 93 | board.analog[1].enable_reporting() 94 | 95 | # showing the plots 96 | mw.show() 97 | 98 | # Starting the QT GUI 99 | # This is a blocking call and only returns when the user closes the window. 100 | pg.exec() 101 | 102 | # needs to be called to close the serial port 103 | board.exit() 104 | 105 | print("Finished") 106 | -------------------------------------------------------------------------------- /examples/blink.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Copyright (c) 2012, Fabian Affolter 4 | # Copyright (c) 2019-2021, Bernd Porr 5 | # 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright notice, this 12 | # list of conditions and the following disclaimer. 13 | # * Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # * Neither the name of the pyfirmata team nor the names of its contributors 17 | # may be used to endorse or promote products derived from this software 18 | # without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY 21 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | # DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY 24 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 27 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | # 31 | # 32 | # This program toggles the digital port 13 on/off every second. 33 | # Port 13 has an LED connected so you'll see def a flashing light! 34 | # Coding is done with a timer callback to avoid evil loops / delays. 35 | 36 | import pyfirmata2 37 | from threading import Timer 38 | 39 | class Blink(): 40 | def __init__(self, board, seconds): 41 | # pin 13 which is connected to the internal LED 42 | self.digital_0 = board.get_pin('d:13:o') 43 | 44 | # flag that we want the timer to restart itself in the callback 45 | self.timer = None 46 | 47 | # delay 48 | self.DELAY = seconds 49 | 50 | # callback function which toggles the digital port and 51 | # restarts the timer 52 | def blinkCallback(self): 53 | # call itself again so that it runs periodically 54 | self.timer = Timer(self.DELAY,self.blinkCallback) 55 | 56 | # start the timer 57 | self.timer.start() 58 | 59 | # now let's toggle the LED 60 | v = self.digital_0.read() 61 | v = not v 62 | if v: 63 | print("On") 64 | else: 65 | print("Off") 66 | self.digital_0.write(v) 67 | 68 | # starts the blinking 69 | def start(self): 70 | # Kickstarting the perpetual timer by calling the 71 | # callback function once 72 | self.blinkCallback() 73 | 74 | # stops the blinking 75 | def stop(self): 76 | # Cancel the timer 77 | self.timer.cancel() 78 | 79 | # main program 80 | 81 | # Adjust that the port match your system, see samples below: 82 | # On Linux: /dev/ttyACM0, 83 | # On Windows: COM1, COM2, ... 84 | PORT = pyfirmata2.Arduino.AUTODETECT 85 | 86 | # Creates a new board 87 | board = pyfirmata2.Arduino(PORT) 88 | 89 | t = Blink(board,1) 90 | t.start() 91 | 92 | print("To stop the program press return.") 93 | # Just blocking here to do nothing. 94 | input() 95 | 96 | t.stop() 97 | 98 | # close the serial connection 99 | board.exit() 100 | -------------------------------------------------------------------------------- /pyfirmata2/mockup.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | import pyfirmata2 3 | 4 | 5 | class MockupSerial(deque): 6 | """ 7 | A Mockup object for python's Serial. Functions as a fifo-stack. Push to 8 | it with ``write``, read from it with ``read``. 9 | """ 10 | 11 | def __init__(self, port, baudrate, timeout=0.02): 12 | self.port = port or 'somewhere' 13 | 14 | def read(self, count=1): 15 | if count > 1: 16 | val = [] 17 | for i in range(count): 18 | try: 19 | val.append(self.popleft()) 20 | except IndexError: 21 | break 22 | else: 23 | try: 24 | val = self.popleft() 25 | except IndexError: 26 | val = bytearray() 27 | 28 | val = [val] if not hasattr(val, '__iter__') else val 29 | return bytearray(val) 30 | 31 | def write(self, value): 32 | """ 33 | Appends bytes flat to the deque. So iterables will be unpacked. 34 | """ 35 | if hasattr(value, '__iter__'): 36 | bytearray(value) 37 | self.extend(value) 38 | else: 39 | bytearray([value]) 40 | self.append(value) 41 | 42 | def close(self): 43 | self.clear() 44 | 45 | def inWaiting(self): 46 | return len(self) 47 | 48 | 49 | class MockupBoard(pyfirmata2.Board): 50 | 51 | def __init__(self, port, layout, values_dict={}): 52 | self.sp = MockupSerial(port, 57600) 53 | self.setup_layout(layout) 54 | self.values_dict = values_dict 55 | self.id = 1 56 | self.samplerThread = Iterator(self) 57 | 58 | def reset_taken(self): 59 | for key in self.taken['analog']: 60 | self.taken['analog'][key] = False 61 | for key in self.taken['digital']: 62 | self.taken['digital'][key] = False 63 | 64 | def update_values_dict(self): 65 | for port in self.digital_ports: 66 | port.values_dict = self.values_dict 67 | port.update_values_dict() 68 | for pin in self.analog: 69 | pin.values_dict = self.values_dict 70 | 71 | 72 | class MockupPort(pyfirmata2.Port): 73 | def __init__(self, board, port_number): 74 | self.board = board 75 | self.port_number = port_number 76 | self.reporting = False 77 | 78 | self.pins = [] 79 | for i in range(8): 80 | pin_nr = i + self.port_number * 8 81 | self.pins.append(MockupPin(self.board, pin_nr, type=pyfirmata2.DIGITAL, port=self)) 82 | 83 | def update_values_dict(self): 84 | for pin in self.pins: 85 | pin.values_dict = self.values_dict 86 | 87 | 88 | class MockupPin(pyfirmata2.Pin): 89 | def __init__(self, *args, **kwargs): 90 | self.values_dict = kwargs.get('values_dict', {}) 91 | try: 92 | del kwargs['values_dict'] 93 | except KeyError: 94 | pass 95 | super(MockupPin, self).__init__(*args, **kwargs) 96 | 97 | def read(self): 98 | if self.value is None: 99 | try: 100 | type = self.port and 'd' or 'a' 101 | return self.values_dict[type][self.pin_number] 102 | except KeyError: 103 | return None 104 | else: 105 | return self.value 106 | 107 | def get_in_output(self): 108 | if not self.port and not self.mode: # analog input 109 | return 'i' 110 | else: 111 | return 'o' 112 | 113 | def set_active(self, active): 114 | self.is_active = active 115 | 116 | def get_active(self): 117 | return self.is_active 118 | 119 | def write(self, value): 120 | if self.mode == pyfirmata2.UNAVAILABLE: 121 | raise IOError("Cannot read from pin {0}".format(self.pin_number)) 122 | if self.mode == pyfirmata2.INPUT: 123 | raise IOError("{0} pin {1} is not an output" 124 | .format(self.port and "Digital" or "Analog", self.get_pin_number())) 125 | if not self.port: 126 | raise AttributeError("AnalogPin instance has no attribute 'write'") 127 | # if value != self.read(): 128 | self.value = value 129 | 130 | 131 | class Iterator(object): 132 | def __init__(self, *args, **kwargs): 133 | self.running = False 134 | 135 | def start(self): 136 | pass 137 | 138 | def stop(self): 139 | pass 140 | 141 | 142 | if __name__ == '__main__': 143 | import doctest 144 | doctest.testmod() 145 | # TODO make these unittests as this doesn't work due to relative imports 146 | -------------------------------------------------------------------------------- /pyfirmata2/util.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, unicode_literals 2 | 3 | import os 4 | import sys 5 | import threading 6 | import time 7 | 8 | import serial 9 | 10 | from .boards import BOARDS 11 | 12 | 13 | def get_the_board( 14 | layout=BOARDS["arduino"], base_dir="/dev/", identifier="tty.usbserial" 15 | ): 16 | """ 17 | Helper function to get the one and only board connected to the computer 18 | running this. It assumes a normal arduino layout, but this can be 19 | overriden by passing a different layout dict as the ``layout`` parameter. 20 | ``base_dir`` and ``identifier`` are overridable as well. It will raise an 21 | IOError if it can't find a board, on a serial, or if it finds more than 22 | one. 23 | """ 24 | from .pyfirmata2 import Board # prevent a circular import 25 | boards = [] 26 | for device in os.listdir(base_dir): 27 | if device.startswith(identifier): 28 | try: 29 | board = Board(os.path.join(base_dir, device), layout) 30 | except serial.SerialException: 31 | pass 32 | else: 33 | boards.append(board) 34 | if len(boards) == 0: 35 | raise IOError("No boards found in {0} with identifier {1}".format(base_dir, identifier)) 36 | elif len(boards) > 1: 37 | raise IOError("More than one board found!") 38 | return boards[0] 39 | 40 | 41 | class Iterator(threading.Thread): 42 | def __init__(self, board): 43 | super(Iterator, self).__init__() 44 | self.board = board 45 | self.daemon = True 46 | self.running = False 47 | 48 | def run(self): 49 | self.running = True 50 | while self.running: 51 | try: 52 | while self.board.bytes_available(): 53 | self.board.iterate() 54 | time.sleep(0.001) 55 | except (AttributeError, serial.SerialException, OSError): 56 | # this way we can kill the thread by setting the board object 57 | # to None, or when the serial port is closed by board.exit() 58 | break 59 | except Exception as e: 60 | # catch 'error: Bad file descriptor' 61 | # iterate may be called while the serial port is being closed, 62 | # causing an "error: (9, 'Bad file descriptor')" 63 | if getattr(e, 'errno', None) == 9: 64 | break 65 | try: 66 | if e[0] == 9: 67 | break 68 | except (TypeError, IndexError): 69 | pass 70 | raise 71 | except (KeyboardInterrupt): 72 | sys.exit() 73 | 74 | def stop(self): 75 | self.running = False 76 | 77 | 78 | def to_two_bytes(integer): 79 | """ 80 | Breaks an integer into two 7 bit bytes. 81 | """ 82 | if integer > 32767: 83 | raise ValueError("Can't handle values bigger than 32767 (max for 2 bits)") 84 | return bytearray([integer % 128, integer >> 7]) 85 | 86 | 87 | def from_two_bytes(bytes): 88 | """ 89 | Return an integer from two 7 bit bytes. 90 | """ 91 | lsb, msb = bytes 92 | try: 93 | # Usually bytes have been converted to integers with ord already 94 | return msb << 7 | lsb 95 | except TypeError: 96 | # But add this for easy testing 97 | # One of them can be a string, or both 98 | try: 99 | lsb = ord(lsb) 100 | except TypeError: 101 | pass 102 | try: 103 | msb = ord(msb) 104 | except TypeError: 105 | pass 106 | return msb << 7 | lsb 107 | 108 | 109 | def two_byte_iter_to_str(bytes): 110 | """ 111 | Return a string made from a list of two byte chars. 112 | """ 113 | bytes = list(bytes) 114 | chars = bytearray() 115 | while bytes: 116 | lsb = bytes.pop(0) 117 | try: 118 | msb = bytes.pop(0) 119 | except IndexError: 120 | msb = 0x00 121 | chars.append(from_two_bytes([lsb, msb])) 122 | return chars.decode() 123 | 124 | 125 | def str_to_two_byte_iter(string): 126 | """ 127 | Return a iter consisting of two byte chars from a string. 128 | """ 129 | bstring = string.encode() 130 | bytes = bytearray() 131 | for char in bstring: 132 | bytes.append(char) 133 | bytes.append(0) 134 | return bytes 135 | 136 | 137 | def break_to_bytes(value): 138 | """ 139 | Breaks a value into values of less than 255 that form value when multiplied. 140 | (Or almost do so with primes) 141 | Returns a tuple 142 | """ 143 | if value < 256: 144 | return (value,) 145 | c = 256 146 | least = (0, 255) 147 | for i in range(254): 148 | c -= 1 149 | rest = value % c 150 | if rest == 0 and value / c < 256: 151 | return (c, int(value / c)) 152 | elif rest == 0 and value / c > 255: 153 | parts = list(break_to_bytes(value / c)) 154 | parts.insert(0, c) 155 | return tuple(parts) 156 | else: 157 | if rest < least[1]: 158 | least = (c, rest) 159 | return (c, int(value / c)) 160 | 161 | 162 | def pin_list_to_board_dict(pinlist): 163 | """ 164 | Capability Response codes: 165 | INPUT: 0, 1 166 | OUTPUT: 1, 1 167 | ANALOG: 2, 10 168 | PWM: 3, 8 169 | SERV0: 4, 14 170 | I2C: 6, 1 171 | """ 172 | 173 | board_dict = { 174 | "digital": [], 175 | "analog": [], 176 | "pwm": [], 177 | "servo": [], 178 | "disabled": [], 179 | } 180 | for i, pin in enumerate(pinlist): 181 | pin.pop() # removes the 0x79 on end 182 | if not pin: 183 | board_dict["disabled"] += [i] 184 | board_dict["digital"] += [i] 185 | continue 186 | 187 | for j, _ in enumerate(pin): 188 | # Iterate over evens 189 | if j % 2 == 0: 190 | # This is safe. try: range(10)[5:50] 191 | if pin[j:j + 4] == [0, 1, 1, 1]: 192 | board_dict["digital"] += [i] 193 | 194 | if pin[j:j + 2] == [2, 10]: 195 | board_dict["analog"] += [i] 196 | 197 | if pin[j:j + 2] == [3, 8]: 198 | board_dict["pwm"] += [i] 199 | 200 | if pin[j:j + 2] == [4, 14]: 201 | board_dict["servo"] += [i] 202 | 203 | # Desable I2C 204 | if pin[j:j + 2] == [6, 1]: 205 | pass 206 | 207 | # We have to deal with analog pins: 208 | # - (14, 15, 16, 17, 18, 19) 209 | # + (0, 1, 2, 3, 4, 5) 210 | diff = set(board_dict["digital"]) - set(board_dict["analog"]) 211 | board_dict["analog"] = [n for n, _ in enumerate(board_dict["analog"])] 212 | 213 | # Digital pin problems: 214 | # - (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) 215 | # + (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13) 216 | 217 | board_dict["digital"] = [n for n, _ in enumerate(diff)] 218 | # Based on lib Arduino 0017 219 | board_dict["servo"] = board_dict["digital"] 220 | 221 | # Turn lists into tuples 222 | # Using dict for Python 2.6 compatibility 223 | board_dict = dict([ 224 | (key, tuple(value)) 225 | for key, value 226 | in board_dict.items() 227 | ]) 228 | 229 | return board_dict 230 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyFirmata2 2 | 3 | ![alt tag](screenshot.png) 4 | 5 | PyFirmata2 turns your Arduino into an analogue to digital data acquistion 6 | card controlled by Python. 7 | Just upload the default firmata sketch into your Arduino and you are all set. 8 | 9 | ## Inputs 10 | - Analogue, measuring volt. Up to 1kHz precise sampling. 11 | - Digital, reacting to digital state changes. 12 | 13 | ## Outputs 14 | - Digital out 15 | - PWM 16 | - Servo control 17 | 18 | No loops and "sleep" commands: pyFirmata2 is an updated version of 19 | pyFirmata which *replaces loops with callbacks*. Instead of unreliable 20 | and disruptive "sleep" commands in a loop the Python application 21 | registers callbacks with pyfirmata2 which are then called every time 22 | new data has arrived. This means for the analogue channels the 23 | callbacks are called at the specified sampling rate while the digital 24 | ports call the callback functions after a state change at the port 25 | (from 0 to 1 or 1 to 0). 26 | 27 | This API has been used in the Digital Signal Processing (DSP) class to 28 | practise realtime filtering of analogue sensor 29 | data. Examples can be viewed on the YouTube channel of the 30 | class: https://www.youtube.com/user/DSPcourse 31 | 32 | 33 | ## Installation 34 | 35 | 36 | 37 | ### Upload firmata 38 | 39 | 40 | Install the Arduino IDE on your computer: https://www.arduino.cc/en/Main/Software 41 | 42 | - Start the Arduino IDE 43 | - Select the serial port under "Tools" 44 | - Select your Arduino board under "Tools" 45 | - Upload the standard firmata sketch to your Arduino with: 46 | ``` 47 | File -> Examples -> Firmata -> Standard Firmata 48 | ``` 49 | 50 | 51 | ### Install pyfirmata2 52 | 53 | 54 | The preferred way to install is with `pip` / `pip3`. Under Linux: 55 | ``` 56 | pip3 install pyfirmata2 [--user] [--upgrade] 57 | ``` 58 | 59 | and under Windows/Mac type: 60 | ``` 61 | pip install pyfirmata2 [--user] [--upgrade] 62 | ``` 63 | 64 | You can also install from source with: 65 | ``` 66 | git clone https://github.com/berndporr/pyFirmata2 67 | cd pyFirmata2 68 | ``` 69 | 70 | Under Linux type: 71 | ``` 72 | python3 setup.py install 73 | ``` 74 | 75 | Under Windows / Mac: 76 | ``` 77 | python setup.py install 78 | ``` 79 | 80 | ## Usage 81 | 82 | 83 | ### Initialisation 84 | 85 | Create an instance of the `Arduino` class: 86 | ``` 87 | PORT = pyfirmata2.Arduino.AUTODETECT 88 | board = pyfirmata2.Arduino(PORT) 89 | ``` 90 | which automatically detects the serial port of the Arduino. 91 | 92 | If this fails you can also specify the serial port manually, for example: 93 | ``` 94 | board = pyfirmata2.Arduino('COM4') 95 | ``` 96 | Under Linux this is usually `/dev/ttyACM0`. Under Windows this is a 97 | COM port, for example `COM4`. On a MAC it's `/dev/ttys000`, `/dev/cu.usbmodem14101` or 98 | check for the latest addition: `ls -l -t /dev/*`. 99 | 100 | 101 | ### Starting sampling at a given sampling interval 102 | 103 | In order to sample analogue data you need to specify a sampling 104 | interval in ms which then applies to all channels. The smallest 105 | interval is 1ms: 106 | ``` 107 | board.samplingOn(samplinginterval in ms) 108 | ``` 109 | Note that the sampling interval is an *integer* number. 110 | Calling `samplingOn()` without its argument sets the sampling interval 111 | to 19ms. 112 | 113 | 114 | ### Enabling and reading from analogue or digital input pins 115 | 116 | To receive data register a callback 117 | handler and then enable it: 118 | ``` 119 | board.analog[0].register_callback(myCallback) 120 | board.analog[0].enable_reporting() 121 | ``` 122 | where `myCallback(data)` is then called every time after data has been received 123 | and is timed by the arduino itself. For analogue inputs that's at 124 | the given sampling rate and for digital ones at state changes from 0 to 1 or 125 | 1 to 0. 126 | 127 | ### Writing to a digital port 128 | 129 | Digital ports can be written to at any time: 130 | ``` 131 | board.digital[13].write(True) 132 | ``` 133 | For any other functionality (PWM or servo) use the pin class below. 134 | 135 | 136 | ### The pin class 137 | 138 | The command `get_pin` requests the class of a pin 139 | by specifying a string, composed of 140 | 'a' or 'd' (depending on if you need an analog or digital pin), the pin 141 | number, and the mode: 142 | - 'i' for input (digital or analogue) 143 | - 'u' for input with pullup (digital) 144 | - 'o' for output (digital) 145 | - 'p' for pwm (digital) 146 | - 's' for servo (digital) 147 | All seperated by `:`, for example: 148 | ``` 149 | analog_in_0 = board.get_pin('a:0:i') 150 | analog_in_0.register_callback(myCallback) 151 | analog_in_0.enable_reporting() 152 | 153 | digital_out_3 = board.get_pin('d:3:o') 154 | digital_out_3.write(True) 155 | ``` 156 | Values for analogue ports and PWM are 0..1, 157 | for servo between 0 and 180 (degrees) and for digital ports 158 | `True` & `False`. 159 | 160 | ``` 161 | class Pin(builtins.object) 162 | | Pin(board, pin_number, type=2, port=None) 163 | | 164 | | A Pin representation 165 | | 166 | | Methods defined here: 167 | | 168 | | __init__(self, board, pin_number, type=2, port=None) 169 | | Initialize self. See help(type(self)) for accurate signature. 170 | | 171 | | __str__(self) 172 | | Return str(self). 173 | | 174 | | disable_reporting(self) 175 | | Disable the reporting of an input pin. 176 | | 177 | | enable_reporting(self) 178 | | Set an input pin to report values. 179 | | 180 | | read(self) 181 | | Returns the value of an output pin. 182 | | 183 | | register_callback(self, _callback) 184 | | Register a callback to read from an analogue or digital port 185 | | 186 | | :arg value: callback with one argument which receives the data: 187 | | boolean if the pin is digital, or 188 | | float from 0 to 1 if the pin is an analgoue input 189 | | 190 | | unregiser_callback(self) 191 | | Unregisters the callback which receives data from a pin 192 | | 193 | | write(self, value) 194 | | Output a voltage from the pin 195 | | 196 | | :arg value: Uses value as a boolean if the pin is in output mode, or 197 | | expects a float from 0 to 1 if the pin is in PWM mode. If the pin 198 | | is in SERVO the value should be in degrees. 199 | | 200 | 201 | ``` 202 | 203 | ### Closing the board 204 | 205 | To close the serial port to the Arduino use the exit command: 206 | ``` 207 | board.exit() 208 | ``` 209 | 210 | ## Example code 211 | 212 | The directory https://github.com/berndporr/pyFirmata2/tree/master/examples 213 | contains two realtime Oscilloscopes with precise sampling rate, 214 | a digital port reader, the ubiquitous flashing LED program, pwm, servo control 215 | and 216 | a program which prints data using the callback handler. 217 | 218 | 219 | ## Troubleshooting 220 | 221 | ### Spyder / pyCharm / IDEs 222 | 223 | Start your program from the console / terminal and never within an IDE. Here is 224 | an example for Windows: 225 | ``` 226 | (base) D:\> 227 | (base) D:\>cd pyFirmata2\examples 228 | (base) D:\pyFirmata2\examples>python realtime_two_channel_scope.py 229 | ``` 230 | The problem with IDEs is that they won't let your Python program terminate properly 231 | which leaves the serial port in an undefined state. If you then re-run your program 232 | it won't be able to talk to your Arduino. In the worst case you need to reboot your 233 | computer. Bottomline: use your IDE for editing, run the program from the console / terminal. 234 | 235 | 236 | ### After an update still the old version is being used 237 | 238 | If you use the `--user` option to install / update packages Python might keep older versions. 239 | 240 | Solution: Do a `pip uninstall pyfirmata2` multiple times until no version is left 241 | on your computer. Then install it again as described above. 242 | 243 | 244 | 245 | 246 | ### Credits 247 | 248 | The [original pyFirmata](https://github.com/tino/pyFirmata) 249 | was written by Tino de Bruijn and is recommended if you'd rather 250 | prefer loops and sleep()-commands. 251 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, unicode_literals 2 | 3 | import unittest 4 | from itertools import chain 5 | 6 | import serial 7 | 8 | import pyfirmata2 9 | from pyfirmata2 import mockup 10 | from pyfirmata2.boards import BOARDS 11 | from pyfirmata2.util import ( 12 | break_to_bytes, from_two_bytes, str_to_two_byte_iter, to_two_bytes, two_byte_iter_to_str 13 | ) 14 | 15 | 16 | # Messages todo left: 17 | 18 | # type command channel first byte second byte 19 | # --------------------------------------------------------------------------- 20 | # set pin mode(I/O) 0xF4 pin # (0-127) pin state(0=in) 21 | # system reset 0xFF 22 | 23 | 24 | class BoardBaseTest(unittest.TestCase): 25 | 26 | def setUp(self): 27 | # Test with the MockupSerial so no real connection is needed 28 | pyfirmata2.pyfirmata2.serial.Serial = mockup.MockupSerial 29 | # Set the wait time to a zero so we won't have to wait a couple of secs 30 | # each test 31 | pyfirmata2.pyfirmata2.BOARD_SETUP_WAIT_TIME = 0 32 | self.board = pyfirmata2.Board('', BOARDS['arduino']) 33 | self.board._stored_data = [] 34 | # FIXME How can it be that a fresh instance sometimes still contains data? 35 | 36 | 37 | class TestBoardMessages(BoardBaseTest): 38 | # TODO Test layout of Board Mega 39 | def assert_serial(self, *incoming_bytes): 40 | serial_msg = bytearray() 41 | res = self.board.sp.read() 42 | while res: 43 | serial_msg += res 44 | res = self.board.sp.read() 45 | self.assertEqual(bytearray(incoming_bytes), serial_msg) 46 | 47 | # First test the handlers 48 | def test_handle_analog_message(self): 49 | self.board.analog[3].reporting = True 50 | self.assertEqual(self.board.analog[3].read(), None) 51 | # This sould set it correctly. 1023 (127, 7 in to 7 bit bytes) is the 52 | # max value an analog pin will send and it should result in a value 1 53 | self.board._handle_analog_message(3, 127, 7) 54 | self.assertEqual(self.board.analog[3].read(), 1.0) 55 | 56 | def test_handle_digital_message(self): 57 | # A digital message sets the value for a whole port. We will set pin 58 | # 5 (That is on port 0) to 1 to test if this is working. 59 | self.board.digital_ports[0].reporting = True 60 | self.board.digital[5]._mode = 0 # Set it to input 61 | # Create the mask 62 | mask = 0 63 | mask |= 1 << 5 # set the bit for pin 5 to to 1 64 | self.assertEqual(self.board.digital[5].read(), None) 65 | self.board._handle_digital_message(0, mask % 128, mask >> 7) 66 | self.assertEqual(self.board.digital[5].read(), True) 67 | 68 | def test_handle_report_version(self): 69 | self.assertEqual(self.board.firmata_version, None) 70 | self.board._handle_report_version(2, 1) 71 | self.assertEqual(self.board.firmata_version, (2, 1)) 72 | 73 | def test_handle_report_firmware(self): 74 | self.assertEqual(self.board.firmware, None) 75 | data = bytearray([2, 1]) 76 | data.extend(str_to_two_byte_iter('Firmware_name')) 77 | self.board._handle_report_firmware(*data) 78 | self.assertEqual(self.board.firmware, 'Firmware_name') 79 | self.assertEqual(self.board.firmware_version, (2, 1)) 80 | 81 | # type command channel first byte second byte 82 | # --------------------------------------------------------------------------- 83 | # analog I/O message 0xE0 pin # LSB(bits 0-6) MSB(bits 7-13) 84 | def test_incoming_analog_message(self): 85 | self.assertEqual(self.board.analog[4].read(), None) 86 | self.assertEqual(self.board.analog[4].reporting, False) 87 | # Should do nothing as the pin isn't set to report 88 | self.board.sp.write([pyfirmata2.ANALOG_MESSAGE + 4, 127, 7]) 89 | self.board.iterate() 90 | self.assertEqual(self.board.analog[4].read(), None) 91 | self.board.analog[4].enable_reporting() 92 | self.board.sp.clear() 93 | # This should set analog port 4 to 1 94 | self.board.sp.write([pyfirmata2.ANALOG_MESSAGE + 4, 127, 7]) 95 | self.board.iterate() 96 | self.assertEqual(self.board.analog[4].read(), 1.0) 97 | self.board._stored_data = [] 98 | 99 | def test_handle_capability_response(self): 100 | """ 101 | Capability Response codes: 102 | 103 | # INPUT: 0, 1 104 | # OUTPUT: 1, 1 105 | # ANALOG: 2, 10 106 | # PWM: 3, 8 107 | # SERV0: 4, 14 108 | # I2C: 6, 1 109 | 110 | Arduino's Example: (ATMega328P-PU) 111 | 112 | (127, 113 | 127, 114 | 0, 1, 1, 1, 4, 14, 127, 115 | 0, 1, 1, 1, 3, 8, 4, 14, 127, 116 | 0, 1, 1, 1, 4, 14, 127, 117 | 0, 1, 1, 1, 3, 8, 4, 14, 127, 118 | 0, 1, 1, 1, 3, 8, 4, 14, 127, 119 | 0, 1, 1, 1, 4, 14, 127, 120 | 0, 1, 1, 1, 4, 14, 127, 121 | 0, 1, 1, 1, 3, 8, 4, 14, 127, 122 | 0, 1, 1, 1, 3, 8, 4, 14, 127, 123 | 0, 1, 1, 1, 3, 8, 4, 14, 127, 124 | 0, 1, 1, 1, 4, 14, 127, 125 | 0, 1, 1, 1, 4, 14, 127, 126 | 0, 1, 1, 1, 2, 10, 127, 127 | 0, 1, 1, 1, 2, 10, 127, 128 | 0, 1, 1, 1, 2, 10, 127, 129 | 0, 1, 1, 1, 2, 10, 127, 130 | 0, 1, 1, 1, 2, 10, 6, 1, 127, 131 | 0, 1, 1, 1, 2, 10, 6, 1, 127) 132 | """ 133 | 134 | test_layout = { 135 | 'digital': (0, 1, 2), 136 | 'analog': (0, 1), 137 | 'pwm': (1, 2), 138 | 'servo': (0, 1, 2), 139 | # 'i2c': (2), # TODO 2.3 specs 140 | 'disabled': (0,), 141 | } 142 | 143 | # Eg: (127) 144 | unavailible_pin = [ 145 | 0x7F, # END_SYSEX (Pin delimiter) 146 | ] 147 | 148 | # Eg: (0, 1, 1, 1, 3, 8, 4, 14, 127) 149 | digital_pin = [ 150 | 0x00, # INPUT 151 | 0x01, 152 | 0x01, # OUTPUT 153 | 0x01, 154 | 0x03, # PWM 155 | 0x08, 156 | 0x7F, # END_SYSEX (Pin delimiter) 157 | ] 158 | 159 | # Eg. (0, 1, 1, 1, 4, 14, 127) 160 | analog_pin = [ 161 | 0x00, # INPUT 162 | 0x01, 163 | 0x01, # OUTPUT 164 | 0x01, 165 | 0x02, # ANALOG 166 | 0x0A, 167 | 0x06, # I2C 168 | 0x01, 169 | 0x7F, # END_SYSEX (Pin delimiter) 170 | ] 171 | 172 | data_arduino = list( 173 | [0x6C] # CAPABILITY_RESPONSE 174 | + unavailible_pin 175 | + digital_pin * 2 176 | + analog_pin * 2 177 | ) 178 | 179 | self.board._handle_report_capability_response(*data_arduino) 180 | for key in test_layout.keys(): 181 | self.assertEqual(self.board._layout[key], test_layout[key]) 182 | 183 | # type command channel first byte second byte 184 | # --------------------------------------------------------------------------- 185 | # digital I/O message 0x90 port LSB(bits 0-6) MSB(bits 7-13) 186 | def test_incoming_digital_message(self): 187 | # A digital message sets the value for a whole port. We will set pin 188 | # 9 (on port 1) to 1 to test if this is working. 189 | self.board.digital[9].mode = pyfirmata2.INPUT 190 | self.board.sp.clear() # clear mode sent over the wire. 191 | # Create the mask 192 | mask = 0 193 | mask |= 1 << (9 - 8) # set the bit for pin 9 to to 1 194 | self.assertEqual(self.board.digital[9].read(), None) 195 | self.board.sp.write([pyfirmata2.DIGITAL_MESSAGE + 1, mask % 128, mask >> 7]) 196 | self.board.iterate() 197 | self.assertEqual(self.board.digital[9].read(), True) 198 | 199 | # version report format 200 | # ------------------------------------------------- 201 | # 0 version report header (0xF9) (MIDI Undefined) 202 | # 1 major version (0-127) 203 | # 2 minor version (0-127) 204 | def test_incoming_report_version(self): 205 | self.assertEqual(self.board.firmata_version, None) 206 | self.board.sp.write([pyfirmata2.REPORT_VERSION, 2, 1]) 207 | self.board.iterate() 208 | self.assertEqual(self.board.firmata_version, (2, 1)) 209 | 210 | # Receive Firmware Name and Version (after query) 211 | # 0 START_SYSEX (0xF0) 212 | # 1 queryFirmware (0x79) 213 | # 2 major version (0-127) 214 | # 3 minor version (0-127) 215 | # 4 first 7-bits of firmware name 216 | # 5 second 7-bits of firmware name 217 | # x ...for as many bytes as it needs) 218 | # 6 END_SYSEX (0xF7) 219 | def test_incoming_report_firmware(self): 220 | self.assertEqual(self.board.firmware, None) 221 | self.assertEqual(self.board.firmware_version, None) 222 | msg = [pyfirmata2.START_SYSEX, 223 | pyfirmata2.REPORT_FIRMWARE, 224 | 2, 225 | 1] + list(str_to_two_byte_iter('Firmware_name')) + \ 226 | [pyfirmata2.END_SYSEX] 227 | self.board.sp.write(msg) 228 | self.board.iterate() 229 | self.assertEqual(self.board.firmware, 'Firmware_name') 230 | self.assertEqual(self.board.firmware_version, (2, 1)) 231 | 232 | # type command channel first byte second byte 233 | # --------------------------------------------------------------------------- 234 | # report analog pin 0xC0 pin # disable/enable(0/1) - n/a - 235 | def test_report_analog(self): 236 | self.board.analog[1].enable_reporting() 237 | self.assert_serial(0xC0 + 1, 1) 238 | self.assertTrue(self.board.analog[1].reporting) 239 | self.board.analog[1].disable_reporting() 240 | self.assert_serial(0xC0 + 1, 0) 241 | self.assertFalse(self.board.analog[1].reporting) 242 | 243 | # type command channel first byte second byte 244 | # --------------------------------------------------------------------------- 245 | # report digital port 0xD0 port disable/enable(0/1) - n/a - 246 | def test_report_digital(self): 247 | # This should enable reporting of whole port 1 248 | self.board.digital[8]._mode = pyfirmata2.INPUT # Outputs can't report 249 | self.board.digital[8].enable_reporting() 250 | self.assert_serial(0xD0 + 1, 1) 251 | self.assertTrue(self.board.digital_ports[1].reporting) 252 | self.board.digital[8].disable_reporting() 253 | self.assert_serial(0xD0 + 1, 0) 254 | 255 | # Generic Sysex Message 256 | # 0 START_SYSEX (0xF0) 257 | # 1 sysex command (0x00-0x7F) 258 | # x between 0 and MAX_DATA_BYTES 7-bit bytes of arbitrary data 259 | # last END_SYSEX (0xF7) 260 | def test_send_sysex_message(self): 261 | # 0x79 is queryFirmware, but that doesn't matter for now 262 | self.board.send_sysex(0x79, [1, 2, 3]) 263 | sysex = (0xF0, 0x79, 1, 2, 3, 0xF7) 264 | self.assert_serial(*sysex) 265 | 266 | def test_send_sysex_string(self): 267 | self.board.send_sysex(0x79, bytearray("test", 'ascii')) 268 | sysex = [0xF0, 0x79] 269 | sysex.extend(bytearray('test', 'ascii')) 270 | sysex.append(0xF7) 271 | self.assert_serial(*sysex) 272 | 273 | def test_send_sysex_too_big_data(self): 274 | self.assertRaises(ValueError, self.board.send_sysex, 0x79, [256, 1]) 275 | 276 | def test_receive_sysex_message(self): 277 | sysex = bytearray([0xF0, 0x79, 2, 1, ord('a'), 0, ord('b'), 0, ord('c'), 0, 0xF7]) 278 | self.board.sp.write(sysex) 279 | while len(self.board.sp): 280 | self.board.iterate() 281 | self.assertEqual(self.board.firmware_version, (2, 1)) 282 | self.assertEqual(self.board.firmware, 'abc') 283 | 284 | def test_too_much_data(self): 285 | """ 286 | When we send random bytes, before or after a command, they should be 287 | ignored to prevent cascading errors when missing a byte. 288 | """ 289 | self.board.analog[4].enable_reporting() 290 | self.board.sp.clear() 291 | # Crap 292 | self.board.sp.write([i for i in range(10)]) 293 | # This should set analog port 4 to 1 294 | self.board.sp.write([pyfirmata2.ANALOG_MESSAGE + 4, 127, 7]) 295 | # Crap 296 | self.board.sp.write([10 - i for i in range(10)]) 297 | while len(self.board.sp): 298 | self.board.iterate() 299 | self.assertEqual(self.board.analog[4].read(), 1.0) 300 | 301 | # Servo config 302 | # -------------------- 303 | # 0 START_SYSEX (0xF0) 304 | # 1 SERVO_CONFIG (0x70) 305 | # 2 pin number (0-127) 306 | # 3 minPulse LSB (0-6) 307 | # 4 minPulse MSB (7-13) 308 | # 5 maxPulse LSB (0-6) 309 | # 6 maxPulse MSB (7-13) 310 | # 7 END_SYSEX (0xF7) 311 | # 312 | # then sets angle 313 | # 8 analog I/O message (0xE0) 314 | # 9 angle LSB 315 | # 10 angle MSB 316 | def test_servo_config(self): 317 | self.board.servo_config(2) 318 | data = chain([0xF0, 0x70, 2], 319 | to_two_bytes(544), 320 | to_two_bytes(2400), 321 | [0xF7, 0xE0 + 2, 0, 0]) 322 | self.assert_serial(*list(data)) 323 | 324 | def test_servo_config_min_max_pulse(self): 325 | self.board.servo_config(2, 600, 2000) 326 | data = chain([0xF0, 0x70, 2], 327 | to_two_bytes(600), 328 | to_two_bytes(2000), 329 | [0xF7, 0xE0 + 2, 0, 0]) 330 | self.assert_serial(*data) 331 | 332 | def test_servo_config_min_max_pulse_angle(self): 333 | self.board.servo_config(2, 600, 2000, angle=90) 334 | data = chain([0xF0, 0x70, 2], to_two_bytes(600), to_two_bytes(2000), [0xF7]) 335 | angle_set = [0xE0 + 2, 90 % 128, 90 >> 7] # Angle set happens through analog message 336 | data = list(data) + angle_set 337 | self.assert_serial(*data) 338 | 339 | def test_servo_config_invalid_pin(self): 340 | self.assertRaises(IOError, self.board.servo_config, 1) 341 | 342 | def test_set_mode_servo(self): 343 | p = self.board.digital[2] 344 | p.mode = pyfirmata2.SERVO 345 | data = chain([0xF0, 0x70, 2], 346 | to_two_bytes(544), 347 | to_two_bytes(2400), 348 | [0xF7, 0xE0 + 2, 0, 0]) 349 | self.assert_serial(*data) 350 | 351 | 352 | class TestBoardLayout(BoardBaseTest): 353 | 354 | def test_layout_arduino(self): 355 | self.assertEqual(len(BOARDS['arduino']['digital']), len(self.board.digital)) 356 | self.assertEqual(len(BOARDS['arduino']['analog']), len(self.board.analog)) 357 | 358 | def test_layout_arduino_mega(self): 359 | pyfirmata2.pyfirmata2.serial.Serial = mockup.MockupSerial 360 | mega = pyfirmata2.Board('', BOARDS['arduino_mega']) 361 | self.assertEqual(len(BOARDS['arduino_mega']['digital']), len(mega.digital)) 362 | self.assertEqual(len(BOARDS['arduino_mega']['analog']), len(mega.analog)) 363 | 364 | def test_pwm_layout(self): 365 | pins = [] 366 | for pin in self.board.digital: 367 | if pin.PWM_CAPABLE: 368 | pins.append(self.board.get_pin('d:%d:p' % pin.pin_number)) 369 | for pin in pins: 370 | self.assertEqual(pin.mode, pyfirmata2.PWM) 371 | self.assertTrue(pin.pin_number in BOARDS['arduino']['pwm']) 372 | self.assertTrue(len(pins) == len(BOARDS['arduino']['pwm'])) 373 | 374 | def test_get_pin_digital(self): 375 | pin = self.board.get_pin('d:13:o') 376 | self.assertEqual(pin.pin_number, 13) 377 | self.assertEqual(pin.mode, pyfirmata2.OUTPUT) 378 | self.assertEqual(pin.port.port_number, 1) 379 | self.assertEqual(pin.port.reporting, False) 380 | 381 | def test_get_pin_analog(self): 382 | pin = self.board.get_pin('a:5:i') 383 | self.assertEqual(pin.pin_number, 5) 384 | self.assertEqual(pin.reporting, True) 385 | self.assertEqual(pin.value, None) 386 | 387 | def tearDown(self): 388 | self.board.exit() 389 | pyfirmata2.serial.Serial = serial.Serial 390 | 391 | 392 | class TestMockupSerial(unittest.TestCase): 393 | 394 | def setUp(self): 395 | self.s = mockup.MockupSerial('someport', 4800) 396 | 397 | def test_only_bytes(self): 398 | self.s.write(0xA0) 399 | self.s.write(100) 400 | self.assertRaises(TypeError, self.s.write, 'blaat') 401 | 402 | def test_write_read(self): 403 | self.s.write(0xA1) 404 | self.s.write([1, 3, 5]) 405 | self.assertEqual(self.s.read(2), bytearray([0xA1, 0x01])) 406 | self.assertEqual(len(self.s), 2) 407 | self.assertEqual(self.s.read(), bytearray([3])) 408 | self.assertEqual(self.s.read(), bytearray([5])) 409 | self.assertEqual(len(self.s), 0) 410 | self.assertEqual(self.s.read(), bytearray()) 411 | self.assertEqual(self.s.read(2), bytearray()) 412 | 413 | def test_none(self): 414 | self.assertEqual(self.s.read(), bytearray()) 415 | 416 | 417 | class TestMockupBoardLayout(TestBoardLayout, TestBoardMessages): 418 | """ 419 | TestMockupBoardLayout is subclassed from TestBoardLayout and 420 | TestBoardMessages as it should pass the same tests, but with the 421 | MockupBoard. 422 | """ 423 | 424 | def setUp(self): 425 | self.board = mockup.MockupBoard('test', BOARDS['arduino']) 426 | 427 | 428 | class RegressionTests(BoardBaseTest): 429 | 430 | def test_correct_digital_input_first_pin_issue_9(self): 431 | """ 432 | The first pin on the port would always be low, even if the mask said 433 | it to be high. 434 | """ 435 | pin = self.board.get_pin('d:8:i') 436 | mask = 0 437 | mask |= 1 << 0 # set pin 0 high 438 | self.board._handle_digital_message(pin.port.port_number, mask % 128, mask >> 7) 439 | self.assertEqual(pin.value, True) 440 | 441 | def test_handle_digital_inputs(self): 442 | """ 443 | Test if digital inputs are correctly updated. 444 | """ 445 | for i in range(8, 16): # pins of port 1 446 | if not bool(i % 2) and i != 14: # all even pins 447 | self.board.digital[i].mode = pyfirmata2.INPUT 448 | self.assertEqual(self.board.digital[i].value, None) 449 | mask = 0 450 | # Set the mask high for the first 4 pins 451 | for i in range(4): 452 | mask |= 1 << i 453 | self.board._handle_digital_message(1, mask % 128, mask >> 7) 454 | self.assertEqual(self.board.digital[8].value, True) 455 | self.assertEqual(self.board.digital[9].value, None) 456 | self.assertEqual(self.board.digital[10].value, True) 457 | self.assertEqual(self.board.digital[11].value, None) 458 | self.assertEqual(self.board.digital[12].value, False) 459 | self.assertEqual(self.board.digital[13].value, None) 460 | 461 | def test_proper_exit_conditions(self): 462 | """ 463 | Test that the exit method works properly if we didn't make it all 464 | the way through `setup_layout`. 465 | """ 466 | del self.board.digital 467 | try: 468 | self.board.exit() 469 | except AttributeError: 470 | self.fail("exit() raised an AttributeError unexpectedly!") 471 | 472 | 473 | class UtilTests(unittest.TestCase): 474 | 475 | def test_to_two_bytes(self): 476 | for i in range(32768): 477 | val = to_two_bytes(i) 478 | self.assertEqual(len(val), 2) 479 | 480 | self.assertEqual(to_two_bytes(32767), bytearray(b'\x7f\xff')) 481 | self.assertRaises(ValueError, to_two_bytes, 32768) 482 | 483 | def test_from_two_bytes(self): 484 | for i in range(32766, 32768): 485 | val = to_two_bytes(i) 486 | ret = from_two_bytes(val) 487 | self.assertEqual(ret, i) 488 | 489 | self.assertEqual(from_two_bytes(('\xff', '\xff')), 32767) 490 | self.assertEqual(from_two_bytes(('\x7f', '\xff')), 32767) 491 | 492 | def test_two_byte_iter_to_str(self): 493 | string, s = 'StandardFirmata', [] 494 | for i in string: 495 | s.append(i) 496 | s.append('\x00') 497 | self.assertEqual(two_byte_iter_to_str(s), 'StandardFirmata') 498 | 499 | def test_str_to_two_byte_iter(self): 500 | string, itr = 'StandardFirmata', bytearray() 501 | for i in string: 502 | itr.append(ord(i)) 503 | itr.append(0) 504 | self.assertEqual(itr, str_to_two_byte_iter(string)) 505 | 506 | def test_break_to_bytes(self): 507 | self.assertEqual(break_to_bytes(200), (200,)) 508 | self.assertEqual(break_to_bytes(800), (200, 4)) 509 | self.assertEqual(break_to_bytes(802), (2, 2, 200)) 510 | 511 | 512 | if __name__ == '__main__': 513 | unittest.main(verbosity=2) 514 | -------------------------------------------------------------------------------- /pyfirmata2/pyfirmata2.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, unicode_literals 2 | 3 | import inspect 4 | import time 5 | import warnings 6 | 7 | import serial 8 | import serial.tools.list_ports 9 | from sys import platform 10 | 11 | from .util import pin_list_to_board_dict, to_two_bytes, two_byte_iter_to_str, Iterator 12 | 13 | 14 | # Message command bytes (0x80(128) to 0xFF(255)) - straight from Firmata.h 15 | DIGITAL_MESSAGE = 0x90 # send data for a digital pin 16 | ANALOG_MESSAGE = 0xE0 # send data for an analog pin (or PWM) 17 | DIGITAL_PULSE = 0x91 # SysEx command to send a digital pulse 18 | 19 | # PULSE_MESSAGE = 0xA0 # proposed pulseIn/Out msg (SysEx) 20 | # SHIFTOUT_MESSAGE = 0xB0 # proposed shiftOut msg (SysEx) 21 | REPORT_ANALOG = 0xC0 # enable analog input by pin # 22 | REPORT_DIGITAL = 0xD0 # enable digital input by port pair 23 | START_SYSEX = 0xF0 # start a MIDI SysEx msg 24 | SET_PIN_MODE = 0xF4 # set a pin to INPUT/OUTPUT/PWM/etc 25 | END_SYSEX = 0xF7 # end a MIDI SysEx msg 26 | REPORT_VERSION = 0xF9 # report firmware version 27 | SYSTEM_RESET = 0xFF # reset from MIDI 28 | QUERY_FIRMWARE = 0x79 # query the firmware name 29 | 30 | # extended command set using sysex (0-127/0x00-0x7F) 31 | # 0x00-0x0F reserved for user-defined commands */ 32 | 33 | EXTENDED_ANALOG = 0x6F # analog write (PWM, Servo, etc) to any pin 34 | PIN_STATE_QUERY = 0x6D # ask for a pin's current mode and value 35 | PIN_STATE_RESPONSE = 0x6E # reply with pin's current mode and value 36 | CAPABILITY_QUERY = 0x6B # ask for supported modes and resolution of all pins 37 | CAPABILITY_RESPONSE = 0x6C # reply with supported modes and resolution 38 | ANALOG_MAPPING_QUERY = 0x69 # ask for mapping of analog to pin numbers 39 | ANALOG_MAPPING_RESPONSE = 0x6A # reply with mapping info 40 | 41 | SERVO_CONFIG = 0x70 # set max angle, minPulse, maxPulse, freq 42 | STRING_DATA = 0x71 # a string message with 14-bits per char 43 | SHIFT_DATA = 0x75 # a bitstream to/from a shift register 44 | I2C_REQUEST = 0x76 # send an I2C read/write request 45 | I2C_REPLY = 0x77 # a reply to an I2C read request 46 | I2C_CONFIG = 0x78 # config I2C settings such as delay times and power pins 47 | REPORT_FIRMWARE = 0x79 # report name and version of the firmware 48 | SAMPLING_INTERVAL = 0x7A # set the poll rate of the main loop 49 | SYSEX_NON_REALTIME = 0x7E # MIDI Reserved for non-realtime messages 50 | SYSEX_REALTIME = 0x7F # MIDI Reserved for realtime messages 51 | 52 | 53 | # Pin modes. 54 | # except from UNAVAILABLE taken from Firmata.h 55 | UNAVAILABLE = -1 56 | INPUT = 0 # as defined in wiring.h 57 | OUTPUT = 1 # as defined in wiring.h 58 | ANALOG = 2 # analog pin in analogInput mode 59 | PWM = 3 # digital pin in PWM output mode 60 | SERVO = 4 # digital pin in SERVO mode 61 | INPUT_PULLUP = 11 # Same as INPUT, but with the pin's internal pull-up resistor enabled 62 | 63 | # Pin types 64 | DIGITAL = OUTPUT # same as OUTPUT below 65 | # ANALOG is already defined above 66 | 67 | # Time to wait after initializing serial, used in Board.__init__ 68 | BOARD_SETUP_WAIT_TIME = 5 69 | 70 | 71 | class PinAlreadyTakenError(Exception): 72 | pass 73 | 74 | 75 | class InvalidPinDefError(Exception): 76 | pass 77 | 78 | 79 | class NoInputWarning(RuntimeWarning): 80 | pass 81 | 82 | 83 | class Board(object): 84 | """The Base class for any board.""" 85 | firmata_version = None 86 | firmware = None 87 | firmware_version = None 88 | _command_handlers = {} 89 | _command = None 90 | _stored_data = [] 91 | _parsing_sysex = False 92 | AUTODETECT = None 93 | 94 | def __init__(self, port, layout=None, baudrate=57600, name=None, timeout=None, debug=False): 95 | if port == self.AUTODETECT: 96 | l = serial.tools.list_ports.comports() 97 | if l: 98 | if platform == "linux" or platform == "linux2": 99 | for d in l: 100 | if 'ACM' in d.device or 'usbserial' in d.device or 'ttyUSB' in d.device: 101 | port = str(d.device) 102 | elif platform == "win32": 103 | comports = [] 104 | for d in l: 105 | if d.device: 106 | if ("USB" in d.description) or (not d.description) or ("Arduino" in d.description): 107 | devname = str(d.device) 108 | comports.append(devname) 109 | comports.sort() 110 | if len(comports) > 0: 111 | port = comports[0] 112 | else: 113 | for d in l: 114 | if d.vid: 115 | port = str(d.device) 116 | if port == self.AUTODETECT: 117 | self.samplerThread = None 118 | self.sp = None 119 | raise Exception('Could not find a serial port.') 120 | if debug: 121 | print("Port=",port) 122 | self.samplerThread = Iterator(self) 123 | self.sp = serial.Serial(port, baudrate, timeout=timeout) 124 | # Allow 5 secs for Arduino's auto-reset to happen 125 | # Alas, Firmata blinks its version before printing it to serial 126 | # For 2.3, even 5 seconds might not be enough. 127 | # TODO Find a more reliable way to wait until the board is ready 128 | self.__pass_time(BOARD_SETUP_WAIT_TIME) 129 | self.name = name 130 | self._layout = layout 131 | if not self.name: 132 | self.name = port 133 | 134 | if layout: 135 | self.setup_layout(layout) 136 | else: 137 | self.auto_setup() 138 | 139 | # Iterate over the first messages to get firmware data 140 | while self.bytes_available(): 141 | self.iterate() 142 | # TODO Test whether we got a firmware name and version, otherwise there 143 | # probably isn't any Firmata installed 144 | 145 | def __str__(self): 146 | return "Board{0.name} on {0.sp.port}".format(self) 147 | 148 | def __del__(self): 149 | """ 150 | The connection with the a board can get messed up when a script is 151 | closed without calling board.exit() (which closes the serial 152 | connection). Therefore also do it here and hope it helps. 153 | """ 154 | self.exit() 155 | 156 | def send_as_two_bytes(self, val): 157 | self.sp.write(bytearray([val % 128, val >> 7])) 158 | 159 | def setup_layout(self, board_layout): 160 | """ 161 | Setup the Pin instances based on the given board layout. 162 | """ 163 | # Create pin instances based on board layout 164 | self.analog = [] 165 | for i in board_layout['analog']: 166 | self.analog.append(Pin(self, i)) 167 | 168 | self.digital = [] 169 | self.digital_ports = [] 170 | for i in range(0, len(board_layout['digital']), 8): 171 | num_pins = len(board_layout['digital'][i:i + 8]) 172 | port_number = int(i / 8) 173 | self.digital_ports.append(Port(self, port_number, num_pins)) 174 | 175 | # Allow to access the Pin instances directly 176 | for port in self.digital_ports: 177 | self.digital += port.pins 178 | 179 | # Setup PWM pins 180 | for i in board_layout['pwm']: 181 | self.digital[i].PWM_CAPABLE = True 182 | 183 | # Disable certain ports like Rx/Tx and crystal ports 184 | for i in board_layout['disabled']: 185 | self.digital[i].mode = UNAVAILABLE 186 | 187 | # Create a dictionary of 'taken' pins. Used by the get_pin method 188 | self.taken = {'analog': dict(map(lambda p: (p.pin_number, False), self.analog)), 189 | 'digital': dict(map(lambda p: (p.pin_number, False), self.digital))} 190 | 191 | self._set_default_handlers() 192 | 193 | def _set_default_handlers(self): 194 | # Setup default handlers for standard incoming commands 195 | self.add_cmd_handler(ANALOG_MESSAGE, self._handle_analog_message) 196 | self.add_cmd_handler(DIGITAL_MESSAGE, self._handle_digital_message) 197 | self.add_cmd_handler(REPORT_VERSION, self._handle_report_version) 198 | self.add_cmd_handler(REPORT_FIRMWARE, self._handle_report_firmware) 199 | 200 | def samplingOn(self, sample_interval=19): 201 | # enables sampling 202 | if not self.samplerThread.running: 203 | if sample_interval < 1: 204 | raise ValueError("Sampling interval less than 1ms") 205 | self.setSamplingInterval(sample_interval) 206 | self.samplerThread.start() 207 | 208 | def samplingOff(self): 209 | # disables sampling 210 | if not self.samplerThread: 211 | return 212 | if self.samplerThread.running: 213 | self.samplerThread.stop() 214 | self.samplerThread.join() 215 | 216 | def auto_setup(self): 217 | """ 218 | Automatic setup based on Firmata's "Capability Query" 219 | """ 220 | self.add_cmd_handler(CAPABILITY_RESPONSE, self._handle_report_capability_response) 221 | self.send_sysex(CAPABILITY_QUERY, []) 222 | self.__pass_time(0.1) # Serial SYNC 223 | 224 | while self.bytes_available(): 225 | self.iterate() 226 | 227 | # handle_report_capability_response will write self._layout 228 | if self._layout: 229 | self.setup_layout(self._layout) 230 | else: 231 | raise IOError("Board detection failed.") 232 | 233 | def add_cmd_handler(self, cmd, func): 234 | """Adds a command handler for a command.""" 235 | len_args = len(inspect.getfullargspec(func)[0]) 236 | 237 | def add_meta(f): 238 | def decorator(*args, **kwargs): 239 | f(*args, **kwargs) 240 | decorator.bytes_needed = len_args - 1 # exclude self 241 | decorator.__name__ = f.__name__ 242 | return decorator 243 | func = add_meta(func) 244 | self._command_handlers[cmd] = func 245 | 246 | def get_pin(self, pin_def): 247 | """ 248 | Returns the activated pin given by the pin definition. 249 | May raise an ``InvalidPinDefError`` or a ``PinAlreadyTakenError``. 250 | 251 | :arg pin_def: Pin definition as described below, 252 | but without the arduino name. So for example ``a:1:i``. 253 | 254 | 'a' analog pin Pin number 'i' for input 255 | 'd' digital pin Pin number 'o' for output 256 | 'p' for pwm (Pulse-width modulation) 257 | 's' for servo 258 | 'u' for input with pull-up resistor enabled 259 | 260 | All seperated by ``:``. 261 | """ 262 | if type(pin_def) == list: 263 | bits = pin_def 264 | else: 265 | bits = pin_def.split(':') 266 | a_d = bits[0] == 'a' and 'analog' or 'digital' 267 | part = getattr(self, a_d) 268 | pin_nr = int(bits[1]) 269 | if pin_nr >= len(part): 270 | raise InvalidPinDefError('Invalid pin definition: {0} at position 3 on {1}' 271 | .format(pin_def, self.name)) 272 | if getattr(part[pin_nr], 'mode', None) == UNAVAILABLE: 273 | raise InvalidPinDefError('Invalid pin definition: ' 274 | 'UNAVAILABLE pin {0} at position on {1}' 275 | .format(pin_def, self.name)) 276 | if self.taken[a_d][pin_nr]: 277 | raise PinAlreadyTakenError('{0} pin {1} is already taken on {2}' 278 | .format(a_d, bits[1], self.name)) 279 | # ok, should be available 280 | pin = part[pin_nr] 281 | self.taken[a_d][pin_nr] = True 282 | if pin.type is DIGITAL: 283 | if bits[2] == 'p': 284 | pin.mode = PWM 285 | elif bits[2] == 's': 286 | pin.mode = SERVO 287 | elif bits[2] == 'u': 288 | pin.mode = INPUT_PULLUP 289 | elif bits[2] == 'i': 290 | pin.mode = INPUT 291 | elif bits[2] == 'o': 292 | pin.mode = OUTPUT 293 | else: 294 | pin.mode = INPUT 295 | else: 296 | pin.enable_reporting() 297 | return pin 298 | 299 | def __pass_time(self, t): 300 | """Non-blocking time-out for ``t`` seconds.""" 301 | cont = time.time() + t 302 | while time.time() < cont: 303 | time.sleep(0) 304 | 305 | def send_sysex(self, sysex_cmd, data): 306 | """ 307 | Sends a SysEx msg. 308 | 309 | :arg sysex_cmd: A sysex command byte 310 | : arg data: a bytearray of 7-bit bytes of arbitrary data 311 | """ 312 | msg = bytearray([START_SYSEX, sysex_cmd]) 313 | msg.extend(data) 314 | msg.append(END_SYSEX) 315 | self.sp.write(msg) 316 | 317 | def bytes_available(self): 318 | return self.sp.inWaiting() 319 | 320 | def iterate(self): 321 | """ 322 | Reads and handles data from the microcontroller over the serial port. 323 | This method should be called in a main loop or in an :class:`Iterator` 324 | instance to keep this boards pin values up to date. 325 | """ 326 | byte = self.sp.read() 327 | if not byte: 328 | return 329 | data = ord(byte) 330 | received_data = [] 331 | handler = None 332 | if data < START_SYSEX: 333 | # These commands can have 'channel data' like a pin nummber appended. 334 | try: 335 | handler = self._command_handlers[data & 0xF0] 336 | except KeyError: 337 | return 338 | received_data.append(data & 0x0F) 339 | while len(received_data) < handler.bytes_needed: 340 | received_data.append(ord(self.sp.read())) 341 | elif data == START_SYSEX: 342 | data = ord(self.sp.read()) 343 | handler = self._command_handlers.get(data) 344 | if not handler: 345 | return 346 | data = ord(self.sp.read()) 347 | while data != END_SYSEX: 348 | received_data.append(data) 349 | data = ord(self.sp.read()) 350 | else: 351 | try: 352 | handler = self._command_handlers[data] 353 | except KeyError: 354 | return 355 | while len(received_data) < handler.bytes_needed: 356 | received_data.append(ord(self.sp.read())) 357 | # Handle the data 358 | try: 359 | handler(*received_data) 360 | except ValueError: 361 | pass 362 | 363 | def get_firmata_version(self): 364 | """ 365 | Returns a version tuple (major, minor) for the firmata firmware on the 366 | board. 367 | """ 368 | return self.firmata_version 369 | 370 | def servo_config(self, pin, min_pulse=544, max_pulse=2400, angle=0): 371 | """ 372 | Configure a pin as servo with min_pulse, max_pulse and first angle. 373 | ``min_pulse`` and ``max_pulse`` default to the arduino defaults. 374 | """ 375 | if pin > len(self.digital) or self.digital[pin].mode == UNAVAILABLE: 376 | raise IOError("Pin {0} is not a valid servo pin".format(pin)) 377 | 378 | data = bytearray([pin]) 379 | data += to_two_bytes(min_pulse) 380 | data += to_two_bytes(max_pulse) 381 | self.send_sysex(SERVO_CONFIG, data) 382 | 383 | # set pin._mode to SERVO so that it sends analog messages 384 | # don't set pin.mode as that calls this method 385 | self.digital[pin]._mode = SERVO 386 | self.digital[pin].write(angle) 387 | 388 | def setSamplingInterval(self, intervalInMs): 389 | data = to_two_bytes(int(intervalInMs)) 390 | self.send_sysex(SAMPLING_INTERVAL, data) 391 | 392 | def exit(self): 393 | """Call this to exit cleanly.""" 394 | for a in self.analog: 395 | a.disable_reporting() 396 | for d in self.digital: 397 | d.disable_reporting() 398 | self.samplingOff() 399 | # First detach all servo's, otherwise it somehow doesn't want to close... 400 | if hasattr(self, 'digital'): 401 | for pin in self.digital: 402 | if pin.mode == SERVO: 403 | pin.mode = OUTPUT 404 | if hasattr(self, 'sp'): 405 | if self.sp: 406 | self.sp.close() 407 | 408 | # Command handlers 409 | def _handle_analog_message(self, pin_nr, lsb, msb): 410 | value = round(float((msb << 7) + lsb) / 1023, 4) 411 | # Only set the value if we are actually reporting 412 | try: 413 | if self.analog[pin_nr].reporting: 414 | self.analog[pin_nr].value = value 415 | if not self.analog[pin_nr].callback is None: 416 | self.analog[pin_nr].callback(value) 417 | except IndexError: 418 | raise ValueError 419 | 420 | def _handle_digital_message(self, port_nr, lsb, msb): 421 | """ 422 | Digital messages always go by the whole port. This means we have a 423 | bitmask which we update the port. 424 | """ 425 | mask = (msb << 7) + lsb 426 | try: 427 | self.digital_ports[port_nr]._update(mask) 428 | except IndexError: 429 | raise ValueError 430 | 431 | def _handle_report_version(self, major, minor): 432 | self.firmata_version = (major, minor) 433 | 434 | def _handle_report_firmware(self, *data): 435 | major = data[0] 436 | minor = data[1] 437 | self.firmware_version = (major, minor) 438 | self.firmware = two_byte_iter_to_str(data[2:]) 439 | 440 | def _handle_report_capability_response(self, *data): 441 | charbuffer = [] 442 | pin_spec_list = [] 443 | 444 | for c in data: 445 | if c == CAPABILITY_RESPONSE: 446 | continue 447 | 448 | charbuffer.append(c) 449 | if c == 0x7F: 450 | # A copy of charbuffer 451 | pin_spec_list.append(charbuffer[:]) 452 | charbuffer = [] 453 | 454 | self._layout = pin_list_to_board_dict(pin_spec_list) 455 | 456 | 457 | class Port(object): 458 | """An 8-bit port on the board.""" 459 | def __init__(self, board, port_number, num_pins=8): 460 | self.board = board 461 | self.port_number = port_number 462 | self.reporting = False 463 | 464 | self.pins = [] 465 | for i in range(num_pins): 466 | pin_nr = i + self.port_number * 8 467 | self.pins.append(Pin(self.board, pin_nr, type=DIGITAL, port=self)) 468 | 469 | def __str__(self): 470 | return "Digital Port {0.port_number} on {0.board}".format(self) 471 | 472 | def enable_reporting(self): 473 | """Enable reporting of values for the whole port.""" 474 | self.reporting = True 475 | msg = bytearray([REPORT_DIGITAL + self.port_number, 1]) 476 | self.board.sp.write(msg) 477 | 478 | for pin in self.pins: 479 | if pin.mode == INPUT or pin.mode == INPUT_PULLUP: 480 | pin.reporting = True # TODO Shouldn't this happen at the pin? 481 | 482 | def disable_reporting(self): 483 | """Disable the reporting of the port.""" 484 | if not self.reporting: 485 | return 486 | self.reporting = False 487 | msg = bytearray([REPORT_DIGITAL + self.port_number, 0]) 488 | self.board.sp.write(msg) 489 | 490 | def write(self): 491 | """Set the output pins of the port to the correct state.""" 492 | mask = 0 493 | for pin in self.pins: 494 | if pin.mode == OUTPUT: 495 | if pin.value == 1: 496 | pin_nr = pin.pin_number - self.port_number * 8 497 | mask |= 1 << int(pin_nr) 498 | # print("type mask", type(mask)) 499 | # print("type self.portnumber", type(self.port_number)) 500 | # print("type pinnr", type(pin_nr)) 501 | msg = bytearray([DIGITAL_MESSAGE + self.port_number, mask % 128, mask >> 7]) 502 | self.board.sp.write(msg) 503 | 504 | def _update(self, mask): 505 | """Update the values for the pins marked as input with the mask.""" 506 | if self.reporting: 507 | for pin in self.pins: 508 | if pin.mode is INPUT or pin.mode is INPUT_PULLUP: 509 | pin_nr = pin.pin_number - self.port_number * 8 510 | pin.value = (mask & (1 << pin_nr)) > 0 511 | if not pin.callback is None: 512 | pin.callback(pin.value) 513 | 514 | 515 | class Pin(object): 516 | """A Pin representation""" 517 | def __init__(self, board, pin_number, type=ANALOG, port=None): 518 | self.board = board 519 | self.pin_number = pin_number 520 | self.type = type 521 | self.port = port 522 | self.PWM_CAPABLE = False 523 | self._mode = (type == DIGITAL and OUTPUT or INPUT) 524 | self.reporting = False 525 | self.value = None 526 | self.callback = None 527 | 528 | def __str__(self): 529 | type = {ANALOG: 'Analog', DIGITAL: 'Digital'}[self.type] 530 | return "{0} pin {1}".format(type, self.pin_number) 531 | 532 | def _set_mode(self, mode): 533 | if mode is UNAVAILABLE: 534 | self._mode = UNAVAILABLE 535 | return 536 | if self._mode is UNAVAILABLE: 537 | raise IOError("{0} can not be used through Firmata".format(self)) 538 | if mode is PWM and not self.PWM_CAPABLE: 539 | raise IOError("{0} does not have PWM capabilities".format(self)) 540 | if mode == SERVO: 541 | if self.type != DIGITAL: 542 | raise IOError("Only digital pins can drive servos! {0} is not" 543 | "digital".format(self)) 544 | self._mode = SERVO 545 | self.board.servo_config(self.pin_number) 546 | return 547 | 548 | # Set mode with SET_PIN_MODE message 549 | self._mode = mode 550 | self.board.sp.write(bytearray([SET_PIN_MODE, self.pin_number, mode])) 551 | if mode == INPUT or mode == INPUT_PULLUP: 552 | self.enable_reporting() 553 | 554 | def _get_mode(self): 555 | return self._mode 556 | 557 | mode = property(_get_mode, _set_mode) 558 | """ 559 | Mode of operation for the pin. Can be one of the pin modes: INPUT, OUTPUT, 560 | ANALOG, PWM. or SERVO (or UNAVAILABLE). 561 | """ 562 | 563 | def enable_reporting(self): 564 | """Set an input pin to report values.""" 565 | if self.mode is not INPUT and self.mode is not INPUT_PULLUP: 566 | raise IOError("{0} is not an input and can therefore not report".format(self)) 567 | if self.type == ANALOG: 568 | self.reporting = True 569 | msg = bytearray([REPORT_ANALOG + self.pin_number, 1]) 570 | self.board.sp.write(msg) 571 | else: 572 | self.port.enable_reporting() 573 | # TODO This is not going to work for non-optimized boards like Mega 574 | 575 | def disable_reporting(self): 576 | """Disable the reporting of an input pin.""" 577 | if self.type == ANALOG: 578 | if not self.reporting: 579 | return 580 | self.reporting = False 581 | msg = bytearray([REPORT_ANALOG + self.pin_number, 0]) 582 | self.board.sp.write(msg) 583 | else: 584 | self.port.disable_reporting() 585 | # TODO This is not going to work for non-optimized boards like Mega 586 | 587 | def read(self): 588 | """Returns the value of an output pin.""" 589 | if self.mode == UNAVAILABLE: 590 | raise IOError("Cannot read pin {0}".format(self.__str__())) 591 | if (self.mode is INPUT) or (self.mode is INPUT_PULLUP) or (self.type == ANALOG): 592 | raise IOError("Reading via polling is not supported by this library. Please use the original pyfirmata.") 593 | return self.value 594 | 595 | def register_callback(self, _callback): 596 | """ 597 | Register a callback to read from an analogue or digital port 598 | 599 | :arg value: callback with one argument which receives the data: 600 | boolean if the pin is digital, or 601 | float from 0 to 1 if the pin is an analgoue input 602 | """ 603 | self.callback = _callback 604 | 605 | def unregiser_callback(self): 606 | """ 607 | Unregisters the callback which receives data from a pin 608 | """ 609 | self.callback = None 610 | 611 | def write(self, value): 612 | """ 613 | Output a voltage from the pin 614 | 615 | :arg value: Uses value as a boolean if the pin is in output mode, or 616 | expects a float from 0 to 1 if the pin is in PWM mode. If the pin 617 | is in SERVO the value should be in degrees. 618 | 619 | """ 620 | if self.mode is UNAVAILABLE: 621 | raise IOError("{0} can not be used through Firmata".format(self)) 622 | if self.mode is INPUT or self.mode is INPUT_PULLUP: 623 | raise IOError("{0} is set up as an INPUT and can therefore not be written to" 624 | .format(self)) 625 | if value is not self.value: 626 | self.value = value 627 | if self.mode is OUTPUT: 628 | if self.port: 629 | self.port.write() 630 | else: 631 | msg = bytearray([DIGITAL_MESSAGE, self.pin_number, value]) 632 | self.board.sp.write(msg) 633 | elif self.mode is PWM: 634 | value = int(round(value * 255)) 635 | msg = bytearray([ANALOG_MESSAGE + self.pin_number, value % 128, value >> 7]) 636 | self.board.sp.write(msg) 637 | elif self.mode is SERVO: 638 | value = int(value) 639 | msg = bytearray([ANALOG_MESSAGE + self.pin_number, value % 128, value >> 7]) 640 | self.board.sp.write(msg) 641 | --------------------------------------------------------------------------------