Kategória: Python

Zmenené: 1. január 2017

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. 1 B, celé so znamienkom
  2. 2 B, celé so znamienkom
  3. 1 B, celé bez znamienka
  4. 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:

  1. môj počítač i AVR v Arduino používajú poradie little-endian
  2. desatinné číslo nie je celkom rovnaké ani pri správnom poradí bajtov
  3. 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.