V tejto časti si vytvoríme GUI klientsku aplikáciu v jazyku C#, ktorej základom bude rozhranie na komunikáciu medzi ovládačom zariadenia a .Net aplikáciou vytvorené s pomocou PInvoke (Platform Invoke).
Úvod
V tomto kroku máme funkčné USB zariadenie a ovládač s ním komunikujúci pomocou libusb. Ďalším krokom je vytvorenie koncovej aplikácie, pomocou ktorej môže používateľ hardvér ovládať. Pre zmenu som si vybral platformu .NET a jazyk C#, vďaka ktorým bude jednoduchšie demonštrovať princípy.
Aplikácia bude tvorená jedným oknom, v ktorom sa bude zobrazovať stav tlačidiel na zariadení, teplota procesora a napätie na potenciometri. Taktiež bude obsahovať prvky na ovládanie LED diód, na vypísanie textu a odoslanie bitmapy na LCD. Pri vytvorení okna sa aplikácia pokúsi otvoriť spojenie s USB zariadením; pokiaľ zariadenie nebude pripojené, skončí s chybovým hlásením.
Celú komunikáciu so zariadením vložíme do jedného objektu, ktorý bude pomocou P/Invoke (Platform Invoke) komunikovať s natívnou knižnicou ovládača vytvorenou v minulom dieli tohto seriálu. Tento objekt alebo wrapper sa bude taktiež starať o spracovanie vzniknutých chýb a namiesto číselných hodnôt vrátených ovládačom vyhodí príslušnú výnimku.
P/Invoke – Platform Invoke
Pokiaľ v .NET programujete už nejaký ten čas, je dosť možné, že ste sa stretli s potrebou importovať funkcie natívnych knižníc. V C/C++ by ste toho dosiahli jednoducho prilinkovaním knižnice k vášmu programu alebo by ste knižnicu načítali pomocou funkcie LoadLibrary, získali adresu funkcie volaním GetProcAddress a pretypovaním by vám vznikol odkaz na funkciu, ktorú by ste zavolali ako každú inú. V .Net to ale funguje trochu inak, na volanie funkcií v natívnych knižniciach musíme využiť DllImport (System.Runtime.InteropServices v knižnici System.Core).
V prvom kroku si v zdrojovom súbore zadeklarujeme použitie názvoslovia:
using System.Runtime.InteropServices;
Potom nám stačí v triede deklarovať statickú metódu nasledovným spôsobom:
/// <summary>
/// Vyčistí obrazovku jednou farbou
/// </summary>
/// <param name="color">Farba</param>
/// <returns></returns>
[DllImport("DeviceWrapper.dll",
CallingConvention = CallingConvention.Cdecl)]
private static extern uint ClearScreen(uint color);
Čím prekladač vie, že metóda ClearScreen je externá a nachádza sa v knižnici DeviceWrapper.dll (to je náš ovládač). Každá takto vložená funkcia musí byť označená ako static extern s atribútom DllImport.
Naša klientska aplikácia bude importovať všetky funkcie exportované v ovládači DeviceWrapper, nebudú sa ale používať priamo. Napr. funkcia ClearScreen preberá jeden argument, ktorým je 24bitové číslo reprezentujúce farbu. Návratová hodnota je taktiež číslo (0 v prípade, že funkcia uspeje alebo iná hodnota popisujúca chybu). My ale túto metódu zabalíme v objekte (wrapper) tak, aby parameter farby nebol číslo, ale typ System.Color, a odstránime návratový typ, zmeníme ju na void. V prípade, že volanie funkcie ClearScreen z knižnice DeviceWrapper.dll zlyhá, nevrátime volajúcemu kód chyby, ale vyhodíme výnimku. Je to omnoho prijateľnejší spôsob. Týmto spôsobom dosiahneme to, aby bola metóda ClearScreen volaná vždy v správnom kontexte.
/// <summary>
/// Vyčistí obrazovku
/// </summary>
/// <param name="color">Farba</param>
public void ClearScreen(Color color)
{
// Spojenie so zariadením musí byť otvorené
if (!isOpen)
throw new DeviceFailException(
DeviceFailException.DeviceError.NotOpen);
// Zavolať funkciu ClearScreen z DeviceWrapper.dll
uint result = ClearScreen((uint)color.ToArgb());
// Pokiaľ volanie zlyhalo, vyhodíme výnimku s kódom chyby
if (result != 0)
throw new DeviceFailException(
(DeviceFailException.DeviceError)result);
}
Rozhranie na komunikáciu s ovládačom
Základ aplikácie bude tvorený jedným objektom – rozhraním na komunikáciu s ovládačom. Pri štarte aplikácie sa vytvorí inštancia tohto objektu, otvorí sa spojenie s USB zariadením a ďalšia komunikácia s ním už bude prebiehať iba pomocou tohto objektu. Pri ukončení aplikácie sa objekt odstráni, tým sa zatvorí spojenie s ovládačom a zariadením. Na obrázku je znázornené, aké metódy bude objekt obsahovať a ďalšie pomocné štruktúry, triedy a dátové typy.
Je dôležité uvedomiť si, že dátové štruktúry (pakety) sú rovnaké pre zariadenie, ovládač aj koncovú aplikáciu. Preto pri vývoji ovládača pracujeme s rovnakými dátovými typmi v zariadení aj na strane hosta. Táto klientska aplikácia definuje štyri takéto štruktúry, a to usbapp_bulk_out_t, usbapp_control_in_t, usbapp_control_out_t a usbapp_interrupt_in_t.
Ďalej potrebujeme definovať výnimky, ktoré objekt vyhodí pri chybnej komunikácii alebo pri nesprávnom použití rozhrania. Na to nám budú slúžiť dve triedy, DeviceFailException, ktorá bude vyhodená, ak dôjde k chybe v ovládači alebo zariadení, a DeviceNotOpenException, ktorú rozhranie vyhodí, ak sa programu nepodarí nadviazať spojenie so zariadením.
Nakoniec zadefinujeme štruktúry a enumy, ktoré poskytnú informácie z paketov cieľovému používateľovi rozhrania. Ako príklad uvediem nastavenie LED a LCD.
Aplikácia potrebuje odoslať zariadeniu príkaz, aby rozsvietilo modrú a zelenú LED diódu, zaplo LCD displej a taktiež aby posunulo obsah LCD displeja o 50 px. Keďže každé nastavenie v pakete je číselné, užívateľ nášho rozhrania by musel poznať, aké čísla pripadajú na modrú a zelenú LED. Taktiež by musel vedieť, akým príznakom zapne LCD displej. Preto je dôležité, aby naše rozhranie tieto čísla skrylo za enum dátové typy ako napr. zoznam LED diód a ich číselné hodnoty:
/// <summary>
/// Nastavenia LED diód
/// </summary>
public enum Led
{
None = 0,
Blue = 0x1,
Green = 0x2,
Amber = 0x4
};
Potom môže užívateľ jednoduchým spôsobom zostaviť nastavenia a odoslať ich zariadeniu:
// Vytvoriť nastavenie LED a LCD
Device.LedLcdOptions ledLcd = new Device.LedLcdOptions(
Device.Led.Blue | Device.Led.Green, // Modrá a zelená LED
0, // Oranžová nebude svietiť
Device.DisplayOption.DisplayOn, // Zapnúť LCD displej
0, // Displej nebude podsvietený
50); // Obsah displeja bude posunutý o 50 px
// Odoslať nastavenie zariadeniu
device.SetupLedLcd(ledLcd);
Metóda Device.SetupLedLcd(LedLcdOptions options) skontroluje, či je spojenie na zariadenie otvorené a ak áno, zostaví paket usbapp_control_out_t z nastavení, ktoré užívateľ predal ako argument options. Zároveň skontroluje platnosť nastavení a to, či hodnota PWM a hodnota podsvietenia LCD displeja neprekročia max. hodnoty stanovené USB zariadením. Následne paket posunie ovládaču volaním funkcie SetupDevice z knižnice ovládača DeviceWrapper.dll.
Tá vytvorí USB paket, jeho dáta naplní obsahom štruktúry usbapp_control_out_t a odošle ho zariadeniu, ktoré paket prijme a spracuje. Akonáhle host potvrdí príjem paketu zariadením, funkcia SetupDevice vráti hodnotu určujúcu, či prenos prebehol v poriadku alebo sa vyskytla chyba.
Naše rozhranie túto vrátenú hodnotu porovná a pokiaľ zistí, že počas prenosu nastala chyba, vyhodí výnimku.
/// <summary>
/// Nastavi LED a LCD
/// </summary>
/// <param name="options">LED a LCD nastavenia</param>
public void SetupLedLcd(LedLcdOptions options)
{
if (!isOpen)
throw new DeviceFailException(
DeviceFailException.DeviceError.NotOpen);
// Vytvoriť paket obsahujúci nastavenia LED a LCD
usbapp_control_out_t data = new usbapp_control_out_t();
data.leds = options.Leds;
data.pwmLedDuty = options.PwmLedDuty > MaxPwm ?
MaxPwm : options.PwmLedDuty;
data.display = options.LcdOptions;
data.backlight = options.Backlight > MaxBacklight ?
MaxBacklight : options.Backlight;
data.displayScroll = options.LcdScroll;
// Posunúť paket ovládaču, ktorý ho odošle
uint result = SetupDevice(ref data);
// Vyhodiť výnimku ak počas prenosu vznikla chyba
if (result != 0)
throw new DeviceFailException(
(DeviceFailException.DeviceError)result);
}
Obdobným spôsobom trieda Device poskytuje prístup k ostatným funkciám ovládača. V princípe je potrebné iba „zabaliť“ volania ovládača takým spôsobom, aby klientska aplikácia na platforme .NET mohla využívať tieto metódy v súlade s konvenciami .NET a taktiež aby bolo možné spracúvať výnimky namiesto chybových kódov.
Front end – používateľské rozhranie
Na otestovanie funkčnosti poslúži jednoduchá GUI aplikácia, ktorá pri svojom štarte skontroluje, či je k počítaču pripojené naše zariadenia a ak áno, vytvorí klientske rozhranie pre komunikáciu s týmto zariadením. Používateľ tak bude mať možnosť jednoducho odosielať príkazy zariadeniu pomocou nami vytvoreného rozhrania.
Komunikácia bude pozostávať z troch častí:
- Príkazy odosielané klientom do zariadenia – nastavenie LED a vykresľovanie na LCD
- Pravidelná aktualizácia hodnôt z ADC prevodníka
- Vlákno bežiace na pozadí aplikácie čakajúce na prerušenia zo strany zariadenia
Keďže väčšinu práce bude mať na starosti rozhranie v triede Device, GUI aplikácii zostáva konvertovať vstupné hodnoty od používateľa a zobrazovať výstup zo zariadenia a čo je podstatnejšie, spracovanie chýb. V našom prípade pri vzniku chyby program zobrazí bližšie informácie o príčine vzniku chyby a následne sa ukončí.
Záver
A na záver si môžete stiahnuť zdrojový kód aplikácie a pozrieť ukážku toho, ako zariadenie a klientska aplikácia fungujú v praxi: