Skocz do zawartości

Arduino w modelarstwie kolejowym


Pomocna odpowiedź

Czyli widzisz że coś przeszkadza. Postaram się w następnej wersji zrobić jakąś uczciwą regulację (maksymalizacja mocy nadajników, ilosć powtórzeń, czas między powtórzeniami).

Łatwo domyślić się co - albo wszystko jednocześnie:

- odległość (5-6m)

- 2 ściany z żelbetu (wiadomo, zbrojenie zakłóca)

- idealnie na linii prostej mam domowy router z Vectry (2,4 i 5 GHz)

- stoi także w linii wentylator (kupa aluminium i stali)

Tak więc sygnał łatwo nie ma.

No, w makiecie raczej nie będzie po drodze wentylatora i routera 🙂 Poza tym wemosa można bardzo ładnie schować żeby udawał jakiś inny element kolejowej infrastruktury (np. jakąś wolnostojacą skrzynkę od prądu) 🙂

A w razie czego zawsze można wrzucić ESP12F czy jakuś odpowiednik z zewnętrzną antenką udającą choinkę albo słup telegraficzny...

  • Lubię! 1

Tak na szybko:

Dla semafora w pliku wifi.cpp trzeba dorzucić jedną linijkę w funkcji initWiFi, czyli:

    esp_now_register_send_cb(onDataSent);
    WiFi.setOutputPower(20.5); // dopisana linijka
    // no i prosimy o dostarczenie ustawień
    dataValid=false;
    askData();
}

Dla panelu podobnie:

    startenow();
    startWebServer();
    WiFi.setTxPower(WIFI_POWER_19_5dBm); // dopisana linijka
}

Ciekawe jakie teraz będą straty.

(edytowany)

Jako że kwestię semaforów mamy już za sobą, można teraz zająć się rozkładem jazdy.

I na wstępie: dzisiaj kod samego rozkładu jazdy, do tego dorzucam kod dla ESP32 (prowizoryczny nadajnik, powinien pobrać plik rozkładu i wyświetlić go na monitorze serial, a potem co 15 sekund zmieniać pozycję w rozkładzie).

No i na początku mamy jedno utrudnienie: ESP8266 obsługujący rozkład powinien przekazać do głównego układu (panel) plik z rozkładem jazdy - tymczasem protokół esp-now może przekazać najwyżej 250 bajtów. Trzeba zrobić coś, co umożliwi transfer większej ilości danych... no, ale o tym później. Na początek kilka drobnych zmian w plikach.

Plik wifi.cpp na dobrą sprawę jest taki sam jak w przypadku semafora, czyli mogłem go sobie stamtąd skopiować. Dochodzi tylko kilka zmian. Przede wszystkim ID to teraz 0x30 (gdzieś na początku pisałem o przyporządkowaniu adresów MAC). Do tego obiecane wcześniej zmiany (podwyższenie mocy i możliwość ustawienia sobie ilości powtórek oraz czasu między powtórkami). A więc zaczynamy.

#define MY_ID 0x30

#define RETRY_COUNT 10
#define RETRY_TIME random(40,60)

Tu chyba wszystko jest jasne... Natomiast w funkcji onDataReceive mamy dodatkową instrukcję:

static void onDataReceive(uint8_t *mac,uint8_t *data,uint8_t len)
{
    useAuto=false; // wyłączamy tryb demo
    // jeśli dostaliśmy aktualne dane
    // zapamiętujemy to
    if (runCommand(data, len)) {
        dataValid=true;
        lastReceived=millis();
    }
}

Ponieważ program startuje w trybie demonstracyjnym, ustawienie useAuto na false będzie informować program, że tryb demo został wyłączony i od tej pory musi reagować wyłącznie na sygnały z panela.

Doszedł jeszcze jeden warunek w funkcji wifiLoop:

void wifiLoop()
{
#if MY_ID == 0x30
    // w przypadku rozkładu tylko jeśli jest potwierdzenie,
    // że panel ma aktualny rozkład
    if (masterHasScd) { 
#endif
    // jeśli dane są nieważne lub upłynęło za dużo czasu
    // od ostatniego ustawienia poproś o dane
    if (dataValid) {
        if (millis() - lastReceived >=10000) {
            dataValid=false;
            askData();
        }
    }
    else {
        if (millis() - lastAsk >=1000UL) askData();
    }
#if MY_ID == 0x30
    }
#endif
    retrySend();
}

Semafor prosił o powtórzenie ustawień po 10 sekundach. Tu też jest podobnie, ale tylko wtedy gdy panel potwierdził transfer rozkładu jazdy poprzez ustawienie zmiennej masterHasScd.

Również w funkcji initWiFi mamy zmianę:

#if MY_ID == 0x30
    // wysyłamy rozkład
    sendFullData();
#else
    // albo prosimy o dostarczenie ustawień
    askData();
#endif
}

Semafor na początku działania wysyła prośbę o ustawienia. Rozkład zamiast tego próbuje wysłać do panela aktualny plik.

To tak mniej więcej wszystko jeśli chodzi o wifi.cpp - niewielkie zmiany są również w pliku rozkład.cpp:

// zmienna sched jest teraz globalna

const char *sched="P Kraków 1/1 10:30\n\
O Bielsko-Biała    Komorowice 3/2 10:45\n\
P Katowice 4/2 11:30\n\
E Białystok 2/1 12:25\n\
E Szczecin    Główny 1/1 13:40\n\
O Żywiec 3/2 16:40\n\
O Katowice 4/2 17:50\n\
P Częstochowa 3/2 18:20\n\
E Warszawa Wschodnia 1/1 21:30";

// zmienna schedPos również

int schedPos;

Te zmienne będą potrzebne zarówno do przesłania pliku rozkładu do panela, jak i późniejszej zmiany pozycji w rozkładzie.

// useAuto oznacza, że rozkład pracuje w trybie demo

bool useAuto=true;

void schedLoop()
{
    if (!useAuto) return;
    if (millis() - lastSched < 120000UL) return;
    nextSched();
}

Tutaj właśnie mamy użycie zmiennej useAuto (jeśli jest false - nie będzie automatycznej zmiany pozycji).

Najmniej zmian wymaga plik display.cpp - dochodzi tam tylko jedna funkcja:

bool isInfo() {return infoMode;}

Jej zadaniem jest poinformowanie głównego programu, że aktualnie wyświetlane są informacje wstępne i nie należy wymuszać wyświetlania nowego rozkładu.

Następnym plikiem, gdzie nie trzeba wielu zmian jest główny plik *.ino:

#include "rozklad.h"

void setup()
{
    Serial.begin(115200);
    delay(1000); // tak z przyzwyczajenia
    initDisplay();
    initSched();
    initWiFi();
}

void loop()
{
    schedLoop();
    displayLoop();
    transLoop();
    wifiLoop();
    delay(1); // ESP8266 to lubi
}

Doszły tylko: inicjalizacja WiFi w setup(), wywołanie wifiLoop() w loop() i tajemnicza funkcja transLoop. Ale o niej właśnie za chwilę opowiem.


Dodatkowym plikiem, którego wcześniej nie było jest ecommand.cpp, obsługujący wszystkie polecenia od panela. Przede wszystkim zadeklarowana jest struktura przesyłana do panela przy transferze plików:

struct fileTransCtl {
    uint8_t cmd;    // komenda
    uint8_t nr;     // numer części pliku
    uint8_t tnr;    // ilość części w pliku
    uint8_t datalen;    // długość bieżących danych
    uint16_t totlen;    // całkowita wielkość pliku
    uint16_t filepos;   // pozycja w pliku bieżącej części
    uint8_t databuf[240];   
};    

static struct fileTransCtl fctl; // bufor danych do wysłania


Mamy tu teraz kilka ważnych zmiennych, odpowiadających za transfer:

bool masterHasScd=false; // czy panel potwierdził odebranie pliku

static const char *scdPart; // skąd kopiować dane do bufora
static uint16_t scdPartLen; // długość danych w buforze
static uint16_t scdTotalLen; // rozmiar całego pliku
static uint16_t scdPartPos; // pozycja danych w pliku
static uint8_t scdPartNo; // numer paczki danych
static uint8_t scdPartTot; // ilość wszystkich paczek 

static uint32_t fileTimer; // taki sobie ogólny timerek

Następną ważną zmienną jest transmitterState. Określa ona stan nadajnika (tzn. co trzeba wyałać do panela). Gdzieś tam dalej jest prosta maszyna stanów która realizuje nadawanie określonych danych:

static uint8_t transmitterState;

enum {
    TSCD_TRANS_RELAX=0,
    TSCD_TRANS_FILE,        // muszę wysyłać plik rozkładu
    TSCD_TRANS_FILE_PART,   // muszę wysyłać kolejną część pliku
    TSCD_TRANS_WAIT_FILE,   // czekam na potwierdzenie odbioru
    TSCD_TRANS_SEND_POS     // muszę wysłać pozycję do mastera
};

    
Kolej na główną funkcję realizującą transfer pliku. Może być ona wywołana z parametrem stanowiącym wskaźnik do wysyłanego tekstu (wtedy jest to traktowane jako początek wysyłania) lub bez parametru (oznacza to wysyłanie kolejnej części).

Za pierwszym razem ustawiane są wszystkie zmienne kontrolujące transfer, a do panela przesłana będzie informacja, że to początek pliku (ENOW_MY_FULL_DATA). Przy kolejnych wywołaniach będzie wysyłana informacja, że to kolejna część (ENOW_MY_FULL_DATA_PART). Pozwoli to programowi w panelu przygotować się do odbioru i kontrolować czy odebrana część jest tą właściwą. Ponieważ za każdym razem wysyłany jest komplet metadanych, kontrola może sprowadzać się do sprawdzenia np. zgodbości wielkości pliku z tą otrzymaną na początku i zgodności numeru części z oczekiwanym.

 

static void transferScd(const char *txt = nullptr)
{
    if (txt) { // początek transmisji
        masterHasScd = false;
        scdTotalLen = scdPartLen = strlen(txt);
        scdPartTot = (scdTotalLen+239)/240;
        scdPart=txt;
        scdPartPos = 0;
        scdPartNo = 0;
        fctl.cmd=ENOW_MY_FULL_DATA;
        
    }
    else {
        if (scdPartNo >= scdPartTot) {
            transmitterState = TSCD_TRANS_RELAX;
            masterHasScd=true;
            return;
        }
        fctl.cmd=ENOW_MY_FULL_DATA_PART;
    }
    transmitterState = TSCD_TRANS_WAIT_FILE;
    fctl.totlen = scdTotalLen;
    fctl.filepos = scdPartPos;
    fctl.nr = scdPartNo++;
    fctl.tnr = scdPartTot;
    fctl.datalen = (scdPartLen > 240) ? 240: scdPartLen;
    memcpy(fctl.databuf, scdPart, fctl.datalen);
    scdPart += fctl.datalen;
    scdPartPos += fctl.datalen;
    scdPartLen -= fctl.datalen;
    sendToPanel((uint8_t *)&fctl, fctl.datalen+8);
}

O ile poprzednia funkcja realizuje fizyczny transfer, o tyle następna wywoływana jest po potrzymaniu polecenia z panela. Ta funkcja działa w uprzywilejowanym wątku (callback onDataReceive), dlatego też nie wykonuje żadnych skomplikowanych czynności - w miejsce tego co najwyżej zmienia stan globalnych zmiennych. Funkcja pomocnicza sendFullData również tylko ustawia wartości zmiennych:

void sendFullData()
{
    transmitterState = TSCD_TRANS_FILE;
    masterHasScd = false;
}

Jak widać, ustawia tylko stan głównej maszyny stanów i zapisuje sobie, że panel nie ma aktualnych danych.Ale wróćmy do funkcji runCommand:

bool runCommand(const uint8_t *data, int len)
{
    uint8_t cmd = *data++;
    len--;
    bool rc=false;
    switch(cmd) {

        case ENOW_GIMMA_FULL_DATA:
        //panel prosi o wysłanie rozkładu
        sendFullData();
        break;

        case ENOW_ACCEPTED_DATA_PART:
        // panel potwierdził przyjęcie części rozkładu
        if (scdPartNo >= scdPartTot) {
            // kończę wysyłanie jeśli cały plik został presłany
            transmitterState = TSCD_TRANS_RELAX;
            masterHasScd=true;
        }
        else {
            // wysyłam następną część pliku
            transmitterState = TSCD_TRANS_FILE_PART;
        }
        break;

        case ENOW_GIMMA_DATA:
        // panel prosi o podanie bieżącej pozycji w rozkładzie
        transmitterState = TSCD_TRANS_SEND_POS;
        break;

        case ENOW_SCD_SET_POS:
        // panel wysyła nową pozycję
        setCurrentPos(*data);
        rc=true;
        break;
    }
    return rc; // zwraca true, jeśli była to nowa pozycja
}

Jej działanie powinno być jasne. Dodatkowa funkcja setCurrentPos wygląda tak:

static uint8_t newSPos; // pozycja  przesłana przez panel
static bool haveNewSPos = false; // odebrano nową pozycję

void setCurrentPos(uint8_t pos)
{
    newSPos=pos;
    haveNewSPos = true;
}

Czyli znów jest to tylko ustawienie paru zmiennych. Kolej sięc na obiecaną maszynę stanów.

Funkcja jest wywoływana wewnątrz loop() i powinna sterować realizacją wszystkich poleceń. Mam nadzieję, że komentarze w kodzie są na tyle szczegółowe, że nie trzeba dodatkowego opisu:

void transLoop()
{
    // realizacja smiany pozycji w rozkładzie
    if (haveNewSPos) {
        // mam nową pozycję
        haveNewSPos = false;
        if (schedPos != newSPos) { // czy już tego nie wyświetlam?
            schedPos=newSPos;
            if (!isInfo()) showSched();
        }
    }
        
    switch (transmitterState) {
        case TSCD_TRANS_FILE:
        // muszę wysłać plik, a więc rozpoczynam transmisję
        transferScd(sched);
        break;

        case TSCD_TRANS_FILE_PART:
        // muszę wysłać następną część pliku, ale poczekam chwilę
        // od wysłania ostatniej - tak na wszelki wypadek
        if (millis() - fileTimer >= 50UL) transferScd(); 
        break;

        case TSCD_TRANS_WAIT_FILE:
        // oczekuję ba potwierdzenie odebrania części pliku
        // jeśli w ciągu dwóch sekund go nie dostanę, przerywam
        // transmisję
        if (millis() - fileTimer > 2000UL) {
            transmitterState = TSCD_TRANS_RELAX;
        }
        break;

        case TSCD_TRANS_SEND_POS:
        // muszę wysłać aktualną pozycję
        transmitterState = TSCD_TRANS_RELAX;
        sendCurrentPos();
        break;
    }
}

Wywoływana tu funkcja sendCurrentPos jest również prosta:

void sendCurrentPos()
{
    uint8_t buf[2];
    buf[0] = ENOW_MY_DATA;
    buf[1] = schedPos;
    sendToPanel(buf,2);
}

I to wszystko - jak widać, dodanie nowej funkcjonalności do programu wcale nie było aż takie skomplikowane!

Jak zwykle kod w załączniku. I tu uwaga: wewnątrz znajdują się dwa foldery szkiców. Szkic kolejrozklad5 to właściwy program rozkładu jazdy. Szkic KolejRozTest należy wgrać na ESP32 - obserwując monitor serial można zobaczyć, że program odbiera plik rozkładu, a następnie co 15 sekund przesuwa rozkład o jedna pozycję:rozklad5.zip

Teraz już wystarczy połączyć rozkład z naszym panelem. Niestety - dodanie całkowicie nowej funkcjonalności wymaga również rozbudowania kilku funkcji, przede wszystkim obsługujących wyświetlacz i klawiaturę tak, aby można było wprowadzić właściwą pozycję. Tak więc strorzenie opisu może mi zająć trochę więcej czasu, ale na wszelki wypadek: stay tuned!


 

Edytowano przez ethanak
  • Lubię! 1

Dzień dobry,

Pierwszy raz chyba nie widzę problemów u siebie z podłączeniem, wgraniem, działaniem, monitorem etc.

ESP8266 (wyświetlacz) pokazuje poniższe w monitorze:

Plik rozklad1.txt rozmiar 398
16:46:30.784 -> LFS rozklad1.txt rozmiar 396
16:46:30.832 -> Pliki są identyczne
16:46:30.832 -> INFO 0910
16:46:30.927 -> Wysyłam pakiet
16:46:30.927 -> RT 54
16:46:30.927 -> Master odebrał pakiet
16:46:30.974 -> Wysyłam pakiet
16:46:30.974 -> RT 45
16:46:30.974 -> Master odebrał pakiet
16:46:31.022 -> Wysyłam pakiet
16:46:31.022 -> RT 40
16:46:31.022 -> Master odebrał pakiet
16:46:31.022 -> Proszę o dane
16:46:31.022 -> Wysyłam pakiet
16:46:31.022 -> RT 41
16:46:31.022 -> Master odebrał pakiet
16:46:32.043 -> Proszę o dane
16:46:32.043 -> Wysyłam pakiet
16:46:32.043 -> RT 50
16:46:32.043 -> Master odebrał pakiet
16:46:33.021 -> Proszę o dane
16:46:33.021 -> Wysyłam pakiet
16:46:33.021 -> RT 55
16:46:33.021 -> Master odebrał pakiet
16:46:34.049 -> Proszę o dane
16:46:34.049 -> Wysyłam pakiet
16:46:34.049 -> RT 54
16:46:34.049 -> Master odebrał pakiet
16:46:35.024 -> Proszę o dane
16:46:35.024 -> Wysyłam pakiet
16:46:35.024 -> RT 47
16:46:35.024 -> Master odebrał pakiet
16:46:36.050 -> Proszę o dane
16:46:36.050 -> Wysyłam pakiet
16:46:36.050 -> RT 58
16:46:36.050 -> Master odebrał pakiet
16:46:37.027 -> Proszę o dane
16:46:37.074 -> Wysyłam pakiet
16:46:37.074 -> RT 47
16:46:37.074 -> Master odebrał pakiet
16:46:38.045 -> Proszę o dane
16:46:38.045 -> Wysyłam pakiet
16:46:38.045 -> RT 54
16:46:38.045 -> Master odebrał pakiet
16:46:39.070 -> Proszę o dane
16:46:39.070 -> Wysyłam pakiet
16:46:39.070 -> RT 52
16:46:39.070 -> Master odebrał pakiet
16:46:40.047 -> Proszę o dane
16:46:40.047 -> Wysyłam pakiet
16:46:40.047 -> RT 49
16:46:40.047 -> Master odebrał pakiet
16:46:41.107 -> Proszę o dane
16:46:41.107 -> Wysyłam pakiet
16:46:41.107 -> RT 54
16:46:41.107 -> Master odebrał pakiet
16:46:42.085 -> Proszę o dane
16:46:42.085 -> Wysyłam pakiet
16:46:42.085 -> RT 52
16:46:42.085 -> Master odebrał pakiet

Natomiast ESP32 poniższe:

16:45:05.286 -> ENOW mam data

16:45:05.286 -> Dane odebrane przez slave
16:45:05.286 -> Dane odebrane przez slave
16:45:05.286 -> ENOW mam data

16:45:05.286 -> Dane odebrane przez slave
16:45:05.380 -> ENOW mam chunk 1/2

16:45:05.380 -> Sced chunk 2/2

16:45:05.380 -> Final

16:45:05.380 -> E Wieluń 4/2 10:40
16:45:05.380 -> O Bielsko-Biała Komorowice 1/1 12:10
16:45:05.380 -> S Český Těšín 2/1 13:50
16:45:05.380 -> S Żywiec 1/1 13:52
16:45:05.380 -> E Šibenik 3/2 14:30
16:45:05.380 -> E Katowice Piotrowice 4/2 15:40
16:45:05.380 -> O Bielsko-Biała Główna 1/1 16:10
16:45:05.380 -> E Berlin Östbahnhoff 2/1 16:50
16:45:05.380 -> P Kraków 1/1 16:52
16:45:05.380 -> P Tychy Lodowisko 3/2 17:30
16:45:05.380 -> E Hradec Králové 4/2 18:40
16:45:05.380 -> P Szczecin Dąbie 1/1 19:10
16:45:05.380 -> P Częstochowa 2/1 19:50
16:45:05.380 -> S Zamość 1/1 19:52
16:45:05.380 -> P Tychy 3/2 20:30

16:45:05.380 -> Dane odebrane przez slave
16:45:05.380 -> Dane odebrane przez slave
16:45:05.380 -> Sched pos 0

16:45:20.321 -> Wysyłam pozycję 1

16:45:20.321 -> Dane odebrane przez slave
16:45:35.321 -> Wysyłam pozycję 2

16:45:35.321 -> Dane odebrane przez slave
16:45:50.306 -> Wysyłam pozycję 3

16:45:50.306 -> Dane odebrane przez slave

Zauważyłem, że w tej wersji kodu "kolejrozklad5" TFT nie pokazuje polskiej czcionki.

20 minut temu, prezesedi napisał:

Zauważyłem, że w tej wersji kodu "kolejrozklad5" TFT nie pokazuje polskiej czcionki.

Zapewniam że pokazuje, tylko jej nie zainstalowałeś.

Wgrywałeś na nowo bibliotekę Adafruit_GFX, prawda? Albo jakiś update poszedł? No to teraz zgodnie z instrukcją trzeba polską czcionkę zainstalować...

Kolejne wersje:

1obraz.thumb.png.6e4d10b69a948a394af7cd4b66c4b396.png

2obraz.thumb.png.2d5c7ebcb266cde7a0d15eee11aa661e.png

3obraz.thumb.png.2601c2b2dd1c8aac00db67dee52395f3.png

4obraz.thumb.png.630ada9eb1e89986bbfe45888568ba56.png

5obraz.thumb.png.a019d65a6b84708615c5d142df8b1581.png

Dobra, pozbędę się biblioteki Adafruit, wrzucę na nowo i na nowo plik z czcionkami. Reset PC i wgranie kodu 5 ponownie

To może być kwestia cache Arduino - jeśli skompilowałeś program z niespatchowaną wersją GFX to Arduino IDE może nie zauważyć zmiany. Niestety, nie ma możliwości wyczyszczenia cache z poziomu Arduino IDE a nie wiem gdzie to siedzi w Windowsie.

Jest natomiast na to prosty sposób: zmień nazwę folderu ze szkicem np. na kolejrozklad5a i plik ino na kolejrozklad5a.ino - Arduino IDE potraktuje to jako nowy szkic.

Podobno pomaga również przełączenie na inną płytkę, skompilowanie, przełączenie z powrotem, skompilowanie. Cóż - autorzy Arduino IDE mają to chyba gdzieś, bo issue na githubie wisi od dłuższego czasu.

Ech, u mnie na Linuksie nie ma tych problemów...

Już jest ok. Jest polska czcionka. Moje powyższe działania pomogły.

1 godzinę temu, prezesedi napisał:

Pierwszy raz chyba nie widzę problemów u siebie z podłączeniem, wgraniem, działaniem, monitorem etc.

Chyba bym musiał odwołać ten wpis 😉

3 minuty temu, prezesedi napisał:

Już jest ok. Jest polska czcionka. Moje powyższe działania pomogły.

1 godzinę temu, prezesedi napisał:

Niestety - Arduino IDE ma swoje wady i widzimisia. Trzeba się po prostu do tego przyzwyczaić.

Tak przy okazji - IDE w wersji 2.x nie ma części tych wad, ale wprowadza nowe ciekawsze 🙂

To jak wszystko działa to siadam do opisu wersji panela z możliwością sterowania rozkładem. Już gdzieś na horyzoncie pojawia się koniec 🙂

Super, tylko na spokojne. Jutro jestem w Wolsztynie na paradzie parowozów. Pierwszej od wielu lat. Niedziela jak to niedziela - będę wieczorem

9 minut temu, prezesedi napisał:

Jutro jestem w Wolsztynie na paradzie parowozów.

Mogę tylko pozazdrościć 😞 Może specjalnym maniakiem nie jestem, ale lubię wszystkie stare maszyny...

A do niedzieli może się wyrobię - tam jest jednak trochę kodu i chciałbym opisać co on robi (np. na wypadek, gdyby ktoś chciał to później ulepszyć czy coś zmienić).

 

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...