W poprzednim poście zapoczątkowałem artykuł odnośnie oprogramowania panelu LED, którą zakupiłem jakiś czas temu. W dzisiejszym poście spróbujemy rozkodować cześć dotyczącą wysyłania informacji do płytki. Spróbujemy poznać protokół jaki obsługuje nasz kawałek hardware’u. Na początek jednak jestem winny wam trochę wyjaśnienia.

W ostatnim wpisie zdekodowaliśmy sobie kawałek procedury, która odpowiada za wykrycie portu do którego podłączona jest płytka, ale tak na prawdę nie pokazałem, gdzie na UI jest uruchamiana dana funkcja. Porównajcie sobie obrazek z poprzedniego wpisu i poniższy.

Tak, tak. Okazuje się, że UI jest dość specyficznie napisane i jego część jest ukryta (kto wie po co?). Po rozwinięciu okna ukazuje się jeszcze kilka przycisków w tym jeden (Initial Badge), którego procedurę zanalizowaliśmy ostatnim razem. Ciekawy sposób na interfejsu użytkownika.

Dziś zajmiemy się drugą funkcją, która faktycznie powoduje zapis do urządzenia tekstu. Wywoływana jest po naciśnięciu przycisku Send.

Patrząc na UI widzimy, że panel umożliwia wpisanie do siebie 6 tekstów, które będą wyświetlane a dodatkowo w 7 i 8 “kanale” możemy umieścić logo (te sobie rozpracujemy w którymś z kolejnych wpisów). Dodatkowo możemy sterować prędkością wyświetlania (1. przycisk za tekstem), oraz efektem użytym do pokazania danego tekstu (2. przycisk). Z tego co udostępnia nam aplikacja dostępne jest 5 prędkości oraz 4 efekty (scroll, śnieg, flash, stały tekst).

Analiza

Aby poprawnie wysłać tekst do płytki (czy innego urządzenia) musimy poznać jej protokół. W tym wypadku mamy dwie możliwości do wyboru, podobnie jak poprzednim razem posłużyć się IDA i zobaczyć co jest wysyłane, bądź też skorzystać z programu za pomocą, którego będziemy mogli podejrzeć całą komunikację z wybranym portem COM. Zobaczymy obie metody. Na początek – IDA.

Tym razem skupimy się na drugiej metodzie, w której znaleźliśmy referencję do naszej metody sio_open a mianowicie _TForm1_suitempSendClick. Metoda jest dość obszerna z racji tego, że odpowiada jak już pisałem wcześniej za wszystkie 8 kanałów, które płytka wspiera. Obszerność/złożoność całej metody można zobaczyć na zrzucie obok.
Spory, skomplikowany kawałek kodu.
My jednak skupimy się tylko na wybranym fragmencie. Ponownie odszukujemy sobie wszystkie odwołania do sio_open i przechodzimy do pierwszego tego w naszej metodzie (powinny być 2). Jest tam standardowe otwarcie portu i ustawienie jego parametrów (prędkość, bity, parzystość, etc.) a następnie wysyłamy do płytki 0 (reset?). Dalej sporo kodu, ustawiającego progressbar’y na UI (pomijamy) i związanego z kanałem 7 i 8 więc na razie to pomijamy. Dochodzimy jednak do następującego fragmentu.

Widzimy tu początek tworzenia ramki/pakietu, która będzie wysyłana do urządzenia. W zmiennej var_D8 przechowywany jest br kanału, var_B4 to kolejny numer paczki (będzie o tym jeszcze dalej). 
Nasz nagłówek po wykonaniu tego pierwszego fragmentu kodu wygląda następująco (hex): 02 31 06 00, gdzie: 02 31 – stały identyfikator. 06 – numer kanału + 6, 00 – numer kanału * 64 (offset danych) W drugim fragmencie w pętli zostaje przepisane maksymalnie 64 znaki z naszej wiadomości do pakietu z czego pierwsze 4 stanowią dodatkowe informacje dla płytki. Jeśli więc nasza wiadomość to abc (0x61, 0x62, 0x63) to w buforze znajdą się następujące dane do wysłania. 0x35 0x31 0x41 0x03 0x61 0x62 0x63, gdzie 0x35 – prędkość, 0x31 – numer kanału w ASCII (ponownie?) oraz 0x42 – efekt (innym razem opiszemy co robią poszczególne). Każde 64 znaki z nagłówkiem (czyli 68 znaków) to jedna paczka. Aplikacja wysyła 4 paczki (dodatkowe dane znajdują się tylko w pierwszej).

Jednak to nie wszystko. Gdybyśmy wysłali tylko tyle, aplikacja uraczyłaby nas pięknym czerwonym XXX (hmm – jakaś tajna funkcja :]). Aby uzyskać OK (też czerwone) – musimy zrobić jeszcze jedno (a w zasadzie jeszcze 2 rzeczy). Jak przejdziemy z kodem niewiele poniżej fragmentu, który widzieliśmy powyżej zobaczymy taki fragment kodu:

var_18C wskazuje na nasz bufor danych a eax służy w tym wypadku za indeks. Iterujemy się po 68 znakach bufora zaczynając od 2 (eax = 1) i liczymy sumę % 255 czyli prosta suma kontrolna. Tę dodajemy na koniec naszej ramki danych.
Podsumowując więc tworzymy nagłówek (pierwsze 8 bajtów), w których są informacje o kanale, prędkości, efekcie wyświetlanego tekstu, długość tekstu. Następnie 60 znaków tekstu a na koniec zamykamy to sumą kontrolną. To jednak jeszcze nie koniec….
Gdybyśmy na tym poprzestali, to po zapisie nadal raczyłby nas groźny napis XXX. Jeśli dobrze poszukamy (oczywiście nie trzeba szukać, tylko przeanalizować kod) to przed samym zamknięciem komunikacji z portem COM wysyłane są jeszcze dodatkowe bajty danych. Jak popatrzymy na poniższy fragment kodu

to zobaczymy, że przed ostatecznym wywołaniem sio_close wysyłamy jeszcze 3 bajty: 0x02, 0x33 oraz jakiś bajt znajdujący się w var_A5. Odszukując gdzie jest ustawiany ten bajt trafiamy na następujący kod:

Co nie jest pokazane na tym zrzucie w eax mamy ilość zapisywanych linii (np. 6). Ten prosty kod przekłada się na (0xFF >> (8-eax)). Finito….

Uff.. no to teraz C#.

LEDBoard

Code Snippet
  1. private static byte[] CreatePackage(int lineNo, int packageNo, string text)
  2. {
  3.     byte[] data = new byte[PackageSize];
  4.     var startIndex = (byte) (packageNo << 6);
  5.     data[0] = 0x02;
  6.     data[1] = 0x31;
  7.     data[2] = (byte) (lineNo + 6);
  8.     data[3] = startIndex;
  9.     if (IncludeLineInfo(packageNo))
  10.     {
  11.         data[4] = (byte)Speed.Fast;
  12.         data[5] = (byte)(lineNo + 0x31);
  13.         data[6] = (byte)Effect.Scroll;
  14.         data[7] = (byte)text.Length;
  15.     }
  16.     if (HasMoreData(startIndex, text))
  17.     {
  18.         Array.Copy(text.Select(x => (byte) x).ToArray(), startIndex, data, packageNo == 0 ? 8 : 4,
  19.                    Math.Min(text.Length – startIndex, MaxDataSize));
  20.     }
  21.     data[data.Length – 1] = (byte) (data.Sum(x => x) – 2);
  22.     return data;
  23. }
  24.  
  25. private static bool HasMoreData(byte startIndex, string text)
  26. {
  27.     return startIndex < text.Length;
  28. }
  29.  
  30. private static bool IncludeLineInfo(int packageNo)
  31. {
  32.     return packageNo == 0;
  33. }

Metoda przygotowuje dane do pakietu na podstawie numeru linii, pakietu oraz samych danych. W zależności od tego czy jest, który to pakiet dokładamy informacje o prędkości czy użytym efekcie. Następnie zapisujemy do bufora dane i dopełniamy sumą kontrolną. Odejmujemy od niej 2 jako, że aplikacja liczyła ją ze wszystkich danych bufora z pominięciem początkowego 02.
Film pokazujący wgrane dane do płytki.

Poza udostępnieniem możliwości zapisu w kodzie trochę się pozmieniało. Dodałem ViewModel oraz implementację interfejsu ICommand do obsługi zapisu do płytki.

Zainteresowany dalszą analizą? Zamieść komentarz. Co jest do poprawienia? Czego ma być więcej?

Pierwsza część: LED Board – cz. 1. Jak poprzednio wszelkie zmiany w kodzie odwzorowane są na GitHub’ie – LEDBoard.