Java i Spring na Raspberry Pi, część 2

java pi 2015-12-14, 01:12,

Pierwsze kroki za nami. Pusta aplikacja się uruchamia i coś robi. Czas najwyższy dobrać się do tego, co w Pi najcenniejsze, czyli do linii wejścia-wyjścia (GPIO).

Do kontaktu z warstwą sprzętowową potrzebna nam będzie biblioteka Pi4J, która pod spodem korzysta z WiringPI. Ponieważ integrujemy się ze sprzętem to aplikacja musi być uruchamiana z uprawnieniami roota.

Zadanie

Na początek zdefiniujmy zadanie, które będzie do wykonania. Dysponując czujnikiem ruchu (w razie braku można skorzystać z przycisku typu NO, normalnie otwarty) chcemy, by czerwona dioda LED płynnie się zapaliła, a po upływie określonego czasu równie płynnie zgasła. Dodatkowo montujemy dwie diagnostyczne diody LED. Zieloną, która będzie sygnalizować puls życia naszej aplikacji oraz żółta, która będzie zapalona wtedy, gdy czujnik ruchu wykrywa ruch w otoczeniu.

Przygotowanie sprzętu

Na początek łączymy wszystko zgodnie ze schematem. W pierwszej kolejności podłączamy masę do niebieskiej szyny płytki prototypowej.

Następnie umieszczamy diody LED oraz rezystory (330R - 470R), których jeden koniec łączy diodę LED, drugi masę zasilania.

Kolejne połączenia to połączenie czerwonej diody LED z PIN 12, żółtej z PIN 11, zielonej z PIN 15.

Ostatni element to podłączenie czujnika PIR. Mój ma trzy złącza. VCC, GND oraz Sensor. VCC podłączamy do linii +5V (PIN2), GND do masy na płytce prototypowej, sensor do GPIO 2, czyli PIN 13 Raspberry Pi.

WAŻNE: Wyjścia/wejścia PI nie tolerują napięcia wyższego niż 3,3V. Jeśli sensor posługuje się sygnałem o wyższym napięciu należy użyć konwertera poziomów, w przeciwnym razie port wejściowy ulegnie spaleniu.

Jeśli nie masz sensora ruchu to nic straconego. Połącz przycisk, łącząc go przez rezystor 1kOhm pomiędzy PIN 13 a linię 3,3V. Naciśnięcie go będzie emulowało fakt wykrycia ruchu przez czujnik PIR. W kodzie aplikacji dalszych różnic nie będzie.

Oprogramowanie

Kod aplikacji znajduje się w repozytorium spring-pi4j-sample-app.

Ponieważ mamy dwie diody LED służące do statusowania to tworzymy prosty typ ENUM, który będzie stanowił warstwę abstrakcji, między faktycznymi PINami Pi4J, a resztą aplikacji, która o użytym sprzęcie nie musi zbyt wiele wiedzieć.

public enum SystemLeds {
    STATUS_LED,
    MOTION_LED
}

Następnie tworzymy interfejs HardwareController. Definiuje on metody, którymi będziemy komunikować się z warstwą sprzętową.

public interface HardwareController {

    GpioPinDigitalInput getSensorInput();

    void pulseShort( SystemLeds led );

    void pulseLong( SystemLeds led );

    void on(SystemLeds led);

    void off(SystemLeds led);

    void setPwm( int level );

    void shutdown();

}
  • pulseShort - krótkie błyśnięcie LED
  • pulseLong - długie błyśnięcie LED
  • on - włączenie LED
  • off - wyłączenie LED
  • setPwm - ustawienie stopnia wypełnienia PWM (0 - 1023)
  • shutdown - wyłączenie warstwy GPIO

Do tak przygotowanego interfejsu tworzymy implementację, która zapewni realizację założonych metod:

Dzięki annotacji @Component nie będziemy musieli tworzyć ręcznie instancji klasy, a jeśli kilka elementów aplikacji miałoby się komunikować z warstwą fizyczną to tylko jedna klasa zostanie w tym celu utworzona.

Z chwilą tworzenia obiektu automatycznie konfigurowane jest kilka elementów, które mają być dostępne w naszej aplikacji. Najpierw pobieramy instancję Gpio, za pomocą której będziemy komunikować się ze światem zewnętrznym.

Następnie żądamy odpowiedniej konfiguracji pinów wyjściowych (ProvisionDigitalOutputPin). Jako parametr podajemy numer PINu (numeracja PINów zgodna z WiringPi, jest ona inna niż w dokumentacji układu BCM, będącego sercem Maliny) oraz domyślny stan PIN (pull-up/pull-down lub wysoka impedancja). My chcemy, by poziom PINu był niski.

Z pomocą ProvisionDigitalInputPin tworzymy instancję wejściową. Stan tego PINu będzie odpowiadał stanowi czujnika ruchu. W przypadku mojego czujnika jest to stan niski, gdy nie wykryto ruchu oraz stan wysoki przez 5 sekund od wykrycia ruchu. Jeśli obiekt się cały czas porusza przed sensorem to czujnik ruchu cały czas utrzymuje PIN w stanie wysokim.

Ostatni PIN to PwmOutputPin. Jest to jedno z dwóch wyjść PWM Maliny (drugie nie jest wyprowadzone, używa go kanał audio). Wskazując poziom od 0 do 1023 można uzyskać regulację jasności diody LED.

W dalszej kolejności tworzymy mapę, w której zapamiętujemy obiekt pinu diody LED, jako klucza używając wcześniej zdefiniowanego typu ENUM. Dzięki temu zabiegowi główna aplikacja nie musi znać szczegółów implementacji diód statusowych. Równie dobrze mogłaby to być ta sama dioda, lub jej brak, a główny kod zostałby bez zmian.

Ostatni element, może nie konieczny, ale zostawiłem go do dalszego rozwoju, to tzw. Shutdown Hook. Gdy użytkownik zamyka aplikację przez CTRL+C lub maszyna JVM jest zamykana to ten fragment kodu się wykona, pozwalając np. ustawić stan LED lub innych elementów na znany, bezpieczny dla aplikacji poziom.

Kolejne metody są już tylko implementacją fizycznych działań Pi4J, czyli ustawieniem poziomu PWM lub stanu odpowiedniej diody LED. W tym miejscu warto nadmienić, że warstwa sprzętowa Pi nie ma żadnych zabezpieczeń przed kolidującymi poleceniami. Jeśli wyślemy na tę samą diodę polecenie BLINK, a zaraz po tym OFF, to dioda nie będzie migała. Nie poleci także żaden wyjątek. To samo dotyczy dwóch równolegle uruchomionych aplikacji odwołujących się do tego samego zasobu sprzętowego.

Mając gotową warstwę sprzętową można zabrać się za kodowanie głównej części apliacji.

Metodą run() uruchamiamy główną część aplikacji. Niewiele ona ma tu do roboty, sprzętową konfigurację wykonała wcześniejsza klasa, która została zainicjowana podczas wstrzykiwania jej do klasy aplikacji.

Run() zatem wyświetla powiadomienie dla użytkownika o tym, jak opuścić aplikację. Następnie konfiguruje zdarzenie sprzętowe, czyli reakcję na czujnik ruchu i przechodzi do niekończącej się pętli, w której raz na pół sekundy ustawia docelowy poziom jasności PWM i zasypia.

Więcej dzieje się poniżej. Korzystając z wbudowanego w Spring schedulera wywułujemy metodę updatePwmLevel(). Jej zadaniem jest co 80 ms zaktualizować jasność diody LED. Jeśli docelowy poziom jest wyższy niż obecny to podnosi jasność, jeśli niższy to obniża. Robi to w określonej długości kroku, a jeśli osiągnięte zostaną poziomy graniczne to wyrównuje wartość do nich.

Kolejna metoda wykonywana w regularnych odcinkach czasu to heartbeatAction(). Jej zadaniem jest, by co dwie sekundy zielona dioda LED błysnęła, a na ekranie wyświetlony został zapis parametrów diagnostycznych. Dzięki temu możemy łatwo określić, czy aplikacja wciąż działa oraz co robi w tle. Pokazuje także, że zadania mogą działać wielowątkowo i sobie nie przeszkadzać.

Ostatnia metoda, wykonywana co sekundę to licznik zdarzeń. Można byłoby to zrealizować również na dwóch timestampach w pętli głównej, niemniej jej zadaniem jest odliczanie sekund od ostatniego wykrycia ruchu. Po prostu co sekundę podnosi wartość licznika, jeśli zmienna isMotion ustawiona jest na false, w przeciwnym razie zeruje licznik, który służy do wygaszenia diody LED po określonym czasie.

A teraz najważniejsza część, czyli faktyczne wykrywanie ruchu. Każdy PIN w Pi może generować przerwanie, czyli przerwać działanie kodu aplikacji i zasygnalizować odpowiednią zmianę stanu. Może być to zmiana np. z wysokiego na niski lub z niskiego na wysoki. W aplikacji posługuję się zdarzeniem GpioPinDigitalStateChangeEvent, które niesie w sobie zaszytą informację, jaki jest poziom PINu, który wywołał zdarzenie. W zależności od tego, czy poziom jest wysoki (czujnik widzi ruch) czy niski (ruch zamarł) wywołuje odpowiednio metody motionDetectedAction() oraz motionOutAction(). Odbywa się to przez dodanie słuchacza do określonego PINu wejściowego.

Metoda motionDetectedAction() loguje datę i czas wykrycia ruchu, następnie zeruje czas od ostatniego zdarzenia, ustawia zmienną isMotion na true, zapala diodę LED odpowiedzialną za sygnalizację wykrytego ruchu i ustawia poziom jasności diody czerwonej na maksimum. W tym momencie zadanie updatePwmLevel() zadba o jej płynne rozświetlenie, a główna pętla będzie pilnowała czasu, po którym należy ją zgasić.

Natomiast motionOutAction() zmienia stan isMotion na false oraz gasi żółtą diodę statusowę.

Teraz wystarczy aplikację skompilować, skopiować na Malinę i uruchomić poleceniem:

sudo java -jar spring-pi4j-sample-app-1.0.jar

Jeśli wszystko przebiegło pomyślnie to na ekranie powinno pojawić się podobne wyjście:

gru 14, 2015 2:24:52 AM org.springframework.context.annotation.AnnotationConfigApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@6b2d4a: startup date [Mon Dec 14 02:24:52 CET 2015]; root of context hierarchy
Naciśnij CTRL+C, aby przerwać działanie aplikacji.
[pool-1-thread-1] INFO com.roszczyk.pi4jexample.PiDemoApp - 2015-12-14T01:24:54.982Z - pwmLevel: 0, targetPwmLevel: 0, secondsSinceLastMotion: 0, isMotion: false
[pool-1-thread-1] INFO com.roszczyk.pi4jexample.PiDemoApp - 2015-12-14T01:24:57.332Z - pwmLevel: 0, targetPwmLevel: 0, secondsSinceLastMotion: 2, isMotion: false
[pool-1-thread-1] INFO com.roszczyk.pi4jexample.PiDemoApp - 2015-12-14T01:24:59.337Z - pwmLevel: 0, targetPwmLevel: 0, secondsSinceLastMotion: 4, isMotion: false
[pi4j-gpio-event-executor-0] INFO com.roszczyk.pi4jexample.PiDemoApp - 2015-12-14T01:24:59.961Z - wykryto ruch
[pool-1-thread-1] INFO com.roszczyk.pi4jexample.PiDemoApp - 2015-12-14T01:25:01.342Z - pwmLevel: 1023, targetPwmLevel: 1023, secondsSinceLastMotion: 0, isMotion: true
[pool-1-thread-1] INFO com.roszczyk.pi4jexample.PiDemoApp - 2015-12-14T01:25:03.348Z - pwmLevel: 1023, targetPwmLevel: 1023, secondsSinceLastMotion: 1, isMotion: false
[pool-1-thread-1] INFO com.roszczyk.pi4jexample.PiDemoApp - 2015-12-14T01:25:05.354Z - pwmLevel: 1023, targetPwmLevel: 1023, secondsSinceLastMotion: 3, isMotion: false
[pool-1-thread-1] INFO com.roszczyk.pi4jexample.PiDemoApp - 2015-12-14T01:25:07.360Z - pwmLevel: 1023, targetPwmLevel: 1023, secondsSinceLastMotion: 5, isMotion: false
[pool-1-thread-1] INFO com.roszczyk.pi4jexample.PiDemoApp - 2015-12-14T01:25:09.366Z - pwmLevel: 1023, targetPwmLevel: 1023, secondsSinceLastMotion: 7, isMotion: false
[pool-1-thread-1] INFO com.roszczyk.pi4jexample.PiDemoApp - 2015-12-14T01:25:11.372Z - pwmLevel: 1023, targetPwmLevel: 1023, secondsSinceLastMotion: 9, isMotion: false
[pool-1-thread-1] INFO com.roszczyk.pi4jexample.PiDemoApp - 2015-12-14T01:25:13.377Z - pwmLevel: 523, targetPwmLevel: 0, secondsSinceLastMotion: 11, isMotion: false
[pool-1-thread-1] INFO com.roszczyk.pi4jexample.PiDemoApp - 2015-12-14T01:25:15.383Z - pwmLevel: 0, targetPwmLevel: 0, secondsSinceLastMotion: 13, isMotion: false
[pool-1-thread-1] INFO com.roszczyk.pi4jexample.PiDemoApp - 2015-12-14T01:25:17.389Z - pwmLevel: 0, targetPwmLevel: 0, secondsSinceLastMotion: 15, isMotion: false

Podsumowanie

Wykorzystując Springa oraz Pi4J bardzo łatwo zrealizować aplikację, która komunikuje się ze światem zewnętrznym. Błyskanie diodami LED to oczywiście banalny przykład, za którym mogą stać dużo bardziej ambitne rozwiązania. Dzięki Pi4J uzyskujemy dostęp do sprzętowych magistral I2C, SPI oraz UART. Nic więc nie stoi na przeszkodzie, by wysokopoziomowa aplikacja w Javie (np. wystawiąjąca webserwis) komunikowała się z dużą liczbą różnych czujników, a także innych urządzeń opartych np. o Arduino. Platforma linuxowa oraz Java daje tu fajny zestaw narzędzi, realizujących komunikację sieciową, wifi, ale także dostęp do baz danych, korzystanie z których na 8-bitowych AVRach jest znacznie trudniejsze.

Budując aplikacje tego typu należy pamiętać, że w Javie funkcjonuje coś takiego jak Garbage Collector. Od czasu do czasu wstrzymuje on aplikację na kilkadziesiąt milisekund, celem oczyszczenia pamięci z nieużywanych obiektów. W skrajnym przypadku, gdy obiektów przybędzie dużo i w niekontrolowany sposób możemy liczyć się z wstrzymaniem pracy naszej aplikacji nawet na kilka sekund. W sam raz, by np. nasz robot rozbił się o ścianę. Warto zatem pamiętać, by wszędzie gdzie to możliwe używać typów prostych (mały int, boolean etc.) oraz tablic zamiast list. Warto też rozważyć użycie kolektora G1, który czyści pamięć w sposób nieco bardziej przewidywalny.

A będąc już przy robotyce, czy innych układach czasu rzeczywistego, bądź układach wymagających bezwarunkowej reakcji na pewne zdarzenia to warto rozważyć wykonanie układu bezpieczeństwa przy użyciu np. Arduino, a tylko wyniki jego pracy przekazywać poprzez SPI, UART lub I2C do aplikacji. Np. czujnik ultradźwiękowy, który wykryje przeszkode przed samym nosem robota mógłby odciąć napęd silników nawet wtedy, gdy aplikacja w Javie jest zajęta. Podobnie czujnik przegrzania mógłby obniżyć parametry pracy albo załączyć wentylatory niezależnie od głównej aplikacji, podobnie jak dzieje się to w układach chłodzenia laptopów.

Komentarze