Physical Computing: a case study Matteo Pessina [email protected] matricola 759315 Relatore: Andrea Trentini [email protected] Università degli Studi, Milano Facoltà di scienze Matematiche, Fisiche e Naturali Corso di laurea in Informatica Anno accademico 2011-2012 Abstract Questo documento mostra nel dettaglio il percorso seguito per la realizzazione di un progetto riconducibile, per la maggior parte, all’area del physical computing. Si parte introducendo il test di Hess/Lancaster, di ambito ortottico, andando a sottolineare i requisiti che il dispositivo vuole soddisfare. Si suddivide il progetto in moduli indipendenti e si descrive la realizzazione di questi, suddividendo ancora hardware e software. Si tenta di affrontare il tema della calibrazione del dispositivo raggiungendo un modello matematico teorico corretto ma non riuscendo ad ottenere risultati tanto precisi nella pratica. Si riformulano i requisiti e si conclude il progetto delegando la calibrazione a terzi. 1 Contents 1 Introduzione 1.1 Test di Lancaster-Hess: finalità e modalità . . . . . . . . . 1.1.1 Contesto . . . . . . . . . . . . . . . . . . . . . . . . 1.1.2 Esecuzione del test . . . . . . . . . . . . . . . . . . 1.1.3 Hess, Lancaster: modalità comuni e differenze . . . 1.1.4 Scopo . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Formulazione dei requisiti e modularizzazione del progetto 1.2.1 Requisiti . . . . . . . . . . . . . . . . . . . . . . . . 1.2.2 Suddivisione in moduli . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4 4 4 5 6 7 7 8 2 Realizzazione dell’hardware 2.1 Scelte di progetto . . . . . . . . . . . . . . . . . . 2.2 Arduino . . . . . . . . . . . . . . . . . . . . . . . 2.2.1 Che cos’è e perchè è ”bello” . . . . . . . . 2.2.2 Panoramica . . . . . . . . . . . . . . . . . 2.2.3 Arduino IDE e la programmazione dei chip 2.3 Servomotori . . . . . . . . . . . . . . . . . . . . . 2.3.1 PWM . . . . . . . . . . . . . . . . . . . . 2.3.2 Leggere il datasheet . . . . . . . . . . . . . 2.3.3 Installazione . . . . . . . . . . . . . . . . . 2.4 Laser e altoparlanti . . . . . . . . . . . . . . . . . 2.4.1 Laser . . . . . . . . . . . . . . . . . . . . . 2.4.2 Transistor . . . . . . . . . . . . . . . . . . 2.4.3 Altoparlante . . . . . . . . . . . . . . . . . 2.5 Wii Nunchuck . . . . . . . . . . . . . . . . . . . . 2.6 Schema circuitale completo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 9 9 9 10 12 12 13 14 15 16 16 16 16 17 17 . . . . . . . . . . . 20 20 20 21 23 25 26 28 28 29 30 31 3 Realizzazione del software 3.1 Drivers . . . . . . . . . . . . . . . . . . . 3.1.1 Introduzione . . . . . . . . . . . . 3.1.2 Servomotori . . . . . . . . . . . . 3.1.3 Nunchuck . . . . . . . . . . . . . 3.1.4 Altoparlante . . . . . . . . . . . . 3.2 La comunicazione seriale . . . . . . . . . 3.3 La calibrazione e i metodi di conversione 3.3.1 Premessa . . . . . . . . . . . . . 3.3.2 Il problema . . . . . . . . . . . . 3.3.3 Interpolazione . . . . . . . . . . . 3.3.4 Approccio trigonometrico . . . . 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . AVR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4 4 R 4.1 4.2 3.3.5 Problemi e riformulazione dei requisiti . . . . . . 3.3.6 I risultati . . . . . . . . . . . . . . . . . . . . . . 3.3.7 Approccio per fasce . . . . . . . . . . . . . . . . . Lo sketch finale . . . . . . . . . . . . . . . . . . . . . . . 3.4.1 Inclusioni, macro, allocazioni statiche permanenti 3.4.2 Setup . . . . . . . . . . . . . . . . . . . . . . . . 3.4.3 Il controller dell’applicazione . . . . . . . . . . . . 3.4.4 La comunicazione . . . . . . . . . . . . . . . . . . 3.4.5 Calibrate . . . . . . . . . . . . . . . . . . . . . . 3.4.6 Wait for user . . . . . . . . . . . . . . . . . . . . 3.4.7 Wait for nunchuck . . . . . . . . . . . . . . . . . 3.4.8 Execute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 34 36 38 38 40 40 42 43 44 44 45 47 Perchè R . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 Creare un ponte tra R e Arduino . . . . . . . . . . . . . . . . 47 5 Installazione e tutorial 5.1 Prerequisiti . . . . . . . . . . . . 5.2 RXTX . . . . . . . . . . . . . . . 5.2.1 OSX . . . . . . . . . . . . 5.2.2 Linux . . . . . . . . . . . 5.2.3 Windows . . . . . . . . . . 5.2.4 Verifica . . . . . . . . . . 5.3 ArduBridge . . . . . . . . . . . . 5.3.1 OSX e Linux . . . . . . . 5.3.2 Windows . . . . . . . . . . 5.3.3 Verifica . . . . . . . . . . 5.4 Compilazione Sketch . . . . . . . 5.4.1 OSX . . . . . . . . . . . . 5.4.2 Linux . . . . . . . . . . . 5.4.3 Windows . . . . . . . . . . 5.5 Tutorial per l’uso . . . . . . . . . 5.6 Troubleshooting . . . . . . . . . . 5.6.1 Could not find COM port 6 Conclusioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 53 53 53 54 54 54 55 55 55 55 56 56 56 56 56 58 58 58 3 1 1.1 1.1.1 Introduzione Test di Lancaster-Hess: finalità e modalità Contesto Il movimento degli occhi rispetto al cranio è garantito da 12 muscoli, 6 per lato, che prendono il nome di muscoli extrabulbari. Nella figura 1 possiamo vederne uno schema. Non è semplice diagnosticare un malfunzionamento o Figure 1: Schema muscoli occhi. Il Retto Interno (RI) è adduttore. Il Retto Esterno (RE) abduttore. Il Retto Superiore (RS) è Elevatore. Il Retto Inferiore (RI) è abbassatore. Il Grande Obliquo (GO) è ruotatore interno. Il Piccolo Obliquo (PI) è ruotatore esterno. la paralisi di questi, infatti, in caso di deficit, gli altri muscoli tendono a compensare la disfunzione. Al fine di valutare correttamente quale muscolo è in deficit e di quanto, in ortottica, vengono usati di preferenza il test di Hess e quello di Lancaster dei quali segue una breve descrizione. 1.1.2 Esecuzione del test Il paziente indossa un paio di occhiali con una lente verde e una rossa ed impugna una torcia, emanante un fascio di luce verde, mentre l’operatore (medico ortottista) impugna una torcia con luce rossa. L’operatore posiziona lo spot della sua torcia su una serie di punti predefiniti a griglia sullo schermo (di Hess o Lancaster), il paziente deve sovrapporre lo spot della sua torcia a quella dell’operatore. Lo scarto (errore) del paziente viene riportato manualmente dall’operatore su una scheda che riproduce in piccolo la griglia. 4 Figure 2: Schermo di Hesse. Questo è il prospetto dove il medico segna i punti mirati dal paziente. Uno schema per occhio. Terminata la prima serie di punti s’invertono le torce o le lenti degli occhiali (rosso al posto di verde) in modo da testare l’occhio controlaterale. La diagnosi viene effettuata analizzando l’insieme di tutti gli scarti effettuati dal paziente. A seconda della disposizione della griglia dei punti-test sullo schermo, l’esame prende il nome di test di Hess o di Lancaster. 1.1.3 Hess, Lancaster: modalità comuni e differenze Il test si effettua generalmente con una distanza tra il paziente e lo schermo di 50cm. Il paziente inoltre ha la testa appoggiata su un apposito sostegno che la posiziona sulla normale al centro dello schermo. La differenza tra il test di Hess e quello di Lancaster si vede nelle figure 2, 3: nella prima la distanza tra i punti è il risultato della riduzione della sfera avente raggio 50 cm su un piano bidimensionale. Solo nella sfera i punti sono equidistanti. Nel secondo invece i punti hanno la medesima distanza sul piano. Nella figura 3 viene mostrato il risultato del test e la relativa diagnosi. 5 Figure 3: Esempio di prospetto compilato su schermo di Lancaster. La diagnosi in questo caso è miopia unilaterale. 1.1.4 Scopo Il test, per come è effettuato ora, ha il limite di calcolare lo scostamento per via empirica, introducendo l’errore umano. Inoltre l’uso di uno schermo fisico rende l’apparato piuttosto ingombrante. Scopo di questo tirocinio è quello di creare, in collaborazione con il dottor Mario Cigada (www.mariocigada. com), un dispositivo in grado di effettuare il test in maniera automatica, aumentando quindi la precisione delle misure sullo scostamento del puntatore del paziente e rendendo il sistema più versatile, in quanto in grado di eseguire indifferentemente il test di Hess, quello di Lancaster o uno “custom”. 6 1.2 1.2.1 Formulazione dei requisiti e modularizzazione del progetto Requisiti 1. Il dispositivo deve fornire una chiara interfaccia al linguaggio R in esecuzione su una macchina collegata ad esso. Questa interfaccia deve dare la possibilità all’operatore di eseguire il test con diversi parametri e di riceverne i risultati. 2. Il dispositivo deve avere due puntatori laser, rosso e verde, che, date delle coordinate piane, possano mirare un qualsiasi punto di uno schermo verticale. Con coordinate piane si intende una coppia (x, y) di valori che esprimano la distanza di un punto p obiettivo da un riferimento preimpostato sullo schermo. 3. Il dispositivo deve essere in grado di eseguire il test di Hess-Lancaster: ricevuta una sequenza di punti espressi in coordinate piane, inizia a mirare il primo punto con uno dei due laser lasciando il controllo dell’altro al paziente. Il dispositivo dovrà quindi attendere un segnale di conferma da parte di questo, registrare quindi le coordinate piane del punto mirato, emettere un suono e procedere con il punto successivo. Finita la lista dei punti deve emettere un suono diverso dal precedente e ricominciare il test scambiando il controllo dei laser. Finito il test, deve restituire due sequenze di punti rappresentanti quelli mirati dal paziente. 4. Uno dei due laser deve essere controllato in modo semplice dal paziente. Il nostro scopo non sarà quindi di occuparci dell’interezza del progetto ma solamente della parte che riguarda il dispositivo ed il software relativo ad esso. Le ulteriori valutazioni sui dati grezzi, la creazione del prospetto e la relativa diagnosi saranno a carico del medico collaboratore. 7 1.2.2 Suddivisione in moduli Ecco una rappresentazione ad albero dei moduli e dei sottomoduli del progetto. Project Dispositivo Realizzazione HW Sistema di puntamento Controllo paziente Realizzazione SW Gestione delle periferiche Calibrazione e traduzione sistemi di coordinate Progetto del codice principale Interfaccia R Comunicazione Seriale Accesso interfaccia dispositivo Realizzazione funzioni utili elaborazione dati L’approccio blackbox scelto richiede quindi di identificare i moduli principali e di definire delle chiare interfacce per essi. Le due black box saranno il Dispositivo e l’Interfaccia R. Il primo sarà accessibile dall’esterno tramite seriale e attenderà una stringa che specifichi tutti i parametri relativi al test e restituirà ancora una stringa contenente i punti mirati dal paziente; il secondo dovrà fornire una funzione lancaster(...) che accetta una coppia di matrici (2 ∗ n, ogni colonna rappresenta un punto) di coordinate virtuali rispetto ad un determinato centro e restituisce la coppia di matrici relative ai punti effettivamente mirati dal paziente. Oltre a questa renderà disponibili altre funzioni come la scrittura su file dei dati e la differenza tra due liste di matrici. 8 2 Realizzazione dell’hardware 2.1 Scelte di progetto Il dispositivo deve poter comunicare con: • Servomotori: avrà quindi bisogni di un microcontrollore con interfaccia PWM (Pulse Width Modulation); • Laser: semplice pin up and down da 5-3.3 volt; • Wii nunchuck: interfaccia I 2 C; • Altoparlante: PWM; • Computer: comunicazione seriale. Per un dispositivo con queste necessità la scelta più comoda è utilizzare Arduino, un ambiente hardware di sviluppo che consiste in una board, una porta usb, un microcontrollore e tutto quello che esso necessita per funzionare e comunicare con il mondo esterno. Esso semplifica di molto il progetto di dispositivi riducendo il tutto al collegamento di componenti esterni, al massimo mediati da qualche resistenza. Al fine di permettere ai laser di mirare un qualsiasi punto su uno schermo si utilizzeranno una coppia di servomotori, detti Pan e Tilt. Questi sono dei motori con un ”braccetto” che, controllati in PWM, assumono determinati angoli tra 0 e π. Per il controllo da parte del paziente del puntatore laser da lui comandato, è stato scelto il Nintendo Wii Nunchuck. Infatti il suo joypad può comandare comodamente il movimento, sia verticale che orizzontale, del laser. Facile infine confermare con i pulsanti di questa semplice periferica i punti mirati. 2.2 2.2.1 Arduino Che cos’è e perchè è ”bello” Come abbiamo precedentemente detto, Arduino è un’insieme di componenti preassemblati che facilita la programmazione e l’interfacciamento ad un microcontrollore. Questo non è altro che un piccolo processore che ha all’interno tutto il necessario al suo funzionamento: una memoria RAM, una Flash, delle interfacce per la comunicazione con il mondo esterno, et cetera. Nell’immagine 4 viene mostrata la versione Arduino UNO, equipaggiata con un microcontrollore ATmega328 (indicato dal numero 1). Prima che ci fossero questo tipo di sistemi preassemblati, un progetto di questo genere avrebbe comportato innanzitutto la scelta di un chip, la costruzione di un ambiente adatto 9 al suo funzionamento (condensatori, resistenze, pin, socket, etc), l’acquisto di un programmer, dispositivo in grado di programmarlo, e tutto un ampio corredo di conoscenze che solo un esperto del settore avrebbe potuto possedere. Proprio in questo possiamo dire che Arduino è ”bello”: chiunque può utilizzarlo con semplicità collegandolo con un cavo USB al proprio computer e scaricando l’ambiente di sviluppo Arduino IDE. Figure 4: Arduino: panoramica 2.2.2 Panoramica Nella figura 4 vengono numerati i principali componenti di cui è indispensabile una minima conoscenza. Come abbiamo già detto, al punto 1 si trova il microcontrollore, il ”cervello” di tutta la board. Una domanda sorge spontanea: ”Ma come fa ad esserci dentro tutto quello che nei computer di utilizzo comune viene distribuito sulla scheda madre?”. La risposta è semplice. Quello a cui ci troviamo davanti è un microcontrollore, in quanto tale, il processore che ha all’interno è centinaia di volte meno potente di quelli utilizzati per i dispositivi general purpose, quindi meno complesso; anche la memoria di cui è equipaggiato è decisamente minore, si parla di 2 KB di SRAM e 32 KB di flash memory. Essendo tali, con la tecnologia moderna è possibile inserire all’interno tutto il necessario. (System on Chip). Con il numero 2 è evidenziata una porta USB tipo B; non dobbiamo far altro che ottenere un cavo USB A-B per collegarlo al nostro computer. Il microcontrollore ATmega328 nativamente non supporta questo tipo di comunicazione, essa è possibile in quanto simula una comunicazione seriale. 10 Figure 5: Collegamente dispositivo Per mettere in condizione Arduino di controllare attuatori o di ricevere dati da sensori, dobbiamo fisicamente collegarli con dei fili elettrici. Questi saranno messi nei socket mostrati dal punto 3 o 4. Al punto 3 abbiamo quelli collegati ai pin digitali del microchip. Un pin digitale può essere utilizzato come output o come input. Nel primo caso può applicare una tensione di 5 volt, che rappresenta l’1 in elettronica digitale (a volte si può utilizzare anche 3.3 volt), oppure comportarsi da massa (GND), ovvero portare la tensione a 0 volt. E’ importante sapere che l’intensità di corrente emessa da uno qualsiasi dei pin digitali è di 40mA; quando andremo a collegarci dei componenti sarà indispensabile abbassare questo valore con delle resistenze secondo l’intensità adatta, specificata nel datasheet di ogni componente, pena bruciarlo. Nel caso sia invece utilizzato come input allora riceverà una tensione determinata da un altro componente e potrà leggere 1, se la tensione è 5 volt, o 0 in caso contrario. I pin sono numerati e alcuni hanno un ∼ prima del numero. Questi hanno la possibilità di essere usati in PWM, di cui parleremo nella sezione 2.3.1. Il pin GND è il pin di massa, ovvero è sempre a 0 volt; i pin 0,1 possono essere utilizzati anche per la comunicazione seriale (oltre a 11 quella simulata dall’USB). I socket indicati dal numero 4 sono collegati ai pin analogici del chip. Anch’essi possono essere utilizzati sia passivamente, per leggere il valore delle tensioni applicate, o attivamente per applicarle (range: 0-5 volt). La conversione A/D avviene con 10 bit di precisione, di cui 8 leggibili. L’alimentazione della board può avvenire sia tramite USB che tramite la porta indicata dal numero 5. E’ possibile utilizzarle anche assieme, come faremo nella nostra applicazione. Questo è necessario quando si alimentano tanti sensori e attuatori, infatti, l’intensità di corrente diminuisce drasticamente il tal caso e può causare un reset del microcontrollore. Questo accade spesso se si alimenta Arduino solo tramite usb collegata ad un notebook non alimentato a sua volta. L’alimentazione della board non deve comunque mai superare i 20 volt, pena il danneggiamento della stessa. Con il numero 6 si indicano infine i pin 5V, 3.3V, che non sono altro che pin digitali, in modalità output, costantemente a 1. Il primo però eroga 5 volt mentre il secondo 3.3 volt. 2.2.3 Arduino IDE e la programmazione dei chip AVR Arduino IDE è un software multipiattaforma che permette di programmare il microcontrollore all’interno di Arduino. I microcontrollori AVR normalmente si programmano in C ma Arduino IDE permette di utilizzare il linguaggio Processing, decisamente più semplice per i non esperti di programmazione. Processing permette l’uso degli oggetti. Il codice viene tradotto dall’IDE in C++ con però alcune limitazioni: ad esempio non è possibile creare e deallocare dinamicamente oggetti (keyword new, delete). Perchè utilizzare gli oggetti dunque? Semplicemente per rendere più semplice la comprensione del codice e la consultazione delle librerie. Il codice C++ viene compilato con il compilatore g++ appositamente settato per la programmazione su AVR. Quanto c’è da sapere è molto semplice: tutto il codice è contenuto in due funzioni: setup, chiamata all’accensione del chip, loop, eseguita continuamente dopo la prima. Di default vengono incluse le definizioni delle librerie avr.h e Arduino.h. Per la documentazione di queste e altre librerie disponibili fare riferimento a http://arduino.cc/en/Tutorial/HomePage. Consigliato il learning-through examples. 2.3 Servomotori Un servo motore altri non è che un motore capace di ruotare in un dato range (generalmente [0, π]). Esso è un attuatore poco costoso se non si hanno particolari pretese di potenza e precisione. Quelli analogici vengono comandati 12 Figure 6: Arduino IDE tramite PWM (Pulse Width Modulation, figura 8). 2.3.1 PWM In elettronica digitale, essendoci 2 soli valori possibili di tensione, per comunicare con alcuni attuatori o per alzare la tensione ad un arbitrario livello compreso tra 0-5 volt si usa emettere un’onda quadra. Un onda quadra è una funzione del tempo f (t) che ha un periodo T , ovvero f (t) = f (t + iT ), i = {0, ..., n} e un duty cycle τ tali che f (t) = 1 per iT ≤ t ≤ iT + τ, i = {0, ..., n}. La tensione risultante V sarà: V = τ 5 volt T (1) Nel nostro caso però ci servirà per dire al servomotore quanto deve ruotare. Lo standard vorrebbe che ad un angolo di 0 rad corrispondesse una PWM con τ = 1000µs e a π τ = 2000µs. I nostri economici servomotori hanno però range diverso. 13 Figure 7: Servo motore Figure 8: Pulse Width Modulation La PWM di Arduino ha frequenza 500Hz quindi T = 2000µs. Una coppia di servomotori può essere utilizzata in modo tale da permettere ad un laser montato su di essi di mirare un range ampio su di una parete. Quando vengono usati in tale modo si nomina pan il servomotore che muove orizzontalmente il laser, tilt quello che lo muove verticalmente. 2.3.2 Leggere il datasheet Tutti i componenti elettronici sono dotati di un documento che ne dà le specifiche. Nota però che per saperli leggere è necessaria una minima conoscenza del pezzo in questione. Nella figura 9 è mostrato una parte del datasheet dei servomotori acquistati per questo progetto. Le informazioni di nostro interesse sono: • control system: specifica a quanti microsecondi di duty cycle il servo motore si posiziona al centro del suo range (neutral), ovvero a 1500 µs; • operating voltage range: indica l’intervallo di tensione adatta al componente: [4.8, 6]volt; 14 Figure 9: Datasheet dei servo motori acquistati. • operating angle: ci informa del range d’azione del servomotore e della sua mappatura lineare. 45 gradi corrispondono a 400µs e il range d’azione è di soli π2 . Con questa informazione siamo in grafo di definire la mappa lineare m : RAD → M ICROS, che associa ad un angolo in radianti i microsecondi di duty cycle equivalenti, e la sua inversa m−1 : M ICROS → RAD. 2.3.3 Installazione Per installare un servo su Arduino è necessario conoscerne il cavo di collegamento. Con una veloce ricerca su internet è facile capire che i tre filamenti che lo costituiscono, rosso, nero e giallo, vanno rispettivamente collegato all’alimentazione, alla messa a terra e ad un pin PWM, tramite cui lo controlleremo. Figure 10: Simbolo servo nello schema 15 2.4 2.4.1 Laser e altoparlanti Laser I due laser che useremo nel progetto, uno rosso e uno verde, possiamo vederli come black box attivabili con una certa tensione. Per quanto riguarda il laser rosso, esso necessita di 5 volt per accendersi, quindi ci basterà collegare il filo rosso ad uno qualsiasi dei pin digitali; mentre il laser verde ha bisogno di ∼ 3.3 volt. La soluzione è quindi mettere un transistor comandato da un pin digitale, alimentato dal pin sempre acceso a 3.3 volt presente su Arduino. Figure 11: Laser nel nostro schema: resistenza interna + LED 2.4.2 Transistor Il transistor è uno dei componenti attivi più importanti dell’elettronica digitale. Il suo funzionamento è complesso; ci limitiamo qui a dire l’indispensabile per il suo utilizzo. Innanzitutto quello utilizzato nel progetto è un transistor NPN. Nella figura 12 viene mostrato il suo aspetto più comune e il simbolo circuitale. Viene da noi utilizzato come un semplice interruttore tra la tensione applicata al collettore (+) e all’emettitore (-), comandata dalla base, collegata quindi ad uno dei pin digitali di Arduino tramite una resistenza. Infatti una corrente eccessiva applicata alla base può danneggiare il transistor. 2.4.3 Altoparlante Un altoparlante è un componente in grado di trasformare un onda elettromagnetica in un onda sonora. Collegandolo ad un pin PWM trasformerà l’onda quadra in un segnale udibile. In questo modo saremo in grado di dare un feedback al paziente che sta eseguendo il test sull’andamento dello stesso. Modificando il duty cycle della PWM emetteremo note differenti. 16 Figure 12: Transistor npn Figure 13: Altoparlante 2.5 Wii Nunchuck Il nunchuck della Nintendo è un comodo ed economico dispositivo comprendente un set di sensori; in questo caso risultano perfetti per l’interazione del paziente con Arduino. Abbiamo a disposizione due pulsanti, B e Z, un joypad ed un accelerometro. Questo comunica con l’interfaccia I 2 C, protocollo di comunicazione sincrono, presente in Arduino. Il nunchuck è dotato di un connettore proprietario ovviamente non collegabile direttamente con la board; dovremo quindi tagliare il cavo e collegare direttamente i fili ai pin corretti. Su internet è semplice reperire informazioni su questo: una volta tagliato si vedranno 4 fili di colore diverso. Rosso corrisponde all’alimentazione (+), bianco alla massa, giallo al clock (pin A5 sulla board), green al data (pin A4). 2.6 Schema circuitale completo Per la realizzazione del dispositivo è stata utilizzata una prototype shield, ovvero una piccola board che è possibile installare direttamente su Arduino e che permette saldature direttamente su di essa. Nella figura 15 viene 17 Figure 14: Nintendo Wii Nunchuck mostrato lo schema completo del nostro dispositivo. Nota: nell’immagine il cavo bianco del nunchuck è disegnato con colore nero. 18 Figure 15: Schema circuitale completo 19 Figure 16: Dispositivo completo 3 3.1 3.1.1 Realizzazione del software Drivers Introduzione Realizzare i driver dei vari componenti collegati ad Arduino non ha niente a che vedere con la creazione degli stessi sui moderni sistemi operativi. Arduino infatti non ha SO: semplicemente avvia una funzione setup all’accensione e poi continua ad ciclare con la funzione loop. Non avendo meccanismi di protezione per l’accesso all’ISA (Instruction Set Architecture) come User e Protected Mode, abbiamo direttamente a disposizione primitive in grado di controllare i pin digitali e analogici. Realizzare un driver sarà equivalente a creare delle funzioni che determinino un interfaccia ad un certo attuatore o sensore. In genere un driver viene scritto sotto forma di libreria in c, inclusa dal main di un programma e aggiunta al compile path dell’IDE. Tuttavia può essere creato anche uno sketch in Processing, modalità forse più semplice ma meno 20 adatta al riutilizzo. 3.1.2 Servomotori Fortunatamente Arduino fornisce un’interfaccia generica per i servomotori controllati in PWM, Servo.h, di cui si trova la documentazione al seguente link: http://arduino.cc/en/Reference/Servo. Listing 1: esempio di uso 1 2 3 4 5 Servo servo ; // a l l o c o s t a t i c a m e n t e l ’ o g g e t t o servo . attach (3 ,700 ,2300); // p i n 3 e mappa l i n e a r e servo . write (75); // a n g o l o 75 g r a d i delay (1000); // w a i t f o r 1 s e c s e r v o . w r i t e M i c r o s e c o n d s ( 1 4 0 0 ) ; // a n g o l o e q u i v 1 4 0 0 Il listato 1 è un semplice esempio d’uso. L’oggetto servo viene allocato staticamente poichè non è possibile farlo dinamicamente, infatti non è presente la keyword new nel compilatore customizzato per microcontrollori AVR. Con il metodo attach costituiamo una nuova mappa lineare a partire dai valori relativi a 0, π, che nel nostro caso sono 700, 2300µs; infatti 1500µs corrispondono al centro π2 e uno spostamento equivalente ad un angolo di π4 viene effettuato con un decremento o incremento di 400µs del τ della PWM, come scritto nel datasheet. Per meglio capire il relazione tra microsecondi, angoli e la funzione di mappatura qui sotto è riportata una sezione di codice presa del file Servo.cpp. Listing 2: Dai gradi ai microsecondi 286 287 288 289 290 291 292 293 294 295 void S e r v o : : w r i t e ( i n t v a l u e ) { i f ( v a l u e < MIN PULSE WIDTH) { i f ( value < 0) value = 0 ; i f ( value > 180) value = 180; v a l u e = map ( v a l u e , 0 , 1 8 0 , SERVO MIN ( ) , } t h i s −>w r i t e M i c r o s e c o n d s ( v a l u e ) ; } SERVO MAX ( ) ) ; Quando si passa al metodo write un angolo in gradi sessagesimali, viene trasformato l’angolo nel valore corrispondente in microsecondi secondo la mappa di quello specifico servomotore. Servo motori diversi hanno spesso mappatura differente. Qui di seguito la definizione della funzione map. Listing 3: Funzione di mappatura scritta in c long map ( long x , long i n m i n , long in max , long out min , long out max ) { return ( x − i n m i n ) ∗ ( out max − o u t m i n ) / ( in max − i n m i n ) + o u t m i n ; } Poichè i servo motori sono in coppia si è creata la libreria Aimer.h (4) che è semplicemente un wrapper di due servo; per la gestione di punti da mirare invece la libreria Point.h (5). In questo modo si potrà operare ad alto livello 21 come mostrato nel listato 6. Si noti che l’allocazione dinamica della memoria per le strutture è consentita, infatti le funzioni malloc, free sono disponibili. Listing 4: ottica/Sketch/Libraries/Aimer/Aimer.h 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #i f n d e f AIMER H #d e f i n e AIMER H 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #i f n d e f POINT H #d e f i n e POINT H #include ” Arduino . h” #include ” P o i n t . h” #include ” S e r v o . h” c l a s s Aimer { public : Aimer ( ) ; /∗ t h e f o l l o w i n g f u n c t i o n s a r e j u s t w r a p p e r s ∗/ void a t t a c h ( i n t panPin , i n t t i l t P i n ) ; void a t t a c h ( i n t panPin , i n t t i l t P i n , i n t panMin , i n t panMax , i n t t i l t M i n , void w r i t e M i c r o s e c o n d s ( i n t mpan , i n t m t i l t ) ; /∗ t a k e s a P o i n t p and a i m s i t ∗/ void w r i t e M i c r o s e c o n d s ( P o i n t p ) ; /∗ r e a d s t h e p o s i t i o n s o f t h e s e r v o s and w r i t e s them i n c o n t a i n e r ∗/ void r e a d M i c r o s e c o n d s ( P o i n t c o n t a i n e r ) ; private : S e r v o pan ; Servo t i l t ; }; #e n d i f Listing 5: ottica/Sketch/Libraries/Point/Point.h #include <s t d i o . h> #include < s t d l i b . h> #include < s t r i n g . h> #d e f i n e NSSIZE 7 struct point { int x , y ; struct point ∗ next ; }; typedef s t r u c t p o i n t ∗ P o i n t ; /∗ i t t a k e s a c o u p l e o f i n t and r e t u r n s a p o i n t e r t o s t r u c t p o i n t ∗/ P o i n t p o i n t B u i l d e r ( int , i n t ) ; /∗ i t t a k e s a p o i n t e r t o s t r u c t p o i n t and d e l e t e s t h a t p o i n t and a l l l i n k e d i n i t ( ∗ n e x t ) ∗/ void d e s t r o y ( P o i n t ) ; /∗ i t t a k e s t w o p o i n t e r t o s t r u c t p o i n t and add t h e f o r m e r t o t h e l a t t e r ( ∗ n e x t ) ∗/ void add ( P o i n t , P o i n t ) ; /∗ i t t a k e s a p o i n t e r t o s t r u c t p o i n t and a p o i n t e r t o a b u f f e r ; p r i n t i n i t t h e v a l u e s i n s i d e t h e p o i n t i n t h i s way . e x a m p l e : ” 1 0 0 , 2 0 0 ” ∗/ void t o S t r i n g ( P o i n t , char ∗ ) ; /∗ t h e same a s a b o v e b u t i t p r i n t s a l l t h e p o i n t s l i n k e d t h r o u g h ∗ n e x t . b e s u r e t h e b u f f e r i s b i g e n o u g h ∗/ void a l l T o S t r i n g ( P o i n t , char ∗ ) ; /∗ i t t a k e s a p o i n t e r t o a b u f f e r w i t h i n , f o r e x a m p l e : ” 5 2 , 1 2 0 ” and c r e a t e s t h e r e l a t i v e p o i n t ∗/ P o i n t c r e a t e P o i n t ( char ∗ ) ; /∗ t h e same a s a b o v e b u t i t t a k e s more t h a n one p o i n t . f o r e x a m p l e ” 5 2 , 1 3 0 , 5 0 0 , 2 5 0 ” ∗/ P o i n t c r e a t e P o i n t s ( char ∗ ) ; /∗ a u x i l i a r y f u n c t i o n ∗/ char ∗ s t r t r i m ( char ∗ ) ; #e n d i f 22 int tiltMax ) ; Listing 6: Utilizzo combinato di Aimer.h e Point.h Aimer a i m e r 1 ; aimer1 . attach ( 3 , 4 , 7 0 0 , 2 3 0 0 , 7 0 0 , 2 3 0 0 ) ; P o i n t head = c r e a t e P o i n t s ( ” 1 4 0 0 , 1 5 0 0 , 1 3 0 0 , 1 4 0 0 , 1 5 0 0 , 1600 ” ) ; P o i n t p = head ; while ( p!=NULL) { aimer1 . writeMicroseconds ( p ) ; // aim p delay (1000); // w a i t 1 s e c p = p−>n e x t ; // n e x t p o i n t } d e s t r o y ( head ) ; // f r e e p o i n t l i s t memory 3.1.3 Nunchuck Abbiamo già accennato nella sezione riguardante l’HW che il Nunchuck utilizza il protocollo di comunicazione sincrono I 2 C. Nell’environment Arduino la libreria Wire.h (http://arduino.cc/en/Reference/Wire) fornisce delle primitive per la comunicazione tramite questo protocollo. Fortunatamente si è trovato sulla rete uno sketch capace di ricevere tutte le informazioni utili dai sensori. La versione non era recente quindi non compilava. Lavorando sul porting di questo sketch si è arrivati al risultato sperato, mostrato sotto. Listing 7: ottica/Sketch/Rino/Nunchuck.ino 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <Wire . h> #include <P o i n t . h> #d e f i n e JOYPAN 132 #d e f i n e JOYTILT 126 #d e f i n e POLLING DELAY2 15 // // Nunchuck // static functions uint8 t nunchuck buf [ 6 ] ; // a r r a y to store nunchuck data , // i n i t i a l i z e t h e I2C s y s t e m , j o i n t h e I2C b u s , // and t e l l t h e n u n c h u c k we ’ r e t a l k i n g t o i t void n u n c h u c k i n i t ( ) { Wire . b e g i n ( ) ; // j o i n i 2 c b u s a s m a s t e r Wire . b e g i n T r a n s m i s s i o n ( 0 x52 ) ; // t r a n s m i t t o d e v i c e 0 x 5 2 Wire . w r i t e ( 0 x40 ) ; // s e n d s memory a d d r e s s Wire . w r i t e ( 0 x00 ) ; // s e n d s s e n t a z e r o . Wire . e n d T r a n s m i s s i o n ( ) ; // s t o p t r a n s m i t t i n g } // Send a r e q u e s t f o r d a t a t o t h e n u n c h u c k // was ” s e n d z e r o ( ) ” void n u n c h u c k s e n d r e q u e s t ( ) { Wire . b e g i n T r a n s m i s s i o n ( 0 x52 ) ; // t r a n s m i t t o Wire . w r i t e ( 0 x00 ) ; // s e n d s one b y t e Wire . e n d T r a n s m i s s i o n ( ) ; // s t o p t r a n s m i t t i n g } d e v i c e 0 x52 // R e c e i v e d a t a b a c k f r o m t h e n u n c h u c k , int nunchuck get data ( ) { i n t c n t =0; Wire . r e q u e s t F r o m ( 0 x52 , 6 ) ; // r e q u e s t d a t a f r o m n u n c h u c k while ( Wire . a v a i l a b l e ( ) ) { // r e c e i v e b y t e a s an i n t e g e r n u n c h u c k b u f [ c n t ] = n u n c h u k d e c o d e b y t e ( Wire . r e a d ( ) ) ; c n t ++; } nunchuck send request ( ) ; // s e n d r e q u e s t f o r n e x t d a t a p a y l o a d // I f we r e c i e v e d t h e 6 b y t e s , t h e n g o p r i n t them i f ( c n t >= 5 ) { return 1 ; // s u c c e s s } 23 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 return 0 ; // f a i l u r e } // Encode d a t a t o f o r m a t t h a t most w i i m o t e d r i v e r s e x c e p t // o n l y n e e d e d i f you u s e one o f t h e r e g u l a r w i i m o t e d r i v e r s char n u n c h u k d e c o d e b y t e ( char x ) { x = ( x ˆ 0 x17 ) + 0 x17 ; return x ; } // I ’m u s i n g P o i n t l i b r a r y Point getJoy (){ nunchuck get data ( ) ; d e l a y (POLLING DELAY2 ) ; i n t d i f f p a n = n u n c h u c k b u f [ 0 ] − JOYPAN; i n t d i f f t i l t = n u n c h u c k b u f [ 1 ] − JOYTILT ; return p o i n t B u i l d e r ( d i f f p a n , d i f f t i l t ) ; } int getZ ( ) { nunchuck get data ( ) ; d e l a y (POLLING DELAY2 ) ; i f ( ! ( ( n u n c h u c k b u f [ 5 ] >> 0 ) & 1 ) ) return 1 ; return 0 ; } i n t getC ( ) { nunchuck get data ( ) ; d e l a y (POLLING DELAY2 ) ; i f ( ! ( ( n u n c h u c k b u f [ 5 ] >> 1 ) & 1 ) ) return 1 ; return 0 ; } La funzione nunchuck init effettua l’handshake mentre nunchuck send request inivia al nunchuck la richiesta di una nuova rilevazione. Tramite nunchuck get data avviene la ricezione dei dati richiesti e la bufferizzazione all’interno dell’array nunchuck buf. Sono state poi dai noi aggiunte delle funzioni che permettono, con semplicità, di leggere il valore dei due bottoni Z, C e gli spostamenti del joystick. La funzione getJoy restituisce, all’interno dei una struttura point, i valori x, y di scostamento dalla posizione neutrale del joystick. Per far ciò si chiama subito nunchuck get data, si attende per 15 ms, misura introdotta per stabilità del driver, e si sottraggono i primi due byte del buffer ai rispettivi valori neutrali definiti nelle macro JOYPAN e JOYTILT (x, y). Le due funzioni getZ, getC non fanno altro che restituire 1 se il relativo tasto è premuto, 0 altrimenti. Nonostante le informazioni per scrivere queste funzioni siano state apprese leggendo lo sketch di esempio, tutto poteva essere compreso tramite il datasheet relativo. 24 3.1.4 Altoparlante Abbiamo già detto nella sezione HW che possiamo emettere differenti tonalità a seconda del valore del duty cycle τ della PWM che controlla l’altoparlante. Sfruttando quindi la funzione tone, già fornita dalle Arduino API, si sono create delle funzioni per dare un feedback in risposta a certi eventi. 25 Listing 8: ottica/Sketch/Rino/Sound.ino 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include ” p i t c h e s . h” #d e f i n e ALTO 8 void f e e d b a c k ( ) { t o n e (ALTO, NOTE C6 , } 300); void e r r o r ( ) { t o n e (ALTO, NOTE E2 , } 500); void c h a n g e l a s e r ( ) { t o n e (ALTO, NOTE A5 , 3 0 0 ) ; t o n e (ALTO, NOTE B5 , 3 0 0 ) ; } void end ( ) { t o n e (ALTO, NOTE B5 , t o n e (ALTO, NOTE A5 , } 3.2 300); 300); La comunicazione seriale Per poter scambiare informazioni tra il computer e il microprocessore all’interno di Arduino dobbiamo stabilire una comunicazione seriale. Figure 17: comunicazione seriale. Tuttavia i moderni computer difficilmente sono equipaggiati di una porta seriale. (figura 18) La comunicazione con le periferiche avviene ormai prevalentemente tramite l’USB. L’Universal Serial Bus è però un protocollo ”pesante” da gestire e l’ATmega328 non ha un controller build-in al suo interno. Per semplificare il tutto, Arduino utilizza una comunicazione seriale simulata tramite USB. Questo significa che sulla board il protocollo USB viene tradotto in seriale e letto da pin specifici del microcontrollore. Grazie a questo artefatto, saremo in grado di comunicare con il microcontrollore esattamente come se avessimo una porta seriale RS232 nel nostro computer. Nei sistemi operativi discendenti da UNIX, quindi OSX e Linux, l’I/O è permesso dal sistema operativo tramite dei file speciali, detti device files, presenti nella directory /dev/. Questi si dividono in characters e blocks special files. I primi 26 Figure 18: Porta seriale RS232. servono alla comunicazione con i terminali e i vecchi modem e stampanti; i secondi con dischi fissi e memorie di vario tipo. La seriale simulata tramite USB renderà disponibile un device file di tipo characters. Per meglio capire come funziona la comuncazione seriale tramite questi file, vediamo un esempio con quelli associati ai terminali. Listing 9: ttys000 1 2 3 4 $tty / dev / t t y s 0 0 0 h e l l o world ! $ 1 2 3 $tty / dev / t t y s 0 0 1 $ e c h o −e ” h e l l o Listing 10: ttys001 w o r l d ! \ n” >> / dev / t t y s 0 0 0 Si aprano due terminali, in seguito rappresentati dai listati 9 e 10. Eseguendo il comando tty viene stampato il nome del device file associato a quel terminale. Nel nostro caso (OSX) vengono fuori i nomi alle righe 2 di entrambi i terminali, ma possono essere differenti. Si vada nel secondo terminale ed si esegua il comando alla riga 3 del 10 cambiando il nome del file finale di modo da andare a scrivere sul file relativo all’altro terminale. Il comando va semplicemente ad appendere sul file specificato la stringa passata come argomento. Fatto questo si guardi cosa è successo nel listato 9. Ciò che è stato scritto sul device file /dev/ttys000 viene riportato dal terminale. Una volta collegato Arduino in /dev/ apparirà un file relativo, con un nome diverso per sistema operativo, che ci permetterà di comunicare col microcontrollore esattamente in questo modo. Nel dettaglio, ogni volta che si connette un usb ad una porta del computer, per prima cosa viene comunicato un codice produttore e un codice prodotto. Solo se il kernel ha al suo interno un driver associato viene creato lo special file. Per agevolare questo tipo di comunicazione, usare un programma adatto. (es. picocom, minicom, screen) Proviamo adesso a comunicare direttamente con Arduino. 27 Listing 11: ottica/Sketch/SerialReceiver/SerialReceiver.ino 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int led = 13; void s e t u p ( ) { S e r i a l . begin (9600); pinMode ( l e d , OUTPUT) ; } void l o o p ( ) { i f ( S e r i a l . a v a i l a b l e ( ) > 0) { char c = S e r i a l . r e a d ( ) ; d i g i t a l W r i t e ( l e d , HIGH ) ; delay (500); d i g i t a l W r i t e ( l e d , LOW) ; delay (500); Serial . print ( c ); } } Caricare lo sketch 11 in Arduino. Aprire il terminale e iniziare una comunicazione seriale con esso. $ p i c o c o m / dev / t t y . usbmodem411 Scrivete ”hello world”. I caratteri verranno inviati a Arduino; questo, per ognuno di essi, farà lampeggiare un led su di esso e rimanderà indietro il carattere. Avrete quindi l’impressione che ciò che scrivete venga mostrato a schermo molto lentamente. Si noti che, per ricevere ed inviare caratteri da Arduino si fa uso della classe Serial. http://arduino.cc/en/Reference/Serial 3.3 3.3.1 La calibrazione e i metodi di conversione Premessa Le figure 19, 20 mostrano dei valori registrati senza alcun tipo di manipolazione relativi ad una stessa serie di 16 punti, disposti in quattro colonne sullo schermo di Hess/Lancaster. Questi indicano i µs di duty cycle τ equivalenti agli angoli assunti per poterli mirare. La figura 19 rappresenta i valori estrapolati dai servomotori che guidano il laser sinistro (verde), la figura 20 quelli del laser destro (rosso). Il test è stato effettuato ad una distanza di 60cm. L’osservazione di questi valori dà un’idea della difficoltà di tradurre questi angoli in coordinate piane. 28 1253 10cm 1341 1419 1504 981 976 983 1253 1339 1417 1501 1039 1030 1029 1032 1252 1336 1418 1504 1097 1093 1088 1093 1252 1339 1415 1501 1158 1154 1154 1154 987 10cm n Figure 19: Insieme di valori m , dove n rappresenta il valore pan mentre m tilt, relativi a 16 punti disposti a griglia, come sopra. I valori sono espressi in microsecondi equivalenti. Laser sinistro. 1556 10cm 1644 1721 1801 1065 1073 1087 1554 1636 1718 1797 1114 1121 1127 1138 1551 1636 1712 1792 1171 1172 1182 1190 1547 1629 1706 1787 1239 1239 1249 1254 1062 10cm n Figure 20: Insieme di valori m , dove n rappresenta il valore pan mentre m tilt, relativi a 16 punti disposti a griglia, come sopra. I valori sono espressi in microsecondi equivalenti. Laser destro. 3.3.2 Il problema Affinché i laser possano mirare punti determinati da coordinate piane sullo schermo di Hess/Lancaster e affinchè si possa risalire alle coordinate piane 29 dei punti mirati a partire dagli angoli assunti, è necessario trovare un appropriato metodo di conversione ed effettuare una appropriata calibrazione dello strumento. La figura 21 ci mostra la situazione attuale. O rappresenta uno dei due laser. O0 è la proiezione ortogonale di O sul piano yz. Questa verrà chiamata origine reale ed è differente per ogni laser. P è il generico punto che desideriamo mirare. Innanzitutto abbiamo bisogno di un sistema di coordinate comune ai due laser. E’ stato quindi introdotto il sistema coordinate virtuali, le quali hanno origine in D, detto origine virtuale, per entrambi i laser. P avrà coordinate (px , py ), ovvero la distanza in mm tra P e D nelle due componenti. La distanza tra D e O0 sarà invece etichettata con (cx , cy ). Il nostro obiettivo è quello di trovare una funzione φ : Z 2 → [mlb, mub]2 che, date le coordinate virtuali (px , py ), restituisca gli angoli (α, β), espressi nei µs equivalenti, e la funzione inversa φ−1 : [mlb, mub]2 → Z 2 che date invece una coppia di angoli (α, β) espressi in µs restituisca le coordinate virtuali (px , py ) del punto mirato. Con mlb, mub si indicano i valori in microsecondi relativi a 0 e π di un dato servomotore. Ogni puntatore otterrà risultati diversi a seconda dei parametri caratterizzanti dati alle sopracitate funzioni. Nei successivi paragrafi verranno mostrati i vari tentativi per trovare questa funzione e la sua inversa. 3.3.3 Interpolazione Con il termine interpolazione si intende un metodo per definire una funzione incognita a partire da alcuni punti campione di cui si conosce l’immagine. La funzione incognita φ : Z 2 → [mlb, mub]2 è particolarmente complessa e non è riducibile a due funzioni φx : Z → [mlb, mub], φy : Z → [mlb, mub] che si occupino delle due componenti poichè esse sono una dipendente dall’altra. Questa dipendenza si vede dalle seguenti relazioni. φx (px ) = α000 6= α φy (py ) = β 00 6= β (2) (3) La non indipendenza può essere formalizzata come segue. φ(px , py ) 6= (φx (px ), φy (py )) (4) A questo si aggiunge il fatto di dover trovare l’inversa φ−1 . Non si è in nessun modo riusciti a risolvere questi problemi, ammesso che esistano delle soluzioni; si è quindi cambiata strategia. 30 z x P py cx D px α y cy β 00 O’ β α 000 y O x Figure 21: Situazione attuale. Un solo puntatore. 3.3.4 Approccio trigonometrico Siano noti i seguenti parametri. d := OO0 (cx , cy ) (5) (6) Per semplicità espositiva definiamo le seguenti abbreviazioni. x := cx + px y := cy + py (7) (8) Riusciamo allora a definire delle funzioni simili a φ, φ−1 ma che misurano gli angoli in radianti. φ0 : Z 2 → [0, π]2 φ0−1 : [0, π]2 → Z 2 31 (9) (10) x y φ0 (x, y) = (arctan( p ), arctan( √ )) = (α, β) d 2 + x2 d2 + y 2 s s 1 + tan2 α ) = (x, y) 1 − tan2 α tan2 β (12) Per completezza seguono le definizioni senza le abbreviazioni 7 e 8; saranno quelle che noi useremo. φ0−1 (α, β) = (d tan α 1 + tan2 β , d tan β 1 − tan2 β tan2 α (11) p x + cx cy + p y φ0 (px , py ) = (arctan( p ), arctan( p )) = (α, β) d2 + (py + cy )2 d2 + (cx + px )2 (13) s s 1 + tan2 α −cy ) = (px , py ) 1 − tan2 α tan2 β (14) Si noti che nella 11, 12, 13, 14 gli angoli sono considerati rispetto alla normale al piano yz mentre ogni servomotore ha l’angolo 0 più o meno parallelo a quel piano. Necessitano quindi di una trasformazione. Nel nostro caso i servomotori tilt partono a calcolare gli angoli dall’alto, di conseguenza al posto di prendere α abbiamo preso il complementare di α. I servomotori pan invece hanno l’angolo 0 diretto completamente a sinistra; è stato scelto di sommare ad ogni β π2 . In questo modo, con α0 , β 0 > 0, dove gli angoli 0 sono gli angoli trasformati, si mira il primo quadrante, in alto a destra rispetto all’origine virtuale D. Queste trasformazioni dipendono da come si sono montati i servomotori e da quale quadrante si voglia considerare quello positivo. Dobbiamo ora trovare delle funzioni m, m−1 che ci permettano di passare dai radianti ai microsecondi e viceversa. φ0−1 (α, β) = (d tan α 2 1 + tan β −cx , d tan β 1 − tan2 β tan2 α map(x) = out max − out min x + out min in max − in min (15) L’equazione 15 è la riscrittura matematica e parametrica della funzione presente nel listato 3. Sostituendo ai parametri di questa quelli dei nostri servomotori siamo in grado di trovare le funzioni di mappatura che fanno al caso nostro. m : [0, π] → [mlb, mub] m : [mlb, mub] → [0, π] −1 32 (16) (17) m(x) = mub − mlb x + mlb π (18) π x (19) mub − mlb dove mub, mlb sono rispettivamente i microsecondi relativi all’angolo π e 0 dei nostri servomotori. Sono stati lasciati parametrici perchè abbiamo bisogno, affinchè i puntatori mirino effettivamente il punto P desiderato, che dato un angolo π2 mirino esattamente la loro proiezione ortogonale O0 . Per far ciò bisogna innanzitutto trovarla con precisione; un trucco è quello di mettere uno specchio sulla parete e spostare il puntatore affinchè il raggio riflesso non illumini esattamente se stesso. Una volta trovato O0 è necessario prendere nota dei valori (µα , µβ ), ovvero degli angoli assunti espressi in microsecondi. Le corrette funzioni di mappatura, una per il servomotore pan e l’altra per il servomotore tilt, si troveranno a partire dai seguenti valori di mub, mlb. m−1 (x) = mubα mlbα mubβ mlbβ = µα + 800 = µα − 800 = µβ + 800 = µβ − 800 (20) (21) (22) (23) dove 800 sono i µs equivalenti ad un angolo π2 per i nostri servomotori. Diversi servomotori avranno valori differenti. Questo procedimento va effettuato per entrambe le coppie di servomotori ovvero per i due puntatori. Abbiamo ora tutto l’occorrente matematico per il nostro obiettivo; mancano solo da trovare i parametri d, cx , cy per ogni puntatore. Il parametro d è la distanza del puntatore dallo schermo. Precedentemente avevamo trovato la posizione di O0 . Un metodo empirico efficace è quello di attaccare a O0 , con dello scotch, un filo, quindi allungarlo fino al puntatore e tagliarlo su di esso. Misurare quindi il filo con un metro. Per trovare cx , cy basta misurare con un metro sulla parete le due componenti della distanza tra D e O0 . Entrambi i procedimenti vanno ripetuti per i due puntatori. 3.3.5 Problemi e riformulazione dei requisiti Nonostante quanto detto nel paragrafo sopra sia stato implementato (si trova nel branch coordinate virtuali del repository del progetto), non si è riusciti, con precisione, a mirare i punti voluti. L’errore massimo è stato misurato 33 in 4cm; tale rende il dispositivo inadatto alle applicazioni mediche. Crediamo che il modello matematico sottostante sia corretto ma che fattori come l’inclinazione dovuta al montaggio manuale dei servomotori e l’imprecisione degli strumenti stessi, che ricordo essere economici, siano causa del fallimento. Infatti, un minimo errore nell’assunzione di un angolo da parte di un servomotore comporta un’ampio scostamento sullo schermo. Sono due le soluzioni possibili: • Un azienda, specializzata in strumenti di misurazione, crea un dispositivo perfettamente calibrato e montato a macchina e applica le funzioni citate in questa sezione. • I punti da mirare vengono mirati prima dall’operatore. All’esecuzione del test Arduino mira i punti precedentemente presi e registra non le coordinate virtuali ma i microsecondi relativi agli angoli assunti dai servomotori per mirare i punti del paziente. Vengono ritornati questi la cui traduzione e correzione viene affidata ad un programma in R. Sappiamo che ciò è possibile e nella sezione 3.3.7 diamo un’idea di come si potrebbe fare. Si è scelti la seconda delegando però l’applicazione del metodo di correzione e traduzione a terzi. I requisiti posso essere quindi riformulati come segue. Il dispositivo dovrà accettare due modalità di esecuzione. La prima è quella relativa alla calibrazione, ove, specificando un numero di punti, l’operatore potrà mirare i punti di riferimento per il test successivo con entrambi i laser. Fatto ciò vengono restituiti i microsecondi equivalenti agli angoli assunti. La seconda modalità è quella di esecuzione del test. Arduino si aspetterà di ricevere gli angoli da far assumere ai due laser per mirare i punti di riferimento, dopodichè comincerà l’esecuzione del test che dovrà registrare gli angoli assunti nei punti mirati dal paziente e restituirli alla fine del test. 3.3.6 I risultati Si anticipano qui i risultati ottenuti effettuando un test ad una distanza di 50 cm, puntando i vertici di un quadrato di lato 30 cm. I punti mirati sono quattro. Con un apice si fa riferimento ai risultati ottenuti dal puntatore sinistro, con due a quello destro. I dati sono espressi in microsecondi relativi agli angoli assunti. 34 Punti di riferimento. 1215 R = 1134 1531 R00 = 1209 0 1210 1464 1465 967 946 1117 1536 1787 1780 1045 1065 1223 (24) (25) Test paziente sano. 1215 1210 1463 1467 A = 1135 966 945 1120 1533 1539 1790 1782 A00 = 1209 1047 1065 1225 0 0 1 2 0 0 |R − A | = 1 1 1 3 2 3 3 2 00 00 |R − A | = 0 2 0 2 0 Ideale paziente che sbaglia a mirare tutti i punti di 5cm verso l’alto. 1212 1209 1465 1466 0 B = 1094 943 922 1080 1530 1540 1791 1780 00 B = 1174 1019 1037 1188 3 1 1 1 0 0 |R − B | = 40 24 24 37 1 4 4 0 00 00 |R − B | = 35 26 28 35 (26) (27) (28) (29) (30) (31) (32) (33) Ideale paziente che sbaglia a mirare tutti i punti di 5 cm in orizzontale. 1176 1176 1410 1412 0 C = (34) 1139 973 947 1119 1485 1490 1741 1732 00 C = (35) 1211 1048 1056 1220 39 34 54 53 0 0 |R − C | = (36) 5 6 1 2 46 46 46 48 00 00 |R − C | = (37) 2 3 9 3 I test sono coerenti con quanto aspettato ed evidenziano gli errori di mira. Tuttavia, a pari scostamento, si nota una grande differenza tra i punti (espressi 35 dalle colonne) 1,4 e 2,3 nelle matrici 32, 33, 36, 37. Questo significa che la correzione dei dati è assolutamente necessaria e non rimovibile, a meno che non si faccia un’analisi puramente quantitativa. 3.3.7 Approccio per fasce Figure 22: Spreadsheet - approccio fasce Nella figura 22 viene mostrata una griglia relativa i dati estrapolati dal puntamento, da parte di uno solo dei due laser, di 25 punti, disposti in righe da 5, tutti a 10 cm di distanza dai vicini. I dati sono puri, non vi è quindi stata fatta alcuna correzione. Nelle colonne B,E,H,K,N, alle righe 7,13,18,24,30, i valori mostrati con sfondo bianco rappresentano gli angoli assunti dal servomotore pan, espressi in µs; mentre nelle colonne C,F,I,L,O, alle righe 8,14,19,25,31, sempre con sfondo 36 bianco, sono rappresentati i corrispondenti valori tilt. Le celle a sfondo blu rappresentano il punto D, origine virtuale (0, 0). La griglia viene divisa in quattro fasce orizzontalmente e quattro fasce verticalmente. Per far rifermento ad una fascia useremo la notazione F (X, Y ), dove X, Y saranno le lettere o i numeri corrispondenti alle colonne o alle righe nello spreadsheet. I valori con sfondo verde chiaro si trovano tra le misure tilt e ne calcolano la differenza, mentre quelli in giallo fanno lo stesso per quelle pan. I valori grigi mostrano le medie, per riga e per colonna, di questi scostamenti e le chiameremo δF (XY ) , mentre quelli in arancione la media dei valori pan, tilt, che referenzieremo con X, dove X sarà una lettera relativa ad una colonna o un numero relativo ad una riga. Le celle in verde scuro mostrano le deviazioni standard dei valori della riga o della colonna corrispondente. Il test è stato effettuato ad una distanza di 60 cm e la griglia di punti aveva la riga inferiore ad un’altezza maggiore di quella del dispositivo, mentre la colonna H si trovava approssimativamente al centro relativo della parete. Questa disposizione della griglia è subito riscontrata nei dati, infatti, se guardiamo le deviazioni standard prese sulle colonne, notiamo che queste sono molto basse, se invece guardiamo quelle delle righe, ci accorgiamo che queste sono molto maggiori e aumentano più ci si sposta lontani dalla proiezione ortogonale dei laser sulla parete. Nella tabella B35 - K38, vengono mostrati i risultati della conversione empirica dei valori pan, tilt in coordinate virtuali. Per verificare se il metodo è corretto, sono stati presi dei punti campione p0 , p1 , p2 (celle B36,B37,B38) a distanze note dall’origine virtuale (colonne px , py della stessa tabella). Sono stati poi presi i valori degli angoli αp , βp , sempre mostrati nelle colonne denominate pan ms, tilt ms. Le colonne p0x , p0y mostrano i valori ottenuti dal processo di conversione di angoli in coordinate piane che spieghiamo qui con un esempio. Il punto p0 appartiene ad una fascia orizzontale e ad una fascia verticale come mostrato dalle seguenti proposizioni. (B ≤ αp0 ≤ E) → p0 ∈ F (B, E) (8 ≤ βp0 ≤ 14) → p0 ∈ F (8, 14) (38) (39) Sappiamo che la colonna E dista da H, quella dell’origine, 10cm. Poichè p0 sta nella nella fascia F (B, E) significa che la distanza tra p0 e l’origine è ≥ 10cm. Per sapere il valore da sommare a dieci prendiamo lo scostamento tra αp0 e la colonna E , ovvero dp0 E = |αp0 − E|. In questa fascia sappiamo che 10 cm sono equivalenti a δF (BE) quindi per trovare questa distanza in 37 d 0E centimetri facciamo δFp(BE) ∗ 10. Il risultato si trova nella cella H36. Per il tilt si segue un procedimento analogo. Il segno è stato deciso intuitivamente, determinato il quadrante positivo. Nelle colonne etichettate |p0x − px | |p0y − py | si valuta la bontà di queste conversioni rispetto alle distanze reali. Possiamo osservare che mentre le misure pan risultano buone, quelle di tilt non lo sono. Questo accade proprio perchè la griglia è posizionata ad un’altezza maggiore del dispositivo; il punto p2 , infatti, che si trova più in basso, e quindi più vicino alla proiezione ortogonale del laser, presenta una differenza dalla distanza reale < 1cm mentre quelle di p1 , p0 sono > 1.5cm. Concludiamo quindi che questo metodo, sopra mostrato molto empiricamente, può funzionare a patto che: • Il punto centrale dello schermo di Hess/Lancaster si trovi il più possibile vicino alle proiezioni ortogonali dei laser; • Le fasce siano fitte; • La distanza e il dispositivo non vengano spostati poichè altrimenti si dovrebbe ricostruire l’intero modello di correzione e conversione degli angoli in coordinate piane. 3.4 3.4.1 Lo sketch finale Inclusioni, macro, allocazioni statiche permanenti Nel listato 12 viene mostrata la parte iniziale dello sketch Rino.ino (nome scelto per il dispositivo) in cui vengono fatte delle inclusioni di librerie, delle definizioni di macro e allocazione di risorse statiche. Listing 12: ottica/Sketch/Rino/Rino.ino 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include #include #include #include #include ” P o i n t . h” ” S e r v o . h” ” p i t c h e s . h” ” MemoryFree . h” ” Aimer . h” #d e f i n e DEBUG MODE 0 #d e f i n e SERIALFREQ 9600 // c a s e s w i t c h #d e f i n e RED 0 #d e f i n e GREEN 1 #d e f i n e #d e f i n e #d e f i n e #d e f i n e #d e f i n e #d e f i n e RIGHTPAN 2 RIGHTTILT 3 LEFTPAN 4 LEFTTILT 5 REDLASER 11 GREENLASER 12 #d e f i n e BPSIZE 14 #d e f i n e BTSIZE 200 38 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 // d e l a y #d e f i n e #d e f i n e #d e f i n e #d e f i n e per s t a b i l i t a ’ : senza l a DELAY BETWEEN TESTS 15 DELAY BETWEEN POINTS 250 SOUND DELAY 300 POLLING DELAY 15 seriale crasha #d e f i n e WHO STARTS 0 #d e f i n e JOYPAD SENSIBILITY 20 Aimer r i g h t A i m e r ; Aimer l e f t A i m e r ; P o i n t p1 , p2 , r e s 1 , r e s 2 ; char ∗ b u f f e r p a r z ; char ∗ b u f f e r t o t ; Vengono incluse le due librerie da noi create Point.h, Aimer.h al fine di operare a più alto livello sui puntatori e sulle coordinate dei punti. Viene caricata inoltre MemoryFree.h, da noi solo impacchettata. La libreria Servo.h è già inclusa nell’Arduino IDE e viene utilizzata da Aimer.h. pitches.h non è altro che un insieme di macro che definiscono le frequenze corrette per emettere determinate note tramite altoparlante; non è rilasciata come libreria Arduino ma è semplicemente un file copiato nel nostro progetto. Vengono quindi definite una serie di macro di cui la prima è DEBUG MODE; se settata ad 1 vengono mandate via seriale un insieme di informazioni utili al debug come la memoria libera in un determinato istante o il punto mirato appena il paziente ne ha dato conferma. E’ molto importante infatti verificare di aver deallocato quanto allocato dinamicamente; ciò può essere fatto controllando la quantità di memoria all’inizio dell’esecuzione di ogni iterazione del loop. Si è di fronte ad una corretta gestione della memoria solo quando questa rimane costante. Di seguito è definita la velocità di comunicazione seriale. 9600 è il valore di default ma può agevolmente essere cambiato. Molte procedure vengono ripetute per entrambi i laser come calibrate o execute; per comunicargli quindi quale dei due laser utilizzare gli passiamo come parametro un intero, infatti le definizioni RED, GREEN associano ogni laser ad un numero. Le successive macro associano un dispositivo al pin relativo che lo controlla, ad esempio, RIGHTPAN viene controllato dal pin 2 come GREENLASER dal pin 12. Con BPSIZE, BTSIZE definiamo le dimensioni di due buffer i cui puntatori vengono dichiarati a linea 38,39. La necessità di creare dei buffer nasce da diversi fatti. Per un oculato utilizzo delle risorse si è preferito utilizzare gli array di caratteri, C-style, anzichè la classe String fornita da Processing. Allocare e deallocare però lo spazio necessario all’invio o alla ricezione di un messaggio via seriale aumenta la possibilità di dimenticarsi di deallocare qualcosa; poichè la comunicazione avviene frequentemente, soprattutto in debug mode, si è pensato di creare due buffer SW sempre disponibili: un buffer di dimensioni molto limitate, buffer parz, e uno di dimensioni più elevate, buffer tot. All’utilizzatore il compito di 39 ripulire il buffer dopo averne fatto uso. Un’altra ragione che ha motivato la creazione dei buffer SW è stata la limitatezza di quello HW, 64 byte; infatti, quando dobbiamo comunicare i punti da mirare o quelli mirati, si eccede spesso questo limite. Quando si scrive codice a bassissimo livello, effettuando comandi o richieste di dati a periferiche, spesso è necessario introdurre dei tempi di delay: una periferica potrebbe non tollerare una frequenza di polling troppo elevata. Le quattro macro definite tra la riga 26 e 29 hanno proprio questo scopo all’interno del codice. Con la macro POLLING DELAY ci si riferisce al polling effettuato sul nunchuck, mentre con WHO STARTS si decide con quale laser si vuol partire. Dell’ultima macro definita JOYPAD SENSIBILITY si parlerà più avanti. (vedi 3.4.7) Ci sono variabili che non ha senso allocare e deallocare ogni volta che si cicla, per questa ragione ne sono definite qui alcune. Vengono allocati staticamente i puntatori laser, aimer, e alcune variabili teste di liste di punti: p1,p2 quelle definite dall’operatore, res1,res2 quelle mirate dal paziente. 3.4.2 Setup Nel codice sotto (13) viene inizializzata la seriale e la comunicazione I 2 C col nunchuck, settati i pin relativi ai laser per essere usati in modalità output digitale, collegati i puntatori ai pin dei servo motori che li compongono, allocati i buffer SW e ripuliti. Listing 13: ottica/Sketch/Rino/Rino.ino 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 void s e t u p ( ) { S e r i a l . b e g i n (SERIALFREQ ) ; // h a n d s h a k e n u n c h u c k nunchuck init ( ) ; // l a s e r s pinMode (REDLASER, OUTPUT) ; pinMode (GREENLASER, OUTPUT) ; // b i n d i n g r i g h t A i m e r . a t t a c h (RIGHTPAN, RIGHTTILT ) ; l e f t A i m e r . a t t a c h (LEFTPAN, LEFTTILT ) ; // a l l o c a t i n g and p r e p a r i n g b u f f e r s b u f f e r p a r z = ( char ∗ ) m a l l o c ( BPSIZE ) ; b u f f e r t o t = ( char ∗ ) m a l l o c ( BTSIZE ) ; memset ( b u f f e r p a r z , 0 , BPSIZE ) ; memset ( b u f f e r t o t , 0 , BTSIZE ) ; } 3.4.3 Il controller dell’applicazione Il loop non fa altro che chiamare la funzione launcher, decisiva nello sketch, e applicare un delay tra un’iterazione e l’altra. 40 Listing 14: ottica/Sketch/Rino/Rino.ino 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 void l o o p ( ) { i f (DEBUG MODE) S e r i a l . p r i n t l n ( ” S t a r t e d ! ” ) ; launcher ( ) ; d e l a y (DELAY BETWEEN TESTS ) ; } void l a u n c h e r ( ) { i n t pnt nmb ; i f (DEBUG MODE) { S e r i a l . p r i n t ( ” i n i t i a l f r e e memory v a l u e : ” ) ; S e r i a l . p r i n t l n ( freeMemory ( ) ) ; } creceive ( buffer parz ); switch ( b u f f e r p a r z [ 0 ] ) { case ’ c ’ : i f (DEBUG MODE) S e r i a l . p r i n t l n ( ” c a l i b r a t e ” ) ; pnt nmb = a t o i ( b u f f e r p a r z + 1 ) ; c l e a n ( b u f f e r p a r z , BPSIZE ) ; r e s 1 = c a l i b r a t e (WHO STARTS, pnt nmb ) ; d e l a y (SOUND DELAY ) ; change laser ( ) ; r e s 2 = c a l i b r a t e ( 1 − WHO STARTS, pnt nmb ) ; d e l a y (SOUND DELAY ) ; end ( ) ; allToString ( res1 , b u f f e r t o t ) ; Serial . println ( buffer tot ); c l e a n ( b u f f e r t o t , BTSIZE ) ; destroy ( res1 ) ; allToString ( res2 , b u f f e r t o t ) ; Serial . println ( buffer tot ); c l e a n ( b u f f e r t o t , BTSIZE ) ; destroy ( res2 ) ; break ; case ’ s ’ : i f (DEBUG MODE) S e r i a l . p r i n t l n ( ” t e s t ” ) ; t u r n o n l a s e r (RED ) ; t u r n o n l a s e r (GREEN) ; c l e a n ( b u f f e r p a r z , BTSIZE ) ; creceive ( buffer tot ); p1 = c r e a t e P o i n t s ( b u f f e r t o t ) ; c l e a n ( b u f f e r t o t , BTSIZE ) ; creceive ( buffer tot ); p2 = c r e a t e P o i n t s ( b u f f e r t o t ) ; c l e a n ( b u f f e r t o t , BTSIZE ) ; i f (DEBUG MODE) { a l l T o S t r i n g ( p1 , b u f f e r t o t ) ; Serial . println ( buffer tot ); c l e a n ( b u f f e r t o t , BTSIZE ) ; a l l T o S t r i n g ( p2 , b u f f e r t o t ) ; Serial . println ( buffer tot ); c l e a n ( b u f f e r t o t , BTSIZE ) ; } r e s 1 = e x e c u t e ( p2 , WHO STARTS ) ; d e s t r o y ( p2 ) ; change laser ( ) ; r e s 2 = e x e c u t e ( p1 , 1 − WHO STARTS ) ; d e s t r o y ( p1 ) ; t u r n o f f l a s e r (RED ) ; t u r n o f f l a s e r (GREEN) ; end ( ) ; allToString ( res1 , b u f f e r t o t ) ; Serial . println ( buffer tot ); c l e a n ( b u f f e r t o t , BTSIZE ) ; destroy ( res1 ) ; allToString ( res2 , b u f f e r t o t ) ; Serial . println ( buffer tot ); c l e a n ( b u f f e r t o t , BTSIZE ) ; destroy ( res2 ) ; break ; default : i f (DEBUG MODE) S e r i a l . p r i n t l n ( ”command n o t r e c o g n i z e d ” ) ; c l e a n ( b u f f e r p a r z , BPSIZE ) ; } } Questa funzione è il controller dell’applicazione, ha infatti il compito di dialogare con il computer e lanciare i test da effettuare. Chiamando la funzione creceive, linea 75, riceve dal computer un primo messaggio. Se questo è 41 ”cn”, dove n è un numero intero, calibrerà lo strumento per x punti, se invece riceve ”s” inizierà l’esecuzione del test vero e proprio. La calibrazione ha il fine di lasciar mirare all’operatore i punti sullo schermo di Hess/Lancaster, in modo da avere un riferimento per quando dovrà eseguire il test vero e proprio. Alla riga 79 si ottiene il numero di punti da mirare, per laser. Per questa breve comunicazione si è usato il buffer parziale che adesso viene ripulito con clean. Successivamente con la funzione calibrate si effettua la prima calibrazione sul laser specificato in pwhich. Il delay successivo è stato introdotto per distinguere il suono prodotto dall’ultima conferma del paziente e quello che segnala il cambio laser, emesso dalla funzione change laser. La calibrazione viene poi ripetuta per l’altro laser. Con la funzione end si emette un suono che indica la conclusione della calibrazione. Quanto fatto dopo è solamente l’invio delle liste di coordinate puntate servendosi del buffer tot, e la deallocazione delle liste di punti con la funzione destroy che dealloca un’intera lista dato un puntatore ad essa. Nel caso in cui si è dato comando di iniziare il test, ovvero si è inviato ”s” ad Arduino, entriamo nel secondo case dello switch (linea 96). Vengono innanzitutto accesi entrambi i laser e ripulito, come nell’altro case, il buffer parz, come da convenzione. Vengono poi, in due momenti differenti, ricevute le liste di punti nel buffer tot e create due liste concatenate di essi che saranno puntate da p1,p2. Se il programma si trova in debug mode vengono mandate via seriale le liste create per verificare che non ci siano errori nella ricezione. Dopo questo viene eseguito il test con la funzione execute. Si noti che viene selezionato il primo laser, pwhich, mentre viene passata la seconda lista di punti p2. Ciò accade perchè pwhich verrà controllato dall’utente mentre 1 − pwhich da Arduino, sarà quest’ultimo a mirare p2. Con destroy viene deallocata la lista p2, poichè non è più utile. Si segnala il cambio di laser e si procede a rieseguire il test invertendo le parti. Alla fine si segnala il termine del test e si restituiscono i risultati al computer. Vediamo ora le funzioni chiamate da launcher nel dettaglio. 3.4.4 La comunicazione La comunicazione seriale tra computer e Arduino è spesso motivo di problemi, poichè il protocollo è connectionless, ovvero non viene confermata la ricezione di un messaggio da parte del ricevente. Si è scelto di implementare cosı̀ un protocollo connection lato periferica, significa quindi che ogni volta che il computer invia un messaggio ad Arduino, si mette in attesa dell’ack di quest’ultimo. La funzione creceive ha il compito di bufferizzare quanto inviato dal computer. Per far ciò chiama la funzione bufferize che a sua volta chiama blocking read. Quest’ultima si mette in busy waiting finchè 42 non è presente un carattere nel buffer HW, dopodichè restituisce tale carattere. bufferize continua a chiamare blocking read e a riempire il buffer finchè non viene letto $, segnale che il computer ha terminato il suo messaggio. Per confermare l’avvenuta ricezione della stringa, si invia tramite la funzione ack il carattere %. L’implementazione di questo meccanismo ha reso stabile e robusto lo sketch. Listing 15: ottica/Sketch/Rino/Rino.ino 161 162 163 164 165 166 167 168 169 170 void c r e c e i v e ( char ∗ b u f f e r ) { bufferize ( buffer ); ack ( ) ; } 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 void b u f f e r i z e ( char ∗ b u f f e r ) { i f (DEBUG MODE) S e r i a l . p r i n t l n ( ” b u f f e r : ” ) ; int i ; char c = b l o c k i n g r e a d ( ) ; f o r ( i =0; c != ’ $ ’ ; i ++) { i f (DEBUG MODE) S e r i a l . p r i n t l n ( c ) ; i f ( c >= 0 ) b u f f e r [ i ] = c ; c = blocking read ( ) ; } i f (DEBUG MODE) S e r i a l . p r i n t l n ( ” e x i t b u f f e r : ” ) ; } void ack ( ) { S e r i a l . p r i n t l n ( ’% ’ ) ; } Point execute ( Point p , i n t whoiswho ) { Listing 16: ottica/Sketch/Rino/Rino.ino char b l o c k i n g r e a d ( ) { char c ; while ( ( c=S e r i a l . r e a d ())== −1); return c ; } 3.4.5 Calibrate La funzione calibrate prende l’intero relativo al laser che si vuole calibrate e il numero di punti richiesti, restituisce quindi la lista dei punti mirati. Chiamando wait for user aspetta che l’operatore sposti il nunchuck sul punto desiderato e che ne dia conferma. In current viene messo il punto da lui mirato. Viene qui distinta la prima mira dalle altre, tramite la condizione n == NULL poichè nel primo caso si assegna a n, testa della futura lista, l’indirizzo di current, mentre negli altri casi si adopera la funzione add che automaticamente va a mettere il punto in coda. Listing 17: ottica/Sketch/Rino/Rino.ino 300 301 302 303 304 305 306 307 308 P o i n t c a l i b r a t e ( i n t who , i n t pnt nmb ) { P o i n t n = NULL ; Point current ; int i ; i f (DEBUG MODE) S e r i a l . p r i n t l n ( ” c a l i b r a t i o n : ” ) ; t u r n o n l a s e r ( who ) ; f o r ( i =0; i < pnt nmb ; i ++) { i f (DEBUG MODE) S e r i a l . p r i n t l n ( ” i t e r ” ) ; i f ( n == NULL) { 43 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 c u r r e n t = w a i t f o r u s e r ( who ) ; i f (DEBUG MODE) S e r i a l . p r i n t l n ( ” j u s t n = current ; after wait for user ” ); } else { c u r r e n t = w a i t f o r u s e r ( who ) ; add ( n , c u r r e n t ) ; } i f (DEBUG MODE) { S e r i a l . print ( ” paziente value : ” ) ; toString ( current , b u f f e r p a r z ) ; Serial . println ( buffer parz ); memset ( b u f f e r p a r z , 0 , BPSIZE ) ; } } t u r n o f f l a s e r ( who ) ; return n ; } 3.4.6 Wait for user La seguente funzione prende come parametro il numero associato al laser e restituisce il punto mirato dal paziente dopo che esso ha spostato il puntatore e dato conferma. Con la chiamata alla funzione wait for nunchuck il controllo viene ceduto a chi manovra il nunchuck e la funzione non ritorna fino a che non sia data conferma con la pressione del tasto Z. Fatto ciò vengono lette le inclinazioni degli angoli dei servomotori relativi al puntatore scelto e vengono scritte all’interno di container. Infine questo viene restituito al chiamante. Listing 18: ottica/Sketch/Rino/Rino.ino 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 P o i n t w a i t f o r u s e r ( i n t which ) { // 0 , 0 d e f a u l t ; Point c o n t a i n e r = p o i n t B u i l d e r ( 0 , 0 ) ; // don ’ t r e m o v e i n i t i a l i z a t i o n : i t c r a s h e s // i t i s n ’ t an Obj , j u s t a s t r u c t u r e ! ! without w a i t f o r n u n c h u c k ( which ) ; i f (DEBUG MODE) S e r i a l . p r i n t l n ( ” a f t e r w a i t f o r n u n c h u c k ” ) ; switch ( which ) { case RED: // r i g h t A i m e r . readVC ( c o n t a i n e r ) ; rightAimer . readMicroseconds ( container ) ; break ; case GREEN: // l e f t A i m e r . readVC ( c o n t a i n e r ) ; leftAimer . readMicroseconds ( container ) ; break ; } i f (DEBUG MODE) S e r i a l . p r i n t l n ( ” r e a d t h e v a l u e s ” ) ; return c o n t a i n e r ; } 3.4.7 Wait for nunchuck Questa funzione effettua un polling sul pulsante Z del nunchuck. Finchè esso non viene premuto continua a prendere i valori del joystick, getJoy, e aggiornare, update values, il punto mirato dal laser selezionato tramite il parametro which. Si noti la variabile nunchuck flag: essa viene messa per evitare che un’unica pressione del tasto Z dia conferma a più punti anzichè solo ad uno. Con l’introduzione di questo meccanismo ciò viene impedito. 44 Con update values si aggiorna la posizione di un laser passandogli un punto che contiene lo spostamento del joypad. Con JOYPAD SENSIBILITY si scala il valore al fine di aumentare o diminuire lo spostamento subito in seguito ad un certo scostamento del joypad; abbassando il valore viene aumentata la sensibilità, alzandolo diminuita. Si leggono quindi i valori correnti dei servomotori relativi al puntatore che si vuole spostare e gli si sommano gli incrementi/decrementi. Listing 19: ottica/Sketch/Rino/Rino.ino 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 void w a i t f o r n u n c h u c k ( i n t which ) { int l a s t , nunchuck flag ; Point p ; char ∗ s t r ; nunchuck flag = 1; /∗ p e r e v i t a r e c h e una p r e s s i o n e d e l t a s t o a t t i v i p i u ’ e v e n t i c o n s e c u t i v i , d e v e v e n i r e l e t t o a l m e n o uno 0 p r i m a d i a t t i v a r e un e v e n t o con un 1 . p o s s o q u i n d i m e t t e r e l a d i c h i a r a z i o n e d i n u n c h u c k f l a g a l l ’ i n t e r n o q u e s t a f u n z i o n e i n i z i a l i z z a t o a 1 ∗/ while ( ( ( l a s t = g e t Z ( ) ) ! = 1 ) | | n u n c h u c k f l a g ) { nunchuck flag = l a s t ; p = getJoy ( ) ; u p d a t e v a l u e s ( p , which ) ; free (p ) ; d e l a y (POLLING DELAY ) ; } // e m i t t i n g s o u n d s feedback ( ) ; } void u p d a t e v a l u e s ( P o i n t q , i n t which ) { struct point c o n t a i n e r ; i n t panValue = q−>x / JOYPAD SENSIBILITY ; i n t t i l t V a l u e = q−>y / JOYPAD SENSIBILITY ; switch ( which ) { case RED: r i g h t A i m e r . r e a d M i c r o s e c o n d s (& c o n t a i n e r ) ; panValue += c o n t a i n e r . x ; tiltValue = container . y − tiltValue ; r i g h t A i m e r . w r i t e M i c r o s e c o n d s ( panValue , t i l t V a l u e ) ; break ; case GREEN: l e f t A i m e r . r e a d M i c r o s e c o n d s (& c o n t a i n e r ) ; panValue += c o n t a i n e r . x ; tiltValue = container . y − tiltValue ; l e f t A i m e r . w r i t e M i c r o s e c o n d s ( panValue , t i l t V a l u e ) ; break ; } } 3.4.8 Execute Data una lista di punti p e un intero relativo al laser che si vuol far controllare al paziente whoiswho, execute fa puntare la lista di punti p al laser 1 − whoiswho, aspettando, per ogni punto, che il paziente tenti di sovrapporvi il laser whoiswho, da lui controllato. La funzione restituisce la lista di punti mirata dal paziente. Listing 20: ottica/Sketch/Rino/Rino.ino 170 171 172 173 174 175 Point execute ( Point p , Point t = p ; P o i n t n = NULL ; Point current ; i n t whoiswho ) { while ( t !=NULL) { 45 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 if (DEBUG MODE) { S e r i a l . p r i n t ( ” I ’m gonna aim : ” ) ; toString (t , buffer parz ); Serial . println ( buffer parz ); memset ( b u f f e r p a r z , 0 , BPSIZE ) ; } aim ( t , 1 − whoiswho ) ; i f ( n == NULL) { c u r r e n t = w a i t f o r u s e r ( whoiswho ) ; n = current ; } else { c u r r e n t = w a i t f o r u s e r ( whoiswho ) ; add ( n , c u r r e n t ) ; } i f (DEBUG MODE) { S e r i a l . p r in t ( ” Valore paziente : ” ) ; toString ( current , b u f f e r p a r z ) ; Serial . println ( buffer parz ); memset ( b u f f e r p a r z , 0 , BPSIZE ) ; } t = t−>n e x t ; d e l a y (DELAY BETWEEN POINTS ) ; } return n ; } 46 4 4.1 R Perchè R R è un potente linguaggio e framework finalizzato principalmente a calcoli statistici e alla generazione di espressivi grafici relativi a questi. Esso è, a differenza di framework simili come MatLab e Mathematica, rigorosamente opensource e multipiattaforma. Figure 23: Esempi di grafici R R è semplice e veloce da imparare e contiene nei suoi moduli base, importati di default, un ampia e completa gamma di funzioni. E’ un linguaggio interpretato, non tipato, che astrae completamente dalla gestione della memoria, quindi comodo e immediato. Sito di riferimento sia per i primi passi che come reference è www.r-project.org. Per tutte queste ragioni esso è adatto all’utilizzo finale del dispositivo da parte di un qualsiasi utente non particolarmente esperto di informatica. 4.2 Creare un ponte tra R e Arduino Punto a sfavore nell’utilizzo del framework R in questo progetto sta nel fatto di essere un ambiente chiuso, che difficilmente permette l’accesso a risorse 47 gestite dal sistema operativo. Nel nostro caso abbiamo bisogno di accedere alla porta seriale per poter comunicare con Arduino. Come si è gia detto nella sezione 3.2, ogni sistema operativo fornisce delle funzioni wrapper, solitamente scritte in C, che permettono di accedere a risorse del computer, come la porta seriale. Poichè vogliamo che l’applicazione finale sia eseguibile sui più noti sistemi operativi (Windows, OSX, Linux), teoricamente dovremmo cercare delle librerie per ognuno di essi e creare quindi dei pacchetti R differenziati almeno per le due grandi famiglie Unix e Windows, completamente diverse. Ricordo che OSX e Linux derivano entrambi da Unix. Per rendere semplice lo sviluppo di un unico pacchetto che funzioni su tutte le piattaforme menzionate, si è ricorsi al linguaggio Java e al pacchetto rJava che permette la comunicazione alla JVM tramite R. Java delega alla JVM il compito di tradurre nel sistema operativo su cui è installato, chiamate a basso livello. Ogni sistema operativo menzionato ha una JVM ad hoc. La libreria RXTX http://rxtx.qbang.org/ fornisce delle classi che permettono di accedere alle porte seriali da java. Una volta installato java, la libreria RXTX, il pacchetto rJava, l’utente potrà comunicare da R ad Arduino. Definito quanto abbiamo bisogno, possiamo ora procedere alla creazione di un package unico. Chiameremo il pacchetto ArduBridge perchè esso funge da ponte tra Arduino e R. La cartella contenente il pacchetto deve avere lo stesso nome del pacchetto, che deve essere capital letter. Creiamo poi le cartelle inst, dove andrà il codice java, R, dove risiederanno gli script, man, manuale, e i file DESCRIPTION, NAMESPACE, che contengono rispettivamente la descrizione del pacchetto e le direttive riguardo le dipendenze e le funzioni che vogliamo rendere accessibili all’utente. ArduBridge inst javasrc java R man Listing 21: ottica/R/packages/ArduBridge/inst/javasrc/Arduino.java 1 2 3 4 5 6 7 8 9 10 11 import j a v a . i o . ∗ ; import import import import import gnu . gnu . gnu . gnu . java i o . CommPortIdentifier ; io . SerialPort ; io . SerialPortEvent ; io . SerialPortEventListener ; . u t i l . Enumeration ; public c l a s s Arduino { SerialPort serialPort ; 48 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 /∗ ∗ The p o r t we ’ r e n o r m a l l y g o i n g t o u s e . ∗/ p r i v a t e s t a t i c f i n a l S t r i n g PORT NAMES [ ] = { ” / dev / t t y . usbmodem411 ” , // Mac OS X ” / dev / t t y . usbmodem641 ” , ” / dev / t t y S 3 3 ” , // L i n u x ” / dev / ttyUSB0 ” , ”COM3” , // Windows ”COM9” , }; /∗ ∗ B u f f e r e d i n p u t s t r e a m f r o m t h e p o r t ∗/ private BufferedReader i n p u t ; /∗ ∗ The o u t p u t s t r e a m t o t h e p o r t ∗/ private PrintStream output ; /∗ ∗ M i l l i s e c o n d s t o b l o c k w h i l e w a i t i n g f o r p o r t o p e n ∗/ p r i v a t e s t a t i c f i n a l i n t TIME OUT = 2 0 0 0 ; /∗ ∗ D e f a u l t b i t s p e r s e c o n d f o r COM p o r t . ∗/ p r i v a t e s t a t i c f i n a l i n t DATA RATE = 9 6 0 0 ; /∗ ∗ M i l l i s e c o n d s t o w a i t w h i l e A r d u i n o s e t s up s e r i a l c o m m u n i c a t i o n ∗/ p r i v a t e s t a t i c f i n a l i n t SETUP TIME = 2 0 0 0 ; public void i n i t i a l i z e ( boolean debug mode ) { CommPortIdentifier portId = null ; Enumeration portEnum = C o m m P o r t I d e n t i f i e r . g e t P o r t I d e n t i f i e r s ( ) ; // i t e r a t e t h r o u g h , l o o k i n g f o r t h e p o r t while ( portEnum . hasMoreElements ( ) ) { C o m m P o r t I d e n t i f i e r c u r r P o r t I d = ( C o m m P o r t I d e n t i f i e r ) portEnum . n e x t E l e m e n t ( ) ; f o r ( S t r i n g portName : PORT NAMES) { i f ( debug mode ) System . o u t . p r i n t l n ( c u r r P o r t I d . getName ( ) + ” =?= ” + portName ) ; i f ( c u r r P o r t I d . getName ( ) . e q u a l s ( portName ) ) { portId = currPortId ; break ; } } } if ( p o r t I d == n u l l ) { System . o u t . p r i n t l n ( ” Could n o t return ; f i n d COM p o r t . ” ) ; } else { System . o u t . p r i n t l n ( ”RXTX works ” ) ; } try { // o p e n s e r i a l p o r t , and u s e c l a s s name f o r t h e appName . s e r i a l P o r t = ( S e r i a l P o r t ) p o r t I d . open ( t h i s . g e t C l a s s ( ) . getName ( ) , TIME OUT ) ; // s e t p o r t p a r a m e t e r s s e r i a l P o r t . s e t S e r i a l P o r t P a r a m s (DATA RATE, S e r i a l P o r t . DATABITS 8 , S e r i a l P o r t . STOPBITS 1 , S e r i a l P o r t . PARITY NONE ) ; // o p e n t h e s t r e a m s i n p u t = new B u f f e r e d R e a d e r (new I n p u t S t r e a m R e a d e r ( s e r i a l P o r t . g e t I n p u t S t r e a m ( ) ) ) ; o u t p u t = new P r i n t S t r e a m ( s e r i a l P o r t . g e t O u t p u t S t r e a m ( ) ) ; // s e t up t i m e Thread . s l e e p (SETUP TIME ) ; } catch ( E x c e p t i o n e ) { System . e r r . p r i n t l n ( e . t o S t r i n g ( ) ) ; } } /∗ ∗ ∗ T h i s s h o u l d b e c a l l e d when you s t o p u s i n g t h e p o r t . ∗ T h i s w i l l p r e v e n t p o r t l o c k i n g on p l a t f o r m s l i k e L i n u x . ∗/ public synchronized void c l o s e ( ) { i f ( s e r i a l P o r t != n u l l ) { serialPort . close (); } } public void w r i t e ( S t r i n g output . p r i n t ( s ) ; } s ) throws E x c e p t i o n { public S t r i n g r e a d ( ) throws E x c e p t i o n { while ( ! i n p u t . r e a d y ( ) ) ; return i n p u t . r e a d L i n e ( ) ; 49 95 96 97 98 99 100 101 102 103 104 105 } public s t a t i c void main ( S t r i n g [ ] a r g s ) throws E x c e p t i o n { boolean dmode = f a l s e ; Arduino main = new Arduino ( ) ; i f ( a r g s . l e n g t h > 0 && a r g s [ 0 ] . e q u a l s ( ”−d” ) ) dmode=true ; main . i n i t i a l i z e ( dmode ) ; main . c l o s e ( ) ; } } Nel listato 21 è mostrato il codice java che permette la comunicazione seriale fornendo le interfacce read, write. Esso è stato preso da http:// playground.arduino.cc/Interfacing/Java e opportunamente modificato. In particolare sono state aggiunte delle porte nell’array PORT NAMES (linee 1319), delle stampe di debug attivate solo in debug mode e le funzioni read, write. Il main ha solo funzione di debug e non viene usato nel package. Nella cartella java si trova Arduino.jar, che è l’archivio java di Arduino.java, ed è quello effettivamente usato dal codice R. Nella cartella R si trova innanzitutto il file che permette l’inclusione del .jar, ovvero onLoad.R Listing 22: ottica/R/packages/ArduBridge/R/onLoad.R . onLoad <− function ( l i b n a m e , pkgname ) { . j p a c k a g e ( pkgname , l i b . l o c = l i b n a m e ) } Inoltre è presente il codice R che definisce sia i wrapper alle funzioni read,write del codice del .jar, sia le funzioni che usando le due citate implementano l’interfaccia del package R richiesto dai requisiti di progetto. Listing 23: ottica/R/packages/ArduBridge/R/ArduBridge.R 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 c r t A r d u <− function ( ) { a r d <− . jnew ( ” Arduino ” ) return ( a r d ) } s r l I n i t <− function ( a r d ) { . j c a l l ( ard , ”V” , ” i n i t i a l i z e ” , FALSE) } s r l R e a d <− function ( a r d ) { i n p u t <− . j c a l l ( ard , ”S” , ” r e a d ” ) return ( i n p u t ) } s r l W r i t e <− function ( ard , o u t S t r ) { . j c a l l ( ard , ”V” , ” w r i t e ” , o u t S t r ) } s r l C l o s e <− function ( a r d ) { . j c a l l ( ard , ”V” , ” c l o s e ” ) } waitForAck <− function ( a r d ) { ack <− s r l R e a d ( a r d ) i f ( ack == ”%” ) { p r i n t ( ” ack r e c e i v e d ” ) } e l s e print ( ” e r r o r ” ) } c a l i b r a t e <− function ( ard , pntNmb ) { 50 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 s r l W r i t e ( ard , paste ( ” c ” , t o S t r i n g ( pntNmb ) , ”$” , s e p=” ” ) ) waitForAck ( a r d ) t o k e n 1 <− s r l R e a d ( a r d ) t o k e n 2 <− s r l R e a d ( a r d ) l s t 1 <− eval ( parse ( text = paste ( ’ c ( ’ , t o k e n 1 , ’ ) ’ ) ) ) l s t 2 <− eval ( parse ( text = paste ( ’ c ( ’ , t o k e n 2 , ’ ) ’ ) ) ) inMtx1 <− matrix ( l s t 1 , nrow = 2 ) inMtx2 <− matrix ( l s t 2 , nrow = 2 ) r e s u l t s <− l i s t ( inMtx1 , inMtx2 ) return ( r e s u l t s ) } s t a r t T e s t <− function ( ard , o u t L s t ) { s r l W r i t e ( ard , ” s $” ) waitForAck ( a r d ) o u t S t r 1 <− t o S t r i n g ( o u t L s t [ [ 1 ] ] ) o u t S t r 2 <− t o S t r i n g ( o u t L s t [ [ 2 ] ] ) o u t S t r 1 <− paste ( o u t S t r 1 , ”$” , s e p=” ” ) o u t S t r 2 <− paste ( o u t S t r 2 , ”$” , s e p=” ” ) s r l W r i t e ( ard , o u t S t r 1 ) waitForAck ( a r d ) s r l W r i t e ( ard , o u t S t r 2 ) waitForAck ( a r d ) t o k e n 1 <− s r l R e a d ( a r d ) t o k e n 2 <− s r l R e a d ( a r d ) l s t 1 <− eval ( parse ( text = paste ( ’ c ( ’ , t o k e n 1 , ’ ) ’ ) ) ) l s t 2 <− eval ( parse ( text = paste ( ’ c ( ’ , t o k e n 2 , ’ ) ’ ) ) ) inMtx1 <− matrix ( l s t 1 , nrow = 2 ) inMtx2 <− matrix ( l s t 2 , nrow = 2 ) r e s u l t s <− l i s t ( inMtx1 , inMtx2 ) return ( r e s u l t s ) } b r i d g e W r i t e <− function ( l s t , f i l e n a m e , ncol ) { f o r ( i i n 1 : length ( l s t ) ) { write ( t ( l s t [ [ i ] ] ) , f i l e = f i l e n a m e , s e p =” \ t ” , append = TRUE, write ( c ( ” ” ) , f i l e = f i l e n a m e , append = TRUE) } } b r i d g e W r i t e D i f f <− function ( l s t 1 , l s t 2 , f i l e n a m e , ncol ) { f o r ( i i n 1 : length ( l s t 1 ) ) { write ( t ( abs ( l s t 1 [ [ i ] ] − l s t 2 [ [ i ] ] ) ) , f i l e = f i l e n a m e , write ( c ( ” ” ) , f i l e = f i l e n a m e , append = TRUE) } } b r i d g e D i f f <− function ( l s t 1 , l s t 2 ) { l <− l i s t ( abs ( l s t 1 [ [ 1 ] ] − l s t 2 [ [ 1 ] ] ) , return ( l ) } ncolumns = ncol ) s e p =” \ t ” , append = TRUE, ncolumns = ncol ) abs ( l s t 1 [ [ 2 ] ] − l s t 2 [ [ 2 ] ] ) ) Per comunicare con la JVM dobbiamo usare le funzioni forniteci dal package rJava. Per creare oggetti si usa .jnew con parametro una stringa contenente il nome dell’oggetto. La funzione crtArdu semplicemente crea un oggetto Arduino. Per chiamare metodi si usa la funzione .jcall che prende come parametri, l’oggetto, una stringa indicante il tipo ritornato, una stringa con il nome del metodo e i vari parametri di questo. Un esempio si vede nella funzione srlInit che, dato un oggetto Arduino, chiama il suo metodo initialize con parametro false, ovvero per una esecuzione normale, non in debug mode. Questa inizializza la comunicazione seriale. srlRead, srlWrite sono i wrapper dei metodi java read, write mentre srlClose di close che termina la connessione. waitForAck attende di ricevere via seriale il carattere di ack %, secondo quanto definito nel protocollo di comunicazione implementato. 51 calibrate prende l’oggetto e il numero di punti che si vogliono calibrare e restituisce una lista di due matrici 2*n rappresentante i microsecondi equivalenti agli angoli assunti dai servomotori per mirare i punti. Nel dettaglio manda via seriale la stringa ”cn$” dove n è il numero di punti e $ è il carattere di termine del messaggio come definito dal nostro protocollo. Attende poi l’ack da parte di Arduino, riceve le due liste di punti che trasforma prima in vettori e poi in matrici, ritorna una lista di queste. startTest riceve l’oggetto e una lista di due matrici e restituisce la lista delle matrici risultato. Prima di tutto invia ad Arduino ”s$” e attende un ack da esso, trasforma le matrici in stringhe e ci aggiunge il carattere $ alla fine, le manda quindi al dispositivo e si mette in attesa. Una volta pronte le stringhe finali rappresentanti i punti le trasforma prima in vettori e poi in matrici come nella funzione calibrate. bridgeWrite prende una lista di matrici, una stringa specificante il nome del file su cui scrivere e il numero di punti presenti in ognuna delle due matrici. Scrive sul file specificato il contenuto di esse. bridgeWriteDiff è simile alla precedente ma accetta due liste di matrici e ne scrive sul file i valori assoluti delle differenze. bridgeDiff come bridgeWriteDiff ma restituisce il risultato anzichè scriverlo su file. Nel file NAMESPACE qui sotto, vediamo scritte le direttive che ci permettendo di importare package ed esportare, e quindi rendere accessibili, le funzioni definite in ArduBridge.R all’utente che importa il pacchetto. Listing 24: ottica/R/packages/ArduBridge/NAMESPACE import ( ” rJava ” ) export ( ” crtArdu ” ) export ( ” s r l I n i t ” ) export ( ” srlRead ” ) export ( ” srlWrite ” ) export ( ” s r l C l o s e ” ) export ( ” c a l i b r a t e ” ) export ( ” startTest ” ) export ( ” bridgeWrite ” ) export ( ” bridgeWriteDiff ” ) export ( ” bridgeDiff ” ) In DESCRIPTION invece una descrizione minimale del pacchetto. Listing 25: ottica/R/packages/ArduBridge/DESCRIPTION Package : ArduBridge Type : Package T i t l e : A b r i d g e between Arduino and R Version : 1.0 Date : 2013−01−07 Author : Matteo P e s s i n a M a i n t a i n e r : <matteo . p e s s i n a 3 @ s t u d e n t i . u n i m i . i t > D e s c r i p t i o n : T h i s p a c k a g e p r o v i d e s p r i m i t i v e f u n c t i o n s f o r communication between Arduino and R . I t p r o v i d e s a l s o some f u n c t i o n s f o r t h e e x e c u t i o n o f Hess / L a n c a s t e r t e s t . L i c e n s e : GPL Depends : r J a v a 52 5 Installazione e tutorial 5.1 Prerequisiti E’ necessario installare: • Una recente versione del Java Runtime Environment (JRE) e conoscerne la directory di installazione. Il pacchetto rJava infatti utilizza questo come ponte per accedere alla seriale e comunicare cosı̀ con Arduino. • Una recente versione dell’Arduino IDE; questo fornisce i driver necessari alla comunicazione con il chip controller USB-RS232 all’interno dell’Arduino Board. • Git, source code management system, per ottenere le build e il codice sorgente del progetto. • Il software di statistica R, il nostro frontend per l’operatore che vuole comunicare con il dispositivo, calibrarlo ed eseguire i test. Ci si posizioni ora in una directory dove si vuol far risiedere il materiale relativo al progetto e si esegua il seguente comando al fine di scaricare il repository del progetto. $git clone g i t o l i t e @ s l −l a b . i t : o t t i c a Nella directory /ottica/Archive/ si trova il necessario per l’installazione del software che permette di comunicare con il dispositivo Rino. Nelle successive sezioni è spiegato nel dettaglio le operazioni da svolgere. 5.2 5.2.1 RXTX OSX Listing 26: Installazione RxTX in OSX 1 2 3 $ u n z i p / A r c h i v e / r x t x −2.1−7− b i n s −r 2 . z i p $cp / A r c h i v e / r x t x −2.1−7− b i n s −r 2 /RXTXComm. j a r / L i b r a r y / Java / E x t e n s i o n s / $cp / A r c h i v e / r x t x −2.1−7− b i n s −r 2 /Mac OS X/ l i b r x t x S e r i a l . j n i l i b / L i b r a r y / Java / E x t e n s i o n s / Innanzitutto viene scompattato l’archivio contente la RXTX java library per la comunicazione seriale; si procede quindi copiando il file .jar (indipendente dalla piattaforma utilizzata) e lo si pone nella cartella dove java cerca le estensioni; si fa infine lo stesso con il file dipendente dal sistema operativo utilizzato. 53 5.2.2 Linux Listing 27: Installazione RXTX in Linux 1 2 3 4 5 $ u n z i p / A r c h i v e / r x t x −2.1−7− b i n s −r 2 . z i p $cp / A r c h i v e / r x t x −2.1−7− b i n s −r 2 /RXTXComm. j a r / u s r / l i b / jvm / j a v a −7− o r a c l e / j r e / l i b / e x t / $cp / A r c h i v e / r x t x −2.1−7− b i n s −r 2 / Linux / i 6 8 6 −unknown−l i n u x −gnu / l i b r x t x S e r i a l . s o / u s r / l i b / jvm / j a v a −7− o r a c l e / j r e / l i b / i 3 8 6 / $ l n −s / dev /ttyACM0 / dev / t t y S 3 3 Viene estratto l’archivio e vengono copiati il .jar generico e la libreria .so in cartelle specifiche del jre. Quelle scritte sopra potrebbero non corrispondere con il jre di una differente macchina; l’importante è mantenere invariato il postfisso da jre/ in poi. Infine viene creato un link simbolico allo special file /dev/ttyACM0 (relativo ad Arduino) chiamato /dev/ttyS33. Questa operazione è necessaria poichè la libreria RXTX, in ambiente linux, controlla solo gli special file ttyS. 5.2.3 1 2 3 4 Windows A r c h i v e \ r x t x −2.1−7− b i n s −r 2 \RXTXComm. j a r C: \ Programmi \ j a v a \ j r 7 \ l i b \ e x t \ A r c h i v e \ r x t x −2.1−7− b i n s −r 2 \Windows\ r x t x s e r i a l . d l l C: \ Programmi \ j a v a \ j r 7 \ l i b \ b i n \ Si copi il file alla riga 1 nel percorso alla riga 2 e si faccia lo stesso con il file a riga 3 e il percorso a riga 4. 5.2.4 Verifica Per verificare il corretto funzionamento della libreria RXTX: • Si colleghi il dispositivo ad una qualsiasi porta usb; • Si lanci il file Arduino.class tramite java. $cd A r c h i v e /R/ p a c k a g e s / ArduBridge / i n s t / j a v a / $ j a v a − j a r Arduino . j a r Listing 28: RXTX funzionante Experimental : JNI OnLoad c a l l e d . Stable Library ========================================= N a t i v e l i b V e r s i o n = RXTX−2.1−7 Java l i b V e r s i o n = RXTX−2.1−7 RXTX works Listing 29: RXTX non trova la porta Arduino Experimental : JNI OnLoad c a l l e d . Stable Library ========================================= N a t i v e l i b V e r s i o n = RXTX−2.1−7 Java l i b V e r s i o n = RXTX−2.1−7 Could n o t f i n d COM p o r t . 54 Se viene stampato a schermo il listato 28 allora la libreria ha trovato la porta corrispondente ad Arduino; se viene mostrato il listato 29 semplicemente la porta seriale non è inserita all’interno di Arduino.jar e necessita una veloce modifica di cui si parla nella sezione 5.6.1. Nel caso di errori differenti si faccia riferimento al wiki http://rxtx.qbang.org/wiki/index.php/Main_Page. 5.3 ArduBridge 5.3.1 OSX e Linux Listing 30: show .libPaths $R >. l i b P a t h s ( ) Nel listato 30 viene avviato R e dato il comando che mostra i path delle librerie. E’ necessario sceglierne uno dove installare il pacchetto. Listing 31: Installazione ArduBridge $cd o t t i c a /R/ p a c k a g e s / $R CMD INSTALL − l <p e r c o r s o s c e l t o p r e c e d e n t e m e n t e > ArduBridge Portarsi nella cartella packages come indicato in 31. Compilare quindi il pacchetto con il comando seguente. 5.3.2 Windows In Windows è possibile installare il pacchetto direttamente dalla GUI caricando l’archivio ArduBridge.zip presente nella cartella ottica/Archive/. Nota che in questo caso non si compila il pacchetto. La compilazione di un pacchetto R su Windows necessita di tools aggiuntivi di cui non parleremo. 5.3.3 Verifica Arduino deve essere collegato tramite usb durante la verifica. Listing 32: Verifica ArduBridge $R > l i b r a r y ( ArduBridge ) >a r d <− c r t A r d u ( ) > s r l I n i t ( ard ) Se il pacchetto è installato e settato correttamente, a seguito dei comandi qui sopra, apparirà il listato 28. 55 5.4 Compilazione Sketch Al fine della compilazione dello sketch ottica/Sketch/Rino/Rino.ino, che contiene il codice per fare eseguire ad Arduino il test in questione, è necessario copiare le librerie Aimer, Point e FreeMemory situate in Sketch/Libraries/ nella cartella delle librerie user-defined di Arduino, dipendente dal sistema operativo. 5.4.1 OSX La cartella delle librerie user-defined è ∼/Documents/Arduino/libraries/; è possibile spostarle manualmente o utilizzare lo script restore libraries mac.sh. 5.4.2 Linux Su Arduino IDE 1.0 si verificano dei problemi nella compilazione della libreria Wire a causa di un bug. Si consiglia quindi di installare una versione ≥ 1.0.3. La locazione della cartella libraries dipende dal tipo di installazione, si faccia quindi riferimento al sito arduino.cc nel caso non se ne sia a conoscenza. 5.4.3 Windows Come per OSX, basta spostare le librerie Aimer, Point, MemoryFree nella cartella nomeutente/Documenti/Arduino/libraries. 5.5 Tutorial per l’uso Listing 33: Esempio d’uso > l i b r a r y ( ArduBridge ) Carico i l pacchetto r i c h i e s t o : rJava > a r d <− c r t A r d u ( ) > s r l I n i t ( ard ) Experimental : JNI OnLoad c a l l e d . Stable Library ========================================= N a t i v e l i b V e r s i o n = RXTX−2.1−7 Java l i b V e r s i o n = RXTX−2.1−7 RXTX works > v a l u e s <− c a l i b r a t e ( ard , 4 ) [ 1 ] ” ack r e c e i v e d ” > values [[1]] [ ,1] [ ,2] [ ,3] [ ,4] [ 1 , ] 1283 1473 1473 1283 [ 2 , ] 1013 999 1138 1144 [[2]] [1 ,] [2 ,] [ ,1] [ ,2] [ ,3] [ ,4] 1610 1791 1783 1599 1091 1105 1235 1219 > r e s <− [ 1 ] ” ack [ 1 ] ” ack [ 1 ] ” ack > res s t a r t T e s t ( ard , received ” received ” received ” values ) 56 [[1]] [1 ,] [2 ,] [ ,1] [ ,2] [ ,3] [ ,4] 1290 1473 1475 1286 1013 999 1138 1142 [[2]] [1 ,] [2 ,] [ ,1] [ ,2] [ ,3] [ ,4] 1607 1792 1784 1599 1093 1104 1236 1220 > b r i d g e D i f f ( res , values ) [[1]] [ ,1] [ ,2] [ ,3] [ ,4] [1 ,] 7 0 2 3 [2 ,] 0 0 0 2 [[2]] [ ,1] [ ,2] [ ,3] [ ,4] [1 ,] 3 1 1 0 [2 ,] 2 1 1 1 > bridgeWrite ( values , ” values . txt ” , 4) > bridgeWrite ( res , ” r e s . txt ” , 4) > bridgeWriteDiff ( res , values , ” d i f f . txt ” , 4) Si apra R e si esegua quanto visto nel listato 33. Per prima cosa vengono importate le funzioni del pacchetto ArduBridge con il comando library. Viene poi creato un oggetto Arduino e viene inizializzata la seriale come mostrato nella sezione 5.3.3. Fatto ciò viene chiamata la funzione calibrate che prende come parametri l’oggetto Arduino e il numero di punti che si vogliono calibrare, in questo caso 4. A questo punto l’operatore mira i punti di riferimento che vuole fissare per il test. I valori vengono salvati nella lista values che contiene due matrici 2 ∗ n, dove ogni colonna rappresenta un punto: in prima riga il valore pan, in seconda tilt. Dando alla funzione startTest l’oggetto Arduino e i valori dei punti di riferimento, viene cominciato effettivamente il test, il paziente dovrà quindi comandare il dispositivo tramite il nunchuck. Il risultati del test vengono messi nella variabile res, dello stesso tipo di values. Per vedere subito la differenza tra i punti di riferimento e quelli mirati dal paziente si utilizza la funzione bridgeDiff che, prese due liste di matrici, restituisce una lista di matrici rappresentanti le differenze delle prime due in valore assoluto. Vengono poi salvati i punti di riferimento e i punti mirati dal paziente, su un file, tramite la funzione bridgeWrite; questa prende tre argomenti: la lista di matrici che si vuole salvare, il nome del file, e il numero di punti mirati. Vengono poi salvati gli scostamenti tra i punti mirati con la funzione bridgeWriteDiff che prende le due liste, il nome del file e i punti mirati. Si noti che sarebbe stato anche possibile salvare in una variabile gli scostamenti tramite la funzione bridgeDiff e scriverli poi con bridgeWrite. bridgeWriteDiff altri non è che la combinazione delle due. I file vengono creati se non sono presenti, nel caso invece lo fossero, i dati vengono semplicemente ”appesi”. In questo modo è possibile scrivere anche tutti i risultati su un unico file. Un ulteriore tutorial nella forma di screencast è disponibile al seguente indirizzo. 57 http://sl-lab.it/dokuwiki/doku.php?id=tesi:phsclcmptnglncstr 5.6 5.6.1 Troubleshooting Could not find COM port Viene mostrato Could not find COM port quando la porta seriale a cui è connesso Arduino non è tra quelle in lista nel codice sorgente del listato 21 linee 13-20. Listing 34: debug mode $cd A r c h i v e /R/ p a c k a g e s / ArduBridge / i n s t / j a v a / $ j a v a − j a r Arduino . j a r −d Ci sono vari modi per capire il nome del device file a cui è associato Arduino; uno di questi consiste nell’eseguire in debug mode Arduino.jar come nel listato 34. Verrà quindi stampato a schermo quanto si trova alla linea 40 del listato 21: sulla sinistra le porte attive, sulla destra quelle inserite nel codice. Ci sono una lista di porte che sono sempre attive ma che non sono relative ad Arduino: OSX /dev/cu.Bluetooth-Modem, /dev/tty.Bluetooth-Modem, /dev/cu.Bluetooth-PDA-Sync, /dev/tty.Bluetooth-PDA-Sync; Win COM1. Ne apparità un’altra nella lista che non è tra queste; questa sarà quella cercata. Aggiungerla quindi nella lista presente nel codice di Arduino.java a riga 19; ricompilare Arduino.java e ricreare l’archivio Arduino.jar; dopodichè ricompilare il pacchetto ArduBridge seguendo le istruzioni nella sezione 5.3.1. (la compilazione è spiegata solo su OSX e Linux) Su Linux non ha senso effettuare questo test, infatti abbiamo definito noi il nome del device file relativo ad Arduino (sezione 5.2), ovvero /dev/ttyS33. 6 Conclusioni Il progetto è stato fin dall’inizio molto ambizioso poiché si voleva costruire un apparecchio medico in grado di dare risultati precisi con una bassa disponibilità economica e senza l’aiuto di esperti nei settori della misura e della valutazione degli errori. Abbiamo imparato che non a caso esistono tantissime branche della tecnologia, e un progetto completo, con importanti esigenze in campo medico, necessita di una forte modularizzazione, affidando ad ogni persona coinvolta 58 compiti che riguardino le discipline di sua competenza, e di buone disponibilità economiche. Il modulo che non si è riusciti a portare a termine completamente è stato quello della misura e della calibrazione; su questo si è fatto un passo indietro e si è delegato a terzi. Pensiamo comunque che nella sezione relativa si sia costruito correttamente un modello matematico che possa risolvere i problemi almeno in via teorica; riteniamo quindi che quella possa essere utile a qualsiasi persona interessata alla risoluzione di problemi di questo tipo. Per quanto riguarda le restanti parti del progetto, si può dire con soddisfazione di essere riusciti a portarle a termine rendendo la comunicazione, l’esecuzione e le interfacce, stabili e robuste. Poichè l’approccio seguito è quello blackbox, diverse librerie e funzioni all’interno dello sketch saranno utilizzabili anche in altri progetti poichè assolutamente aspecifiche e rilasciate sotto licenza GPL. In conclusione diciamo di essere riusciti a portare a termine il progetto comprendente i moduli inerenti al percorso di studi di un corso di informatica. References [1] J.-B. Weiss, Squilibri oculomotori e coordimetria. CERES Editeurs, Paris, 1st Edition, 1988. [2] Roodhooft J. M., Screen tests used to map out ocular deviations. US National Library of Medicine National Institutes of Health, http://www. ncbi.nlm.nih.gov/pubmed/18018429, 1997. [3] Massimo Banzi, Arduino: la guida ufficiale. Tecniche Nuove, Varese, 2a Edizione, 2012. [4] Charles Platt, Make: Electronics. O’Reilly, Canada, 1st Edition, 2009. [5] Tobias Verbeke, Hello Java World! A Tutorial for Interfacing to Java Archives inside R Packages. http://cran.r-project.org/web/ packages/helloJavaWorld/vignettes/helloJavaWorld.pdf , 2010. [6] Sito ufficiale Arduino, http://arduino.cc. [7] Sito ufficiale R, http://www.r-project.org. 59