Постановка задачи


Цифровые и аналоговые датчики, подключенные к 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.

Ссылки


  1. Python Programming for Arduino.
  2. Язык программирования Си (Керниган и Ритчи).

Комментарии (2)



  1. snnwolf
    11.09.2017 10:13

    У вас с Листинге 3 лишняя кавычка


    # Associate port and board with pyFirmata
    port = '/dev/cu.usbmodemfa1321''
    board = pyfirmata.Arduino(port)