Search
Aplikaci lze implementovat rozličným způsobem a lze také začít úplně od začátku, avšak doporučený postup je využít řešení předešlého domácího úkolu HW 8, který obsahují komunikaci pomocí roury, vícevláknovou aplikaci a vyčítání klávesnice.
Doporučených postup při implementaci ovládací aplikace může být následující:
'a
EV_ABORT
MSG_ABORT
INFO
DEBUG
WARN
ERROR
xwin_init()
xwin_close()
xwin_redraw()
compute()
uint8_t compute(double cx, double cy, double px, double py, uint8_t max_iteration);
'q
Pokud to bude možné konzultujte svůj postup s řešením HW9 se svým cvičícím na cvičení. (Jsou k tomu vyhrazena cvičení 11 a 12.) Zdrojové kódy včas odevzdejte do odevzdávacího systému a domluvte si s cvičícím termín časného odevdzání odevzdání, abyste mohli zapracovat zpětnou vazbu do úkolu.
Použití vláken není vyloženě nutné, nicméně je velmi doporučené, protože umožňuje organizovat a zpřehlednit kód. V každém vláknu je možné se soustředit na konkrétní části odpovídající obsluze zdroje asynchroních zpráv, případně vizualizaci. Lze využít realizovaného programu HW 8 - Interaktivní aplikace s meziprocesorou komunikací, který v zásadě odpovídá ukázce na 8. přednášce.
Například lze realizovat vlákna pro každý zdroj asynchronních události, které jsou v aplikaci v zásadě dva a to
Přirozeně se nabízí vytvořit jedno hlavní vlákno, ve kterém můžeme implementovat aplikační logiku a po jednom vláknu pro každý zdroj asynchronních události (vstup z klávesnice a vyčítání dat ze roury).
Jelikož chceme umožnit okamžité (nebo velmi rychlé) ukončení aplikace při stisku klávesy q, použijeme načítání dat ze roury, dostupné v modulu prg_io_nonblock.c.
q
prg_io_nonblock.c
Komunikace přes roury je v našem případě plně duplexní, proto můžeme bezpečně v jednom vláknu pouze číst ze roury a v jiném vláknu zapisovat. Určitě je dobré číst vstup pouze v jediném vláknu (platí jak pro rouru, tak pro klávesnici). Z hlediska zápisu do roury je obecně vhodné též přistupovat k zápisu z jediného vlákna a to zejména z důvodu zapsání kompletní zprávy. Proto je nutné v přístupu k rouře z více vláken využít kritické sekce (tj. exkluzivní přístup řízený mutexem). V našem případě je plně postačující zapisovat do roury například pouze z hlavního vlákna, které řeší kompletní aplikační logiku programu.
Komunikaci mezi vlákny lze realizovat opět různými způsoby. Nicméně v případě hlavního vlákna a dvou vláken pro vyčítání interaktivního uživatelského vstupu z klávesnice a čtení dat ze roury probíhá komunikace pouze směrem od čtecích vláken do hlavního vlákna. Proto hlavní vlákno realizujeme jako smyčku, ve které zpracováváme zprávy z fronty, do které zapisují zprávy vyčítací vlákna. Hlavní vlákno reaguje na uživatelský vstup vytvořením příslušené zprávy pro Modul aplikaci nebo výpisem či aktualizací grafického výstupu po zpracování zpráv od Modulu.
Komunikace mezi vlákny probíhá pomocí fronty zpráv s rozhraním popsaným v event_queue.h, který implementačně (event_queue.c) odpovídá např. kruhové frontě realizované v HW 6 - Kruhová fronta v poli. Hlavní rozdíl je v přístupu k datové struktuře z více vláken. Frontu používáme ve stylu producent/konzument, kdy hlavní vlákno je konzumentem a vyčítací vlákna produkují zprávy. Proto je nutné zajistit exkluzivní přístup do fronty prostřednictvím kritické sekce (mutexu).
event_queue.h
event_queue.c
V případě, že je fronta prázdná, očekáváme, že konzument pozastaví svou činnost do doby, než se objeví nová zpráv, tak abychom zbytečně nevyužívali výpočetní zdroje. Vynucené ukončení od uživatele v tomto případě pošle zprávu, proto nepředstavuje žádný problém. V opačném případě, kdy konzument nestíhá zpracovat zprávy a fronta je plná, je nutné pozastavit činnost konzumentů a v případě zpracování zprávy (uvolnění místa ve frontě) je vhodné takového konzumenta znovou aktivovat. Proto použijeme podmíněné proměnné a mutex. Obecně očekáváme, že hlavní vlákno je dostatečně rychlé a k přeplnění by nemělo docházat často, případně jen nárazově. Podle toho volíme velikost fronty.
Vlastní zprávy ve frontě lze realizovat různě, od jednoduchého ukazatele na typ void až například po samostatné typy zpráv ve výčtovém typu a rozlišení zdroje zprávy jak je použito v dostupném souboru event_queue.h.
void
Konkrétně v event_queue.h definujeme dva zdroje:
typedef enum { EV_NUCLEO, EV_KEYBOARD, EV_NUM } event_source;
a pro každý zdroj zprávy definujeme samostatný obsah zprávy strukturami
typedef struct { int param; } event_keyboard; typedef struct { message *msg; } event_serial;
Tedy pro vstup z klávesnice máme pouze datovou část v podobně int hodnoty. V případě zprávy ze sériového portu používáme přímo datovou strukturu message, kterou předáváme jako ukazatel, abychom se vyhnuli zbytečnému kopírování kompletní zprávy, která může být několik (desítek) bajtů dlouhá.
int
message
Samotnou zprávu pak definujeme jako zdroje zprávy (event_source) a typ zpráv (event_type), které jsou následně doplněny o vlastní zprávu a to buď struct event_keyboard nebo struct event_serial podle toho jestli je typ zprávy EV_KEYBOARD nebo EV_NUCLEO. Proto zde s výhodou použijeme typ union.
event_source
event_type
struct event_keyboard
struct event_serial
EV_KEYBOARD
EV_NUCLEO
union
typedef struct { event_source source; event_type type; union { int param; message *msg; } data; } event;
Položka data struktury event tak obsahuje param i msg. To zdali je v proměnné typu event zpráva od klávesnice (param) nebo ze sériového portu (msg) rozlišíme při vytváření zprávy nastavením příslušné datové části a typu zprávy. Příklad použití může vypadat například
data
event
param
msg
void process_event(event *ev) { if (ev->type == EV_KEYBOARD) { printf("Param value %\d\n", ev->data.param); } else if (ev->type == EV_NUCLEO) { printf("Msg ptr %p\n", ev->data.msg); } }
Komunikace proudem dat je zápis a čtení posloupností bajtů, které jsou organizovány do zpráv. Ve zvoleném případě zpráv se jedná o zprávy definovaných (fixních) délek. Zcela jistě lze přistupovat ke zprávám jako posloupnosti bajtů a k jednolivým částech přistupovat prostřednictvím indexů prvků. Takový přístup však není příliš čitelný, neboť vyžaduje detailní znalost organizace jednotlivých zpráv. Proto může být výhodnější definovat si pro každý typ zprávy samostatnou strukturu tak, jak je to provedeno v přiloženém souboru messages.h. Tím získáme relativně čitelný přístup na dílčí položky jednotlivých zpráv. Komunikační zpráva pro Modul aplikaci pak odpovídá struktuře message, která obsahuje jednobajtový indikátor typu zprávy type a union možných dílčích zpráv
messages.h
type
typedef struct { uint8_t type; // message type union { msg_version version; msg_startup startup; msg_set_compute set_compute; msg_compute compute; msg_compute_data compute_data; } data; uint8_t cksum; // message command } message;
Kromě typu a dílčího obsahu zprávy v položce data dále zpráva obsahuje položku cksum, která představuje primitivní způsob jak kontrolovat konzistenci a správnost došlých bajtů. Hodnota cksum je spočítána tak, aby součet jednotlivých bajtů (uint8_t) zprávy včetně cksum byl 0xFF. Tedy pokud se stane nějaká chyba, je možné ji detekovat. Jedná se o základní detekčních mechanismus a v praktických úloha se zpravidla navrhují cycklické redundatní kódy podle charakteru možných chyb. V našem případě nám postačuje tento jednoduchý kontrolní součet cksum, který je součástí transformace struktury zprávy do pole bajtů, která se provádí ve funkci fill_message_buf() v modulu messages.c ve druhé části funkce.
cksum
uint8_t
fill_message_buf()
messages.c
// 2nd - send the message buffer if (ret) { // message recognized buf[0] = msg->type; buf[*len] = 0; // cksum for (int i = 0; i < *len; ++i) { buf[*len] += buf[i]; } buf[*len] = 255 - buf[*len]; // compute cksum *len += 1; // add cksum to buffer }
Se zprávou jako strukturou se z hlediska přístupu k jednotlivým položkám zprávy pracuje mnohem intuitivněji než indexování do pole bajtů. Nicméně přenos zprávy po sériové lince je přenos definovaného počtu bajtů odpovídající jednotlivým položkám. Převodu zprávy (objektu / datové struktury) na posloupnost bajtů se říká marshalling a v dostupném modulu messages.c je implementován ve funkci fill_message_buf(), která je jednou ze tří funkcí vyžadující detailní znalost organizace jendotlivých zpráv jako posloupnosti bajtů. Princip můžeme demonstrovat na zprávě MSG_VERSION, která je 5 bajtů dlouhá (typ zprávy, tři datové položky - major, minor, patch a cksum). Vyplnění prvního batju a posledního bajtu je pro všechny zprávy identické, a proto je součástí druhé části funkce uvedené výše. Vlastní datová část pak odpovída případu kdy msg→type je MSG_VERSION, např.
MSG_VERSION
msg→type
switch(msg->type) { ... case MSG_VERSION: buf[1] = msg->data.version.major; buf[2] = msg->data.version.minor; buf[3] = msg->data.version.patch; *len = 4; break; ... buf[0] = msg->type; ... buf[*len] = 255 - buf[*len];
data.version
Při čtení zprávy ze sériového portu očekáváme, že ja vstupní buffer prázdný a první znak, který přečteme, odpovídá typu zprávy. Na základě typu zprávy můžeme určit počet bajtů zprávy, které je nutné přečíst, abychom mohli vytvořit zprávu konkrétního typu jako proměnnou typu struct. V modulu messages.c je definice funkce get_message_size(), která vrací očekávanou délku zprávy podle typu prostřednictvím předáváného ukazatele len. Například pro zprávu typu MSG_VERSION může funkce vypadat
struct
get_message_size()
len
bool get_message_size(uint8_t msg_type, int *len) { bool ret = true; switch(msg_type) { ... case MSG_VERSION: *len = 2 + 3 * sizeof(uint8_t); // 2 + major, minor, patch break; ... default: ret = false; break; } return ret; }
*len = 5;
V okamžiku, že načteme očekávaný počet bajtů, můžeme pole bajtů interpretovat jako konkrétní zprávu. Příklad implementce je opět dostupný v modulu messages.c ve funkci parse_message_buf(), která může například pro MSG_VERSION vypadat následovně.
parse_message_buf()
bool parse_message_buf(const uint8_t *buf, int size, message *msg) { uint8_t cksum = 0; for (int i = 0; i < size; ++i) { cksum += buf[i]; } bool ret = false; int message_size; if ( size > 0 && cksum == 0xff && // sum of all bytes must be 255 ((msg->type = buf[0]) >= 0) && msg->type < MSG_NBR && get_message_size(msg->type, &message_size) && size == message_size) { ret = true; switch(msg->type) { ... case MSG_VERSION: msg->data.version.major = buf[1]; msg->data.version.minor = buf[2]; msg->data.version.patch = buf[3]; break; ... default: // unknown message type ret = false; break; } // end switch } return ret; }
false