Clicca sull'immagine seguente per andare direttamente ai video di Ninobot.
Videodd
VIDEO


Carissimi amici di Ninobot!
Ho il piacere di presentarvi la nuova versione del robottino.
Lungi dall'essere un'esposizione completa (la scrivero' non appena mi sara' possibile), le tre foto che vedete in questa pagina vogliono solo mostrare il nuovo aspetto di Ninobot e qualche particolarita'.
Questa e' la parte anteriore.
Dall'alto verso il basso si possono vedere:
  • La sfera di plastica che contiene le due telecamere, l'anteriore e la posteriore. In questa foto si osserva bene quella anteriore con i led a infrarossi per le riprese notturne
  • L'oblo centrale per controllare i meccanismi interni. La copertura puo' essere rimossa per accedere al servomotore dello sterzo, la scheda del driver del motore ecc.
  • La lampada a led per l'illuminazione notturna.
  • Il gruppo motore e ruota motrice con il suo sistema di bilanciamento.
In questa seconda foto e' possibile osservare la parte laterale di Ninobot. Dall'alto verso il basso abbiamo:
  • La sfera che contiene le due telecamemere, adesso visibili entrambe.
  • L'altoparlante per la diffusione sonora. Sul lato opposto ne e' presente un altro per garantire l'effetto stereo.
  • La ruota motrice con il bilanciamento ed il vano batteria richiudibile.
Nella parte posteriore e' visibile uno degli elementi principali di Ninbot.
Andando come al solito con ordine, abbiamo:
  • La sfera con le telecamere.
  • Il blocco di controllo del robottino. Si tratta di una scatola incastonata nel corpo di Ninobot dove si trovano la Raspberry PI, l'Arduino ed il nuovo componente di base: un telefonino (visibile in foto) utilizzato come modulo radio e interfaccia grafica. Nel blocco di controllo sono visibili anche il voltometro e l'amperometro oltre ad alcuni led si stato.
  • Le ruote posteriori ed il retro del vano batteria.

La trattazione che segue non e' stata modificata e riguarda ancora la versione prototipale di Ninobot.
Nonostante non sia aggiornata, illustra tuttavia tutte le principali funzionalita' del sistema e come e' stato realizzato.

Introduzione

Correva l'anno 1980 ed un ragazzino di un piccolo paese della Sicilia vedeva, per la prima volta nella sua vita, il modellino radiocomandato di un'automobile.

Fu un amore a prima vista. Quel ragazzino, da sempre appassionato di elettronica e tecnologia, quel giorno decise che avrebbe costruito un suo modellino radiocomandato, ma non un modellino qualsiasi. Doveva essere qualcosa di simile ad un robot e si sarebbe dovuto pilotare comodamente da casa, senza vederlo fisicamente, anche a distanza di chilometri. Poco meno che un sogno per un quattordicenne, con tanta passione ma senza conoscenze, che amava mettere il naso nei televisori e nelle radio a valvole per cercare invano di carpire qualche segreto.

Quel ragazzino ero io e solo ora a distanza di anni mi rendo conto di quanto quel sogno, assurdo ed impossibile, sia stato per me la chiave di tante delle decisioni che ho preso nella vita, dalla scelta di iscrivermi alla Scuola Radio Elettra di Torino a quella di prendere la laurea in Ingegneria Elettronica.

Con il passare del tempo il robottino ha cambiato forma, si è evoluto sia funzionalmente che strutturalmente e tante delle caratteristiche che avrebbe dovuto avere hanno trovato una chiara soluzione tecnica.

Andiamo con ordine però. La storia è appena iniziata.


Il target minimale

Quello che volevo realizzare, almeno inizialmente, è un oggetto conosciuto con il nome di UGV (Unmanned Ground Vehicle – Veicolo di terra senza pilota). Ho aggiunto anche l'acronimo LR alla definizione (Long Range) in quanto l'idea era quella di poterlo pilotare, almeno in teoria, “da qualsiasi posto, in ogni posto”. Ho dovuto accettare qualche compromesso però. Per adesso, per semplicità, posso pilotarlo solo da casa (le questioni tecniche relative le descriverò in seguito) ma, virtualmente, posso fargli coprire un'area pari all'intero territorio italiano. In effetti sarebbe meglio dire che la copertura di Ninobot è pari alla copertura della scheda SIM che utilizzo nel modem 3G. Dove arriva il segnale di quell'operatore telefonico può arrivare anche Ninobot.

Gli UGV, attualmente, sono utilizzati principalmente per scopi militari e sono controllati attraverso un satellite per telecomunicazioni. Gli UAV (Unmanned Aerial Vehicle), anche conosciuti come Droni, sono anch'essi telecomandati ma, come recita lo stesso acronimo, non “strisciano” sulla nuda terra: volano! Vedrò, in un prossimo futuro, di apportare tale miglioramento a Ninobot.


Requisiti di un UGV

Ma cosa deve avere a bordo un UGV per funzionare correttamente? Compiliamo la lista della spesa.

Per quanto riguarda i sistemi di controllo, oggi non c'è che l'imbarazzo della scelta oltre che la “noia” di doverli programmare. Sto parlando di veri e propri microcomputer, con tanto di processore, uscita video, uscita audio, USB, scheda ethernet, sui quali si può far girare comodamente un sistema operativo completo e potente come Linux, in una delle sue varie distribuzioni.

Ho provato due di queste soluzioni.

La prima, in ordine temporale, è stata la BeagleBoard. Si tratta di una scheda basta su un processore ARM Cortex A8 con 512 MB di RAM. Ci gira senza problemi Ubuntu e, configurata correttamente, pare sia in grado di riprodurre un film in Full HD. Ho da poco scoperto che ne è uscita un'altra versione, la BeagleBone, dalle dimensioni e dai costi più contenuti.

La seconda è la Raspberry PI. Si basa anch'essa su un processore ARM a 700MHz e nell'ultimissima versione sono disponibili 512 MB di RAM contro i 256 della versione precedente. Il suo punto di forza è il costo: appena 30 euro che diventano poi 38 con le spese di spedizione. Al di là del fatto che ho dovuto aspettare 5 mesi per averla (l'ho ordinata a Giugno del 2012 e mi è arrivata a Novembre) e che ho riscontrato un piccolo problema HW sull'ormai famoso fusibile F3 (http://elinux.org/R-Pi_Troubleshooting#Troubleshooting_power_problems) posso dire di essere molto contento dell'acquisto. L'ho provata con Arch Linux, una versione molto robusta e completa del Sistema Operativo del pinguino, ed ho capito che sarebbe diventata il pezzo principale del mio Ninobot.

La scelta della telecamera è stata molto più complicata. Anzi, a dire il vero, è stata bloccante. Per mesi ho cercato una soluzione che andasse bene, ma sempre con risultati insoddisfacenti. Di telecamere il mercato è pieno, specie se parliamo di telecamere USB. Adesso la scelta si è ampliata a quelle Ethernet/Wi-Fi con costi davvero contenuti. Appena qualche anno fa, comprare delle “Ipcam” era decisamente proibitivo. Per ragioni tecniche, però, erano anche le uniche che facevano al caso mio. Il problema è la cosiddetta latenza. Per bene che vada, con una telecamera USB non si riesce ad avere l'immagine prima di 1,5/2 secondi; un'era geologica per un'applicazione in tempo reale. A questo effetto “hardware” indesiderato si aggiunge anche il buffering “software”. Tutti i server per video streaming che ho provato, prima di inviare l'immagine, la bufferizzano creando ulteriore ritardo. La soluzione doveva per forza essere una Ipcam che ha dei tempi di risposta estremamente rapidi ed un suo software proprietario per l'accesso ai dati. Il problema della latenza, comunque, è stato un problema ricorrente in questo progetto e, spesso, per evitare il dilatarsi dei tempi, specie sulle connessioni 3G, ho dovuto rivedere gran parte del software.

Altra cosa delicata, è stato reperire il motore. I motori, dovrei dire, perché Ninobot ne utilizza due. La direzione, infatti, viene determinata modulando opportunamente la potenza che viene fornita ad entrambi: uguale potenza per farlo andare diritto, potenza al solo destro per farlo andare a sinistra e viceversa. In generale se Pd>Ps (Pd=potenza motore di destra, Ps=potenza al motore di sinistra), Ninobot va a sinistra, se Ps>Pd Ninobot va a destra. E' lo stesso principio di tutti i mezzi cingolati. La soluzione dei due motori, se le ruote sono posizionate all'esatta metà del veicolo, consente di azzerare l'angolo di curvatura. In altre parole, fornendo la stessa potenza ai motori ma invertendo la rotazione di uno dei due, il robot inizia a girare su se stesso, intorno al suo centro di massa, rendendo semplice il cambio di direzione anche in spazi ristretti.

Il nuovo inizio

Ero all'edizione invernale del Marc, la fiera dell'elettronica di Genova, poco prima del Natale 2012. L'ho vista lì. Era la telecamera Ethernet con “tilt e pan” (brandeggiabile) che avevo sempre sognato ad un prezzo incredibile: solo 45 euro. Non ero sicuro dei tempi di risposta del software a bordo, ma mi sono detto: “forse non sarà perfetta però quando mi ricapita un'occasione simile?”

I test mi hanno dato ragione. L'immagine veniva trasferita in un tempo abbastanza breve da poter essere considerato istantaneo, almeno per le mie esigenze. Certo ho dovuto studiare parecchio per poterla integrare con la Raspberry PI che avevo ricevuto appena il mese prima.


Mancavano solo i motori. Nel negozio di modellismo dove ero andato, esposto il mio problema, mi avevano detto che ci volevano anche 400 euro per un motore con le caratteristiche da me richieste ed i relativi riduttori. Decisamente troppi per un progetto squisitamente casalingo. Sono andato allora da Brico e mi sono guardato intorno sicuro che nel regno del “fai da te” avrei trovato un'ispirazione.

E' inimmaginabile la quantità di motori elettrici e non che si trova in un negozio del genere, peccato che sono sempre integrati in qualcos'altro. Ci sono i motori a scoppio dei tagliaerba e delle seghe a nastro, i motori elettrici dei trapani, dei seghetti alternativi, delle smerigliatrici. Tutti troppo potenti per le mie necessità e quelli elettrici vanno alimentati con a 220 Volt, tensione decisamente elevata per Ninobot. A parte questo non avrebbe avuto molto senso spendere 50x2=100 euro, come minimo, per comprare due attrezzi da cui estrarre il motore. Quando l'impresa sembrava ormai disperata, l'occhio mi è caduto sulla soluzione. In un semplice strumento avevo trovato tutto quello che mi serviva: il prezzo contenuto, la tensione bassa, il riduttore di giri e quindi la necessaria potenza. Sembrava incredibile, ma i miei desideri avevano un nome preciso: l'avvitatore/svitatore elettrico!

Ne ho comprati due della Black & Decker per 9,90 euro l'uno e, anche se li ho dovuti aprire e sezionare rendendoli di fatto inutilizzabili per lo scopo per il quale erano stati costruiti, la spesa era abbastanza irrisoria per poter affrontare il rischio di rimanere con nulla in mano.Riduttore planetario

L'operazione è stata “chirurgica” ma è riuscita. Sono riuscito a recuperare non solo i motori ma anche i riduttori di giri, i cosiddetti “riduttori planetari” il cui nome altisonante è dovuto al loro particolare funzionamento e forma.

Un esempio di riduttore planetario è rappresentato nell'immagine a fianco. N.B. Il riduttore del Black & Decker non è così grande :-) .


Componenti utilizzati

A questo punto, non me ne vogliano i meno esperti, entriamo nel vivo della parte tecnica. Inizio dall'hardware elencando e descrivendo tutti i componenti fisici che sono stati utilizzati per Ninobot. Proseguirò poi con il software, quindi listati dei programmi e scelte tecnologiche.

Qualcuno sarà interessato a questa parte, altri la troveranno estremamente noiosa. Io, da parte mia, cercherò sempre di spiegare tutto nel modo più semplice possibile in modo da rendere gradevole la lettura anche a coloro che non amano entrare nel merito delle questioni tecniche.


Raspberry PI

E' il componente hardware principale di Ninobot. Ma esattamente di cosa si tratta? Tutti ormai utilizziamo il computer e non credo che ci sia bisogno di spiegare a nessuno cos'è. Ecco la Raspberry PI è un computer ma è talmente piccolo che può stare comodamente nel palmo della vostra mano. Miracoli della miniaturizzazione.Raspberry PID'altra parte i nuovi telefoni, i cosiddetti “smartphone”, cosa sono se non dei piccoli computer? Nell'immagine a fianco potete vedere come si presenta questa piccola meraviglia tecnologica. 

 Iniziando dall'alto e procedendo in senso orario abbiamo:


  1. GPIO (General Purpose Input Output) Headers. Sono connettori di ingresso/uscita programmabili. Le tensioni in gioco sono di massimo 3,3 volt. Sarebbe stato meglio avere i classici 5 volt ma va bene così. A cosa servono? Volete accendere una lampadina con la Raspberry? Oppure sapere la lampadina è accesa? Un po' di circuiteria di contorno e potete farlo con i GPIO.

  2. RCA Video Out. Classica uscita a cui si può collegare un televisore o un monitor con ingresso video composito.

  3. JTAG (Join Test Action Group) Headers. E' un standard ormai molto diffuso per effettuare il debug di schede come questa. Debug è una parola che può sgomentare i meno esperti. Tradotto dal tecnichese vuol dire che, collegando una apparecchiatura compatibile a questo morsetto, è possibile capire cosa sta succedendo dentro la scheda stessa.

  4. Audio out. Ebbene sì: la Raspberry ha anche un uscita audio stereo. Peccato che non abbia anche un ingresso audio, ma a questo si può porre rimedio.

  5. Status LEDs. Sono i led che indicano lo stato della scheda come in ogni computer che si rispetti (alimentazione, accesso al disco, scheda di rete ecc.)

  6. USB 2.0. Due porte.

  7. Ethernet Out. E' la presa di rete con i soliti due led, verde e arancione.

  8. CSI (Camera Serial Interface) Connector Camera. E' uno standard sviluppato dalla MIPI Alliance per connettere telecamere compatibili. A dire il vero, non saprei proprio dove trovarne con quelle caratteristiche.

  9. HDMI OUT. Avete letto bene! Con l'apposito cavetto, potete collegare la Raspberry ad un TV o un monitor di ultima generazione con risoluzione Full HD 1920x1080.

  10. Broadcom BCM2835. E' il processore utilizzato da questa scheda. Si tratta di un ARM a 700MHz, non poco considerando lo spazio contenuto.

  11. MICRO USB POWER. La Raspberry va alimentata a 5V con un cavo USB-MicroUSB. Non è necessario avere un alimentatore con uscita USB. Funziona anche con la corrente erogata dalla normale presa USB di un PC.

  12. SD CARD SLOT. Alloggiamento per la scheda SD (quello delle più comuni macchine fotografiche digitali per intenderci) che conterrà il sistema operativo della Raspberry.


Virtualmente è possibile usare qualsiasi sistema operativo ma sul sito (http://www.raspberrypi.org/), nella sezione download, ad oggi 30/03/2013, ne vengono proposti solo quattro:


  1. Raspbian “wheezy”. E' quella consigliata per “partire”. Dispone di una interfaccia grafica molto leggera e funziona piuttosto bene.

  2. Soft-float Debian “wheezy”. Come la precedente ma ottimizzata per l'utilizzo di software come Oracle JVM.

  3. Arch Linux ARM. Si basa sulla distribuzione standard di Arch Linux e non dispone immediatamente di una interfaccia grafica che, se ritenuta opportuna, va scaricata dopo l'installazione.

  4. Risc OS. Si basa su un sistema operativo sviluppato a Cambridge. Questa versione è rilasciata gratuitamente agli utilizzatori della Raspberry PI.


Non avendo la necessità delle particolari ottimizzazioni offerte dalla Soft-float Debian e non essendo ancora disponibile la Risc OS quando ho iniziato il progetto, la scelta si limitava alla “wheezy” o alla Arch Linux. Le ho provate entrambe ma sulla “wheezy” non riuscivo a far girare alcune periferiche che, per contro, giravano benissimo sulla Arch Linux. Forse spendendoci più tempo avrei potuto risolvere il problema ma la scelta della Arch Linux, più compatta e più professionale, mi è sembrata la migliore delle due e l'ho adottata.

Non senza qualche remora però. La “wheezy” si basa sulla Debian che è un sistema operativo che conosco bene in quanto è alla base di Ubuntu, la versione di Linux più utilizzata probabilmente. Di Arch Linux, invece, non ne avevo mai sentito parlare. Per l'installazione dei pacchetti non si usa l'ormai classico apt-get, i servizi vengono gestiti in modo diverso, file di sistema estremamente importanti non ci sono o sono sostituiti da altri a me sconosciuti. La scelta di un sistema operativo è di estrema importanza per lo sviluppo di un progetto e, per un po' di tempo, ho temuto che mi sarei dovuto fermare per qualche strana incompatibilità del software o per l'impossibilità di gestire condizioni particolari. Per fortuna non è stato così grazie anche alla varietà dei pacchetti ed alla documentazione disponibile.

Cosa ho installato? Come? Perché? Nel prossimo paragrafo vedremo in dettaglio la configurazione di questo sistema operativo anche per capire in cosa differisce da quelli più noti.


Arch Linux e configurazione S.O.

Installare il sistema operativo è una cosa molto semplice. Basta seguire le istruzioni sul sito quindi non mi dilungherò su questo argomento. Terminata questa operazione bisogna configurare il sistema.

Di seguito elencherò tutti i punti degni di nota.

Pacman.

Non è il nome del famoso gioco di parecchi anni fa, ma quello del gestore di pacchetti software (Packet Manager) che sostituisce in Arch Linux i più noti apt-get e yum. Che bisogno ci fosse di un nuovo gestore pacchetti non lo so e non me lo voglio chiedere. Il comando, comunque, è piuttosto semplice da utilizzare.

Per installare il pacchetto xyz ad esempio basta digitare:

pacman -S xyz

Per installarlo aggiornando la lista dei pacchetti disponibili sul repository:

pacman -Sy xyz

Per rimuoverlo:

pacman -R xyz

Per fare l'upgrade di tutto il sistema:

pacman -Syu

Queste istruzioni sono più che sufficienti per iniziare. Se si volesse approfondire l'argomento, consiglio la pagina di ArchWiki relativa.

Rete.

Ninobot è pilotato da una interfaccia web che risiede sul server Apache della Raspberry. Sarò più dettagliato in seguito sull'argomento. Adesso volevo solo rimarcare il fatto che tale interfaccia esiste e che in qualche modo deve essere raggiungibile dall'esterno. Come? Via wireless!

Quando Ninobot viene acceso, si collega automaticamente all'hotspot Wi-Fi di casa mia e da questo momento è possibile pilotarlo da qualsiasi computer connesso alla stessa rete. E se volessi controllarlo da casa ma si trovasse dall'altra parte della città o, addirittura, in un'altra città? In questo caso, collego Ninobot ad un telefonino di ultima generazione che dispone del tethering USB (la possibilità di usare il telefonino come modem) e creo una VPN (una sorta di tubo virtuale) fra la Raspberry ed il computer da cui mi voglio connettere. E' una possibile soluzione, ma non è l'unica.

Qualunque sia la soluzione, comunque, ho bisogno di configurare la rete della scheda di controllo e per farlo devo:

  1. installare il pacchetto netcfg (pacman -S netcfg);

  2. installare il pacchetto wireless_tools (pacman -S wireless_tools);

  3. configurare netcfg in modo che parta all'avvio della Raspberry (systemctl enable netcfg.service);

  4. creare un file /etc/network.d/nomemiarete che contiene:

    CONNECTION='wireless'

    DESCRIPTION='A simple WEP encrypted wireless connection'

    INTERFACE='YOURINTERFACE'

    SECURITY='YOURSECURITY'

    ESSID='YOURSID'

    KEY='YOURKEY'

    IP='dhcp'

    # Uncomment this if your ssid is hidden

    #HIDDEN=yes

    ## Uncomment if you are using an ad-hoc connection

    #ADHOC=1

    dove YOURINTERFACE sarà qualcosa del tipo wlan0, YOURSECURITY può essere “wep” o “wpa” (ci sono altri due casi ma più complicati per cui vi rimando al manuale di netcfg), YOURSID è il SID della vostra rete Wi-Fi (lo trovate sulla configurazione del router di casa), YOURKEY è la password che conoscete solo voi (anche questa si ricava dalla configurazione del router). Questa è una delle configurazioni più semplici. Se avete bisogno di qualcosa di più complicato, fate riferimento al manuale.

  5. Modificare il file /etc/conf.d/netcfg dove la linea

    NETWORKS=(last) diventa NETWORKS=(last nomemiarete)


Si noti che al punto 3 ho utilizzato il comando systemctl. Tale comando non è presente su Ubuntu, almeno non di default, quindi anche questa si può considerare una piccola differenza fra le due versioni di Linux. Devo dire tuttavia che systemctl è davvero comodo. Se lanciato senza parametri, fornisce l'elenco dei servizi ed il loro stato, ma non solo.

Per disabilitare un servizio all'avvio del sistema operativo:

systemctl disable nomeservizio.service

Per attivare un servizio:

systemctl start nomeservizio.service

Per fermare un servizio:

systemctl stop nomeservizio.service

Per fare ripartire un servizio:

systemctl restart nomeservizio.service

Per ulteriori informazioni riguardo a questo interessantissimo comando, anche stavolta vi rimando al manuale.

Come ho prima accennato, Ninobot può essere configurato anche per il telecontrollo a lunga distanza. Questo si ottiene senza modifiche eccezionali utilizzando un telefono 3G con tethering. Sul tethering ci sarebbe da parlare a lungo. Vi basti sapere, tuttavia, che si tratta di una tecnica che vi consente di utilizzare il telefono come se fosse un modem collegandolo al PC. Il collegamento può essere fatto in tre modi diversi: con il Wi-Fi, con il Bluetooth oppure, ed è il metodo che ho usato per questo progetto, via USB.

Per completezza, scrivo anche che ho provato ad utilizzare diverse chiavette 3G USB ma senza alcun risultato. Purtroppo Arch Linux sembra non essere compatibile con la maggior parte delle chiavette in circolazione. Ho provato diversi modelli ma non sono riuscito a farne funzionare nemmeno una. Anche la E173 Huawei, molto diffusa, non va in modalità modem. Su alcuni forum consigliano di usare la E173s ma non vorrei acquistarla senza essere prima sicuro di poterla utilizzare.

Detto questo torniamo al tethering. Di cosa abbiamo bisogno?

Di un cellulare con tethering e di un cavetto usb.

Cosa dobbiamo fare? Di seguito spiegherò tutti i passi necessari per effettuare il collegamento. Do per scontato che:

  1. la macchina da cui ci colleghiamo sia una macchina Linux, o Unix più in generale, e su di essa siano installati i seguenti programmi:

  1. il cellulare utilizzato sia Android;

  2. netcat e pppd devono essere installati anche su Arch Linux, se non già presenti, utilizzando la solita procedura (pacman -S openbsd-netcat e pacman -s ppp)

Detto questo, ecco, passo passo, cosa bisogna fare.

  1. Accendere il telefonino (ovviamente) ed attivare la connessione 3G (attenti al credito);

  2. collegare il telefonino alla Raspberry PI utilizzando l'apposito cavetto microUSB-USB;

  3. andare su impostazioni => altre impostazioni => tethering e router wi-fi => tethering USB (dovrebbe apparire il segno di spunta ad indicare che la modalità è attiva);

  4. eseguire su Raspberry ifconfig usb0 up && dhcpcd usb0; si noti che:

  1. Sul PC (connesso ad Internet con indirizzo IP pubblico) da cui si vuole controllare Ninobot lanciare il comando

pppd noauth pty "netcat -l -p 5600" silent 192.168.1.1:192.168.1.2 local

Si noti che:

  1. Eseguire sulla Raspberry PI il comando

pppd noauth pty "netcat <ip_pubblico_pc_di_controllo> 5600" 192.168.1.2:192.168.1.1 local

L'ip pubblico del PC di controllo potrà variare da connessione a connessione in quanto molto spesso tale ip è scelto dinamicamente dal gestore telefonico.

  1. A questo punto sul PC di controllo e sulla Raspberry sarà apparsa una nuova periferica di rete del tipo ppp0 o simile. Sul PC tale periferica avrà l'indirizzo 192.168.1.1 mentre sulla RPI sarà 192.168.1.2. Si è così creata una sorta di VPN (un tubo di collegamento) fra i due punti. Per connettersi al server web di Ninobot (o a qualsiasi altro servizio di rete) dal PC, sarà sufficiente utilizzare l'indirizzo 192.168.1.2 ed il gioco è fatto.

  2. Per diconnettersi basterà killare il processo netcat da uno dei due lati. Sulla RPI sarà anche necessario il comando dhcpcd -x usb0 per interrompere il collegamento tethering con il cellulare.

Scusatemi ma non potevo essere meno tecnico di così.

Rete per Ipcam

La telecamera principale Ipcam può essere collegata alla rete via Ethernet o via Wireless. Io ho preferito la prima soluzione per due motivi.

  1. Avendo collegato la Raspberry via Wireless, rimaneva la porta Ethernet di questa scheda libera;

  2. se anche l'Ipcam fosse stata collegata via Wireless, Ninobot, per forza di cose, avrebbe avuto due indirizzi di rete separati invece che uno solo. La telecamera infatti viene “raggiunta” adesso utilizzando la Raspberry come ponte.

Per realizzare il collegamento è necessaria una configurazione ad hoc. A questo scopo è stato aggiunto il file /etc/network.d/ethernet che contiene

CONNECTION='ethernet'

DESCRIPTION='A basic static ethernet connection using iproute'

INTERFACE='eth0'

IP='static'

ADDR='192.168.100.24'

NETMASK='255.255.255.0'

POST_UP='ip route del 192.168.100.0/24 dev eth0 proto kernel scope link src 192.168.100.24; ip route add 192.168.100.20/32 dev eth0'

e modificata di conseguenza la riga NETWORKS=(... ... ethernet) del file /etc/conf.d/netcfg dove la connessione “ethernet” così creata è stata inserita come ultima nella lista delle reti (networks) da gestire.

Con il file precedente:

  1. imponiamo l'indirizzo statico della telecamera (192.168.100.24);

  2. imponiamo la netmask della telecamera (255.255.255.0);

  3. eseguiamo un apposito script di post-configurazione (POST-UP) che esegue:

a) la cancellazione della “rotta” di default (192.168.100.0) che è quella della rete LAN di tutto il sistema. Purtroppo in questo caso non è quella giusta. La Raspberry non può comunicare con tale rete utilizzando l'interfaccia ethernet in quanto, a tale interfaccia, è collegata solo la telecamera.

b) l'aggiunta della nuova “rotta” (192.168.100.20 che è l'indirizzo statico della telecamera impostato attraverso il server web della telecamera stessa) punto-punto (/32 telecamera-Raspberry) per la device fisica eth0.

In altre parole, con il POST-UP script, si informa la Raspberry che l'unica sua destinazione possibile, via ethernet, è quella che risponde all'indirizzo 192.168.100.20 e cioè la telecamera. In caso contrario tenterà di collegarsi a qualcosa a cui non è fisicamente connessa.

Si ottiene lo stesso risultato eseguendo i seguenti comandi da terminale:

route del -net 192.168.100.0 netmask 255.255.255.0 dev eth0

route add -host 192.168.100.20 dev eth0

Apache

Ninobot si può pilotare attraverso una interfaccia web. Inizialmente il mio obiettivo era quello di realizzare tale interfaccia in modo che fosse compatibile con i browser più comuni. Tale speranza/progetto aveva una sua base di certezze rappresentata dal nuovo standard HTML5 che ho usato pesantemente per risolvere molti problemi specie a livello di connessione. Purtroppo la vita mi ha insegnato che ciò che ci ostiniamo a chiamare standard spesso non è altro che un insieme di “desiderata”, non sempre universalmente gradito, dai contorni poco netti e marcati. In altre parole, come diceva Tanenbaum, “gli standard sono belli perché sono tanti”. Incredibilmente, in questo caso, uno dei browser più utilizzati, cioè Internet Explorer, non supporta tutte le novità di HTML5, le websocket in particolare. Di certo, comunque, non mi strapperò i capelli per questo, no davvero, anche perché ne ho pochi. Non ho mai considerato Internet Explorer un browser, come non ho mai considerato Winzoz un sistema operativo. Firefox e Chrome funzionano a meraviglia e molto probabilmente anche Opera anche se non l'ho mai utilizzato. Con questo chiudo la mia piccola parentesi “tecnico/religiosa”.

Un'interfaccia web presuppone l'utilizzo di un server web. Nel mondo Linux, Apache è quasi una scelta obbligata. Come si fa ad installare su Arch Linux? Molto semplice, basta eseguire:

pacman -S apache

Per abilitare il servizio alla partenza del sistema si esegue:

systemctl enable httpd.service

La DocumentRoot (cioè la directory dove si trovano i file resi disponibili dal server web) è di default /srv/http contrariamente ad Ubuntu e ad altre distribuzioni (di solito /var/www/... ). Il file di configurazione httpd.conf lo troviamo invece sotto la dir /etc/httpd/conf (su Ubuntu ed altre distribuzioni è /etc/apache2 ed è diversa la struttura delle directory). La prima modifica che ho fatto sul tale file è stata cambiare la porta su cui sta in ascolto il server. Dalla classica 80 sono passato alla 25000 (riga Listen <port> su httpd.conf).

Il perché di tale scelta sta nel fatto che avevo necessità di un gruppo di porte contigue su cui mettere in ascolto i vari server e nella zona della porta 80 sono tutte occupate.

La seconda modifica è stata aggiungere, in fondo, le seguenti righe:

ProxyPass /videostream http://192.168.100.20:81/videostream.cgi

ProxyPass /snapshot http://192.168.100.20:81/snapshot.cgi

ProxyPass /camera/ http://192.168.100.20:81/

In questo modo si informa il server web che tutte le richieste fatte a “/snapshot”, ad esempio, vengono dirottare ad un altro server, il 192.168.100.20, sulla porta 81, che è quello della Ipcam.

node.js

Devo fare una premessa. L'installazione di node.js, delle websocket e tutte le modifiche che tale soluzione ha comportato al vecchio codice non erano state preventivate all'inizio di questo progetto. Se così non fosse stato, non avrei perso tempo a scrivere CGI e roba Ajax naturalmente. Aggiungo, senza vergogna, che non sono il pioniere delle nuove tecnologie a tutti i costi. Uso la roba che mi serve, quando mi serve e come mi serve. Se posso ottenere lo stesso risultato usando due metodi completamente diversi, scelgo il più semplice. In questo senso mi ritengo un seguace di Guglielmo di Occam applicando il suo famoso rasoio a tutte le discipline, informatica ed elettronica comprese.

Perché in questo caso non ho potuto farlo? Per spiegarlo devo anticipare qualcosa di cui tratterò in seguito e quindi cercherò di essere molto breve. Per prelevare le immagini dalla IP Cam, utilizzavo Ajax. Stessa cosa per inviare i comandi ai motori di Ninobot. Ajax, per i non addetti ai lavori, è una tecnologia che permette di effettuare delle richieste http da una pagina web e di processare il risultato di tale richiesta all'interno della pagina stessa. Facile, veloce, collaudato. Ma anche connectionless!

Ci sono due modi per fare una richiesta su una rete dati. Pensate alla classica richiesta di una pagina web. Digitate l'indirizzo della pagina sul browser, date “Invio” e dopo qualche secondo, o anche prima se avete una rete veloce, i contenuti da voi richiesti appaiono sul vostro browser sotto forma di testo, immagini, suoni, video ecc. Per fare questo, in modo assolutamente trasparente, il client (il browser) apre una connessione con il server, invia la richiesta sul “tubo dati” appena creato e sullo stesso tubo il server invia la risposta. A questo punto il tubo viene chiuso è il gioco è fatto. Questa è la connessione “connectionless”. Non c'è bisogno di creare un percorso fra stabile fra client e server ma bisogna disporre di un nuovo “tubo” tutte le volte che si richiede una nuova pagina. E' una soluzione che va bene in svariati contesti, soprattutto se la comunicazione client-server avviene all'interno di una rete locale come nel caso in cui voglio pilotare dal PC di casa Ninobot che si trova nell'altra stanza.

Ma cosa succede se Ninobot si trova invece in un'altra città? Rispondo subito alle solite due domande. Cosa ci dovrebbe fare Ninobot in un'altra città? A cosa servirebbe? Praticamente a niente; didatticamente, contemplare una simile evenienza, significa affrontare problemi non indifferenti, quindi significa trovare nuove soluzioni tecniche. Tutto ciò che viene fatto per imparare, alla fine, non è mai davvero inutile!

Detto questo, nella speranza di essere stato chiaro, riprendo il discorso sulle tipologie di connessioni introducendo il concetto di latenza. Di fatto la latenza è un ritardo e misura il tempo che intercorre fra la richiesta di un dato e la disponibilità dello stesso. Ci sono diversi fattori che concorrono alla determinazione di questo valore. E' meglio vedere in concreto la questione. Supponiamo, ad esempio, di esserci collegati con un browser al server web di Ninobot. La prima cosa che viene richiesta dal browser è l'immagine proveniente dalla telecamera. La sequenza, per ogni fotogramma, è più o meno questa:

  1. richiesta del fotogramma al server;

  2. generazione del fotogramma;

  3. invio del fotogramma al browser.

La latenza totale è dalla somma delle singole latenze dei punti 1, 2 e 3. Il tempo per generare un fotogramma è sempre costante, il tempo per trasmettere la richiesta e ricevere la risposta varia a seconda della dimensione del fotogramma e del mezzo trasmissivo utilizzato. Se sono su una rete locale, i tempi di trasmissione e ricezione non sono apprezzabili. Se sono su una rete geografica e sto utilizzando un modem 3G la cosa cambia completamente aspetto. Completamente! E non sono solo le immagini ad essere “rallentate”, ma anche i comandi. Questo significa che lo stop dei motori può arrivare quando Ninobot si è già schiantato contro il muro di fronte.

Come ho già detto, ridurre la latenza in questi casi estremi è stata una delle sfide più interessanti che ho dovuto affrontare in questo progetto. La chiave per la soluzione mi è stata fornita dal nuovo standard HTML5, dalla possibilità, cioè, di poter utilizzare delle comunicazioni “connection oriented” (orientate alla connessione).

Tornando all'esempio precedente, in questo caso non ho più bisogno di cambiare tubo tutte le volte che chiedo un dato nuovo: apro il tubo all'inizio della connessione, lo chiudo quando non mi serve più. I tempi di latenza si abbassano drasticamente.

Questa tecnica non è una novità assoluta. I primi protocolli di comunicazione su Internet (telnet, ftp …) la usavano, poi con l'avvento del World Wide Web si è passati a protocolli come l'http di tipo connectionless, più adatti alla natura delle richieste provenienti da un browser. Le comunicazioni “connection oriented” su HTML5 sono state quindi una riscoperta più che una scoperta, come i pannolini unisex per bambini dopo anni di pannolini per lui e per lei. Il tubo tecnicamente si chiama “socket”, nella sua versione WWW è stato chiamato, con molta fantasia devo dire, “websocket”.

Le implementazioni di websocket si sprecano. Ce ne sono per Java, per C++, per C#, Python, PHP, Perl, Server-Side JavaScript (SSJS). Quest'ultimo, che, come è noto, è un linguaggio di scripting, a sua volta è stato implementato in molti modi diversi (vedi il seguente link). Un po' per la semplicità d'uso, un po' per la semplicità di compilazione, ho deciso di utilizzare node.js che è una particolare implementazione di SSJS.

Come si installa?

Purtroppo non esiste nulla che riguardi node.js nei repository di Arch Linux (almeno io non l'ho trovato) e quindi ho scaricato da qui il codice sorgente e l'ho compilato. Per compilare bisogna avere un compilatore (incredibile ma vero) ma questo può essere installato senza problemi con il solito comando.

pacman -S gcc

dove gcc è il compilatore di cui abbiamo bisogno.

Se non presente deve essere installato anche il programma make

pacman -S make

Infine è necessario installare Python (versione 2.6 o 2.7), un ormai famoso linguaggio di scripting.

pacman -S python

A questo punto, dalla directory dove è stato espanso il pacchetto node.js, si esegue:

./configure

make

make install

Se tutto ha funzionato, dopo un po' di tempo (tanto tempo sulla Raspberry), avremo un ambiente SSJS perfettamente funzionante e pronto all'uso. Non ci resta che installare la libreria delle websocket per node.js. A questo scopo, le precedenti istruzioni make hanno provveduto a compilare ed installare il programma npm (Node Package Manager) che serve proprio a questo scopo.

Il comando è

npm install websocket

ed il gioco è fatto. Troverete, al termine della sua esecuzione, una directory node_modules al cui interno un'altra directory, websocket, conterrà le librerie necessarie per il corretto funzionamento del modulo. Si noti che l'eseguibile node, il server SSJS in altre parole, deve trovarsi allo stesso livello della dir node_modules.

Come funziona?

Far partire un server SSJS è davvero semplice. Se ws.js è il suo codice, il comando

node ws.js

eseguirà il server. I più esperti noteranno che questa è la stessa procedura per eseguire un qualsiasi codice interpretato (<interprete> <codice linguaggio da interpretare>). Niente di nuovo sotto il sole quindi. Node è l'interprete, ws.js è il codice scritto in SSJS, un linguaggio molto simile a JavaScript da cui differisce per le estensioni server di cui è dotato.

E cosa c'è di meglio per capire come funziona un server se non il classico “chat example”? Il codice del server lo trovate qui, quello del client invece è qui. Su Internet ci sono diversi altri esempi ma questo mi sembra uno dei più semplici per iniziare. Chi fosse interessato, poi, può dare un'occhiata al manuale di node.js.

Per Ninobot sono stati utilizzati 3 diversi websocket server. Ciò si è reso necessario al fine di rendere il controllo delle varie device il più possibile parallelo (multitasking). Arch Linux non è un sistema operativo real time, il rapporto causa/effetto, quindi, può subire ritardi imprecisati. Con il multitasking i ritardi non si annullano ma si riducono sensibilmente.

I 3 server suddetti controllano nell'ordine:

  1. l'acquisizione dalla telecamera frontale principale (IP CAM) e l'invio delle immagini al client;

  2. l'invio delle immagini al client acquisite dalla telecamera posteriore (USB CAM – per questioni tecniche l'acquisizione è effettuata da un altro task);

  3. attivazione, disattivazione e velocità dei due motori.

Il funzionamento di questi server è trattato in seguito nel paragrafo relativo al software.

espeak

Non è un “must” ma fa tanta scena!

Come ho già scritto in precedenza, Raspberry ha un'uscita audio e mi sono detto perché non sfruttarla? I modi per farlo sono tanti e io ho utilizzato quello che più si addiceva al progetto in questione: la sintesi vocale. E infatti cos'è un robottino se non può parlare?

Espeak si installa nel solito modo

pacman -S espeak

e si usa molto semplicemente così:

espeak -v it “la frase da sintetizzare”

Se preferiamo farlo parlare in inglese non c'è nemmeno bisogno dei parametri “-v it”. L'elenco delle lingue supportate si può dedurre dal contenuto della directory /usr/share/espeak-data. Per avere un elenco delle altre opzioni supportate, basta digitare il comando

espeak -h

Ninobot utilizza espeak in due modi diversi. Il primo è per segnalare eventuali stati del sistema ma anche guasti, allarmi ecc. Il secondo è attraverso l'interfaccia web. Il “pilota”, attraverso un'apposita maschera, può infatti inserire una frase che Ninobot deve riprodurre. Come dicevo, non necessario ma fa la sua scena. Vedremo più in dettaglio l'interfaccia dedicata a questo scopo in uno dei prossimi paragrafi.

rc.local

Una differenza che ho trovato sostanziale fra Arch Linux ed altre distribuzioni come Ubuntu, è la relativa difficoltà nel lanciare script all'avvio del sistema. Non so davvero se esistono altre soluzioni oltre a quella che ho usato io, di certo è che, se fosse l'unica, è piuttosto complicata e non capisco mai la ragione di rendere complicate le cose semplici.

Su Ubuntu, ed altre distribuzioni, esiste il file rc.local sotto la directory /etc. Quello che bisogna fare è aggiungere gli script di lancio in questo file e, all'avvio, saranno eseguiti tutti nell'ordine in cui sono stati inseriti.

Considerando che al boot del sistema devo far partire 5 server, uno streamer audio e 2 script di controllo (per adesso), si capisce che di quel file, o qualcosa di simile, io ne avevo proprio bisogno. Su Arch Linux però non c'è!

O meglio non c'era. Come ho scoperto, leggendo blog e manuali, è possibile ricreare il comportamento Linux standard aggiungendo il file /usr/lib/systemd/system/rc-local.service con il seguente contenuto:

[Unit]

Description=/etc/rc.local Compatibility

After=network.target


[Service]

Type=oneshot

ExecStart=-/etc/rc.local

TimeoutSec=0

RemainAfterExit=yes


[Install]

WantedBy=multi-user.target


In questo modo, dopo aver fatto partire i servizi di rete, Arch Linux farà partire un nuovo servizio, rc-local.service, che non fa altro che eseguire il contenuto del file /etc/rc.local che deve essere prima reso eseguibile con

chmod 755 /etc/rc.local

Ultimo passo è attivare il nuovo servizio con il comando

systemctl enable rc-local.service

Se fosse utile far partire questo nuovo servizio prima di qualsiasi altra cosa, bisognerà apportare le seguenti modifiche al suddetto file.

Before=reboot.target #Al posto di After

WantedBy=sysinit.target #Al posto di multi-user.target

Il mio rc.local è il seguente:

#!/bin/bash


/usr/local/bin/startSerialServer.sh &

/usr/local/bin/startTestaRete.sh &

/usr/local/bin/startRearView.sh &

/usr/local/bin/startAudio.sh &

/usr/local/bin/startStatistics.sh &

/usr/local/bin/node /usr/local/bin/ws.js > /dev/null 2>&1 &

/usr/local/bin/node /usr/local/bin/ws_frontCam.js > /dev/null 2>&1 &

/usr/local/bin/node /usr/local/bin/ws_RearCam.js > /dev/null 2>&1 &


exit 0

Telecamera posteriore

La telecamera posteriore è di tipo USB, la Logitech C200. E' utilizzata non solo per acquisire immagini ma anche per sopperire alla mancanza del microfono sulla Raspberry. Non ci sono particolari problemi per il gestirla con Arch Linux, ma, come al solito abbiamo bisogno di scaricare i pacchetti giusti che, in questo caso, sono due: streamer e alsa-utils.

Il primo è utilizzato per acquisire le immagini da una periferica video. Tipicamente, quando si collega un “qualcosa” ad un sistema Linux da cui possono essere acquisiti video, il sistema operativo (se riconosce l'oggetto collegato) crea un file che si chiama /dev/video0. Se colleghiamo un'altra periferica, crea il file /dev/video1 e così via. Su Ninobot, nonostante ci siano due telecamere, avremo solo un /dev/video0 in quanto la telecamera principale funziona con un altro principio.

Per acquisire un'immagine (che poi è il nostro caso) dalla C200 (e periferiche simili), si digita il comando

streamer -o nomeImmagine.jpeg -c /dev/video0

Al termine dell'acquisizione, nella directory corrente avremo il file nomeImmagine.jpeg estratto dalla device /dev/video0. Vedremo più in dettaglio come funziona l'intera procedura per questa periferica.

Con il secondo pacchetto, alsa-utils, installeremo diversi tools per la gestione dell'audio. Dei tanti disponibili, io ho utilizzato il solo “arecord”. Questo programma, oltre che registrare l'audio dalle periferiche compatibili, consente di elencare tutti i dispositivi che possono essere utilizzati dal programma stesso.

Digitando il comando

arecord -l

saranno elencati tutti gli input audio disponibili sul sistema separati per “card” e “device”.

Esempio:

**** List of CAPTURE Hardware Devices ****

card 1: U0x46d0x802 [USB Device 0x46d:0x802], device 0: USB Audio [USB Audio]

Subdevices: 0/1

Subdevice #0: subdevice #0

Dall'output si evince che abbiamo solo una “card” (1) con una device (0). Per acquisire l'audio da questa periferica, si utilizza il seguente comando.

arecord -r 16000 -f S16_LE -D hw:1,0 > prova.wav

dove -r è la frequenza di campionamento, -f il formato e -D la device di acquisizione, nel nostro caso la device 0 della card 1. La registrazione sarà salvata nel file prova.wav.

Anche in questo caso, vedremo più in dettaglio in seguito tutto il procedimento di acquisizione.

Software

La seguente immagine mostra l'architettura software del sistema. Per una rapida identificazione degli elementi, sono stati usati colori diversi per le diverse tipologie di oggetti.

  1. ** Client html5 compatibile (Firefox, Chrome, ecc).

  2. ** Network. La rete può essere una LAN, con il Wi-Fi, o una WAN con un modem 3G.

  3. ** Server Web. Nel sistema ne sono presenti 2: il server WEB di Ninobot che gira sulla Raspberry e quello della telecamera IPCAM.

  4. ** Server Websocket. Come già scritto, ce ne sono 3: uno per i motori, uno per la telecamera anteriore ed uno per quella posteriore.

  5. ** Script di shell. Attualmente ne girano 4 sul sistema con funzionalità quasi sempre di servizio eccetto per lo Streamer della Camera Posteriore. Gli altri sono lo streamer audio per trasmettere i suoni ambientali, lo script per la generazione del file delle statistiche e quello per testare le funzionalità di rete

  6. **Un server per le comunicazioni seriali (in C). In effetti, per motivi che vedremo in seguito, tale server è affiancato da un client che non è evidenziato nel disegno. Il blocco relativo rappresenta quindi l'aggregato di client+server. Consente la comunicazione seriale fra la Raspberry e l'Arduino.

  7. **Il disco di sistema. E' chiaro che tutto quello che gira su Ninobot si trova sul disco e che quindi dovrebbe apparire sempre collegato ad ogni blocco funzionale. In questo caso è stato esplicitamente evidenziato perché si comporta come una specie di buffer (un'area dati temporanea) fra il WSS e lo streamer.

  8. **Il firmware della scheda Arduino rappresenta a tutti gli effetti uno dei blocchi software necessari al funzionamento del sistema. Il suo utilizzo fondamentale è pilotare i motori ma fornisce anche una parte delle informazioni di stato per le statistiche. Si interfaccia alla Raspberry attraverso il server per le comunicazioni seriali.

Architettura software


Nei successivi paragrafi, vedremo tutti i codici sorgenti di Ninobot. Si tratta di versioni in divenire, spesso non ottimizzate, ma perfettamente (spero!) funzionanti.


Codice html/css/js

La pagina web di controllo di Ninobot, quella utilizzata per pilotare il robot, è stata scritta interamente in HTML5. Si fa molto uso di JavaScript e di CSS anche se quest'ultima parte va decisamente migliorata ed estesa.

Il codice, ninobot.html, è il seguente.


ninobot.html

<html>
  <head>
    <title> NINOBOT </title>
    <script language="JavaScript" type="text/javascript" src="ajaxcode.js"></script>
    <link rel="stylesheet" type="text/css" href="ninobot.css">
  </head>

  <body onload="load()" bgcolor="blue">

  <div id="hiddenDiv" style="visibility: hidden">
    <audio id="audioObject" type="audio/wave"> </audio>
  </div>

  <script language="Javascript">
    var sStatistics="stat.dat";
    var img;
    img = new Image();
    imgObj = new Image();
    imgR = new Image();
    imgObjR = new Image();
    var engineOutput;
    var imgControl;
    var statusArea;
    var frames=0;
    var swl=window.location.href.split(":",2);
    var wsurl="ws://"+swl[1].substring(2)+":25001";
    var wsurlfc="ws://"+swl[1].substring(2)+":25002";
    var wsurlrc="ws://"+swl[1].substring(2)+":25003";
    var audioLink="http://"+swl[1].substring(2)+":25004";
    var socket;
    var socketfc;
    var socketrc;
    var standardPane;
    var circlePane;
    var circle;
    var audioButton;
    var audioObject;
    var hiddenTable;
    var audioOffImage = "url(images/audioOff.png)";
    var audioOnImage = "url(images/audio.png)";
    var i=0;

    function startCameras()
    {
      socketfc.send('dummy');
      socketrc.send('dummy');
    }

    function statusPolling()
    {
      statusArea.innerHTML="";
      new WgetRequest(sStatistics+"?"+(new Date()).getTime(),getStatus,false);
      setTimeout(statusPolling,1500);
    }

    function load()
    {
      imgObj = document.getElementById('video_stream');
      imgObjR = document.getElementById('video_streamR');
      engineOutput = document.getElementById('engineOutput');
      imgControl = document.getElementById('imgControl');
      statusArea = document.getElementById('statusArea');
      standardPane = document.getElementById('standardPane');
      circlePane = document.getElementById('circlePane');
      circle = document.getElementById('cross');
      hiddenTable = document.getElementById('hiddenTable');
      audioButton = document.getElementById('audioButton');
      audioObject = document.getElementById('audioObject');
      statusPolling();

      socket = new WebSocket(wsurl, "echo-protocol");
      socketfc = new WebSocket(wsurlfc, "echo-protocol");
      socketrc = new WebSocket(wsurlrc, "echo-protocol");

      setTimeout(startCameras,5000);

      socket.addEventListener("open", function(event) {
        //alert("Connected");
        ;
      });

      socket.addEventListener("close", function(event) {
        alert("Engine connnection closed");
      });
 
      socket.addEventListener("message", function(event) {
        engineOutput.innerHTML=event.data;
      });

      socketfc.addEventListener("open", function(event) {
        //alert("Connected");
        ;
      });
 
      socketfc.addEventListener("message", function(event) {
         imgObj.src= "data:image/jpeg;base64,"+event.data;
         imgControl.innerHTML="Frames: "+i;
         i++;
         socketfc.send('dummy');
      });

      socketfc.addEventListener("close", function(event) {
        alert("Foreward camera connnection closed");
      });

      socketrc.addEventListener("open", function(event) {
        //alert("Connected");
        ;
      });
 
      socketrc.addEventListener("message", function(event) {
         imgObjR.src= "data:image/jpeg;base64,"+event.data;
         socketrc.send('dummy');
      });

      socketrc.addEventListener("close", function(event) {
        alert("Rear camera connnection closed");
      });
    }

    var szCmdUrl='camera/decoder_control.cgi?onestep=1';
    function ptzUpSubmit()
    {
      new Image().src = szCmdUrl + "&command=0&" + (new Date()).getTime();
    }
   
    function ptzDownSubmit()
    {
      new Image().src = szCmdUrl + "&command=2&" + (new Date()).getTime();
    }
   
    function ptzLeftSubmit()
    {
      new Image().src = szCmdUrl + "&command=4&" + (new Date()).getTime();
    }
   
    function ptzRightSubmit()
    {
      new Image().src = szCmdUrl + "&command=6&" + (new Date()).getTime();
    }
    function OnSwitchOff()
    {
      new Image().src =szCmdUrl + "&command=94&" + (new Date()).getTime();
    }
    function OnSwitchOn()
    {
      new Image().src = szCmdUrl + "&command=95&" + (new Date()).getTime();
    }

    function getStatus(data)
    {
      if(data!="") {
        statusArea.innerHTML=data;
      }
    }

    function styleSelector(value)
    {
      if(value) {
        standardPane.style.visibility="visible";
        circlePane.style.visibility="hidden";
      } else {
        standardPane.style.visibility="hidden";
        circlePane.style.visibility="visible";
      }
    }

    function point_it(event)
    {
      xpos=event.pageX-circle.offsetLeft-circlePane.offsetLeft;
      ypos=event.pageY-circle.offsetTop-circlePane.offsetTop;
      if(event.shiftKey) {
        slp=.15;
      } else {
        slp=0;
      }
      socket.send('CMOTOR#'+xpos+'#'+ypos+'#'+slp)
    }

    function audioOn()
    {
      audioButton.style.backgroundImage=audioOnImage;
      audioButton.onclick=function (){audioOff();};
      audioObject.setAttribute('src', audioLink);
      audioObject.play();
      hiddenTable.style.visibility="visible";
    }

    function audioOff() {
      audioButton.style.backgroundImage=audioOffImage;
      audioButton.onclick=function (){audioOn();};
      audioObject.pause();
      audioObject.setAttribute('src', '');
      hiddenTable.style.visibility="hidden";
    }

    function volumePiu() {
      if(audioObject.volume+.1<=1) {
        audioObject.volume=audioObject.volume+.1;
      }
    }

    function volumeMeno() {
      if(audioObject.volume-.1>=0) {
        audioObject.volume=audioObject.volume-.1;
      }
    }

  </script>

    <img src="images/ninobot.png">
    <hr>
    <table>
      <tr>
       <td><img src="images/red_bl.gif"></td>
       <td rowspan="3">
         <table border=3><tr><td>
         <img src="camera/snapshot.cgi" id="video_stream" height="240" width="320">
         <img id="video_streamR" height="120" width="160">
         </td></tr></table>
       </td>
       <td></td>
       <td><input name="btnUP" type="button" class=button id="btnUP" onClick="ptzUpSubmit()" style="background:url(images/tvc-up.png)"/></td>
       <td></td>
      </tr>
      <tr>
       <td></td>
       <td><input name="btnLeft" type="button" class=button id="btnLeft" onClick="ptzLeftSubmit()" style="background:url(images/tvc-left.png)"/></td>
       <td></td>
       <td><input name="btnRight" type="button" class=button id="btnRight" onClick="ptzRightSubmit()" style="background:url(images/tvc-right.png)"/></td>
      </tr>
      <tr>
       <td></td>
       <td></td>
       <td><input name="btnDown" type="button" class=button id="btnDown" onClick="ptzDownSubmit()" style="background:url(images/tvc-down.png)"/></td>
       <td></td>
      </tr>
      <tr>
        <td></td>
        <td><font color="yellow"><center><b id='imgControl'></b></center></font></td>
        <td></td>
      </tr>
      <tr>
        <td></td>
        <td>
          <center>
          <div id="standardPane"><table>
            <tr>
             <td></td>
             <td><input type="button" class="button" onClick="socket.send('MOTOR#100#0#0#0#.15')" style="background:url(images/for-left.png)"></td>
             <td><input type="button" class="button" onClick="socket.send('MOTOR#100#0#100#0#0')" style="background:url(images/forward.png)"></td>
             <td><input type="button" class="button" onClick="socket.send('MOTOR#0#0#100#0#.15')" style="background:url(images/for-right.png)"></td>
             <td></td>
             <td>&nbsp;&nbsp;&nbsp;&nbsp;</td>
             <td></td>
             <td></td>
            </tr>
            <tr>
             <td><input type="button" class="button" onClick="socket.send('MOTOR#100#0#100#1#0')" style="background:url(images/round-left.png)"></td>
             <td><input type="button" class="button" onClick="socket.send('MOTOR#100#0#0#0#0')" style="background:url(images/left.png)"></td>
             <td><input type="button" class="button" onClick="socket.send('MOTOR#0#0#0#0#0')" style="background:url(images/stop.png)"></td>
             <td><input type="button" class="button" onClick="socket.send('MOTOR#0#0#100#0#0')" style="background:url(images/right.png)"></td>
             <td><input type="button" class="button" onClick="socket.send('MOTOR#100#1#100#0#0')" style="background:url(images/round-right.png)"></td>
             <td></td>
             <td></td>
             <td></td>
            </tr>
            <tr>
             <td></td>
             <td><input type="button" class="button" onClick="socket.send('MOTOR#100#1#0#0#.15')" style="background:url(images/rev-left.png)"></td>
             <td><input type="button" class="button" onClick="socket.send('MOTOR#100#1#100#1#0')" style="background:url(images/reverse.png)"></td>
             <td><input type="button" class="button" onClick="socket.send('MOTOR#0#0#100#1#.15')" style="background:url(images/rev-right.png)"></td>
             <td></td>
             <td></td>
             <td></td>
             <td></td>
            </tr>
          </table></div>
          <div id="circlePane" style="visibility:hidden;position:absolute;left:133;top:400"><table>
            <tr>
             <td></td>
             <td></td>
             <td></td>
             <td></td>
             <td></td>
             <td></td>
             <td></td>
             <td></td>
            </tr>
            <tr>
             <td></td>
             <td></td>
             <td> <img src="images/circle.png" id="cross" style="position:relative;" onClick="point_it(event)"> </td>
             <td></td>
             <td></td>
             <td></td>
             <td></td>
             <td></td>
            </tr>
            <tr>
             <td></td>
             <td></td>
             <td></td>
             <td></td>
             <td></td>
             <td></td>
             <td></td>
             <td></td>
            </tr>
          </table></div>
          </center>
        </td>
        <td>
        <font color="yellow">Std:</font><input type="checkbox" checked="true" onClick="styleSelector(this.checked)"></td>
        <td><input type="button" class="button" onClick="window.open('audiobot.html','audiobot','height=300,width=350,
 scrollbars=no,titlebar=no,toolbar=no,location=no')" style="background:url(images/audio.png)"></td>
      </tr>
      <tr>
        <td>&nbsp;</td><td></td><td></td><td></td>
      </tr>
      <tr>
        <td>&nbsp;</td><td></td><td><input id="audioButton" type="button" class="button" onClick="audioOn()" style="background:url(images/audioOff.png)"></td><td>
        <table id="hiddenTable" style="visibility: hidden">
          <tr><td><input id="audioButton" type="button" class="halfButton" onClick="volumePiu()" style="background:url(images/volPiu.png)"></td></tr>
          <tr><td><input id="audioButton" type="button" class="halfButton" onClick="volumeMeno()" style="background:url(images/volMeno.png)"></td></tr>
        </table>
      </td>
      </tr>
      <tr>
        <td>&nbsp;</td><td></td><td></td><td></td>
      </tr>
      <tr>
        <td>&nbsp;</td><td></td><td></td><td></td>
      </tr>
      <tr>
        <td>&nbsp;</td><td></td><td></td><td></td>
      </tr>
      <tr>
        <td colspan="4" align="left"><font color="yellow"><b id='engineOutput'></b></font></td>
      </tr>
    </table>
   <hr>
   <font color="yellow"><b id='statusArea'></b></font>
  </body>
</html>


Notiamo subito l'utilizzo di due file esterni: ajaxcode.js e ninobot.css. Il primo è una semplice libreria che ho scritto per utilizzare Ajax e che, molto probabilmente, in futuro sostituirò con un altro WSS, il secondo è un piccolo file per la gestione degli stili. Troppo piccolo a dire la verità. Anche questo, in futuro, sarà modificato in modo da uniformare la gestione degli stili per adesso utilizzata spesso direttamente all'interno del file html.

L'oggetto con id=audioObject è nascosto dentro un div ed è utilizzato per trasmettere l'audio ambientale.

Segue tutta la parte JavaScript, con la dichiarazione degli oggetti e delle porte utilizzate dai WSS e dallo streamer audio.

La funzione load(), associata all'apertura della pagina, inizializza tutti gli oggetti, le websocket, le funzioni di controllo ed acquisizione delle telecamere, la gestione delle statistiche. Seguono le funzioni ptz* per muovere la telecamera principale (pan e tilt). Tali funzionalità sono messe a disposizione dal web server della telecamera stessa. Abbiamo infine le funzioni per la gestione fine dei motori (point_it) e per l'audio ambientale.

La rimanente parte del file è html puro e definisce la struttura interna della pagina web.

Quest'ultima si presenta come si vede nella figura seguente.

Le aree di cui si compone la pagina sono diverse sia per dimensione che per funzionalità.

  1. In alto c'è l'intestazione della pagina “NINOBOT”. Attualmente è l'unica area statica e non interattiva.

  2. Alla sinistra della foto più grande c'è l'area allarmi. Solo per prova è presente un led rosso lampeggiante. Sarà utilizzata per fornire i seguenti allarmi:

  1. Output telecamere. L'immagine più grande è fornita dalla telecamera anteriore, quella più piccola dalla posteriore. Sotto sono indicati i frame ricevuti dalla sola telecamera anteriore. Il numero di frame per secondo varia per tipo di connessione e per telecamera. Di solito, su rete LAN, 10 frame/sec per la IPCAM sono piuttosto normali, ma scendono drasticamente, su rete WAN, a 2 o anche meno. Per la telecamera posteriore potremmo avere 2 frame/sec in LAN ma, spesso, lo streamer impiega più tempo del dovuto ad acquisire l'immagine e la velocità si riduce in modo sensibile. A proposito: quello nell'immagine grande sono io. Che meraviglia, vero?

  2. Alla destra delle immagini acquisite dalle telecamere, c'è un'area con 4 pulsanti. Questi servono per attivare le funzioni di brandeggio (pan e tilt) della telecamera principale. Di default l'inquadratura è centrata secondo la direzione di marcia di Ninobot.

  3. Sotto l'indicazione dei frame ricevuti c'è l'area di controllo ed è costituita da 11 pulsanti. Dall'alto in basso e da sinistra a destra abbiamo:

  1. Tipo di visualizzazione. Di default è presente il segno di spunta su “Std” (Standard). In questo stato è visibile la pulsantiera di controllo del punto precedente. Togliendo il segno di spunta, la pulsantiera viene sostituita dalla “circonferenza di controllo”. Tornerò fra breve sull'argomento.

  2. Pulsante “Dici”. Come ho già scritto in precedenza, sulla Raspberry è stato installato il pacchetto “espeak”. Questo sintetizzatore vocale si attiva in automatico quando Ninobot deve fornire qualche informazione di servizio ma si può attivare anche attraverso l'interfaccia Web. Selezionando questo pulsante, infatti, si apre la pagina “AUDIOBOT” che si presenta come segue.


E' una pagina molto semplice con un text box dove si può digitare una frase ed un tasto “Dici” che invia tale frase al server ed infine al sintetizzatore vocale. Il codice della pagina è riportato nella seguente tabella.

audiobot.html

<html>
  <head>
    <title> AUDIOBOT </title>
    <script language="JavaScript" type="text/javascript" src="ajaxcode.js"></script>
    <style>
      .button2
      {
        width:52;
        height:30;
      }
    </style>
  </head>
 
  <body onload="load()" bgcolor="blue">

  <script language="Javascript">
    var sEspeakCommand="cgi-bin/espeak.cgi?say=";
    var stts;
   
    function load()
    {
      stts = document.getElementById('tts');
    }

    function espeak()
    {
      new WgetRequest(sEspeakCommand+stts.value+"&"+(new Date()).getTime(),getData,false);
      //alert(sEspeakCommand+stts.value+"&"+(new Date()).getTime());
    }

    function getData(data)
    {
      engineOutput.innerHTML=data;
    }

  </script>

    <table>
      <tr>
       <td>
         <input type="button" class="button2" onClick="espeak()" value="Dici">
       </td>
       <td>
         <input type="textbox" id="tts">
       </td>
      </tr>
    </table>
  </body>
</html>

Attualmente il funzionamento si basa su uno script cgi, espeak.cgi, richiamato dalla pagina AUDIOBOT.

espeak.cgi

#!/bin/bash

function getValue
{
  for REQ in $REQUESTS
  do
    CHIAVE=`echo $REQ | awk -F \= '{print $1}'`
    if [ X$CHIAVE = X$1 ]
    then
      echo $REQ | awk -F \= '{print $2}'
      break
    fi
  done
}

REQUESTS=`echo $REQUEST_URI | awk -F \? '{print $2}'`
REQUESTS=`echo ${REQUESTS//\&/ }`

echo Content-type: text/html
echo ""

STTS=`getValue say`

sudo /usr/bin/espeak -v it "${STTS//\%20/ }"

Si tratta di uno script bash che semplicemente legge la frase digitata sul text box della pagina web e la passa, dopo un'opportuna elaborazione, al programma espeak. L'elaborazione di cui prima parlavo non è ancora completa: manca la corretta gestione di tutte le lettere accentate.

  1. Pulsante per attivare l'audio ambientale. Ebbene sì: l'icona è la stessa del pulsante “Dici”; la cambierò quanto prima. Tale attivazione comporta anche una modifica a livello di aspetto della pagina. Vedremo in seguito come e perché cambia.

  2. Area stato e statistiche. Sono 5 le informazioni che vengono fornite

startStatistics.sh


#!/bin/bash

DIR=/srv/http/images
FILEOUT=/srv/http/stat.dat
FILETMP=/tmp/stat.tmp

control_c()
# run if user hits control-c
{
  exit
}
 
# trap keyboard interrupt (control-c)
trap control_c SIGINT SIGTERM SIGKILL

sleep 15

while true
do
  VA=`serialIO "SA_SV#"`
  AMP=`echo $VA | awk -F \# '{print $1}'`
  AMP=`echo $AMP | awk -F = '{print $2}'`
  VOLT=`echo $VA | awk -F \# '{print $2}'`
  VOLT=`echo $VOLT | awk -F = '{print $2}'`
  VOLT="$(echo "$VOLT*0.015" | bc)"
  AMP="$(echo "1.4+(438-$AMP)*0.1" | bc)"
  echo -n "Volt=${VOLT}&nbsp;&nbsp;&nbsp;Ampere=${AMP}&nbsp;&nbsp;&nbsp;" > $FILETMP
  iwconfig wlan0 | grep Link | awk '{print $1 " " $2 "&nbsp;&nbsp;&nbsp;" $3 " " $4 "&nbsp;&nbsp;&nbsp;" $5 " " $6}' >> $FILETMP
  cp $FILETMP $FILEOUT
  sleep 2
done


Questo script, come diversi altri del sistema basati su un loop infinito, definiscono una funzione per terminare il processo sulla combinazione dei tasti ctrl+c. Tale comportamento è utile quando lo script viene lanciato da linea di comando, cioè in fase di test.

Una sleep di 15 secondi evita che la prima richiesta al server seriale venga effettuata prima dell'avvio del server stesso.

Il primo comando all'interno del while è proprio la richiesta suddetta: serialIO “SA_SV#”. Vedremo più in dettaglio in seguito il significato di tale istruzione. Per ora basti sapere che esso ci fornisce in output una stringa che contiene due valori compresi fra 0 e 1023 (Esempio: “OK: A=409#OK: V=787#”); il primo inversamente proporzionale alla corrente erogata dalla batteria (A=409), il secondo direttamente proporzionale alla tensione ai presente ai suoi capi (V=787). “OK” sta ad indicare che la richiesta ha avuto successo e che il data fornito è da considerarsi valido.

I due valori devono essere normalizzati, cioè trasformati nell'esatta indicazione di tensione e corrente. Il metodo seguito è assolutamente empirico e dipende dall'elettronica usata. L'Arduino possiede 6 ingressi analogici ai quali è possibile applicare una tensione compresa fra 0 e 5V. Questa tensione potrà essere letta con apposite istruzioni e sarà indicata con un valore compreso fra 0 e 1023. Trattandosi di una trasformazione lineare, se 0 corrisponde a 0V e 1023 a 5V, per una tensione di 2,5V avremo un valore pari a circa 512.

La tensione di una batteria al piombo è di 13,8V quando è carica. Si considera scarica, invece, quando tale tensione scende a 11V o meno. In entrambi i casi non si può collegare direttamente la batteria all'ingresso analogico dell'Arduino perché, se non bruciamo l'ingresso stesso, otterremo sempre il valore 1023. Per ottenere una tensione leggibile, si è utilizzato quindi un partitore (si veda il paragrafo relativo all'HW) in modo tale da avere un punto del circuito in cui esiste una tensione compresa fra 0 e 5V proporzionale a quella presente ai capi della batteria.

La costante 0,015 (vedi script) è stata calcolata empiricamente con il seguente metodo. Detto LA il valore (>=0 e <=1023) per una tensione di batteria pari a VA ed essendo 0 il valore per un tensione pari a 0V, si può dire che l'incremento di una unità nel valore letto dall'Arduino è pari a VA/LA volt. Questa è la costante che moltiplicata per il valore letto fornisce infine la tensione.

Per il calcolo della corrente assorbita la questione si complica anche circuitalmente. La legge di Ohm ci dice che V=R*I da cui segue I=V/R. Conoscendo quindi il valore di R, posso determinare I leggendo una tensione e questo ci riporta al caso precedente. La resistenza R, di valore molto piccolo ma di potenza molto elevata, deve essere posta in serie rispetto al carico in modo tale che la tensione ai suoi capi sia in qualche modo proporzionale alla corrente assorbita dal carico stesso. Per motivi tecnici, il modo più semplice per misurare questa tensione è misurare la differenza rispetto a quella totale ed è proprio questa differenza, che, con l'opportuno partitore, viene fornita ad un altro ingresso analogico dell'Arduino. Ci troviamo quindi nella condizione in cui ad un valore alto corrisponde un assorbimento basso e viceversa. La formula proposta nello script tiene in considerazione questa proporzionalità inversa ma non ancora la tensione effettiva della batteria e questo produce valutazioni errate. Per adesso, quindi, non la spiegherò ma mi riprometto di farlo non appena l'avrò perfezionata.

Il paragrafo sarebbe concluso ma i precedenti punti 6 e 8 meritano un ulteriore approfondimento. Immaginiamo, infatti, di togliere il segno di spunta (punto 6) e di cliccare sull'icona (punto 8).

Il layout della pagina cambia decisamente come si vede nella seguente figura.

Iniziamo ad esaminare la modifica meno appariscente, quella che segue dal punto 8. Adesso l'icona è cambiata e sono apparsi altri due pulsanti, un “+” e un “-”. Da questo momento l'audio ambientale, catturato dal microfono della telecamera posteriore, sarà udibile attraverso il browser web. Come si può intuire, i pulsanti “+” e “-” servono per regolare il volume dell'audio. Cliccando sull'icona posta alla loro sinistra, l'audio sarà escluso e si ritornerà nella situazione precedente.

Lo streamer audio è realizzato utilizzando il semplice script che segue.

startAudio.sh

#!/bin/bash

control_c()
# run if user hits control-c
{
  exit
}
 
# trap keyboard interrupt (control-c)
trap control_c SIGINT SIGTERM SIGKILL

while true
do
  { echo -e 'HTTP/1.1 200 OK\r\nContent-Type: audio/wave\r\n';
    arecord -r 16000 -f S16_LE -D hw:1,0 2> /dev/null ;
  } | netcat -l -p 25004 > /dev/null 2>&1 ;
done

E' uno script molto semplice ma anche molto efficace. Avevo provato ad utilizzare per lo stesso scopo ffmpeg ed altri streamer ma, al di là del fatto che si tratta di programmi che consumano parecchie risorse, sembra che non funzionino molto bene con il nuovo tag “audio” di HTML5.

Nel paragrafo “Telecamera posteriore”, nel quale spiegavo la configurazione di questa telecamera appunto, avevo già parlato del comando “arecord” e dei parametri che servono per farlo funzionare. Riepilogo brevemente le cose essenziali che riguardano lo script di cui sopra.

“Arecord” acquisisce da una periferica data (hw:1,0) uno stream audio in formato wav. Questo viene reso disponibile sulla porta 25004 da netcat di cui ho già parlato. Perché tutto funzioni, prima di fornire tale stream, il tag “audio” ha bisogno non solo dell'indirizzo dove trovare lo stream ma anche del suo tipo, “audio/wave” nel nostro caso. L'header “HTTP/1.1 200 OK\r\nContent-Type: audio/wave\“ viene fornito tutte le volte che ci si connette ed ecco spiegato anche il “while” infinito presente nello script.

Come si può notare, nella figura precedente sono scomparsi tutti i pulsanti di controllo ed al loro posto è apparsa una circonferenza. Si tratta di un'area “attiva” nel senso che è possibile cliccare su un qualsiasi punto interno alla circonferenza stessa per determinare il movimento di Ninobot. Secondo quale regola? Semplice.

Per capire meglio come funziona, nell'immagine seguente è stata riproposta l'area di intervento con alcune aggiunte, in particolare le due rette nere che si incrociano al centro. Una è stata indicata con la lettera S (sinistra) e l'altra con la lettera D (destra). Cosa succede se clicchiamo nel punto indicato con P1?

Il sistema calcola la proiezione di questo punto sulle due rette date ottenendo in questo caso due valori P1s e P1d. La circonferenza in questione ha raggio 100 pixel e quindi P1s<=100 e P1d<=100. Questi valori rappresenteranno la velocità rispettivamente del motore sinistro e del motore destro. Nel caso del punto P1 si considera P1s>0 e P1d>0 in quanto la parte più in alto delle rette viene considerata positiva. Cosa succede se clicchiamo nel punto P2? In questo caso le proiezioni sono P2s e P2d ma, mentre P2d>0, per l'altra retta avremo P2s<0. In termini moto, le componenti si traducono nel motore destro che si attiva a velocità P2d e nel motore sinistro che si attiva a velocità P2s ma applicando anche una inversione di polarità. In altre parole, Ninobot inizierà a ruotare su se stesso.


Non è difficile immaginare tutte le possibili combinazioni. Se si clicca vicino al punto estremo superiore, entrambi i motori verranno attivati alla massima velocità e Ninobot andrà dritto, se si clicca più vicino al centro la velocità diminuirà, fino ad annullarsi se si clicca nella zona centrale (quella blu). La marcia indietro si attiva cliccando vicino al punto estremo inferiore.

In questo modo, oltre che dirigere Ninobot, sarà possibile anche imporre la sua velocità, cosa che non si poteva fare con il controllo a pulsanti.

Per ragioni tecniche al di sotto di una velocità pari a 20 (vedremo in dettaglio cosa vuole dire imporre una certa velocità), Ninobot si blocca. In altre parole, l'area centrale, delimitata da una circonferenza di 20 pixel di raggio, equivale al pulsante di stop.

Interfaccia seriale

La gestione dell'interfaccia seriale, come in parte evidenziato dallo schema software, passa attraverso diversi moduli. Riassumo brevemente la sequenza.

  1. L'accesso alla seriale si ottiene utilizzando il programma serialIO. Lo utilizzano ad esempio lo script startStatistics.sh ed il server “WSS Motori” (file ws.js, vedi in seguito).

  2. Il programma serialIO di fatto non accede direttamente alla porta seriale della Raspberry ma ad una pipe, una sorta di tubo di comunicazione, all'altro capo della quale c'è serialServer.

  3. serialServer, al suo avvio, apre e tiene aperta la porta seriale sulla quale invia i messaggi prelevati dalla pipe. Non è possibile comunicare direttamente con la seriale in quanto aprire e chiudere tale porta comporta un dispendio di tempo notevole. Si preferisce quindi tenerla aperta (serialServer parte all'avvio del sistema e rimane sempre attivo) e comunicare con essa attraverso serialIO (che rimane attivo solo per l'invio e la ricezione dei dati).

  4. I dati inviati sulla seriale vengono presi in carico dal firmware caricato sull'Arduino che svolge le funzioni di basso livello (attivazione degli output PWM e lettura degli input analogici).

serialIO

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>

#define FIFO_NAME_CS "/usr/local/etc/serial_server_cs"
#define FIFO_NAME_SC "/usr/local/etc/serial_server_sc"
#define FILE_LOCK "/tmp/fs_lck"
#define S_LEN 256
#define B_LEN 256

#define FILE_LOG "/tmp/serialIO.log"
FILE* fileLog=NULL;

void printLog(char* data)
{
   time_t ltime;
   struct tm ltm;

   if(fileLog==NULL) {
     fileLog = fopen (FILE_LOG,"a");
   }

   time(&ltime);
   ltm=*localtime(&ltime);
   fprintf(fileLog,"%d/%d/%d %d:%d:%d - >%s<\n", ltm.tm_mday, ltm.tm_mon+1, ltm.tm_year-100, ltm.tm_hour, ltm.tm_min, ltm.tm_sec, data);
}

int main(int argc, char** argv)
{
    int num, fdcs, fdsc, fl=-1, ret=-1;
    char c=0;
    char s[S_LEN];
    char buffer[B_LEN];
    int counter=0;
    int res, resc=0;

    if(argc!=2)
     {
       fprintf(stderr,"Parameters number error (message)\n");
       exit(-1);
     }

    while(fl<0) {
      fl=open(FILE_LOCK,O_EXCL|O_CREAT);
    }
   

    printLog(argv[1]);
    fdcs = open(FIFO_NAME_CS, O_WRONLY);
    num = write(fdcs, argv[1], strlen(argv[1])+1);
    close(fdcs);

    fdsc = open(FIFO_NAME_SC, O_RDONLY);

    if(fdsc<0) exit;
    while ((num = read(fdsc, s, S_LEN)) > 0) {
      strcat(buffer,s);
    }
    close(fdcs);

    strcat(buffer,&c);
    printf("%s\n", buffer);

    close(fdsc);
    close(fl);

    printLog(buffer);

    while(ret<0) {
      ret=unlink(FILE_LOCK);
    }
   
    return 0;
}

Inizio subito col dire che le pipe utilizzate sono 2; una per la comunicazione dal client verso il server (serialIO => serialServer) ed una per quella dal server verso il client (serialServer => serialIO). Al fine di garantire l'accesso esclusivo ad entrambe le pipe, si è predisposto un file di lock che viene creato prima di accedere all'area critica e viene cancellato subito dopo. In questo modo, un secondo task serialIO dovrà aspettare la fine del primo per poter comunicare con il server.

Il programma legge da linea di comando il messaggio da inviare alla seriale e lo scrive sulla pipe CS quindi apre la pipe SC e rimane in attesa di una risposta. Il protocollo implementato garantisce che una risposta c'è sempre anche se negativa.

Si noti che le due pipe vanno create fisicamente prima di lanciare il programma con i seguenti comandi:


mkfifo /usr/local/etc/serial_server_cs

mkfifo /usr/local/etc/serial_server_sc

serverSerial

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <time.h>

#define FIFO_NAME_CS "/usr/local/etc/serial_server_cs"
#define FIFO_NAME_SC "/usr/local/etc/serial_server_sc"
#define S_LEN 256
#define B_LEN 1024
 
#define FILE_LOG "/tmp/serialServer.log"
FILE* fileLog=NULL;

void printLog(char* data)
{
   time_t ltime;
   struct tm ltm;

   if(fileLog==NULL) {
     fileLog = fopen (FILE_LOG,"a");
   }

   time(&ltime);
   ltm=*localtime(&ltime);
   fprintf(fileLog,"%d/%d/%d %d:%d:%d - >%s<\n", ltm.tm_mday, ltm.tm_mon+1, ltm.
tm_year-100, ltm.tm_hour, ltm.tm_min, ltm.tm_sec, data);
}

void sendMessageOnSerial(int tty_fd, const char* msg)
{
   int i=0;
   int len;
 
   len=strlen(msg);

   for (i=0;i<len;i++)
   {
     write(tty_fd,&msg[i],1);
     usleep(2000);
   }
   //printf("SENT: %s\n",msg);
}

int readMessageFromSerialToPipe(int tty_fd, int fd)
{
  char c=0;
  char buffer[256];
  int counter=0;

  while(c!='\n') {
    if(read(tty_fd,&c,1)>0) {
      buffer[counter]=c;
      counter++;
      //printf("%c (%d)\n",c,c);
    }
  }

  buffer[counter]='\0';
  write(fd,buffer,counter);
  printLog(buffer);
  //printf(">>>%s<<<%d\n",buffer,counter);
  //return strlen(buffer);
  return counter;
}

int main(int argc,char** argv)
{
        struct termios tio;
        int tty_fd;
        char s[S_LEN];
        char buffer[B_LEN];
        int num, fdcs, fdsc, delay=0;

        if(argc!=2) {
          fprintf(stderr,"Usage: %s <device> \n",argv[0]);
          exit(-1);
        }

        unsigned char c='D';
 
        memset(&tio,0,sizeof(tio));
        tio.c_iflag=0;
        tio.c_oflag=0;
        tio.c_cflag=CS8|CREAD|CLOCAL;           // 8n1, see termios.h for more information
        tio.c_lflag=0;
        tio.c_cc[VMIN]=1;
        tio.c_cc[VTIME]=5;
 
        tty_fd=open(argv[1], O_RDWR | O_NONBLOCK);     
        cfsetospeed(&tio,B9600);            // 9600 baud
        cfsetispeed(&tio,B9600);            // 9600 baud
 
        tcsetattr(tty_fd,TCSANOW,&tio);

        while (1)
        {
          fdcs = open(FIFO_NAME_CS, O_RDONLY);
          if(fdcs<0) exit;
          memset(buffer,0,B_LEN);
          while ((num = read(fdcs, s, S_LEN)) > 0) {
            strcat(buffer,s);
          }
          close(fdcs);
          //printf("Ho ricevuto >%s<\n",buffer);
          sendMessageOnSerial(tty_fd,buffer);
          fdsc = open(FIFO_NAME_SC, O_WRONLY);
          delay=readMessageFromSerialToPipe(tty_fd,fdsc);
          close(fdsc);
        }
 
        close(tty_fd);
 
        return EXIT_SUCCESS;
}

serverSerial viene lanciato all'avvio del sistema e stabilisce una connessione permanente con la seriale (USB) connessa all'Arduino. Chiaramente dialoga anche con il serialIO che abbiamo visto in precedenza attraverso le due pipe CS ed SC. Dopo la dichiarazione delle variabili e la lettura dei parametri di ingresso (in questo caso la device USB), il programma si occupa della configurazione della porta seriale. Si noti che vengono definite sia la velocità di input che di output a 9600 baud. Non dovrebbe essere un problema aumentare queste velocità sia lato Raspberry che lato Arduino. Le prove, tuttavia, le ho effettuate solo utilizzando tale valore ed anche tutta la letteratura relativa si ostina a riportare i 9600 baud. Non è una ragione sufficiente, vero, ma per adesso, dato che funziona tutto, non intendo modificare nulla anche perché, per me, non è affatto un parametro critico.

All'interno del while (infinito), per prima cosa troviamo l'apertura della pipe CS. Il server si predispone cioè a ricevere dati dal serialIO. L'apertura è bloccante nel senso che, fino a quando non vengono ricevuti dei dati da serialIO, il programma rimane fermo sulla open. I dati ricevuti sono processati dalla while interna e quindi inviati sulla seriale dalla funzione sendMessageOnSerial.

Si noti, nella suddetta funzione, la usleep(2000) fra l'invio di un carattere ed un altro che equivale ad un intervallo di circa 2 millisecondi. Questo si è reso necessario per il fatto che, dall'altra parte, i caratteri ricevuti non possono essere processati tutti e subito. Se non mettessi un ritardo, alcuni caratteri andrebbero perduti perché, prima di finire di processare l'ennesimo, arriverebbe l'ennesimo + 1 che non potrebbe essere gestito.

Il messaggio di ritorno letto dalla seriale viene inviato sulla pipe SC dalla funzione readMessageFromSerialToPipe. In questa funzione il loop di lettura continua fino a quando non viene ricevuto un carattere “\n”, fine riga, previsto dal protocollo di comunicazione che ho implementato fra Raspberry ed Arduino. Solo allora l'intero messaggio viene trasferito sulla pipe e quindi viene chiusa. Il ciclo riparte dalla open iniziale in attesa di un nuovo messaggio sulla pipe di ingresso.

Ninobot (Firmware su Arduino)

/*
 Ninobot
 
 Programma di comando/controllo Ninobot
 
 */

const int MOTORE_SINISTRO=9;
const int MOTORE_DESTRO=10;
const int A_SENSOR=A0;
const int V_SENSOR=A1;
const int FINE_COMANDO='_';
const int FINE_STRINGA='#';

int sensorValue=0;
int inChar;
String buffer="";

void setup()  {
  // declare pin in output:
  pinMode(MOTORE_SINISTRO, OUTPUT);
  pinMode(MOTORE_DESTRO, OUTPUT);
  Serial.begin(9600);
}

void stringAnalysis(String str) {
  int value;

  switch (str.charAt(0)) {
    case 'M': //Gestione motori
      value=(str.substring(2,str.length()).toInt()*255)/100;
      //Serial.print(value, DEC);
      switch (str.charAt(1)) {
        case 'S':
          //Gestione motore sinistro
          analogWrite(MOTORE_SINISTRO,value);
          Serial.print("OK: "+str+"#");
          break;
        case 'D':
          //Gestione motore destro
          analogWrite(MOTORE_DESTRO,value);
          Serial.print("OK: "+str+"#");
          break;
        default:
          Serial.print("Error: motor command <"+str+"> not valid");
          break;
      }
      break;
    case 'S':
      switch (str.charAt(1)) {
        case 'A':
          //Lettura corrente
          sensorValue=analogRead(A_SENSOR);
          Serial.print("OK: A="+String(sensorValue)+"#");
          break;
        case 'V':
          //Lettura tensione
          sensorValue=analogRead(V_SENSOR);
          Serial.print("OK: V="+String(sensorValue)+"#");
          break;
       }
      break;
    default:
      Serial.print("Error: command <"+str+"> not valid");
      break;
  }
  if (inChar==FINE_STRINGA) {
    Serial.println();
  }
}

//I comandi sono del tipo: COMANDO_COMANDO_COMANDO#
//Un comando si chiude con un _ o con # se singolo
//Questa funzione attende di ricevere un # o un _
//per restituire un comando completo
//altrimenti restituisce una stringa vuota
String readSerialString() {
  String retStr;
 
  while (Serial.available() > 0) {
    inChar = Serial.read();
    if (inChar!=FINE_COMANDO && inChar!=FINE_STRINGA) {
      buffer += (char)inChar;
      //Serial.println(">>"+retStr+"<<");
    } else {
      retStr=buffer;
      buffer="";
      return retStr;
      //break;
    }
  }
 
  //Serial.println(">>"+retStr+"<<");
  return "";
}

void loop()  {
 
  while (Serial.available() > 0) {
    String inStr = readSerialString();
    if(inStr.compareTo("")!=0) {
      stringAnalysis(inStr);
    }
  }
}

Il paragrafo sulla gestione della seriale si conclude con il codice del firmware caricato a bordo dell'Arduino. Lo scopo di questo software è quello di leggere dalla seriale del dispositivo ed eseguire le azioni impartite nel comando ricevuto.

Si noti, prima di andare avanti, che il codice è scritto in un linguaggio di programmazione proprietario la cui sintassi è ampiamente descritta a partire dalla pagina http://www.arduino.cc/en/Tutorial/HomePage.

La procedura loop() è l'equivalente della procedura main di linguaggi come il C/C++/Java. Nel nostro caso si tratta di poche righe di codice all'interno di un while infinito che hanno lo scopo di leggere dalla seriale, se ci sono dei dati presenti, e quindi analizzare la stringa ricevuta.

La procedura readSerialString si occupa di leggere i dati ricevuti e di spezzare tali dati nei comandi che compongono la stringa stessa. La stringa termina sempre con il carattere “#” mentre i comandi sono separati dal carattere “_”. Il singolo comando viene passato come parametro alla procedura stringAnalysis che si occupa di eseguirlo.

I comandi sono di due tipi: quelli di tipo “M” che servono per pilotare i motori e quelli di tipo “S” per leggere i dati dei sensori di tensione e corrente. Il carattere “M” è sempre seguito da uno specificatore che può essere “S” per sinistro e “D” per destro. Dopo di esso è presente un valore intero compreso fra 0 e 100. Il comando “MD75”, ad esempio, comunica alla scheda di attivare il motore Destro al 75% della sua potenza. Se il comando è stato eseguito correttamente, l'Arduino invia sulla seriale la stringa “OK: MD75#” informando la Raspberry che l'esecuzione ha avuto successo. Anche il comando “S” è seguito da uno specificatore che può essere “V” o “A”. Il comando “SV” restituisce un valore compreso fra 0 e 1023 proporzionare alla tensione della batteria, mentre “SA” restituisce un valore, nello stesso range, inversamente proporzionale alla corrente assorbita dal sistema.

Tipicamente i due comandi vengono usati in congiunzione con la stringa “SA_SV#”.

Gestione Relè (gpio)

Come ho già avuto modo di accennare, i relè vengono pilotati direttamente dalla Raspberry. Nel capitolo relativo all'hardware vedremo, infatti, che questa scheda ha un connettore interamente dedicato alle connessioni esterne. Cercando in rete ho trovato una libreria in Python (RPi.GPIO-0.4.1a) per poter utilizzare il suddetto connettore, peccato, però, che i tempi di attivazione/disattivazione si aggirassero nell'ordine di un secondo. Davvero troppo per un'applicazione che voleva e vuole essere real-time.

Mi sono chiesto cosa potevo fare per rendere più veloce l'interazione con gli IO della Raspberry e mi sono risposto: perché non usare il buon vecchio C? E così mi sono messo alla ricerca di librerie che potessero andare bene con questo linguaggio di programmazione ma con risultati a dir poco insoddisfacenti. E' probabile che adesso ci siano delle soluzioni diverse ma, quando ho iniziato, alla fine del 2012, non ho trovato nulla che facesse al caso mio (il che non vuol necessariamente dire che non ci fosse).

Non mi ero accorto che quello che cercavo era già a mia disposizione.

Il pacchetto che avevo scaricato, Rpi.GPIO-0.4.1a.tar.gz (adesso ci sarà sicuramente una versione più recente), conteneva al suo interno una directory source con diversi file C, due dei quali, c_gpio.h e c_gpio.c, erano proprio quella libreria che stavo cercando. Ebbene sì: la libreria Python si basava su un sorgente C; perché non utilizzare il tutto direttamente da C allora?

Il piccolo programma che segue, se compilato con c_gpio.h e c_gpio.c, realizza proprio quella funzionalità minima che volevo ottenere.

gpio.c

#
#Attiva o disattiva l'output di un pin
#
#Per compilare gcc gpio.c c_gpio.c -o gpio
#

#include <stdio.h>
#include <stdlib.h>
#include "c_gpio.h"

main(int argc, char** argv)
{
  int pin, value;

  if(argc < 3) {
    fprintf(stderr, "Wrong parameter number (pin value)\n");
    exit(-1);
  }

  pin = atoi(argv[1]);
  value = atoi(argv[2]);

  setup();
  setup_gpio(pin,OUTPUT,PUD_OFF);
  output_gpio(pin,value);
  cleanup();
}

Si tratta di un programma davvero molto semplice (per compilare seguire le istruzioni ad inizio file) che legge due parametri dalla linea di comando. Il primo è il numero che identifica un pin di IO, il secondo il valore che si vuole fornire in output da questo pin. Il comando gpio 9 1, quindi, imporrà l'uscita logica 1 sul pin 9 della Raspberry. Il pin 9, in particolare, pilota il relè per l'inversione di polarità del motore destro come vedremo meglio nel prossimo paragrafo.

Controllo motori

Il controllo dei motori è affidato al server winsocket che con molta fantasia (era il primo) ho chiamato ws.js. In questo paragrafo mi occuperò solo della parte software di tale controllo ma, per capire il funzionamento del sistema, devo fare alcune premesse che riguardano l'hardware.

I motori sono pilotati da 3 relè e 2 transistori. 2 relè servono per invertire la polarità dei motori (per la retromarcia), il terzo viene utilizzato per la “sincronizzazione”. Quest'ultimo si è reso necessario per risolvere un problema presente nel precedente circuito di controllo. Supponiamo che si vogliano attivare entrambi i motori alla stessa potenza per percorrere un tratto diritto. Nonostante le attivazioni del motore di destra e di quello di sinistra devono avvenire contemporaneamente, con due relè non c'è modo per evitare di serializzare tali operazioni. In altre parole, se sono da solo e devo accendere due lampadine con due interruttori diversi, per quanto posso essere veloce, devo prima accenderne una e poi un'altra. Nel tempo, minimo, in cui un motore è già acceso ma l'altro deve ancora accendersi, il robot tenderà a destra o a sinistra a seconda di quale dei due viene attivato prima. Si tratta di una frazione di secondo, ma basta per modificare la traiettoria iniziale di Ninobot. Stessa cosa avviene quando i motori vengono disattivati. Ci sarà un momento in cui uno dei due è già spento ma l'altro continua ad andare. Per evitare questo inconveniente, si è pensato ad un terzo relè, di sincronizzazione appunto, che dà corrente ad entrambi i motori contemporaneamente. E' ovvio che tale relè sarà l'ultimo ad attivarsi alla partenza dei motori ed il primo a disattivarsi in fase di stop. Immagino che sarà tutto più chiaro vedendo lo schema elettrico.

I due transistori servono invece per “modulare” la potenza fornita ai motori attraverso l'Arduino. Per la cronaca, anticipo che la Raspberry pilota direttamente i transistori con il comando “gpio” ed indirettamente (tramite Arduino) i transitori con il comando “serialIO”.

Il server winsocket gestisce, in completa autonomia, tutti e 5 i componendi hardware suddetti sulla base delle richieste dell'utente.

ws.js

#!/usr/bin/env node
var WebSocketServer = require('websocket').server;
var http = require('http');
var exec = require('child_process').exec,
    child;
var localDir="/usr/local/bin/";
var serIO=localDir+"serialIO";
var invS=localDir+"gpio 3 ";
var invD=localDir+"gpio 9 ";
var stop=localDir+"gpio 4 0;";
var start=localDir+"gpio 4 1;";

var server = http.createServer(function(request, response) {
    console.log((new Date()) + ' Received request for ' + request.url);
    response.writeHead(404);
    response.end();
});
server.listen(25001, function() {
    console.log((new Date()) + ' Server is listening on port 25001');
});

wsServer = new WebSocketServer({
    httpServer: server,
    // You should not use autoAcceptConnections for production
    // applications, as it defeats all standard cross-origin protection
    // facilities built into the protocol and the browser.  You should
    // *always* verify the connection's origin and decide whether or not
    // to accept it.
    autoAcceptConnections: false
});

function originIsAllowed(origin) {
  // put logic here to detect whether the specified origin is allowed.
  return true;
}

wsServer.on('request', function(request) {
    if (!originIsAllowed(request.origin)) {
      // Make sure we only accept requests from an allowed origin
      request.reject();
      console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
      return;
    }

    var connection = request.accept('echo-protocol', request.origin);
    console.log((new Date()) + ' Connection accepted.');
    connection.on('message', function(message) {

        var command=message.utf8Data.split("#");
        var cToSend="";

        console.log('Received command: ' + command[0]);
        if (command[0]=="MOTOR") {
          if(command.length!=6) {
            connection.sendUTF("Error: MOTOR command malformed");
          } else {
         cToSend=serIO+' "MS'+command[1]+'_MD'+command[3]+'#"; '+invS+' '+command[2]+'; '+invD+' '+command[4]+';';
            if(command[1]==0 && command[3]==0) {
              cToSend=stop+cToSend;
            } else {
              cToSend=cToSend+start;
            }
            if(command[5]!=0) {
              cToSend=cToSend+'sleep '+command[5]+'; '+stop+' '+invS+' 0; '+invD+' 0; '+serIO+' "MD0_MS0#"';
            }
          }
        } else if (command[0]=="CMOTOR") {
          if(command.length!=4) {
            connection.sendUTF("Error: CMOTOR command malformed");
          } else {
            //Normalizzazione
            xc=command[1]-141;
            yc=141-command[2];
            //Calcolo distanza dalle rette
            dsx=Math.round((xc-yc)*100/141);
            ddx=Math.round((xc+yc)*100/141);
            //Controllo inversione
            if(dsx<0 && ddx >= 0) {
              gpios=0;
              gpiod=0;
            } else if(dsx>=0 && ddx>=0) {
              gpios=1;
              gpiod=0;
            } else if(dsx>=0 && ddx<0) {
              gpios=1;
              gpiod=1;
            } else if(dsx<0 && ddx<0) {
              gpios=0;
              gpiod=1;
            }
            //Applico funzione abs
            dsx=Math.abs(dsx);
            ddx=Math.abs(ddx);
            //Applico limitazione a 100 come massimo
            if(dsx>100) {
              dsx=100;
            }
            if(ddx>100) {
              ddx=100;
            }
            //Applico limitazione a 20 come minimo
            if(dsx<20) {
              dsx=0;
            }
            if(ddx<20) {
              ddx=0;
            }
            //Attuazione comando
            if(dsx==0 && ddx==0) {
              cToSend=cToSend+stop+invS+' 0; '+invD+' 0; ';
            }
            cToSend=cToSend+'serialIO "MS'+dsx+'_MD'+ddx+'#"; ';
            if(dsx>0) {
              cToSend=cToSend+invS+gpios+"; ";
            }
            if(ddx>0) {
              cToSend=cToSend+invD+gpiod+"; ";
            }
            if(dsx>0 || ddx>0) {
              cToSend=cToSend+start;
            }
            if(command[3]>0) {
              cToSend=cToSend+' sleep '+command[3]+'; '+stop+' '+invS+' 0; '+invD+' 0; '+serIO+' "MD0_MS0#"';
            }
         }
        } else {
            connection.sendUTF("Error: command malformed");
            return;
        }
 
        child = exec(cToSend,
          function (error, stdout, stderr) {
            //console.log('stdout: ' + stdout);
            //console.log('stderr: ' + stderr);
            if (error !== null) {
              console.log('exec error: ' + error);
            }
            connection.sendUTF(stdout);
        });

    });

    connection.on('close', function(reasonCode, description) {
        console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
    });
});

La prima parte del programma è dedicata alla definizione di alcune variabili. In particolare vorrei focalizzare l'attenzione su serIO, invS, invD, start e stop che rappresentano l'insieme dei comando utilizzati per “dialogare” con l'hardware. Sono nell'ordine il programma per le comunicazioni seriali, il comando per invertire la polarità sul motore sinistro, quello per invertire la polarità del destro, il comando per attivare il relè di sincronizzazione e quello per disattivarlo. Con questi comandi, ed i relativi parametri, sarà possibile pilotare Ninobot.

Il protocollo di comunicazione fra il client web che abbiamo già visto, ninobot.html, ed il server ws.js è basato su due comandi di base: “MOTOR” e “CMOTOR”. Quello più semplice, “MOTOR”, prevede l'invio di 5 parametri separati dal carattere “#”. Il comando completo sarà quindi “MOTOR#<VS>#<IS>#<VD>#<ID>#<ST>” dove:

Esempio. “MOTOR#100#0#75#1#.5” applicherà la velocità massima al motore di sinistra, senza inversione, ed il 75% della velocità al motore destro, con inversione, per mezzo secondo al termine del quale sarà inviato il comando di stop.

Il comando “CMOTOR” si usa con la circonferenza di controllo e prevede invece solo tre parametri; sarà quindi del tipo “CMOTOR#<PX>#<PY>#<ST>” dove:

Si noti che PX e PY si riferiscono ad un'origine degli assi posta nell'angolo in alto a sinistra dell'immagine e quindi tali valori vanno normalizzati per porre tale origine al centro della circonferenza. Dopo la normalizzazione segue il calcolo della distanza del punto dalle rette (quelle che abbiamo visto in uno dei precedenti paragrafi). A seconda della posizione del punto, poi, si applica o meno l'inversione. Si noti che le rette dividono l'immagine in 4 parti. Indicando con “A” l'area più in alto e con “B”, “C” e “D” rispettivamente le altre tre aree partendo dalla “A” e procedendo in senso orario possiamo dire che:

Le distanze dalle rette vengono limitate come valore massimo (100) e minimo (20) e quindi inviate come valore assoluto a serial IO, mentre i flag di inversione sono forniti come parametri a invS e invD.

Se il parametro ST è diverso da 0, dopo un tempo ST (in secondi), sarà inviato il comando di stop ad entrambi i motori.

Vediamo brevemente come funziona il tutto lato client (codice ninobot.html).

In fase di inizializzazione, viene definita la variabile

wsurl="ws://"+swl[1].substring(2)+":25001"

dove ws è il protocollo usato (WebSocket), swl è un vettore che contiene l'indirizzo del server (più la porta che non viene considerata) ed infine 25001 che è la porta su cui rimane in ascolto il server ws.js. Tale variabile viene utilizzata per inizializzare la websocket con l'istruzione

socket = new WebSocket(wsurl, "echo-protocol");

A questo punto siamo pronti per inviare un comando ai motori. L'istruzione tipo è la seguente

socket.send('MOTOR#100#0#100#1#0')

Le websocket funzionano in modo asincrono, in altre parole non ho bisogno di aspettare che il comando sia processato. A questo punto, però, si pone un problema. Come faccio ad intercettare ed utilizzare l'output che genera l'Arduino sopo aver attivato/disattivato i motori? Semplice: attraverso una callback. Quando il dato è disponibile, cioè è presente un “message” proveniente dalla socket, viene eseguita la seguente funzione.

socket.addEventListener("message", function(event) {

engineOutput.innerHTML=event.data;

});


Il dato restituito, “event.data”, viene semplicemente mostrato in una area dedicata “Status”, puntata dalla variabile “engineOutput”. Come vedremo nel prossimo paragrafo, la gestione della callback per le telecamere è più complessa.

Gestione telecamera principale

Ottenere un'immagine (quasi) in tempo reale anche su una rete WAN 3G mi sembrava davvero impossibile. Non c'era uno streamer video che mi avesse dato un ritardo inferiore ai 2 secondi su LAN!

Riuscire in questa piccola impresa ha richiesto un numero notevole di piccoli accorgimenti, ad esempio l'utilizzo di una sequenza di immagini statiche invece che di un video vero e proprio. La maggior parte dei player video bufferizza i dati prima di riprodurli e questo non me lo potevo permettere; le immagini pure invece non subiscono questo trattamento.

ws_frontCam.js

#!/usr/bin/env node
var WebSocketServer = require('websocket').server;
var http = require('http');

var server = http.createServer(function(request, response) {
    console.log((new Date()) + ' Received request for ' + request.url);
    response.writeHead(404);
    response.end();
});
server.listen(25002, function() {
    console.log((new Date()) + ' Server is listening on port 25002');
});

wsServer = new WebSocketServer({
    httpServer: server,
    // You should not use autoAcceptConnections for production
    // applications, as it defeats all standard cross-origin protection
    // facilities built into the protocol and the browser.  You should
    // *always* verify the connection's origin and decide whether or not
    // to accept it.
    autoAcceptConnections: false
});

function originIsAllowed(origin) {
  // put logic here to detect whether the specified origin is allowed.
  return true;
}

wsServer.on('request', function(request) {
    if (!originIsAllowed(request.origin)) {
      // Make sure we only accept requests from an allowed origin
      request.reject();
      console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
      return;
    }

    var connection = request.accept('echo-protocol', request.origin);
    console.log((new Date()) + ' Connection accepted.');
    connection.on('message', function(message) {

      var options = {
        hostname: 'localhost',
        port: 25000,
        path: '/camera/snapshot.cgi?'+(new Date()).getTime(),
        method: 'GET',
        auth: 'prova:prova'
      };

      var req = http.request(options, function(res) {
        var buffer="";
        res.setEncoding('base64');
        res.on('data', function (chunk) {
          buffer=buffer+chunk;
        });
        res.on('end', function () {
          connection.send(buffer);
          req.end();
        });
      });
     
      req.on('error', function(e) {
        console.log('problem with request: ' + e.message);
      });

      req.end();
    });

    connection.on('close', function(reasonCode, description) {
        console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
    });
});

Questo server rimane in ascolto sulla porta 25002. La richiesta che può soddisfare è una sola: l'acquisizione di una immagine (statica) dal web server della telecamera che, grazie all'utilizzo dei proxy del server Apache, è raggiungibile attraverso l'indirizzo “/camera/snapshot.cgi”. A questo proposito si veda quanto detto sulla configurazione di Apache ed in particolare della direttiva “ProxyPass”. Si noti ancora che, dovendo fornire una login ed una password al server della IPCAM, la richiesta si fa leggermente più complicata. Non posso fare un ragionamento generale in quanto ogni telecamera ha la sua configurazione, le sue caratteristiche ecc. Nel mio caso ho dovuto accedere alla pagina di configurazione dell'apparecchio ed aggiungere un utente “prova”, con password “prova” per poter formalizzare una richiesta accettabile. Tale richiesta è contenuta nella variabile “options” e deve essere effettuata attraverso il protocollo http con la direttiva “http.request”.

La sintassi di JSS alle volte è un po' criptica. Cercherò di spiegare più semplicemente che posso cosa succede adesso. La variabile “res” è l'oggetto associato alla risposta (response) del server della IPCAM. Poiché tale server ci restituisce un'immagine, che è un dato binario e non viaggia molto bene sul protocollo http se non opportunamente codificato, la prima cosa che facciamo è proprio quella di settare la codifica della risposta; “base64” va benissimo; è la stessa utilizzata per inviare immagini o altri dati binari via email. Sull'oggetto “res” costruiamo quindi un callback, sull'evento “data”, che si prenda in carico tutti i “chunk” che il server ci invia. Sì, purtroppo l'immagine non ci arriva intera ma a “chunk” di tot byte per volta. Come facciamo a sapere quando li abbiamo ricevuti tutti? Attraverso l'evento “end” a cui è associata un'altra callback che semplicemente invia la somma di tutti i “chunk”, contenuti in “buffer”, al client che li ha richiesti.

Il client riceve il dato attraverso la callback “message” sulla socketfc, connessa al ws_frontCam.js, e lo passa all'oggetto imgObj specificando che si tratta di una immagine jpeg codificata con base64. Viene aggiornato il conteggio dei frame e si richiede immediatamente al server l'immagine successiva con la direttiva “send”.

socketfc.addEventListener("message", function(event) {

imgObj.src= "data:image/jpeg;base64,"+event.data;

imgControl.innerHTML="Frames: "+i;

i++;

socketfc.send('dummy');

});

Si noti che in questo modo l'acquisizione delle immagini procede alla massima velocità possibile perché è il client che, una volta libero, effettua la richiesta e non il server che le invia senza sapere se qualcuno, dall'altro lato della connessione, è in grado di processarla.

Gestione telecamera secondaria

La gestione della telecamera secondaria passa attraverso due diversi software. Il primo è uno script, uno di quelli che viene lanciato al boot del sistema. Il suo scopo è quello di acquisire a ciclo continuo immagini dalla telecamera con il comando streamer e salvarle sulla directory /srv/http/images. Per evitare di riempire il disco, lo script mantiene solo le ultime tre immagini acquisite e cancella tutte le altre. Per fare questo, vengono ordinate dalla più recente alla più vecchia e tutte quelle a partire dalla quarta in poi vengono eliminate.

startRearView.sh

#!/bin/bash

DIR=/srv/http/images

control_c()
# run if user hits control-c
{
  exit
}
 
# trap keyboard interrupt (control-c)
trap control_c SIGINT SIGTERM SIGKILL

rm $DIR/rearView_*

while true
do
  MS=`date +%s%N | cut -b1-13`
  streamer -s 160x120 -o $DIR/rearView_$MS.jpeg > /dev/null 2>&1
  FNUM=`ls -1 $DIR/rearView_* | wc | awk '{print $1}'`
  DIFF=$((FNUM-3))
  rm `ls -1t $DIR/rearView_* | tail -$DIFF`
done

La prima delle immagini rimanenti, la più recente quindi, viene letta dal server winsocket attraverso il comando associato alla variabile “getFile”. Questa operazione viene effettuata tutte le volte che il server riceve un messaggio dal client (qualunque esso sia, anche in questo caso il server non fa altro). Si noti che l'esecuzione del comando “getFile” fornisce solo il nome del file da caricare, il caricamento vero e proprio viene effettuato dalla funzione “getFileCbk” che lo converte anche in “base64”, esattamente come avveniva per la telecamera principale. E' la funzione “readImage” che legge infine l'immagine dal file system e la invia al client. Quest'ultima operazione avviene solo ed esclusivamente se l'immagine non ha lo stesso nome di quella inviata precedentemente; in caso contrario si assume che il comando “streamer” lanciato da “startRearView,sh” sia rallentato per qualche motivo e si esegue nuovamente la “getFile” per verificare se sia pronta o meno una nuova immagine.

ws_RearCam.js

#!/usr/bin/env node
var WebSocketServer = require('websocket').server;
var http = require('http');
var exec = require('child_process').exec,
    child;
var localDir="/usr/local/bin/";
var getFile="echo -n `ls -1t /srv/http/images/rearView_* | head -1`";
var fs = require('fs');
var fileName;
var oldFileName="";
var connection;

var server = http.createServer(function(request, response) {
    console.log((new Date()) + ' Received request for ' + request.url);
    response.writeHead(404);
    response.end();
});
server.listen(25003, function() {
    console.log((new Date()) + ' Server is listening on port 25003');
});

wsServer = new WebSocketServer({
    httpServer: server,
    // You should not use autoAcceptConnections for production
    // applications, as it defeats all standard cross-origin protection
    // facilities built into the protocol and the browser.  You should
    // *always* verify the connection's origin and decide whether or not
    // to accept it.
    autoAcceptConnections: false
});

function originIsAllowed(origin) {
  // put logic here to detect whether the specified origin is allowed.
  return true;
}

function readImage (err, data) {
    if (err) throw err;
    if(fileName!=oldFileName) {
      connection.send(data);
      oldFileName=fileName;
    } else {
      child = exec(getFile, getFileCbk);
    }
}

function getFileCbk(error, stdout, stderr) {
  if (error !== null) {
    console.log('exec error: ' + error);
  }
  fileName=stdout.toString();
  fs.readFile(''+fileName, 'base64', readImage);
}

wsServer.on('request', function(request) {
    if (!originIsAllowed(request.origin)) {
      // Make sure we only accept requests from an allowed origin
      request.reject();
      console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
      return;
    }

    connection = request.accept('echo-protocol', request.origin);
    console.log((new Date()) + ' Connection accepted.');
    connection.on('message', function(message) {
      //console.log('messaggio arrivato');

      child = exec(getFile, getFileCbk);

    });

    connection.on('close', function(reasonCode, description) {
        console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
    });
});

Hardware

Nella figura seguente è illustrato lo schema hardware di Ninobot. Si tratta di uno schema pensato anche per i non addetti ai lavori ma completo in ogni sua parte.


Schema Hardware

Anche se non sempre è esplicitamente indicato, tutti i componenti del sistema sono connessi a massa. Nei limiti del possibile, i colori dei cavi di collegamento, sono quelli effettivamente usati nel circuito reale. I segmenti rosso tratteggiato rappresentano i cavi che hanno a monte un interruttore (ed un led di stato) e sono tre. Quello più spesso è il cavo che proviene direttamente dalla batteria (12V), gli altri due sono a monte dell'alimentazione per i motori (6V) e della telecamera (5V). La scelta di escludere l'alimentazione per motori e telecamera nasce dalla necessità di testare il sistema senza consumare troppa batteria disinserendo, quindi, tutte le utenze non necessarie in un contesto di semplice prova.

Tutti gli oggetti rappresentati sono abbastanza autoesplicativi a parte forse l'oggetto giallo denominato “Alimentatore” posto sull'estrema sinistra dello schema. Si tratta un riduttore di tensione a 4 uscite. Converte la tensione a 12V in tre tensioni da 5V e una da 6V. L'utilizzo di più moduli a 5V si è reso necessario per evitarne il surriscaldamento, cosa che, purtroppo, rimane ancora un problema da risolvere per il modulo principale, quello che alimenta la Raspberry (e quindi anche l'Arduino attraverso l'USB) e lo Switch USB. A proposito di quest'ultimo componente si dovrebbero evitare tutti quei prodotti che assorbono corrente prelevandola non dal proprio alimentatore ma dalla presa USB Host a cui sono collegati. A mio parere non c'è modo di saperlo se non testandoli.

Tornando ai moduli dell'alimentatore, i circuiti base prevedono l'utilizzo dei MicroA 7805 (per i moduli da 5V) e del MicroA 7806 (per il modulo da 6V). Un condensatore di capacità elevata in uscita dai Micro è utilizzato per livellare la tensione. Il circuito canonico prevede anche l'utilizzo di un condensatore in ingresso, ma, essendo l'input la tensione di una batteria, continua per definizione, ho preferito non metterlo anche per risparmiare spazio. A fianco è presente uno schema di massima.

In alto, al centro dello schema, c'è la scheda di controllo dei motori collegata sia alla Raspberry, che gli fornisce il segnale per attivare i relais, sia all'Arduino, che gli fornisce il segnale PWM per regolare la velocità dei motori.

Nella parte destra dello schema, in basso, ci sono alcune resistenze che non hanno trovato posto in una scheda dedicata e quindi sono state evidenziate a livello di macrosistema. Si tratta di partitori di tensione, limitatori e simili che svolgono di fatto una funzione ausiliaria legata alla lettura digitale (da parte dell'Arduino) di tensione e corrente della batteria, come si è visto in un precedente paragrafo relativo al software.

La resistenza da 0,1 Ohm, 5W, crea una minima caduta di tensione ai suoi capi che è proporzionale alla corrente assorbita. Questa tensione giunge sul pin A0 dell'Arduino dove viene letta come indicato nel paragrafo relativo. Il valore è riferito a massa grazie alla resistenza da 100k.

Sul pin A1, invece, è presente invece una tensione proporzionale a quella della batteria. Il partitore 270k/100k riduce la tensione letta dall'Arduino in modo da non saturare l'ingresso.

Guardando lo schema, frutto di molteplici e diversi tentativi, mi rendo conto adesso che, probabilmente, l'altra resistenza da 100k collegata a massa non serve a molto. Proverò a toglierla quando potrò.


Schema scheda motori

La scheda controllo motori, rappresentata nell'immagine precedente, è provvista di 5 input e 4 output. Come input abbiamo (da sinistra a destra, dall'alto in basso):

  1. Syncro.

  2. Inv. Destro.

  3. Inv. Sinistro.

  4. PWM Destro.

  5. PWM Sinistro.


Come output abbiamo invece:

  1. Motore destro +.

  2. Motore sinistro +.

  3. Motore destro -.

  4. Motore sinistro -.

Gli ultimi due input, il 4 ed il 5 (PWM), sono elaborati dal circuito in modo tale da ottenere ai capi dei condensatori C1 e C2 una tensione in grado di alimentare i motori di Ninobot che lavorano a 6V. Queste trasformazioni sono operate dai transistori TIP102. Un diodo led (giallo) segnala il corretto funzionamento del circuito.

PWM sta per Pulse Width Modulation (modulazione ad ampiezza di impulso) ed è una tecnica utilizzata in tutti quei casi in cui è necessario poter “modulare” la potenza erogata ad una utenza elettrica. Molti conoscono o hanno in casa i variatori di intensità luminosa. Si presentano, di solito, come una normale abat-jour o simile dove al ponto dell'interruttore si trova una manopola che consente di variare la luce emessa dalla lampada; le due posizioni estreme della manopola corrispondo a “illuminazione massima” e “spento”. Si potrebbe pensare che, per realizzare questo tipo di funzionamento, basti fornire alla lampada una tensione proporzionale al grado di illuminazione che vogliamo ottenere. Purtroppo non è così semplice. Immaginiamo di avere una lampada che funziona a 220V, la tensione standard che abbiamo in casa, e di fornire a questa lampada solo 110V per ottenere il 50% dell'illuminazione massima. In linea teorica nessun problema, ma una lampada a 220V difficilmente si accende con soli 110V. La situazione si complica se pensiamo di scendere al 25% dell'illuminazione nominale. Fornire soli 55V volt ad una lampada, o un motore o qualsiasi altra utenza, che funziona a 220V non produce alcun risultato visibile. Come si fa allora? Il trucco consiste nel fornire sempre una tensione di 220V ma non per il 100% del tempo. Il segnale PWM in uscita dall'Arduino è un'onda quadra a 500Hz con valore di picco pari a 5V (standard TTL: 0 logico = 0V, 1 logico = 5V).

Facendo riferimento all'immagine di fianco (fonte http://arduino.cc/en/Tutorial/PWM), la distanza fra ogni linea verde è quindi di 2 millisecondi. Nel primo dei casi elencati la tensione è sempre 0; tecnicamente si dice che il Duty Cycle dell'onda quadra è 0. Nel secondo caso il Duty Cycle è il 25%. La tensione è pari a 5V solo per il 25% del tempo di ciclo, cioè per il 25% di 2 millisecondi. Nell'ultimo caso il Duty Cycle è 100% e l'onda quadra degenera in una tensione continua. Per ottenere una tensione così modulata con l'Arduino, sul pin 10 ad esempio (che corrisponde al motore destro), si utilizza la funzione di libreria

analogWrite(10,255*DC)

dove DC rappresenta il DutyCycle.

Nel secondo caso avremo quindi 255*.25=63,75 che arrotondiamo a 64 (non sono ammessi numeri decimali) e cioè

analogWrite(10,64)

e così via per tutti gli altri casi.

La tensione in uscita dal pin 10 non è adatta a pilotare direttamente i motori per due motivi. Il primo è che i motori funzionano a 6V e non a 5V. Il secondo, ben più importante, è che l'assorbimento di corrente dei motori è ben più elevato di quanto l'Arduino possa tollerare. Tipicamente, se non in casi eccezionali, l'onda quadra in uscita dai pin cosiddetti “analogici” non è mai utilizzata in modo diretto ma sempre attraverso un circuito tipo quello visibile nello schema (Q1,R7,D4,C1) per effettuare la “trasformazione” di segnale di cui parlavo prima.

L'onda quadra così ottenuta viene ancora elaborata dai due circuiti di inversione, destro e sinistro, pilotati dai segnali di ingresso 2 e 3. Si tratta di due input digitali “quasi” puri, nel senso che lo 0 logico corrisponde a 0V mentre l'1 logico vale solo 3,3V contro i 5V canonici. Si tratta di una scelta progettuale della Raspberry sulla quale poco si può dire o fare. 3,3V non sono sufficienti ad eccitare i relè di inversione (indicati nei blocchi grigi) per cui è necessario passare attraverso un transistore, il BC337, che funziona come un normale interruttore.

Le onde quadre presenti sui pin S1 ed S2 del relè K3 possono quindi avere polarità opposte a seconda dello stato (indicato dai led verdi) di K1 e K2. Lo stato del relè K3 (led rosso, di sincronizzazione) ad opera del segnale “Syncro” rende disponibili, o meno, nello stesso istante le suddette onde quadre ai capi delle uscite, + e -, del motore destro e del motore sinistro.


Nell'immagine a sinistra è visibile l'assemblaggio delle schede principali. Dall'alto verso il basso abbiamo:

Si noti anche lo spinotto maschio a 25 poli utilizzato per i vecchi collegamenti seriali (si trova a fianco dell'Arduino). In questo caso è stato utilizzato (riciclato) per il collegamento con l'altra parte del circuito, quella che, potrei definire, di presentazione.




Nell'immagine in basso è rappresentata la seconda metà del circuito. Si noti che tutti i cavi fanno capo al connettore femmina a 25 poli eccetto il video composito (da collegare direttamente alla Raspberry) che, essendo schermato, ha un suo connettore dedicato.

Sulla parte sinistra sono visibili i 4 moduli di alimentazione con i MicroA, i condensatori di uscita e le alette di raffreddamento. A destra ci sono due strumenti di misura (tester di cui ho riadattato alcune parti) per tensione e corrente ed al centro gli interruttori di alimentazione ed i led di controllo (in basso).

Visto così non dice molto. La prossima immagine, quindi, mostrerà la parte anteriore di questo pannello che mi auguro sia più esaustiva.


Qui si notano i due strumenti di misura, gli interruttori, l'uscita video ed i led che indicano lo stato di funzionamento dei motori. Da quello che si vede nell'immagine risulta, ad esempio che sono attivi il motore destro ed il motore sinistro, che quello destro è invertito e che il relè di sincronizzazione è attivato.

Il dispositivo che copre parzialmente lo strumento di sinistra (con il led blu acceso) è la chiavetta Wi-Fi USB. Non ho ancora trovato un posto più comodo dove piazzarla.


Nelle prossime due immagini, invece, ecco finalmente Ninobot come appare adesso.


Nell'ordine dal basso verso l'alto.

  1. Piano terra: motori.

  2. Secondo piano: telecamere (fronte e retro) e casse.

  3. Terzo piano: batteria (nella foto è presente anche il modem Android per collegamenti a lunga distanza).

  4. Quarto ed ultimo piano: centro di controllo con relativo pannello.



Ed ecco il retro di Ninobot “nudo”. Non so ancora come vestirlo!

Una considerazione riguardo ai motori. Si tratta di carichi fortemente induttivi, spesso a causa dell'elevata coppia che devono generare, e questo produce spesso effetti collaterali indesiderati. I disturbi elettromagnetici generati da un motore in fase di accensione e spegnimento, infatti, non sono sempre trascurabili ed inducono comportamenti aleatori in tutti i circuiti esterni, specie se si tratta di circuiti basati su microcontrollori e microprocessori. Per questo motivo, in parallelo ad entrambi i motori, ho inserito un filtro che è conosciuto con il nome di “snubber” (https://en.wikipedia.org/wiki/Snubber). Si tratta di un circuito con un resistore ed un condensatore in serie, entrambi di valore abbastanza piccolo, che spesso hanno in parallelo un varistore o transorb. In letteratura si trovano diversi circuiti di questo genere ed il loro utilizzo e dimensionamento dipendono dai carichi induttivi che si devono pilotare. Possono anche essere assolutamente inutili, ma, in caso di comportamenti anomali, ne suggerisco vivamente l'utilizzo.

La struttura di Ninobot è stata realizzata quasi interamente in legno. Le quattro colonne portanti, quelle che reggono i vari piani della struttura, sono dei profilati metallici ad “L” reperibili in qualsiasi negozio di “Fai da Te”. Sempre negli stessi negozi è possibile reperire anche le ruote sterzanti anteriori e posteriori.

Il video che segue mostra una vecchia versione di Ninobot, “pre Arduino”, dove non era ancora stato implementato il pannello di controllo.


In questo video, invece, si puo' vedere l'ultima versione con Arduino, device Android e tutte le ultime innovazioni

Futuri sviluppi

Lo stato dell'arte di questo progetto non è di certo un punto di arrivo: è solo l'ennesimo punto di partenza. I possibili sviluppi sono molti. In questo capitolo ne esaminerò solo alcuni; dai più probabili, ai futuribili, ai fantascientifici (perché no). Porsi dei limiti è sempre il primo passo verso l'insuccesso.

Motori e cingoli

Una cosa relativamente semplice sarebbe rimpiazzare il “sistema di propulsione” con qualcosa di più adatto alle esigenze di un robot. Semplice nei limiti della reperibilità dei materiali e del loro costo.

Ninobot è nato e si è sviluppato in un ambiente domestico. I suoi “primi passi” nel cortile del condominio sono stati un evento a di poco scoraggiante! Per come è costruito, un terreno non liscio limita notevolmente le sue possibilità di spostamento; in altre parole nessun problema sui pavimenti ma sul duro asfalto la situazione si complica a dismisura.

Molte scelte progettuali sono state determinate dai materiali a mia disposizione ed i risultati in questo senso lasciano a desiderare.

Servirebbero motori più potenti e dei cingoli che lo renderebbero adatto ad ogni tipo di terreno. Chiaramente motori e cingoli andrebbero abbinati e supportati da pulegge, raccordi e tutti qui piccoli meccanismi che opportunamente assemblati produrrebbero qualcosa di funzionante ed affidabile.

Fino ad ora le mie ricerche non hanno avuto molto successo e, se da una parte ho forse trovato i motori che andrebbero bene, dall'altra i cingoli sono ancora un'utopia ben lontana da una concreta realizzazione. E non è tutto: anche se trovassi i cingoli come faccio ad integrarli con i motori?

La soluzione che cerco deve essere assolutamente affidabile; non mi va di raccogliere i pezzi di Ninobot per strada.

Distanziometro ad ultrasuoni

Ninobot ha un solo occhio e quindi non può distinguere le profondità. A dire il vero la sua dotazione di sensori è davvero scarsa e, a prescindere dal numero di telecamere utilizzate, non è in grado attualmente di identificare un ostacolo calcolandone, in qualche modo, la distanza.

In commercio esistono diversi distanziometri ad ultrasuoni (parlo del sensore, non di tutto lo strumento) e dovrebbe essere relativamente semplice collegarli all'Arduino per ottenere delle indicazioni che poi la Raspberry potrebbe elaborare.

In questo scenario, Ninobot, ruotando sul proprio asse più volte, sarebbe in grado di fare una scansione completa dell'ambiente nel quale si trova, costruirsi una mappa interna dell'ambiente stesso, e decidere sulla base di algoritmi più o meno complessi che direzione seguire.

Una possibile applicazione, più commerciale, potrebbe essere un automa in grado di disegnare la planimetria di un ambiente, ad esempio.

Braccio meccanico

Ma cos'è un robot senza braccia? Su diversi siti di robotica ho trovato soluzioni per dotare Ninobot di braccia meccaniche ma, al di là dell'aspetto finanziario (costano ancora troppo per un privato), stiamo parlando di meccanismi in grado di spostare oggetti molto leggeri e quindi inadatti, ad esempio, per aprire una porta.

E' un'area di sviluppo molto interessante ma anche molto oscura, per adesso, soltanto futuribile.

OpenCV

Con quest'argomento entriamo a pieno titolo nel campo dell'intelligenza artificiale. OpenCV è una libreria gratuita di funzioni di base per il riconoscimento delle immagini (CV sta per “Computer Vision”). Non ho fatto ancora molti esperimenti ma ho letto qualche articolo in proposito e l'idea di base è molto interessante.

Il sensori principali di Ninobot, attualmente, sono due telecamere: quella anteriore e quella posteriore. Senza scendere molto in dettaglio nell'anatomia animale, si può dire la stessa cosa riguardo a gran parte degli esseri viventi: il principale sensore di cui dispongono e di cui, come essere umani, disponiamo è l'occhio; un sensore così avanzato e così importante che, qualcuno dice, sarebbe impossibile concepirlo se non esistesse già in natura.

Ma come è possibile sfruttare le informazioni provenienti dalla telecamera per “far capire” a Ninobot dove si trova, dove sta andando, qual è il percorso migliore per arrivare ad una certa destinazione?

OpenCV consente di estrarre da una immagine le sue caratteristiche principali, le relazioni fra queste caratteristiche ed il loro numero. Gli angoli contenuti all'interno di una scena, ad esempio, come sono disposti fra loro, come si collegano, sono quel genere di informazioni utili per classificare quello che vediamo e dargli un significato. Alcuni ricercatori hanno dimostrato infatti che, se da un disegno eliminiamo tutti gli angoli, cioè i punti di intersezione delle rette che compongono il disegno stesso, il cervello umano non sarà più in grado di attribuire un significato a quello che rimane. Da questa esperienza la domanda: se in una immagine lascio solo gli angoli, sarà possibile capire quello che l'immagine rappresenta?

Per quanto riguarda la visione artificiale potremmo dire che tutto dipende dall'algoritmo utilizzato. Comunque sì, in linea di massima la risposta alla precedente domanda è positiva e, estrapolando il concetto, si può dire che diverse sono le caratteristiche che possono essere estratte da una immagine per effettuarne il riconoscimento.

In generale la tecnica è semplice. Si estraggono le caratteristiche di una immagine non nota e si confrontano con le caratteristiche di altre immagini note che sono state elaborate precedentemente. Difficilmente da un confronto di questo genere potremo ottenere una perfetta concordanza (a meno che non si tratti della stessa immagine), ma se ci “accontentiamo” di una concordanza del 90 o 95% (la soglia dipende dall'algoritmo utilizzato) allora il riconoscimento diventa fattibile in quanto si basa su un calcolo prettamente statistico.

Questo non deve stupire; il cervello umano funziona nello stesso identico modo. Quante volte abbiamo percorso la strada da casa nostra al bar dove prendiamo il caffè la mattina, alla stazione o a casa di un amico? E quante volte quello che vediamo lungo il percorso sarà cambiato dalla prima volta che lo abbiamo fatto? Incontreremo dell'altra gente, vedremo automobili diverse, a causa di lavori in corso avremo anche dovuto modificare leggermente il nostro cammino. Per non parlare delle condizioni di luce e di quelle ambientali. Avremo fatto quella strada di notte, di giorno, con la neve, con la pioggia, in mezzo a nuvole di polvere spinte dal vento. Se per decidere in che direzione andare dovessimo confrontare le immagini che arrivano al nostro cervello con quelle memorizzate in precedenza e pretendessimo di trovare una corrispondenza perfetta fra i due insiemi, potete credermi: non riusciremmo ad andare da nessuna parte.

Prendiamo le nostre decisioni in base alle esperienze passate, è ovvio, ma senza aspettarci che tali esperienze siano copie esatte della realtà che stiamo vivendo. Il nostro cervello ragiona per similitudini ed è il riconoscimento di tali similitudini che ci porta ad “attivare” una procedura piuttosto che un'altra.

Nel prossimo paragrafo entrerò più nel dettaglio di questo funzionamento, introducendo l'ultimo degli sviluppi che vorrei intraprendere per Ninobot, fra tutti certamente quello più complicato ma anche quello di gran lunga più interessante.

Reti neurali

E' domenica, una di quelle afose domeniche d'estate dove le magliette rimangono attaccate alla pelle come se fossero incollate, ed avete tanta voglia di leggere il giornale ma non avete il coraggio di mettere il becco fuori di casa.

La soluzione è chiedere ad un figlio ben sapendo che al prezzo del giornale dovrete aggiungere una commissione per il servizio. Il figlio/a, se siete fortunati, vi fagociterà solo una banconota da 5 euro, aprirà la porta di casa e si recherà dal giornalaio più vicino al quale chiederà il vostro giornale preferito. Il giornalaio darà il quotidiano ed il resto al figlio/a e quest'ultimo/a tornerà a casa dopo aver fatto sparire il suddetto resto in qualche tasca dei pantaloni.

Non avete un figlio/a? Lo avete ma non è affidabile? A questo punto vi ricordate di avere anche un cane! La cosa si complica notevolmente, è chiaro, e richiede uno sforzo ed una pazienza ben maggiore. In fondo, però, si tratta di operazioni abbastanza semplici e, a parte l'aprire la porta e pagare il giornalaio, operazione quest'ultima che potreste in qualche modo concordare con il giornalaio stesso, non c'è nulla che un cane ben addestrato non possa fare.

Non avete un figlio/a e non avete un cane. Però avete Ninobot a casa. La domanda è: potete addestrarlo per andare a comprare il giornale? La risposta a questa domanda potrebbe sembrare abbastanza ovvia, anzi, diciamo che lo è, ma, prima di rinunciare del tutto, riflettiamo sul significato del termine addestrare.

Nel caso degli esseri umani questo termine si usa solo in determinati contesti, quello militare ad esempio, spesso si usa il termine allenare, ma è improprio in questo esempio, più spesso e sicuramente si usa il verbo insegnare. Se vogliamo che un bambino, un ragazzo ma anche un uomo faccia qualcosa, questo qualcosa glielo dobbiamo insegnare.

Una operazione elementare, come quella di comprare il giornale, richiede poco tempo per essere appresa, anzi, spesso l'”umano” ha già tutte le informazioni per portarla a termine. Al limite bisognerà spiegargli dove si trova il giornalaio se non lo sa già.

Si noti, a questo punto, un'altra cosa importante. In generale, di qualsiasi operazione si tratti, il tempo necessario per “insegnarla” sarà inversamente proporzionale alla capacità di comunicazione fra chi insegna e chi impara. Se a comprare il giornale ci mandate il vostro amico che parla solo il cinese, lingua che non conoscete affatto, sarà dura che voi possiate leggere le notizie del giorno.

Ecco un'altra parola chiave: “comunicare”. La mettiamo insieme all'altra, quella più importante: ”imparare”(/“insegnare”). Ci serviranno entrambe in seguito.

Torniamo al nostro cane. Addestrarlo (insegnarli) a comprare il giornale è un'operazione che richiede sicuramente molto tempo, la comunicazione uomo-cane non è perfetta (a volte anche uomo-uomo a dire il vero), ma non è in linea di massima una cosa impossibile. Un cane, come gran parte degli esseri viventi, se non tutti, è in grado di imparare e, con un po' di difficoltà, ci si può comunicare.

E con Ninobot cosa facciamo? Il motivo per cui la risposta alla domanda “possiamo addestrarlo per andare a comprare il giornale?” ci sembra addirittura triviale è perché ci troviamo di fronte a due problemi che appaiono subito insormontabili: non è in grado di imparare, quindi è inutile insegnare, e non possiamo comunicare con lui. Ci sono poi un'altra serie di limitazioni fisiche naturalmente. Non ha le gambe, non ha le braccia ma sappiamo benissimo che la sua limitazione più significativa è di ordine intellettuale: Ninobot è stupido o, il che è lo stesso, non è intelligente.

Insegnare a Ninobot a seguire un percorso o ad aprire una porta, se avesse le mani, sarebbe un'impresa disperata; non ci sono dubbi, non sapremmo nemmeno da che parte iniziare.

Qualcuno potrebbe obiettare che i computer possono, entro certi limiti, imparare a svolgere dei compiti e Ninobot non è altro che un computer con le ruote, almeno per adesso. Vero. Un programma in fondo è una “intelligenza artificiale” che il programmatore presta al computer per eseguire una procedura anche complessa. In fondo, però, solo in fondo.

Analizziamo un semplice pezzo di codice.

se (questo=quello) allora faiQualcosa

I programmi per computer sono pieni di istruzioni di questo genere. Se si verifica una certa condizione (questo=quello) allora fai qualcosa, esegui cioè una procedura ben dettagliata. Badate bene: “questo” deve essere uguale a “quello” non solo simile, deve essere identico, e “faiQualcosa” farà solo quel “qualcosa” e niente di più. Se “questo” può essere uguale a “quello” ma anche a “quell'altro” e oltre a “fare qualcosa” deve “fareQualcosaDiPiù” dobbiamo per forza scriverlo nel programma.

Forse un programma complesso, enormemente complesso, potrebbe simulare i miliardi di miliardi di “se” e di “faiQualcosa” che rappresentano un essere umano ma rimarrà sempre e comunque una procedura, tutto quello che potrà fare lo dovremo codificare in anticipo. Sarebbe come dire che potremmo prendere un uomo qualsiasi, “convertire” la sua mente in software e sapere in ogni istante della sua vita che decisione prenderà di fronte ad una certa situazione. Un po' esagerato oltre che macchinoso.

Cos'è quindi che fa la differenza fra una procedura, per quanto complessa, ed una entità che possiamo definire intelligente? La capacità di quest'ultima di imparare, di fare esperienza.

“Guarda mamma: il fratellino mi sorride!”

Una piccola grande tappa nello sviluppo di un essere umano: un bambino che impara a riconoscere un volto familiare e gli sorride. Nessuna procedura, se non adeguatamente e puntualmente addestrata, sarebbe capace di tanto.

Rinunciamo allora? Nemmeno per idea. Nel 1990, all'università di Genova, seguendo un corso di informatica, mi trovai di fronte ad un tecnologia molto interessante: le reti neurali. Il contesto in cui si intendeva applicarle era, col senno di poi posso dirlo, completamente sbagliato ma l'idea di fondo si prestava alla soluzione di svariati problemi. Anche il principio di questa tecnologia mi aveva colpito: simulare il funzionamento del cervello umano; roba da fantascienza.

Come parecchie cose, all'inizio sembra tutto molto complicato ma poi si scopre che l'essenza dell'argomento è piuttosto semplice. Il nostro cervello ad esempio; niente che ci circondi è più avanzato e complesso ma l'unità fondamentale di questa macchina meravigliosa, il neurone, è ormai abbastanza ben conosciuto. Senza scendere in un dettaglio che non potrei sostenere, si può dire che la cellula neuronale si comporta come un interruttore a più ingressi. Da una parte ci sono gli input provenienti da altri neuroni (dendriti), dall'altra parte c'è un unico output (l'assone) che può attivare i neuroni limitrofi. Se la somma degli input applicati supera una certa soglia, si genera un impulso nervoso, cioè l'output lungo l'assone. Un'operazione molto semplice ma che viene ripetuta cento miliardi di volte, tanti sono i neuroni del nostro cervello. E' quello che in informatica chiamiamo calcolo parallelo: tante unità elaborative che svolgono, contemporaneamente, delle operazioni molto elementari.

Se ci fermassimo qui, però, sarebbe tutto davvero troppo semplice. Con i supercomputer attuali non sarebbe difficile simulare un tale funzionamento. In effetti il vero problema non è gestire centomila diverse unità di elaborazione (anche questo non è uno scherzo comunque) ma i collegamenti fra di loro. Che criterio usiamo per collegarli? Ma soprattutto, ed è questo il cuore della questione, come aggiorniamo i collegamenti?

Quello che pensiamo, quello che ricordiamo, quello che facciamo passa tutto attraverso le nostre connessioni nervose. Lo stesso apprendimento consiste “semplicemente” nel riorganizzare questi collegamenti. La bontà dell'algoritmo con cui la nostra mente effettua tale operazione, in un certo senso, può essere considerata una misura della nostra intelligenza. Quale sia il motivo per cui, di fatto, il suddetto algoritmo funzioni meglio nella specie umana piuttosto che in quella canina nessuno è ancora in grado di dirlo.

Ricapitolando, la nostra mente è come un computer: l'hardware è l'ammasso di neuroni che costituisce il cervello, il software è quel meccanismo che modifica le connessioni fra i neuroni stessi.

Le reti neurali sono un tentativo abbastanza ben riuscito di simulare il funzionamento dell'hardware. Poco dopo essere state inventate, però, vennero abbandonate perché nessuno aveva un software adatto per farle funzionare. Solo molti anni dopo, con l'algoritmo della Back Propagation, fu davvero possibile utilizzarle per scopi concreti, l'OCR ad esempio.

OCR sta per Object Character Recognition ed è quella procedura in grado di leggere la pagina di un libro o di un giornale. Non è fantascienza ma solo un'applicazione delle reti neurali. Quando eseguiamo la scansione di un libro con uno scanner non otteniamo il testo stesso ma una immagine che lo contiene, di fatto è come se lo avessimo fotografato. Per ottenere davvero il testo che poi potremo copiare su Writer di OpenOffice (e non voglio sentire parlare di altri editor) dovremo analizzarlo carattere per carattere. Se pensate che i caratteri possono avere grandezze e stili diversi, vi sarà subito chiaro che non si tratta di un'operazione molto semplice per un non umano.

Le reti neurali risolvono questo problema in un modo che potrei definire naturale.

Come si fa ad insegnare ad un bambino a leggere? Si parte dall'alfabeto.

Questa è una “a”, questa è una “b” e così via.

Con le reti neurali si fa la stessa cosa. Si fa “vedere” una lettera alla rete (un'immagine che rappresenta tale lettera) e gli si “dice”: quando vedi questa devi rispondermi con il carattere “a”, a quest'altra con il carattere “b” ecc. L'algoritmo della Back Propagation adegua di conseguenza i pesi dei collegamenti fra i neuroni (un valore numerico che contraddistingue ogni collegamento) in modo tale che la differenza fra il risultato ottenuto e quello atteso diminuisca ad ogni iterazione. Finita la fase di apprendimento, si fa “vedere” alla rete un'immagine nuova, mai mostrata prima, e se tutto ha funzionato a dovere l'output indicherà correttamente la lettera che l'immagine rappresenta. Vi risparmio tutta la matematica che c'è dietro ma sappiate che gli OCR di ultima generazione producono un output corretto nel 99% dei casi.

Cosa potrebbe farsene Ninobot delle reti neurali? Potrebbe seguire un percorso, ad esempio, potrebbe evitare degli ostacoli, riconoscere l'ambiente nel quale si trova ed essere capace di andare da una stanza all'altra se gli viene ordinato.

Non è molto, è vero, ma potrebbe essere un buon punto di partenza per sviluppare qualcosa che possa un giorno fregiarsi dell'appellativo “intelligente”.






© Mialan 2001 - 2018