czwartek, 5 października 2017

finite state machine (automaty skończone) w praktyce Arduino - GrzyboSuszarka po nowemu

Pierwszy raz próbuję świadomie napisać program z zastosowaniem automatu skończonego. Świadomie - wcześniej oczywiście mówiłem prozą ale w stanie pewnej pomroczności. Nazwanie stanów programu a w zasadzie ich ponumerowanie samo poukładało strukturę wewnętrzną programu i dowiązało do nich potrzebne instrukcje i procedury. Najważniejszą jest decyzja jak stany będą w programie zmieniane określonymi sygnałami (wejściowymi) i w jaki sposób poszczególne stany zostaną obsłużone tzn jak wygenerują sterowania (sygnały wyjściowe).
Do tej pory w programach określony stan na wejściu modułu wywoływał program obsługi w następstwie czego generowane było sterowanie. W automacie skończonym sygnał powoduje jedynie zmianę stanu wewnętrznego programu. Obsługa zaś stanów wewnętrznych czyli generowanie sterowań wyjściowych dokonywana jest w całkiem innym miejscu. Może tego nie widać na pierwszy rzut oka ale to  naprawdę jest REWOLUCJA.

Klasyczne programowanie wygląda tak
SYGNAŁ >>> STEROWANIE
Sterowanie z użyciem automatów skończonych tak
SYGNAŁ >>>> STAN WEWNĘTRZNY  >>>  STEROWANIE
Jaką przewagę uzyskujemy wprowadzając punkt pośredni pomiędzy sygnałem a sterowaniem? Dajemy sobie dodatkowy stopień swobody, który możemy dowolnie wykorzystać natychmiast lub w przyszłości. Przy sterowaniu bezpośrednim sygnał wyjściowy jest dość mocno związany z sygnałem wejściowym i czasami trudno to rozdzielić. Trzeba się nieźle nagimnastykować by wprowadzić w taki schemat opóźnienia czasowe czy zależności od wcześniejszych sygnałów, które już zanikły. Natomiast stany wewnętrzne potrafią zapamiętać te zdarzenia i użyć je we właściwym momencie do wygenerowania sygnału wyjściowego. Ważne jest też rozdzielenie zdarzeń wejściowych od sygnałów wyjściowych. Daje to ogromną swobodę dopinania innych procedur do poszczególnych stanów np. dla zwielokrotnienia wyjść lub dołożenia dodatkowych linii sterujących programem. Ale najważniejsza moim zdaniem jest możliwość tworzenia połączeń wewnętrznych między stanami. To te połączenia pozwalają budować złożone algorytmy sterowania, pętle, i rozgałęzienia decyzji.
Trudno mi ocenić jak często programiści bibliotek stosują ten mechanizm ale moje najpopularniejsze biblioteki - timers.h i onebutton.h - na pewno zostały w ten sposób zaprojektowane. Autor OneButton jest chyba nawet specjalistą w automatach skończonych i zamieszcza pełen graf działania swojej biblioteki. Udostępnia również bibliotekę Finite State Machines (FSM)  pozwalającą tworzyć własne złożone automaty skończone w  projektach dla Arduino. Temat jest bardzo ciekawy i mocno rozwojowy.

Program grzybosuszarki zawierał więc będzie kilka FSM rozsianych po różnych bibliotekach. Z przyczyn oczywistych przeanalizuję działanie jedynie swojego automatu choć inne automaty będą się pewnie przewijać w tle. I tak sygnały wejściowe z przycisku SONOFFa dowiązałem do automatu biblioteki OneButton.h dzięki czemu nie muszę już rozpoznawać takich stanów jak Klik, Podwójny Klik czy Długi Klik. Procedury wyjściowe FSM biblioteki użyłem do zmiany stanów wewnętrznych grzybosuszarki wg grafu z poprzedniego postu. Podprogram umieszczony w osobnym pliku przycisk.h wygląda mniej więcej tak:

#include "OneButton.h"
#define klaw1 4

OneButton myklaw1(klaw1, true);

byte stan_programu = 0; //stan wewnętrzny automatu skończonego
void updateled(int t);

void myclick1() { //przełączanie klik
  stan_programu =  stan_programu + 10;
  stan_programu =  stan_programu % 20;
} // click1

void mydoubleclick1() { //przełączanie podwójny klik
  if (stan_programu == 0) stan_programu = 1; else  stan_programu = 0;
} // doubleclick1

void mylongPressStart1() { //przełączanie stanów długi klik
  stan_programu =  stan_programu % 10;
  stan_programu++;
  stan_programu =  stan_programu % 4; //licznik od 0 do 3
  if (stan_programu == 2)    updateled(200); //miganie LED jako wskaźnik stanu 2
  if (stan_programu == 3)    updateled(100);
} // longPressStart1

void mylongPress1() {
} // longPress1

void mylongPressStop1() {
  updateled(0);    //wyłączenie migania LED
} // longPressStop1

void klawsetup()  //procedura do umieszczenia w funkcji setup()
{
  myklaw1.setDebounceTicks(100); // opóźnienie na drganie styków domyśle 50 msek
  myklaw1.setClickTicks(700); // opóźnienie po którym jest click domyślne 600 msek
  myklaw1.setPressTicks(1200);

  myklaw1.attachClick(myclick1);  //deklaracje procedur wywoływanych zdarzeniami przycisku
  myklaw1.attachDoubleClick(mydoubleclick1);
  myklaw1.attachLongPressStart(mylongPressStart1);
  myklaw1.attachLongPressStop(mylongPressStop1);
  myklaw1.attachDuringLongPress(mylongPress1);

}

void klawkakcja() {  //procedura do umieszczenia w pętli loop() - pooling
  myklaw1.tick();
}

Stany od 0 do 3 są przełączane długim Klikiem (tu zmiana w stosunku do grafu). Stany przejściowe 2<>12 i 3<>13 są zmieniane Klikiem. Włączenie/ wyłączenie urządzenia(stan 0<>1) dokonuje się podwójnym Klikiem. Jak widać nie ma w tym żadnej procedury sterującej ani przekaźnikiem ani LEDami SONOFFa. Tylko zmiana stanów wewnętrznych automatu. I zmiana nastawy częstotliwości migania LEDa przy przełączaniu stanów pracy urządzenia. LED obsługiwany jest przez jedną z procedur biblioteki Timers.h (nb. też automat skończony). W czasie Długiego Klika przy naciśniętym przycisku LED sygnalizuje funkcję grzybosuszarki różnym sposobem świecenia

  • zgaszony - urządzenie wyłączone
  • ciągłe - grzanie ciągłe
  • miganie wolne - regulacja temperaturą
  • miganie szybkie - regulacja czasem

Zwolnienie przycisku przywraca normalną funkcję LED - jest nią wskaźnik wł/wył grzania.

Obsługę stanów wewnętrznych automatu i generowanie sterowań przekaźnikiem i LEDem umieściłem w procedurze GSmain() wywoływanej cyklicznie przez jeden z timerów biblioteki Timers.h.  Procedura sprawdza numer stanu automatu grzybosuszarki i odpowiednio przełącza przekaźnik i LED w SONOFFie. Tu też umieściłem przełączanie stanów wewnętrznych od zmian temperatury i odliczania czasów załączenia i wyłączenia grzania.
Do analizy stanów wolę stosować switch/case zamiast if ze względu na większość czytelność kodu. Lubię szczególnie deklarację default: obsługującą pominięte w analizie wartości zmiennej switch. To ważny element "domknięcia" wszystkich ścieżek w programie tak by kod nie poszedł w maliny z braku obsłużenia jakiegoś stanu wewnętrznego.

void GSon() {
  Serial.println("GSon");
  digitalWrite(led_wew, LOW);
}
void GSoff() {
  Serial.println("GSoff");
  digitalWrite(led_wew, HIGH);
}

void GSmain()
{
  gstime++;
  switch (stan_programu) {
    case 1: {
        GSon();
      } break;
    case 2: {
        GSon();
        Serial.println(temperature);
        if (temperature > tempoff)stan_programu = 12;
      } break;
    case 12: {
        Serial.println(temperature);
        GSoff();
        if (temperature < tempon)stan_programu = 2;
      } break;
    case 3: {
        Serial.println(gstime);
        GSon();
        if (gstime > timeon) {
          gstime = 0;
          stan_programu = 13;
        }
      } break;
    case 13: {
        Serial.println(gstime);
        GSoff();
        if (gstime > timeoff) {
          gstime = 0;
          stan_programu = 3;
        }
      } break;
    default:
      {
        GSoff();
        stan_programu = 0;
      } break;
  }
}

Program ruszył praktycznie od ręki i robi dokładnie to o co go poproszono. I choć pewnie dla mistrzów programowania to elementarz to dla mnie Automaty Skończone to potęga!

Teraz tylko dowiązać do tego BLYNKa i grzybosuszarkę będzie można zapędzić do roboty Czy będzie równie prosto dzięki wprowadzeniu stanów wewnętrznych - się okaże w następnym wpisie.


100

Brak komentarzy:

Publikowanie komentarza