Binárne dáta cez UART
Už dlhšiu dobu sa pohrávam s myšlienkou vybudovať sieť bezdrôtových senzorov. Mám dokonca v zásobe aj „vysoko-úrovňové” moduly HC-11 a RFM-69, no najprv som sa rozhodol naučiť sa posielať dáta po sériovej linke (HC-11 používa UART) v binárnej podobe. Prečo v binárnej uvediem na konci.
Obsah článku
Moja predstava je posielať na centrálny uzol (asi Orange Pi) hodnoty z viacerých senzorov (teplota, vlhkosť, intenzitu svetla a pohyb). Môžem samozrejme posielať dáta z každej veličiny samostatne, no chcem ich posielať ako jednu dátovú štruktúru, a tak som sa pustil do trochy experimentovania.
V AVR
Vytvoril som dátovú štruktúru, ktorá v sebe zapúzdruje hodnoty všetkých
snímačov. Štruktúru som definoval pomocou atribútu packed
, aby kopmilátor
nezarovnával jej polia (on to asi robí predvolene, ale istota je istota):
- 1 B, celé so znamienkom
- 2 B, celé so znamienkom
- 1 B, celé bez znamienka
- 4 B, desatinné
// packed structure
typedef struct __attribute__ ((packed))
{
int8_t prva; // 1 B
int16_t druha; // 2 B
uint8_t tretia; // 1 B
float stvrta; // 4 B
} TMyStruct;
V programe som potom deklaroval premennú typu tejto štruktúry a priradil nejaké hodnoty (aby som mohol skontrolovať prenos):
// declare variable
TMyStruct mystruct;
// add some values
mystruct.prva = 21;
mystruct.druha = 11999;
mystruct.tretia = 255;
mystruct.stvrta = 27.35456;
Najprv som chcel poslať jednu položku štruktúry za druhou:
Serial.write(mystruct.prva);
Serial.write(mystruct.druha);
Serial.write(mystruct.tretia);
Serial.write(mystruct.stvrta);
Tento pokus samozrejme skončil fiaskom, zlyhal už pri kompilácii pri
posielaní hodnoty float
.
Tak som skúsil odobrať float
celkom a tentokrát kompilácia prebehla OK,
ale výsledok bol opäť fiaskom, pretože odoslané boli len 3 bajty namiesto 4 –
jednoducho funkcia Serial.write()
dokáže takto posielať len jeden bajt.
Potom som skúsil iný formát funkcie Serial.write()
, kde je posielaný bufer
a je nutné zadať jeho dĺžku:
Serial.write(mystruct.prva);
Serial.write( (const uint8_t *) &mystruct.druha, sizeof(mystruct.druha) );
Serial.write(mystruct.tretia);
Serial.write( (const uint8_t *) &mystruct.stvrta, sizeof(mystruct.stvrta) );
Tentokrát už všetko prebehlo ako malo a v počítači som dostal odoslané hodnoty, no uznajte sami, že to vyzerá nepekne a je to dlhé. Preto som ako ďalší krok vyskúšal podobným spôsobom odoslať celú štruktúru naraz:
Serial.write((const uint8_t*) &my_struct, sizeof(my_struct));
A svet div sa! Ono to funguje.
Teraz by som mohol pomerne jednoducho definovať funkciu pre konkrétny typ štruktúry, ktorá ju týmto spôsobom odošle, ale chcel som vytvoriť univerzálnu funkciu a zároveň otestovať použitie šablóny C++, a tak som skúsil vytvoriť funkciu. Hneď ako som pochopil, že definícia šablóny i funkcie musí byť na jednom riadku to pekne fungovalo:
// sends arbitrary data over Serial in binary form
template <typename T> void sendData (const T data)
{
// send data
Serial.write( (const uint8_t *) &data, sizeof(data) );
}
Samozrejme, chcem na začiatku poslať dĺžku štruktúry, aby bol kód na strane prijímača aspoň trochu univerzálna, no teraz už stačí jednoduchá úprava funkcie:
// sends arbitrary data over Serial in binary form
template <typename T> void sendData (const T data)
{
uint8_t size;
size = sizeof(data);
// send message length
Serial.write(size);
// send data
Serial.write( (const uint8_t *) &data, size );
}
S výsledkom som spokojný. Je to pomerne jednoduché riešenie, ktoré možno umiestniť do knižnice. Jedinou nevýhodou je, že to nie je dostatočne univerzálne, pretože funkcia neposiela informácie o type jednotlivých zložiek, to však nebolo mojim cieľom, pretože posielanie formátu takto krátkej dátovej štruktúry by ju zbytočne predĺžilo a celá táto operácia by stratila zmysel.
V počítači
Ostáva strana prijímača (teda počítača). Tu je potrebné správu prijať a potom
znova rozložiť na jednotlivé položky. V jazyku Python to vlastne vôbec nie je
problém, len to chcelo trochu trpezlivosti. Na pripojenie k sériovému portu poslúži
knižnica serial
a na rozbalenie štruktúry zase knižnica struct
.
S knižnicou serial
som už viackrát pracoval, takže táto časť bola rýchlo hotová
a najprv som čítal dĺžku odoslanej štruktúry, aby som si overil, že je poslaná celá:
import serial
from struct import unpack, calcsize
SERPORT = "/dev/ttyACM0"
with serial.Serial(SERPORT, baudrate=9600) as ser:
length = ser.read(1)[0]
print (length)
Pretože je dĺžka posielaná ako 1 B (tak som sa rozhodol), prosto čítam prvý bajt
a používam prvú hodnotu bajtového reťazca (bytes
).
Potom už len nastaviť časový limit a prečítať zadaný počet bajtov správy:
import serial
from struct import unpack, calcsize
SERPORT = "/dev/ttyACM0"
with serial.Serial(SERPORT, baudrate=9600) as ser:
length = ser.read(1)[0]
print (length)
ser.timeout = 1
packet = ser.read(length)
print ( "%d: %s" % (len(packet), packet) )
print ( unpack(">bhBf", packet) )
Celá veda spočíva vo funkcii struct.unpack()
, ktorej je potrebné zadať
formát a zbalenú štruktúru dát.
Trochu mi dal zabrať formát. Nie celkom som rozumel popisu v dokumentácii, ale
pretože viem aké dáta posielam, rozhodol som sa skúsiť formát "bhBF"
,
lenže hneď som sa dozvedel, že funkcia unpack()
očakáva 12 bajtov, no ja posielam
len 8. Ako to? Jednoducho, do formátu som nezadal, aby neočakával zarovnané dáta,
čo predvolene robí. No dobre, na „nezarovnávanie” musím zvoliť jednu z troch možností:
- natívne (
=
) - little-endian (
<
) - big-endian (
>
)
Inými slovami, potrebujem zvoliť poradie bajtov viac-bajtových hodnôt. Super, aké poradie používa AVR neviem… Takže metóda pokus omyl:
Typ | prva | druha | tretia | stvrta |
---|---|---|---|---|
Odoslané | 21 | 11999 | 255 | 27.35456 |
natívne | 21 | 11999 | 255 | 27.35456085205078 |
LE (< ) |
21 | 11999 | 255 | 27.35456085205078 |
BE (> ) |
21 | -8402 | 255 | 9.317744246368058e-17 |
Tento malý experiment mi teda ukázal dve veci:
- môj počítač i AVR v Arduino používajú poradie little-endian
- desatinné číslo nie je celkom rovnaké ani pri správnom poradí bajtov
- poradie bajtov je dôležité len pri viac-bajtových hodnotách
Mohol by som teda použiť natívne poradie ("=bhBF"
), ale aby som nezávisel
na architektúre počítača, bude iste lepšie použiť priamo poradie AVR, tzn.
little-endian ("<bhBF"
). Ostáva problém s desatinnými číslami. Nie je
to nič, čo by ma prekvapilo (problém s presnosťou prevodov desatinných čísel je
známy) a asi to vyriešim veľmi jednoducho – budem používať len päť desatinných
miest:
>>> import math
>>> a = 27.35456085205078
>>> round (a, 5)
27.35456
Som úplne spokojný. Výsledný kód je síce prakticky nepoužiteľný, ale tento experiment mi pomohol pochopiť, ako posielať dáta sériovým kanálom v binárnej forme, ktorá je kratšia ako posielanie hodnôt v číselnej forme. Neveríte? Tak si zrátajte koľko znakov by zabrali čísla z príkladu. Mne vyšlo 18 (slovom osemnásť) znakov, a teda 18 bajtov, ale posielam ich len deväť (1 B dĺžky a 8 B správy). Poviete si, že na tom až tak nezáleží? Nesúhlasím. Plánujem napájať senzory z batérie, a tam záleží na každom ušetrenom mA (presnejšie mAs) a kratšia správa znamená kratšie vysielanie, a teda menšiu spotrebu, čiže dlhšiu životnosť batérie.
Kód tiež používa šablóny C++, a takéto riešenie som nikde inde nenašiel. Ostáva to asi celé zabaliť do triedy, ale to by už nemal byť veľký problém.