Protocol de comunicare serială, între plăcuțe de [630356]
Protocol de comunicare serială, între plăcuțe de
dezvoltare Arduino, cu „idei” din stiva TCP/IP
2
Cuprins
Introducere………………………………………………………………………………………………………. 7
Ideea lucrării………………………………………………………………………………………………… 7
Comunicare fără confirmare …………………………………………………………………………… 7
Comunicare cu confirmare ……………………………………………………………………………… 8
Alte abordări ale acestei probleme …………………………………………………………………… 8
Contribuții………………………………………………………………………………………………….. 10
Metodele publice ale bibliotecii …………………………………………………………………. 11
Concluzie……………………………………………………………………………………………….. 13
Structura licenței…………………………………………………………………………………………. 13
1Abordare teoretică…………………………………………………………………………………….. 15
1 . 1Arduino – hardware și software ……………………………………………………………. 15
1.1.1Specificații tehnice ………………………………………………………………………… 16
1.1.2Comunicarea serială ………………………………………………………………………. 16
1.1.3Concluzie……………………………………………………………………………………… 17
1 . 2Procedeul de conectare Three Way Handshake ……………………………………….. 17
1 . 3Procedeul de încheiere a conexiunii ………………………………………………………. 18
1 . 4Algoritmul lui Fletcher – Suma de control a lui Fletcher ………………………….. 19
1.4.1Tipuri de erori și detectarea acestora ………………………………………………… 20
1.4.2Concluzie……………………………………………………………………………………… 21
3
1 . 5Coduri Hamming – Algoritmul de detecție și corectare de erori prin distanța
Hamming21
1.5.1Determinarea numărului de biți redundanți și a poziției acestora ………….22
1.5.2Calculul valorilor biților redundanți …………………………………………………. 23
1.5.3Detectarea și corectarea biților greșiți ………………………………………………. 23
1.5.4Concluzie……………………………………………………………………………………… 24
1 . 6Biblioteca Arduino SoftwareSerial ……………………………………………………….. 24
1.6.1Concluzie……………………………………………………………………………………… 25
2Descrierea soluției…………………………………………………………………………………….. 26
2 . 1Modulele bibliotecii ……………………………………………………………………………. 26
2.1.1Clasa ArduinoSerialCom ………………………………………………………………… 26
2.1.2Clasa Connection…………………………………………………………………………… 28
2.1.3Clasa Error……………………………………………………………………………………. 29
2.1.4Clasa UdpPacket…………………………………………………………………………… 29
2.1.5Clasa UdpProtocol …………………………………………………………………………. 30
2.1.6Clasa TcpConnectionPacket ……………………………………………………………. 32
2.1.7Clasa TcpPacket……………………………………………………………………………. 32
2.1.8Clasa TcpProtocol………………………………………………………………………….. 33
2 . 2Gestionarea memoriei …………………………………………………………………………. 35
2 . 3Arhitectura proiectului – Diagrama UML ………………………………………………. 37
3Concluziile lucrării ……………………………………………………………………………………. 38
Bibliografie……………………………………………………………………………………………………. 39
4
Anexe……………………………………………………………………………………………………………. 40
Anexă 1……………………………………………………………………………………………………… 40
5
6
Introducere
Ideea lucrării
Protocol de comunicare între plăcuțe de dezvoltare Arduino, cu idei din stiva
TCP/IP este o lucrare practică ce are ca scop implementarea unei biblioteci specifice
mediului de lucru Arduino prin care două (sau mai multe) plăcuțe de acest tip pot comunica
între ele mesaje, la nivel hardware, prin intermediul firelor. Scopul prezentei lucrări este de a
oferi un mod sigur de comunicare serială prin pinii digitali puși la dispoziție de către plăcuță,
atât din punct de vedere al integrității mesajelor transmise cât și al securității, folosindu-se
idei din stiva TCP/IP.
Ideea acestei teme a plecat de la necesitatea existenței unei biblioteci care să asigure
integritatea datelor transmise între două plăcuțe Arduino cât și posibilitatea de a interconecta
și comunica simultan între mai mult de două plăcuțe de acest tip . Acesta bibliotecă, ce
împrumută idei teoretice din protocolul TCP/IP, își poate găsi cu ușurință, o utilizare în
domeniul IoT, un domeniu tehnologic aflat în plină expansiune în ultimul deceniu.
Pentru aceasta, am creat o bibliotecă Arduino, denumită TwoArduinoSerialCom .
Această bibliotecă permite interconectarea a două plăcuțe Arduino prin metode (funcții)
stabilite la nivel software. Paradigma de programare folosită în dezvoltarea acestei biblioteci
este cea de Programare Orientată pe Obiecte, limbajul de dezvoltare fiind o submulțime de
funcții al bibliotecii C standard. TwoArduinoSerialCom suportă două categorii de
comunicare între plăcuțe de tip Arduino: comunicarea fără confirmare (asemănătoare
protocolului UDP din stiva TCP/IP) și comunicarea cu confirmare (asemănătoare protocolului
TCP din stiva TCP/IP). Un element comun al acestor două protocoale este fragmentarea
mesajului inițial în pachete de dimensiuni prestabilite, din motive de performanță dar și
calcularea unei sume de control a pachetului transmis pentru identificarea integrității acestuia.
Comunicare fără confirmare
Comunicarea fără confirmare sau comunicarea fără conexiune presupune trimiterea
mesajului fragmentat în blocuri, de la expeditor către destinatar, fără a se asigura că există o
confirmare de primire din partea destinatarului, a pachetelor de date transmise. În acest mod,
7
expeditorul nu este depedent în niciun fel de destinatar, viteza de transfer a datelor fiind una
ridicată deoarece nu există retransmitere de pachete, ca în cazul comunicării cu confirmare.
La destinatar, pachetele se vor uni, astfel formând mesajul trimis de către expeditor. Acest tip
de comunicare conține un mechanism de validare a corectitudinii datelor, implementat prin
intermediul unei sume de control ce face parte din fiecare pachet tranzacționat. Astfel,
destinatarul va putea ști dacă pachetul primit conține date valide sau nu, neputând însă sa le
corecteze.
Comunicare cu confirmare
Comunicarea cu confirmare sau comunicarea cu o conexiune stabilă presupune în
primul rând o metodă de interconectare între interlocutori. Prin aceasta se asigură faptul că
expeditorul și destinatarul se „cunosc” unul pe celălalt și sunt pregătiți să înceapă
comunicarea de date prin intermediul porturilor seriale. Ulterior interconectării, se poate
începe comunicarea de mesaje între cele două părți. Similar protocolului de comunicare fără
confirmare, mesajul este împărțit în pachete de dimensiune fixă, un pachet conținând o sumă
de control. O primă diferentă sunt biții redundanți pentru corectarea mesajului la expeditor, în
cazul în care datele transmise sunt corupte din cauza erorilor la nivel fizic / hardware. Prin
acest mecanism se asigura integritatea datelor transferate.
O altă diferența între acest tip de comunicare și cel enunțat în subcapitolul anterior
este confirmarea primirii cu succes a pachetului. În caz contrar, destinatarul va cere
retrimiterea pachetului de către expeditor până când toate pachetele, implicit tot mesajul
compus din pachete, au fost transmise cu succes. În acest mod, se asigură o comunicare fără
erori între cele două părți.
Alte abordări ale acestei probleme
O abordare similară a acestei probleme reprezintă biblioteca SerialTransfer ce poate
fi găsită pe GitHub1. Aceasta permite fragmentarea datelor în pachete de dimensiune variabilă,
detectare de erori prin algoritmul Cyclic Redundancy Check – 8, transmitere de tipuri de date
precum bytes, int, floats și chiar structuri de date, compatibilitate cu porturile hardware
UART2 și cele software prestabilite de către plăcuțele Arduino. Biblioteca conține și un
mecanism de tratare și expunere, către utilizator, a erorilor apărute în procesul de transmitere.
1 https://github.com/PowerBroker2/SerialTransfer
2 Universal asynchronous receiver-transmiter
8
De asemenea, suportă orice rată BAUD de transfer a datelor. Rata BAUD reprezintă numărul
maxim de biți ce pot fi transferați într-o secundă, în cadrul unui canal de comunicații.
În următoarele paragrafe, voi arăta un exemplu de utilizarea a bibliotecii
SerialTransfer, conform prezentării din secțiunea README.md de pe GitHub1,, pentru a
putea compara apoi cu biblioteca TwoArduinoSerialCom .
Includerea bibliotecii și declararea unui obiect al clasei SerialTransfer.h:
#include "SerialTransfer.h"
SerialTransfer myTransfer;
Schema pachetului tranzacționat:
Inițializarea obiectului myTransfer și a ratei BAUD:
Serial1.begin(115200);
myTransfer.begin(Serial1);
Transmiterea datelor de la expeditor:
myTransfer.txBuff[0] = 'h';
myTransfer.txBuff[1] = 'i';
myTransfer.txBuff[2] = '\n';
myTransfer.sendData(3);
Primirea datelor la destinatar:
if(myTransfer.available()){
// see next step
}
else if(myTransfer.status < 0){
Serial.print("ERROR: ");
9
if(myTransfer.status == -1)
Serial.println(F("CRC_ERROR"));
else if(myTransfer.status == -2)
Serial.println(F("PAYLOAD_ERROR"));
else if(myTransfer.status == -3)
Serial.println(F("STOP_BYTE_ERROR"));
}
Spre deosebire de această bibliotecă, lucrarea de licență prezentă conține mecanisme
suplimentare inspirate din protocolul TCP/IP, care oferă o comunicare orientată pe conexiune
stabilă între cele două părți. În subcapitolul următor vom vedea mai detaliat diferențele dintre
cele două biblioteci.
Contribuții
În această secțiune vă voi prezenta prin ce se diferențiază aceasta bibliotecă față de cea
prezentată în subcapitolul anterior dar și metodele publice (funcțiile) acestei lucrări de licență.
Biblioteca TwoArduinoSerialCom conține o serie de metode ce vor fi arătate mai jos, prin
intermediul cărora se realizează transmiterea de date către o altă plăcuță Arduino ce rulează
aceeași bibliotecă. Mai întâi se va include headerul care corespunde modului de comunicare
dorit, apoi se va inițializa biblioteca cu porturile hardware prin care se dorește realizarea
comunicării. În funcție de modul de comunicare ales, în continuare se poate începe
transmiterea de mesaje prin intermediul metodelor de scriere și citire, sau daca s-a ales modul
de comunicare cu confirmare, se vor apela funcțiile care interconectează cele două plăcuțe
Arduino, la nivel software, iar apoi se pot realiza scrierile și citirile, sau viceversa.
O primă diferență între cele două biblioteci este faptul că TwoArduinoSerialCom
suportă transferul datelor prin oricare două porturi (RX, TX), și nu doar porturile 0 și 1 ale
plăcuței Arduino, care sunt în mod implicit pentru comunicarea serială. Deoarece sunt doar
două porturi, biblioteca SerialTransfer permite implicarea în cadrul aceluiași proiect a doar
două plăcuțe. Prin mărirea numărului de porturi ce suportă comunicarea serială, biblioteca
TwoArduinoSerialCom face posibilă integrarea în cadrul aceluiași proiect a mai multe
plăcuțe ce pot intercomunica, pe rând, două câte două.
10
O a doua diferență este aceea că biblioteca TwoArduinoSerialCom asignează fiecărui
plăci Arduino un număr unic de identificare denumit în cadrul sistemului: UAID3. Astfel,
utilizatorul are opțiunea să folosească un sistem de adresare, în caz că dorește acest lucru.
Spre exemplu, dacă întreg sistemul conține mai multe plăcuțe Arduino interconectate și există
un server / router ce va centraliza și redirecționa datele, metodele de scriere și citire suportă
un argument prin care se specifică numărul unic de identificare al plăcuței destinatare /
expeditoare.
În cazul în care utilizatorul a optat pentru modul de comunicare cu confirmare, alte
diferențe între cele două biblioteci sunt existența mecanismelor de detectare și corectare dar și
de retrimitere a pachetelor corupte. Cele două mecanisme conlucrează și încercă să corecteze
un pachet, dacă acesta conține maximum doi biți greșiți. În cazul în care se identifică mai
mulți biți greșiți, mecanismul va cere retransmiterea pachetului care a fost identificat ca și
corupt. De asemenea, înaintea începerii comunicării, plăcuțele își vor semnala una alteia
prezența în cadrul sistemului, printr-un mecanism de conectarea. Rolul acestuia este evitarea
trimiterii de date către un destinatar care nu este pregătit să le primească. De asemenea există
și un mecanism de încheiere a conexiunii, ce funcționează similar.
Metodele publice ale bibliotecii
În continuare, vă voi prezenta pe scurt funcțiile principale ale acestei lucrări,
disponibile pentru utilizatori. Pentru a include și folosi biblioteca, trebuie mai întâi sa stabilim
modul de comunicare dorit: comunicare fără confirmare sau cu confirmare. Mai jos găsim
ambele moduri de includere:
#include “UdpProtocol.hpp” // comunicare fără confirmare
#include “TcpProtocol.hpp” // comunicare cu confirmare
Urmează inițializarea bibliotecii care este la fel pentru ambele moduri de comunicare.
Aici vom stabili și portul serial pe care vrea utilizatorul să-l foloseacă, pentru că acesta nu este
unic:
TcpProtocol myProtocol; // UdpProtocol myProtocol;
myProtocol.initializePorts(rxPort, txPort);
myProtocol.initializeSerial(Serial, 9600); //Serial, rata BAUD
3 Unique Arduino Identifier
11
În caz că utilizatorul a ales modul de comunicare cu confirmare, deoarece acesta
funcționează asemănător cu principiul client – server, se vor utiliza următoarele funcții:
listen() și connect(). Serverul va apela funcția listen(), care „ascultă” clientul deja inițializat să
se conecteze. Clientul va apela funcția connect(), pentru a se conecta la server. În cazul în care
conexiune nu poate fi stabilită, serverul returnează un mesaj de eroare și „ascultă” în
continuare. Protocolul de conectare este asemănător cu procedeul Three Way Handshake
prezent în cadrul protocolului TCP din TCP/IP. Funcția printLastError() va afișa, la portul
serial stabilit în inițializarea biblioteci, ultima eroare detectată de către aceasta.
// Server
int UAID = myProtocol.listen();
// Client
if (myProtocol.connect() < 0) {
myProtocol.printLastError();
}
Funcțiile de citire și scriere se numesc: read() și write(). Acestea permit două
argumente: primul este un pointer către o zonă de memorie unde se află mesajul ce va fi
primit / trimis, iar al doilea parametru este UAID-ul plăcuței Arduino de unde a fost trimis
mesajul / către cine sa fie trimis. Acest ultim argument al funcției este opțional.
char data[] = „Data”;
int senderUAID, destinationUAID;
if ((length = tcpProtocol.read(dataToReceive, senderUAID)) < 0) {
tcpProtocol.printLastError();
}
if (!tcpProtocol.write(userInput, &destinationUAID)) {
tcpProtocol.printLastError();
}
Pentru încheierea conexiunii între client și server, se vor apela următoarele funcții ce
au rolul de a semnala bibliotecii încheierea transmiterii de mesaje între cele două părți.
// Server
myProtocol.serverClose();
//Client
12
myProtocol.clientClose();
Concluzie
În concluzie, contribuțiile acestei biblioteci față de cea prezentată în subcapitolul
precedent sunt următoarele:
mecanism de interconectare între două plăcuțe pe baza unui UAID
disponibilitatea tuturor porturilor pentru comunicare de mesaje
creare unui id unic pentru fiecare plăcuță Arduino
transmiterea mesajelor în două moduri: cu și fără confirmare
segmentarea mesajului în blocuri de dimensiune fixă
detectarea și corectarea mesajelor la destinație în caz ca au apărut erori în
procesul de transmitere
mecanism de reordonare a pachetelor primite
mecanism de încetare a conexiunii
În capitolul Anexe, subcapitolul Anexă 1, puteți găsi un exemplu funcțional de cod
client – server, în care se află toate metodele descrise în acest capitol.
Structura licenței
Această lucrare de licență conține trei capitole, Abordări teoretice, Descrierea Soluției
și Concluziile lucrării. De asemenea, fiecare subcapitol parte a unui capitol, va conține la
final o secțiune de concluzii, în care se vor sumariza principalele subiecte atinse.
În primul capitol al acestei licențe veți găsi explicații ale părților teoretice din spatele
bibliotecii TwoArduinoSerialCom, precum o scurtă introducere despre mediul de dezvoltare
și plăcuța Arduino, algoritmul de detectare de erori, algoritmul de detectate și corectare de
erori, protocolul de stabilire a conexiunii cât și principiile protocolului TLS, implementat
parțial în lucrarea de față.
În al doilea capitol se atinge subiectul descrierii soluției găsite. Aici voi detalia,
explica și arăta, structurile de date utilizate, o parte din algoritmii folosiți, structura pachetului
de date transmis către destinatar cât și o mică secțiune în care voi vorbi despre problemele
întâlnite de-a lungul procesului de dezvoltare al acestei biblioteci.
13
Capitolul al treilea conține concluziile aceste lucrări: un sumar al tuturor celorlalte
capitole anterioare și câteva statistici din punctul de vedere al memoriei și al vitezei utilizate
de către biblioteca.
14
1Abordare teoretică
În acest capitol vom discuta despre aspectele teoretice ale acestei lucrări de licență.
Deoarece această bibliotecă împrumută idei din protocolul TCP/IP, idei prin care se realizează
diferite mecanisme de conectare între plăcuțe, de detectare și corectare de erori, și de
retransmitere de pachete, în acest capitol voi explica fundamentele teoretice ce stau la baza
acestora și cu ajutorul cărora se realizează transferul datelor fără erori de la expeditor la
destinatar.
1.1 Arduino – hardware și software
Ce este o plăcuță Arduino și la ce ne folosește? Arduino este o companie open-source
ce poate fi privită din două perspective: hardware și software. La nivel hardware, compania
Arduino produce multiple modele de plăcuțe Arduino ce sunt compuse dintr-un
microcontroler, de obicei de tip Atmel AVR de 8, 16 sau 32 de biți. Plăcuța are atașați diferiți
pini analogici și digitali ce permit conectarea acesteia la alte plăcuțe hardware, denumite
shielduri. Modul de comunicare între shielduri și plăcuța Arduino mamă se efectuează, de
obicei, prin conexiune serială. Scopul principal este de a controla și programa diferiți senzori
și dispozitive electronice similare ce sunt conectate la plăcuța Arduino. Aici intervine partea
de software, compania Arduino oferind suport în acest sens programatorilor. Aceasta a creat
un mediu integrat de dezvoltare ( IDE) prin care se poate scrie cod, asemănător limbajului C și
C++, cod ce conține diferite metode și API-uri ce ușurează accesul la pinii plăcuței. Se poate
spune că Arduino este un limbaj de programare, dar este mai mult o formulare improprie,
deoarece acesta conține o mulțime de funcții împrumutate din limbajul C / C++. IDE-ul
deține un compilator de C/C++ (avr-g++) și poate încărca codul scris în memoria plăcuței
Arduino. Programele pentru plăcuță Arduino suportă orice limbaj de programare atâta timp
cât există un compilator ce va produce codul mașină binar pentru microcontrolerul Atmel
AVR. Comunitatea Arduino oferă o serie de biblioteci open-source ce sunt incluse în ( IDE) și
care oferă diferite funcționalități precum biblioteca din această lucrare. Datorită sprijinului
oferit de programatori prin crearea diverselor biblioteci, dezvoltarea proiectelor electronice
cu Arduino este una simplă și fără prea multe bătăi de cap, chiar și pentru programatorii
începători.
15
1.1.1Specificații tehnice
În continuare, voi vorbi despre specificațiile tehnice ale plăcuței de dezvoltare Arduino
UNO. Cum am menționat mai sus, plăcuța dispune de un microcontroler Atmel AVR de tip
Atmega328p, 14 pini digitali de intrare / ieșire, 6 pini analogici, port USB, și un buton de
repornire. Viteza de executarea a microcontrolerului este de 16MHz, suficientă pentru a
efectua sarcini complexe în contextul programării componentelor electronice. Tensiunea de
funcționare a plăcuței este de 5V, compatibilă cu majoritatea shieldurilor aflate pe piață.
Memoria plăcuțelor Arduino este împărțită în trei categorii: memoria flash, memoria
SRAM4 și memorie EEPROM5. Arduino UNO dispune de 32 KB de memorie flash din care
0.5 KB sunt ocupați de programul de inițializare a programelor utilizatorilor ( bootloader-ul).
În această memorie se stochează imaginea programul utilizatorului, denumit și sketch, dar și
orice inițializare de date. Memoria SRAM4 este de 2 KB și este o memorie volatilă. Aceasta
este, din punctul meu de vedere, cel mai important tip de memorie al plăcuței Arduino,
deoarece programatorul este limitat la o cantitate de memorie mică pe care trebuie să o
gestioneze corect, în caz contrat aceasta poate fi depășită și codul poate avea un
comportament impredictibil. În acest tip de memorie se află salvate datele statice, memoria de
tip Heap și de tip Stack. Memoria EEPROM5 este un tip de memorie nevolatilă de 1 KB, care
poate fi scrisă și citită din codul utilizatorului. Chiar dacă aceasta are o viteză de scriere /
citire mai mică decât memoria SRAM, aceasta poate fi uneori foarte folositoare.
1.1.2Comunicarea serială
Plăcuța Arduino UNO deține doi pini digitali de tip RX / TX, special pentru
comunicarea serială de mesaje între plăcuțe Arduino prin intermediul firelor de lungime
scurtă. Aceasta se realizează prin intermediul unei componente hardware UART6, integrate în
arhitectura microcontrolerului, prin intermediul căreia se obține trimiterea, recepționarea și
interpretarea biților (0 și 1) de la un microcontroler la altul. Transmiterea datelor se realizează
asincron, neexistând niciun mecanism de sincronizare sau de validare a datelor primite de
către destinatar. Viteza de transmitere este un parametru configurabil asupra căruia trebuie să
cadă de acord cele două microcontrolere.
4 Static Random Access Memory
5 Electrically Erasable Programmable Read-Only Memory
6 Universal Asynchronous Receiver Transmitter
16
1.1.3Concluzie
În concluzie, privind specificațiile tehnice, plăcuța Arduino UNO se potrivește pentru
dezvoltarea diverselor componente electronice deoarece are o viteză mare de reacție, în ciuda
faptului că are o putere mică de calcul și deține o memorie limitată (doar 2 KB de memorie
SRAM4), ce trebuie gestionată cu atenție de către programator. De asemenea microcontrolerul
plăcuței suportă comunicarea serială, subiect principal al acestei lucrări.
1.2 Procedeul de conectare Three Way Handshake
Procedeul de conectare Three Way Handshake este un procedeu ce face parte din
protocolul TCP (Transmission Control Protocol). Acesta presupune o interschimbare de
pachete de comunicare, rolul fiind de a stabili un contract comun stabil și de încredere între
două părți (client – server) ce doresc să comunice. Utilizarea acestuia în cadrul bibliotecii
TwoArduinoSerialCom a fost necesară pentru a interconecta două plăcuțe Arduino în modul
de comunicare cu confirmare, astfel încât apoi să se poată începe comunicarea propriu-zisă.
Implementarea acestui protocol se găsește în două metode publice ale bibliotecii: connect() și
listen() și este realizată pe baza a două flaguri: SYN (Synchronize Sequence Number), ACK
(acknowledged).
Pașii de conectare a unui client la server sunt următorii:
1.Serverul apeleaza metoda listen(), prin care ascultă și așteaptă ca clientul să
se conecteze.
2.Clientul apelează metoda connect(), prin care dorește să stabilească o
conexiune, și va trimite către server un pachet de conectare de tip SYN, ce
conține flag-ul SYN = 1, valoarea inițială a secvenței, seq_client, este
numărul ce identifică unic plăcuța Arduino client, și flag-ul ACK = 0.
3.Serverul primește pachetul de conectare și va trimite înapoi un nou pachet de
tip SYN-ACK, unde SYN = 1, valoarea secvenței, seq_server, este numărul
ce identifică unic plăcuța Arduino server, și ACK = seq_client + 1;
4.Clientul primește pachetul de conectare și va trimite înapoi un pachet de tip
ACK, unde SYN = 0, seq_client = ACK și ACK = seq_server + 1
Prin intermediul acestor 4 pași, se realizează o conexiune între client și server. Putem
observa că flagurile ce vor fi trimise sunt incrementate pe baza celor din pachetul primit la
pasul anterior. La fiecare pas, clientul cât și serverul vor face verificările necesare astfel încât
17
procedeul să fie respectat. Dacă unul dintre flaguri nu respectă regulile convenției, înseamnă
ca a apărut o eroare în procedeul de conectare, metoda listen() având un mecanism de resetare
prin care așteaptă conectarea clienților în continuare. Metoda connect() nu dispune de un
astfel de mecanism, ea returând o eroare în acest caz. Mai jos în Figura 1 găsim o schemă a
pașilor descriși mai sus.
Figura 1 – Schema procedeului THW
1.3 Procedeul de încheiere a conexiunii
Similar procedeului de stabilire a conexiunii între client și server, procedeul de
încheiere a conectării între cele două părți are loc prin transmitere de pachete, în 4 pași, cu
ajutorul a 2 flaguri, FIN (Finish) și ACK (acknowledged). Pașii sunt similari cu cei de mai
sus, utilizându-se flagul FIN în de SYN. Metodele publice ce trebuie să fie apelate sunt:
clientClose() și serverClose(). O schema simplificată a procedeului se găsește în Figura 2.
18
Figura 2 – Schema procedeului de încheiere a comunicării
1.4 Algoritmul lui Fletcher – Suma de control a lui Fletcher
Algoritmul lui Fletcher face parte din categoria celor pentru detectarea erorilor în
cadrul comunicării seriale a mesajelor. În cursul transmiterii mesajelor de la expeditor la
destinatar, acestea sunt transformate în secvențe de biți și transportați unul câte unul.
Modificare unui singur bit în timpul acestui proces din 0 în 1 sau invers, reprezintă o eroare
de bit și este inclus în categoria erorilor de transmitere. Astfel tot pachetul transmis este
categorisit ca și un pachet cu eroare, consecințele fiind pierderea sensului mesajului și
propagarea de erori mai departe în sistem. Cauzele care pot provoca erorile de bit sunt de
două feluri: software și hardware. În cazul erorilor software, în subcapitolul anterior am vorbit
despre memorie și importanța gestionării corecte a acesteia. Erorile în transmiterea biților pot
apărea chiar înainte ca trimiterea datelor să aibă efectiv loc, acestea neputând fi „prinse” de
programator. O cauză des întâlnită este depășirea memoriei SRAM prin alocări dinamice
excesive în memoria Heap și depășirea acesteia, ce automat conduce la un comportament
impredictibil al programului și la apariția de erori. În cazul erorilor hardware, aici vom intra în
domeniul telecomunicațiilor unde este prezent efectul de zgomot, când semnalul electric
transmis este alterat din motive externe, provocate de om sau de alte mecanisme ce induc
câmpuri electromagnetice. Transferul biților este un concept teoretic, aceștia în practică
19
transformându-se în semnale de intensități diferite care sunt trimise prin fir către cealaltă
plăcuță. În cazul Arduino UNO, care funcționează cu o intensitatea a curentului electric de 5V,
bitul 0 este transformat la într-un semnal de intensitate 0V, iar bitul 1 într-un semnal de
intensitate 5V. Apariția unei erori înseamnă modificarea intensităților de 0V sau 5V, în timpul
transportului curentului electric, în altă intensitate pe care mai apoi protocolul de comunicare
serială va încerca să-l interpreteze în 0V sau 5V, în acest mod existând posibilitatea apariției
unei erori de bit.
1.4.1Tipuri de erori și detectarea acestora
Există trei tipuri de erori: erori singulare de bit, erori multiple de biți și erori în masă
de biți (trei sau mai mulți biți). Ultimul tip de eroare este cel mai dificil de detectat și corectat,
fiind unul des întâlnit în comunicarea serială. De asemenea, există diferiți algoritmi de
detecție a erorilor: verificarea parității biților, verificarea redundanței ciclice (CRC), și sume
de control.
Verificarea parității biților presupune adăugarea unui bit suplimentar la mesaj. Acest
bit suplimentar este calculat astfel: se numără biții de 1 (sau 0, depinde de abordare) din
mesaj iar apoi se vede dacă aceștia sunt în număr par sau nu. Dacă sunt în număr par, bitul
suplimentar va fi 1, altfel va fi 0. Același proces este repetat la destinatar, iar dacă bitul
suplimentar are o valoare diferită, atunci se poate spune că a apărut o eroare. Acest algoritm
este unul ce nu ar trebui folosit în practică, nefiind unul sigur, din motive lesne de înțeles.
Sumele de control sunt o metodă ce poate fi folosită în practică deoarece are rezultate
foarte bune. Există diferiți algoritmi ce folosesc acest concept, asemănător cu algoritmul de
verificare a parității biților. Un astfel de algoritm este cel al lui Fletcher: Sumele de control ale
lui Fletcher. Unul dintre proprietățile acestui algoritm, a cărui implementare este în Tabela 1,
este viteza de calcul a sumelor de control, vitală în transmiterea mesajelor. Algoritmul este
unul simplu, pe care îl voi descrie mai jos: acesta va conține două valori – sume de 8 biți
fiecare, ce vor fi calculate și adăugate la mesajul inițial. Prima sumă va reprezenta suma
tuturor byților mesajului modulo 255, pentru a respecta lungimea maximă de 8 biți. A doua
sumă se va calcula simultan cu prima și va fi suma tuturor valorilor pe care o va avea prima
sumă modulo 255.
int checkSum1 = 0, checkSum2 = 0, dataLength = strlen(data);
for (int index = 0; index < dataLength; index++) {
checkSum1 = (checkSum1 + data[index]) % 255;
checkSum2 = (checkSum2 + checkSum1) % 255;
20
}
checkSum1 %= 255;
data[dataLength + 1] = checkSum1;
checkSum2 %= 255;
data[dataLength + 2] = checkSum2;
Tabela 1 – Algoritmul lui Fletcher în C / C++
Noul mesaj cu cele două sume adăugate la cel inițial este transmit către destinatar,
unde algoritmul lui Fletcher va fi din nou utilizat pentru a calcula din nou sumele de control.
Dacă sumele de control de la destinatar coincid cu cele primit de la expeditor, înseamnă că
transmiterea mesajului a avut loc cu succes, altfel avem o eroare ce trebuie corectată. Despre
această problemă voi relata în subcapitolul următor.
1.4.2Concluzie
Această bibliotecă utilizează algoritmul de detectare de erori al lui Fletcher în
ambele moduri, atât cel de comunicare fără confirmare cât și cel cu confirmare. Deoarece
algoritmul este unul light și viteza de calcul a sumelor este redusă, mi s-a părut o idee
bună utilizarea acestuia în detrimentul algoritmului CRC care are rezultate similare cu
acesta, dar cu o viteză de calcul mai ridicată. Mai multe detalii despre implementarea
acestui algoritm le veți putea găsi în capitolul următor: Descrierea soluției.
1.5 Coduri Hamming – Algoritmul de detecție și corectare de erori prin
distanța Hamming
Algoritmul de detecție și corectare de erori prin distanță Hamming este un algoritm
prin care se pot detecta (ca în cazul algoritmului lui Fletcher) și corecta erorile apărute în
procesul de transmitere al unui mesaj. Codul lui Hamming poate ajuta la detectarea a până la
doi biți greșiți și corectarea acestora. Ideea principală a acestui algoritm este de a insera în
mesajul inițial, pe poziții stabilite, biți de paritate denumiți în cadrul acestui algoritm și biți
redundanți. Prin intermediul acestora se vor detecta, și ulterior corecta eventualele erori
apărute în procesul de transmitere.
Modalitatea de calcul a acestor biți redundanți dar și raționamentul matematic din
spatele algoritmului, aplicate în această lucrare de licență, constituie subiectul acestui
subcapitol. Acestea vor fi explicate și exemplificate în următoarele rânduri.
21
1.5.1Determinarea numărului de biți redundanți și a poziției acestora
Să presupunem că avem un cuvânt inițial, format dintr-o înșiruire de biți 0 și 1 din
mulțimea {0,1}, de lungime k. Primul pas în aplicarea algoritmului lui Hamming este
determinarea numărului de biți redundanți m, ce vor fi adăugați cuvântului inițial.
Lungimea noului cuvânt obținut este n=k+m. O singură eroarea apărută în noul cuvânt
poate fi pe orice poziție 1…n. Două erori apărute pot fi pe oricare două poziții 1…n,
numărul total de posibilități de poziții al acestora fiind combinări de n luate câte 2, (n
2).
Generalizând, dacă avem e erori în total în noul cuvânt, numărul de configurări ale
cuvântului nou cu erori posibile este ∑i=1e
(n
i). Numărul total de cuvinte noi ce pot fi
construite cu m biți redundanți sunt 2m, unde 2 reprezintă cardinalul mulțimii {0, 1}. Dacă
vom considera că o poziție din cele 2m este corectă, rezultă relația cunoscută sub numele
de Marginea lui Hamming , prin care vom calcula numărul de biți de redundanță ce trebuie
adăugați la cuvântul inițial.
2m−1≥∑i=1e
(n
i)
Tabela 2 – Relația Marginea lui Hamming
Pentru calculul dinamic în cod al numărului m, vom utiliza o aproximare a
termenului al doilea al inegalității de mai sus. Numărul minim de configurări al noului cuvânt
este combinări de n luate câte 1, așadar relația de mai sus se poate rescrie ca:
2m−1≥∑i=1e
(n
i)≥(n
1)→2m−1≥(n
1)→2m−1≥n→2m−1≥m+k
Tabela 3 – Relația Marginea lui Hamming simplificată
Așadar, pentru a găsi numărul de biți redundanți m vom folosi relația
2m−1≥k+m, unde k este lungimea cuvântului inițial.
În cazul aceste biblioteci, lungimea mesajului este de 16 byți, rezultând lungimea
cuvântului de 128 biți. Numărul de biți redundanți va fi, conform relației simplificate din
Tabela 3 egal cu m=8. Aceștia vor fi plasați pe pozițiile ce reprezintă puteri ale lui 2 (de
22
exemplu, 1, 2, 4, 8, 16, etc.) și vor fi denumiți biții m1,m2,,m4,m8,m16etc. Lungimea
noului mesaj va fi de 136 biți, 17 byți. (128 biți ai mesajului inițial + 8 biți redundanți).
1.5.2Calculul valorilor biților redundanți
Biții de redundanță sunt biți de paritate și se calculează pe baza biților din cuvântul
inițial. După inserarea acestora, se va crea un cuvânt nou care va avea forma următoare a
biților: m1m2k3m4k5k6k7m8k9. Putem vedea cum biții redundanți au fost inserați printre
biții ki ai cuvântului inițial k, pe pozițiile puteri ale lui 2.
Un exemplu al calculului valorii biților redundanți este următorul:
m1 va fi bitul de paritate al biților ce în reprezentarea lor binară au 1 pe
prima poziție: (3, 5, 7, 9, 11, etc).
m2 va fi bitul de paritate al biților ce în reprezentarea lor binară au 1 pe a
doua poziție: (3, 6, 7, 10, 11, 12, etc).
m4 va fi bitul de paritate al biților ce în reprezentarea lor binară au 1 pe a
treia poziție: (5-7, 12-15, 20-23, etc).
În acest mod, fiecare bit ki din cuvântul inițial face parte din măcar un bit de
redundanță mi, corectarea oricărui bit din cuvântul inițial fiind astfel posibil.
1.5.3Detectarea și corectarea biților greșiți
Pentru a detecta și corecta biții greșiți, mecanismul ce se execută la destinatar va trebui
să efectueze operațiile în sens inverse celor de mai sus. Un prim pas este înlăturarea biții
redundanți din mesajul primit, astfel obținând mesajul original trimis de expeditor. Pe baza
acestuia, urmează recalcularea biților redundanți și compararea lor cu biții redundanți primiți
de la expeditor care au fost înlăturați la pasul anterior. Dacă există o egalitate perfectă,
înseamnă ca mesajul transmis nu conține nicio eroare de bit. În cazul în care se identifică biți
redundanți cu o valoare diferită față de biții redundanți primiți de la expeditor, mecanismul va
detecta o eroare de bit și o va corecta în modul următor: se formează un număr în baza 2 din
biții redundanți, iar valoarea transformată în baza 10 reprezintă poziția bitului ce a cauzat
eroarea. Acest lucru se întâmplă datorită legăturilor matematice create prin selecția specială a
biților mesajului care intră în calculul unui bit de redundanță.
23
Spre exemplu dacă biții redundanți calculați la destinatar sunt 1, 1, 0, 1, rezultă că
eroarea se află pe poziția 13 ( 1101(2)=13(10)). Odată ce știm poziția bitului greșit, trebuie
doar să-l inversăm, aplicând operatorul Not, astfel corectându-l.
1.5.4Concluzie
Această bibliotecă folosește algoritmul de detecție și corecție a lui Hamming pentru
corectarea mesajelor transmise de la o plăcuță la alta. Algoritmul se aplică pe un cuvânt inițial
de lungime 128 de biți și este capabil să corecteze până la doi biți de eroare. După corectarea
a maxim doi biți de eroare, se aplică algoritmul lui Fletcher, iar dacă sumele de control
coincid cu cele trimise de expeditor, atunci pachetul primit este unul corect și dublu verificat.
În caz contrar, destinatarul nu poate corecta pachetul, cerând expeditorului retransmiterea
acestuia.
Algoritmul lui Hamming de detecție și corecție a mesajelor este un algoritm simplu de
implementat cu o complexitate scăzută, rapiditatea acestuia fiind un factor de decizie
important pentru care am optat să-l implementez în această bibliotecă. Detaliile despre
implementarea acestuia le vom discuta în capitolul următor.
1.6 Biblioteca Arduino SoftwareSerial
Într-un capitol anterior am vorbit despre cum se realizează comunicarea serială la nivel
hardware și faptul că o plăcuță Arduino are în mod implicit doar două porturi ce o permit,
portul 0 și 1. Biblioteca SoftwareSerial este una open-source, ce permite comunicarea serială
prin orice port al unei plăcuțe Arduino, simulând prin software transferul de biți ce se
efectuează în mod normal la nivel hardware. Biblioteca este foarte folositoare atunci când vrei
să interconectezi mai multe plăcuțe Arduino, acest subiect fiind unul dintre cele principale ale
acestei lucrări de licență, motiv pentru care am și ales să o intregrez în acest proiect. În
continuare, vă voi arăta cum am folosit metodele bibliotecii în acest proiect.
După includerea bibliotecii în proiect, în clasa principală am creat un pointer de tip
SoftwareSerial, deoarece doresc să inițializez în mod dinamic instanța clasei, pe baza datelor
clientului. În constructorul clasei principale, care primește prin parametri porturile la care se
dorește să se realizeze transferul datelor, și viteza de transfer a acestora (rata BAUD), am
inițializat pointerul creat cu o instanță a clasei SoftwareSerial, iar apoi am „pornit-o” prin
24
apelul metodei begin(rataBAUD). În Tabela 4, găsiți fragmente de cod de inițializare a
bibliotecii:
SoftwareSerial *softwareSerial;
softwareSerial = new SoftwareSerial(rxPort, txPort);
softwareSerial->begin(beginSpeed);
Tabela 4 – Inițializarea bibliotecii SoftwareSerial
Pentru a putea primi date prin intermediul bibliotecii, trebuie apelată metoda listen().
Rolul acesteia este de a schimba portul la care se ascultă, în cazul folosirii mai multor porturi
de tip SoftwareSerial, deoarece o constrângere a bibliotecii este faptul că doar un singur port
poate primi date la un moment dat . Acum că avem portul stabilit pentru a primi date, trebuie
să vedem dacă sunt date în bufferul serial de primire al clasei. Pentru aceasta, vom apela
metoda available(), care va returna numărul de caractere ce „așteaptă” să fie citite. Dacă
numărul de caractere este mai mare ca zero, putem apela metoda de citire, read(), care
returnează un caracter citit. Pentru a scrie, se apelează funcția write() ce primește ca
parametru un pointer către o zonă de memorie unde se află un string. În Tabela 5 vedeți
metodele prin care se utilizează această bibliotecă.
softwareSerial->listen();
if (softwareSerial->available() > 0){
char c = softwareSerial->read();
}
char writeBuffer[] = „Write Buffer Text”.
softwareSerial.write(writeBuffer);
Tabela 5 – Utilizarea bibliotecii SoftwareSerial
1.6.1Concluzie
Utilizarea bibliotecii open-source Arduino SoftwareSerial, mi-a ușurat și permis
multiplicarea porturilor prin care poate face comunicarea serială între mai multe plăcuțe
Arduino. Datorită acesteia dar și prin mecanismul implementat în biblioteca acestei lucrări,
care permite identificarea plăcuțelor printr-un număru unic, se poate comunica între mai mult
de două plăcuțe, în cadrul aceluiași program, după modelul client – server – client. Biblioteca
este foarte ușor de utilizat, după cum ați văzut metodele descrise mai sus.
25
2Descrierea soluției
În acest capitol voi detalia clasele principale împreună cu metodele ce fac parte din
biblioteca TwoArduinoSerialCom , prin intermediul cărora este posibilă comunicarea serială
între două plăcuțe Arduino. Paradigma de programare utilizată în dezvoltarea acestei
biblioteci este Programarea Orientată pe Obiecte care presupune lucrul cu clase și obiecte din
limbajul de programare C++. Principiile de bază ale Programării Orientate pe Obiecte folosite
în această lucrare sunt încapsularea datelor, polimorfosmul și moștenirea. În primul subcapitol
vom vorbi despre rolul fiecărui modul existent și structura acestora. Al doilea subcaptiol va fi
despre gestionarea memoriei și statistici ale bibliotecii, în timp ce în ultimul capitol voi
prezenta diagrama arhitecturală UML a bibliotecii.
2.1 Modulele bibliotecii
2.1.1Clasa ArduinoSerialCom
Deoarece biblioteca suportă două moduri de comunicare, comunicarea serială fără
comunicare și comunicare serială cu comunicare, codul pentru implementarea acestora deține
câteva funcționalități comune pe care am decis să le implementez într-o clasă comună, de
bază, numită ArduinoSerialCom . Membrii acestei clase sunt de tip public și protected și vor
fi moșteniți de către clasele UdpProtocol și TcpProtocol. Metodele de tip public vor putea fi
apelate în mod direct de către utilizatorii bibilotecii.
Membrii principali de tip protected ale acestei clase sunt:
Connection connection – clasa Connection este una creată în cadrul
bibilotecii și se ocupă de starea conexiunii între expeditor și destinatar;
Error error – Clasa Error este una creată în cadrul bibliotecii și se ocupă de
tratarea erorilor din acest sistem;
HardwareSerial *hardwareSerial – această clasă este una ce face partea din
familia claselor Arduino prin care biblioteca va avea acces direct la un pointer
de tip HardwareSerial ce va fi inițializat în mod dinamic de către utilizator;
26
SoftwareSerial *softwareSerial – acest pointer este de tip SoftwareSerial,
clasă ce face parte din biblioteca SoftwareSerial, și va fi inițializat în mod
direct de către utilizator;
bool showLogs – prin acestă variabilă booleană, utilizatorul poate stabili dacă
vrea să vadă la consola serială logurile făcute de bibliotecă, în timpul
transmiterii datelor;
void softwareSerial_readBytes(char *data, int length) – această metodă este
una internă prin care se vor citi datele de lungime length de la portul serial
stabilit și se vor salva în pointer data;
void computeChecksum(char *data, int &checkSum1, int &checkSum2) –
prin această metodă se vor calcula cele două sume de control ale pachetului de
date transmis atât în modul de comunicare fără confirmare cât și cu
confirmare;
bool hasPacketErrors(char *data, int _checkSum1, int _checkSum2) – prin
această metodă se va identifica la destinatar dacă pachetul conține eroare prin
compararea sumelor de control calculate la destinatar și cele primite de la
expeditor;
void setUniqueArduinoIDToEEPROM(char *UAID) – prin această metodă
se va salva în memoria nevolatilă a plăcuței Arduino un număr unic de
identificare denumit de către mine în cadrul sistemului UAID (Unique Arduino
Identifier);
char specialChr[2] = "\f";
Membrii principali de tip public ale acestei clase și care sunt disponibile și
utilizatorului, sunt:
void initializePorts(int rxPort, int txPort) – prin această metodă se
inițializează de către utilizator , porturile de comunicare serială ale obiectului
SoftwareSerial;
void initializeSerial(HardwareSerial &Serial, int beginSpeed, int timeout,
bool showLogs),
void initializeSerial(HardwareSerial &Serial, int beginSpeed, int timeout),
void initializeSerial(HardwareSerial &Serial, int beginSpeed, bool
showLogs),
void initializeSerial(HardwareSerial &Serial, int beginSpeed); – aceasta
este metoda publică care configurează în cadrul bibilotecii portul serial la care
se vor afișa eventualele erori și insanțiază obiectul serial . Argumentul
beginSpeed reprezintă rata BAUD cu care vrem să se efectueze comunicarea
27
serială. Această metodă vine în patru versiuni cu ajutorul utilizării conceptului
de supraîncărcare. Valorile în mod implicit pentru parametrii funcției ce
lipsesc sunt: timeout = 1000, showLogs = false;
void printLastError() – această metodă va afișa la portul serial stabilit de
către utilizator prin metoda initializeSerial, eroarea de tip Error ce reprezintă
o eroare în cadrul procesului de transmitere a informației;
const char *getConnectionStatus() – prin această metodă publică, utilizatorul
poate afla care este starea conexiunii sistemului. Funcția va returna o
reprezentarea string de tip Connection;
static int getUniqueArduinoIDFromEEEPROM() – această metoda va
returna numărul unic UAID al plăcuței Arduino. În cazul în care plăcuță nu are
salvată în memoria EEPROM un UAID, funcția va sesiza acest lucru și va
genera un UAID random de patru cifre pe care îl va transmite ca parametru
metodei setUniqueArduinoIDToEEPROM.
2.1.2Clasa Connection
În această clasă se găsesc constantele ce definesc tipul unei conexiuni în cazul
comunicării cu sau fără confirmare. În Tabela 6, se află valorile pe care le poate avea o
conexiune. În mod implicit, tipul conexiunii este DISCONNECTED . Scopul acestei clase
este de a marca starea unei conexiuni, clasa având un membru private de tip
connectionStatus, prin intermediul căruia s-a implementat un mecanism de restricționare a
utilizării funcțiilor. Spre exemplu, în modul de comunicare cu confirmare, scrierea sau citirea
nu se pot realiza decât dacă tipul conexiunii este CONNECTED. Acest lucru presupune în
prealabil apelarea metodei de conectare, care dacă se realizează cu succes, modifică starea
conexiunii din DISCONNECTED în CONNECTED.
enum connectionStatus {
ERROR, // eroare în sistem, ex: în mecanismul de conectare
DISCONNECTED,
FINISHED,
CONNECTED,
};
Tabela 6 – Stările conexiunii
2.1.3Clasa Error
Clasa Error se ocupă de erorile ce pot apărea în această bibliotecă. Aceasta conține
două constante de tip enumerabil: errorMessages, în care se găsesc toate mesajele de eroare
ce pot fi afișate către utilizator, și errorCodes, care sunt niște coduri de eroare în strânsă
28
legătură cu tipul conexiunii. Spre exemplu, dacă utilizatorul dorește să scrie un mesaj prin
funcția write, dar anterior nu a fost apelată metoda de conectare (starea conexiunii este de tip
DISCONNECTED) , funcția write va returna o valoare int de tip errorCodes
(Error::DISCONNECTED = -2). Valorile celor două constante enumerabile sunt:
enum errorMessages {
CONNECT_PROTOCOL_NOT_CONNECTED_ERROR,
CONNECT_PROTOCOL_PROTOCOL_HAS_FINISHED,
WRITE_PROTOCOL_NOT_CONNECTED_ERROR,
READ_PROTOCOL_NOT_CONNECTED_ERROR,
CLOSE_PROTOCOL_NOT_CONNECTED_ERROR,
LISTEN_INTERNAL_ERROR,
CONNECT_INTERNAL_ERROR,
WRITE_INTERNAL_ERROR,
READ_INTERNAL_ERROR,
CLIENT_CLOSE_INTERNAL_ERROR,
SERVER_CLOSE_INTERNAL_ERROR,
};
enum errorCodes {
ERROR = -3,
DISCONNECTED = -2,
FINISHED = -1,
CONNECTED = 1,
CLOSED_CONN = 1,
};
Metodele public ale clasei sunt:
void setError(errorMessages _error);
char const* getError() – returnează o valoare string a valorilor de tip
errorMessages.
2.1.4Clasa UdpPacket
Așa cum am menționat în primul capitol al acestei lucrări, transmiterea mesajului de la
destinatar către expeditor se realizează prin împărțirea mesajului inițial în blocuri de câte 16
byți. Un astfel de bloc va reprezenta corpul unui pachet ce va fi trimis către destinatar. Clasa
UdpPacket definește structura unui pachet din cadrul protocolului de comunicare fără
confirmare. Dimensiunea totală a unui astfel de pachet este de 49 de byți, din care antetul
pachetului reprezintă 33 de byți, iar corpul acestuia 16 byți. În antetul pachetului se află
informații despre acesta precum lungimea totală (49 byți), lungimea corpului pachetului (16
byți), numărul total de pachete ce vor fi transmise, indexul pachetului curent ce este transmis,
cele două sume de control calculate pe baza corpului, UAID-ul expeditorului și UAID-ul
29
destinatarului. În Tabela 7 , puteți vedea schema unui pachet de tip UdpPacket cât și membrii
acestei clase.
int pSize; // dimensiunea pachetului in byți
int bLength; // lungimea bData
int bNumber; // numărul total de pachete
int bOffset; // indexul pachetului curent
int checkSum1;
int checkSum2;
int fromUAID; // UAID-ul expeditorului
int toUAID; // UAID-ul destinatarului
char *bData; // bloc de 16 byți din mesajul inițial
enum blockInformation {
BLOCK_SIZE = 49,
BLOCK_HEADER_SIZE = 33,
BLOCK_BODY_SIZE = 16
};
Tabela 7 – Schema pachetului UdpPacket
2.1.5Clasa UdpProtocol
Clasa UdpProtocol, ce moștenește clasa ArduinoSerialCom , implementează
protocolul de comunicare fără confirmare, unde stabilirea conexiunii nu este necesară înainte
de a se începe transmiterea pachetelor. Metodele de tip public ale acestei clase sunt:
int write(char *dataToSend, int toUAID);
int write(char *dataToSend, int fromUAID, int toUAID) – metoda
principală de scriere a mesajelor către destinatar.
int read(char *dataToReceive, int &fromUAID);
int read(char *dataToReceive, int &fromUAID, int &toUAID) – metoda
principală de citire a mesajelor trimise de către expeditor.
În continuare, voi detalia mecanismul de scriere și citire a mesajelor. Funcția write
primește ca argument un pointer de tip char, reprezentând mesajul ce trebuie trimis, și două
argumente, id-ul plăcuței Arduino de unde se trimite mesajul și id-ul plăcuței Arduino către
care este destinat mesajul. Funcția returnează o valoarea de tip int, ce reprezintă, în caz că
30
scrierea a fost realizată cu succes, numărul de byți scriși, altfel un cod de tip Error, descris în
subcapitolul anterior, Clasa Error.
Implementarea funcției presupune segmentarea mesajului din dataToSend, în bucăți
de câte 16 byți, și popularea unui obiect de tip UdpPacket cu informațiile necesare. Spre
exemplu dacă funcția se apelează în acest mod: write(„Ana are mere si Ion pere”,
1234, 5678); deoarece lungimea mesajului strlen(„Ana are mere si Ion pere”) = 24 byți, se
vor transmite succesiv două pachete, unul de 16 byți și altul de 8 byți, de la expeditor către
destinatar. Forma pachetelor va fi următoarea:
UdpPaacket packet;
// Pachetul 0
packet.pSize = 49; // dimensiunea pachetului in byți
packet.bLength = 16; // lungimea bData
packet.bNumber = 2; // numărul total de pachete
packet.bOffset = 0; // indexul pachetului curent
packet.checkSum1 = 232;
packet.checkSum2 = 21;
packet.fromUAID = 1234; // UAID-ul expeditorului
packet.toUAID = 5678; // UAID-ul destinatarului
char *bData = „Ana are mere si ; // bloc de 16 byți din mesajul inițial
// Pachetul 1
packet.pSize = 49; // dimensiunea pachetului in byți
packet.bLength = 8; // lungimea bData
packet.bNumber = 2; // numărul total de pachete
packet.bOffset = 1; // indexul pachetului curent
packet.checkSum1 = 153;
packet.checkSum2 = 89;
packet.fromUAID = 1234; // UAID-ul expeditorului
packet.toUAID = 5678; // UAID-ul destinatarului
char *bData = „Ion pere ; // bloc de 16 byți din mesajul inițial
După popularea unui astfel de pachet, acesta va fi formatat și trimis prin portul serial
către destinatar. Formatarea presupune crearea unui string prin concatenarea elementelor
pachetului packet separate printr-un caracter special. Caracterul special ales de mine este
caracterul de control \f, care nu reprezintă un caracter text (ce poate fi scris de la tastatură).
Așadar în urma concatenării elementelor, stringul ce va fi transmis arată în felul următor.
Pentru lizibilitate, am înlocuit aici caracterul de control \f, cu caracterul text , (virgulă).
49,16,2,0,232,21,1234,5678,Ana are mere si // Pachetul 0 formatat
49,8,2,1,153,89,1234,5678,,,,,,,,,,,,,,,,Ion pere // Pachetul 1 formatat
În cazul în care lungimea blocului extras din dataToSend, este mai mică de 16 byți,
atunci diferența va fi suplimentată prin adăugare de caractere de control, astfel încât lungimea
totală să fie persistentă la toate pachetele. Am decis să procedez în acest fel deoarece funcția
31
read va extrage din stringul formatat datele necesare pe baza caracterului special și va popula
un obiect de tip UdpPacket, acest proces nefiind unul complex de implementat. Așadar,
funcția read se va ocupa de primirea pachetelor formatate în acest mod, și de reansamblarea
lor.
În acest fel, trasferul de date are loc între destinatar și expeditor, prin împărțirea
pachetelor în blocuri de dimensiune fixă, oferind astfel viteză de transmitere și siguranța
integrității datelor.
2.1.6Clasa TcpConnectionPacket
Clasa TcpConnectionPacket este similară cu clasele UdpPacket și TcpPacket.
Aceasta conține membri necesari procesului de conectare între plăcuțele Arduino, descris în
primul capitol al aceste lucrări, Abordare teoretică . Totodată, această clasă este folosită
pentru trimiterea / primirea confirmării faptului că pachetul de tip TcpPacket a fost primit cu
succes. Membrii clasei sunt:
int syn;
int seq; // /ACK:0, SYN:1 CON:2 FIN:3 ERR:9
int ack;
enum blockInformation {
BLOCK_SIZE = 12,
};
2.1.7Clasa TcpPacket
Clasa TcpPacket se aseamănă ca rol și funcționalitate cu clasa UdpPacket și este
folosită pentru modul de comunicare cu confirmare. Diferențele între cele două clase sunt
lipsta campului fromUAID, deoarece acesta este deja cunoscut de către destinatar în urma
procesului de conectare, dar și mărirea lungimii corpului pachetului cu 1 byte, din cauza
adăugării celor 8 biți redundanți ai algoritmului de detecție și corectare de erori prin distanța
Hamming.
Schema clasei TcpPacket cât și membrii acesteia se găsesc în Tabela 8:
32
int pSize; // dimensiunea pachetului in byți
int bLength; // lungimea bData
int bNumber; // numărul total de pachete
int bOffset; // indexul pachetului curent
int checkSum1;
int checkSum2;
int UAID; // UAID-ul destinatarului
char *bData; // bloc de 16 byți din mesajul inițial
enum blockInformation {
BLOCK_SIZE = 46,
BLOCK_HEADER_SIZE = 29,
BLOCK_BODY_SIZE = 17
};
Tabela 8 – Schema pachetului TcpPacket
2.1.8Clasa TcpProtocol
Clasa TcpProtocol, ce moștenește proprietățile clase ArduinoSerialCom ,
implementează modul de comunicare cu confirmarea, ce reprezintă cea mai mare parte a
funcționalității acestei biblioteci. Principalele mecanisme implementate aici sunt cel de
interconectare între plăcuțe în trei pași, scriere și citire a pachetelor cu confirmare de primire,
dar și cel de încheiere a conexiunii între cele două părți. Voi începe cu structura clasei, apoi
voi exemplifica și câteva idei din implementările metodelor acestei clase.
Metodele de tip public ale acestei clase sunt:
int listen() – metodă folosită de către plăcuța Arduino, ce va juca rol de server
în acest protocol de comunicare ; valoarea returnată va fi UAID-ul plăcuței care
s-a conectat;
int connect() – metodă folosită de către plăcuță Arduino, ce va juca rol de
client, pentru a se conecta către alt Arduino ce va juca rol de server ;
int write(char *dataToSend, int UAID);
int read(char *dataToReceive, int &UAID);
int clientClose() – metodă folosită de către client pentru a notifica serverul că
conexiunea va lua sfârșit ;
int serverClose() – metodă folosită de server pentru a asculta notificarea
clientului de sfârșire a conexiunii ;
33
Membrii principali de tip protected ai acestei clase sunt:
TcpPacket packetRead;
TcpPacket packetWrite;
TcpConnection packetConnectionRead;
TcpConnection packetConnectionWrite;
char **orderedPackets – o matrice ce va fi folosită pentru salvarea
mesajelor extrase din pachete, și ordonarea acestora ;
char *dataSendEncodedString – stringul rezultat în urma codificării prin
algoritmul lui Hamming ;
void encodeWithHammingDistanceCode(char *dataSendString);
void decodeWithHammingDistanceCode(char*
dataSendEncodedString);
void baseTwoToChar(int *bits, int length, char *str);
void charToBaseTwo(char *str, int *bits);
bool isPowerOfTwo(int number);
Procesul de creare a pachetelor și trimitere a acestora de la destinatar către expeditor
este identic ca în cazul implementării descrise în subcapitolul Clasa UdpProtocol, singurele
diferențe fiind codificarea câmpului bData, reordonarea pachetelor trimise la destinatar și
mecanismul de confirmare de primire a pachetelor cu succes.
Funcția write primește ca argument un pointer de tip char, reprezentând mesajul ce
trebuie trimis, și id-ul plăcuței Arduino către care este destinat mesajul. Funcția returnează o
valoarea de tip int, ce reprezintă, în caz că scrierea a fost realizată cu succes, numărul de byți
scriși, altfel un cod de tip Error, descris în subcapitolul anterior, Clasa Error. În această,
funcție, este implementat mecanismul de trimite cu confirmare a pachetelor. Acesta
funcționează în felul următor: se trimite un pachet către destinatar iar destinatarul în caz că a
reușit să-l decodeze cu succes prin algoritmul cu distanțe al lui Hamming, trimite înapoi un
pachet de tip conexiune ce va conține numărul pachetului decodat plus unu. Astfel,
expeditorul va putea ști dacă destinatarul a primit pachetul corect, și va putea să transmită
următorul pachet, sau în caz contrar, îl va trimite din nou pe același.
În continuare, în Tabela 9, vă voi arătă fragmente din codul ce reprezintă mecanismul
de trimitere cu confirmare a pachetelor.
SENT_ACK = false;
while (SENT_ACK == false) {
softwareSerial->write(packet, strlen(packet));
waitRead();
softwareSerial_readBytes(bDataConnection, bDataConnectionLength);
34
formatReceiveConnectionData(bDataConnection);
if (packetConnectionRead.ack == packetWrite.bOffset + 1
&& packetConnectionRead.syn == 1) { // daca am primit confirmarea
SENT_ACK = true; // trecem la pachetul următor, altfel rămânem in
bucla while și trimitem din nou același pachet.
free(packetWrite.bData);
}
}
Tabela 9 – Mecanismul de confirmare al mesajelor transmise
Funcția read, pe lângă decodarea pachetului cu ajutorul algoritmului cu distanțe al lui
Hamming, mai are și rolul de a reordona pachetele. Pentru aceasta, am folosit o matrice
orderedPackets, în care voi insera fiecare string bData din pachet pe poziția bOffset. Astfel,
evităm folosirea unui algoritm de sortarea, aceasta realizându-se în mod natural. Matricea
orderedPackets se alocă în mod dinamic în momentul primirii primului pachet, în funcție de
lungimea acestuia. În Tabela 10, se află codul de alocare dinamică și de salvare a datelor în
matrice.
if (packetRead.bOffset == 0) {
orderedPackets = (char **)malloc(sizeof(char *) * packetRead.bNumber);
for (int index = 0; index < packetRead.bNumber; index++) {
orderedPackets[index] = (char *)malloc(sizeof(char *) *
packetRead.bLength + 1);
memset(orderedPackets[index], '\0', sizeof(char) * packetRead.bLength
+ 1);
}
}
strcpy(orderedPackets[packetRead.bOffset], packetRead.bData);
Tabela 10 – Alocare dinamică și folosirea matricei orderedPackets
2.2 Gestionarea memoriei
Gestionarea memoriei în cadrul oricărei biblioteci Arduino are un rol foarte
important, deoarce așa cum am prezentat în primul capitol al acestei lucrări, capacitatea
memoria utilizabilă SRAM este foarte redusă, de doar 2KB. Din cauza acestei limitări,
bibilioteca Arduino trebuie foarte bine optimizată din punctul de vedere al memoriei,
deoarce cei 2KB sunt împărțiți între alocări statice de date, alocările dinamice ( Heap) dar
și alocările variabilelor locale la nivelul funcțiilor ( Stack). Majoritatea problemelor de
memorie apar atunci când memoriile utilizate de Stack și Heap încep să se suprapună,
astfel apârând conflicte și eventual erori în comportamentul programului. În cazul
alocărilor dinamice de memorie și a dealocării acesteia când nu mai avem nevoie de ea,
acel spațiu utilizat nu poate fi refolosit de către Stack, și uneori, datorită fragmentării prea
mari a acesteia nu poate fi refolosit nici de Heap. Pentru a rezolva această problemă de
35
suprapunere, există diferite metode prin care se poate reduce considerabil memoria
SRAM:
ștergerea fragmentelor de cod nefolosite: funcții, variabile, bucăți de cod care
nu vor fi niciodată executate;
eliminarea codului duplicat și utilizarea funcțiilor generale care să
îndeplinească multiple funcționalități;
folosirea macroului F() prin care mutăm în memoria Flash PROGMEM,
variabilele statice din memoria SRAM pentru că nu le vom rescrie niciodată,
așadar reprezintă o risipă de memorie SRAM.
alocările vectorilor să nu ocupe mai multă memorie decât este necesar
înlocuirea variabilelor supradimensionate cu cele de dimenisune potrivită ( de
exemplu variabilele int care ocupă 4 byți să fie înlocuite cu byte sau uint8_t
ce ocupă 1 byte), acolo unde este cazul.
În cadrul dezvoltării acestei biblioteci, deoarece am folosit algoritmi care au avut
nevoie de memorie, de exemplu Algoritmul de detecție și corectare de erori prin distanța
Hamming, am încercat pe cât de mult posibil să respect indicațiile de mai sus și astfel să
obțin un cod cât mai optimizat. În marea majoritate a codului, am folosit alocarea dinamică a
unei resurse cu ajutorul metodelor malloc și memset, iar după ce am terminat utilizarea acelei
resurse, am eliberat-o prin metoda free.
În Figura 3 de la pagina 36 , se află un exemplu de memorie utilizată de un program ce
implementează biblioteca TwoArduinoSerialCom în modul de comunicare cu confirmare și
care joacă rol de server, având două instanțe obiect ale clasei TcpProtocol, adică doi clienți ce
se vor conecta. Observăm că sketchul nostru împreună cu biblioteca utilizează 1082 byți din
2048 byți disponibili, însă, în acest sketch a fost declarată o variabilă char
dataToReceive[300]; care ocupă 300 byți.
Figura 3 – Memoria utilizată de un sketch TwoArduinoSerialCom
În concluzie, dimensiunea bibliotecii ocupă în medie, în jur de 40-50% din memoria
SRAM a plăcuței Arduino, ceea ce din punctul meu de vedere este un procentaj bun, lâsând
spațiu de flexibilitate și utilizatorului pentru implementarea funcționalităților proprii.
36
2.3 Arhitectura proiectului – Diagrama UML
În Figura 4 găsim principalele clase ale acestei biblioteci: UdpProtocol și
TcpProtocol ce moștenesc proprietățile clasei de bază ArduinoSerialCom .
Figura 4 – Schema UML a claselor principale
În se află structura celorlalte clase din bibliotecă: Udpacket, TcpPacket, Connection, Error,
TcpConnection. Aceste clase sunt utilizate în cadrul claselor din Figura 4.
37
38
3Concluziile lucrării
Ma laud, de ce am facut bine, ce am facut bine, ce as putea imbunatati, ce n-am facut
bine, o comparatie cu alte chestii existente vezi Introducere
Ti-au testat limitele de programator ca a trebuit sa scrii eficient codul si sa ai tot timpul
grija la memory management ( asta daca reusesc sa fac code refactoring si sa minimizez
folosirea memoriei)
2 pagini
39
Bibliografie
carti articole aici pot pune protocoalele UDP TCP: ISO-XXXX and shit ISO TLS
PROTOCOL ISO ISO ISOOOOO
40
Anexe
Anexă 1
Exemplu funcțional server – client
41
Copyright Notice
© Licențiada.org respectă drepturile de proprietate intelectuală și așteaptă ca toți utilizatorii să facă același lucru. Dacă consideri că un conținut de pe site încalcă drepturile tale de autor, te rugăm să trimiți o notificare DMCA.
Acest articol: Protocol de comunicare serială, între plăcuțe de [630356] (ID: 630356)
Dacă considerați că acest conținut vă încalcă drepturile de autor, vă rugăm să depuneți o cerere pe pagina noastră Copyright Takedown.
