Skocz do zawartości

Arduino w modelarstwie kolejowym


Pomocna odpowiedź

20 minut temu, ethanak napisał:

Może specjalnym maniakiem nie jestem, ale lubię wszystkie stare maszyny...

Właśnie ja też. A ma być z Belgii, Luxemburga, Niemiec, Czechów, 2 polskie (z 3 obecnie czynnych) i drezyna na bazie Warszawy M20.

Nie wiadomo kiedy znów tak obrodzi. Pozwolę sobie wkleić link do wydarzenia.

https://www.parowozowniawolsztyn.pl/parada_parowozow_17.html

 

 

  • Lubię! 1

Zgodnie z obietnicą - dziś umożliwię sterowanie rozkładem jazdy z poziomu panela.

Na początek drobna zmiana.

O ile semafor wysyłał tylko jeden komunikat (prośba o aktualne dane), o tyle w przypadku rozkładu trzeba odebrać również inne: treść rozkładu i aktualna pozycja. Dlatego w strukturze ETrans musi znaleźć się wskaźnik do dodatkowej funkcji obsługującej odbierane komunikaty. W przypadku semafora będzie on oczywiście miał wartość NULL (oznaczającą, że nie ma żadnej funkcji). Tak więc drobna modyfikacja deklaracji struktury:

struct ETrans {
    void *parent; //wskaźnik do obiektu nadrzędnego (np. semafora)
    uint8_t rxId; // indeks transceivera w tablicy
    uint8_t rxPos; // indeks urządzenia w transceiverze
    uint8_t rxDataPos; // pozycja danych w buforze
    uint8_t rxDataLen; // długość danych w buforze
    uint8_t dataChanged; // dane zmienione, trzeba nadać

    // funkcja pobierająca ustawienia do transmisji
    void (*getData)(struct ETrans *et, uint8_t *buffer);
    // funkcja wywoływana po odebraniu danych
    void (*recvData)(struct ETrans *et, const uint8_t *buffer, int len);
    
};

Również w konstruktorze semafora trzeba dodać instrukcję wstawiającą NULL do tego pola:

    // ustawienia dla zdalnego semafora

    if (_whereto & 0x80) {
        tx.parent = (void *)this;
        tx.rxId = (whereto>>2) & 7;
        tx.rxPos = whereto & 3;
        tx.rxDataPos = tx.rxPos;
        tx.rxDataLen=1;
        tx.getData = getSemaData;
        tx.recvData = nullptr; // nie obsługujemy odbierania danych
    }
}

Niewielkich poprawek wymaga również wifi.cpp - związane one są przede wszystkim z dodatkową funkcją w ETrans, ale nie tylko. Dodatkowa funkcja:

void sendToSlave(struct ETrans *t, const uint8_t *data, int len)
{
    txs[t->rxId].send(data, len);
}

umożliwia wysłanie informacji do urządzenia np. z funkcji obsługujących rozkład jazdy.

Ponieważ w kilku miejscach w programie zachodzi konieczność idendyfikacji indeksu obiektu Transceiver w tabeli na podstawie MAC, stworzyłem dodatkową funkcję:

int findTransceiver(uint8_t macid)
{
    int i;
    for (i=0;i<CNT_TRANS;i++) if (txs[i].macid() == macid) return i;
    return -1;
}

Pozwala ona na stworzenie bardziej czytelnego kodu funkcji onDataSent i onDataReceive. Dodatkowo będzie użyta przez rozkład jazdy do odnalezienia właściwego Transceivera - ale o tym potem. Oto nowe wersje wspomnianych funkcji:

void onDataSent(const uint8_t *mac, esp_now_send_status_t status)
{
    int i=findTransceiver(mac[5]);
    if (i >= 0) txs[i].onSent(status);
}

void onDataReceive(const uint8_t *mac, const uint8_t *data, int len)
{
    int i=findTransceiver(mac[5]);
    if (i>=0) txs[i].onReceive(data, len);
}

Ostatnia ważna rzecz to dopisanie kodu wywołującego funkcje recvData z ETrans oraz dodanie Transceivera obsługującego rozkład do tablicy::

void Transceiver::onReceive(const uint8_t *data, int len)
{
    if (data[0] == ENOW_GIMMA_DATA) {
        // obsługiwane globalnie
        printf("Wysyłam nowe dane dla %02x\r\n",_macadr[5]);
        _sendAll = 1;
        return;
    }
    // tu dla każdego slave'a wywoływana jest funkcja recvData
    // jeśli istnieje
    int i;
    for (i=0; i<4; i++) if (slaves[i] && slaves[i]-> recvData) {
        slaves[i]->recvData(slaves[i], data, len);
    }
}

static Transceiver txs[]={
    Transceiver(0x20), // semafor
    Transceiver(0x30)  // rozkład
};

Pozostałe zmiany związane są z obiecaną możliwością zmian ilości i odstępu czasowego powtórzeń.

Dobra, to tyle na temat drobiazgów, przejdę może do rzeczy poważniejszych 🙂

Zacznę od deklaracji klasy Rozklad:

class Rozklad
{
    public:
    Rozklad(uint8_t macid=0x30);
    void attach();
    void send(const uint8_t *data, int len);
    void cntlines();
    void set(int pos);

    uint8_t pos;    // bieżąca pozycja
    uint8_t lines;
    bool valid;     // mam dane
    bool validPos;  // znam pozycję

    // odbierany plik
    uint16_t filelen;   // długość pliku
    uint16_t filepos;   // pozycja w odbieranym pliku
    uint8_t chunks;     // ilość pakietów
    uint8_t thisChunk;  // bieżący pakiet
    char frozklad[8192]; // miejsce na rozkład jazdy
    
    private:
    uint8_t _macid;
    struct ETrans tx;
};

extern Rozklad rozklad; // obiekt przedstawiający rozkład

Jest tu tylko jeden element tej klasy (panel obsługuje tylko jeden wyświetlacz). W odróżnieniu od semafora nie podaję tu w konstruktorze indeksu do tablicy Transceiverów - zamiast tego w funkcji attach wyszukuję pozycję na podstawie adresu MAC:

Rozklad::Rozklad(uint8_t macid)
{
    pos=0;
    valid=0;
    validPos = 0;
    _macid = macid;

    tx.rxId =0xff;
    tx.rxPos = 0;
    tx.rxDataPos = 0;
    tx.rxDataLen=1;
    tx.getData = getSchedData;
    tx.recvData = recvSchedData;
    tx.parent = (void *)this;
}

void Rozklad::attach()
{
    tx.rxId = findTransceiver(_macid);
    attachEnowTx(&tx);
}

O funkcjach getSchedData i recvSchedData napiszę później, na razie pokażę pozostałę metody klasy.

Metoda send wysyła dowolne dane do urzędzenia:

void Rozklad::send(const uint8_t *data, int len)
{
    sendToSlave(&tx, data, len);
}

Metoda cntlines po prostu liczy linie w otrzymanym pliku. Warto zauważyć, że plik nie kończy się znakiem nowej linii (stąd liczenie od jedynki):

void Rozklad::cntlines()
{
    int i,n;
    for (i=0,n=1;frozklad[i];i++) if (frozklad[i] == '\n') n++;
    lines = n;
}

Metoda set ustawia aktualną pozycję w rozkładzie oraz poprzez ustawienie pola dataChanged informuje odpowiedni Transceiver, że są nowe dane do wysłania:

void Rozklad::set(int pos)
{
    this->pos=pos;
    tx.dataChanged = 1;
}

Ostatnia metoda po prostu pobiera n-tą linię rozkładu jazdy i parsuje ją, umieszczając odpowiednie wartości w odpowiednim polu struktury. Warto zauważyć, że nie ma tu zabezpieczenia przed ucięciem w połowie ostatniego znaku w UTF-8 - ale funkcja konwersji dla wyświetlacza i tak zatrzyma się na nieprawidłowym znaku:

struct SchedTrain {
    uint8_t type;
    uint8_t tor;
    uint8_t hour;
    uint8_t minute;
    
    char name[32];
};

uint8_t Rozklad::getParsedLine(struct SchedTrain *sched, int n)
{
    const char *c=textLine(frozklad, n);
    if (!c) return 0;
    return parseScheduleLine(sched, &c);
}

Funkcja parseScheduleLine jest praktycznie taka sama jak w programie "rozkład jazdy".

Zajmę się teraz dwiema funkcjami które pominąłem:

void getSchedData(struct ETrans *t, uint8_t *buffer)
{
    Rozklad *r = (Rozklad *)t->parent;
    buffer[t->rxDataPos] = r->pos;
}

Ta funkcja jest odpowiednikiem znanej funkcji z obsługi semafora - po prostu umieszcza numer pozycji rozkładu we właściwym miejscu bufora, który będzie przekazany do urządzenia. Ciekawszą funkcją jest recvSchedData, wywoływana po przyjściu informacji z urządzenia.

Trzy globalne zmienne służą do sterowania:

bool confirmFile = false; // muszę potwierdzić przyjęcie części pliku
bool gimmaData = false; // chcę pobrać pozycję rozkładu
bool gimmaFullData = true; // chcę pobrać plik rozkładu

Strukturę fileTransCtl opisywałem poprzednio przy rozkładzie jazdy. Właściwa funkcja recvSchedData wygląda tak:

void recvSchedData(struct ETrans *t, const uint8_t *buffer, int len)
{
    struct fileTransCtl fctl;
    memcpy(&fctl, buffer, len);
    
    Rozklad *r=(Rozklad *)t->parent;
    switch(fctl.cmd) {

Jeśli to początek transferu, trzeba zaznaczyć że rozkład który mamy jest nieaktualny oraz ustawić sobie wszystkie wewnętrzne zmienne takm aby być gotowym na przyjęcie kolejnej części. Na koniec trzeba zaznaczyć, że musimy wysłać potwierdzenie:

        case ENOW_MY_FULL_DATA: // początek transferu rozkładu
        r->valid = false;
        r->validPos = false;
        r->filelen=fctl.totlen;
        r->chunks=fctl.tnr;
        r->thisChunk=1;
        memcpy(r->frozklad,fctl.databuf,fctl.datalen);
        r->filepos = fctl.datalen;
        confirmFile = true;
        break;

Przy otrzymaniu kolejnej części rozkładu sprawa wygląda prościej:

        case ENOW_MY_FULL_DATA_PART: // kolejna część rozkładu
        r->valid = false;
        r->validPos = false;
        r->thisChunk=fctl.nr+1;
        memcpy(r->frozklad+r->filepos,fctl.databuf,fctl.datalen);
        r->filepos += fctl.datalen;
        confirmFile = true;
        break;

I wreszcie ostatnia możliwość - otrzymano aktualną pozycję o trzeba zaktualizować rzeczywistą znana pozycję:        

        case ENOW_MY_DATA:  // aktualna rzeczywista pozycja rozkładu
        if (r->valid) {
            r->pos = buffer[1];
            r->validPos = true;
        }
        break;
    }
}

Jak widać, funkcja jest dość prosta i nie wymaga specjalnego omawiania. Warto jednak powiedzueć o kilku sprawach:

  • zmienna frozlad to tablica na tyle duża, aby pomieścić rozkład. Nie jest to specjalnie dobre rozwiązanie, ale będzie to poprawione w następnej wersji;
  • również w następnej wersji będzie dodana kontrola czy to właściwa część rozkładu.

I kolejna ważna funkcja: rozkladLoop. Będzie ona wywoływana z głównej funkcji loop() - oto jej początek:

static uint32_t transTimer=0; // taki ogólny timerek

void rozkladLoop()
{
    uint8_t cmd; // polecenie wysyłane do urządzenia

Teraz następuje sprawdzenie, czy mamy rozkład. Jeśli wysłano prośbę o rozkład a w ciągu trzech sekund nie ma jeszcze pełnego rozkładu - coś jest nie tak i prośbę trzeba powtórzyć:

    if (!gimmaFullData && !rozklad.valid && millis() - transTimer > 3000UL) {
        gimmaFullData=true;
    }

jeśli zmienna gimmaFullData jest ustawiona na true, następuje wysłanie polecenia do urządzenia:

    if (gimmaFullData) {
        gimmaFullData=false;
        printf("Chcę dostać plik\r\n");
        cmd=ENOW_GIMMA_FULL_DATA;
        rozklad.send(&cmd, 1);
        transTimer=millis();
        return;
    }

Jeśli ootrzymano fragment pliku, trzeba o tym poinformować urządzenie. Jednocześnie jeśli to końcowy fragment, należy również zakończyć przyjmowanie pliku oraz poinformować system, że teraz będziemy prosić o pozycję (ustawiając gimmaData na true):

    if (confirmFile) {
        if (rozklad.thisChunk >= rozklad.chunks) {
            printf("Dostałem cały rozkład\r\n");
            rozklad.valid=true;
            gimmaData = true;
            rozklad.frozklad[rozklad.filelen]=0;
            rozklad.cntlines();
            printf("%s\r\n", rozklad.frozklad);
        }
        confirmFile = false;
        cmd=ENOW_ACCEPTED_DATA_PART;
        rozklad.send(&cmd, 1);
        transTimer=millis();
        return;
    }

Jeśli w tym miejscu nie ma jeszcze ważnego rozkładu, nie można nic dalej zrobić:

    if (!rozklad.valid) return;

Teraz jeśli wysłano prośbę o pozycję, a w ciągu trzech sekund nie było odpowiedzi, trzeba ponowić prośbę:

    if (!gimmaData && !rozklad.validPos && millis() - transTimer > 3000) {
        gimmaData=true;
    }

I wreszcie jeśli trzeba wysłać prośbę - cóż, trzeba wysłać.

    if (gimmaData) {
        gimmaData=false;
        printf("Proszę o pozycję\r\n");
        cmd=ENOW_GIMMA_DATA;
        rozklad.send(&cmd, 1);
        transTimer=millis();
        return;
    }
}


I to tyle o komunikacji urządzeń.

Następną sprawą jest obsługa klawiatury i wyświetlacza. Zacznijmy jednak od drobnych zmian w głównej pętki programu.

Pozostaje założenie, że funkcja getCommand() zwróci kompletne polecenie. Teraz jednak oprócz semaforów mamy jeszcze ustawianie rozkładu. A więc stwórzmy sobie kilka definicji:

#define CMD_IS_FUNCTION(a) ((a) & 0x8000)
#define CMD_FUNCTION_MASK 0xf000
#define CMD_DISPLAYALL 0x8000
#define CMD_SCHEDULE 0xc000

Trochę rozbudowuje się fragment kodu w loop() odpowiedzialny za wykonanie polecenia:

    if (cmd) { // mamy coś do zrobienia
        if (CMD_IS_FUNCTION(cmd)) {
            switch(cmd & CMD_FUNCTION_MASK) {
                case CMD_DISPLAYALL:
                displayAllSema();
                break;

                case CMD_SCHEDULE:
                rozklad.set(cmd & 255);
                break;
            }
        }
        else {
            semafor[SEM_NUM(cmd)].set(SEM_CMD(cmd));
        }
    }

Na razie zmiany są stosunkowo niewielkie, ale całą czarną robotę wykonuje tu funkcja getCommand. Założyem, że:

 

  • przycisk C będzie służył do ustawiania pozycji rozkładu;
  • jeśli rozkład będzie nieaktualny, wyświetlony zostanie napis "BŁĄD!" i klawiatura wróci do stanu początkowego;
  • wyświetlona zostanie aktualna pozycja rozkładu;
  • klawisze A, B, C i D służą do ustalania pozycji: A - przeskok o pięć w tył, B - poprzednia, C - następna, D - przeskok o pięć w przód.
  • po ustawieniu pozycji wciśnięcie gwiazdki spowoduje wysłanie polecenia do urządzenia.

Tak więc należało zadeklarować nowy stan klawiatury i uwzględnić go w tavlicy keySel:

enum {
    KBD_RELAX=0,
    KBD_SEMA, // wybrano semafor
    KBD_PROG, // wybrano ustawienie semafora
    KBD_SELECTSEM, // wybrano semafor lub tarczę
    KBD_SETSCHED, // wybrano ustawianie rozkładu
    KBD_MAX
};

static struct keySel {
    uint8_t mode;
    uint8_t param;
    const char *name;
} keySel[16] = {
    {0}, // zawsze zero
    {0},{0}, {0}, {0}, {0}, // klawisze 1..5
    {0},{0}, {0}, {0}, {0}, // klawisze 6..0
    {KBD_SELECTSEM, 0, "Semafor"},          // klawisz A: semafor
    {KBD_SELECTSEM, 16, "Tarcza"},          // klawisz B: tarcza
    {KBD_SETSCHED,0,"Rozklad"},          // klawis C: rozkład
    {0},               // klawisz D
    {0} // zawsze 0
};

Potrzebna jest jeszcze dodatkowa zmienna, zawierająca aktualnie wybieraną pozycję:

static int schedDispPos;


Funkcja displayKbdStatus musi we właściwy sposób zareagować na wybranie tego stanu. Jeśli rozkłąd jest nieaktualny wyświetla ingformację o błędzie, w przeciwnym przypadku aktualną pozycję rozkładu:

        case KBD_SETSCHED:
        if (!rozklad.valid || !rozklad.validPos) {
            kbdStatus = KBD_RELAX;
            displayError();
            break;
        }
        displaySchedTrainSel(schedDispPos=rozklad.pos);
        break;

        default: // tu nie powinno dojść!
        break;

Również w funkcji getCommand dodatkowy kod obsługuje klawisze wyboru rozkładu:

        case KBD_SETSCHED:
        switch(g) {
            case 11: // A
            schedDispPos = (schedDispPos + rozklad.lines - 5) % rozklad.lines;
            break;

            case 12: // B
            schedDispPos = (schedDispPos + rozklad.lines - 1) % rozklad.lines;
            break;

            case 13: // A
            schedDispPos = (schedDispPos + 1) % rozklad.lines;
            break;

            case 14: // A
            schedDispPos = (schedDispPos + 5) % rozklad.lines;
            break;

            case KEY_ASTERISK:
            displayStored(true);
            kbdStatus = KBD_RELAX;
            return CMD_SCHEDULE | schedDispPos;
            
            default:
            return 0;
        }
        displaySchedTrainSel(schedDispPos);
        break;

Natomiast w display.cpp mamu to dodatkowe funkcje. displayTrainPart wypisuje na dwóch dolnich linijkach informacje o aktualnie wybranym pociągu:

static void displayTrainPart(int pos)
{
    struct SchedTrain st;
    display.setCursor(0,16);
    display.setTextSize(1);
    display.printf("Nr %d/%d", pos+1,rozklad.lines);
    if (rozklad.getParsedLine(&st, pos)) {
        display.setCursor(128-6*5-4-6,16);
        display.print((char)st.type);
        display.setCursor(128-6*5,16);
        display.printf("%02d:%02d",st.hour, st.minute);
        display.setCursor(0,24);
        char buf[22];
        utf2local(st.name, buf,22);
        display.printf("%s",buf);
    }
}

A następna funkcja wyświetla już całą informację:

void displaySchedTrainSel(int pos)
{
    display.clearDisplay();
    display.setTextSize(1,2);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(64-3*13,0);
    display.print("Ustaw rozk\222ad");
    displayTrainPart(pos);
    display.display();
    displayMode = DPM_KBD;
}

Jako że użyta jest tu funkcja utf2local, należy jeszcze podać na początku:

#include "localconv.h"

i oczywiście pliki localconv.h i localconv.cpp (użyte wcześniej przy wyświetlaczu rozkładu jazdy) muszą znaleźć się w folderze szkicu.

W głównym pliku ino potrzebne są jeszcze dwie zmiany: w setup() musimy wywołać połączenie obiektu Rozklad z Transceiver, czyli wyglądać będzie tak:

void setup()
{
    Serial.begin(115200);
    initWiFi();
    attachSema();
    rozklad.attach();
    Wire.begin();
    wwrite16(PCFADR[0], 0xffff);
    wwrite16(PCFADR[1], 0xffff);
    initDisplay();
}

oraz dodać wywołanie:

    rozkladLoop();

na końcu loop().

To tak mniej więcej wszystko na dziś. Kod jak zwykle w załączniku: kolejpan3.zip

Co prawda teraz wszystko powinno działać, ale to jeszcze nie koniec... tak że jak zwykle: stay tuned!
 

  • Lubię! 1
4 godziny temu, prezesedi napisał:

Chyba się wyrabiam

A to zobaczymy.

Do tego, żeby uruchomić następną wersję trzeba niestety nieco poprawić to, o czym zapomnieli twórcy Arduino IDE. Mianowicie program wymaga, aby cały praktycznie flash zajął skompilowany kod - a takiej możliwości nie ma 😞

Na początek musisz znaleźć miejsce, gdzie Arduino trzyma swoje wszystkie rzeczy potrzebne do kompilacji programu. Nie, nie wiem gdzie on to trzyma na Windowsie. U mnie ścieżka (dla wersji boardu 2.0.7) wygląda tak:

/home/ethanak/.arduino15/packages/esp32/hardware/esp32/2.0.7/

W windowsie będzie jakoś podobnie.

Teraz w tym folderze powinien być folder tools, a w nim folder partitions zawierający kilkanaście plików .csv. Należy z załącznika wydobyć plik apponly.csv i przegrać go w to miejsce.

To było prostsze, teraz trudniejsze.

W folderze do którego ścieżkę podałem jest plik boards.txt - należy go otworzyć w jakimś uczciwym edytorze tekstu (nie, notatnik nie jest uczciwym edytorem tekstu, MS Office takoż, a nawet LibreOffice się do tego nie nadaje). Warto przed zrobieniem zmian zrobić sobie kopię tego pliku, żeby nie trzeba było ponownie instalować IDE.

W pliku trzeba znaleźć sekcję odpowiadającą płytkę, którą masz ustawioną. Jest to dość łatwe, bo wszystkie sekcje zaczynają się od czegoś w stylu:

cośtam.name=Pełna nazwa

Np. dla ESP32 Dev Module będzie to:

esp32.name=ESP32 Dev Module

Jeśli już to znajdziesz, nieco niżej będą linijki typu:

esp32.menu.PartitionScheme.cośtamcośtam. Należy znaleźć ostatnią linijkę w sekcji (czyli zaczynającą się w tym przypadku od esp32.menu.PartitionScheme) i za nią wstawić następujące trzy linijki:

esp32.menu.PartitionScheme.apponly=4M Flash (Application only)
esp32.menu.PartitionScheme.apponly.build.partitions=apponly
esp32.menu.PartitionScheme.apponly.upload.maximum_size=4128768

Oczywiście jeśli korzystasz z innej płytki, zamiast esp32 operujesz na swojej.

Aha, nie muszę wspominać, że robisz to wszystko przy wyłączonym Arduino IDE!

Teraz po uruchomieniu powinieneś mieć możliwość ustawienia dla swojej płytki schematu partycji "4M Flash (Application only)":

apponly.thumb.png.cba6ab86607609fbfbaf491cf1dfdca9.png

Ustaw, skompiluj z tym schematem np. ostatnią wersję panela i napisz, jaki procent pamięci zajmuje kod...

Załącznik: apponly.zip

W razie czego zawartość apponly.csv to:

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x5000,
otadata,  data, ota,     0xe000,  0x2000,
app0,     app,  ota_0,   0x10000, 0x3F0000,

To się musi udać - bez tego po prostu następna wersja się nie skompiluje.

Czekam na wyniki!

 

  • Lubię! 1

Jutro z rana do tego siądę. Pracuję z domu więc mam czas. Dziś niestety muszę dokończyć to co rozgrzebałem w domu. Oczywiście podzielę się wynikami 

Dzień dobry.

A więc mamy:

c:\Users\Adam\Documents\ArduinoData\packages\esp32\hardware\esp32\2.0.11\tools\partitions\

plik apponly.csv skopiowany

plik boards.txt zmodyfikowany

Wszystko działa i mamy:

Szkic używa 919353 bajtów (22%) pamięci programu. Maksimum to 4128768 bajtów.

Zmienne globalne używają 52248 bajtów (15%) pamięci dynamicznej, pozostawiając 275432 bajtów dla zmiennych lokalnych. Maksimum to 327680 bajtów.

2 godziny temu, prezesedi napisał:

Szkic używa 919353 bajtów (22%) pamięci programu. Maksimum to 4128768 bajtów.

Czyli wszystko w porządku. Muszę jeszcze parę poprawek zrobić, dziś się pewnie nie wyrobię z opisem ale na jutro będzie już zapowiadać pociągi 🙂

 

  • Lubię! 1

@prezesedi jednak się wyrobiłem 🙂

Cóż - pora kończyć. Dzisiaj po raz pierwszy pełna wersja programu ze wszystkimi możliwościami. Ale zacznę od kabelkologii.

Prede wszystkim - użycie zdalnych semaforów nie wyklucza użycia takowych podłączonych bezpośrednio do panela. Oczywiście nie należy podłączać ich do wyjść ukłądu PCF8575 - potrzebny będzie bufor ULN2003 (tak jak w semaforze radiowym). Pojawia się tu jednak pewna trudność: otóż układ PCF8575 ma wyjścia typu open-drain co oznacza, że albo są zwarte do masy albo wiszą w powietrzu. ULN raczej wiszących wejść nie lubi, trzeba zastosować rezystory podciągające:

pcf_uln.thumb.png.be433c41684d83c3c15dedb6d61464a0.png

Niestety - nie mam PCF8575 więc nie jestem w stanie tego sprawdzić, ale z różnych wykopanych w Internecie informacji wynika, że rezystory powinny mieć ok. 3 kΩ. Trzeba to po prostu sprawdzić na jednym kanale i podobne zastosować we wszystkich.

Drugą sprawą jest podłączenie modułu wzmacniacza/dekodera I2S. Ja użyłem tu popularnego i niedrogiego układu MAC98357a, ale można zastosować dowolny nie wymagający sygnału MCLK. Podłączyć należy jak na schemacie:

  • i2s.thumb.png.a801c0040a31590fd5e6a72906b8f55f.png

Trzeba pamiętać, że jest to wzmacniacz mocy (co prawda tylko 3W, ale trochę prądu potrafi użyć) i zasilanie z USB po prostu jest za słabe - układ nie będzie działać. W swojej doświadczalnej wersji użyłem zewnętrznego zasilacza 5V, ale konkretne rozwiązanie trzeba znaleźć samemu.

Wyjście układu nie może być połączone z żadną masą czy wejściem dodatkowego wzmacniacza - w przypadku konieczności użycia takowego należy zastosować inny dekoder, np. taki. Trzeba też pamiętać, że w przypadku podłąćzania kilku głośników wypadkowa impedancja nie może być niższa niż 4Ω.

Jeśli wzmacniacz jest za cichy, można spróbować podłączyć wejście GAIN bezpośrednio do GND (+3dB) lub przez rezystor 100 kΩ (+6dB).

Ale wrócę do programu. Na początku obiecane poprawki.

Sprawdzenie poprawnej kolejności przesyłania fragmentów rozkłądu okazało się proste: jeśli coś się nie zgadza, program prosi o ponowne przesłanmie rozkładu. Drobna zmiana w funkcji recvSchedData załatwia sprawę:

        case ENOW_MY_FULL_DATA_PART:
        r->valid = false;
        r->validPos = false;
        if (r->thisChunk != fctl.nr) { // dopisany fragment
            gimmaFullData = true;
            break;
        }
        r->thisChunk=fctl.nr+1;
        memcpy(r->frozklad+r->filepos,fctl.databuf,fctl.datalen);
        r->filepos += fctl.datalen;
        confirmFile = true;
        break;

Druga sprawa to pozbycie się statycznego bufora na zawartość rozkładu. W obiekcie Rozkład zmienna frozklad jest teraz wskaźnikie, a metoda allocsched dba o przydział i ewentualną realokację bufora:

void Rozklad::allocsched()
{
    int size=filelen+1;
    if (!frozklad) {
        frozklad=(char *)malloc(size);
        _frmemsize = size;
        return;
    }
    if (size <= _frmemsize) {
        return;
    }
    frozklad=(char *)realloc((void *)frozklad, size);
    _frmemsize=size;
}

Zmienna _frmemsize przechowuje wielkość przydzielonego bufora - w przypadku ponownego wysłania innego rozkładu jeśli będzie on mniejszy niż poprzedni nie będzie realokacji.

Metoda wywoływana jest po pdebraniu pierwszej części rozkładu:

        case ENOW_MY_FULL_DATA:
        r->valid = false;
        r->validPos = false;
        r->filelen=fctl.totlen;
        r->chunks=fctl.tnr;
        r->thisChunk=1;

        r->allocsched(); // <-- o właśnie tu!!!

        memcpy(r->frozklad,fctl.databuf,fctl.datalen);
        r->filepos = fctl.datalen;
        confirmFile = true;
        break;


Muszę tu wspomnieć o jeszcze jednej modyfikacji w pliku rozklad.cpp - w funkcji parsującej linię rozkładu użyłem wcześniej zwykłego memcpy, aby pobrać do pola strukctury nazwę stacji. W przypadku, gdyby długość nazwy przekraczała wielkość tablicy w strukturze, była ona po prostu ucinana. Nie stwarzało to problemów - tablica była używana wyłącznie do wyświetlania nazwy stacji na wyświetlaczu panela i w przypadku niekompletnego kodu UTF-8 po prostu wyświetlał się fragment nazwy (funkcja konwersji przerywa pracę w przypadku wykrycia błędnej sekwencji). Tu juz tak nie mogłem zrobić: ponieważ nazwa jest częścią komunikatu wygłaszanego przez syntezator - w przypadku błędnej sekwencji dalsza część komunikatu nie zostałaby odczytana. Stąd konieczność użycia funkcji, która będzie dbała o prawidłowość sekwencji. Nie będę jej tu pokazywał bo kod nic ciekawego nie wnosi - zainteresowani mogą sobie obejrzeć kod w pliku z załącznika.

I jeszcze jedna modyfikacja dotycząca wypisywania różnych mądrych rzeczy na serialu. Ponieważ w tej wersji programy przewidziana jest interakcja z uzytkownikiem (ustawianie różnych parametrów pracy) takie wyświetlanie raczej by preszkadzało. Dlatego w całym programie instrukcje printf wypisujące coś automatycznie zostały poprzedzone warunkiem - np:

if (debugging) printf("Coś ważnego\r\n");

Zmienna debugging jest globalna i można ją ustawić wpisując odpowiednie polecenie na monitorze serial ("debug 1" lub "debug 0"). Przy starcie programu zmienna ma wartość false, czyli komunikaty nie będą przeszkadzać.

Zacznę więc od wspomnianego dialogu z użyekownikiem. Wszystko zostało ujęte w pliku prefs.cpp. Na początku następuje wczytanie danych z preferencji:

static Preferences preferences;
bool debugging=false;
char nazwaStacji[32]  = "Pieprzdowice Większe";
uint8_t gadaczPitch = 12;
uint8_t gadaczSpeed = 12;
uint8_t gadaczVol = 12;
uint8_t gadaczContr = 4;
String komunikaty[9];


void initPreferences()
{
    char buf[80];
    int i;
    preferences.begin("makieta",false);
    if (!preferences.getString("stacja", nazwaStacji, 31))
        strcpy(nazwaStacji,"Pieprzdowice Większe");
    Gadacz::setPitch(gadaczPitch=preferences.getUChar("pitch",12));
    Gadacz::setSpeed(gadaczSpeed=preferences.getUChar("speed",12));
    Gadacz::setVolume(gadaczVol=preferences.getUChar("volume",12));
    Gadacz::setContrast(gadaczContr=preferences.getUChar("contrast",4));
    serpos=0;
    for (i=0;i<9;i++) {
        sprintf(buf,"kom%d",i);
        sprintf(buf+6,"Komunikat numer %d",i+1);
        komunikaty[i]=preferences.getString(buf, buf+6);
    }
    sprintf(buf, "Makieta stacji %s gotowa do pracy", nazwaStacji);
    sayText(buf, true);
}

Funkcje z namespace Gadacz:: służą po prostu do ustawiania parametrów mowy. Oprócz tego preferencje zawierają nazwę stacji którą imituje makieta (na razie używaną wyłącznie przy przedstawieniu się programu) oraz dziewięć stałych komunikatów ustawianych przez użytkownika.


W pętli wywoływana jest główna funkcja, ponierająca znaki z wejścia serial i po zakończeniu linii (czyli otrzymaniu dowolnego ze znaków '\r' lub '\r') przekazywała ową linię do funkcji obsługi:

static char serbuf[256];
static int serpos;

void serialLoop()
{
    while(Serial.available()) {
        int znak=Serial.read();
        if (!serpos && isspace(znak)) continue;
        if (znak == '\r' || znak == '\n') {
            serbuf[serpos] = 0;
            doSerCommand();
            serpos=0;
            continue;
        }
        else if (serpos < 255) serbuf[serpos++]=znak;
    }
}

Każde polecenie opisywane jest prostą strukturą:

struct SerCmd {
    const char * cmd;
    void (*fun)(char *tail);
    const char *help;
};

Struktura zawiera:

  • nazwę komendy;
  • funkcję wywoływaną z parametrem będącym resztą wpisanego polecenia po komendzie;
  • tekst wyświetlanej pomocy.

Cała tablica poleceń wygląda tak:

static const struct SerCmd Cmds[]={
    {"debug", setDebugMode, "włącza/wyłącza komunikaty debugowania (0 lub 1)"},
    {"test",testSpeech,"czyta na głos podany tekst"},
    {"zap",testSpeechB,"czyta na głos podany tekst z sygnałem"},
    {"show",showAll,"pokazuje aktualne dane"},
    {"name",setName,"ustawia nazwę stacji"},
    {"pitch",setPitch,"ustawia wysokość (od 0 do 24)"},
    {"speed",setSpeed,"ustawia prędkość (od 0 do 24)"},
    {"vol", setVol,"ustawia głośność (od 0 do 24)"},
    {"contrast", setContrast,"ustawia wyrazistość (od 0 do 100)"},
    {"store", storeAll,"zapisuje aktualne dane"},
    {"annos",showAnnos,"pokazuje aktualne komunikaty"},
    {"anno",setAnno,"ustawia komunikat (numer od 1 do 9)"},
    {"storeann",storeAnno,"zapisuje komunikaty"},
        
    {NULL,NULL, NULL}};

Jak widać nie ma tu polecenia "help" - pomoc wyświetlana jest po stwierdzeniu, że nie jest to komenda zawarta w tablicy. Funkcja doSerCmd jest więc stosunkowo prosta:

static void doSerCommand()
{
    char *cmd = serbuf;
    char *tail;
    for (tail = cmd; *tail && !isspace(*tail); tail++);
    if (*tail) {
        *tail++=0;
        while (*tail && isspace(*tail)) tail++;
    }
    int i;
    for (i=0;Cmds[i].cmd;i++) if (!strcmp(cmd, Cmds[i].cmd)) {
        Cmds[i].fun(tail);
        return;
    }
    printf("Polecenia to:\r\n");
    for (i=0; Cmds[i].cmd;i++) {
        printf("%s - %s\r\n", Cmds[i].cmd, Cmds[i].help);
    }
}

Nie będę tu pokazywał wszystkich funkcji, bo są bardzo podobne Ptrzykładowa funkcja realizująca ustawianie paremetrów mowy:

static void setSpeed(char *str)
{
    int t=getUParam(str,0,24);
    if (t<0) return;
    Gadacz::setSpeed(gadaczSpeed=t);
    sayText("Test brzmienia wprowadzonego parametru", false);
}

i funkcja zapisu parametrów do pamięci flash:

static void storeAll(char *str)
{
    char buf[6];int i;
    preferences.putString("stacja",nazwaStacji);
    preferences.putUChar("pitch", gadaczPitch);
    preferences.putUChar("speed", gadaczSpeed);
    preferences.putUChar("volume", gadaczVol);
    preferences.putUChar("contrast", gadaczContr);
    printf("Ustawienia zapisane\r\n");
    
}

To właściwie tyle, jeśli chodzi o interakcję z użytkownikiem. Zajrzyjmy sobie do pliku talk.cpp, w którym zawarte są wszystkie funkcje dotyczące syntezy mowy/ I tu uwaga: nie będę opiswał funkcji z biblioteki Gadacz - zainteresowanych zapraszam do zerknięcia na githuba, gdzie opisana jest większość ważniejszych funkcji oraz przykłady: https://github.com/ethanak/ESP32Gadacz

Wrócę więc do talk.cpp. Najciekawszą funkcją jest chyba funkcja realizująca zapowiedź pociągu. Mamy tu trzy możliwości: zapowiedź przyjazdu, informacja o postoju pociągu oraz zapowiedź odjazdu:

static void sayTrainString(struct SchedTrain *sched, uint8_t which)
{
    const char *typ = (sched->type == 'P')?"przyspieszony":
        (sched->type == 'E')?"ekspresowy":
        (sched->type == 'P')?"pospieszny":"osobowy";

    switch(which) {
        case 1: // przyjazd
        Gadacz::sayfmt("Pociąg %s do stacji %s wjedzie na tor %d przy peronie %d. Planowy odjazd pociągu godzina %02d:%02d",
            typ, sched->name, sched->tor >> 4, sched->tor & 15,
            sched->hour, sched->minute);
        break;

        case 2: //stoi przy peronie
        Gadacz::sayfmt("Na torze %d przy peronie %d stoi pociąg %s do stacji %s. Planowy odjazd pociągu godzina %02d:%02d",
            sched->tor >> 4, sched->tor & 15,typ,sched->name, sched->hour,sched->minute);
        break;

        default:
        Gadacz::sayfmt("Pociąg %s do stacji %s odjedzie z toru %d przy peronie %d. Podróżnym życzymy przyjemnej podróży",
            typ, sched->name, sched->tor >> 4, sched->tor & 15);
        break;
    }
    
}

Funkcja Gadacz::sayfmt jest odpowiednikiem dobrze znanego printfa - po prostu przekazuje do syntezatora utworzony na podstawie formatu i parametrów tekst. Funkcja wewnętrznie używa sprintf, tak że formatowanie jest typowe dla języka C/C++.

Kilka zmiennych statycznych przechowuje potrzebne wartości:

enum {
    SPEAK_MODE_NONE=0,
    SPEAK_MODE_TRAIN,
    SPEAK_MODE_TEXT
};

static struct SchedTrain STR; // sparsowana linia rozkładu
static uint8_t whichTrain;    // przyjazd, postój czy odjazd
static uint32_t bellTimer;    // taki sobie timerek
static uint8_t speakMode = 0; // co się właściwie dzieje
static char *spkText;         // tekst który ma być wypwoiedziany

Najprostsza chyba funkcja służy do odtworzenia gongu przed zapowiedzią:

void bing()
{
    Gadacz::play(megafon, 16000, sizeof(megafon)/2);
}

Funkcja sayTrain realizuje komunikat dotyczący pociągu, zapamiętując potrzebne wartości i odpalając gong:

void sayTrain(uint8_t train, uint8_t which)
{
    if (!rozklad.getParsedLine(&STR, train)) return;
    whichTrain=which;
    bing();
    speakMode=SPEAK_MODE_TRAIN;
    bellTimer=millis();
}


Podobnie funkcja sayText, przy czym możemy sobie zażyczyć że ma być odtworzony gong. Jeśli nie - po prostu wywoływana jest funkcja Gadacza.

void sayText(const char *text, bool bell)
{
    if (bell) {
        if (spkText) free(spkText);
        spkText=strdup(text);
        bing();
        speakMode=SPEAK_MODE_TEXT;
        bellTimer=millis();
    }
    else {
        speakMode = 0;
        Gadacz::say(text);
    }
}


No i funkcja wywoływana w pętli:

void talkLoop()
{
    if (!speakMode) return;
    if (millis() - bellTimer < 50) return;
    if (Gadacz::isSpeaking()) return;
    if (speakMode == SPEAK_MODE_TRAIN) {
        sayTrainString(&STR,whichTrain);
    }
    else {
        Gadacz::say(spkText);
        free(spkText);
        spkText=nullptr;
    }
    speakMode=0;
}


Opóźnienie 50 msec jest potrzebne, bo z przyczyn technicznych może się zdarzyć że funkcja isSpeaking() (zwracająca true również w przypadku odgrywania fali dźwiękowej) może zwrócić false zaraz po wydaniu polecenia play lub say.

I to właściwie wszystko jeśli chodzi o syntezę mowy... 

Pozostaje tylko dodanie obsługi klawiatury. Założyłem, że:

  • wciśnięcie D spowoduje wejście w ttryb wyboru komunikatu;
  • klawisze A, B, C i D będą służyły do wyboru pociągu;
  • Klawisze 1 do 9 wybierają jeden ze stałych komunikatów;
  • Wciśnięcie '*' w trybie wyboru komunikatu powiduje wysłanie go do syntezatora;
  • Wciśnięcie '*' w trybie wyboru pociągu spowoduje wejście w tryb wyboru zapowiedzi;
  • W trybie wyboru zapowiedzi wciśnięcie klawisy 1, 2 lub 3 spowoduje wysłanie zapowiedzi do syntezatora.

Nie będę tu zanudzał nikogo pokazywaniem kodu, bo na tym etapie (jeśli ktokolwiek to czyta) wystarczy już zerknięcie do pliku aby się zorientować o co chodzi bez zbędnych komentarzy.

I to właściwie wszystko... program działa, semaforki na biurku migają zgodnie z tym co im wpiszę, rozkład jazdy wesoło świeci a z głośnika płyną wielce krzepiące komunikaty. Oczywiście nie znaczy to że to docelowa wersja. Mam jeszcze kilka pomysłów, ale wstrzymam się z nimi na razie (bo mam trochę innej roboty - muszę taki gadżecik żonie zrobić a to trochę zajmie). W każdym razie kod jak zwykle w załączniku:kolejpan4.zip

Folder libraries z załącznika zawiera te same biblioteki które są na githubie - tyle że już skonfigurowane pod konkretną wersję płytki (4 MB Flash, difony Mbroli w formacie A-law wkompilowane w kod). Należy je po prostu wrzucić do sojego katalogu libraries.

Do skompilowania programu potrzebna jest biblioteka ESP8266Audio (z managera bibliotek).

Oczywiście wszystkie funkcje dostępne są przez www - przypomnę może:

  • SSID: Kolejka
  • Hasło: PrezesEdi7
  • Serwer: 192.168.4.1

Mam nadzieję, że o niczym nie zapomniałem 🙂

I jeśli się komuś jeszcze nie znudziło - stay tuned!


 

  • Lubię! 2

Super robota, wszystko teoretycznie działa (praktycznie już nie - nic nowego u mnie).

Podpiąłem moduł wzmacniacza z głośnikiem. Za PCF zabiorę się później, gdyż muszę wyjść na kilka godzin.

Działa: sterowanie z klawiatury A, B, C, D; wyświetla się wszystko na małym OLED; Działa wszystko z poziomu smartfonu (semafory, komunikaty, zapowiedzi)

Nie działa: głośnik. Znaczy działa ale źle - cały czas słychać głośne charczenie a komunikat jest ledwie słyszalny. Sprawdziłem poprawność połączeń trzykrotnie. Sprawdzone na dwóch różnych głośnikach 😞

(edytowany)
56 minut temu, prezesedi napisał:

cały czas słychać głośne charczenie

Z czego zasilasz moduł? Może jakiś problem z zasilaniem? Spróbuj zasilić np. z baterii (3V albo 4.5V)...

56 minut temu, prezesedi napisał:

prawdziłem poprawność połączeń trzykrotnie.

Ten moduł albo działa poprawnie albo w ogóle nie działa, sam z siebie nie będzie warczał czy charczał. Przy jakimkolwiek błędzie w kabelkach będzie po prostu milczał. Na wszelki wypadek sprawdziłem czy się nie machnąłem w schemacie - ale nie, jest OK. Zresztą Gadacza wykorzystuję w paru swoich projektach i wszystkie grzecznie działają...

Na wszelki wypadek zrobiłem sobie update pakietu ESP32 i biblioteki audio do najnowszych wersji (tak na wszelki wypadek, bo chłopaki od IDF lubią namieszać w interfejsie I2S, a autor ESP8266Audio nie zawsze nadąża) -  działa bez problemu.

 

Edytowano przez ethanak

Nie, to jest bunt - bunt maszyn jak w Terminatorze, a w moim przypadku elektroniki.

4 godziny temu, ethanak napisał:

Z czego zasilasz moduł? Może jakiś problem z zasilaniem? Spróbuj zasilić np. z baterii (3V albo 4.5V)...

zasilanie osobne, zasilacz 12V 1,5A poprzez moduł zasilania do płytek stykowych lub zasilacz z kolejki z regulowanym napięciem (ustawiłem na 4,0V).

Działa to tak jak w załączeniu.

Jedyne co mi przychodzi na myśl to jeszcze zasilacz stabilizowany - nie mam takiego pod ręką.

20230823_161452.zip

17 minut temu, prezesedi napisał:

zasilacz z kolejki z regulowanym napięciem

Nie wiem co to za zasilacze, ale o ile pamiętam zasilacz fo kolejki daje napięcie wyprostowane jakimś mostkiem i to wszystko. Do zasilania urządzeń audio się nie nadaje i lepiej nie eksperymentuj bo zaraz pogonisz po drugi moduł. Podejrzewam, że podobnie jest z tym drugim - bo na moje ucho starego dźwiękowca to typowy objaw zasilania napięciem bez filtracji. Tym się żarówki i silniki zasila.

Jeśli nie możesz podłączyć jakiejś baterii to zrób coś takiego:

W monitorze serial przy wyłączonym zasilaniu I2S wydaj polecenia:

vol 5
store

Podłącz zasilanie I2S do pinu 5V płytki ESP i spróbuj. Przy takiej głośności powinno jeszcze prądu wystarczyć. Daj znać co wychodzi.

A tak przy okazji:

Przyszło do mnie właśnie coś takiego: https://botland.com.pl/wyswietlacze-lcd-tft-i-ips/19193-wyswietlacz-lcd-ips-114-240x135px-spi-65k-rgb-waveshare-18231--5904422371852.html

Po zmianie paru linijek w programie rozkład wygląda całkiem sensownie, jest bliżej do skali H0 i można wyświetlić przewoźnika 🙂

 

 

 

Na bank uszkodzony jest moduł dźwięku. Przestał wydawać jakiekolwiek dźwięki.

Próbowałem podłączyć 2 baterie typu AAA z koszyczka (taki miałem akurat pod ręką) oraz z zasilacza 12v poprzez przetwornicę LM2596

https://allegro.pl/oferta/przetwornica-lm2596-lm2596s-step-down-out-0-37v-3a-11736943373?bi_s=ads&bi_m=listing:desktop:query&bi_c=Y2U4NWU4MTgtNTY4YS00MmIxLTk5MGMtODQ5ZmJlZmIwNGYzAA&bi_t=ape&referrer=proxy&emission_unit_id=dfe481c6-7004-477a-a9a2-8c64b493ac84

Efektów brak. Zamawiam nowy moduł dźwięku. Może przyjdzie przed weekendem.

10 minut temu, prezesedi napisał:

Na bank uszkodzony jest moduł dźwięku.

Nie dziwię się - przy takim zasilaniu...

11 minut temu, prezesedi napisał:

Zamawiam nowy moduł dźwięku.

A zasilacz?

Przy okazji - co myślisz o tym wyświetlaczu o którym pisałem? 

28 minut temu, ethanak napisał:

Przy okazji - co myślisz o tym wyświetlaczu o którym pisałem

Ale to ma być wyświetlacz w panelu? Jest ok. Kupić?

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