Постановка задачи
Цифровые и аналоговые датчики, подключенные к 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:
#!/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 создадим программу для получений массивов данных от потенциометра и датчика движения и построения графиков по данным массивам:
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].
Пишем программу для потенциометра создавая динамику выходного сигнала путём изменения его активного сопротивления постоянному току.
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.
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.
Ссылки
Автор: Юрий Тараненко