Постановка задачи
Цифровые и аналоговые датчики, подключенные к Arduino, генерируют большие объёмы информации, которая требует обработки в реальном масштабе времени [1].
В настоящее время данные от Arduino распечатывают из командной строки или отображают в графическом интерфейсе с запаздыванием. Поэтому данные в режиме реального времени и не сохраняются, что делает невозможным их дальнейший анализ.
Данная публикация посвящена программному решению задачи хранения информации от датчиков, работающих с Arduino и её графическому представлению в реальном масштабе времени. В примерах используются широко известными датчиками, такими как потенциометр и датчик движения PIR.
Использование CSV-файлов для хранения данных полученных от датчиков, работающих с Arduino
- Для записи данных в CSVфайл можно использовать простой листинг:
import csv data = [[1, 2, 3], ['a', 'b', 'c'], ['Python', 'Arduino', 'Programming']] with open('example.csv', 'w') as f: w = csv.writer (f) for row in data: w.writerow(row) - Для чтения данных из CSV-файл можно использовать следующий листинг:
import csv with open('example.csv', 'r') as file: r = csv.reader(file) for row in r: print(row)
Рассмотрим сохранения данные Arduino на примере двух датчиков – потенциометра с аналоговым выходным сигналом и движения (PIR) с цифровым выходным сигналом.
Потенциометр подключён к аналоговому выводу 0, а датчик движения PIR к цифровому выводу 11, как показано на следующей схеме:

Для работы данной схемы, необходимо загрузить в Python модуль pyFirmata и эскиз StandardFirmata в плату Arduino.
В любом файле Python разместим следующий код, который запускает и останавливает запись данных от обеих датчиков в файл SensorDataStore.csv:
Листинг № 1
#!/usr/bin/python import csv import pyfirmata from time import sleep port = 'COM3' board = pyfirmata.Arduino(port) it = pyfirmata.util.Iterator(board) it.start() pirPin = board.get_pin('d:11:i') a0 = board.get_pin('a:0:i') print(pirPin) with open('SensorDataStore.csv', 'w+') as f: w = csv.writer(f) w.writerow(["Number", "Potentiometer", "Motion sensor"]) i = 0 pirData = pirPin.read() m=25 n=1 while i < m: sleep(1) if pirData is not None: i += 1 potData = a0.read() pirData = pirPin.read() row = [i, potData, pirData] print(row) w.writerow(row) print ("Done. CSV file is ready!") board.exit()
В результате работы листинга №1, получим запись данных в файл 'SensorDataStore.csv':
Рассмотрим код связанный с хранением данных датчиков. Первая строка записи в CSV файл– строка заголовка, которая объясняет содержимое столбцов: w.writerow([«Number», «Potentiometer», «Motion sensor»]).
Когда появляется динамика изменения данных, которую для приведенной схемы можно искусственно создать поворотом ручки потенциометра или движением руки возле датчика движения критичным становиться число записей в файл данных. Для восстановления формы сигнала частота записи данных в файл должна быть вдвое больше частоты изменения сигнала. Для регулировки частоты записей может использоваться шаг – n, а для регулировки времени измерения число циклов – m. Однако ограничения на n снизу накладывает быстродействие самих датчиков. Эту функцию в приведенной выше программе выполняет следующий фрагмент кода:
m=25 n=1 while i < m: sleep(n) if pirData is not None: i += 1 row = [i, potData, pirData] w.writerow (row)
Приведенная программы может быть изменена в соответствии с проектными требованиями следующим образом:
- Можно изменить номера выводов Arduino и количество выводов, которые будут использоваться. Это можно сделать, добавив в код Python и эскиз StandardFirmata в Arduino строки дополнительных значений для новых датчиков.
- CSV-файл: имя файла и его местоположение можно изменить с SensorDataStore.csv на то, которое относится к вашему приложению.
- Частоту записей m в файл SensorDataStore.csv можно изменить, изменяя шаг, а длительность записи изменяя число циклов при постоянном шаге.
Графический анализ данных из файла CSV
Используя файл SensorDataStore.csv создадим программу для поучений массиво�� данных от потенциометра и датчика движения и построения графиков по данным массивам:
Листинг №2
import sys, csv import csv from matplotlib import pyplot i = [] mValues = [] pValues = [] with open('SensorDataStore.csv', 'r') as f: reader = csv.reader(f) header = next(reader, None) for row in reader: i.append(int(row[0])) pValues.append(float(row[1])) if row[2] == 'True': mValues.append(1) else: mValues.append(0) pyplot.subplot(2, 1, 1) pyplot.plot(i, pValues, '-') pyplot.title('Line plot - ' + header[1]) pyplot.xlim([1, 25]) pyplot.xlabel('X Axis') pyplot.ylabel('Y Axis') pyplot.subplot(2, 1, 2) pyplot.bar(i, mValues) pyplot.title('Bar chart - ' + header[2]) pyplot.xlim([1, 25]) pyplot.xlabel('X Axis') pyplot.ylabel('Y Axis') pyplot.tight_layout() pyplot.show()
В результате работы листинга №2, получим два графика на одной форме для отображения в реальном масштабе времени выходных данных потенциометра и датчика движения.

В этой программе мы создали два массива значений датчиков — pValues и mValues — путем чтения файла SensorDataStore.csv по строкам. Здесь pValues и mValues представляют данные датчика для потенциометра и датчика движения соответственно. С использованием методов matplotlib построим два графика на одной форме.
Созданный код после каждого цикла формирования массивов данных pValues и mValues полученным от датчиков Arduino обновляет интерфейс, что позволяет сделать вывод о получении данных в реальном масштабе времени.
Однако метод хранения и визуализации данных имеет следующие особенности – весь набор данных сначала записываются в файл SensorDataStore.csv (Листинг №1), а затем считываются из этого файла (Листинг №2). Графики должны перерисовываться каждый раз, когда поступают новые значения от Arduino. Поэтому нужно разработать такую программу, в которой планирование и обновление графиков происходит в реальном времени, а не строится весь набор значений датчиков, как в листингах №1,2.[2].
Пишем программу для потенциометра создавая динамику выходного сигнала путём изменения его активного сопротивления постоянному току.
Листинг №3
import sys, csv from matplotlib import pyplot import pyfirmata from time import sleep import numpy as np # Associate port and board with pyFirmata port = '/dev/cu.usbmodemfa1321' board = pyfirmata.Arduino(port) # Using iterator thread to avoid buffer overflow it = pyfirmata.util.Iterator(board) it.start() # Assign a role and variable to analog pin 0 a0 = board.get_pin(''a:0:i'') # Initialize interactive mode pyplot.ion() pData = [0] * 25 fig = pyplot.figure() pyplot.title(''Real-time Potentiometer reading'') ax1 = pyplot.axes() l1, = pyplot.plot(pData) pyplot.ylim([0,1]) # real-time plotting loop while True: try: sleep(1) pData.append(float(a0.read())) pyplot.ylim([0, 1]) del pData[0] l1.set_xdata([i for i in xrange(25)]) l1.set_ydata(pData) # update the data pyplot.draw() # update the plot except KeyboardInterrupt: board.exit() break
В результате работы листинга №3, получим график.
Планирование в реальном времени в этом упражнении достигается с помощью комбинации функций pyplot ion (), draw (), set_xdata () и set_data (). Метод ion () инициализирует интерактивный режим pyplot. Интерактивный режим помогает динамически изменять значения x и y графиков на рисунке pyplot.ion ().
Когда интерактивный режим установлен в True, график будет вырисовываться только при вызове метода draw (). Инициализируем плату Arduino с помощью модуля pyFirmata и входных пинов для получения значений датчиков.
Как вы можете видеть в следующей строке кода, после настройки платы Arduino и интерактивного режима pyplot, мы инициализировали график с набором пустых данных, в нашем случае 0: pData = [0] * 25.
Этот массив для значений y, pData, затем используется для добавления значений из датчика в цикле while. Цикл while продолжает добавлять новые значения в этот массив данных и перерисовывает график с этими обновленными массивами для значений x и y.
В листинге №3 мы добавляем новые значения датчиков в конце массива, одновременно удаляя первый элемент массива, чтобы ограничить размер массива:
pData.append(float(a0.read())) del pData[0]
Методы set_xdata () и set_ydata () используются для обновления данных осей x и y из этих массивов. Эти обновленные значения наносятся с использованием метода draw () на каждой итерации цикла while:
l1.set_xdata([i for i in xrange(25)]) l1.set_ydata(pData) # update the data pyplot.draw() # update the plot
Вы также заметите, что мы используем функцию xrange () для генерации диапазона значений в соответствии с предоставленной длиной, которая равна 25 в нашем случае. Фрагмент кода [i for i in xrange(25)] будет генерировать список из 25 целых чисел, которые начинаются постепенно с 0 и заканчиваются на 24.
Интеграция графиков в окне Tkinter
Благодаря мощным возможностям интеграции Python, очень удобно связать графики, созданные библиотекой matplotlib, с графическим интерфейсом Tkinter. Напишем программу для соединения между Tkinter и matplotlib.
Листинг №4
import sys from matplotlib import pyplot import pyfirmata from time import sleep import Tkinter def onStartButtonPress(): while True: if flag.get(): sleep(1) pData.append(float(a0.read())) pyplot.ylim([0, 1]) del pData[0] l1.set_xdata([i for i in xrange(25)]) l1.set_ydata(pData) # update the data pyplot.draw() # update the plot top.update() else: flag.set(True) break def onPauseButtonPress(): flag.set(False) def onExitButtonPress(): print "Exiting...." onPauseButtonPress() board.exit() pyplot.close(fig) top.quit() top.destroy() print "Done." sys.exit() # Associate port and board with pyFirmata port = 'COM4' board = pyfirmata.Arduino(port) # Using iterator thread to avoid buffer overflow it = pyfirmata.util.Iterator(board) it.start() # Assign a role and variable to analog pin 0 a0 = board.get_pin('a:0:i') # Tkinter canvas top = Tkinter.Tk() top.title("Tkinter + matplotlib") # Create flag to work with indefinite while loop flag = Tkinter.BooleanVar(top) flag.set(True) pyplot.ion() pData = [0.0] * 25 fig = pyplot.figure() pyplot.title('Potentiometer') ax1 = pyplot.axes() l1, = pyplot.plot(pData) pyplot.ylim([0, 1]) # Create Start button and associate with onStartButtonPress method startButton = Tkinter.Button(top, text="Start", command=onStartButtonPress) startButton.grid(column=1, row=2) # Create Stop button and associate with onStopButtonPress method pauseButton = Tkinter.Button(top, text="Pause", command=onPauseButtonPress) pauseButton.grid(column=2, row=2) # Create Exit button and destroy the window exitButton = Tkinter.Button(top, text="Exit", command=onExitButtonPress) exitButton.grid(column=3, row=2) top.mainloop()
В результате работы листинга №4, получим встроенный в окно Tkinter. график с элементами кнопочного интерфейса – startButton, pauseButton и exitButton.

Кнопки «Start» и «Exit» предоставляют контрольные точки для операций matplotlib, та-кие как обновление графика и закрытие графика с помощью соответствующих функций onStartButtonPress () и onExitButtonPress (). Функция onStartButtonPress () также состоит из точки взаимодействия между библиотеками matplotlib и pyFirmata. Как вы можете видеть из следующего фрагмента кода, мы начнем обновлять график, используя метод draw () и окно Tkinter, используя метод update () для каждого наблюдения с аналогового вывода a0, который получается с помощью метода read ().
Функция onExitButtonPress () реализует функцию exit, как описано в самом имени. Она закрывает фигуру pyplot и окно Tkinter перед отключением платы Arduino от последовательного порта.
После внесения соответствующих изменений в параметр порта Arduino запустим листинг №4. Вы должны увидеть окно на экране, подобное тому, которое показано на предыдущем скриншоте. С помощью этого кода вы можете теперь управлять графиками в реальном времени, используя кнопки «Start» и «Pause». Нажмите кнопку «Start» и начните вращать ручку потенциометра. Когда вы нажимаете кнопку «Pause», вы можете заметить, что программа остановила построение новых значений. При нажатии кнопки «Пауза» даже вращение ручки не приведет к каким-либо изменениям графика.
Как только вы снова нажмете на кнопку «Start», вы снова увидите, что график обновляется в реальном времени, отбрасывая значения, сгенерированные во время паузы. Нажмите кнопку «Exit», чтобы безопасно закрыть программу.
При подготовке материалов данной публикации принимал участие Мисов О. П.
Выводы
В этой публикации представлены две основные парадигмы программирования Python: создание, чтение и запись файлов с использованием Python, а также хранение данных в этих файлах и построение значений датчиков и обновление графиков в реальном времени. Мы также изучили методы хранения и отображения данных датчика Arduino в реальном времени. Помимо помощи Вам в проектах с Arduino, эти методы также можно использовать в повседневных проектах Python.
