Постановка задачи
Цифровые и аналоговые датчики, подключенные к 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, как показано на следующей схеме:
![](https://habrastorage.org/web/3f3/155/171/3f31551712ce44d9923773c88032bec4.png)
Для работы данной схемы, необходимо загрузить в 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, получим два графика на одной форме для отображения в реальном масштабе времени выходных данных потенциометра и датчика движения.
![](https://habrastorage.org/web/44e/d66/1bf/44ed661bf3c7498aa12160a4542b7e16.png)
В этой программе мы создали два массива значений датчиков — 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.
![](https://habrastorage.org/web/ade/4a3/379/ade4a3379118436487db9c99046ebe33.png)
Кнопки «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.