Skocz do zawartości

Arduino w modelarstwie kolejowym


Pomocna odpowiedź

Znalazłem w sklepie na K... Pewnie by ruszył na tym programie co mam (parę linijek do zmiany), ale nie chce mi się stówki wydawać na coś co mi się nie przyda 🙂

Ale dobrze wiedzieć, czasem potrzebuję takich maleństw a o tym nawet nie wiedziałem 😞

 

Muszę przeprosić - jeśli ktoś to czyta to po prostu materiału byłoby za dużo, a przecież nie chodzi tu aby zrobić gotową makietę, tylko aby pokazać jak się to robi...

Ale w zamian kol. @prezesedi pewnie opisze swoje boje  z uruchomieniem czytnika na NodeMCU i podzieli się uwagami co do karty. Czekamy!

Tak, więc zacznę od tego, że chyba zmienię nick na "problem". Bo te od początku projektu mnie nie opuszczają. Część to moja wina, część sprzętu.

W tej chwili opiszę ostatni problem - spowodowany sprzętem.

Tak więc okazało się, że ESP nie widzi karty - w żaden sposób. Nie radził sobie z wczytywaniem kodu, gdy karta była w slocie (wyrzucał komunikat, że ESP nie jest podłączone - dziwne). Dzięki podpowiedzi kol. @ethanak spiąłem pin D8 poprzez rezystor 10k z masą - i nagle ESP zaczęło widzieć kartę podczas wczytywania kodu.

Żeby nie było łatwo, to mimo wgrania pliku .txt na kartę (8GB Nokia), ESP nie potrafiło go "przeczytać". Formatowanie karty na FAT32, potem na NTFS i powrotnie na FAT32 nie pomagało. Wziąłem kartę 64GB z systemem plików exFAT - to samo. Wpadłem na pomysł by programem do partycjonowania nadać karcie system plików FAT32. Program się burzył ale na wymusiłem na nim ten system, wgrałem plik z zapowiedziami - I DZIAŁA! Przywróciłem exFAT - nie działa. Tak więc znów na siłę FAT32 i możemy działać dalej.

11 godzin temu, prezesedi napisał:

spiąłem pin D8 poprzez rezystor 10k z masą

A to w ogóle ciekawa sprawa. Ten rezystor powinien być na płytce NodeMCU i bez niego ESP8266 raczej nie ruszy.  Ponieważ po odłączeniu D8 (czyli GPIO15) od czytnika ESP działał pomyślałem, że jedyna możliwość to ta, że nasz żółty przyjaciel pomylił np. 103 i 104 (bo te znaczki co je Długie Nosy wymyśliły to takie dziwne i podobne do siebie) i wsadził tam 100k albo coś podobnego... jak się okazało chyba miałem rację 🙂

Ale wróćmy do dalszego ciągu zabawy z rozkładem jazdy.

Zacznijmy od lekkiej kosmetyki.

Wspominałem wcześniej, że chcę oznaczyć typ pociągu "przyspieszony" literą "S". Dlaczego? Bo po pierwsze P jest już zajęte na "pospieszny", po drugie jeden z takich znanych mi pociągów nosi miano "Strzała" 🙂

Tyle że wyświetlenie literki S na rozkładzie jazdy nie jest chyba tym, co tygrysy lubią najbardziej. Prosiłoby się o wyświetlenie dwóch literek "Pr", ale wtedy tracimy miejsce na nazwę.

Na szczęście biblioteka pozwala nakładać jeden znak na drugi, a "P" o wysokości 2 połączone z lekko przesuniętym "r" o wysokości 1 daje bardzo ciekawy efekt. Zamieńmy więc w pliku display.cpp linię:

    tft.printf("%c", sch->type);

na taki kod: 

  if (sch->type == 'S') {
        tft.print('P');
        tft.setCursor(2,y+8);
        tft.setTextSize(1);
        tft.print('r');
        tft.setTextSize(1,2);
    }
    else tft.print((char )sch->type);

Jak widać, przy okazji pozbyłem się jednego printf na rzecz prostszego print (niestety, przyzwyczajenie).

Po drodze zróbmy trochę porządku.

Ponieważ jak już pisałem, program powinien działać na dowolnym wyświetlaczu TFT SPI, trzeba pozbyć się z kodu wszystkich fragmentów zależnych od wyświetlacza. Oprócz oczywistych definicji wyświetlacza i wywołań funkcji inicjalizacji pozostały tylko nazwy kolorów. Ponieważ wyświetlacze (a przynajmniej większość z nich) pracuje w trybie RGB565, warto chyba móc wybrać sobie dowolne kolory. Co prawda biblioteka Adafruit_GFX przewiduje funkcję rgb565, ale tylko jako metodę instancji displaya. Zacznijmy więc od definicji makra:

#define RGB565(r,g,b) ((((r) & 0b11111000) << 8) | (((g) & 0b11111100) << 3) | ((b) >> 3))

Teraz możemy zdefiniować sobnie dwa podstawowe kolory:

#define SC_BGCOLOR RGB565(0,0,96)
#define SC_FGCOLOR 0xffff

Teraz wystarczy tylko podstawić SD_FGCOLOR i SD_BGCOLOR we wszystkich miejscach gdzie występowały ST77XX_cośtam i już mamy kod niezależny od wyświetlacza. Dodatkowo możemy precyzyjniej sobie ustalić kolor - ja wybrałem ciemnoniebieski na kolor tła, wygląda dużo lepiej niż czarny.

Jak już jesteśmy przy kolorkach - czasem może istnieć potrzeba wyświetlenia jakiejś linii nagłówkowej (np. "ODJAZDY"). Spróbujmy wprowadzić traką możliwość.

Na początki zdefiniujmy sobie w pliku display.cpp ową linię nagłówkową oraz kolory tła i napisu - przykładowo:

#define SC_HEADER_LINE "ODJAZDY"

#define SC_HDRFGCOL 0
#define SC_HDRBGCOL RGB565(192,192,192)

Teraz musimy zmienić położenie na ekranie każdej pozycji rozkładu. Ponieważ robimy to teraz w jednym miejscu, możemy w łatwy sposób umożliwić prawidłowe wyświetlanie w obu sytuacjach (tzn. z linia nagłówkową i bez):

#ifdef SC_HEADER_LINE
#define YPOS(pos) ((pos) * 18 + 20)
#else
#define YPOS(pos) ((pos) * 21 + 2)
#endif

Teraz w funkcji initDisplay trzeba wstawić kod wyświetlający tę linię. Ponieważ linijki się trochę poprzestawiły, teraz kod funkcji będzie wyglądać tak:
void initDisplay()
{
    tft.initR(INITR_BLACKTAB);

    tft.setRotation(3);
    tft.cp437();
    wscreen = tft.width();
    hscreen = tft.height();
    tft.fillScreen(SC_BGCOLOR);
    tft.setTextWrap(0);
#ifdef SC_HEADER_LINE
    tft.fillRect(0,0,wscreen,18,SC_HDRBGCOL);
    tft.setTextColor(SC_HDRFGCOL);
    tft.setTextSize(2);
    tft.setCursor(wscreen/2-strlen(SC_HEADER_LINE) * 6,1);
    tft.print(SC_HEADER_LINE);
    tft.setCursor(wscreen/2-strlen(SC_HEADER_LINE) * 6+1,1);
    tft.print(SC_HEADER_LINE);
#endif
    tft.setTextColor(SC_FGCOLOR);
    
// trochę obliczeń

    pos_name = 10;  // pozycja wyświetlania nazwy
    pos_hour = wscreen - 5 * 6; // pozycja wyświetlania godziny
    pos_tor = pos_hour - 10; // pozycja wyświetlania toru i peronu
    max_namelen = (pos_tor -14) / 6; // maksymalna długość nazwy
}

Po dokonaniu tych zmian możemy się już bawić we włączanie i wyłączanie linii nagłówkowej (poprzez zakomentowanie definicji SC_HEADER_LINE), zmiany kolorów i tak dalej. Ale zajmijmy się czymś poważniejszym, mianowicie skrolowaniem.

Idealnym rozwiązaniem byłoby skrolowanie po pikselu zamiast po znakach. Niestety - biblioteka nie zapewnia bezpośrednio takiej możliwości (a przynajmniej nie przy tym wyświetlaczu). Spróbujmy więc podejść do problemu inaczej.

Można stworzyć obiekt typy canvas (taki wirtualny jednobitowy ekran) na którym będziemy sypisywać odpowiednio przesuniętą nazwę, a potem przygotowany fragment obrazu wyświetlić na ekranie. Tu już bblioteka ma wbudowane potrzebne mechanizmy, a więc zaczynamy.

Dodajmy gdzieś przed funkcją initDisplay następujący kod:

int wcanvas, hcanvas;
GFXcanvas1 *canvas;

a na końcu funkcji initDisplay:

    wcanvas = max_namelen*6;
    hcanvas = 16;
    canvas = new GFXcanvas1(wcanvas, hcanvas);
    canvas->setTextColor(1);
    canvas->setTextSize(1,2);
    canvas->setTextWrap(0);
    canvas->cp437();

W ten sposób mamy już obiekt, który będzie nam służył jako prowizoryczna tablica do wpisywania przesuniętej nazwy.

Teraz musimy zrobić kilka modyfikacji. Zmienimy całkowicie kod funkcji displayTrain:

static void displayTrainName(int pos, struct SchedTrain *sch)
{
    int y=YPOS(pos);

    // przygotowujemy tekst
    
    canvas->fillScreen(0);
    canvas->setCursor(-sch->scrollpos,0);
    canvas->print(sch->name);

    // i wzięte z podręcznika:
    
    tft.drawBitmap(pos_name,y,canvas->getBuffer(),wcanvas, hcanvas, SC_FGCOLOR, SC_BGCOLOR);
}

w funkcji redisplayTrain zmieniamy dosłownie jedną linijkę, dodając mnożenie przez szerokość znaku:

 

       // jeśli doszliśmy do końca, zmieniamy stan na końcowy
        if (sch->scrollpos > sch->scrollsize * 6) {

Różnież w displayLoop konieczna będzie zmiana (teraz przerysowanie musi odbywać się częściej):

void displayLoop()
{
    if (millis() - lastScroll < 60UL) return;

No i po skompilowaniu cieszymy się pięknym, płynnym scrollingiem...

E... znaczy się tego... miał być płynny, ale jeśli poruszają się dwie linijki to scrolling lekko zwalnia, a przy trzech jednocześnie staje się strasznie niemrawy... dlaczego???

Sprawdźmy co tam się dzieje. Podejrzenie pada na funkcję wyświetlania bitmapy, sprawdźmy więc ile to trwa. Zmodyfikujmy nieco kod:

    uint32_t m=millis();
    tft.drawBitmap(pos_name,y,canvas->getBuffer(),wcanvas, hcanvas, SC_FGCOLOR, SC_BGCOLOR);
    printf("TIME %d\r\n", millis() - m);

Po uruchomieniu okazuje się, że wrzucenie tej nieszczęsnej bitmapy do wyświetlacza trwa około 50 msec. No to faktycznie,nie ma prawa działać przy więcej niż jednej skrolowanej linii.

Może zwiększyć prędkość SPI? Można próbować tylko po to, że przy 64 MHz zyskujemy całe dwie milisekundy, a przy 80 MHz (max co może ESP8266) wyświetlacz prezentuje coś w rodzaju rozgwieżdżonego nieba...

Trzeba więc znależć przyczynę. Popatrzmy w kod funkcji drawBitmap... o, ciekawe! Funkcja pobiera sobie wartości pikseli z bitmapy (to jest akurat szybkie), przekształca je na jedną z dwóch wartości (fgcolor, bgcolor) i wykonuje operację writePixel().

Zaraz moment. Jeśli ktoś ma ochotę pooglądać sobie kod to może policzyć, ile bajtów trzeba wysłać do wyświetlacza aby postawić jedną durną kropkę. Pewnie nikomu się nie będzie chciało, więc powiem: dziesięć. Dlaczego tak dużo???

Przyjrzyjmy się funkcji writePixel. W skrócie wygląda tak:

  1. rozpocznij komunikację
  2. ustaw okno robocze o wymiarach 1x1 na pozycji x/y (czyli wysłanie 8 bajtów do wyświetlacza)
  3. wyślij dwa bjty koloru piksela
  4. zakończ komunikację

A gdyby tak ustawić okno robocze na wielkość bitmapy i przesłąć longiem wartości wszystkich pikseli? Spróbujmy!

Utwórzmy funkcję szybkiego kopiowania umieszczając kod przed funkcją displayTrainName:

static void drawBitmapFast(int x, int y, uint16_t fgcolor, uint16_t bgcolor)
{
    tft.startWrite();
    tft.setAddrWindow(x,y,wcanvas,hcanvas);
    int i,j;
    for (j=0;j<hcanvas;j++) for (i=0;i<wcanvas;i++) {
        uint16_t color = canvas->getPixel(i,j) ? fgcolor: bgcolor;
        tft.SPI_WRITE16(color);
    }
    tft.endWrite();
}

Teraz w displayTrainName zakomentujmy linię zawierającą wywołanie drawBitmap i wpiszmy w jej miejsce:

    drawBitmapFast(pos_name, y, SC_FGCOLOR, SC_BGCOLOR);

Kompilujemy, uruchamiamy i... mogliśmy się spodziewać pięciokrotnego skrócenia czasu a jest nawet lepiej: wywołanie tej funkcji trwa poniżej 8 msec!

Możemy teraz usunąć lub zakomentować kod pomiarowy. Ponieważ w przyszłości będziemy używać WiFi - warto dodać na końcu funkcji displayTrainName wywołanie yield - tak więc cały kod będzie się przedstawiał następująco:

static void displayTrainName(int pos, struct SchedTrain *sch)
{
    int y=YPOS(pos);

    // przygotowujemy tekst
    
    canvas->fillScreen(0);
    canvas->setCursor(-sch->scrollpos,0);
    canvas->print(sch->name);

    // i wysyłamy do wyświetlacza

    drawBitmapFast(pos_name, y, SC_FGCOLOR, SC_BGCOLOR);

    yield();
}

Tak więc kwestię wyświetlacza możemy uznać za na razie rozwiązaną, przejdźmy więc do odczytu rozkładu z karty. Powinien działać w ten sposób:

Jeśli nie odczytano pliku z karty, następuje próba odczytu z wewnętrznej pamięci flash. W przypadku jego braku zastosowany będzie rozkład demo wkompilowany w program.

Jeśli odczytano plik z karty, również następuje próba odczytu z wewnętrznej pamięci. Jeśli próba się powiodła i rozkłady są identyczne, nie robimy nic. W przeciwnym przypadku nowy rozkład będzie skopiowany do pamięci wewnętrznej.

Zacznijmy więc od kodu odpowiedzialnego za obsługę tej wewnętrznej pamięci. Zastosujemy tu LittleFS jako wskazany do stosowania w nowych aplikacjach. Czyli - bierzemy na warsztat plik sd.cpp.

Przede wszystkim musimy włączyć obsługę LittleFS. W tym celu dodajemy kolejną dyrektywę include:

#include <Arduino.h>
#include <SD.h>
#include <LittleFS.h>
#include "rozklad.h"

Dalej przyszła kolej na rozwiązanie dylematu. Mam funkcję getSD, która zwraca odczytany plik i do której odwołuję się w innej części programu. Teraz jednak ta funkcja będzie bardziej skomplikowana - musi obsługiwać odczyt z LittleFS i ewentualne kopiowanie. Miałem do wyboru:

modyfikację kodu getSD (totalnie bez sensu, ta funkcja robi jedną konkretną rzecz)
napisanie innej funkcji korzystającej z getSD (wymagałoby to zmian w kolku miejscach poza tym plikiem)
pozostawienie funkcji jak jest, zmienienie tylko jej nazwy i zakresu oraz napisanie nowej funkcji getSD

Wybrałem trzecie rozwiązanie. Tak więc dawna funkcja getSD otrzymuje nową nazwę i nie będzie widoczna spoza pliku sd.cpp, czyli zmieniamy:

const char *getSD()

na

static const char *readSD()

Teraz można napisać nową funkcję getSD(), czyli coś w rodzaju:

const char *getSD()
{
    const char *txt = readSD();
    const char *txt2 = readLittleFS();

 // i tu sprawa byłaby prosta...

Tak, byłaby prosta gdyby nie możliwość skopiowania zawartości txt do pliku na LittleFS. Jako że funkcja odczytująca rozkład powinna zamknąć filesystem, trzeba by go było otwierać na nowo, a ja nie lubię czynności niepotrzebnych. Postanowiłem inaczej: do funkcji readLittleFS przekażę wartość odczytaną z karty i potem się będę zastanawiał co robić. Tak więc funkcja getSD będzie wyglądać tak:

const char *getSD()
{
    const char *txt = readSD();
    return readLFS(txt);
}

Spróbuję teraz napisać funkcję readLFS. Będzie bardzo podobna do readSD, a więc zaczynamy:

const char *readLFS(const char *sch)
{
    File plik;
    if (!LittleFS.begin()) {
        printf("Nie mogę zamontować LittleFS\r\n");
        return sch;
    }
    char *txt = NULL;
    plik=LittleFS.open("/rozklad1.txt","r");
    if (!plik) {
        printf("Brak pliku na LittleFS\r\n");
    }
    else {
        int size=plik.size();
        printf("LFS rozklad1.txt rozmiar %d\r\n", size);
        txt=(char *)malloc(size+1);
        plik.read((uint8_t *)txt, size);
        plik.close();
        txt[size]=0;
    }

... i w tym miejscu mamy już oba rozkłady przeczytane, a LittleFS nie jest jeszcze zamknięty. Pozostało napisać kod, który wybierze właściwą wersję. Ponieważ lubię, gdy dana funkcja robi jedną dobrze określoną rzecz, zakończę funkcję readLFS:

    sch = selectVersion(sch, txt);
    LittleFS.end();
    return sch;
}

Kolej na funkcję selectVersion.

static const char *selectVersion(const char *sch, const char *txt)
{
    // nie było karty i nie ma z czego wybierać?
    if (!sch) return txt;
    
    if (txt && !strcmp(txt,sch)) {
        printf("Pliki są identyczne\r\n");
        free((void *)txt); // już niepotrzebne
        return sch;
    }
    // teraz mamy sytuację, że albo nie było pliku txt
    // albo różni się on od nowego pliku z karty. W oby
    // tych przypadkach należy zawartość sch wpisać
    // do pliku na LittleFS
    
    if (txt) free((void *)txt); // już niepotrzebne
    printf("Kopiuję rozkład do LittleFS\r\n");
    File plik=LittleFS.open("/rozklad1.txt","w");
    if (!plik) {
        printf("Nie mogę otworzyć pliku do zapisu\r\n");
    }
    else {
        plik.write((uint8_t *)sch, strlen(sch));
        plik.close();
    }
    return sch;
}

Funkcje umieszczamy w kodzie w kolejności umożliwiającej ich prawidłową widoczność bez konieczności oddzielnego deklarowania, czyli będą to kolejno:

  1. checkSched
  2. compactSched
  3. readSD
  4. selectVersion
  5. readLFS
  6. getSD

Po skompilowaniu i wgraniu możemy sprawdzić, jak zachowa się nasz program. Kod jak zwykle w załączniku: rozklad3.zip

Cóż - to jeszcze nie wszystko. Program co prawda działa, ale warto parę rzeczy jeszcze ulepszyć, a więc: stay tuned!

 

  • Lubię! 2

Ale się zawzioles @ethanak ...ja już na samym początku odpadłem z powodu zbyt wysokiego poziomu i nie wiem co Ty tu tworzysz, ale widać że chcesz to ogarnąć na najwyższym poziomie...gratuluję samozaparcia👍👍👍

  • Lubię! 2
50 minut temu, farmaceuta napisał:

ja już na samym początku odpadłem z powodu zbyt wysokiego poziomu i nie wiem co Ty tu tworzysz

To wcale nie jest taki wysoki poziom, wydaje Ci się. A co ja tworzę? Chcę na przykładzie pokazać, jak drobnymi krokami ze stosunkowo prostej aplikacji (pierwsza moja wersja) dojść do czegoś bardziej skomplikowanego. Radzę na spokojnie sobie poczytać, zacząć od pierwszej wersji kodu i podpatrywać, w jaki sposób go poprawiam czy dopisuję nowe rzeczy.

 

  • Lubię! 2

No dobra - trzeba wreszcie skończyć z tym rozkładem jazdy, bo ja osobiście mam już go dość. 

Ale do rzeczy.

Przede wszystkim - wychodzimy z radosnego założenia, że plik jak już jest to się zawsze wczyta. Tymczasem to nie do końca prawda - różne czynniki mogą wpłynąć na błędy odczytu Spróbujmy więc temu zaradzić.

Wiadomo, że funkcja read(buf, size) zwraca ilość rzeczywiście wczytanych znaków. Znając rozmiar pliku na karcie czy lokalnym systemie plików w bardzo prosty sposób możemy sprawdzić, czy nie było błędów odczytu, po prostu porównując wartość zwracaną przez read z oczekiwaną długością pliku. Spróbujmy więc lekko zmodyfikować funkcję readSD. Zamieniamy fragment kodu:

        plik.read(txt,s);
        plik.close();
        txt[s]=0;
        compactSched((char *)txt);
        if (!checkSched((const char *)txt)) {
            free(txt);
            txt=NULL;
        }

na nieco bardziej skomplikowany:

        // czy plik wczytał się prawidłowo?
        if (plik.read(txt,s) != s) {
            printf("Błąd wczytania pliku\r\n");
            free(txt);
            txt=NULL;
        }
        else {
            txt[s]=0;
            compactSched((char *)txt);
            if (!checkSched((const char *)txt)) {
                free(txt);
                txt=NULL;
            }
        }
        plik.close();


Podobnie zrobimy w funkcji readLFS, zamieniając istniejący fragment:

   

        plik.read((uint8_t *)txt, size);
        plik.close();
        txt[size]=0;

 
na:

        if (plik.read((uint8_t *)txt, size) != size) {
            printf("Błąd czytania z LittleFS\r\n");
            free(txt);
            txt=NULL;
        } else {
            txt[size]=0;
        }
        plik.close();
 


Prawdopodobnie trudno nam będzie sprawdzić reakcję programu na błąd - po prostu skompilujmy poprawiony program aby upewnić się że gdzieś nie popełniliśmy błędu i przejdźmy do następnego problemu, mianowicie skrolowania.

Nie wszystkim podoba się taki sposób jak zastosowany przeze mnie. Lepiej, aby tekst po prostu przewinął się do pozyzji początkowej i tam na chwilę pozostał. Spróbujmy więc zmodyfikować plik display.cpp tak, aby można było wybrać pasującą możliwość.

Niech wyznacznikiem sposobu skrolowania będzie wystąpienie jakiejś definicji. Tak więc gdzieś na początku umieśćmy linię:

#define SC_MODE_ROLL

oznaczającą, że korzystamy z drugiego sposobu skrolowania. Teraz musimy to tylko wykorzystać.

W funkcji displayTrainName musimy zaraz za linią

    canvas->print(sch->name);

dopisać:

#ifdef SC_MODE_ROLL
    if (sch->scrollsize) {
        canvas->print(" \xb7 ");
        canvas->print(sch->name);
    }
#endif

Jak widać, przy nowych ustawieniach funkcja dopisuje znak podziału i powiela tekst. Można to zrobić w inny sposób, ale ten jest akurat najprostszy.

Musimy też zmienić sposób wykrywania zakończenia skrolowania. Odpowiednia gałąź w funkcji redisplayTrain będzie teraz wyglądać tak:

        case SCROLL_SCROLLING:

#ifdef SC_MODE_ROLL
        // jeśli doszliśmy do końca, zmieniamy stan na początkowy
        if (sch->scrollpos > sch->namelen * 6 + 18) {
            sch->scrollphase = SCROLL_START;
            sch->scrollpos=0;
            sch->timer = millis();
            displayTrainName(pos, sch);
#else
        // jeśli doszliśmy do końca, zmieniamy stan na końcowy
        if (sch->scrollpos > sch->scrollsize * 6) {
            sch->scrollphase = SCROLL_END;
            sch->timer = millis();
#endif
            return;
        }
        // w przeciwnym razie wyświetlamy przesuniętą nazwę
        // i zwiększamy przesunięcie
        displayTrainName(pos, sch);
        sch->scrollpos ++;
        break;


I to wszystko. Możemy teraz skompilować program, wgrać go na płytkę i wybrać odpowiadający nam sposób skrolowania.

Teraz jeszcze wrócę do odczytu plików. Co prawda program nam pięknie wypisuje na wyjściu Serial co robi, ale w rzeczywistych warunkach raczej nie będziemy mieli podłączonego tam komputera. A dobrze by było wiedzieć, czy coś nieprzewidzianego się nie stało...

Niech więc przy starcie programu na wyświetlaczu znajdą się informacje o plikach a dopiero po chwili program przejdzie do właściwego wyświetlania rozkładu.

Zacznijmy jednak od koniecznych poprawek. W pliku rozklad.cpp mamy sobie funkcję displaySched. Mało tego, że nieco niefortunnie nazwana (bo nazwa sugeruje funkcję bezpośrednio związaną z wyświetlaczem), to jeszcze zupełnie niepotrzebnie przekazywana jest jako parametr wartość zmiennej globalnej, do której przecież cały czas mamy dostęp! Zmieńmy więc nieco tę funkcję:

void showSched()
{
    const char *c=textLine(sched,schedPos);
    if (!c) {
        // jesteśmy poza rozkładem, wracamy do początku
        schedPos=0;
        c=sched;
    }
    int i;
    for (i=0; i< SCHED_SIZE; i++) {
        if (parseScheduleLine(&SchedTrain[i], &c)) continue;
        // wyszliśmy poza rozkład, musimy kontynuować od początku
        c=sched;
        parseScheduleLine(&SchedTrain[i], &c);
    }
    for (i=0; i<SCHED_SIZE; i++) displayTrain(i);
    lastSched = millis();
}

Musimy dodatkowo zmienić tylko w funkcjach nextSched i initSched linijki:

    displaySched(sched);

na 

    showShed();

Możemy teraz na wszelki wypadek skompilować i wgrać tak poprawiony program.

No i teraz więcej roboty.

Załóżmy, że istnieje jakaś 16-bitowa zmienna, w której znajdzie się informacja o tym jak przebiegało czytanie plików, a każdy bit będzie miał odpowiednie znaczenie. Dodajmy więc do pliku rozklad.h przed kończącym #endif taki fragment kodu:

// bity komunikatów
enum {
    SC_INFO_NO_CARD=0,
    SC_INFO_NO_FILE,
    SC_INFO_BAD_FILE,
    SC_INFO_READ_ERROR,
    SC_INFO_READ_OK,
    SC_INFS_BAD_MOUNT,
    SC_INFS_NO_FILE,
    SC_INFS_READ_ERROR,
    SC_INFS_READ_OK,
    SC_INFS_WRITE_ERROR,
    SC_INFS_NEW_FILE,
    SC_INFO_USE_OLD,
    SC_INFO_USE_NEW,
    SC_INFO_USE_BUILTIN,
    SC_INFO_MAX
};

// maska bitów oznaczajęcych, że komunikat jest informacją a nie błędem
#define SC_INFO_NE ((1<<SC_INFO_NO_CARD) | (1<<SC_INFO_READ_OK) | \
(1<<SC_INFS_READ_OK) | (1<<SC_INFS_NEW_FILE) | \
(1<<SC_INFO_USE_OLD) | (1<<SC_INFO_USE_NEW))

// makro sprawdzające, czy komunikat o danym numerze jest błędem
#define SC_INFO_IS_ERROR(a) (!((1<<(a)) & SC_INFO_NE))

extern uint16_t sc_info;

Teraz musimy zmusić funkcję getSD do ustawienia odpowiednich bitów. Zabieramy się znowu za plik sd.cpp. Musimy przede wszystkim zdefiniować zmienną sc_info, wyzerować ją na początku i przy każdej operacji dodawać jakiś bit. Czyli końcowy fragment będzie wyglądać tak:

uint16_t sc_info;

const char *getSD()
{
    sc_info = 0;
    const char *txt = readSD();
    txt = readLFS(txt);
    // kontrolnie wypisujemy słowo info
    printf("INFO %04x\r\n", sc_info);
    return txt;
}

No i teraz wstawiamy sobie ustawianie odpowiedniego bitu w różne miejsca w kodzie. Zacznijmy od checkSched, kod tej funkcji zostanie "wzbogacony" o kilka linijek:

static bool checkSched(const char *txt)
{
    struct SchedTrain st;
    int n;
    for (n=0;parseScheduleLine(&st,&txt);n++) ;
    while (*txt && isspace(*txt)) txt++;
    if (*txt) {
        sc_info |= 1<<SC_INFO_BAD_FILE;
        printf("Śmieci w pliku rozklad1: %s\r\n", txt);
        return false;
    }
    if (n < 7) {
        sc_info |= 1<<SC_INFO_BAD_FILE;
        printf("Za mało pozycji w rozkładzie\r\n");
        return false;
    }
    sc_info |= 1<<SC_INFO_READ_OK;
    return true;
}

Nie będę może dalej pokazywał całych funkcji, a jedynie ich fragmenty gdzie zostały wstawione odpowiednie linie kodu.

static const char *readSD()
{
    File plik;
    if (!SD.begin(15)) {
        sc_info |= 1<<SC_INFO_NO_CARD;
        printf("Brak karty lub karta nieczytelna\r\n");
...
    plik = SD.open("/rozklad1.txt");
    if (!plik) {
        sc_info |= 1<<SC_INFO_NO_FILE;
        printf("Brak pliku rozklad1.txt\r\n");
...
static const char *selectVersion(const char *sch, const char *txt)
{
    // nie było karty i nie ma z czego wybierać?
    if (!sch) {
        if (txt) sc_info |= 1<<SC_INFO_USE_OLD;
        else sc_info |= 1<<SC_INFO_USE_BUILTIN;
        return txt;
    }
    
    if (txt && !strcmp(txt,sch)) {
        if (txt) sc_info |= 1<<SC_INFO_USE_OLD;
        printf("Pliki są identyczne\r\n");
...

    if (!plik) {
        sc_info |= 1<<SC_INFS_WRITE_ERROR;
        printf("Nie mogę otworzyć pliku do zapisu\r\n");
    }
    else {
        sc_info |= 1<<SC_INFS_NEW_FILE;
        plik.write((uint8_t *)sch, strlen(sch));
...
const char *readLFS(const char *sch)
{
    File plik;
    if (!LittleFS.begin()) {
        sc_info |= 1<<SC_INFS_BAD_MOUNT;
        printf("Nie mogę zamontować LittleFS\r\n");
...
    if (!plik) {
        sc_info |= 1<<SC_INFS_NO_FILE;
        printf("Brak pliku na LittleFS\r\n");
    }
...
        if (plik.read((uint8_t *)txt, size) != size) {
            sc_info |= 1<<SC_INFS_READ_ERROR;
            printf("Błąd czytania z LittleFS\r\n");
            free(txt);
            txt=NULL;
        } else {
            sc_info |= 1<<SC_INFS_READ_OK;

Można teraz skompilować kod i uruchomić - zobaczymy, że na monitorze pojawia się informacja o zmiennej sc_info.

Weźmy się teraz za plik display.cpp. Musimy dodać funkcję wyświetlania informacji:

static bool infoMode;
static uint32_t infoStart;

static const char * const infonames[]={
    "Brak karty SD",
    "Brak pliku na karcie",
    "B\222\204dny plik na karcie",
    "B\222\202d odczytu karty",
    "Plik na karcie OK",
    "B\222\202d montowania LFS",
    "Brak pliku na LFS",
    "B\222\202d odczytu LFS",
    "Plik na LFS OK",
    "B\222\202d zapisu LFS",
    "Skopiowano nowy plik",
    "U\214ywam starej wersji",
    "U\214ywam nowej wersji",
    "U\214ywam wersji wbudowanej"};

void displayInfo()
{
    int i,pos;
    tft.setTextSize(1,2);
    for (i=0, pos=20;i<SC_INFO_MAX;i++) if (sc_info & (1<<i)) {
        // centrujemy napis
        tft.setCursor(wscreen/2-3*strlen(infonames[i]),pos);
        //kolor zielony dla informacji, czerwony dla nłędu
        tft.setTextColor(SC_INFO_IS_ERROR(i) ? RGB565(255,0,0) : RGB565(0,255,0));
        tft.print(infonames[i]);
        pos += 20;
    }
    infoMode = true;
    infoStart = millis();
}

Ponieważ teraz na początku będą wyświetlane informacje, nie można już w funkcji initDisplay rysować tła rozkładu. Trzeba więc strorzyć nową funkcję:

const void initBackground()
{
    tft.fillScreen(SC_BGCOLOR);
#ifdef SC_HEADER_LINE
    tft.fillRect(0,0,wscreen,18,SC_HDRBGCOL);
    tft.setTextColor(SC_HDRFGCOL);
    tft.setTextSize(2);
    tft.setCursor(wscreen/2-strlen(SC_HEADER_LINE) * 6,1);
    tft.print(SC_HEADER_LINE);
    tft.setCursor(wscreen/2-strlen(SC_HEADER_LINE) * 6+1,1);
    tft.print(SC_HEADER_LINE);
#endif
    tft.setTextColor(SC_FGCOLOR);
}

a w samej funkcji initDisplay dokonac potrzebnych zmian:

void initDisplay()
{
    tft.initR(INITR_BLACKTAB);
    tft.setRotation(3);
    tft.cp437();
    wscreen = tft.width();
    hscreen = tft.height();
    tft.setTextWrap(0);
    tft.fillScreen(0);
// trochę obliczeń

    pos_name = 10;  // pozycja wyświetlania nazwy
    pos_hour = wscreen - 5 * 6; // pozycja wyświetlania godziny
    pos_tor = pos_hour - 10; // pozycja wyświetlania toru i peronu
    max_namelen = (pos_tor -14) / 6; // maksymalna długość nazwy

    wcanvas = max_namelen*6;
    hcanvas = 16;
    canvas = new GFXcanvas1(wcanvas, hcanvas);
    canvas->setTextColor(1);
    canvas->setTextSize(1,2);
    canvas->setTextWrap(0);
    canvas->cp437();
    infoMode = true;
    infoStart = millis();
}

W funkcji realizującej pętlę wyświetlania dodajemy dodatkowy kod odpowiedzialny za wyłączenie informacji po pewnym czasie:

void displayLoop()
{
    if (infoMode) {
        if (millis() - infoStart < 10000UL) return; // 10 sekund
        infoMode = false;
        initBackground();
        showSched();
        lastScroll = millis();
        return;
    }
        
    if (millis() - lastScroll < 60UL) return;
    int i;
    lastScroll=millis();
    for (i=0;i<SCHED_SIZE;i++) redisplayTrain(i);
}

Pozostają nam jeszcze dwie drobne zmiany.

W pliku rozklad.cpp w funkcji initSched nie będziemy wyświetlać od razu rozkładu, a informacje. Czyli:

void initSched()
{
    const char *newsched = getSD();
    if (newsched) sched = newsched;
    schedPos = 0;
    displayInfo();
}

Funkcje showSched i displayInfo muszą być teraz globalne, czyli w pliku rozklad.h dopisujemy jeszcze:

extern void showSched();
extern void displayInfo();

Można to sobie skompilować, wrzucić na ESP i cieszyć się działającym rozkładem jazdy. Kod jak zwykle w załączniku: rozklad4.zip

I to już wszystko. Rozkład jazdy w wersji standalone jest ukończony, wrócimy do niego jeszcze kiedy będziemy realizować radiowe połączenie między panelem a układami wykonawczymi, ale musimy wrócić do rozgrzebanych na razie semaforów... 

Od razu mówię: stworzenie pełnego programu wymaga oprócz napisania całkiem nowego kodu dla ESP8266 obsługującego semafor dopisania całej maszynerii związanej z transmisją radiową do kodu panela sterującego. Tak więc od razu informuję: będę tworzyć kod dla semafora na ESP8266, ale na ESP32 trzeba będzie napisać prosty kod demo. Trochę to potrwa, ale kolegów którzy uważają że to jakiś wybitnie wysoki poziom zapewniam, że wcale nie i zachęcam do dalszej lektury. Czyli jak zwykle: stay tuned!

  • Lubię! 2

Mistrzostwo z tym przewijaniem. Ta zmiana kolorów również dała fajny efekt. Dziś bez zakłóceń.

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...