Skocz do zawartości

Arduino w modelarstwie kolejowym


Pomocna odpowiedź

Rozumiem że kalibracja działa (po wciśnięciu klawisza A)? Jeśli tak, to idziemy dalej. Jeśli nie - na razie nie będę wykorzystywać dotyku.

Niestety - teraz zostawiłem całość w trybie mało używalnym: wyświetlacz OLED już nie działa, a TFT jeszcze nie... a więc na koniec dnia wersja która co prawda niewiele wnosi, ale da się z niej korzystać: kolejpan7.zip

Trochę za późno aby opowiadać o kodzie, ale jutro to nadrobię. Czyli jak zwykle: stay tuned!

(edytowany)

Krzyżyk to nieeee (wody święconej też brak)

Ale Plus w lewym górnym rogu jest 🙃

 

edit.

naciskając plusik pojawia sie

C dalej

i zmieniają się (no właśnie co?)

np 1122 x 2334

Edytowano przez prezesedi
  • Lubię! 1

Wczoraj wrzuciłem tylko kawałek kodu, ale pewnie warto coś o nim więcej napisać.

Przede wszystkim od początku moim założeniem było, aby kolejne wersje programu były stuprocentowo użyteczne. Tymczasem usunięcie wyświetlacza OLED uniemożliwiło bezpośrednie sterowanie z klawiatury (chociaż dalej było możliwe sterowanie przez interfejs WWW). Po prostu - nie było na czym wyświetlać...
Z drugiej strony zakładałem minimalną ingerencję w istniejący kod, polegającą raczej na jego stopniowym rozbudowywaniu a nie kompletnych zmianach. Tymczasem przejście na sterowanie panelem dotykowym wymaga przepisania w zupełnie inny sposób dużej części programu. Jak to pogodzić?

Rozwiązanie było proste. Zamiast tworzyć nowe funkcje, postanowiłem po prostu wy świetlić na ekranie TFT to, co byo do tej pory wyświetylane na małym OLED-zie.

Na początek doszła oczywiście inicjalizacja wyświetlacza TFT. Ta jest w miarę prosta, nawet biorąc pod uwagę możliwość pracy z dwoma różnymi driverami. Na szczęście biblioteki Adafruit są tak skonstruowane, że większość funkcji jest wspólna - różnice polegają jedynie na ilicjalizacji owych driverów. Tak więc uzależniłem ową inicjalizację od jednej definicji w config.h. Zacząłem od definicji pinów i wyświetlacza:

// w config.h
#define DISP_CS 4
#define DISP_RST 5
#define DISP_DC 18
#define DISP_MOSI 19
#define DISP_SCK 13
#define DISP_MISO 12
#define DISP_CS2 14
#define DISP_IRQ2 27
// w display.cpp
#include <SPI.h>

#ifdef USE_ILI9341
#include <Adafruit_ILI9341.h>
Adafruit_ILI9341 tft(&SPI,DISP_DC, DISP_CS, DISP_RST);
#else
#include <Adafruit_ST7789.h>
Adafruit_ST7789 tft(&SPI,DISP_CS, DISP_DC, DISP_RST);
#endif

Tu widać, że zależnie od zdefiniowania USE_ILI9341 tworzę obiekt klasy związanej z konkretnym driverem. Ponieważ sama procedura inicjalizacji jest trochę inna, oto odpowiedni fragment:

void initDisplay()
{
    SPI.begin(DISP_SCK, DISP_MISO, DISP_MOSI, DISP_CS);
#ifdef USE_ILI9341
    tft.begin();
#else
    tft.init(240,320);
    tft.invertDisplay(false);
#endif
    tft.setRotation(
#ifdef SCREEN_ROTATE
1
#else
3
#endif
    );
    tft.setTextWrap(0);
    tft.fillScreen(RGB565(64,64,64));

Jak widać, różnica jest wyłącznie w wywołaniach metod inicjalizujących fizycznie driver; kolejne metody jak setRotation czy setTextWrap są już wspólne dla obu driverów. W ten sposób można użyć praktycznie dowolnego wyświetlacza (chociaż pozostawiłem ograniczenie do orozdzielczości 320x240). Skorzystałem tu z faktu, że magistralę SPI można w ESP8266 uruchomić na dowolnych uniwersalnych pinach GPIO - czyli podłączyłem po prostu ekran tam, gdzie do tej pory podłączona była klawiatura matrycowa.

A co z OLED-em?

Wykorzystałem tu fakt, że wszystkie funkcje biblioteki Adafruit_GFX działają również na wirtualnych wyświetlaczach (czyli canvas). Tak więc deklaracja obiektu display zmieniła się z:

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

na:

GFXcanvas1 display(SCREEN_WIDTH, SCREEN_HEIGHT);

Ponieważ wyświetlacz OLED jest jednobitowy (tzn. piksel jest albo zapalony, albo zgaszony) wystarczył mi również jednobitowy wirtualny wyświetlacz GFXcanvas1.

Trzeba było dokonać tylko kilku kosmetycznych zmian związanych z brakiem definicji stałych związanych z konkretnym wyświetlaczem - i w ten sposób prawie wszystkie funkcje pozostały bez zmian.

Oczywiście - znalazł się jeden wyjątek: metoda display() drivera SSD1306 kopiowała zawartość bufora do fizycznego ekranu - canvas tego nie potrafi (bo nic nie wie o jakichś fizycznych ekranach). Mogłem oczywiście stworzyć klasę opartą na GFXcanvas1 wzbogaconą o tę metodę, ale wymagałaby ona podania obiektu wyświetlacza TFT albo użycia zmiennej globalnej, co uznałem za niezbyt dobry pomysł. W miejsce tego stworzyłem prostą funkcję display_display(), która realizuje fizyczne wyświetlanie, a przy okazji powiększa dwukrotnie wyświetlany obraz. Podobną funkcję stworzyłem wcześniej do wyświetlania rozkładu jazdy przez ESP8266, tam jednak chodziło o szybkość wyświetlania - tu mamy kopiowanie z powiększeniem: 

void display_display()
{
    tft.startWrite(); // rozpoczęcie "rozmowy" z wyświetlaczem
    tft.setAddrWindow(160-128,5,256,64); // określenie obszaru obrazu
    int x,y,n;uint16_t color;
    for (y=0;y<32;y++) {
        for (n=0;n<2;n++) { // każda linia skanowana jest dwukrotnie
            for (x=0;x<128;x++) {
                color = display.getPixel(x,y)?0xffff:0;
                tft.SPI_WRITE16(color); //i każdy poksel wysyłany dwa razy
                tft.SPI_WRITE16(color);
            }
        }
    }
    tft.endWrite();
}


Przypomnę, że dla wszystkich wyświetlaczy klasy SPITFT wygląda to identycznie: metoda setAddrWindow (ta może być wewnętrznie różna dla różnych wyświetlaczy, ale o to dba już odpowiednia klasa biblioteki) określa położenie i rozmiar obszaru na którym będziemy operować, a kolejne 16-bitowe wartości odpowiadają kolorom kolejnych wyświetlanych pikseli.

Dla ciekawych - tak wygląda ekran TFT:

tftroz.thumb.jpg.1450e4b4db555a104aef8ca111308b3f.jpg

A gdyby ktoś chciał zobaczyć jak mógłby wyglądać kod bez żadnych przeróbek dotyczących wyświetlania na SSD... oto odpowiedni fragmencik:

// ponieważ nie włączamy nagłówka SSD1306

#define SSD1306_WHITE 1

class VirtualSSD : public GFXcanvas1
{
    public:
    VirtualSSD(int w, int h, Adafruit_SPITFT *tft) : GFXcanvas1(w,h) {
        this->tft=tft;
        };
    void display();
    void clearDisplay() {fillScreen(0);};
    private:
    Adafruit_SPITFT *tft;
};

void VirtualSSD::display()
{
    tft->startWrite();
    tft->setAddrWindow(160-128,5,256,64);
    int x,y,n;uint16_t color;
    for (y=0;y<32;y++) {
        for (n=0;n<2;n++) {
            for (x=0;x<128;x++) {
                color = getPixel(x,y)?0xffff:0;
                tft->SPI_WRITE16(color);
                tft->SPI_WRITE16(color);
            }
        }
    }
    tft->endWrite();
}

VirtualSSD display(SCREEN_WIDTH, SCREEN_HEIGHT,&tft);

Oczywiście jak już wspominałem - jest to rozwiązanie tymczasowe, ale będzie użyte aż ukończenia wszystkich funkcji dotyku i wyświetlania.

No i na wszelki wypadek kod z użyciem klasy VirtualSSD: kolejpan7c.zip

Następna wersja będzie już używać funkcji dotyku i wyświetlać coś ładniejszego - czyli jeśli się interesujesz... stay tuned!

  • Lubię! 1

No cóż: jedziemy dalej, czyli trzeba wreszcie uruchomić dotyk!

Zacznę od pewnego założenia: dopóki funkcje związane z panelem dotykowym nie będą do końca zaimplementowane, nie mogę zmienić sterowania z klawiatury (zgodnie z przyjętą zasadą, że program ma być w całości funkcjonalny). Dlatego wyświetlacz będzie działać na razie w dwóch trybach: wyświetlania informacji na wirtualnym SSD jeśli włączone jest sterowanie z klawiatury, i docelowego sposobu wyświetlania przy sterowaniu z dotyku. W tym celu wprowadzę dodatkową zmienną globalną i deklaracje w pliku Makieta.h:

extern uint8_t inputMode;
enum {
    IMODE_KEYBOARD = 0,
    IMODE_TOUCH
};

Zakładam, że wciśnięcie dowolnego klawisza spowoduje przełączenie w tryb IMODE_KEYBOARD, czyli trzeba nieco zmodyfikować funkcję getCommand z pliku getcommand.cpp:

uint16_t getCommand()
{
    int g=keypad.getKey();
    
    if (!g) {
        if (inputMode == IMODE_KEYBOARD) { // tylko w trybie KEYBOARD
            if (kbdStatus && millis() - lastKeyPressed > 5000UL) {
                kbdStatus=KBD_RELAX;
                displayKbdStatus();
            }
        }
        return 0;
    }
    if (inputMode != IMODE_KEYBOARD) { // jeśli w trybie TOUCH
        // wciśnięcie klawisza przełącza w tryb KEYBOARD
        inputMode = IMODE_KEYBOARD;
        // przygotowuje wyświetlacz do pracy z klawiaturą
        initKeyboardDisplay();
        // i ustawia klawiaturę w stanie początkowym
        kbdStatus = KBD_RELAX;
    }

O funkcji initKeyboardDisplay powiem później, dodam tylko, że będę używał jeszcze jednej funkcji realizującej wszystko co jest związne z dotykiem. Deklaracje owych funkcji wrzucę do Makieta.h:

extern void initKeyboardDisplay();
extern void touchLoop();

I jeszcze ważna sprawa: otóż cały kod związany z dotykiem możę działać tylko wtedy, jeśli w programie do kalibracji zostały zapisane dane. No i oczywiście te dane trzeba jakoś odczytać... Tak więc w Makieta.h dodam odpowiednie deklaracje:

extern struct calpos {
   int16_t x_lu,y_lu,x_rd,y_rd;
} calpos;
extern bool have_calpos;

Zmienna have_calpos będzie zawierać informacje, czy ustawienia zostały wczytane, natomiast calpos to współrzędne lewego górnego i prawego dolnego krzyzyka, według których będę przeliczać rzeczywistą pozycję na ekranie.

W pliku prefs.cpp też trzeba dodać definicje tych zmiennych:

struct calpos calpos = {
    -1,-1,-1,-1
};
bool have_calpos=false;

i w funkcji initPreferences() dopisać linijkę:

have_calpos=preferences.getBytes("calpos",(void *)&calpos,sizeof(calpos)) == sizeof(calpos);

Pozostaje jeszcze drobna zmiana w głównej pętli loop() - muszę wywołać funkcję touchLoop. Czyli w głównym pliku .ino dopisuję wywołanie tej funkcji od razu po pierwszej instrukcji if - będzie to wyglądać tak:

        else {
            semafor[SEM_NUM(cmd)].set(SEM_CMD(cmd));
        }
    }
    touchLoop(); // dopisane

    // niezależnie od tego czy coś robiliśmy czy nie

    for (int i=0; i<CNT_SEMA; i++) semafor[i].update();

To wszystkie zmiany poza plikiem display.cpp, mogę się więc zająć spokojnie wyświetlaniem.

Przede wszystkim chcę wyświetlić jakieś menu. Postanowiłem zrobić to w górnej linii, tak więc musiałem przesunąć miejsce wyświetlania wirtualnego SSD. Na szczęście wymagało to zmiany tylko w jednej linii programu w funkcji display_display (w określeniu okna wirtualnego wyświetlacza). Dodatkowo od razu w tej funkcji mogłem umieścić zabezpieczenie przed wyświetlaniem czegokolwiek w trybie TOUCH. Rozwiązanie może nie jest najwyższych lotów (bo jeśli program będzie usiłował coś wyświetlić będzie i tak skrzętnie rysował na canvas, a wyniki jego ciężkiej pracy zostaną zignorowane) - ale trzeba pamiętać, że docelowo wirtualny SSD zniknie z programu, nie ma co więc dopieszczać kodu o którym wiadomo, że zostanie usunięty.

Tak więc początek funkcji:

void display_display()
{
    if (inputMode != IMODE_KEYBOARD) return;
    tft.startWrite();
    tft.setAddrWindow(160-128,18,256,64);

W funkcji initDispay muszę dodatkowo zainicjalizować interfejs dotykowy i narysować coś sensownego zamiast szarego ekranu. Tak więc zamieniam linijki:

    tft.setTextWrap(0);
    tft.fillScreen(RGB565(64,64,64));

na:

    tft.cp437();
    tft.setTextWrap(0);
    initKeyboardDisplay();
    initTouch();

I tu muszę wrócić do initKeyboardDisplay. Funkcja ma przygotować ekran do wyświetlania wirtualnego wyświetlacza, a przy okazji narysować główne menu. Można to było wszystko upchnąć w jednym miejscu, ale wolałem to podzielić nz dwie funkcje robiące jedną dobrze określoną czynność. Ponieważ główne menu składać ię ma z czterach pozycji odpowiadającym głównym funkcjom programu, funkcja rysująca menu wraz z funkcją inicjalizującą wygląda tak:

const char * const mainMenu[]={
    "Semafory","Rozk\222ad","Komunikaty","Zapowiedzi"};
void drawHButton(int x, int y, int w, int h,const char *txt)
{
    tft.fillRect(x,y,2,h,0xffff);
    tft.fillRect(x+w-2,y,2,h,GRAY565(140));
    tft.drawFastHLine(x,y,w-1,0xffff);
    tft.drawFastHLine(x+1,y+h-1,w-1,GRAY565(140));
    tft.fillRect(x+2,1,w-4,h-2,RGB565(200,200,200));
    tft.setCursor(x+w/2-strlen(txt)*3,y+h/2-8);
    tft.setTextSize(1,2);
    tft.setTextColor(0);
    tft.print(txt);
}
void drawMainMenu()
{
    tft.fillRect(0,0,320,18,0);
    int i;
    for (i=0;i<4;i++) {
        drawHButton(80*i+1,0,78,18,mainMenu[i]);
    }
}

void initKeyboardDisplay()
{
    tft.fillScreen(RGB565(64,64,64));
    drawMainMenu();
}

Użyłem tu makra GRAY565 - po prostu skrótu ustawiającego odcień szarości:

#define GRAY565(n) RGB565(n,n,n)

tftmenu.thumb.jpg.654d1e60b242c5d4e525802b63e02a00.jpg

No i teraz można zająć się funkcjami związanymi z obsługą dotyku..

Postanowiłem całą pracę podzielić na etapy - w pierwszym zaimplementować tylko obsługę interfejsu i najprostszą część (czyli komunikaty stałe). Tak więc zacznę od funkcji touchLoop, a właściwie od zdefiniowania potrzebnych do działania funkcji zmiennych:

static uint8_t touchCtl=0; // zmienna kontrolna
static bool touchMenu=false; // czy ostatni dotyk to było menu?
static uint8_t touchScreenMode; // co jest wyświetlane?
static uint8_t touchLastPos=0; // ostatnia wybrana pozycja
static int scrx, scry; // pozycja dotyku

// możliwe wartości w touchScreenMode:
enum {
    TSM_NONE = 0,
    TSM_SEMA,
    TSM_SCHEDULE,
    TSM_MESSAGES,
    TSM_ANNOUNCES,
    TSM_ERROR,
};

Dodatkowo stworzyłem prostą funkcję odczytującą położenie dotyku:

bool getScreenXY(int *x, int *y)
{
    if (!ts->touched()) return false;
    TS_Point p=ts->getPoint();
    *x=constrain(
        map(p.x,calpos.x_lu, calpos.x_rd, 16,320-16),0,319);
    *y=constrain(
        map(p.y,calpos.y_lu, calpos.y_rd, 16,240-16),0,239);
    return true;
}

Początek funkcji touchLoop wydaje się oczywisty:

void touchLoop()
{
    if (!have_calpos) return; // nic nie robimy jeśli nie było kalibracji
    if (!getScreenXY(&scrx, &scry)) {
        touchCtl=0;
        touchMenu=false;
        return;
    }

Tym, co powinno działać niezależnie od stanu wyświetlacza jest reakcja na menu. Postanowiłem wywoływać funkcję sprawdzającą pozycję wtedy, gdy dotknę górnej linii. Funkcja sprawdzająca menu jest dość prosta:

int getMenuPos()
{
    int mn=scrx/80+1;
    int mx=scrx % 80;
    if (mx <8 || mx >=72 || mn < 1 || mn > 4) { // poza menu
        touchCtl=0;
        touchMenu=false;
        return 0;
    }
    if (!touchMenu || touchLastPos != mn) {
        touchCtl=0;
        touchMenu = true;
        touchLastPos = mn;
        return 0;
    }
    if (touchCtl++ < 4) return 0;
    touchCtl=0;
    touchMenu=0;
    return touchLastPos;
}

Zadowolony z siebie wrzuciłem do touchLoop coś w stylu:

    if (scry < 18) {
        int pos=getMenuPos();
        if (pos) cośtam...

I okazało się, że nie wziąłem pod uwagę jednej rzeczy: po wykryciu pozycji nie mogę interpretować dalszego dotyku dopóki nie odejmę rysika od ekranu. Początkowo chciałem wprowadzić dodatkową zmienną bool ustawianą po każdym potwierdzeniu przyjęcia polecenia z dotyku i blokującą odczyty aż do puszczenia, ale postanowiłem to zrobić porządnie.

Zdefiniowałem sobie zmienne kontrolujące aktualny stan interfejsu dotykowego oraz czas ostatniego puszczenia. Stany mogą być trzy: oczekiwanie na puszczenie, martwy czas po puszczeniu i zezwolenie na odczyty. Czyli:

enum {
    TWS_NOWAIT = 0,
    TWS_WAITING,
    TWS_TOUCHED
};
static uint8_t touchWaitStatus=0;
static uint32_t touchWaitTimer;

Cała obsługa blokowania dotyku została schowana w funkcji getScreenXY, która teraz wygląda tak:

bool getScreenXY(int *x, int *y)
{
    if (!ts->touched()) {
        switch(touchWaitStatus) {
            case TWS_TOUCHED: // rozpoczyamy oczekiwanie
            touchWaitStatus = TWS_WAITING;
            touchWaitTimer = millis();
            break;

            case TWS_WAITING:
            if (millis() - touchWaitTimer > 200) {
                touchWaitStatus = TWS_NOWAIT;
            }
            break;
        }
        return false;
    }

    if (touchWaitStatus) {
        touchWaitStatus = TWS_TOUCHED;
        return false;
    }
    
    TS_Point p=ts->getPoint();
    *x=constrain(
        map(p.x,calpos.x_lu, calpos.x_rd, 16,320-16),0,319);
    *y=constrain(
        map(p.y,calpos.y_lu, calpos.y_rd, 16,240-16),0,239);
    return true;
}

Jak widać, odczyt jest odblokowywany dopiero 200 msec po puszczeniu, co powinno wyeliminować jakieś chwilowe anomalie. W funkcji getMenuPos dodałem ustawienie zmiennej kontrolnej:

    touchWaitStatus = TWS_TOUCHED;
    return touchLastPos;
}

i w ten sposób pozbyłem się niepożądanych efektów.

Wrócę jednak do obsługi. W touchLoop() pojawił się kolejny fragment kodu:

    if (scry < 18) { // menu?
        int pos=getMenuPos();
        if (!pos) return; // poza menu
        inputMode = IMODE_TOUCH;
        switch(pos) {
            case 3:
            drawTMsgList();
            break;

            default:
            drawTError("NIE ZAIMPLEMENTOWANO");
            
        }
        return;
    }


Jak widać, po wykryciu wybrania pozycji z menu następuje przełączenie wyświetlacza w tryb TOUCH (żeby funkcja display_display nie próbowała przeszkadzać) i narysowanie planszy odpowiadającej owej pozycji. Dla nieobsługiwanych (na razie) pokazuje się śliczny napis "NIE ZAIMPLEMENTOWANO":

static void drawTError(const char *txt)
{
    touchScreenMode = TSM_ERROR;
    touchLastPos=0;
    tft.fillRect(0,18,320,240-18,0);
    tft.setTextColor(RGB565(255,0,0));
    tft.setTextSize(2,3);
    tft.setCursor(160-strlen(txt)*6,120);
    tft.print(txt);
}

tftnoim.thumb.jpg.d624ba28b6f8443f6150df08fdb79b45.jpg
Najprostsza do zaimplementowania pozycja to stałe komunikaty. Ponieważ jest ich ograniczona ilość, nie trzeba bawić się w jakieś skrolowanie list, wszystko ładnie mieści się na ekranie:

static void drawTMsgList()
{
    touchScreenMode = TSM_MESSAGES;
    touchLastPos=0;
    tft.fillRect(0,18,320,240-18,0);
    int i;
    tft.setTextSize(1,2);
    tft.setTextColor(0);
    for (i=0; i<9;i++) {
        char buf[64];
        utf2local(komunikaty[i].c_str(),buf,53);
        tft.fillRect(0,20+24*i,320,22,GRAY565(180+40*(i&1)));
        tft.setCursor(3,24+24*i);
        tft.print(buf);
    }        
}

tftkoms.thumb.jpg.b2dda98238def0a5e0bb4cbe17c582c5.jpg

Pozostała jedynie obsługa owych komunikatów. Ta również jest prosta. Fragment kodu w touchLoop() odpowiada za przekazanie pozycji do właściwej funkcji obsługi:

    // kod wywoływany tylko w trybie TOUCH!    
    if (inputMode != IMODE_TOUCH) return;
    switch (touchScreenMode) {
        case TSM_MESSAGES:
        touchDoMessages();
        break;
    }

a sama funkcja jest też stosunkowo prosta:

static void touchDoMessages()
{
    if (scry < 22) {
        touchCtl=0;
        return;
    }
    int mn=(scry-22) / 24;
    int mx=(scry-22) % 24;
    if (mx > 20 || mn > 8) {
        touchCtl=0;
        return;
    }
    mn += 1;
    if (touchLastPos != mn) {
        touchLastPos = mn;
        touchCtl = 0;
        return;
    }
    if (touchCtl++ < 4) return;
    sayText(komunikaty[touchLastPos-1].c_str(),true);
    touchWaitStatus=TWS_TOUCHED;
}

Teraz dotknięcie właściwego komunikatu spowoduje przekazanie go do syntezatora mowy.

Kod jak zwykle w załączniku: kolejpan8.zip

Teraz spróbuję zająć się kolejną funkcjonalnością - rozkładami jazdy. A więc jak zwykle: stay tuned!
 

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

Ale się zawziąłeś

No cóż - sam się tym całkiem nieźle bawię. Zobacz: z prostego programiku na Arduino który potrafił jakieś tam semafory pozapalać i kaszlał jak mu się wyświetlacz podłączyło robi się całkiem ciekawy panel do obsługi makiety z syntezą komunikatów, ekranem dotykowym i gadaniem przez radio...

Zresztą ja już powoli kończę zabawę. Zostało jeszcze parę klocków do wystrugania, a potem ktoś niech to ulepsza. Nie chcesz się na ochotnika zgłosić przypadkiem? 😉

5 minut temu, farmaceuta napisał:

.@prezesedi to tą kolejką będzie musiał chyba do końca życia Cię za darmo wozić

A tu przy okazji ćwiczę cierpliwość 😉

Poza tym jak pewnie zauważyłeś, przemycam w tym wątku trochę swoich rozwiązań. Zawsze to potem łatwiej w razie jakichś pytań skierować go tutaj nioż siedemnasty raz wyjaśniać "jak to się robi".

 

 

  • Lubię! 2
11 minut temu, ethanak napisał:

 z prostego programiku na Arduino który potrafił jakieś tam semafory pozapalać i kaszlał jak mu się wyświetlacz podłączyło robi się całkiem ciekawy panel

Tu nie ma nawet o czym mówić...tamten kod to zniknął już dawno a w miejsce jego odwaliles kawał dobrej roboty...poziom taki że już odpadłem na początku 😜 gratuluję cierpliwości i chęci...samo pisanie tak długich postów już pochłania dużo czasu nie mówiąc już o programie...👍

  • Lubię! 1
22 minuty temu, farmaceuta napisał:

gratuluję cierpliwości i chęci...samo pisanie tak długich postów już pochłania dużo czasu nie mówiąc już o programie...👍

Przyłączam się...

  • Lubię! 1
(edytowany)

Coż - pora na kolejny etap.

Przede wszystkim stwierdziłem, że miło byłoby, aby na ekranie pokazywała się jakaś informacja z której funkcji korzystam (tzn. semafor, rozkóad i tak dalej). Na wyświetlaczu nie bardzo jest na to miejsce, postanowiłem więc, ze odpowiednia pozycja będzie po prostu zaznaczona w menu.

Tak więc do funkcji drawMainMenu doszedł dodatkowy parametr;

static void drawMainMenu(int pos=0)
{
    tft.fillRect(0,0,320,18,0);
    int i;
    for (i=0;i<4;i++) {
        drawHButton(80*i+1,0,78,18,mainMenu[i],pos == i+1);
    }
}

A funkcja drawHBUtton wygląda teraz tak:

static void drawHButton(int x, int y, int w, int h,const char *txt,bool selected)
{
    uint16_t lborder, rborder, bg;
    if (selected) {
        lborder=GRAY565(140);
        rborder=0xffff;
        bg=GRAY565(180);
    }
    else {
        rborder=GRAY565(140);
        lborder=0xffff;
        bg=GRAY565(210);
    }
        
    tft.fillRect(x,y,2,h,lborder);
    tft.fillRect(x+w-2,y,2,h,rborder);
    tft.drawFastHLine(x,y,w-1,lborder);
    tft.drawFastHLine(x+1,y+h-1,w-1,rborder);
    tft.fillRect(x+2,1,w-4,h-2,bg);
    tft.setCursor(x+w/2-strlen(txt)*3,y+h/2-8);
    tft.setTextSize(1,2);
    tft.setTextColor(0);
    tft.print(txt);
}

Efekt można obejrzeć na zdjęciu poniżej:

tftanc.thumb.jpg.4da27950c7ac93e387aab9edc66888ce.jpg

No, ale to szczegół raczej kosmetyczny, spróbuję stworzyć kawałek kodu odpowiedzialny za współpracę z rozkładem jazdy.

Zacznę od najłatwiejszego, czyli wywołania funkcji rysującej rozkład. W tym celu zmodyfikuję nieco kod funkcji touchLoop:

	if (scry < 18) { // menu?
        int pos=getMenuPos();
        if (!pos) return; // poza menu
        inputMode = IMODE_TOUCH;
        drawMainMenu(pos);
        switch(pos) {
            case 2:
            drawTSched();
            break;
            
            case 3:
            drawTMsgList();
            break;

            default:
            drawTError("NIE ZAIMPLEMENTOWANO");
            
        }
        return;
    }

Jak widać, wywułuję tu funkcję drawMainMenu z odpowiednim parametrem i jeśli jest to druga pozycja, wywołuję nową funkcję drawTSched odpowiedzialną za wyświetlanie rozkładu. Tu chyba nie trzeba nic dodawać, teraz więc coś więcej o tej nowej funkcji.

Przede wszystkim w odróżnieniu od komunikatów (gdzie ilość jest stała i zawsze mieści się na ekranie), tu mamy konieczność narysowania jedynie fragmentu rozkładu zaczynając od jakiejś pozycji oraz podświetlenia pozycji odpowiadającewj aktualnej.W tym celu tworzę dwie dodatkowe zmienne:

static int tschedStart,  // od której pozycji wyświetlamy
           tschedMarked; // która pozycja jest zaznaczona.

Zakładam, że istnieje jakaś funkcja odpowiadająca za rysowanie fragmentu rozkładu, tyak więc cała funkcja mogłaby wyglądać tak I rzeczywiście taka była pierwsza wersja):

static void drawTSched()
{
    if (!rozklad.valid || !rozklad.validPos) {
        drawTError("BRAK ROZK\221ADU");
        return;
    }
    tft.fillRect(0,18,320,240-18,0);
    tschedStart=tschedMarked=rozklad.pos;
    
    drawTSchedPart();    
    touchScreenMode=TSM_SCHEDULE;
}

Funkcja drawTSchedPart jest również prosta, po prostu wywołuje funkcję rysującą jedną linię rozkładu: 

static void drawTSchedPart()
{
    int i,n;
    for (i=0; i<6 && i<rozklad.lines;i++) {
        n=(i+tschedStart) % rozklad.lines;
        drawTSchedLine(i,n);
    }
}

Natomiast funkcja rysująca tę linię jest nie tyle bardziej skomplikowana co dłuższa, ponieważ musi wyświetlić trochę więcej informacji i upchać jew jakimś sensownym fragmencie ekranu:

static void drawTSchedLine(int pos, int spos)
{
    // pos to pozycja na ekranie, spos to pozycja w rozkładzie    
    struct SchedTrain st;
    char buf[32];
    if (!rozklad.getParsedLine(&st, spos)) return;
    int y=20+34*pos;

    // kolor tłą jest inny dla wybranej pozycji
    tft.fillRect(0,y,290,33,(spos == tschedMarked)?RGB565(255,255,0):GRAY565(200));

    tft.setTextSize(1,2);
    tft.setTextColor(0);

    // trochę ładniejsze określenia typu pociągu
    if (st.type == 'E') {
        strcpy(buf,"Ex");
    }
    else if (st.type = 'S') {
        strcpy(buf,"Pr");
    }
    else {
        buf[0]=st.type;
        buf[1]=0;
    }
    // reszta to tylko wyświetlanie tekstów
    utf2local(st.oper, buf+4,4);
    tft.setCursor(4,y+1);tft.printf("%2s %-3.3s %d/%d",
        buf, buf+4, st.tor >> 4, st.tor & 15);
    utf2local(st.trname, buf,12);
    tft.setCursor(4,y+17);tft.printf("%-11.11s ",buf);
    if (st.name2[0]) {
        tft.setCursor(70,y+1);
        utf2local(st.name2,buf,29);
        tft.printf("< %02d:%02d %-28.28s",st.hour2,st.minute2,buf);
    }
    if (st.name[0]) {
        tft.setCursor(70,y+17);
        utf2local(st.name,buf,29);
        tft.printf("> %02d:%02d %-28.28s",st.hour,st.minute,buf);
    }
}

Ekran z wyświetlaniem rozkładu wyglądał więc początkowo tak:

tftschb.thumb.jpg.e8a0a39823219f1cae92476decff21c9.jpg

Od razu widać czego brakuje. Wyświetlany jest przecież tylko fragment rozkładu, a jakoś trzeba się sensownie po nim poruszać. Początkowo chciałem zastosować tu jakiś ładny slider który można przesuwać i w ten sposób skrolować rozkład, ale po namyśle z tego zrezygnowałem. Przede wszystkim precyzja takiego slidera (biorąc pod uwagę "średniowatą" precyzję samego interfejsu dotyku) byłaby również "średniowata". Może przy większym ekranie i pojemnościowym interfejsie miałoby to sens - ale tu raczej niespecjalnie. Poza tym dość mocno skomplikowałoby to kod. Postanowiłem narysować jakieś sensowne przyciski do poruszania się po ekranie, a mały sliderek służyłby tylko do pokazania, który fragment rozkładu jest aktualnie wyświetlany.

Zacząłem od przycisków. W funkcji drawTSched dodałem ich rysowanie:

static void drawTSched()
{
    if (!rozklad.valid || !rozklad.validPos) {
        drawTError("BRAK ROZK\221ADU");
        return;
    }
    tft.fillRect(0,18,320,240-18,0);
    tschedStart=tschedMarked=rozklad.pos;
    drawUDKey(20,0);
    drawUDKey(54,1);
    drawUDKey(54+3*34,2);
    drawUDKey(54+4*34,3);
    
    drawTSchedPart();
    touchScreenMode=TSM_SCHEDULE;
}

Funkcja rysująca przycisk przyjmuje dwa parametry: pozycję na ekranie oraz typ przycisku. Sprowadza się do prostego kodu:

static void drawUDKey(int y, int nr)
{
    tft.drawFastHLine(292,y,28,0xffff);
    tft.drawFastVLine(292,y,33,0xffff);
    tft.drawFastHLine(293,y+32,27,GRAY565(180));
    tft.drawFastVLine(319,y+1,32,GRAY565(180));
    tft.fillRect(293,y+1,26,31,GRAY565(220));
    switch(nr) {
        case 0:
        tft.fillTriangle(295,y+16,306,y+3,317,y+16,0);
        tft.fillTriangle(295,y+29,306,y+16,317,y+29,0);
        break;

        case 1:
        tft.fillTriangle(295,y+22,306,y+9,317,y+22,0);
        break;

        case 2:
        tft.fillTriangle(295,y+9,306,y+22,317,y+9,0);
        break;

        case 3:
        tft.fillTriangle(295,y+3,306,y+16,317,y+3,0);
        tft.fillTriangle(295,y+16,306,y+29,317,y+16,0);
        break;

    }
}

Ponieważ zostało miejsce w środku między przyciskami, zmieścił się tam mały sliderek. Wywołanie funkcji rysującej umieściłem w drawTSchedPart (jako że za każdym razem kiedy rysujemy rozkład, należy narysować również slider we właściwej pozycji),a funkcja rysowania też chyba nie wymaga szerszego omawiania:

#define TSLIDER_TOP 90
#define TSLIDER_HEIGHT 64
#define TSLIDER_X 292
#define TSLIDER_W (320 - TSLIDER_X)

static void drawTSlider()
{
    tft.drawFastHLine(TSLIDER_X, TSLIDER_TOP + TSLIDER_HEIGHT-1, TSLIDER_W, 0xffff);
    tft.drawFastVLine(TSLIDER_X+TSLIDER_W-1, TSLIDER_TOP, TSLIDER_HEIGHT, 0xffff);
    tft.drawFastHLine(TSLIDER_X, TSLIDER_TOP, TSLIDER_W-1, GRAY565(150));
    tft.drawFastVLine(TSLIDER_X, TSLIDER_TOP, TSLIDER_HEIGHT-1, GRAY565(150));
    tft.fillRect(TSLIDER_X+1, TSLIDER_TOP+1, TSLIDER_W-2, TSLIDER_HEIGHT-2, RGB565(0,0,255));
    int ysize, ypos;
    if (rozklad.lines <= 6) {
        ysize = TSLIDER_HEIGHT-4;
        ypos = TSLIDER_TOP+2;
    }
    else {
        ysize = ((TSLIDER_HEIGHT-4) * 6) / rozklad.lines;
        ypos = TSLIDER_TOP + 2 + ((TSLIDER_HEIGHT-4-ysize) * tschedStart)/(rozklad.lines-1);
    }
    tft.fillRect(TSLIDER_X+3, ypos, TSLIDER_W-6, ysize, RGB565(255,255,0));
       
}


static void drawTSchedPart()
{
    int i,n;
    for (i=0; i<6 && i<rozklad.lines;i++) {
        n=(i+tschedStart) % rozklad.lines;
        drawTSchedLine(i,n);
    }
    drawTSlider();
}

W efekcie wygląda to tak:

tftscha.thumb.jpg.4072c9d3de964b0f0ba332cb1e8da322.jpg

Przyciski służą do przesuwania zawartości ekranu o stronę (5 pozycji) lub pozycję, slider pokazuje od którego miejsca jest wyświetlania.

Pozostało już tylko zrobić jakąś reakcję na dotyk. Czyli znowu dopisek do funkcji touchLoop:

    switch (touchScreenMode) {
        case TSM_MESSAGES:
        touchDoMessages();
        break;
        case TSM_SCHEDULE:
        touchDoSchedule();
        break;
    }

Sama funkcja touchDoSchedule też nie jest jakoś specjalnie skomplikowana. Pobierana jest pozycja dotknięcie ekramu. Jeśli odpowiada wyświetlanej pozycji, rozkład ustawiany jest na tę pozycję. Jeśli któremuś przyciskowi, wyświetlany rozkład jest przesuwany do właściwego miejsca:

// funkcja zwraca wartość od 1 do 6 odpowiadającą pozycji,
// lub zero jeśli nie jest to prawidłowa pozycja
static int getTouchSchedPos()
{
    if (scrx > 280) return 0;
    int y = scry - 21;
    if (y < 0) return 0;
    int mn = y/34;
    if (mn >= rozklad.lines || mn > 5) return 0;
    if (y % 34 > 31) return 0;
    return mn+1;
    
// funkcja zwraca wartości 10,11,14,15 przyporządkowane wybranemu przyciskowi
// jub zero jeśli to nie jest pozycja przycisku
static int getTouchSchedKey()
{
    if (scrx<295) return 0;
    int y = scry - 21;
    if (y < 0) return 0;
    int mn = y/34;
    if ((mn > 1 && mn <4) || mn > 5) return 0;
    if (y % 34 > 31) return 0;
    return mn+10;
    
}

static void touchDoSchedule()
{
    int mn=getTouchSchedPos();
    if (!mn) mn=getTouchSchedKey();
    if (!mn) {
        touchCtl=0;
        return;
    }
    if (touchLastPos != mn) {
        touchLastPos = mn;
        touchCtl = 0;
        return;
    }
    if (touchCtl++ < 4) return;
    if (mn >= 10) {
        if (mn == 10) tschedStart=(tschedStart+rozklad.lines-5) % rozklad.lines;
        else if (mn == 11) tschedStart=(tschedStart+rozklad.lines-1) % rozklad.lines;
        else if (mn == 14) tschedStart=(tschedStart+1) % rozklad.lines;
        else if (mn == 15) tschedStart=(tschedStart+5) % rozklad.lines;
        touchWaitStatus=TWS_TOUCHED;
        drawTSchedPart();
        return;
    }
    rozklad.set((tschedStart+mn-1) % rozklad.lines);
    tschedMarked=rozklad.pos;
    touchWaitStatus=TWS_TOUCHED;
    drawTSchedPart();
}

I to na razie wszystko. Kod jak zwykle w załączniku: kolejpan9.zip

A ja ze swej strony postaram się opracować jeszcze jeden kawałek kodu (na szczęście krótki, więc może się wyrobię zanim mnie zagonią do jakichś świątecznych prac) - tak że jak zwykle: stay tuned!
 

Edytowano przez ethanak

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