Tramwajowy informator na NodeMCU

Ostatnio dojeżdżając na uczelnię za każdym razem kiedy dobiegałem na przystanek tramwaj akurat odjeżdżał. Postanowiłem więc zbudować sobie prosty informator, który przez sieć ściągnie sobie informację o zbliżających się odjazdach tramwajów z przystanku z którego co rano wsiadam do tramwaju i wyświetli przetworzone dane na ekranie 16×02. 

Co będzie potrzebne?

Dodatkowo przyda nam się jakiś serwer z zainstalowanym php. Może to być np. konto hostingowe takie jak pod strony www, lub komputer działający w naszej sieci lokalnej. Ja u siebie użyłem dysku sieciowego Synology. Skrypt php będzie nam potrzebny do pobrania i przetworzenia XMLa z danymi o tramwajach dla NodeMCU.

Cały kod (PHP  oraz C++ dla NodeMCU) do pobrania tutaj.

Piszemy skrypt PHP

Mój skrypt PHP pobiera dane dla przystanków w Łodzi, jednakże wiele większych miast oraz coraz to więcej mniejszych udostępnia API, dzięki któremu możemy napisać analogiczny skrypt dla komunikacji w swoim mieście.

Skrypt, który napisałem bazuje na API ze strony rozklady.lodz.pl. Jedyne, co musimy sami zmienić w skrypcie to ID przystanku oraz linia, która nas interesuje. Za przystanek odpowiada zmienna $busStopId zadeklarowana w 2 linijce kodu. Aby dowiedzieć się jaki jest identyfikator przystanku, który nas interesuje wchodzimy na stronę z rozkładami i otwieramy tablicę z informacjami o odjazdach na żywo. ID przystanku znajduje się w adresie URL.

Kolejna zmienna, która nas interesuje to $line_number zadeklarowana w 26 linijce. Ja na politechnikę dojeżdżam dziesiątką, ale można sobie zmienić na inny numer, lub przerobić kod tak, aby wyświetlał kilka różnych linii.

Możemy, a nawet powinniśmy też zmienić user-agenta zdefiniowanego w 9 linijce kodu na własny :)

Wyjściowy format dla NodeMCU powinien wyglądać tak: DATA:L-T+L:T , gdzie L to numer linii, a T to czas pozostały do odjazdu lub godzina do odjazdu.

Łączymy kabelki

Mamy już ładnie sparsowane dane. Teraz musimy zbudować sobie urządzenie, które je odbierze i wyświetli. Jest to bardzo proste, gdyż wystarczy nam przycisk i ekran do wyświetlania danych. Powinno to wyglądać tak jak na poniższym schemacie:

Kod dla NodeMCU

Kiedy już wszystko podłączone czas na napisanie kodu dla mikroprocesora. Omówię tutaj jego ważniejsze fragmenty.

Na początek musimy ustalić nazwę naszej sieci wifi, hasło oraz lokalizację skryptu php. Definiujemy te wartości w pierwszych linijkach naszego kodu.

const char* ssid     = ""; // nazwa sieci
const char* password = ""; // hasło

const char* host = "192.168.1.105"; // adres serwera, na któym jest skrypt php
String url = "/Przystanki/getDisplayData.php"; // ścieżka do skryptu

W funkcji loop() same proste rzeczy. Sprawdzamy, czy przycisk został wciśnięty. Jeżeli tak to włączamy podświetlenie ekranu, a następnie 4 razy co 15 sekund odświeżamy dane i wyświetlamy je na ekranie z wykorzystaniem funkcji getData() oraz migając przy tym wbudowaną diodą LED. Po około minucie wyłączamy podświetlenie ekranu i czyścimy go ze znaków. Nie robimy w tym czasie nic innego, więc możemy sobie pozwolić na użycie funkcji delay().

Przejdźmy teraz do samej funkcji getData() zdefiniowanej następująco:

void getData() {
  WiFiClient client;
  if(!client.connect(host, 80)) {
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print("Blad sieci!");
    return;
  }
  
  Serial.println(url);
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" + 
               "Connection: close\r\n\r\n");
  delay(500);

  bool gotCorrectData = false;
  while(client.available()){
    String line = client.readStringUntil('\r');
    line.trim();
    if(line.startsWith("DATA:")) {
      printDataToLCD(line);
      gotCorrectData = true;
    }
  }

  if(!gotCorrectData) {
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print("Bledne dane!");
    return;
  }
}

Na samym początku sprawdzamy, czy udało nam się połączyć z serwerem. Jeżeli nie to drukujemy błąd sieci i wychodzimy z funkcji.

Jeżeli udało nam się nawiązać połączenie z serwerem to wysyłamy żądanie HTTP i czekamy na odpowiedź. Odpowiedź z serwera będzie zawierała nie tylko kod HTML, ale także nagłówki takie jak kod odpowiedzi HTTP itp. W tym celu sprawdzamy linijka po linijce, czy dane nie zaczynają się przypadkiem od słówka kluczowego DATA:. Jeżeli tak to bingo, mamy nasze dane. Teraz możemy wywołać funkcję printDataToLCD(String data), która wypisze nam otrzymane dane na ekranie LCD.

Omówmy ją krótko:

void printDataToLCD(String data) {
  String trams[2], times[2];
  data = data.substring(5);

  int line = 0;
  bool isTram = true;

  for(int x = 0; x < 2; x++) {
    trams[x] = "";
    times[x] = "";
  }
  
  for(int x = 0; x < data.length(); x++) {
    if(data.charAt(x) == '+') {
      isTram = true;
      line++;
      continue;
    }

    if(data.charAt(x) == '-') {
      isTram = false;
      continue;
    }

    if(line >= 2) break;
    
    if(isTram) {
      trams[line] += data.charAt(x);
    } else {
      times[line] += data.charAt(x);
    }
  }

  lcd.clear();
  for(int x = 0; x < 2; x++) {
    lcd.setCursor(0,x);
    lcd.print(trams[x]);
    lcd.setCursor(15 - times[x].length(),x);
    lcd.print(times[x]);
  }
}

Na samym początku deklarujemy sobie dwie dwuelementowe tablice Stringów. Rozmiar tablicy jest tutaj równy ilości wierszy na ekranie. Jeżeli dysponujemy ekranem 16×04 lub 20×04 możemy ustawić tam 4. Obcinamy także początkowy ciąg DATA: z naszych danych wejściowych.

  for(int x = 0; x < 2; x++) {
    trams[x] = "";
    times[x] = "";
  }

Następnie za pomocą pętli for przypisujemy puste ciągi znaków do każdego elementu. Tak na wszelki wypadek.

Kolejna pętla sprawdzi nam znak po znaku cały ciąg. Jeżeli trafimy na + to wiadomo, że jest to symbol, który separuje nam kolejne linijki. Ustawiamy więc zmienną isTram na prawdę (mówi nam ona o tym, czy aktualnie odczytujemy czas, czy może numer linii) i przechodzimy do kolejnego znaku.

if(data.charAt(x) == '+') {
      isTram = true;
      line++;
      continue;
    }

Jeżeli trafiliśmy na znak – to jest to informacja o tym, że zmieniamy typ danych z tramwaju na czas. Ustawiamy isTram na fałsz i czytamy kolejny znak.

if(data.charAt(x) == '-') {
      isTram = false;
      continue;
    }

Kolejną czynnością jest sprawdzenie za pomocą instrukcji if, czy wystarczy nam linii do wydrukowania na ekranie. Jeżeli nie to musimy wyjść z pętli.

if(line >= 2) break;

Każdy inny napotkany znak powinien być fragmentem numeru linii lub czasu odjazdu (w zależności od zmiennej isTram).

Kiedy odczytaliśmy już wszystkie znaki czyścimy ekran i wypisujemy na nim nowe dane. Ustawiając kursor na pozycji w poziomie równej 15 – times[x].length() gwarantujemy, że wypisany ciąg będzie wyrównany do prawej strony na 16-sto kolumnowym ekranie. Jest to po prostu pozycja ostatniej kolumny pomniejszona o liczbę znaków w ciągu z czasem do odjazdu.

Testy

Kiedy już wszystko gotowe możemy podłączyć nasze urządzenie gdzieś w widocznym miejscu i cieszyć się informacją o odjazdach z naszego najbliższego przystanku. Niestety tak jak spóźniałem się na tramwaj tak dalej się spóźniam, ale teraz już nie mogę zwalić winy na to, że nie wiedziałem za ile odjazd :)

U mnie urządzenie prezentuje się tak: