Vai al contenuto

Articolo: Arduino Tutorial: Python - Leggere ed elaborare con il PC i valori sulle porte di Arduino

Inviato

Python ed Arduino

 

Tramite la porta USB arduino può inviare una serie di dati che il computer può leggere e visualizzare. Questo è particolarmente utile perchè un programma sviluppato ad hoc sarebbe in grado di elaborare i dati forniti da Arduino.

Una delle possibili applicazioni di questa funzione è quella di automatizzare la creazione di grafici sulla base dei dati raccolti: immaginatevi ad esempio un sensore di temperatura (come un resistore che varia il suo valore al variare della temperatura) collegato ad Arduino, e il PC che continua a leggere il valore del resistore trasformando il dato in °C e mettendo il tutto su di un grafico. Dopo aver letto questo tutorial sarete in grado di fare anche una cosa del genere.

Innanzitutto riprendiamo lo schema dell'ultima volta:

http://www.nonsologaming.com/vd/arduino_led_potv1.png

...e il programma scritto per Arduino:

int pinpot = 0;
int pinled = 9;
int valpot = 0;
int valled = 0;

void setup() {
 pinMode(pinled, OUTPUT);
}

void loop() {
 valpot = analogRead(pinpot);
 valled = map(valpot, 0, 1023, 0, 255);
 analogWrite(pinled, valled);
 delay(10);
}

Per aprire un flusso di dati dobbiamo inizializzare la periferica seriale su Arduino aggiungendola alla funzione setup:

void setup() {
 pinMode(pinled, OUTPUT);
 Serial.begin(9600);
}

Ora dobbiamo dire quale valore Arduino deve inviare sulla porta seriale: io ho preso in considerazione il valore del potenziometro quindi la funzione loop diventa:

void loop() {
 valpot = analogRead(pinpot);
 valled = map(valpot, 0, 1023, 0, 255);
 analogWrite(pinled, valled);
 Serial.println(valpot);
 delay(10);
}

Sull'IDE di Arduino c'è un pulsante denominato "serial monitor": se noi carichiamo l'attuale programma e poi clicchiamo su "serial monitor" ci verrà mostrata una serie di numeri che variano se spostiamo il potenziometro... ecco, questi sono i valori della variabile valpot, che come tutti gli ingressi analogici di Arduino può assumere tutti gli stati che vanno da 0 a 1023 (come abbiamo già visto).

Per vedere il flusso di dati non è necessario avere necessariamente sempre aperto l'IDE di arduino, infatti grazie al linguaggio Python è possibile leggere questo flusso, e sviluppare di conseguenza dei programmi che possono elaborare i dati inviati al PC dalla board.

C'è da ricordare comunque che per fare questo è necessario prima installare python-serial, atrimenti python non è in grado di aprire la porta seriale.

Creiamo il file "Arduino_Monitor.py" e all'interno andiamo a scrivere:

from threading import Thread
import time
import serial

last_received = ''
def receiving(ser):
   global last_received
   buffer = ''
   while True:
       buffer = buffer + ser.read(ser.inWaiting())
       if '\n' in buffer:
           lines = buffer.split('\n') 
           last_received = lines[-2]
           buffer = lines[-1]


class SerialData(object):
   def __init__(self, init=50):
       try:
           self.ser = ser = serial.Serial(
               port='/dev/ttyACM0',
               baudrate=9600,
               bytesize=serial.EIGHTBITS,
               parity=serial.PARITY_NONE,
               stopbits=serial.STOPBITS_ONE,
               timeout=0.1,
               xonxoff=0,
               rtscts=0,
               interCharTimeout=None
           )
       except serial.serialutil.SerialException:
           self.ser = None
       else:
           Thread(target=receiving, args=(self.ser,)).start()
       
   def next(self):
       if not self.ser:
           return 100 
       for i in range(40):
           raw_line = last_received
           try:
               return float(raw_line.strip())
           except ValueError:
               print 'bogus data',raw_line
               time.sleep(.005)
       return 0.
   def __del__(self):
       if self.ser:
           self.ser.close()

if __name__=='__main__':
   s = SerialData()
   for i in range(500):
       time.sleep(.015)
       print s.next()

Andando ad eseguire questo nostro programma, vedremo sul terminale che non fa altro che scrivere a schermo i valori del potenziometro, esattamente come succede con l'IDE di arduino.

Avendo a disposizione anche il modulo wx e matplotlib di python possiamo creare un vero e proprio grafico, basta creare un altro file python, ad esempio "wx_arduino_dynamic_graph.py" e all'interno aggiungiamo:

import os
import pprint
import random
import sys
import wx

REFRESH_INTERVAL_MS = 90

import matplotlib
matplotlib.use('WXAgg')
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import \
   FigureCanvasWxAgg as FigCanvas, \
   NavigationToolbar2WxAgg as NavigationToolbar
import numpy as np
import pylab
from Arduino_Monitor import SerialData as DataGen


class BoundControlBox(wx.Panel):
   def __init__(self, parent, ID, label, initval):
       wx.Panel.__init__(self, parent, ID)
       
       self.value = initval
       
       box = wx.StaticBox(self, -1, label)
       sizer = wx.StaticBoxSizer(box, wx.VERTICAL)
       
       self.radio_auto = wx.RadioButton(self, -1, 
           label="Auto", style=wx.RB_GROUP)
       self.radio_manual = wx.RadioButton(self, -1,
           label="Manual")
       self.manual_text = wx.TextCtrl(self, -1, 
           size=(35,-1),
           value=str(initval),
           style=wx.TE_PROCESS_ENTER)
       
       self.Bind(wx.EVT_UPDATE_UI, self.on_update_manual_text, self.manual_text)
       self.Bind(wx.EVT_TEXT_ENTER, self.on_text_enter, self.manual_text)
       
       manual_box = wx.BoxSizer(wx.HORIZONTAL)
       manual_box.Add(self.radio_manual, flag=wx.ALIGN_CENTER_VERTICAL)
       manual_box.Add(self.manual_text, flag=wx.ALIGN_CENTER_VERTICAL)
       
       sizer.Add(self.radio_auto, 0, wx.ALL, 10)
       sizer.Add(manual_box, 0, wx.ALL, 10)
       
       self.SetSizer(sizer)
       sizer.Fit(self)
   
   def on_update_manual_text(self, event):
       self.manual_text.Enable(self.radio_manual.GetValue())
   
   def on_text_enter(self, event):
       self.value = self.manual_text.GetValue()
   
   def is_auto(self):
       return self.radio_auto.GetValue()
       
   def manual_value(self):
       return self.value


class GraphFrame(wx.Frame):
   title = 'Demo: dynamic matplotlib graph'
   
   def __init__(self):
       wx.Frame.__init__(self, None, -1, self.title)
       
       self.datagen = DataGen()
       self.data = [self.datagen.next()]
       self.paused = False
       
       self.create_menu()
       self.create_status_bar()
       self.create_main_panel()
       
       self.redraw_timer = wx.Timer(self)
       self.Bind(wx.EVT_TIMER, self.on_redraw_timer, self.redraw_timer)        
       self.redraw_timer.Start(REFRESH_INTERVAL_MS)

   def create_menu(self):
       self.menubar = wx.MenuBar()
       
       menu_file = wx.Menu()
       m_expt = menu_file.Append(-1, "&Save plot\tCtrl-S", "Save plot to file")
       self.Bind(wx.EVT_MENU, self.on_save_plot, m_expt)
       menu_file.AppendSeparator()
       m_exit = menu_file.Append(-1, "E&xit\tCtrl-X", "Exit")
       self.Bind(wx.EVT_MENU, self.on_exit, m_exit)
               
       self.menubar.Append(menu_file, "&File")
       self.SetMenuBar(self.menubar)

   def create_main_panel(self):
       self.panel = wx.Panel(self)

       self.init_plot()
       self.canvas = FigCanvas(self.panel, -1, self.fig)

       self.xmin_control = BoundControlBox(self.panel, -1, "X min", 0)
       self.xmax_control = BoundControlBox(self.panel, -1, "X max", 50)
       self.ymin_control = BoundControlBox(self.panel, -1, "Y min", 0)
       self.ymax_control = BoundControlBox(self.panel, -1, "Y max", 100)
       
       self.pause_button = wx.Button(self.panel, -1, "Pause")
       self.Bind(wx.EVT_BUTTON, self.on_pause_button, self.pause_button)
       self.Bind(wx.EVT_UPDATE_UI, self.on_update_pause_button, self.pause_button)
       
       self.cb_grid = wx.CheckBox(self.panel, -1, 
           "Show Grid",
           style=wx.ALIGN_RIGHT)
       self.Bind(wx.EVT_CHECKBOX, self.on_cb_grid, self.cb_grid)
       self.cb_grid.SetValue(True)
       
       self.cb_xlab = wx.CheckBox(self.panel, -1, 
           "Show X labels",
           style=wx.ALIGN_RIGHT)
       self.Bind(wx.EVT_CHECKBOX, self.on_cb_xlab, self.cb_xlab)        
       self.cb_xlab.SetValue(True)
       
       self.hbox1 = wx.BoxSizer(wx.HORIZONTAL)
       self.hbox1.Add(self.pause_button, border=5, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL)
       self.hbox1.AddSpacer(20)
       self.hbox1.Add(self.cb_grid, border=5, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL)
       self.hbox1.AddSpacer(10)
       self.hbox1.Add(self.cb_xlab, border=5, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL)
       
       self.hbox2 = wx.BoxSizer(wx.HORIZONTAL)
       self.hbox2.Add(self.xmin_control, border=5, flag=wx.ALL)
       self.hbox2.Add(self.xmax_control, border=5, flag=wx.ALL)
       self.hbox2.AddSpacer(24)
       self.hbox2.Add(self.ymin_control, border=5, flag=wx.ALL)
       self.hbox2.Add(self.ymax_control, border=5, flag=wx.ALL)
       
       self.vbox = wx.BoxSizer(wx.VERTICAL)
       self.vbox.Add(self.canvas, 1, flag=wx.LEFT | wx.TOP | wx.GROW)        
       self.vbox.Add(self.hbox1, 0, flag=wx.ALIGN_LEFT | wx.TOP)
       self.vbox.Add(self.hbox2, 0, flag=wx.ALIGN_LEFT | wx.TOP)
       
       self.panel.SetSizer(self.vbox)
       self.vbox.Fit(self)
   
   def create_status_bar(self):
       self.statusbar = self.CreateStatusBar()

   def init_plot(self):
       self.dpi = 100
       self.fig = Figure((3.0, 3.0), dpi=self.dpi)

       self.axes = self.fig.add_subplot(111)
       self.axes.set_axis_bgcolor('black')
       self.axes.set_title('Arduino Serial Data', size=12)
       
       pylab.setp(self.axes.get_xticklabels(), fontsize=8)
       pylab.setp(self.axes.get_yticklabels(), fontsize=8)

       self.plot_data = self.axes.plot(
           self.data, 
           linewidth=1,
           color=(1, 1, 0),
           )[0]

   def draw_plot(self):
       if self.xmax_control.is_auto():
           xmax = len(self.data) if len(self.data) > 50 else 50
       else:
           xmax = int(self.xmax_control.manual_value())
           
       if self.xmin_control.is_auto():            
           xmin = xmax - 50
       else:
           xmin = int(self.xmin_control.manual_value())

       if self.ymin_control.is_auto():
           ymin = round(min(self.data), 0) - 1
       else:
           ymin = int(self.ymin_control.manual_value())
       
       if self.ymax_control.is_auto():
           ymax = round(max(self.data), 0) + 1
       else:
           ymax = int(self.ymax_control.manual_value())

       self.axes.set_xbound(lower=xmin, upper=xmax)
       self.axes.set_ybound(lower=ymin, upper=ymax)
       
       if self.cb_grid.IsChecked():
           self.axes.grid(True, color='gray')
       else:
           self.axes.grid(False)

       pylab.setp(self.axes.get_xticklabels(), 
           visible=self.cb_xlab.IsChecked())
       
       self.plot_data.set_xdata(np.arange(len(self.data)))
       self.plot_data.set_ydata(np.array(self.data))
       
       self.canvas.draw()
   
   def on_pause_button(self, event):
       self.paused = not self.paused
   
   def on_update_pause_button(self, event):
       label = "Resume" if self.paused else "Pause"
       self.pause_button.SetLabel(label)
   
   def on_cb_grid(self, event):
       self.draw_plot()
   
   def on_cb_xlab(self, event):
       self.draw_plot()
   
   def on_save_plot(self, event):
       file_choices = "PNG (*.png)|*.png"
       
       dlg = wx.FileDialog(
           self, 
           message="Save plot as...",
           defaultDir=os.getcwd(),
           defaultFile="plot.png",
           wildcard=file_choices,
           style=wx.SAVE)
       
       if dlg.ShowModal() == wx.ID_OK:
           path = dlg.GetPath()
           self.canvas.print_figure(path, dpi=self.dpi)
           self.flash_status_message("Saved to %s" % path)
   
   def on_redraw_timer(self, event):
       if not self.paused:
           self.data.append(self.datagen.next())
       
       self.draw_plot()
   
   def on_exit(self, event):
       self.Destroy()
   
   def flash_status_message(self, msg, flash_len_ms=1500):
       self.statusbar.SetStatusText(msg)
       self.timeroff = wx.Timer(self)
       self.Bind(
           wx.EVT_TIMER, 
           self.on_flash_status_off, 
           self.timeroff)
       self.timeroff.Start(flash_len_ms, oneShot=True)
   
   def on_flash_status_off(self, event):
       self.statusbar.SetStatusText('')


if __name__ == '__main__':
   app = wx.PySimpleApp()
   app.frame = GraphFrame()
   app.frame.Show()
   app.MainLoop()

Vi ricordo che questo programma è dipendente da quello precedente infatti il primo legge il flusso di dati ed il secondo li visualizza graficamente.

Il tutto è stato programmato su Ubuntu 12.04 con python3.

Modificato da RyujiAndy

Featured Replies

Inviato
Se hai a disposizione un collegamento Internet puoi anche valutare di usare il servizio online Cosm (prima noto come Pachube) dove tu invii i dati e lui costruisce automaticamente il grafico visualizzabile su browser con parecchie personalizzazioni. Non ricordo se va anche su smartphone ma credo di si.

Crea un account o accedi per lasciare un commento