Upload
trannga
View
213
Download
0
Embed Size (px)
Citation preview
UNIVERSITÀ DEGLI STUDI DI PARMA
FACOLTÀ DI SCIENZE
MATEMATICHE FISICHE E NATURALI
CORSO DI LAUREA IN INFORMATICA
Progettazione e realizzazione di un frameworkper il controllo integrato di dispositivi via porta
seriale, USB ed Ethernet
Relatore: Candidato:
Ing. Federico Bergenti Francesco Sacchi
Indice
1 Ringraziamenti............................................................................................7
2 Introduzione................................................................................................9
2.1 L'azienda Custom Engineering S.p.A...............................................................................9
2.2 I prodotti...........................................................................................................................9
2.2.1 Soluzioni P.O.S. Retail..............................................................................................9
2.2.2 Soluzioni OEM/industriali......................................................................................10
2.2.3 Emettitori di biglietti (TKT) e scanner per scommesse..........................................11
2.2.4 Soluzioni multimediali............................................................................................11
2.3 Il lavoro svolto nel tirocinio aziendale...........................................................................12
2.3.1 Descrizione del lavoro di tesi..................................................................................13
2.4 Il protocollo ad alto livello dei prodotti Custom............................................................13
2.5 Comunicazione Seriale, Ethernet, USB in Linux...........................................................14
2.5.1 Comunicazione seriale............................................................................................14
2.5.2 Comunicazione Ethernet.........................................................................................16
2.5.3 Comunicazione USB...............................................................................................18
2.5.4 La libreria “libusb-1.0”...........................................................................................20
3 Il problema della migrazione da C a C++................................................21
3.1 Motivazioni e vantaggi...................................................................................................21
3.2 Framework software e librerie software.........................................................................21
3.3 Le differenze nell'approccio...........................................................................................22
3.3.1 Le classi..................................................................................................................22
3.3.2 L'overloading dei metodi........................................................................................23
3.3.3 I namespace.............................................................................................................24
3.3.4 I reference...............................................................................................................24
3.3.5 I casting...................................................................................................................24
3.3.6 Le eccezioni............................................................................................................25
3.3.7 La Standard Template Library................................................................................25
3.4 UML...............................................................................................................................26
3.5 Design pattern.................................................................................................................27
4 Analisi funzionale.....................................................................................29
4.1 Requisiti di funzionamento.............................................................................................29
4.2 La scelta di non realizzare un driver...............................................................................30
4.3 Modalità di utilizzo del framework................................................................................31
4.3.1 UML: Use-case diagram.........................................................................................33
4.3.2 UML: Sequence diagram........................................................................................35
4.3.3 UML: Activity diagram..........................................................................................35
5 Progettazione del framework C++............................................................37
5.1 Scelte architetturali con l'uso di Design Pattern.............................................................37
5.1.1 Façade.....................................................................................................................38
5.1.2 Lazy Initialization...................................................................................................38
5.2 Struttura del framework con uso di UML......................................................................39
5.2.1 UML: Class diagram...............................................................................................40
5.2.2 UML: Communication diagram..............................................................................42
5.3 Elenco dei metodi forniti all'utente.................................................................................43
5.3.1 Istanziazione del framework...................................................................................43
5.3.2 Gestione di connessioni multiple contemporanee...................................................43
5.3.3 Inizializzazione di comunicazioni seriali................................................................44
5.3.4 Inizializzazione di comunicazioni Ethernet............................................................45
5.3.5 Inizializzazione di comunicazioni USB..................................................................46
5.3.6 La comunicazione vera e propria coi dispositivi....................................................46
6 Implementazione del framework C++......................................................49
6.1 I sorgenti realizzati.........................................................................................................49
6.1.1 Elenco dei sorgenti che compongono il framework...............................................49
6.1.2 Alcuni dettagli sull'implementazione del codice....................................................49
6.2 La gestione del progetto.................................................................................................56
6.2.1 Le librerie a collegamento dinamico.......................................................................56
6.2.2 Il comando “make” per la compilazione.................................................................56
6.2.3 Il debugging............................................................................................................59
6.3 La documentazione.........................................................................................................60
6.3.1 Istruzioni per la compilazione.................................................................................61
6.3.2 Lo strumento “Doxygen” per la documentazione...................................................61
6.3.3 La documentazione risultante in formato HTML...................................................64
7 Testing e applicazioni di esempio.............................................................65
7.1 Il testing durante l'implementazione...............................................................................65
7.2 Applicazioni di esempio.................................................................................................65
8 Conclusioni...............................................................................................69
8.1 Bibliografia.....................................................................................................................71
1 Ringraziamenti
1 Ringraziamenti
Desidero ringraziare:
L'azienda Custom Engineering che mi ha permesso di effettuare lo stage
aziendale su cui si basa questo lavoro di tesi;
Luca Bettati, che è stato il mio tutor aziendale e che mi ha aiutato nella
comprensione dei prodotti della Custom, fornendomi la documentazione
necessaria e preziosi consigli;
L'ing. Paolo Virgili di Custom Engineering per aver organizzato lo stage;
L'ing. Luciano Varani per avermi segnalato l'offerta di stage dell'azienda;
L'ing. Federico Bergenti per aver accettato di svolgere il compito di tutor
universitario per il tirocinio aziendale e di relatore per questo lavoro di tesi;
La mia famiglia, che mi ha sempre sostenuto in questi anni di studi.
7
1 Ringraziamenti
8
2 Introduzione
2 Introduzione
Il framework progettato e realizzato, di cui tratta questo lavoro di tesi, è stato
sviluppato per la Custom Engineering S.p.A. durante un periodo di tirocinio
universitario svolto presso l'azienda.
2.1 L'azienda Custom Engineering S.p.A.La Custom Engineering S.p.A. (www.custom.biz) è un'azienda nata a Fontevivo
(PR) nel 1992, conta oggi alcune centinaia di dipendenti e ha sedi in Italia e
all'estero. Si è affermata nella progettazione, produzione e distribuzione di
soluzioni per stampa su carta termica, gestione del punto vendita, acquisizione di
immagini e si sta espandendo in altri settori di supporto tecnologico per le aziende.
I mercati principali sono quello italiano, spagnolo, russo e l'emergente mercato
cinese.
La sede legale dell'azienda, nonché ubicazione del mio tirocinio, è in Strada
Berettine 2, a Fontevivo (PR).
2.2 I prodotti
2.2.1 Soluzioni P.O.S. Retail
I prodotti per Point Of Sale (in italiano: punto vendita, o semplicemente “cassa”)
retail della Custom Engineering escono dall'azienda già pronti per l'installazione
presso i clienti. Sono numerosi e suddivisibili in queste categorie:
• Stampanti per scontrini (versioni fiscali e non fiscali)
• Stampanti per etichette
9
2.2 I prodotti
• Scanner
• PC-POS con touch-screen e stampante integrata (fiscali e non fiscali)
• Stampanti portatili (collegabili a palmari via Bluetooth o a reti Wi-Fi)
• Mini Player per immagini e video pubblicitari
• accessori come display aggiuntivi, cassetti rendi-resto, lettori di carte, tastiere
2.2.2 Soluzioni OEM/industriali
Sono soluzioni adatte ad essere installate da parte dei clienti all'interno di sistemi
personalizzati, come chioschi, ATM bancari, registratori di cassa e scanner anche
in formato A4. Alcuni di questi prodotti sono la versione “nuda” (o uno dei
componenti) di altri prodotti retail, mentre altri sono stati progettati in modo
specifico.
10
Da sinistra: stampante portatile MY3, stampante Kube II, punto-cassa Kube T, XPC-POS
Da sinistra: mini-stampante CM 112, stampante industriale PRT 80, scanner A4
2 Introduzione
2.2.3 Emettitori di biglietti (TKT) e scanner per scommesse
Questi dispositivi fanno parte di una famiglia molto numerosa ed in espansione
per la Custom Engineering, e si tratta di prodotti specifici o prodotti delle altre
famiglie integrati con nuovi componenti, come scanner per le schedine delle
giocate o lettori di codici a barre.
Esistono sia versioni retail (pronte per l'uso) sia OEM (da integrare in altri
sistemi personalizzati prima dell'uso).
2.2.4 Soluzioni multimediali
Questo è un settore nuovo ed in
espansione, e include monitor (anche
touch-screen) di varie dimensioni e
prodotti denominati “Mini Player”,
che sono dei piccoli ed economici
computer dal basso consumo, in
grado di gestire schermi S-VGA e riprodurre sequenze pubblicitarie: anch'essi
supportano una parte del protocollo di stampanti e scanner, ma solo via Ethernet, e
sono compatibili con il software realizzato durante il tirocinio.
11
Stampante Kube II Scanner, stampanti di biglietti con RFID e barcode: TK300 II e KPM300 H
Mini Player e monitor touch LCD 17" M17
2.3 Il lavoro svolto nel tirocinio aziendale
2.3 Il lavoro svolto nel tirocinio aziendaleDurante il periodo di tirocinio, che si è svolto da fine dicembre 2008 a fine aprile
2009, su un totale di circa 450 ore, sono stato inserito nel reparto di ricerca e
sviluppo, dove mi è stato assegnato un tutor aziendale (Luca Bettati), che cinque
anni prima aveva svolto un'esperienza simile alla mia proprio alla Custom, e che
mi ha dato il supporto necessario per conoscere i prodotti con cui interfacciarmi, i
relativi comandi integrati, nonché le linee-guida per le specifiche del software. In
ogni caso mi è stata concessa parecchia libertà riguardo la strutturazione del
progetto, la stesura del codice, nonché nel modo in cui creare la documentazione.
L'azienda mi ha richiesto la realizzazione di due versioni, per sistema operativo
Linux, della stessa libreria software: una più semplice in linguaggio C, per
questioni di leggerezza e compatibilità con sistemi embedded ad uso industriale e
vecchi personal computer che sono utilizzati da alcuni clienti, e l'altra, che
chiameremo “framework”, in linguaggio C++, per sfruttare le caratteristiche più
avanzate offerte da questo linguaggio.
Lo scopo del software doveva essere quello di comunicare con i dispositivi
prodotti dalla Custom Engineering attraverso tre tipi di connessione (seriale,
Ethernet, USB) in modo trasparente all'utente, ovvero in modo che non si notino
differenze nel comunicare con connessioni differenti.
Come linea-guida mi è stata consegnata la documentazione di un'analoga libreria
per ambiente Windows, già sviluppata precedentemente dalla Custom Engineering
con Microsoft Visual Studio. Il mio compito è stato quello di progettare e
realizzare le versioni per Linux con un comportamento il più simile possibile a
quella per Windows, in particolar modo riguardo i nomi di funzioni, metodi e
classi e i meccanismi di uso che si devono adottare per interfacciarsi con queste
librerie software.
12
2 Introduzione
A livello di strumentazione mi sono stati forniti un PC con sistema operativo
Linux Fedora 10, per sviluppare e testare il mio lavoro, e alcuni modelli di
stampanti, scanner e Mini Player, da collegare al computer con i tre diversi tipi di
collegamento supportati.
2.3.1 Descrizione del lavoro di tesi
Questo lavoro di testi tratta principalmente della migrazione ed estensione da
“libreria C” (realizzata per prima) a “framework C++”, mostrando le operazioni
svolte per sfruttare il più possibile le caratteristiche del linguaggio orientato agli
oggetti e descrivendo le tecniche di analisi e progettazione utilizzate per lo scopo.
Inoltre si descriveranno le principali chiamate di sistema utilizzate per la
comunicazione a basso livello con i dispositivi, i dettagli sulla documentazione
creata a corredo del framework C++ e alcuni programmi di esempio che lo
utilizzano per mostrarne le possibili applicazioni pratiche.
2.4 Il protocollo ad alto livello dei prodotti Cus tomI collegamenti di cui sono dotati i dispositivi Custom Engineering sono la porta
seriale RS-232, la porta Ethernet (con protocollo TCP o UDP) o la porta USB
(quest'ultima con tre diversi tipi di interfacciamento logico). Tutti i prodotti, ad
eccezione di alcuni accessori, condividono un protocollo di comunicazione
proprietario, basato su sequenze di codici esadecimali dette ESC/POS™ (per
richieste speciali), dati testuali (per la stampa di testo) e dati binari (per la stampa
o l'acquisizione di immagini).
Va sottolineato che la gestione del significato di comandi e relative risposte non
è compito del framework realizzato, che lavora ad un livello più basso,
occupandosi del corretto invio dei dati e della corretta ricezione di eventuali
risposte. A titolo di esempio ecco alcuni dei principali comandi ESC/POS™
13
2.4 Il protocollo ad alto livello dei prodotti Custom
esadecimali (ne esistono decine):
NOME SEQUENZA SIGNIFICATO
DLE_EOT 0x10, 0x04, 0x14 Richiesta informazioni sullo stato attuale deldispositivo. Risposta di 6 byte.
GSI 0x10, 0x04, 0x15 Richiesta informazioni compatte. Risposta di 1 byte.
HD_TEMP 0x1D, 0xDE Richiesta temperatura testina. Risposta di 6 byte.
FULL_CUT 0x1B, 0x6D Taglio della carta con la taglierina integrata.
EJECT 0xB0 Espulsione del ticket già tagliato (pochi modelli).
BAR_CODE 0x0A, 0x0A, …,0x00
Indica l'inizio di un codice a barre da stampare. La parte […] determina i valori del codice a barre.
Alcuni comandi sono disponibili per tutti i dispositivi della Custom Engineering,
mentre altri non lo sono: ad esempio i Mini Player non supportano i comandi di
taglio della carta, mentre l'espulsione dei ticket è possibile solo con i modelli che
stampano con un ulteriore arrotolamento della carta su un secondo rullo interno.
Inoltre, buona parte dei modelli è disponibile con tutti i tre tipi di collegamento al
PC su cui si è svolto questo lavoro, e quasi tutti i comandi sono identici per ogni
collegamento. Ci sono però alcune eccezioni, come per esempio l'acquisizione di
immagini con la Kube II Scanner, che è possibile con solo una delle interfacce
logiche disponibili sul collegamento USB, oppure via Ethernet o seriale.
2.5 Comunicazione Seriale, Ethernet, USB in LinuxOra si vedrà come è possibile comunicare con questi diversi tipi di collegamento
all'interno di un programma per i sistemi operativi della famiglia Linux, in cui i
dispositivi presenti sul PC (i “device” ) sono trattati come i file.
2.5.1 Comunicazione seriale
In ambiente Linux i dispositivi seriali sono normalmente identificati con nomi
del tipo /dev/ttySxxxx dove al posto della xxxx si trova un numero progressivo che
parte da 0 e arriva 9 (anche se di norma le porte seriali sono al massimo due, vale
14
2 Introduzione
a dire /dev/ttyS0 e /dev/ttyS1).
Per l'accesso in fase di programmazione si possono utilizzare le chiamate di
sistema “tcsetattr()”, “open()”, “close()”, “write()”, “read()” e “select()”, incluse
nell'header file <fcntl.h>. Ecco i prototipi di queste funzioni di sistema e la
descrizione delle più interessanti:
int open(const char* pathname, int flags);
int tcsetattr(int fd, int flags, const struct termios* options);
int close(int fd);
ssize_t write(int fd, const void* buf, size_t count);
ssize_t read(int fd, void* buf, size_t count);
int select(int nfds, fd_set* readfds, fd_set* writefds,
fd_set* exceptfds, struct timeval* timeout);
La “open()”, il cui primo parametro dev'essere una stringa come “/dev/ttyS0”
ed il secondo una maschera di bit che permette di specificare opzioni quali la
modalità d'accesso (lettura/scrittura), in caso di successo restituisce un
identificatore per il dispositivo aperto, che sarà poi utilizzato con le altre funzioni.
La “tcsetattr()” permette di impostare le opzioni della porta seriale, cioè baud-
rate (velocità di trasferimento), controllo di parità, bit di stop, controllo di flusso e
lunghezza dei pacchetti. Questo avviene impostando opportunamente una struct
termios, definita nel file <termios.h> e composta di maschere di bit, e passandola a
questa funzione.
La “select()” è stata utilizzata in fase di lettura, con un timeout, per evitare che il
programma vada in blocco se non ci sono dati da leggere: infatti con la
comunicazione seriale i dati vengono subito trasferiti dal dispositivo fisico ad un
buffer interno al computer, che può essere letto con la funzione di sistema
“read()”, la quale rimane in attesa di dati finché non ce ne sono: senza l'uso della
“select()” si rischierebbe di bloccare il programma.
15
2.5 Comunicazione Seriale, Ethernet, USB in Linux
2.5.2 Comunicazione Ethernet
In Linux la comunicazione Ethernet-LAN (applicabile a dire il vero anche a
connessioni senza fili Wi-Fi, se presenti sul sistema) utilizza i “socket”, che
identificano connessioni virtuali ad altri dispositivi sulla rete locale o Internet. Per
stabilire una connessione di rete tramite socket è necessario conoscere l'indirizzo
di rete (IP) o il nome dell'host del dispositivo a cui ci si vuole connettere, oltre al
numero di porta e al tipo di protocollo da utilizzare (TCP o UDP). I dispositivi
della Custom fanno da server, ed ascoltano per accettare connessioni, quindi
vedremo solo le funzioni di sistema lato client utilizzate, definite in <netdb.h> e
<sys/socket.h>, ovvero:
int socket(int socket_family, int socket_type, int protocol);
int connect(int sockfd, const struct sockaddr* serv_addr,
socklen_t addrlen);
int setsockopt(int s, int level, int optname, const void* optval,
socklen_t optlen);
int bind(int sockfd, const struct sockaddr* local_addr,
socklen_t addrlen);
Per quanto riguarda il protocollo TCP è necessaria la chiamata di
“socket(AF_INET, SOCK_STREAM, 0)” che restituisce il numero identificativo per
la connessione, da passare poi alle altre funzioni che dovranno utilizzarlo. È anche
necessaria una chiamata a “setsockopt()” con appositi parametri per attivare il
sistema di “keep-alive”, ovvero una tecnica che si occupa di mantenere attiva la
connessione col server (cioè col dispositivo della Custom Engineering) anche in
assenza di attività per lunghi periodi di tempo. La connessione TCP è bilaterale,
cioè il traffico può viaggiare dal computer al dispositivo e viceversa, ed è sempre
attiva: ciò garantisce il corretto ordine di ricezione dei pacchetti, perché seguono
tutti lo stesso percorso. La ricezione e l'invio di dati avvengono con le funzioni:
ssize_t recv(int socket, void* buf, size_t len, int flags);
ssize_t send(int socket, const void* buf, size_t len, int flags);
16
2 Introduzione
A differenza del TCP, una connessione UDP è stabilita in modo unilaterale,
quindi per comunicare con i dispositivi Custom servono due socket: uno per
ricevere ed uno per inviare. Non è necessaria l'impostazione della tecnica di
“keep-alive”, che non esiste affatto dato che le connessioni UDP si attivano e
disattivano ad ogni trasferimento di dati. Le creazioni dei socket UDP avvengono
con due chiamate a “socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)” che
restituiscono due identificatori. Nel caso del socket per la ricezione serve poi una
chiamata a “bind()” che collega il nome al socket fisico, mentre per l'invio bisogna
distinguere due casi: una chiamata a “bind()” si usa in caso di normale
connessione verso un dispositivo, mentre per l'invio di dati come broadcast di rete
serve la chiamata:
setsockopt(iSocket, SOL_SOCKET, SO_BROADCAST, &iOpt, sizeof(int));
Il broadcast serve se si vuole che i dati siano inviati a tutti i dispositivi collegati
alla rete locale che si trovano in ascolto sulla porta UDP specificata e può essere
utile con certi dispositivi Custom come i Mini Player, per effettuare operazioni su
un numero elevato di essi. L'invio di dati in broadcast di norma non si effettua con
comandi che richiedono una risposta da parte dei dispositivi, in quanto per
ricevere le risposte si dovrebbero creare numerosi socket UDP. Per le connessioni
UDP la ricezione e l'invio di dati avvengono con le funzioni:
ssize_t recvfrom(int socket, void* buf, size_t length, int flags,
struct sockaddr* from, socklen_t* fromlen);
ssize_t sendto(int socket, const void* buf, size_t length, int flags,
const struct sockaddr* to, socklen_t tolen);
Sia per TCP, sia per UDP, si può impostare un “timeout” di ricezione per non
bloccare il programma in caso di mancata risposta da parte del dispositivo. Un
modo per farlo è il seguente, dove “Timeout” è una struct timeval:
setsockopt(iSocket,SOL_SOCKET,SO_RCVTIMEO,&Timeout,sizeof(Timeout));
Allo stesso modo si può impostare un timeout di scrittura per evitare blocchi del
17
2.5 Comunicazione Seriale, Ethernet, USB in Linux
programma se c'è stata un'improvvisa disconnessione.
2.5.3 Comunicazione USB
I dispositivi della Custom Engineering supportano, in generale, tre tipi logici di
connessione USB, a seconda del modello e delle funzionalità che si vogliono
utilizzare, e che distingueremo con questi nomi:
• “interfaccia 0”: la comunicazione è a basso livello, molto simile ad un
driver, ed utilizza gli URB (USB Request Block) forniti dal kernel Linux;
• “interfaccia 1”: normale accesso tramite “device” di sistema, che hanno
nomi del tipo /dev/usb/lpxxxx dove la xxxx è un numero progressivo che
parte da 0;
• “interfaccia 2”: comunicazione di basso livello effettuata in questo
progetto tramite la libreria “libusb-1.0”.
Le comunicazioni con interfaccia 0 richiedono la selezione del dispositivo a cui
connettersi in base a VID (Vendor ID, numero di 2 byte che identifica il produttore
del dispositivo USB, che dovrebbe essere univoco su scala mondiale: 0DD4 in
esadecimale per la Custom Engineering), PID (Product ID, sempre di 2 byte, per
identificare il modello) e Serial Number (stringa testuale di lunghezza variabile
che cambia per ogni esemplare). Tutto ciò richiede una serie di operazioni molto
complesse, che a grandi linee sono queste: si richiede al sistema la lista dei
dispositivi USB presenti, si estraggono da ognuno VID, PID e Serial Number e li
si confrontano con quelli forniti dall'utente (può specificarne anche solo uno);
quando si trova una corrispondenza viene aperta una connessione tramite la
funzione di sistema “open()” passando come parametro una stringa che è il nome
di file che ha nel sistema il device corrispondente ai dati richiesti: questo nome
però non è noto all'utente. La funzione di sistema utilizzata per le operazioni di
18
2 Introduzione
lettura e scrittura con l'interfaccia 0 è la “ioctl()”, definita in <sys/ioctl.h>; eccone
il prototipo:
int ioctl(int id, int request, ...);
Il primo parametro è l'identificatore restituito dalla “open()”, mentre il secondo
deve essere la costante IOCTL_USB_READURB in caso di lettura oppure
IOCTL_USB_SUBMITURB in caso di scrittura; come terzo parametro si è utilizzato
l'indirizzo di una struct che contiene, in un opportuno formato, i dati trasferiti in
caso di lettura, ed i dati da trasferire in caso di scrittura; “ioctl()” non è una
funzione bloccante, per cui la gestione dei timeout di lettura o scrittura va
effettuata manualmente: in particolare per la lettura è necessario gestire
correttamente il timeout (ripetendo ad oltranza la lettura nei limiti di tempo
imposti, se non è stato letto ancora alcun dato) in quanto sull'interfaccia 0 le
risposte dei dispositivi Custom Engineering devono necessariamente essere lette
immediatamente dopo l'invio di dati, motivo per cui la lettura viene effettuata
subito nel programma, ed i dati vengono salvati in un buffer e forniti all'utente
solo quando anch'egli ne richiederà la lettura.
Con l'interfaccia 1 invece la comunicazione avviene in modo analogo alla porta
seriale, anche se non è necessario impostare parametri come la velocità di
trasferimento. Le funzioni di sistema da utilizzare sono quindi “open()”, “close()”,
“write()”, “read()” e “select()”.
Le comunicazioni con l'interfaccia 2 utilizzano le funzioni fornite dalla “libusb-
1.0”, che per lettura e scrittura si comportano in modo simile alle “read()” e
“write()” di sistema; la selezione del dispositivo con cui comunicare è invece
basata su VID, PID e Serial Number, come per l'interfaccia 0, ma
l'implementazione tramite “libusb-1.0” è completamente diversa e si vedrà nel
prossimo paragrafo.
19
2.5 Comunicazione Seriale, Ethernet, USB in Linux
2.5.4 La libreria “libusb-1.0”
Questa libreria (www.libusb.org), fornita con licenza GNU Lesser General
Public License version 2.1, è scritta in linguaggio C e questo non la rende ideale
per l'utilizzo in un framework ad oggetti. Infatti non è possibile istanziare più volte
la “libusb” ma solo chiamare le sue funzioni. Fortunatamente gli sviluppatori
hanno previsto la possibilità di gestire più collegamenti contemporanei: questa
caratteristica è stata implementata con l'uso, in tutte le funzioni, di un parametro
identificativo (che nella versione C della libreria prodotta era lasciato a NULL in
quanto non serviva) che permette di indicare alla “libusb” su quale connessione
effettuare qualunque operazione. Questo parametro è un puntatore ad una struct di
tipo libusb_device_handle che è definita dalla “libusb”.
Le funzioni e le strutture dati fornite iniziano tutte col suffisso “libusb_”. Per
l'attivazione della connessione sono state utilizzate molte funzioni che servono ad
ottenere VID, PID e Serial Number, come “libusb_get_device_descriptor()”,
“libusb_get_string_descriptor_ascii()”, “libusb_open()” e “libusb_close()”, ma
non le vedremo nel dettaglio. La più interessante è la funzione di trasferimento dei
dati:
int libusb_bulk_transfer(struct libusb_device_handle *dev_handle,
unsigned char endpoint, unsigned char *data,
int length, int *transferred,
unsigned int timeout);
che si utilizza sia per la lettura sia per la scrittura, con la differenza che si deve
indicare un differente endpoint in base all'operazione da effettuare. I due endpoint
di lettura e di scrittura devono essere ricavati chiamando la funzione
“libusb_get_active_config_descriptor()” in fase di apertura della connessione, e
sono restituiti all'interno di una struct libusb_config_descriptor.
20
3 Il problema della migrazione da C a C++
3 Il problema della migrazione da C a C++
3.1 Motivazioni e vantaggi
Le principali motivazioni che hanno spinto l'azienda alla richiesta di una
versione in linguaggio C++ sono la volontà di fornire un software facilmente
estensibile, scritto in un linguaggio moderno orientato agli oggetti, e la necessità
di poter gestire senza complicazioni l'esistenza di più connessioni contemporanee.
La struttura a classi rende un software di interfacciamento come questo più
adatto ad essere utilizzato da altri programmi, rispetto ad una semplice libreria di
funzioni scritta in linguaggio C.
3.2 Framework software e librerie softwareLe librerie di funzioni, tipiche delle implementazioni procedurali con linguaggi
imperativi come il C, sono un insieme di funzioni fornite all'utente senza una
struttura che definisca che cosa può fare l'utente e quando può farlo, per cui sono
necessari maggiori controlli “manuali” nel codice per evitare un uso non corretto.
I framework si differenziano dalle librerie perché, grazie ad una struttura a classi
opportunamente realizzata, con interfacce separate, danno all'utente la possibilità
di effettuare solo le operazioni che egli dovrebbe poter fare in quel momento. In
pratica in un framework il flusso che ne definisce l'utilizzo dovrebbe essere
comandato dal framework stesso, invece che dal suo utente.
Inoltre una eventuale futura estensione per supportare altri dispositivi o
funzionalità è semplice: è sufficiente aggiungere le opportune classi e fare poche
modifiche all'interfaccia-utente esistente.
21
3.3 Le differenze nell'approccio
3.3 Le differenze nell'approccio
Si vedranno ora le principali differenze, a livello concettuale, tra i linguaggi di
programmazione C e C++, che sono state sfruttate nella realizzazione del
framework. Nei capitoli successivi si vedranno più nel dettaglio le loro
implicazioni pratiche.
3.3.1 Le classi
Le classi devono essere presenti in un linguaggio di programmazione perché
esso possa dirsi “orientato agli oggetti”.
Le classi sono la più importante estensione del linguaggio C++ rispetto al C:
sono simili alle strutture di dati struct già presenti nel C, ma possono contenere
funzioni, che prendono il nome di metodi, mentre le variabili prendono il nome di
attributi. Questo consente, a tempo di esecuzione, di avere degli oggetti di una
classe, i cui attributi possono essere manipolati dall'utente solo utilizzando i
metodi pubblici forniti dalla classe stessa.
La programmazione ad oggetti consente quindi una scomposizione del codice in
moduli (rappresentati dalle diverse classi) che interagiscono tra loro.
L'incapsulamento è un concetto secondo cui un oggetto, contenendo al suo
interno tutti i suoi metodi ed attributi, è visto dall'esterno come una scatola nera.
Solo i metodi definiti dalla classe come pubblici potranno essere visti ed utilizzati
dall'esterno. In linguaggio C è sempre possibile utilizzare una funzione con dati
anche mal formati, forniti dall'utente; invece in C++, programmando ad oggetti,
sono i metodi della classe che devono occuparsi del mantenimento di uno stato
ben formato e questo rende più facile l'utilizzo corretto del software dall'esterno e
più difficile, o meglio impossibile, il raggiungimento di stati inconsistenti. Una
classe può incapsulare al suo interno anche altre classi, che non sono viste
22
3 Il problema della migrazione da C a C++
dall'esterno, e gestirne direttamente le istanze a tempo di esecuzione.
L'ereditarietà è ciò che permette di creare classi derivate da altre, che ne
ereditano tutte le caratteristiche, ne possono modificare una parte e ne possono
aggiungere altre. Una classe “B” eredita da una classe “A” quando si può dire che
ogni oggetto o istanza di tipo “B” è anche di tipo “A”.
Il polimorfismo si applica al concetto di ereditarietà e consente di ottenere, in
esecuzione, la possibilità di inviare lo stesso messaggio (chiamare lo stesso
metodo) a diversi oggetti di classi derivate da una classe comune ed ottenere un
comportamento differente (a causa della differente implementazione dello stesso
metodo nelle diverse classi).
Per l'allocazione di istanze (oggetti) di classi viene utilizzato il relativo metodo
costruttore, che permette di inizializzare gli attributi e svolgere alcune operazioni
di avvio in automatico. Una classe può avere più di un costruttore (grazie
all'overloading).
Il distruttore è invece il metodo che si occupa dell'eliminazione di ogni oggetto
di una classe e può essere definito in modo personalizzato quando è necessario
liberare aree di memoria istanziate dinamicamente dalla classe.
Le classi possono anche essere copiate o duplicate tramite il costruttore di copia
o l'overloading dell'operatore di assegnamento (caratteristiche che non sono state
utilizzate in questo framework perché non adatte).
3.3.2 L'overloading dei metodi
L'overloading (sovraccaricamento) dei metodi è ciò che consente di avere più di
un metodo con lo stesso nome, ma con tipo o numero di parametri differente: il
compilatore determinerà quale metodo invocare in base alla rispondenza dei
parametri con uno dei metodi che hanno lo stesso nome.
23
3.3 Le differenze nell'approccio
3.3.3 I namespace
Un problema del linguaggio C nella realizzazione di librerie di funzioni è il fatto
che tutte le funzioni, variabili globali ed anche define, a tempo di esecuzione si
trovano nello stesso spazio dei nomi: ad esempio se la libreria sarà utilizzata da
un'altra applicazione che ha una funzione con lo stesso nome di una funzione della
libreria, la compilazione risulterà impossibile e costringerà alla modifica del
codice.
In C++ parte del problema, se si programma strettamente ad oggetti, è risolto
perché i metodi appartengono solo alle istanze della loro classe; invece per
eventuali variabili globali e per le define si possono utilizzare i namespace, che
sono dei contenitori che limitano ad una parte di programma la validità dei nomi.
3.3.4 I reference
Un reference (riferimento) è la possibilità offerta dal C++ di riferirsi ad un
oggetto con un altro nome, oltre a quello originale, ed è utile come alternativa ai
puntatori per il passaggio di parametri se il metodo li deve poter modificare.
All'interno del metodo che riceve il parametro per riferimento, questo viene
utilizzato come se fosse un attributo locale.
3.3.5 I casting
Nel linguaggio C++ è stato aggiunta una nuova sintassi per le conversioni dei
tipi di dati (casting), con quattro diverse tipologie:
• const_cast: cambia l'essere o non essere “const” di un oggetto;
• static_cast: cambia il tipo controllando la validità solo a compile-time;
• dynamic_cast: come lo static_cast, ma controlla a tempo di esecuzione
che la qualificazione sia corretta;
24
3 Il problema della migrazione da C a C++
• reinterpret_cast: cambia il tipo del dato in un altro tipo, anche puntatori
con non puntatori ad esempio.
3.3.6 Le eccezioni
Le eccezioni sono un'importante strumento che consente di gestire la consistenza
degli attributi di una classe, e sono un'alternativa alla gestione manuale degli
errori. Quando si verifica un evento imprevisto l'istanza della classe può lanciare
un'eccezione (identificata da un nome) e terminare. L'eccezione viene propagata
all'oggetto che conteneva quello terminato, che può intercettarla (ed intraprendere
una certa azione, compreso eventualmente propagarla al livello superiore come
una diversa eccezione) oppure non gestirla ed essere terminato a sua volta, il che
può portare fino alla terminazione dell'intero programma.
3.3.7 La Standard Template Library
La STL è una libreria inclusa nel linguaggio C++ che fornisce contenitori,
iteratori, algoritmi e oggetti funzione (oggetti con un solo metodo utilizzabili
come chiamate a funzione sfruttando il loro costruttore).
Nel framework realizzato sono stati utilizzati i contenitori “map” e “vector” e gli
“iterator”:
• le “map” (mappe) sono contenitori associativi ordinati di chiavi uniche,
associate a dei dati: in pratica ogni elemento della mappa è una coppia
<“indice”, “dato”>; le mappe sono di dimensione variabile, l'indice
(intero) è univoco e il tipo di “dato” è uniforme all'interno di una mappa.
• i “vector” sono contenitori che implementano array dinamici, ovvero che
possono variare la propria dimensione, e possono contenere qualunque
tipo di dato; i “vector” sono uniformi: tutti gli elementi di un “vector”
sono dello stesso tipo ed è possibile accedervi anche grazie all'overloading
25
3.3 Le differenze nell'approccio
dell'operatore “[indice]”, implementato dalla STL;
• gli “iterator” sono oggetti che permettono di spostarsi all'interno di
contenitori: dopo aver istanziato un iteratore è possibile utilizzarlo per
accedere ai diversi elementi del contenitore agendo sull'iteratore come se
si trattasse di un indice intero, ad esempio incrementandolo ed usandolo
per controllare cicli. Ecco come si può dichiarare e definire un iteratore
che punta al primo elemento di una mappa:
MyMapType::iterator i = MyMapInstance.begin();
3.4 UMLL'Unified Modeling Language è un linguaggio di modellazione nato per
supportare la progettazione di software orientato agli oggetti ed ha assunto un
ruolo chiave nell'ingegneria del software, evolvendosi assieme ai linguaggi di
programmazione ad oggetti e giungendo oggi alla versione 2.3.
I tipi di diagrammi sono suddivisibili in due macro-categorie:
• strutturali (o statici): mostrano la struttura statica di un sistema, le classi
che lo compongono, la loro composizione e le relazioni statiche tra esse;
• comportamentali (o dinamici): mostrano il comportamento del sistema in
esecuzione, sia rappresentando le collaborazioni con l'utente, sia quelle
interne al sistema.
Attualmente UML definisce ben 14 tipi di diagrammi, suddivisi equamente tra le
due categorie appena descritte. Nei capitoli 4 “Analisi funzionale” e 5
“Progettazione del framework C++” si vedranno i tipi di diagramma UML
utilizzati per questo software durante tali fasi dello sviluppo.
26
3 Il problema della migrazione da C a C++
3.5 Design patternNella progettazione dei software ad oggetti è facile incontrare problemi
ricorrenti, per risolvere i quali è utile trovare soluzioni riutilizzabili: a questo
servono i design pattern. Sono stati definiti nel libro “Design Patterns: Elements
of Reusable Object-Oriented Software” da un gruppo di quattro progettisti
software (la “Gang of Four”) nel 1991.
Con i design pattern è possibile trovare un aiuto per la strutturazione del
progetto, riducendo il tempo necessario alla progettazione ed evitando la possibile
creazione di soluzioni azzardate con architetture poco consistenti o che non
rispettano i paradigmi della programmazione orientata agli oggetti.
Un pattern deve essere definito dettagliando almeno queste caratteristiche:
• Nome
• Tipo di problema che risolve, con eventuali esempi noti
• Struttura e descrizione della soluzione che fornisce
• Conseguenze del suo impiego
Inoltre dovrebbe anche fornire suggerimenti per l'implementazione, codice di
esempio in vari linguaggi di programmazione e i nomi di alcuni pattern correlati.
Le categorie in cui si possono suddividere i design pattern sono tre:
• creazionali: gestiscono la creazione dinamica degli oggetti nel sistema;
• strutturali: permettono il riutilizzo di micro-architetture statiche;
• comportamentali: descrivono il comportamento di un insieme di oggetti.
Nel capitolo 5 si vedranno i design pattern utilizzati nel framework prodotto.
27
3.5 Design pattern
28
4 Analisi funzionale
4 Analisi funzionale
L'analisi funzionale è la prima fase del ciclo di vita di un software, e si occupa di
definire le specifiche del progetto da realizzare, partendo dai requisiti di
funzionamento: in questo caso sono stati definiti all'interno dell'azienda, in base
all'esperienza che il marketing ha riportato riguardo le richieste dei clienti.
Questa fase include anche lo studio di fattibilità, che in questo caso era già stato
fatto per la versione C, in cui è stata raccolta la documentazione necessaria per
utilizzare le funzioni di sistema, che poi sono state usate per implementare le
comunicazioni tramite le porte seriale, Ethernet ed USB.
4.1 Requisiti di funzionamentoIl software realizzato doveva principalmente presentarsi come una libreria a
collegamento dinamico che permettesse all'utente di interfacciarsi con le
stampanti, gli scanner e gli altri prodotti della Custom Engineering, senza doversi
preoccupare del tipo di collegamento utilizzato (seriale, Ethernet-UDP, Ethernet-
TCP o USB, in quest'ultimo caso con tre diversi tipi di interfacce), fatto salvo
naturalmente per la scelta in fase di inizializzazione del collegamento.
Ad esempio un programmatore che dovesse trovarsi a sviluppare un'applicazione
con interfaccia grafica per comunicare con i prodotti della Custom, non dovrà
preoccuparsi di quale porta sarà utilizzata per le comunicazioni: dovrà solo
predisporre la possibilità di scegliere la porta da usare (o anche solo forzarne un
determinato tipo): il framework ComLayer C++ gestirà la comunicazione a basso
livello, senza mostrare, al livello superiore, differenze al variare della connessione
fisica utilizzata.
29
4.1 Requisiti di funzionamento
Un altro requisito era la possibilità di poter connettere più dispositivi
contemporaneamente allo stesso PC o sistema embedded, eventualmente su porte
di tipo differente: riguardo questo aspetto la versione C++ ha presentato in
maniera naturale la possibilità di istanziare più volte la classe principale del
framework realizzato. Tuttavia si è scelto di integrare direttamente nella classe
“ComLayer” la possibilità di gestire autonomamente un numero arbitrario (nei
limiti fisici e software della macchina) di connessioni contemporanee, attraverso
appositi metodi che utilizzano alcuni tipi di dato della C++ Standard Template
Library (std::vector, std::map), in cui vengono memorizzate le istanze
corrispondenti ai diversi collegamenti. Nel capitolo 7.2, riguardante le
applicazioni di esempio, si vedranno dei programmi in grado di sfruttare anche
questa caratteristica.
La versione che sarà fornita ai clienti della Custom Engineering non prevederà la
consegna del codice sorgente, ma solamente della libreria “libComLayer.so” già
compilata e dell'header file “ComLayer.h”, oltre ovviamente alla documentazione,
di cui si parlerà più avanti.
4.2 La scelta di non realizzare un driverGià prima dello sviluppo della versione C si è scelto di non realizzare un driver,
che potrebbe invece sembrare la scelta più logica visto che stiamo parlando
principalmente di stampanti e scanner, ma non è così.
I motivi sono i seguenti:
• La differente tipologia dei prodotti con cui interfacciarsi (stampanti,
scanner, mini-player per la riproduzione di banner pubblicitari, etc...) che
hanno comandi in comune, ma anche comandi differenti, che avrebbero
causato la necessità di creare tanti driver diversi;
30
4 Analisi funzionale
• La possibilità di funzionare comunicando con prodotti futuri non ancora
sviluppati;
• Il fatto che i prodotti Custom Engineering hanno comandi di
formattazione integrati e la possibilità di avere loghi grafici pre-caricati da
richiamare semplicemente con un'istruzione senza appesantire il lavoro
per il sistema: questo tipo di comandi è di facile utilizzo anche senza un
driver;
• Il funzionamento in sistemi “embedded”, ovvero molto specifici per scopi
industriali o commerciali, dotati a volte di poca potenza e quindi di
distribuzioni Linux piuttosto scarne: il framework così realizzato non ha la
necessità di appoggiarsi ad un gestore delle code di stampa come CUPS,
che potrebbe non essere presente nel sistema;
• Il fatto che non sono stampanti e scanner di uso comune ma sono da
utilizzarsi in ambito professionale e specializzato;
• La richiesta da parte di molti clienti di avere il controllo diretto della porta
sia per motivi prestazionali (i driver sono più lenti) sia di compatibilità.
4.3 Modalità di utilizzo del frameworkIn base allo studio effettuato, con l'aiuto della versione “Windows” fornitami
dall'azienda, è stato stabilito nel dettaglio come l'utenza del framework avrebbe
dovuto interfacciarsi con esso, ovvero come avrebbe dovuto fare per comunicare
con i prodotti della Custom Engineering utilizzando questo prodotto software.
Tutte le informazioni di utilizzo, di cui si tratterà sinteticamente in questo
paragrafo e nei successivi, sono disponibili nella documentazione in formato
HTML che è stata generata per i clienti dell'azienda, ed è fondamentale per poterla
utilizzare.
31
4.3 Modalità di utilizzo del framework
Il framework “ComLayer C++”, come già detto, permette di comunicare su tre
diversi tipi di connessione fisica, alcuni dei quali hanno diversi tipi di connessione
logica.
La classe “ComLayer” è quella che dovrà essere istanziata dall'utente del
framework e fornisce tutti i metodi necessari per gestire le connessioni e per
comunicare tramite tutte le porte supportate dai prodotti Custom Engineering:
buona parte di questi metodi sono in comune a tutti i tipi di collegamento (ad
esempio i metodi di apertura, chiusura, lettura, scrittura, impostazione timeout),
mentre altri sono specifici per un singolo collegamento (fondamentalmente i
metodi di inizializzazione/configurazione della connessione, da invocare prima di
tentare l'apertura vera e propria della comunicazione).
Per quanto riguarda i metodi comuni, la classe “ComLayer” va a comportarsi
allo stesso modo con tutte le connessioni, ovvero svolge il ruolo di interfaccia tra
l'utente ed il livello inferiore, utilizzando i metodi dichiarati nella classe astratta
“ComBase”, da cui sono state derivate le classi “ComSerial”, “ComEth” e
“ComUsb”; queste implementano in modo differente i metodi virtuali della classe
base, alcuni dei quali sono virtuali puri (ovvero devono necessariamente essere
implementati da ogni classe derivata), mentre altri sono virtuali semplici (ovvero
possono essere re-implementati da ogni classe derivata).
Riguardo, invece, le funzionalità specifiche per i vari tipi di connessione, la
classe “ComLayer” implementa metodi appositi che si interfacciano con i
corrispondenti metodi delle tre classi derivate dalla “ComBase”; questi metodi non
sono stati dichiarati nella classe base “ComBase”, ma sono aggiunti dalle diverse
classi derivate. La classe “ComLayer” gestisce al suo interno le istanze delle classi
“ComSerial”, ComEth” e “ComUsb” (derivate dalla “ComBase”), ed è quindi in
grado di verificare (con dynamic_cast) se una chiamata da parte dell'utente è
corretta o meno: ad esempio è restituito errore nel caso in cui un utente invochi il
32
4 Analisi funzionale
metodo di inizializzazione per porta seriale su di una connessione Ethernet, dato
che quel metodo non esiste affatto nelle istanze della classe “ComEth”.
4.3.1 UML: Use-case diagram
Questo diagramma “dei casi d'uso” in UML rappresentata come l'utente
(“attore”, in questo caso si tratta di un altro software) del framework (“sistema”)
interagisce con esso. E' anche presente una minimale schematizzazione dei
processi interni conseguenti all'azione dell'utente.
A questi diagrammi è associata una descrizione tabellare per ogni tipo d'azione.
In questo caso c'è sempre un solo attore, quindi sarà omesso nelle descrizioni:
• Caso d'uso: “crea porta”. Tipo: “essenziale”.
Descrizione: l'utente crea una nuova porta indicandone il tipo ed eventuali
dettagli riguardanti il file di log, che è facoltativo.
Azione attore Risposta sistema1. Richiede la creazione di una porta,specificandone il tipo (seriale, Ethernet oUSB) e le eventuali impostazioni del log.
33
4.3 Modalità di utilizzo del framework
2. Crea la porta, la rende attiva e salva leimpostazioni per l'eventuale file di log.
• Caso d'uso: “elimina porta”. Tipo: “primario”.
Descrizione: l'utente elimina una porta esistente indicando l'identificatore.
Azione attore Risposta sistema1. Richiede di eliminare una porta,specificandone l'id univoco.
2. Distrugge tutte le istanze associate allaporta. Eventuali connessioni attivevengono chiuse in modo non corretto.
• Caso d'uso: “seleziona porta”. Tipo: “primario”.
Descrizione: l'utente sceglie quale porta rendere attiva, se ne esiste più di
una, per effettuarvi comunicazioni.
Azione attore Risposta sistema1. Scelta della porta tramite l'id univoco.
2. Cambia la porta attiva per operazionidi configurazione e comunicazione.
• Caso d'uso: “configura porta”. Tipo: “essenziale”.
Descrizione: l'utente invia informazioni di configurazione per la porta
attiva, anche modificandola se già configurata in precedenza, ma non è
possibile cambiarne il tipo (ad esempio da USB a seriale).
Azione attore Risposta sistema1. Invia parametri di configurazione comeil nome del device con cui comunicare.
2. Salva la configurazione relativamentealla porta attiva, per le successive fasi dicomunicazione con essa
3. Se il log è attivo registra su log-file.
34
4 Analisi funzionale
• Caso d'uso: “comunica con la porta”. Tipo: “essenziale”.
Descrizione: l'utente effettua operazioni di scrittura e lettura sulla porta
attualmente attiva.
Azione attore Risposta sistema1. Invia dati posti in un buffer,specificando la quantità da inviare, oppurerichiede una lettura di dati dal dispositivoindicando un buffer e la sua dimensione.
2. Tenta invio o ricezione dei dati coldispositivo (seriale, Ethernet o USB)
3. Se il log è attivo registra su log-file.
4.3.2 UML: Sequence diagram
“Comunica con la porta” è un'attività
che è stata semplificata nel diagramma
precedente e la si vede in dettaglio in
questo sequence diagram, che mostra
come avviene una fase di
comunicazione con la porta, tra utente e
framework (rappresentato dalla classe
“ComLayer”).
4.3.3 UML: Activity diagram
Il seguente diagramma d'attività è stato sviluppato per evidenziare l'ordine con
cui le azioni possono essere svolte dall'utente del framework.
35
4.3 Modalità di utilizzo del framework
All'inizio si deve necessariamente creare almeno
una nuova porta per poter eseguire altre operazioni,
dopodiché l'ordine degli eventi è molto flessibile: ad
esempio si può configurare una porta ma attivarne o
crearne un'altra prima di effettuare comunicazioni.
I rombi con più di una freccia in uscita indicano le
scelte (da parte dell'utente), mentre quelli con più di
una freccia in entrata indicano ricongiunzioni di
eventi.
36
5 Progettazione del framework C++
5 Progettazione del framework C++
La seconda fase prevista nel ciclo di vita di un software, dopo l'analisi, è la
progettazione, in cui si utilizzano i requisiti definiti nella fase precedente per
stabilire il modo in cui raggiungere lo scopo, a livello di struttura del software e di
caratteristiche comportamentali dei suoi componenti.
Un ruolo importante per la scelta della struttura del framework è stato svolto
dalla versione in linguaggio C precedentemente realizzata, che suddivideva
l'implementazione del codice in un file di interfacciamento “ComLayer.c”, un file
per ogni tipo di porta (più un altro file per l'interfaccia 0 USB) ed un file per il log
(senza contare gli header file).
Partendo da questa base si è cercato di ottenere un framework C++ rispondente
alle caratteristiche della programmazione orientata agli oggetti, per cui sono stati
utilizzati “Design Pattern” e diagrammi “UML” allo scopo di trasformare la
semplice struttura della versione C, senza complicarla più del necessario, per
arrivare ad un insieme di classi ben strutturato.
Come già detto, nella versione C non era fornita la possibilità di gestire più
connessioni contemporanee e si è scelto di inserirla nella versione C++ sfruttando
la struttura ad oggetti e i tipi di dato forniti dalla C++ Standard Template Library.
5.1 Scelte architetturali con l'uso di Design Patt ernIn fase di progettazione, alcuni aspetti del framework sono stati ricondotti a
forme tipiche definite in alcuni Design Pattern noti, aiutando così a definire in
modo semplice e convenzionale il modo in cui implementare le relazioni tra le
classi.
37
5.1 Scelte architetturali con l'uso di Design Pattern
5.1.1 Façade
Questo design pattern di tipo strutturale, il cui nome significa “facciata”,
definisce come creare un'interfaccia uniforme per un insieme di interfacce
correlate tra loro.
Ciò significa, nella programmazione orientata agli oggetti, che si dovrà creare
una classe che farà da interfaccia (layer) tra l'utente e le classi che ereditano da
una classe comune (presumibilmente astratta).
Nel caso del framework realizzato si aveva la necessità di fornire all'utente
un'interfaccia simile per comunicare su tipi di connessione differente, anche
fisicamente, per cui è stata creata la classe “ComLayer” che avrebbe dovuto fare
da interfaccia per l'utente con le classi “ComSerial”, “ComEth” e “ComUsb”, che
ereditano da “ComBase”.
Un accesso di tipo normale ai tre tipi di porta si potrebbe
schematizzare con il diagramma a destra: il client ha una
dipendenza da tutti e tre i tipi di porta differenti, e deve
comunicare direttamente con essi in modo differenziato.
Invece, con il pattern “façade” si aggiunge una
classe (in questo caso “Layer”) e si ottiene un
accesso semplificato da parte del client, che può
accedere alle risorse di Serial, Ethernet e Usb in
modo uniforme, dipendendo solo da Layer.
5.1.2 Lazy Initialization
Questo pattern comportamentale consiste nell'istanziare un oggetto di una classe
solo quando esso è effettivamente richiesto.
38
5 Progettazione del framework C++
In questo caso si applicata la “lazy initialization” all'implementazione del
sistema di log su file, la cui istanza viene creata solamente quando viene attivato il
log con l'apposito metodo “StartLog()”, e solo se era stato configurato durante la
creazione della porta.
Per l'implementazione è stata inserita, nella classe astratta “ComBase”, una
verifica sull'effettiva esistenza dell'istanza della classe “ComLog” ad ogni
chiamata di metodi di questa classe; se oggetto non esiste (il suo puntatore è
NULL), non viene eseguito nulla. É un'implementazione alternativa a quella
standard, che prevede di istanziare l'oggetto appena la classe che lo contiene ne ha
bisogno: in questo caso invece l'istanziazione avviene quando è richiesta la prima
attivazione del log da parte dell'utente.
5.2 Struttura del framework con uso di UMLIn questa fase i diagrammi UML sono utili per stabilire e documentare con
precisione quali classi comporranno il progetto, che metodi e che attributi esse
conterranno, quali relazioni le legheranno e in che modo interagiranno tra loro.
In questo caso è stato realizzato un “Class diagram”, che descrive le classi e le
relazioni tra esse, vale a dire ereditarietà (derivazione) e contenimento.
Inoltre sono stati realizzati numerosi “Communication diagram”, che permettono
di definire come le classi dovranno comunicare tra loro per svolgere le diverse
azioni richieste dall'utente.
39
5.2 Struttura del framework con uso di UML
5.2.1 UML: Class diagram
Il class diagram UML qui visualizzato è una rappresentazione di tutte le classi
che compongono il framework progettato; per migliorare la leggibilità, per ogni
classe sono mostrati solo alcuni dei metodi protected (nel caso della classe astratta
“ComBase”, che è l'unica da cui altre classi ereditano) e public, mentre sono stati
omessi gli attributi, che sono quasi tutti protected (“ComBase”) o private.
La classe “ComLayer” è quella che dev'essere istanziata dall'utente del
framework e contiene tutti i metodi public che servono per usare ogni funzionalità,
dalla gestione delle connessioni, alle impostazioni specifiche per le varie porte, per
arrivare alle funzionalità di invio e ricezione dei dati. L'elenco completo dei
metodi pubblici forniti da “ComLayer” è riportato nel capitolo 5.3: “Elenco dei
metodi forniti all'utente”. La classe “ComLayer” si comporta quindi anche da
layer (ovvero da traduttore) tra l'utente e le varie connessioni, ed è in grado di
40
5 Progettazione del framework C++
istanziare, memorizzando nell'attributo privato “portsMap”, un numero arbitrario
di oggetti derivati dalla classe astratta “ComBase”, che si dice quindi essere
contenuta nella classe “ComLayer” (ciò è indicato dal rombo vuoto con freccia
verso la classe contenuta). Dato che si utilizzano puntatori, si parla di
aggregazione.
Nel diagramma appaiono altre due relazioni di tipo contenitivo con
aggregazione (uso di puntatori), che però ammettono una sola istanza della classe
contenuta: queste classi sono la “ComLog” e la “ComUsbIF0”. La classe
“ComLog” è contenuta in “ComBase” e la sua istanza è protected, scelta obbligata
per renderla accessibile alla classe “ComUsb”, che deve renderla disponibile alla
classe contenuta “ComUsbIF0”. Quest'ultima aggregazione è stata realizzata per
mantenere separato il codice di gestione della “interfaccia 0 USB” dal resto del
codice per l'USB, in quanto sia l'utilizzo reale sia l'implementazione differiscono
in modo significativo. Nel caso di utilizzo della “interfaccia 0 USB”, quindi, la
classe “ComUsb” si occupa dell'istanziazione di “ComUsbIF0” e di fare da layer
tra quest'ultima e “ComLayer”.
La classe “ComBase” è astratta, motivo per cui il suo nome è scritto inclinato
nel grafico UML, poiché possiede dei metodi virtuali puri che devono essere
implementati da ogni classe derivata. La derivazione (ereditarietà), che indica una
specializzazione, è rappresentata con una freccia vuota che va dalle classi
specializzate alla classe base. Il suo significato è che una classe specializzata è un
sotto-tipo della classe da cui deriva. In questo framework ci sono tre classi che
ereditano da “ComBase”, e sono “ComSerial”, “ComEth” e “ComUsb”. È stata
utilizzata l'ereditarietà pubblica, poiché nella classe base sono presenti dei metodi
pubblici che devono restare tali anche nelle istanze delle classi derivate.
È da notare la presenza di alcuni distruttori nel diagramma, identificati con nomi
che iniziano con “~”, che indica che è stato scritto un distruttore personalizzato
41
5.2 Struttura del framework con uso di UML
per la classe; non sono stati inclusi i distruttori delle classi “ComSerial”,
“ComEth” e “ComUsbIF0” in quanto esse non istanziano altre classi né aree di
memoria di dimensione variabile, quindi non hanno nulla da de-istanziare quando
vengono distrutte. Un'importante considerazione è che il distruttore, sebbene
debba essere dichiarato virtual per una classe astratta, non viene sovrascritto dai
distruttori delle classi da essa derivate (in effetti hanno nome diverso), ma viene
eseguito dopo l'esecuzione del distruttore della classe derivata. Ad esempio in
questo framework, quando viene distrutta un'istanza della classe “ComUsb”, sarà
chiamato il suo distruttore e poi il distruttore della classe “ComBase”.
5.2.2 UML: Communication diagram
Questo è il diagramma che mostra le collaborazioni tra le classi interne al
framework quando l'utente richiede l'inizializzazione di una porta USB nella
modalità “interfaccia 0”, con il sistema di log attivo:
I numeri indicano l'ordine con cui avvengono le comunicazioni tra le classi,
mentre l'etichetta identifica il metodo della classe destinataria del messaggio,
utilizzato per lo scopo.
Diagrammi di questo tipo possono essere decine per un progetto di questo tipo,
per cui si è scelto di riportare solamente questo a scopo dimostrativo.
42
5 Progettazione del framework C++
5.3 Elenco dei metodi forniti all'utenteAl termine della fase di progettazione è già definito il dettaglio dei metodi che
comporranno le classi. Ora si vedrà l'elenco dei metodi a disposizione dell'utente
del framework, ovvero i metodi pubblici forniti dalla classe “ComLayer”, divisi
per tipologia di operazione.
5.3.1 Istanziazione del framework
Il costruttore non accetta parametri in quanto il framework viene istanziato
sempre allo stesso modo. Riguardo il costruttore di copia e l'operatore di
assegnamento bisogna sottolineare che essi non sono forniti all'utente, in quanto il
concetto di duplicazione applicato ad una connessione fisica con un dispositivo
non ha senso; comunque, anche nel caso in cui si fosse deciso di fornire questa
possibilità, dato che il codice scritto fa uso di occupazione dinamica della
memoria, sarebbe stato necessario implementare la copia manuale delle aree di
memoria, cosa non semplice e rischiosa a livello di sicurezza del software.
Ecco i semplici prototipi del costruttore e del distruttore:
ComLayer();
~ComLayer();
5.3.2 Gestione di connessioni multiple contemporanee
Riguardo la creazione, l'eliminazione e la selezione delle connessioni, i metodi
messi a disposizione dell'utente sono:
int CreateCommunicationPort(int iPortType, const char* strLogFile,
int iLogVerbosity, bool bLogPrintDate,
unsigned int& iId);
int CreateCommunicationPort(int iPortType, const char* strLogFile,
int iLogVerbosity, bool bLogPrintDate);
43
5.3 Elenco dei metodi forniti all'utente
int CreateCommunicationPort(int iPortType, unsigned int& iId);
int CreateCommunicationPort(int iPortType);
void GetCommunicationPorts(CommunicationPortsVector&);
int SelectCommunicationPort(unsigned int);
unsigned int GetCurrentCommunicationPortId();
int DeleteCommunicationPort(unsigned int);
Si noti che ci sono quattro metodi che hanno lo stesso nome ma numero di
parametri diverso, sfruttando l'overloading permesso dal C++. In linguaggio C
sarebbe stato necessario utilizzare quattro nomi differenti.
5.3.3 Inizializzazione di comunicazioni seriali
Metodi specifici per configurare le porte create con tipo seriale; se la porta
attualmente attiva non è di tipo seriale, viene restituito errore:
int InitSerial(const char* strPortName, int iBaudRate,
int iDataLength, int iParity, int iStop,
int iFlowControl);
int InitSerialStrings(const char* strPortName,
const char* strBaudRate,
const char* strDataLength,
const char* strParity, const char* strStop,
const char* strFlowControl);
int InitSerialAutoBaudRate();
size_t GetMaxSerialPacketSize();
void SetMaxSerialPacketSize(size_t iSz);
int GetSerialParameters(char* strPortName, int* iBaudRate,
int* iDataLength, int* iParity,
44
5 Progettazione del framework C++
int* iStop, int* iFlowControl);
int GetSerialParametersStrings(char* strPortName, char* strBaudRate,
char* strDataLength, char* strParity,
char* strStop, char* strFlowControl);
int GetSerialSignals(int* iPtrStatus);
void SetSerialDTR(bool bFlag);
void SetSerialRTS(bool bFlag);
5.3.4 Inizializzazione di comunicazioni Ethernet
Metodi specifici per configurare le porte create con tipo Ethernet; nel caso in cui
la porta attualmente attiva non sia Ethernet, viene restituito errore:
int InitEthTCP(const char* strIPAddress, int iIPPort);
int InitEthTCPStrings(const char* strIPAddress,
const char* strIPPort);
int InitEthUDP(const char* strIPAddress, unsigned int iPortSend,
unsigned int iPortRecv);
int InitEthUDPStrings(const char* strIPAddress,
const char* strPortSend,
const char* strPortRecv);
int GetEthTCPParameters(char* strAddress, unsigned int* iPtrPort);
int GetEthTCPParametersStrings(char* strAddress, char* strPort);
int GetEthUDPParameters(char* strAddress,
unsigned int* iPtrPortSend,
unsigned int* iPtrPortRecv);
int GetEthUDPParametersStrings(char* strAddress,
char* strPortSend,
char* strPortRecv);
45
5.3 Elenco dei metodi forniti all'utente
size_t GetMaxEthPacketSize();
void SetMaxEthPacketSize(size_t iSz);
5.3.5 Inizializzazione di comunicazioni USB
Metodi specifici per configurare le porte create con tipo USB; nel caso in cui la
porta attualmente attiva non sia USB, viene restituito errore:
int InitUsb(const char* strUsb); // interface 1
int InitUsbIf2(int iVID, int iPID, const char* strSN);
int InitUsbIf0(int iVID, int iPID, const char* strSN);
int GetUsbParameters(char* strName); // interface 1
int GetUsbIf2Parameters(int* iPtrVID, int* iPtrPID, char* strSN);
int GetUsbIf0Parameters(int* iPtrVID, int* iPtrPID, char* strSN);
size_t GetMaxUsbPacketSize(); // interface 1
void SetMaxUsbPacketSize(size_t iSz); // interface 1
size_t GetMaxUsbIf2ReadPacketSize();
size_t GetMaxUsbIf2WritePacketSize();
I metodi per l'interfaccia 1 (quella “default”, tramite device di sistema) non
portano nel nome un identificatore, per cui è stato aggiunto un commento a fianco.
5.3.6 La comunicazione vera e propria coi dispositivi
Questi metodi sono validi qualunque sia il tipo di porta attiva, ed il
comportamento effettivo ottenuto sul dispositivo con cui si comunica è
equivalente, ma la loro implementazione può ovviamente essere diversa per ogni
tipo di porta. Ecco l'elenco:
46
5 Progettazione del framework C++
int Open();
int Close();
int Write(const unsigned char* strBuffer, size_t iMax,
size_t* iPtrWritten);
int Write(const unsigned char* strBuffer, const size_t iMax);
int WriteFile(const char* strFileName);
int Read(unsigned char* strBuffer, const size_t iMax,
size_t* iPtrRead);
int StartLog();
int StopLog();
int ChangeVerbosity(int iVerbosity);
int GetVerbosity();
int GetCommunicationPortType();
int GetConnectionStatus();
int GetReadTimeout(struct timeval* ptrTimeout);
int SetReadTimeout(const struct timeval* ptrTimeout);
int Clean();
“WriteFile()” merita un piccolo appunto: si tratta di un metodo in grado di
inviare un interno file (di comandi o un'immagine da stampare) al dispositivo,
implementato nella classe “ComBase()” e che fa uso di numerose chiamate a
“Write()” per completare l'operazione.
47
5.3 Elenco dei metodi forniti all'utente
48
6 Implementazione del framework C++
6 Implementazione del framework C++
Dopo analisi e progettazione il ciclo di vita di un software prevede la fase di
implementazione, ovvero la realizzazione fisica del progetto nel linguaggio di
programmazione scelto, in questo caso il C++.
6.1 I sorgenti realizzati
6.1.1 Elenco dei sorgenti che compongono il framework
Per l'implementazione del framework C++ con le sue 7 classi definite in fase di
progettazione, sono stati creati i seguenti file:
• 9 header file: uno per la dichiarazione di ognuna delle 7 classi (con nomi
del tipo <classe>.h), uno per le defines globali (ComDefines.h) ed uno
per i codici di errore (ComErrors.h);
• 6 file sorgente C++: uno per l'implementazione di ognuna delle classi non
astratte (con nomi del tipo <classe>.cpp).
La lunghezza totale del codice che compone il framework è di poco superiore
alle 16mila righe, includendo la documentazione delle classi e dei metodi,
presente in tutti gli header file corrispondenti all'interfaccia di una classe.
6.1.2 Alcuni dettagli sull'implementazione del codice
Riguardo l'utilizzo delle chiamate di sistema a basso livello, sono stati riutilizzati
gli ormai assodati algoritmi prodotti in precedenza per la “libreria C”. Ecco una
interessante porzione di codice dell'algoritmo che si occupa, per le connessioni
Ethernet TCP, di ricevere i pacchetti di dati fino allo riempimento del buffer
fornito al metodo di lettura, o fino alla fine dei dati disponibili da leggere, il tutto
49
6.1 I sorgenti realizzati
senza superare la dimensione massima dei pacchetti specificata né il timeout
impostato:
iRd_ = recv(m_iSocket, (strBuffer+*iPtrRead),
( (iMax-*iPtrRead) < m_iMaxEthPacketSize) ?
(iMax-*iPtrRead) : m_iMaxEthPacketSize, 0);
Un ciclo si occupa di verificare se viene letta una quantità di dati pari alla
dimensione massima dei pacchetti, ma con il buffer fornito non ancora pieno, e in
quel caso causa la ripetizione del pezzo di codice per tentare un'ulteriore lettura,
che sarà accodata a quelle già effettuate. Ci sono svariati algoritmi simili a questo
per i vari tipi di connessione, sia in lettura sia in scrittura.
In tutto il progetto, per le parti di codice prelevate dalla versione “C” della
libreria, sono stati effettuati alcuni adattamenti riguardanti i casting dei tipi di dato
per adeguarsi agli standard del C++.
Ora si vedranno alcune interessanti porzioni di codice relative
all'implementazione “orientata agli oggetti” del framework.
Tutte le funzionalità aggiunte nella versione C++, come la possibilità di gestire
connessioni multiple internamente al framework, sono state create con massiccio
utilizzo di funzionalità specifiche di questo linguaggio ad oggetti, come i tipi
“templatici” della Standard Template Library (STL), le eccezioni e naturalmente le
tecniche per gestire le relazioni tra classi.
Ecco alcune delle linee di codice di “ComLayer.h”, che si occupano di dichiarare
e definire le strutture dati necessarie a gestire più connessioni contemporanee:
private:
typedef std::map<unsigned int, ComBase*> CommunicationPortsMap;
CommunicationPortsMap portsMap;
Viene creato un tipo di dato “CommunicationPortsMap” che è una mappa STL,
ovvero un insieme di coppie ordinate <indice, oggetto>: gli elementi di tipo
50
6 Implementazione del framework C++
“oggetto” in questo caso sono puntatori alla classe astratta “ComBase”;
ovviamente solo le classi non astratte derivate da essa possono essere istanziate:
questo significa che a tempo di esecuzione la variabile “portsMap” conterrà
puntatori ad istanze (ovvero a connessioni) delle classi “ComSerial”, “ComEth” e
“ComUsb”, che sono trattabili dinamicamente con puntatori a “ComBase”. Questa
utile caratteristica è ottenuta grazie alle tecniche di polimorfismo dinamico
disponibili in C++ ed ha la conseguenza di permettere la chiamata di metodi
diversi nelle classi derivate da “ComBase”, senza alcuna distinzione nel codice
chiamante. Questi metodi hanno un'implementazione completamente differente
nelle tre classi derivate: quello che è comune è solo il prototipo, ovvero la
dichiarazione che si trova nella classe “ComBase”. Ad esempio se l'utente richiede
una lettura di dati sulla porta attualmente attiva, invocando il metodo “Read()”
fornito dalla classe “ComLayer”, quest'ultima, senza controllare se si tratta di
porta seriale, Ethernet o USB, esegue queste semplici istruzioni:
if (ptrActive)
return ptrActive->Read(strBuffer, iMax, iPtrRead);
dove ptrActive è una copia, mantenuta per comodità e velocità di esecuzione,
del puntatore all'istanza attualmente attiva. E' concettualmente equivalente a:
portsMap.find(iActiveId)->second
dove iActiveId è l'indice, nella mappa STL, della porta attualmente attiva.
Ecco invece come è stata implementata la chiamata ai metodi specifici per ogni
tipo di porta (in questo esempio è mostrato il caso di uno dei metodi specifici per
le porte seriali):
int ComLayer::InitSerialAutoBaudRate()
{
ComSerial* ptrSerial;
if ( (ptrSerial = dynamic_cast<ComSerial*>(ptrActive)) )
return ptrSerial->InitSerialAutoBaudRate();
51
6.1 I sorgenti realizzati
else
return ERR_NOT_SERIAL_PORT;
}
Nell'istruzione di “if” è presente un assegnamento al puntatore ptrSerial; se il
“dynamic_cast” fallisce, l'assegnamento restituisce NULL, per cui non viene
eseguita l'istruzione richiesta. Questo succede nel caso in cui la porta attiva al
momento (ptrActive, che è un puntatore a “ComBase”) non sia di tipo seriale.
A seguire, ecco l'implementazione del metodo per la richiesta dell'elenco delle
porte esistenti con alcuni dettagli per ogni porta, in “ComLayer.cpp”:
void ComLayer::GetCommunicationPorts(CommunicationPortsVector&
vectorPorts)
{
struct CommunicationPort CommPort;
CommunicationPortsMap::const_iterator i = portsMap.begin();
CommunicationPortsMap::const_iterator i_end = portsMap.end();
vectorPorts.clear();
while (i != i_end)
{
CommPort.iId = i->first; // first is the Id,second is ComBase*
CommPort.iType = (i->second)->GetCommunicationPortType();
CommPort.iStatus = (i->second)->GetConnectionStatus();
vectorPorts.insert(vectorPorts.end(), CommPort);
++i;
}
}
Per fornire il risultato all'utente è stato definito un nuovo tipo di dato, chiamato
CommunicationPortsVector (l'utente deve passare alla funzione il riferimento
ad una variabile di questo tipo); si tratta di un vector della STL i cui elementi sono
strutture dati appositamente create. Eccone le definizioni, prese dal file
“ComLayer.h”:
public:
52
6 Implementazione del framework C++
struct CommunicationPort
{
unsigned int iId; // same IDs as in internal map
int iType;
int iStatus;
};
typedef std::vector<struct CommunicationPort>
CommunicationPortsVector;
L'utente potrà poi analizzare il vector per scegliere la porta desiderata in base al
tipo ed allo stato attuale (non entreremo nei dettagli dei possibili valori che
possono avere questi attributi, basti sapere che sono delle defines create nel file
“ComDefines.h” e ben descritte nella documentazione per l'utente).
Vediamo ora il codice del metodo di “ComLayer” che permette all'utente di
cambiare la porta attualmente attiva:
int ComLayer::SelectCommunicationPort(unsigned int iId)
{
CommunicationPortsMap::iterator i = portsMap.find(iId); // search
if (i != portsMap.end()) // if the chosen Id was found
{
iActiveId = i->first;
ptrActive = i->second;
return SUCCESS;
}
else // a not existing Id was passed:
return ERR_INVALID_ID;
}
L'utente deve indicare l'identificatore della porta che vuole attivare; questo viene
cercato ed inserito in un iterator che è un costrutto fornito dalla STL e che
permette di lavorare con gli indici dei tipi vettoriali; i->firts e i->second
identificano rispettivamente l'indice e il puntatore alla connessione della porta ora
selezionata. Come già detto, non era strettamente necessario memorizzare anche il
53
6.1 I sorgenti realizzati
puntatore all'oggetto attivo, ma era sufficiente il suo indice; tuttavia si è scelto di
conservarlo per comodità nell'implementazione del codice che lo utilizza e per una
maggiore velocità di esecuzione specie riguardo le operazioni di lettura e scrittura,
che possono essere molto numerose.
Ecco le definizioni del costruttore e del distruttore della classe “ComLayer”:
// constructor:
ComLayer::ComLayer()
{
iActiveId = 0;
ptrActive = NULL;
}
// destructor:
ComLayer::~ComLayer()
{
while (!portsMap.empty()) // while there are elements
{
if ((portsMap.begin()->second) != NULL)// (safety reasons)
{
delete (portsMap.begin()->second); // call port destructor
}
portsMap.erase(portsMap.begin());
}
}
Il distruttore si occupa di distruggere iterativamente tutte le istanze di porte
esistenti, chiamando (tramite la parola chiave delete) i relativi distruttori,
dopodiché elimina la coppia dalla mappa portsMap. Questo è necessario per
evitare i memory leak ovvero le perdite del riferimento a certe aree di memoria,
che resterebbero occupate durante l'esecuzione del programma, ma non
utilizzabili.
Ecco come sono stati disabilitati il costruttore di copia e l'operatore di
assegnamento per la classe “ComLayer” (operazione in realtà effettuata in tutte le
54
6 Implementazione del framework C++
classi del framework):
private:
ComLayer(const ComLayer&);
ComLayer& operator=(const ComLayer&);
E' la tecnica normalmente utilizzata per questo scopo, che dichiara questi metodi
come privati, impedendone quindi l'accesso al software-utente.
Le funzionalità per il sistema di log su file sono state implementate dalla classe
“ComLog”, che viene istanziata, tramite puntatore (aggregazione), da oggetti delle
classi derivate da “ComBase” solo quando necessario, secondo quanto specificato
con il design pattern “Lazy initialization”. Per la scrittura fisica sul file di log si è
scelto di implementare un metodo comune a tutte le classi derivate e definito nella
classe astratta “ComBase”, che chiama i metodi della classe “ComLog” solo se il
log è attivo per l'istanza della porta che è in uso. Ecco il metodo in questione,
preso dal file “ComBase.h”:
inline int PrintLog(char* strClass, char* strMethod,
char* strAction, int iVerb)
{
if (m_Log != NULL)
return m_Log->PrintLog(strClass, strMethod, strAction, iVerb);
else
return SUCCESS; // ok also if called with log not active
}
La parola inline indica espansione in linea (cioè sostituzione del codice del
metodo al posto di ogni sua chiamata) a tempo di compilazione, cosa che porta ad
una maggiore velocità computazionale a tempo di esecuzione, importante dato che
si tratta di un metodo chiamato di frequente.
55
6.2 La gestione del progetto
6.2 La gestione del progetto
6.2.1 Le librerie a collegamento dinamico
La forma finale del framework compilato e pronto all'uso è quella di un file di
libreria a collegamento dinamico per ambiente Linux; queste librerie sono
contraddistinte dal suffisso “.so” e non sono eseguibili di per sé, ma ad esse si
collegano, a tempo di esecuzione, altri programmi (o anche altre librerie)
opportunamente compilati. Ciò porta a dei vantaggi rispetto alle librerie a
collegamento statico:
• possibilità di condividere una libreria tra più programmi che la usano,
caricandola in memoria una sola volta;
• possibilità di aggiornare la libreria senza dover ricompilare o aggiornare
tutti i programmi e le librerie che la usano.
6.2.2 Il comando “make” per la compilazione
Per compilare il codice sorgente si utilizza il comando “make” con le relative
direttive contenute nel makefile. Il programma “make” è uno strumento per il
controllo del processo di costruzione (o ricostruzione) del software: ad esempio
permette di minimizzare i tempi di rielaborazione attraverso una mirata gestione
dei file da coinvolgere nel processo di ricompilazione.
Un makefile è un file di testo, chiamato di norma “Makefile”, contenente le
regole che indicano a “make” cosa produrre e in che modo. Ogni singola regola è
composta da:
• Un target (destinazione), cioè l'elemento che “make” cerca di creare;
generalmente è un file;
• Una lista di dependency (dipendenze), solitamente rappresentate da file,
56
6 Implementazione del framework C++
necessarie per la ricostruzione;
• Una lista di command (comandi) la cui esecuzione è vincolata alle regole
di dipendenza prima citate.
Le regole di scrittura di un makefile sono così formate:
target: dependency dependency [...]
command
command
[…]
Le regole devono essere separate tra loro da una riga vuota.
In testa ai makefile possono essere dichiarate delle variabili che permettono una
costruzione modulare delle regole, ad esempio per permettere in modo semplice di
modificare la directory di destinazione dei file prodotti dalla compilazione.
Il makefile per l'applicazione prodotta è stato scritto completamente in modo
manuale, per avere il massimo controllo sulle modalità di compilazione e sul
linking, e per fornire una serie di opzioni personalizzate per il “make”, che
permettono al programmatore di compilare anche le applicazioni di esempio e la
documentazione con un solo comando. Sono state anche definite delle regole per
effettuare la pulizia facoltativa dei file non necessari, come i file di backup creati
in automatico o i file oggetto.
La compilazione della versione definitiva prodotta, che crea un file di libreria a
collegamento dinamico “libComLayer.so”, non causa l'emissione di warning in
quanto è stata prestata molta attenzione nella scrittura del codice, per avere un
software aderente agli standard e il più possibile privo di malfunzionamenti. Ecco
alcune righe estratte dal makefile creato per la compilazione del framework, che
avviene con GCC/G++ . Definizione di due variabili (le \ a fine riga indicano un
“a-capo” per questioni di spazio, che non è presente nel makefile originale):
57
6.2 La gestione del progetto
COMPILER = g++ -O2 -W -Wall -pedantic
LINKING = -L$(OUT)/ -Wl,-rpath -Wl,../$(OUT)/ -lComLayer \
-L$(LIBUSB_INSTALL_DIR) -Wl,-rpath \
-Wl,$(LIBUSB_INSTALL_DIR) -lusb-1.0
Compilazione di un file oggetto e del file di libreria dinamica:
$(OUT)/ComLayer.o: $(SRC)/ComDefines.h $(SRC)/ComErrors.h \
$(SRC)/ComLayer.cpp $(SRC)/ComLayer.h \
$(OUT)/ComSerial.o $(OUT)/ComEth.o $
(OUT)/ComUsb.o
$(COMPILER) -fPIC -c $(SRC)/ComLayer.cpp -o $(OUT)/ComLayer.o
$(OUT)/libComLayer.so: $(O_FILES)
$(COMPILER) -shared -Wl,-soname,libComLayer.so -o \
$(OUT)/libComLayer.so $(O_FILES) -lc
Compilazione di un programma di test, con opzioni per il linking dinamico al
framework, il quale è sotto forma del file “libComLayer.so”:
$(EXAMPLE)/test: $(EXAMPLE)/test.cpp $(OUT)/libComLayer.so
$(COMPILER) $(EXAMPLE)/test.cpp -o $(EXAMPLE)/test $(LINKING)
Compilazione del programma di test in ambiente grafico:
$(EXAMPLE)/graphic: $(EXAMPLE)/graphic.cpp $(EXAMPLE)/graphic.h \
$(EXAMPLE)/Makefile MakeGraphic.sh libComLayer
sh MakeGraphic.sh
da notare che viene chiamato uno script per la shell di Linux, che richiama la
compilazione dell'applicazione grafica, che avviene con un altro “Makefile” che è
stato generato dall'utility “Qmake”. Per “Qmake” è usato un file di progetto scritto
manualmente chiamato “graphic.pro”, il cui contenuto è il seguente:
TEMPLATE = app
TARGET = graphic
CONFIG += qt warn_on release
58
6 Implementazione del framework C++
LIBS += -L../out/ -Wl,-rpath -Wl,../out/ -lComLayer
LIBS += -L/usr/local/lib/ -Wl,-rpath -Wl,/usr/local/lib/ -lusb-1.0
HEADERS = graphic.h
SOURCES = graphic.cpp
6.2.3 Il debugging
Il debugging è stato effettuato principalmente con la funzionalità di “logging”
(registrazione) inclusa nel framework: la classe “ComLog” infatti si occupa della
scrittura su di un file a scelta (il che naturalmente include anche la console di
sistema) delle operazioni che sono svolte dal software. Sono disponibili sei livelli
di “verbosity”, ovvero di “quanto dovrà scrivere” il sistema di “logging”, che
vanno da NO_VERBOSITY=0 a MAX_VERBOSITY=5. La cosa interessante e
molto utile (almeno nel caso di questo software) di questo sistema di debugging è
la possibilità di registrare anche l'orario in cui avviene un determinato evento, con
una precisione al millesimo di secondo, e volendo anche superiore. Ecco un
esempio del log che si ottiene con “verbosity” al massimo livello utilizzando la
ComLayer C++ per inizializzare e tentare di aprire un collegamento USB (senza
successo):
2010-06-13 11:02:06.686 ComUsb::GetConnectionStatus - return: PORT_NOT_INITIALIZED
2010-06-13 11:02:19.508 ComUsb::InitUsb - Start, parameter: /dev/usb/lp0
2010-06-13 11:02:19.508 ComUsb::InitUsb - Begin device name checking
2010-06-13 11:02:19.508 ComUsb::InitUsb - End, returning: SUCCESS
2010-06-13 11:02:19.508 ComUsb::GetConnectionStatus - return: PORT_INITIALIZED
2010-06-13 11:02:19.509 ComUsb::GetUsbParameters - Function start
2010-06-13 11:02:19.509 ComUsb::GetUsbParameters - End, returning: SUCCESS
2010-06-13 11:02:19.509 ComUsb::GetConnectionStatus - return: PORT_INITIALIZED
2010-06-13 11:02:20.925 ComUsb::Open - Function start
2010-06-13 11:02:20.925 ComUsb::Open - Try to open port (interface 1)
2010-06-13 11:02:20.925 ComUsb::Open - Error, returning: ERR_OPENING_USB
2010-06-13 11:02:20.925 ComUsb::GetConnectionStatus - return: PORT_INITIALIZED
2010-06-13 11:02:20.925 ComUsb::GetUsbParameters - Function start
2010-06-13 11:02:20.925 ComUsb::GetUsbParameters - End, returning: SUCCESS
2010-06-13 11:02:20.926 ComUsb::GetConnectionStatus - return: PORT_INITIALIZED
Ad esempio la seguente riga del log:
59
6.2 La gestione del progetto
2010-06-13 11:02:19.508 ComUsb::InitUsb - Start, parameter: /dev/usb/lp0
è stata generata con questo pezzo di codice:
char strLog[MAX_USB_LOG_LENGTH+1];
strLog[MAX_USB_LOG_LENGTH]=0; // safety reasons
snprintf(strLog, MAX_USB_LOG_LENGTH, "Start, parameter: %s",
strUsb);
PrintLog("ComUsb", "InitUsb", strLog, 3);
dove “PrintLog()”, metodo interno alla classe “ComBase” e non ridefinito nelle
classi derivate come la “ComUsb”, prende come parametri tre stringhe di tipo C
ed un intero che indica il livello minimo a cui la “verbosity” dev'essere impostata
perché la riga sia effettivamente registrata, e poi invoca un metodo dell'istanza
della classe “ComLog”, che si occupa della scrittura su file vera e propria.
Durante la fase di debugging è stato alzato il livello massimo di “verbosity” a 6,
in modo da poter temporaneamente aggiungere delle chiamate speciali a
“PrintLog()” per verificare certi dettagli in caso di problemi, o per sicurezza.
6.3 La documentazioneLa documentazione ha rivestito naturalmente un ruolo importante nella
creazione del framework anche perché sarà utilizzato sia da clienti dell'azienda
che dovranno affidarsi alle informazioni fornite per installare la libreria e
interagire con essa mediante altri programmi, sia dalla stessa Custom Engineering
per eventuali estensioni o aggiornamenti futuri.
Tutta la documentazione è quindi stata scritta in lingua inglese ed è disponibile
ai clienti in formato di pagine web HTML, generate con Doxygen, di cui
parleremo tra poco. La parte HTML riguarda solamente la classe “ComLayer” che
è quella che dev'essere istanziata dall'utilizzatore del framework, mentre
naturalmente tutte le altre classi sono state corredate di commenti standard,
fondamentali in caso di future estensioni o manutenzione.
60
6 Implementazione del framework C++
Anche i commenti all'interno del codice sono in lingua inglese, seguendo così un
precetto aziendale, fondamentale quando ci sono molti dipendenti anche all'estero
(nonostante, per il momento, la ricerca e lo sviluppo si effettuino interamente in
Italia, ovvero nella sede di Fontevivo - PR).
6.3.1 Istruzioni per la compilazione
Le istruzioni per la compilazione del framework, sempre in lingua inglese, sono
state incluse in un file di testo semplice, chiamato “README”. Eccone una breve
sintesi: in sostanza è sufficiente lanciare il comando “make” nella directory
principale del progetto per ottenere nella sotto-directory “out/” il file di libreria
“libComLayer.so”. Vengono anche creati i file oggetto intermedi, che è possibile
eliminare lanciando il comando “make clean”. Nella sotto-directory “example/” si
troveranno dei programmi di esempio in ambiente testuale. Se si vuole compilare
anche l'applicazione grafica di test inclusa (operazione che richiede la presenza del
framework grafico “Qt”) è sufficiente il comando “make all” o “make graphic”.
Per la versione già compilata da fornire ai clienti, le istruzioni per il
collegamento dinamico al framework sono presenti nella home-page HTML della
documentazione generata con Doxygen a partire dai file sorgenti. Qui è anche
specificato che il framework dipende dalla libreria “libusb-1.0” e sono presenti
informazioni su come recuperarla e come installarla nel sistema.
6.3.2 Lo strumento “Doxygen” per la documentazione
Doxygen (www.doxygen.org) è un potente strumento freeware (licenza GNU
GPL) per generare la documentazione in diversi formati e con un layout grafico
gradevole, a partire dai file sorgenti opportunamente commentati. La
documentazione può essere generata in formato HTML, LateX, RTF, PostScript o
anche per pagine di manuale Linux (man).
61
6.3 La documentazione
Per il framework realizzato si è scelto di generare la documentazione soltanto in
formato ipertestuale HTML, che grazie ai collegamenti inseriti e al gradevole
aspetto grafico (di cui si occupa interamente Doxygen) risulta di facile utilizzo per
l'utente. Ciò non toglie che sia possibile generare la documentazione in altri
formati: è sufficiente fare poche modifiche al Doxyfile, che è un po' l'equivalente
per Doxygen del Makefile per “make”. Il Doxyfile iniziale può essere generato in
automatico, ed è piuttosto complesso, ma è standard per tutte le applicazioni; è
sufficiente lanciare il comando “doxygen -g”: successivamente possono essere
fatte modifiche manuali per cambiare il comportamento di Doxygen, come la
scelta dei formati in cui generare la documentazione.
L'invocazione di Doxygen avviene in automatico da parte di “make”, in quanto è
stato specificato così nel Makefile di questo progetto. Se Doxygen non è presente
nel sistema semplicemente sarà saltata la generazione della documentazione.
La documentazione formato HTML del framework “libComLayer” è stata
pensata per l'uso da parte degli utenti, quindi è stata generata solamente per la
classe “ComLayer”, a partire dal file “ComLayer.h”. Ora si vedranno alcune
dettagli dei commenti in formato supportato da Doxygen presenti in questo file (in
totale occupano più di 1000 righe).
Prima parte della creazione della home-page della documentazione:
/**
\mainpage Custom Engineering SPA - ComLayer C++ library
Linux C++ library for communication with Serial, Ethernet, USB printers
Copyright Custom Engineering SPA - (c) 2009
\section intro Introduction
This library has the utility to allow communication with Custom products
on different communication ports, in the most similar possible way.
[ … ]
*/
Ecco la parte che crea le informazioni di base riguardanti il file “ComLayer.h”:
62
6 Implementazione del framework C++
/** \file ComLayer.h
\brief This file contains declaration of the class (ComLayer) that must
\brief be instantiated to manage the whole library
*/
Si vedrà ora come avviene la creazione della documentazione riguardante la
classe “ComLayer” in generale; è mostrato anche l'inizio della dichiarazione della
classe, che segue il commento per Doxygen:
/** \class ComLayer
This class, defined in \ref ComLayer.h manages all the library, creating
and handling instances of communication ports of these three types
[ … ]
\brief This is the class to instantiate to manage the whole library
*/
class ComLayer {
[ … ]
Di seguito, la creazione di un gruppo per la documentazione, che corrisponderà
ad una pagina del manuale e conterrà tutti i metodi che saranno assegnati (con la
clausola \ingroup) a quel gruppo:
/** @defgroup commons Input/Output common functionalities: open/close, \
communication and port control
These methods have almost the same behaviour with all the
[ … ]
*/
Infine, un estratto dei commenti del metodo “Read()” della classe “ComLayer”,
che è stato assegnato al gruppo “commons”:
/** \ingroup commons
Reads data from device (serial, eth, usb - non blocking, timeout):
[ … ]
\param strBuffer buffer where to store read data (allocated by user)
[ … ]
\returns
- SUCCESS if buffer is filled or if reading queue is empty
- ERR_SERIAL_NOT_OPEN if serial port is not currently open
[ … ]
*/
63
6.3 La documentazione
6.3.3 La documentazione risultante in formato HTML
Ecco lo screenshot della prima parte di una pagina HTML della documentazione
del framework; si tratta della sezione che si occupa dei metodi del gruppo
“commons”, cioè quelli comuni tra tutti i tipi di connessione supportati:
L'elenco dei metodi (chiamati impropriamente “Function” da Doxygen) è
nell'ordine in cui si trovano nel file sorgente, mentre le relative spiegazioni
dettagliate sono in ordine alfabetico, ed accessibili direttamente cliccando sui
nomi nell'elenco in alto alla pagina. Nello screenshot, per ovvie questioni di
spazio, è visibile la descrizione di un solo metodo di questa sezione.
64
7 Testing e applicazioni di esempio
7 Testing e applicazioni di esempio
7.1 Il testing durante l'implementazioneLe tecniche di ingegneria del software sono utili anche per limitare al massimo
gli errori di programmazione, fornendo una serie di regole per la corretta scrittura
del codice. Tuttavia non c'è modo di avere la certezza che un software sia scritto
correttamente a livello algoritmico, ma si può cercare di avvicinarsi il più
possibile alla perfezione: per raggiungere questo scopo è indispensabile effettuare
continue verifiche sul codice prodotto, provando ad utilizzarlo in casi particolari
ed estremi, alla ricerca di comportamenti anomali.
Per questo, sia durante lo sviluppo, sia al termine del periodo di tirocinio, il
software è stato verificato con lunghe fasi di prova e nella versione finale non
sono stati trovati difetti anche dopo ore di testing, anche da parte dell'apposito
reparto in azienda.
7.2 Applicazioni di esempioPer i test di collaudo, ma in parte anche per utilizzo da parte di clienti della
Custom, sono stati scritti alcuni programmi di esempio che utilizzano il
framework. Alcuni sono in ambiente testuale, altri in ambiente grafico.
Le funzionalità presenti permettono di cominciare ad utilizzare i prodotti della
Custom Engineering semplicemente eseguendo uno di questi programmi. Le
applicazioni testuali che vengono fornite ai clienti sono due, una delle quali
permette di provare tutte le funzionalità, mentre l'altra effettua dei test avanzati
pre-impostati sulla comunicazione USB.
L'applicazione più interessante e di facile utilizzo è sicuramente quella con
65
7.2 Applicazioni di esempio
interfaccia grafica, che rende molto facile la gestione di più connessioni
contemporanee, il passaggio da una all'altra, la modifica, chiusura e apertura di
una porta, la lettura e scrittura di dati liberi o l'invio di comandi preimpostati nelle
stampanti.
Ecco alcuni screenshot dell'applicazione in questione:
66
7 Testing e applicazioni di esempio
L'applicazione grafica di esempio sviluppata si appoggia al framework grafico
C++ “Qt”, che permette lo sviluppo di applicazioni grafiche su molteplici
piattaforme, tra cui naturalmente “X11”, che è l'ambiente grafico normalmente
utilizzato nei sistemi Linux.
“Qt” è di proprietà di Nokia Corporation, ed è rilasciato sotto la licenza GNU
Lesser General Public License 2.1, che permette lo sviluppo di software
commerciale facente uso della libreria/framework, purché non ci siano modifiche
al codice di “Qt”.
67
I pop-up mostrati quando si preme Initialize, rispettivamente per le porte seriale, Ethernet, USB
7.2 Applicazioni di esempio
68
8 Conclusioni
8 Conclusioni
Lo sviluppo di un software commerciale, di una certa complessità, per
un'azienda, si è rivelato un lavoro molto interessante nonché impegnativo, ma per
questo in grado di dare molte soddisfazioni. Le conoscenze acquisite all'università
nei corsi di programmazione, di ingegneria del software e di sistemi operativi si
sono rivelate fondamentali per riuscire a completare il lavoro in modo
indipendente ed adattandosi alle richieste aziendali in termini di qualità e
documentazione del software.
Si può dire che questa esperienza è stata positiva sia per l'azienda, che ha
arricchito il supporto che è in grado di fornire ai clienti per il sistema operativo
Linux, sia per lo studente, per l'aver messo in pratica le conoscenze acquisite con
lo studio, e per l'approfondimento che è stato necessario su alcune tematiche come
la programmazione per la comunicazione con le periferiche collegate al sistema e
alcune tecniche di ingegneria del software.
Un framework software nasce come un pezzo di codice che può essere esteso ma
non modificato dall'utente, e così sarà per i clienti dell'azienda che ne
richiederanno l'utilizzo come base per scrivere software di alto livello per la
comunicazione con i dispositivi prodotti dalla Custom Engineering. Sotto questo
punto di vista quindi i possibili utilizzi sono molteplici e vanno dal controllo di
scanner collegati a personal computer, all'utilizzo in sistemi embedded (con
sistema operativo Linux) per gestire le stampanti su carta termica.
Il framework sarà probabilmente utilizzato anche all'interno dell'azienda per
fornire ai clienti applicativi Linux di alto livello, anche personalizzati, pronti per
l'uso con i prodotti Custom.
69
8 Conclusioni
La prima possibile estensione del framework potrebbe riguardare il supporto alle
comunicazioni senza fili Bluetooth, tecnologia che si sta diffondendo tra molti dei
prodotti della Custom Engineering, soprattutto quelli portatili.
Un altro possibile sviluppo è quello di rendere il framework C++ indipendente
dalla libreria “libusb-1.0”, andando a scrivere direttamente le complesse parti di
codice che permettono la comunicazione di basso livello via USB tramite la
cosiddetta “interfaccia 2”. Tuttavia per il momento si è scelto di non effettuare
questo gravoso compito dando la priorità ad altre funzionalità, anche perché la
“libusb-1.0” è un software molto leggero e veloce, e non ha finora evidenziato
problemi di compatibilità anche con sistemi embedded poco potenti.
70
8 Conclusioni
8.1 Bibliografia
K. Wall, M. Waston, M. Whitis – “Programmare in Linux Tutto e oltre” –
Apogeo – 2000 – ISBN: 9788873036197
K. Davis, J. W. Turner, N. Yocom – “The Definitive Guide to Linux Network
Programming” – Apress – 2004 – ISBN: 1-59059-322-7
http://www.cplusplus.com/reference/ – C++ Documentation
Daniel Drake – Libusb documentation – http://www.libusb.org/
Nokia Corporation – Qt3 documentation – http://doc.qt.nokia.com/
Simone Piccardi – “GaPiL – Guida alla Programmazione in Linux” – 2002-2008
– http://gapil.truelite.it/
Robert C. Martin – “UML Tutorial” – 1997-98 – http://www.objectmentor.com/
E. Gamma, R. Helm, R. Johnson, J. Vlissides – “Design Patterns: elementi per il
riuso di software a oggetti” – Paerson Education Italia – 2002 – ISBN: 88-7192-
150-X
71