Kurs Arduino – MAX7219
W poprzednim odcinku (UR019) z powodzeniem wykorzystaliśmy moduły z kostką MAX7219 sterującą wyświetlaczami 7-segmentowymi, natomiast natrafiliśmy na poważny kłopot przy wykorzystaniu modułu z czterema wyświetlaczami matrycowymi.
Matrycowe wyświetlacze LED są bardzo efektowne i atrakcyjne, więc nie poddajemy się i spróbujemy rozwiązać problem. Przypomnijmy, że użyta wcześniej biblioteka MCMAX7219 przeznaczona jest do sterowania pojedynczymi modułami, a zastosowanie jej do zgrabnej chińskiej płytki zawierającej cztery moduły FC-16 daje niejako cztery fragmenty obrazu obrócone o 90 stopni. Jednym z rozwiązań jest zastosowanie innej, bardziej zaawansowanej biblioteki.
Oczywiście straciłem dużo czasu, żeby zbadać zagadnienie. Zgodnie z informacjami zaczerpniętymi podczas takich poszukiwań z Internetu, inni już wcześniej mieli podobne problemy i można je rozwiązać na różne sposoby. Jednym z nich jest wykorzystanie biblioteki, a raczej bibliotek, stworzonych przez Marco Colli (MajicDesigns) z Australii, który prowadzi blog pod adresem:
https://arduinoplusplus.wordpress.com/
Jego biblioteki dostępne są w Githubie (https://github.com/MajicDesigns), ale można je też zainstalować z poziomu Arduino przez Menedżer bibliotek, gdzie w okienko wyszukiwania trzeba wpisać: MD_MAX.
Jak pokazuje rysunek 1, dostępne są trzy biblioteki. Podstawowa zapewnia współpracę między Arduino i modułem lub modułami z matrycą LED 8×8 i kostką MAX7219 i nazywa się:
MD_MAX72xx,
Do tej biblioteki dołączonych jest szereg szkiców .ino z przykładami. Zgodnie z rysunkiem 2 otworzyłem przykład MD_MAX72xx_Test.ino i zapisałem jako plik A2001.ino (który dostępny jest w Elportalu wśród materiałów dodatkowych do tego numeru).
Ja do płytki Arduino Uno dołączyłem zestaw czterech modułów oznaczonych FC16 (w sumie 32 × 8 LED), ale na rynku dostępnych jest szereg innych, pojedynczych modułów 8×8, o innym przyporządkowaniu rzędów i kolumn matrycy do kostki MAX7219. To był opisany w poprzednim odcinku problem, który teraz rozwiązany jest w bibliotece przez możliwość wybrania jednej z czterech wersji:
PAROLA_HW, GENERIC_HW,
ICSTATION_HW, FC16_HW.
Dlatego w szkicu A2001.ino trzeba było zmienić dwie linijki, co zaznaczone jest kolorami w szkicu 1.
Szkic1:
(…)
#endif
//określ typ użytego wyświetlacza – cztery możliwości 0…3:
//#define HARDWARE_TYPE MD_MAX72XX::PAROLA_HW //w oryginale
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW //po zmianie
// określ liczbę modułów włańcuchu:
//#define MAX_DEVICES 11 // w oryginale było 11
#define MAX_DEVICES 4 //my mamy cztery moduły FC16
// SPI hardware interface:
#define CLK_PIN 13 // or SCK
#define DATA_PIN 11 // or MOSI
#define CS_PIN 10 // or SS
// tworzymy obiekt o nazwie mx klasy MD_MAX72XX:
MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
// gdyby uzyte było programowe łacze SPI, wtedy:
// MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE,
DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES);
// określamy czas opóźnienia = szybkość zmian:
#define DELAYTIME 100 // milisekundy
void scrollText(char *p)
(…)
Jedna zmiana to typ wyświetlacza (FC16_HW), druga – liczba szeregowo połączonych modułów (4).
Standardowo wykorzystujemy sprzętowe łącze SPI z pinami 13, 11 Arduino, a do tego pin10 jako SS (CS – Chip Select). Układ połączeń jest identyczny jak w poprzednim ćwiczeniu, jednak celowo na rysunku 3 został narysowany w nieco inny sposób. Przyczynę poznasz, gdy uruchomisz przykładowe programy szkice.
U mnie nie obyło się bez niespodzianek: gdy plik przykładu MD_MAX72xx_Test.ino zapisałem jako A2001.ino, to podczas zwyczajnej edycji tekstu komentarza (i związanej z tym bieżącej kompilacji) pojawił się nieoczekiwanie dziwny błąd, jak pokazuje rysunek 4.
Problem rozwiązało zapisanie, zamknięcie pliku A2001.ino, a następnie ponowne jego otwarcie. Nie wiem, z czego wynikał ten błąd. Być może także z tego, że działania były realizowane na nowym komputerze z nową wersją ArduinoIDE pobraną przez sklep Microsoft Store.
Po zmianie dwóch linijek według szkicu 1 skompilowałem i załadowałem program, co przebiegło bez żadnych problemów. Na wyświetlaczu pojawiła się efektowna, interesująca i długa animacja.
Świetnie, ale szkic MD_MAX72xx_Test.ino (A2001.ino) może przestraszyć nie tylko początkujących. Także dlatego, że w czasie animacji Arduino wysyła przez łącze szeregowe (i USB) na konsolę komputera informacje, co aktualnie jest pokazywane na matrycy LED. Aby to zobaczyć, trzeba otworzyć w ArduinoIDE: Narzędzia – Monitor portu szeregowego (Ctrl+Shift+M) – rysunek 5. Trzeba tam ustawić prędkość transmisji 57600bps, bo tak jest ustawione w szkicu.
Nie trzeba się bać: warto przeanalizować szkic A2001.ino, gdzie po wstępnych deklaracjach znajdziemy definicje szeregu funkcji, z których początek pierwszej, przewijania tekstu, widzimy na dole szkicu 1:
void scrollText(char *p)
Obowiązkowe funkcje Arduino, czyli setup() i loop(), znajdziemy na samym końcu programu, jak pokazuje szkic 2.
Szkic2:
void setup() {
mx.begin(); //inicjalizacja
#if DEBUG
Serial.begin(57600); //inicjalizacja
#endif
PRINTS(„\n[MD_MAX72XX Test & Demo]”);}
void loop() {
#if 1
scrollText(„Graphics”);
zeroPointSet(); rows(); columns();
cross(); stripe(); checkboard();
bullseye(); bounce(); spiral();
#endif
#if 1
scrollText(„Control”); intensity();
scanLimit(); blinking();
#endif
#if 1
scrollText(„Transform”);
transformation1(); transformation2();
#endif
#if 1
scrollText(„Charset”);
wrapText(); showCharset();
#endif
}
Jak widać, w pętli loop() mamy jedynie wywoływanie tych wcześniej zdefiniowanych funkcji.
Jeżeli chcesz, samodzielnie przetestuj inne pouczające przykłady dołączone do tej podstawowej biblioteki MD_MAX72xx. Pamiętaj, że w każdym przypadku, w szkicu zawsze trzeba podać rodzaj użytych przez Ciebie modułów oraz ich liczbę.
Biblioteka podstawowa MD_MAX72xx zapewnia kontakt z wyświetlaczem i realizuje elementarne funkcje. Bardziej złożone funkcje trzeba samemu napisać i umieścić w programie. Nie jest to zbyt wygodne. Znakomitym ułatwieniem jest kolejna, pomocnicza biblioteka o nazwie MD_MAXPanel, która wykorzystuje podstawową bibliotekę MD_MAX72xx i zawiera gotowe funkcje metody, pozwalające w wygodny sposób zrealizować szereg typowych zadań, jak choćby rysowanie elementarnych figur geometrycznych: punktów, linii, prostokątów, trójkątów, okręgów i kół, a także wyświetlanie tekstów.
Można się z nią zapoznać za pomocą przykładów do niej dołączonych. Ja zacząłem od przykładu z pliku:
MD_MAXPanel_Test.ino
Znów podczas edycji tekstu „wyskoczył błąd kompilacji” (jak na rysunku 4). Musiałem najpierw otworzyć przykładowy oryginalny szkic, zmodyfikować go, skompilować i załadować do płytki, a dopiero potem zapisać go jako A2002.ino.
Oryginalny plik wymagał ingerencji tylko w jednym miejscu. Biblioteka może obsługiwać wyświetlacze składające się z zestawienia wielu modułów 8×8 umieszczonych w osi poziomej X i w osi poziomej Y. Mój panel ma cztery moduły w jednym rzędzie, więc trzeba było zmienić liczbę 5 na 1, co pokazane jest w szkicu 3:
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4; // mamy cztery moduły w rzedzie
//const uint8_t Y_DEVICES = 5; // tak było w oryginale
const uint8_t Y_DEVICES = 1; // my mamy jeden rząd modułów
Warto przeanalizować cały przykładowy szkic, który też zawiera szereg funkcji, wywoływanych w pętli loop(). Do biblioteki tej dołączonych jest szereg innych przykładowych szkiców – możesz się z nimi zapoznać samodzielnie. Są to w większości interesujące, popularne gry. Do ich realizacji potrzebne są przyciski i brzęczyk, które trzeba dołączyć według informacji z danego szkicu. Trzeba też włączyć konsolę na ekranie komputera.
Nie wymaga tego szkic MD_MAXPanel_Cube.ino, pokazujący obracający się sześcian, jednak na małym wyświetlaczu zawierającym 32 × 8 pikseli uzyskiwany efekt jest mizerny.
Można też poszukać w sieci przykładów użycia tej pożytecznej pomocniczej biblioteki MD_MAXPanel.
Jednak w Internecie zdecydowanie łatwiej znajdziesz przykłady wykorzystania kolejnej biblioteki pomocniczej tego Autora, mianowicie MD_Parola.
Biblioteka ta, też współpracująca z MD_MAX72xx, ułatwia wyświetlanie tekstów na matrycowych wyświetlaczach sterowanych przez MAX7219. Pozwala na dosuwanie wyświetlanego tekstu do krawędzi, centrowanie i przewijanie z określoną prędkością. Oferuje również szereg efektów – animacji. Pozwala też niejako podzielić wyświetlacz na części (zones), obsługuje zdefiniowane przez użytkownika fonty oraz pojedyncze znaki/symbole. Pozwala mieszać tekst z grafiką.
Do biblioteki MD_Parola dołączonych jest mnóstwo przykładowych szkiców. Generalnie trzeba w nich tylko zmieniać typ modułu (hardware), z PAROLA_HW na FC16_HW, a w niektórych też liczbę modułów na 4:
#define MAX_DEVICES 4
Można zacząć od bodaj najprostszego przykładu Parola_HelloWorld.ino, wyświetlającego nieruchomy napis na środku wyświetlacza. Gdy zmienimy napis na dłuższy, zostanie wyświetlony jego początek. Jeszcze prostszy jest szkic Parola_Print_Minimal.ino.
Warto otworzyć i zmodyfikować szkic Parola_Sprites_Simple.ino, gdzie napis jest okresowo kasowany za pomocą grafiki „rocket – rakieta”. Taka mała grafika, dość często ruchoma – animowana, po angielsku nazywa się sprite (sprajt), co w naszym języku nie ma dobrego odpowiednika; czasem używa się nazwy duszek (mały duch).
Wykorzystanie najróżniejszych duszków – sprite,ów pokazuje szkic Parola_Sprites_Library.ino, wymagający też zainstalowania kolejnej biblioteki MD_UISwitch, która jednak z grafiką i wyświetlaczami nie ma nic wspólnego, a służy do obsługi przycisków i klawiatur. Szkic będzie działał i bez dołączenia do pinu A5 przewidzianego tam potencjometru.
Pouczający jest szkic Parola_Print_Test.ino, pokazujący wykorzystanie znanej już wcześniej funkcji-metody .print(). Można się naocznie przekonać, czy i jak można jej użyć do różnego typu zmiennych i dlaczego unikać .println(), by uniknąć pułapek wynikających z niedoskonałości implementacji tych metod dla omawianych wyświetlaczy matrycowych MAX7219.
Jeżeli tylko masz co najmniej 4-modułowy wyświetlacz (lepiej 8-modułowy), koniecznie uruchom przykładowy szkic Parola_Scrolling.ino. Ja zmieniłem dwie linie, zaznaczone kolorami w szkicu 4 i zapisałem plik jako A2003.ino.
Szkic 4:
(…)
// need to be adapted
//#define HARDWARE_TYPE MD_MAX72XX::PAROLA_HW //tak było
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW //nasze moduły
//#define MAX_DEVICES 8 // tak było
#define MAX_DEVICES 4 // my mamy 4 moduły
#define CLK_PIN 13
#define DATA_PIN 11
#define CS_PIN 10
// HARDWARE SPI
MD_Parola P = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
(…)
Szkic ten najpierw wyświetla zachętę do zmiany przewijanego tekstu, co należy zrobić w oknie konsoli monitora na komputerze według rysunku 6. Po wpisaniu w górne okienko konsoli własnego tekstu klikamy przycisk „Wyślij” i ten napis przekazany z komputera do Arduino będzie przewijany na wyświetlaczu.
Oczywiście zauważysz też, że jest problem z wyświetleniem polskich liter. W tym kontekście interesujący jest szkic Parola_Fonts.ino. Wyświetla on nazwę Arduino, korzystając z alfabetów greckiego, arabskiego i japońskiego (katakana). Warto zajrzeć do umieszczonego też tam pliku Parola_Fonts_data.h, gdzie umieszczone są definicje poszczególnych znaków użytych alfabetów (greckiego, arabskiego i japońskiego). Pożyteczne może się okazać zbadanie zawartości plików z fontami (czcionkami) z innych przykładów.
Takie badania pokazują, że w zamieszczonych przykładach nie ma niestety polskich liter. Może się wydawać, że skutecznym remedium będą rozwiązania z przykładowego szkicu o błędnej nazwie Parola_UFT-8_Display.ino, gdzie oczywiście chodzi o kod UTF-8. O nowoczesnym, uniwersalnym, powszechnie wykorzystywanym kodzie UTF-8 mówiliśmy w artykułach „Wokół Arduino”. Szkic ten można łatwo uruchomić; pokaże on przykłady i kolejne znaki spoza ASCII o numerach 128…255, ale niestety nie rozwiąże do końca problemu polskich liter. To dość trudny wątek i na razie nie będziemy się nim zajmować.
Koniecznie uruchom też przykładowy szkic Parola_Animation_Catalog.ino, który pokazuje wszystkie efekty tekstowe dostępne w tej bibliotece
A my wracamy do plików Parola_Scrolling.ino i A2003.ino. Podobnie jak prawie wszystkie przykładowe szkice tego autora, zawiera on instrukcje kompilacji warunkowej. Także z tego powodu analiza kodu jest utrudniona, a początkujących może skutecznie odstraszyć od tego rodzaju rozwiązań, których nie sposób wykorzystać do własnych potrzeb bez zrozumienia ich sensu. A jaki jest sens dodawania instrukcji kompilacji warunkowej? Chyba piszący program wie, czego chce, więc po co takie utrudnienie?
Fakt, analiza tego rodzaju szkiców nie jest łatwa, ale nie zrażaj się!
Ogólny sens nie jest trudny do zrozumienia, a instrukcje kompilacji warunkowej w różnych sytuacjach okazują się bardzo pożyteczne. Po pierwsze z przeznaczoną głównie dla początkujących hobbystów platformą Arduino jest poważny problem, polegający na tym, że nie ma możliwości wyszukiwania błędów przez śledzenie zachowania procesora, do którego załadowano program. A nawet najlepszym programistom zdarzają się błędy, nie mówiąc o początkujących. Tymczasem w Arduino nie ma możliwości debugowania, czyli odpluskwiania programu przez śledzenie przebiegu programu.
Nie ma i już!
Prymitywnym środkiem zastępczym jest umieszczanie w pisanym programie fragmentów kodu, które poinformują programistę, czy podczas działania program doszedł do określonego punktu / określonej instrukcji. Może to być komunikat wysyłany przez jedyny kanał komunikacyjny, czyli przez łącze Serial (i USB) do konsoli komputera. Można dodać w programie rozkazy, które będą wysyłać przez łącze szeregowe Serial i metodę .print() informacje, że dany punkt programu został osiągnięty. Tak, ale te rozkazy zajmują cenne miejsce w pamięci i spowalniają wykonywanie programu. Jeśli są potrzebne, to tylko podczas wyszukiwania błędów w nowo tworzonym programie. A nie powinno ich być w finalnej, odpluskwionej wersji. Tymczasem takich wstawek w obszernym programie może być wiele i ręczne ich usunięcie z finalnej wersji jest żmudne, kłopotliwe. Ponadto warto byłoby na wszelki wypadek mieć wersję z takimi dodatkami.
I właśnie tu pożyteczne okazują się instrukcje kompilacji warunkowej. Można je sformułować tak, że kompilacja podczas pracy nad programem uwzględnia te dodatkowe kawałki kodu, a zmiana jednej drobnej definicji powoduje, że wszystkie te dodatki NIE są uwzględniane przy kompilacji finalnej wersji, choć są nadal w programie źródłowym. Mogą one mieć postać:
#if DEBUG
//instrukcje
#else
//instrukcje
#endif
Takie „pseudodebugowanie” może też być wykorzystywane w finalnej wersji do monitorowania stanu i zmian w programie za pomocą łącza Serial i okna konsoli. I tak jest w większości omawianych przykładów, gdzie na ekranie komputera uzyskujemy dodatkowe informacje.
Po drugie, kompilacja warunkowa jest przydatna, gdy przewidujemy więcej niż jedną wersje finalnego programu. Choćby dwie, np. uboższą i bogatszą.
Oba omówione zastosowania mamy między innymi w szkicach Parola_Scrolling.ino i A2003.ino, których fragment pokazuje szkic 5:
(…)
// set to 1 if we are implementing the user interface pot, switch, etc
// czy w układzie wykorzystujemy potencjometr, przyciski?
#define USE_UI_CONTROL 0 //jest wartość 0, czyli NIE wykorzystujemy
#if USE_UI_CONTROL
#include <MD_UISwitch.h>
#endif
// Turn on debug statements to the serial output
// Czy wykorzystujemy instrukcje Serial.print() do debugowania?
#define DEBUG 0 //jest wartość 0, czyli NIE wykorzystujemy
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) Serial.print(F(x))
#define PRINTX(x) Serial.println(x, HEX)
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTX(x)
#endif
// Define the (…)
Jeżeli dołączymy do Arduino potencjometr i przyciski, to zmienimy też zero na jedynkę w linii:
#define USE_UI_CONTROL 1
i finalny program będzie bogatszy o procedury obsługi tych zewnętrznych elementów.
Aby na bieżąco wysyłać do okna konsoli informacje o pracy programu, można zmienić zero na jedynkę w linii:
#define DEBUG 1
A jeżeli doszliśmy tak daleko, dołączmy do Arduino potencjometr i sześć przycisków według rysunku 7.
W programie-szkicu zmieniamy
#define USE_UI_CONTROL 1
by dodać obsługę tych elementów. Ten szkic wykorzysta tylko potencjometr regulujący prędkość przesuwania napisu oraz dwa ostatnie przyciski. Ostatni S6 „odwraca” napis na negatyw, a przedostatni zmienia kierunek przesuwania napisu na ekranie. W programie 2003.ino trzeba także zmienić numery nóżek Arduino według szkicu 6:
(…)
// set to 1 if we are implementing
//the user interface pot, switch, etc
#define USE_UI_CONTROL 1
#if USE_UI_CONTROL
#include <MD_UISwitch.h>
#endif
(…)
// Scrolling parameters
#if USE_UI_CONTROL
const uint8_t SPEED_IN = A5; //bez zmian
//const uint8_t DIRECTION_SET = 8; // było
const uint8_t DIRECTION_SET = 6; // u mnie jest
//const uint8_t INVERT_SET = 9; // było
const uint8_t INVERT_SET = 7; // u mnie jest
const uint8_t SPEED_DEADBAND = 5;
#endif // USE_UI_CONTROL
(…)
U mnie program zadziałał prawidłowo: można było zmieniać prędkość i używać S5 i S6.
Celowo od razu zamontowałem aż sześć przycisków, by w pełni wykorzystać kolejny przykładowy szkic Parola_Test.ino, który też używa warunkowej kompilacji, a który ma pokazać możliwości omawianych bibliotek.
Znów w oryginalnym pliku Parola_Test.ino zmieniłem kilka linijek, jak pokazuje szkic 7 i zapisałem jako A2004.ino.
Szkic 7:
(…)
// need to be adapted
//#define HARDWARE_TYPE MD_MAX72XX::PAROLA_HW // było
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW //nasze moduły
#define MAX_DEVICES 11 // było
#define MAX_DEVICES 4 //bo mamy zestaw tylko czterech modułów
#define CLK_PIN 13
(…)
// User interface pin and switch definitions
const uint8_t SPEED_IN = A5; // control speed with an external pot
//const uint8_t PAUSE_SET = 4; // toggle pause time
const uint8_t PAUSE_SET = 2; // pauza – pin 2
//const uint8_t FLIP_SET = 5; // toggle flip status
const uint8_t FLIP_SET = 3; // odwróć/odbij – pin 3
//const uint8_t JUSTIFY_SET = 6; // change the justification
const uint8_t JUSTIFY_SET = 4; // wyjustuj – pin 4
//const uint8_t INTENSITY_SET = 7; //change display intensity
const uint8_t INTENSITY_SET = 5; // jasność – pin 5
//const uint8_t EFFECT_SET = 8; // change the effect
const uint8_t EFFECT_SET = 6; // zmień efekt – pin 6
//const uint8_t INVERSE_SET = 9; // inverse display
const uint8_t INVERSE_SET = 7; // „negatyw” – pin 7
uint8_t uiPins[] = { PAUSE_SET, FLIP_SET, JUSTIFY_SET, INTENS(…)
(…)
const char *msg[] =
{
// „Parola for”, //tak było
// „Arduino”,
// „LED Matrix”,
// „Display”
„Arduino”, „EdW”, „matryca”, „diod LED” //nowe, krótsze napisy
};
#define NEXT_STRING ((curString + 1) % ARRAY_SIZE(msg))
Analogicznie jak w A2003.ino, też zmieniłem numery nóżek, do których podłączone są przyciski S1…S6, a dodatkowo wpisałem cztery zmienione teksty do wyświetlenia. Mój wyświetlacz ma tylko 4 moduły oraz 32 × 8 pikseli, więc należy wprowadzić krótsze napisy, gdyż te dłuższe zostaną obcięte i nie będzie widać niektórych efektów i opcji.
Po skompilowaniu i załadowaniu program A2004.ino zaczął pracować, na wyświetlaczu można było zmieniać efekty, ale reakcje przycisków były jakieś dziwne, jakby niepewne.
Dlatego koniecznie trzeba włączyć na komputerze Monitor portu szeregowego (Ctrl+Shift+M), ponieważ zmiany stanu potencjometru oraz naciskanie klawiszy S1…S6 są tam na bieżąco prezentowane. Można w ten sposób w czasie rzeczywistym dokładnie śledzić działanie szkicu. Zestawienie informacji z ekranu komputera i wyświetlacza LED pokazuje, czy i jak system reaguje na naciśnięcie klawisza lub przesunięcie suwaka potencjometru.
Te napisy na ekranie komputera to znacząca pomoc, ale w moim przypadku na konsoli monitora widać było dziwne reakcje. Między innymi po każdej znaczącej zmianie stanu suwaka potencjometru na ekranie wypisywany jest kolejny komunikat o nowej nastawionej wartości. A u mnie bez dotknięcia potencjometru, teoretycznie bez powodu, program sygnalizował nieustanne zmiany napięcia na nóżce A5. Najgorzej było po naciśnięciu S6 (obraz w negatywie), gdy świeciły prawie wszystkie diody matrycy LED.
W następnym odcinku kursu omówię przyczyny i środki zaradcze. A Ty, jeśli tylko masz odpowiedni wyświetlacz matrycowy LED, możesz zbudować model taki sam lub podobny jak na fotografii 8 i osobiście przeprowadzić podobne eksperymenty z omówionymi bibliotekami.
Piotr Górecki