prezesedi Grudzień 18, 2023 Autor tematu Udostępnij Grudzień 18, 2023 Po małych zawirowaniach z wersją 8 i wyjaśnieniem kilku rzeczy z autorem kol. @ethanak, uruchomiłem wersję 8 i następnie wersję 9. Mamy to! Cytuj Link do komentarza Share on other sites More sharing options...
ethanak Grudzień 19, 2023 Udostępnij Grudzień 19, 2023 Teraz będzie krócej. Ponieważ sterowanie rozkładem jazdy i zapowiedziami pociągów jest podobne, postanowiłem zrobić wspólny kod dla tych funkcji. Przy okazji wyszło mi, że pomyliłem się obliczając wolne miejsce na ekranie i musiałem podnieść rozkład jazdy o jeden piksel w górę, ale to szczegół. Wrócę więc do kodu. Ponieważ używam tej samej funkcji do rysowania rozkładu, postanowiłem dodać do funkcji parametr mówiący którą planszę rysuje. Tak więc w touchLoop pojawił się fragment: switch(pos) { case 2: drawTSched(TSM_SCHEDULE); break; case 3: drawTMsgList(); break; case 4: drawTSched(TSM_ANNOUNCES); break; default: drawTError("NIE ZAIMPLEMENTOWANO"); } Natomiast sama funkcja drawTSched zyskała również dodatkowe parę linijek: static const char * const annoMenu[]={"Odjazd","Przyjazd", "Post\363j"}; static void drawTSched(int tsm) { if (!rozklad.valid || !rozklad.validPos) { drawTError("BRAK ROZK\221ADU"); return; } touchScreenMode=tsm; //ustawiam zmienną globalną tft.fillRect(0,18,320,240-18,0); tschedStart=tschedMarked=rozklad.pos; drawUDKey(19,0); drawUDKey(53,1); drawUDKey(53+3*34,2); drawUDKey(53+4*34,3); // i jeśli to zapowiedzi, rysuję na dole dodatkowe klawisze zapowiedzi pociągów: if (tsm == TSM_ANNOUNCES) { int i; for (i=0;i<3;i++) { drawHButton(80*i+41,240-18,78,18,annoMenu[i],false); } } drawTSchedPart(); } Kolejna mała modyfikacja dotyczy funkcji drawTSchedLine. Uznałem, że warto odróżnić kolorami wybraną pozycję na dwóch planszach. Tak więc odpowiednia linijka ustalająca kolor tła wygląda teraz tak: tft.fillRect(0,y,290,33,(spos == tschedMarked)? ((touchScreenMode == TSM_SCHEDULE) ? RGB565(255,255,0) : RGB565(128,255,128)): GRAY565(200)); I to już wszystko jeśli chodzi o rysowanie. Efekt można zobaczyć na zdjęciu: Kolej na reakcję programu na dotyk. Ponieważ użyję tej samej funkcji co w sterowaniu rozkładem, ostatnia instrukcja switch w touchLoop będzie wyglądać następująco: switch (touchScreenMode) { case TSM_MESSAGES: touchDoMessages(); break; case TSM_SCHEDULE: case TSM_ANNOUNCES: touchDoSchedule(); break; } Trochę więcej przeróbek jest potrzebne w samej funkcji touchDoSchedule. Przede wszystkim należy dodać odczyt przycisków zapowiedzi. W tym celu stworzyłem dodatkową funkcję: static int getTouchAnnoKey() { if (scry <240 -16) return 0; int x = scrx-43; if (x < 0) return 0; int mn = x / 80; if (mn > 2) return 0; if ((x % 80) > 73) return 0; return mn + 20; } Jak widać funkcja zwraca wartości 20, 21 lub 22 dla kolejnych przycisków. W samej funkcji touchDoSchedule wywołanie będzie wyglądać następująco: static void touchDoSchedule() { int mn=getTouchSchedPos(); if (!mn) mn=getTouchSchedKey(); if (!mn && touchScreenMode == TSM_ANNOUNCES) { mn=getTouchAnnoKey(); } Funkcja będzie wywołana tylko wtedy, gdy program znajduje się w stanie ANNOUNCE. Następna modyfikacja dotyczy skrolowania rozkładu. Tym razem chciałęm, aby zaznaczona pozycja była zawsze na ekranie, a więc część odpowiadająca za skrolowanie będzie taka: if (mn >= 10 && mn <20) { 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; if (touchScreenMode == TSM_ANNOUNCES) { correctTSchedMark(mn > 12); } touchWaitStatus=TWS_TOUCHED; drawTSchedPart(); return; } Funkcja correctTschedMark wywoływana tylko w trybie ANNOUNCE jako parametr przyjmuje kierunek skrolowania, a jej zadaniem jest utrzymanie zaznaczonej pozycji w obrębie ekranu: static void correctTSchedMark(bool up) { int pos2 = tschedMarked; if (pos2 < tschedStart) pos2 += rozklad.lines; if (pos2 > tschedStart+5) { // poza ekranem tschedMarked = up? tschedStart : ((tschedStart+5) % rozklad.lines); } } Teraz reakcja na dotknięcie przycisku zapowiedzi: if (mn >= 20) { if (touchScreenMode == TSM_ANNOUNCES) { touchWaitStatus=TWS_TOUCHED; sayTrain(tschedMarked, mn-20); } return; } i na koniec na wybranie pozycji: if (touchScreenMode == TSM_SCHEDULE) { rozklad.set((tschedStart+mn-1) % rozklad.lines); tschedMarked=rozklad.pos; } else { tschedMarked = (tschedStart+mn-1) % rozklad.lines; } touchWaitStatus=TWS_TOUCHED; drawTSchedPart(); I to już wszystko - jak widać konieczne zmiany były niewielkie. Kod jak zwykle w załączniku: kolejpan10.zip Oczywiście zdaję sobie sprawę z tego że kod nie jest najładniejszy - ale jak może ktoś pamięta, jednym z moich celów było przedstawienie procesu powstawania aplikacji. Tak że "upiększaniem" zajmę się następnym razem. Niestety - na razie muszę przełożyć wyświetlacz i wzmacniacz do innego urządzenia, tak więc dalsza część programu za parę dni. W każdym razie; stay tuned! 2 Cytuj Link do komentarza Share on other sites More sharing options...
prezesedi Grudzień 19, 2023 Autor tematu Udostępnij Grudzień 19, 2023 Wszystko działa 🙂 1 Cytuj Link do komentarza Share on other sites More sharing options...
Popularny post ethanak 3 stycznia Popularny post Udostępnij 3 stycznia Ech... czas powoli kończyć przygodę z kolejkami. Ale jeszcze nie dziś. Czego na razie brakuje w programie? Oczywiście zmiany ustawień semaforów z poziomu ekranu dotykowego. Ale tu chciałem się trochę przyłożyć do wyglądu ekranu. Dlaczego? Ano - dlatego, że trzeba się będzie pozbyć wreszcie całej naleciałości po ekraniku OLED, klawiaturę wykorzystać do obsługi ekranu TFT. A ponieważ tu nie ma konieczności wygaszania ekranu (wyświetlacze TFT nie chorują na wypalanie pikseli) - coś trzeba na tym ekranie wyświetlić. Oczywiście - można pusty ekran z napisem "Dzień dobry" albo jakąś inną krzepiącą sentencją. Tyle, że modelarze raczej nie siedzą w piwnicy zamknięci na cztery spusty nie dopuszczając nikogo, aby przypadkiem nie zobaczył jego ślicznej makiety. Wręcz przeciwnie - makiety są na pokaz, a sterownik jest oczywiście jej częścią integralną. I jakoś nie wyobrażam sobie, aby np. kolega wątkotwórca nie chciał się pochwalić czymś w stylu "zobacz, jaki mam zajefajny panel do sterowania" 🙂 No i w związku z tym jakąś planszę trzeba wyświetlić. Ponieważ te, które widzieliście do tej pory były raczej nudnawe, wybrałem właśnie ekran ustawiania semaforów i postanowiłem nieco przystosować go właśnie na potrzeby demonstracji. Przede wszystkim, ekrany w pomieszczeniach dyspozytorów wyświetlają zawsze jakąś mapę torów. Postanowiłem wyświetlić coś podobnego (co powinno odwzorowywać makietę) i poustawiać na niej semafory. O ile napisanie kodu wyświetlającego coś takiego jest trywialne, o tyle ułożenie takiej mapy i poustawianie semaforów już nie. Trzeba było coś wymyślić, aby umożliwić w miarę sprawne zaprojektowanie takiej mapy przez modelarza. Teoretycznie można było umieścić to w programie - tyle że byłoby to trochę bez sensu; wymagałoby to napisania dość dużego frgmentu kodu, który zostałby użyty tylko raz. Wybrałem więc inną drogę - zrobienie tego za pomocą przeglądarki WWW. I tu nawet nie bawiłem się w umieszczanie tego na ESP; program to pojedynczy plik HTML, można go po prostu wczytać do przeglądarki i wygenerowane dane umieścić w kodzie. Plik z programem zamieściłem oczywiście w załączniku, interfejs wygląda tak: Obsługa jest bardzo prosta. Kliknięcie spowoduje rozpoczęcie rysowania linii, powtórne kliknięcie kończy rysowanie. Jeśli krzyżyk znajdzie się w lewym górnym rogu semafora, zamiast rysowania spowoduje to chwycenie semafora, a powtórne kliknięcie ustawi go w wybranym miejscu. Kliknięcie prawym klawiszem spowoduje przerwanie bieżącej czynności (tzn. rysowania linii lub zmiany położenia semafora), a jeśli żadna czynność nie jest wykonywana - spowoduje usunięcie najbliższej linii (wyświetlanej na czerwono). Wyniki w polu poniżej pola edycji należy skopiować do pliku schemat.h w folderze programu i ponownie skompilować. Można również wkleić zawartość pliku do tego pola i nacisnąć "Wczytaj tablice" - zustanie odtworzony rysunek użyty w programie. I tu drobna uwaga: na razie edytor jest dostępny pod adresem: http://www.polip.com/tmp/peditor.html - ale w każdej chwili może zostać usunięty! Ale przejdę do właściwego programu. Aby wyświetlić ekran obsługi semaforów, trzeba oczywiście to umożliwić. Stąd w TouchLoop dodatkowy fragment: switch(pos) { case 1: drawTSemas(); break; case 2: drawTSched(TSM_SCHEDULE); break; case 3: drawTMsgList(); break; case 4: drawTSched(TSM_ANNOUNCES); break; default: drawTError("NIE ZAIMPLEMENTOWANO"); } Sama funkcja drawTSemas nie jest specjalnie skomplikowana. Na początku trzeba oczywiście słączyć dane wygenerowane przez edytor, czyli: #include "schemat.h" a sam kod wygląda tak: static void drawTSemas() { touchScreenMode = TSM_SEMA; touchLastPos=0; tft.fillRect(0,18,320,240-18,0); int i; for (i=0;i<sizeof(linexy);i+=4) { tft.drawLine( linexy[i] * 8, linexy[i+1] * 8, linexy[i+2] * 8, linexy[i+3] * 8, RGB565(255,255,0)); } for (i=0;i<CNT_SEMA;i++) { drawTSema(i); } } Funkcja drawTSema rysuje pojedynczy semafor: static uint8_t oldSema[16]; // więcej nie będzie #define SEMAPIC_W 30 #define SEMAPIC_H 38 static void drawTSema(int n, bool full=true) { int x=semaxy[2*n] * 8; int y=semaxy[2*n+1] * 8; tft.setTextSize(1,2); if (full) { tft.setTextColor(0xffff,RGB565(0,0,255)); tft.drawRect(x,y,SEMAPIC_W,SEMAPIC_H,0xffff); tft.fillRect(x+1,y+1,SEMAPIC_W-2,SEMAPIC_H-2,RGB565(0,0,255)); tft.setCursor(x+4,y+3); tft.printf("%-4.4s",semafor[n].name()); } oldSema[n]=semafor[n].current(); tft.setCursor(x+10,y+19); tft.setTextSize(1,2); tft.setTextColor((oldSema[n]!=1)?0xffff:RGB565(255,0,0),RGB565(0,0,255)); tft.printf("%-3.3s",semafor[n].pgname(oldSema[n])); } Jak widać, funkcja przyjmuje dwa parametry: numer wyświetlanego semafora i okre ślenie, czy będzie rysowany cały obrazek semafora czy tylko fragment opisujący aktualne ustawienie (jest to potrzebne dla funkcji, która odświeża widok semaforów w przypadku zmiany ustawienia). W tablicy oldSema umieszczone są aktualne ustawienia semaforów potrzebne do działania funkcji odświeżania. Funkcja odświeżania jest również prosta: static void refreshTSema() { int i; if (touchScreenMode != TSM_SEMA) return; for (i=0; i< CNT_SEMA; i++) { if (oldSema[i]!=semafor[i].current()) { oldSema[i]=semafor[i].current(); drawTSema(i, false); } } } Tu chyba nie muszę nic opisywać? Funkcja jest wywoływana okresowo z touchLoop(): void touchLoop() { if (!have_calpos) return; if (inputMode == IMODE_TOUCH) refreshTSema(); Po skompilowaniu i załadowaniu programu okazało się jednak, że czegoś tu brakuje. Co prawda mapa była narysowana, gusrtowne kwadraciki pokazywały stan semaforów, ale przecież tye semafory świecą na kolorowo - warto by było pokazać również te kolorki! Jako że semafor może mieć zapalone maksymalnie dwie lampy, postanowiłem przy każdym semaforze umieścić dwa niewielkie prostokątry obrazujące zapalone w danej chwili kolory. Jako że semafor nie może być narysowany przy sameej krawędzi ekrnu, umieszczenie ich po lewej sgtronie prostokąta wydało mi się natoralnym rozwiązaniem. Dopisałem więc dodatkową tablicę: static uint8_t oldSemaLed[16]; oraz funkcję: static void drawTSemaLed(int n) { static const uint16_t semaColor[]={ RGB565(0,255,0),RGB565(255,200,0), RGB565(255,0,0),RGB565(255,200,0),0xffff}; static const uint16_t shlColor[]={RGB565(255,0,0),0xffff}; int x=semaxy[2*n] * 8; int y=semaxy[2*n+1] * 8; Semafor *s=&semafor[n]; const uint16_t *scolor=s->prog(4) ? semaColor: shlColor; uint8_t nled = s->prog(4) ? 5: 2; uint16_t led[2]={0,0}; int aled=0,i; uint8_t mask=s->get(); oldSemaLed[n]=mask; for (i=0; i < nled && aled < 2; i++) { if (mask & (1<<i)) led[aled++]= (mask & (1<<i)) ? scolor[i] : 0; } for (i=0; i<2;i++) { tft.fillRect(x-4,y+1,3,12,led[0]); tft.fillRect(x-4,y+18,3,12,led[1]); } } Jak widać użyłem tu pewnego tricku: program co prawda przewiduje dwa rodzaje semaforów (pięć i dwie lampy), ale nigdzie nie ma informacji jaki jest typ konkretnego semafora. Ponieważ tarcza (dwulampowa) ma nieobsadzone kilka pozycji programu, wystarzyło więc wybrać którąś. Numer cztery był czysto przypadkowy, ale fragment kodu działa całkiem prawidłowo, ustalając zarówno mapę kolorów lamp jak i ich ilość: // wybór tablicy kolorów const uint16_t *scolor=s->prog(4) ? semaColor: shlColor; // ilość lamp w danym semaforze uint8_t nled = s->prog(4) ? 5: 2; Dalesze modyfikacje były już proste. W funkcji drawTSema jako ostatnią instrukcję wpisałem wywołanie funkcji wyświetlającej kolory lamp: tft.setTextColor((oldSema[n]!=1)?0xffff:RGB565(255,0,0),RGB565(0,0,255)); tft.printf("%-3.3s",semafor[n].pgname(oldSema[n])); drawTSemaLed(n); } Drobnej modyfikacji wymagała również funkcja odświeżania: static void refreshTSema() { int i; if (touchScreenMode != TSM_SEMA) return; for (i=0; i< CNT_SEMA; i++) { if (oldSema[i]!=semafor[i].current()) { oldSema[i]=semafor[i].current(); drawTSema(i, false); } else if (oldSemaLed[i] != semafor[i].get()) { drawTSemaLed(i); } } } Zadowolony z siebie skompilowałem program, wgrałem, uruchomiłem, i... okazało się, że nie wszystko działa jak trzeba. Jeśli migająca lampa miała niższy indeks niż świecąca na stałe, ta stała wyświetlała się raz jako pierwsza, raz jako druga. Trzeba było ustalić, która lampa odpowiada któremu polu dla każdego ustawienia. Na szczęście okazało się to proste. Ponieważ semafor wie, które dwie lampy mają się palić (informacja która miga jest dodatkowa), można było z tego skorzystać: uint8_t mask=s->get(); oldSemaLed[n]=mask; uint8_t lmask=s->getcmd(); for (i=0; i < nled && aled < 2; i++) { if (lmask & (1<<i)) led[aled++]= (mask & (1<<i)) ? scolor[i] : 0; } Wprowadzenie dodatkowej zmiennej nie skompilowało programu, a zaczął się wreszcie zachowywać tak jak na najważniejszy ekran przystało (czytaj: ładnie i szpanersko) 🙂 Oczywiście to tylko część odpowiedzialna za wyświetlanie, trzeba więc zająć się reakcją na dotyk. Funkcja obsługi jest wywoływana podobnie jak pozostałe w touchLoop(), czyli: switch (touchScreenMode) { case TSM_SEMA: touchDoSelSema(); break; case TSM_MESSAGES: touchDoMessages(); break; case TSM_SCHEDULE: case TSM_ANNOUNCES: touchDoSchedule(); break; } Sama funkcja wygląda podobnie jak pozostałe funkcje obsługi: static uint8_t lastTouchSema; static int touchWhichSema() { int i; for (i=0;i < CNT_SEMA; i++) { if (scrx > semaxy[2*i]*8 && scrx < semaxy[2*i]*8+SEMAPIC_W-2 && scry > semaxy[2*i+1]*8 && scry < semaxy[2*i+1]*8+SEMAPIC_H-2) return i+1; } return 0; } static void touchDoSelSema() { int m=touchWhichSema(); if (!m) { touchCtl = 0; lastTouchSema=0; return; } if (m != lastTouchSema) { lastTouchSema = m; touchCtl = 0; return; } if (touchCtl++ < 4) return; touchCtl=0; touchWaitStatus = TWS_TOUCHED; drawTSemaSet(); } Tym razem po wybraniu konkretnego semafora muszę wyświetlić reqauester (hm... jak to się po polsku nazywa?) wyboru ustawienia. Będzie on wyświetlany na tle planszy z semaforami: static uint8_t touchSemaKeyNum; static uint8_t touchSemaKeys[11]; #define SEMABUT_TOP 32 #define SEMABUT_HLINE 32 #define SEMABUT_TOPLINE 26 #define SEMABUT_W 76 #define SEMABUT_L 10 static void drawTSemaSet() { touchSemaKeyNum = 1; int i; Semafor *s=&semafor[lastTouchSema-1]; for (i=1;i<=10;i++) if (s->prog(i)) touchSemaKeyNum++; int lines = (touchSemaKeyNum+3) / 4; int hcon = SEMABUT_TOPLINE+lines * SEMABUT_HLINE +4; tft.fillRect(2,SEMABUT_TOP, 316, hcon, RGB565(0,0,128)); tft.fillRect(2,SEMABUT_TOP,2,hcon,GRAY565(180)); tft.fillRect(316,SEMABUT_TOP,2,hcon,0xffff); tft.drawFastHLine(2,SEMABUT_TOP,315,GRAY565(180)); tft.drawFastHLine(3,SEMABUT_TOP+hcon-1,316-1,0xffff); tft.setCursor(160-6*strlen(s->name()), SEMABUT_TOP+5); tft.setTextColor(0xffff); tft.setTextSize(2,2); tft.print(s->name()); int xb=0, yb=0, nk=0; for (i=1;i<=10;i++) if (s->prog(i)) { touchSemaKeys[nk++] = i; char buf[8]; sprintf(buf,"%d:%s", nk%10, s->pgname(i)); drawHButton(SEMABUT_L+xb* SEMABUT_W, SEMABUT_TOP+SEMABUT_TOPLINE + yb * SEMABUT_HLINE, SEMABUT_W-4, SEMABUT_HLINE-2, buf, s->current() == i); xb += 1; if (xb == 4) { xb = 0; yb += 1; } } touchSemaKeys[nk]=0; drawHButton(SEMABUT_L+xb* SEMABUT_W, SEMABUT_TOP+SEMABUT_TOPLINE + yb * SEMABUT_HLINE, SEMABUT_W-4, SEMABUT_HLINE-2, "#:Anuluj",false); touchScreenMode = TSM_SETSEMA; touchWaitStatus=TWS_TOUCHED; } W tablicy touchSemaKeys umieszczone zostają pozycje programu odpowiadające poszczególnym klawiszom, a ilość wyświetlonych klawiszy w zmiennej touchSemaKeyNum. Sama funkcja też chyba nie wymaga dokładniejszego omawiania, a efekt jej wykonania wygląda tak: Została jeszcze jedna funkcja obsługi owego requestera. Wywoływana jest oczywiście z touchLoop(): switch (touchScreenMode) { case TSM_SEMA: touchDoSelSema(); break; case TSM_SETSEMA: touchDoSetSema(); break; i znów jest na tyle prosta, że chyba nie trzeba jej bliej omawiać: static void touchDoSetSema() { int m; if (scrx <7 || scry < SEMABUT_TOP + SEMABUT_TOPLINE) m=0; else { int x=(scrx-SEMABUT_L) / SEMABUT_W; int xi = (scrx-SEMABUT_L) % SEMABUT_W; int y=(scry-SEMABUT_TOP-SEMABUT_TOPLINE) / SEMABUT_HLINE; int yi=(scry-SEMABUT_TOP-SEMABUT_TOPLINE) % SEMABUT_HLINE; if (xi < 2 || xi >=SEMABUT_W-6 || yi < 2 || yi>=SEMABUT_HLINE-5) m=0; else { m=1+x+4*y; if (m > touchSemaKeyNum) m=0; } } if (!m) { touchCtl=0; lastSemaSetBut=0; return; } if (m != lastSemaSetBut) { touchCtl = 0; lastSemaSetBut=m; return; } if (touchCtl++ < 4) return; int n=touchSemaKeys[m-1]; if (n) semafor[lastTouchSema-1].set(n); touchScreenMode = TSM_SEMA; touchWaitStatus=TWS_TOUCHED; drawTSemas(); } Pozostaje jeszcze jedno wyjaśnienie dla dociekliwych. Mogłem oczywiście stworzyć klasę HButton zawierającą zarówno metody wyświetlania, jak i obsługi. Wbrew pozorom nieco skomplikowałoby to program: funkcje dotyku obsługują nie tulko klawisze, ale również pozycje wyświetlanej listy. Ponieważ klawisze zawsze wyświetlane są w jakiejś siatce, prostsze wydało i się sprawdzenie, w której pozycji siatki znajduje się punkt dotknięcia i przyporządkowanie tej pozycji do klawisza. Drugim (i chyba ważniejszym) powodem była niechęć do robienia zbyt wielu zmian w programie. Wyszedłem z założenia, że dopóki coś działa nie trzeba tego ruszać, bo ilość efektów ubocznych takiego "ruszenia" (i związana z nimi konieczność kolejnych poprawek i zmian) może być tak duża, że zmiany przestają być opłacalne. Coż - jeśli ktoś chce może zaimplementować podobną klasę. Ja wilę skupić się na ostatniej już poważnej zmianie w programie, czyli pozbycia się emulacji OLED-a i podłączenia klawiatury do tego, co pokazuje wyświetlacz TFT. W związku z tym ostatni chyba raz: stay tuned! Oczywiście kod jak zwykle w załączniku: kolejpan11.zip 3 Cytuj Link do komentarza Share on other sites More sharing options...
Polecacz 101 Zarejestruj się lub zaloguj, aby ukryć tę reklamę. Zarejestruj się lub zaloguj, aby ukryć tę reklamę. Produkcja i montaż PCB - wybierz sprawdzone PCBWay! • Darmowe płytki dla studentów i projektów non-profit • Tylko 5$ za 10 prototypów PCB w 24 godziny • Usługa projektowania PCB na zlecenie • Montaż PCB od 30$ + bezpłatna dostawa i szablony • Darmowe narzędzie do podglądu plików Gerber Zobacz również » Film z fabryki PCBWay
prezesedi 3 stycznia Autor tematu Udostępnij 3 stycznia Dobry wieczór. Kod już na pokładzie ESP32. Wgrał się bez problemów i sterowanie semaforów na wyświetlaczu jest idealne. Jutro z rana postaram się "sklecić" jakiś prosty układ torowy i zobaczyć działanie "po swojemu" Cytuj Link do komentarza Share on other sites More sharing options...
Popularny post ethanak 4 stycznia Popularny post Udostępnij 4 stycznia A więc kończmy tę zabawę. Pozostała jedna rzecz do zrobienia - czyli uruchomienie współpracy klawiatury z ekranem TFT. Czyli do roboty! Pierwsze, co musiałem zrobić to usunąć wszelkie odwołania do pozostałości po OLED-zie i czytaniu poleceń z klawiatury. Z pliku display.cpp usunąłem więc wszelkie deklaracje i funkcje odwołujące się do canvas. Jednocześnie w głównym pliku *.ino trzeba było również dokonać niezbędnych cięć - funkcja loop() przedstawia się teraz następująco: void loop() { touchLoop(); // niezależnie od tego czy coś robiliśmy czy nie for (int i=0; i<CNT_SEMA; i++) semafor[i].update(); // no i realizujemy to co nam w semaforach wyszło updateLed(); // esp-now WiFiLoop(); // rozkład jazdy rozkladLoop(); serialLoop(); talkLoop(); } Trochę trzeba było pozmieniać w pliku getcommand.cpp. Jako, że nie są już potrzebne stany klawiatury (zastępuje je stan ekranu TFT) i kilkuklawiszowe polecenia, wystarczy teraz zwykłą funkcja czytająca wciśnięty klawisz. Ponieważ klawisze będą teraz rozpoznawane w innym pliku, część definicji trzeba było przerzucić do głównego pliku nagłówkowego. Tak więc uproszczony plik getcommand.cpp wygląda teraz tak: #include <Arduino.h> #include "Makieta.h" #include "config.h" #include "Keypad_I2C.h" static const char keys[ROWS][COLS] = { {1,2,3,KEY_A}, {4,5,6,KEY_B}, {7,8,9,KEY_C}, {KEY_ASTERISK,10,KEY_HASH,KEY_D} }; static const uint8_t rowPins[ROWS] = ROW_PINS; static const uint8_t colPins[COLS] = COL_PINS; static Keypad_I2C keypad = Keypad_I2C( makeKeymap(keys), (uint8_t *)rowPins, (uint8_t *)colPins, ROWS, COLS, I2C_KBDADR, KBWIDTH ); void initKeyboard() { keypad.begin(); } int getKey() { return keypad.getKey(); } W pliku Makieta.h oprócz usunięcia zbędnych deklaracji trzeba było dopisać deklarację funkcji getKey oraz kilka stałych. Oto dopisany fragment: extern int getKey(); #define KEY_HASH '#' #define KEY_ASTERISK '*' #define KEY_A 11 #define KEY_B 12 #define KEY_C 13 #define KEY_D 14 #define KEY_PGUP 7 #define KEY_UP 8 #define KEY_PGDN 9 #define KEY_DOWN 10 I od razu wyjaśnienie: klawisze 7, 8, 9 i 0 będą służyć do poruszania się po rozkładzie jazdy, stąd takie a nie inne definicje. Ponieważ nie ma już ekraniku startowego wyświetlanego na OLED-zie (czy cvanvas), trzeba było opracować jakiś ekran startowy. Teoretycznie można było od razu wyświetlić ekran stanu semaforów, ale postanowiłem nie rezygnować z jakiegoś miłego powitania. W związku z tym funkcja initDisplay po niezbędnych korektach przybrała taki kształt: static void drawInfoScreen(); 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.cp437(); tft.setTextWrap(0); initTouch(); drawInfoScreen(); } O funkcji drawInfoScreen napiszę później - na razie można zobaczyć, jak program się upraszcza. Po skompilowaniu i załadowaniu programu wszystko działa jak wcześniej, tyle że operowanie klawiaturą nie wywołuje żadnej reakcji. Postanowiłem nie tworzyć kolejnych funkcji wywoływanych a głównej pętli, zamiast tego umieściłem wywołanie funkcji realizującej reakcje na klawiaturę wewnątrz touchLoop(): void touchLoop() { if (keyboardLoop()) return; refreshTSema(); if (!have_calpos) return; if (!getScreenXY(&scrx, &scry)) { if (touchScreenMode != TSM_SEMA && millis() - lastInputActivity > 20000UL) { drawMainMenu(1); drawTSemas(); } touchCtl=0; touchMenu=false; return; } lastInputActivity = millis(); Zmienna lastInputActivity zdeklarowana wyżej jako uint32_t jest ustawiana za każdym dotknięciem ekranu i (jak pokażę za chwilę) za każdym wciśnięciem klawisza. Potrzebna jest po to, aby w przypadku dłuższej nieaktywności program wrócił do wyświetlania ekranu ustawiania semaforów. Sama funkcja obsługi klawiatury jest teraz dużo prostsza niż poprzednia. Jej początek wygląda tak: static uint32_t lastInputActivity; static bool keyboardLoop() { int n; int key=getKey(); if (!key) return false; // nie było klawisza lastInputActivity = millis(); if (key == KEY_ASTERISK && touchScreenMode <= TSM_ERROR) { // wciśnięcie gwiazdki spowoduje wyświetlenie ekranu powitalnego drawInfoScreen(); return true; } // wciśnięcie hasha zatrzyma natychmiast mowę if (key == KEY_HASH) stopSpeech(); Jak widać, doszła nowa funkcjonalność: możliwość zatrzymania wymawianego komunikatu. Realizowane jest to w sposób bardzo prosty. Funkcja w pliku talk.cpp: void stopSpeech() { speakMode=0; // ustawienie trybu "Nic nie mówię" Gadacz::stop(); // i przerwanie generowania mowy lub dźwięku megafonu } Następnym krokiem jest obsługa klawiszy A-D, czyli obsługa menu: if (key >= KEY_A && key <=KEY_D) { drawMainMenu(key-KEY_A+1); switch(key) { case KEY_A: drawTSemas(); break; case KEY_B: drawTSched(TSM_SCHEDULE); break; case KEY_C: drawTMsgList(); break; case KEY_D: drawTSched(TSM_ANNOUNCES); break; } return true; } Jak widać, ograniczam się tu do wywołania uprzednio używanych funkcji. I tu następna uwaga: w tej wersji przy wszystkich wyświetlanych elementach które reagują na wciśnięcie klawisza wyświetlany jest ów klawisz. Przykładowo definicja napisów w menu głównym zmieniła się na taką: static const char * const mainMenu[]={ "A:Semafory","B:Rozk\222ad","C:Komunikaty","D:Zapowiedzi"}; Nie będę tu pokazywać wszystkich tego typu drobnych zmian, zainteresowani mogą zmaleźć je w kodzie. Przykładowo na ekranie ustawiania semaforów każdy ma zaznaczony numerek odpowiadający klawiszowi: Teraz trzeba było zająć się obsługą poszczególnych ekranów. Najprostsza jest obsługa stałych komunikatów: switch(touchScreenMode) { case TSM_MESSAGES: if (key > 9) return false; sayText(komunikaty[key-1].c_str(),true); return true; Tu nie ma co omawiać - naciśnięcie dowolnego klawisza od 1 do 9 spowoduje wypowiedzenie komunikatu o konkretnym numerze. Trochę bardziej skomplikowana jest obsługa semaforów - ale dalej będę używać znanych już z poprzedniej wersji funkcji: case TSM_SEMA: if (key > CNT_SEMA) return false; lastTouchSema = key; drawTSemaSet(); return true; case TSM_SETSEMA: if (key == KEY_HASH) { drawTSemas(); return true; } if (key >= touchSemaKeyNum) return false; semafor[lastTouchSema-1].set(touchSemaKeys[key-1]); drawTSemas(); return true; Obsługa rozkładu jazdy (wyświetlania i zapowiedzi) wymagała jednak napisania nowej funkcji. Wywołuję ją w kolejnej gałęzi case w funkcji keyboardLoop(): case TSM_ANNOUNCES: case TSM_SCHEDULE: keyboardDoSched(key); return true; A oto funkcja we własnej osobie: static void keyboardDoSched(int key) { if (key >= 7 && key <= 10) { switch(key) { case KEY_PGUP: tschedStart=(tschedStart+rozklad.lines-5) % rozklad.lines; break; case KEY_UP: tschedStart=(tschedStart+rozklad.lines-1) % rozklad.lines; break; case KEY_DOWN: tschedStart=(tschedStart+1) % rozklad.lines; break; case KEY_PGDN: tschedStart=(tschedStart+5) % rozklad.lines; break; } if (touchScreenMode == TSM_ANNOUNCES) { correctTSchedMark(key > KEY_UP); } drawTSchedPart(); return; } Powyższy fragment odpowiada za nawigację po rozkładzie jazdy. Jak już wcześniej wspomniałem, wszelkie interaktywne elementy wyświetlają odpowiadający im klawisz, tak więc ekran rozkładu jazdy wygląda teraz tak: Teraz wciśnięcie klawiszy od 1 do 6 wywoła odpowiednią reakcję, zależną od stanu programu. Jeśli wyświetlany jest ekran rozkładu, po prostu wywoływana jest dobrze już znana funkcja ustawiająca określoną pozycję: if (key >= 1 && key <= 6) { if (touchScreenMode == TSM_SCHEDULE) { rozklad.set((tschedStart+key-1) % rozklad.lines); tschedMarked=rozklad.pos; drawTSchedPart(); } W przypadku zapowiedzi natrafiłem jednak na pewną trudność - mianowicie za mało klawiszy. Trzeba więc było posłużyć się dodatkowym requesterem: else { tschedMarked = (tschedStart+key-1) % rozklad.lines; drawTSchedPart(); drawTAnnoSel(); } } } Jak widać, wywoływana jest funkcja drawTAnnoSel, realizująca wyświetlenie tego requestera. Funkcja na szczęście jest prosta: #define ANNOBUT_TOP 32 #define ANNOBUT_HLINE 32 #define ANNOBUT_W 76 #define ANNOBUT_WALL (3 * ANNOBUT_W + 4 * 5) #define ANNOBUT_L (320 - ANNOBUT_WALL) / 2 #define ANNOBUT_HALL (2 * ANNOBUT_HLINE+ 6) void drawTAnnoSel() { static const char *const butts[]={ "1:Odjazd","2:Przyjazd","3:Post\363j",NULL,"#:Anuluj"}; int i; touchScreenMode = TSM_SETANNO; tft.fillRect(ANNOBUT_L,ANNOBUT_TOP, ANNOBUT_WALL,ANNOBUT_HALL, RGB565(0,0,128)); tft.fillRect(ANNOBUT_L,ANNOBUT_TOP,2,ANNOBUT_HALL,GRAY565(180)); tft.fillRect(ANNOBUT_WALL+ANNOBUT_L-2,ANNOBUT_TOP,2,ANNOBUT_HALL,0xffff); tft.drawFastHLine(ANNOBUT_L,ANNOBUT_TOP,ANNOBUT_WALL-1,GRAY565(180)); tft.drawFastHLine(ANNOBUT_L+1,ANNOBUT_TOP+ANNOBUT_HALL-1,ANNOBUT_WALL-1,0xffff); for (i=0; i<5; i++) { if (!butts[i]) continue; drawHButton(ANNOBUT_L + 5 + (i%3) * (ANNOBUT_W+5), (i / 3) * ANNOBUT_HLINE + ANNOBUT_TOP+3, ANNOBUT_W, ANNOBUT_HLINE-3,butts[i], false); } } Po prostu wyświetlany jest gustowny prostokącik i cztery klawisze. Jednocześnie ustawiana jest zmienna touchScreenMode na nową wartość TSM_SETANNO informującą, że program oczekuje na wciśnięcie klawisza. Wygląda to tak: Wrócę więc znowu do funkcji obsługi klawiatury - oto kolejna, ostatnia już gałąź case: case TSM_SETANNO: if (key != KEY_HASH && key > 3) return true; if (key <= 3) sayTrain(tschedMarked, key-1); drawTSched(TSM_ANNOUNCES, tschedStart, tschedMarked); return true; Jak widać, wciśnięcie klawisza 1 do 3 spowoduje uruchomienie odpowiedniej zapowiedzi, a ekran (również w przypadku wciśnięcia hash) wróci do wyświetlania selekcji pociągu. To wszystko? Aha, też tak mi się wydawało - ale trzeba pamiętać, że requester wyboru komunikatu musi być również obsługiwany dotykiem. Tak więc doszła jeszcze jedna gałąź case na końcu funkcji touchLoop(): case TSM_SETANNO: touchDoSetAnno(); break; gdzie funkcja touchDoSetAnno to: static int touchWhichAnnoSel() { int x=scrx - (ANNOBUT_L+5); if (x < 0) return 0; int y=scry - (ANNOBUT_TOP+3); if (y < 0) return 0; int xi=x % (ANNOBUT_W+5); if (xi < 2 || xi > ANNOBUT_W-2) return 0; x = x / (ANNOBUT_W + 5); if (x > 2) return 0; int yi=y % (ANNOBUT_HLINE); if (yi < 2 || yi > ANNOBUT_HLINE-5) return 0; y = y / ANNOBUT_HLINE; int n = x + 3 * y; if (n >4 || n == 3) return 0; return n+1; } static void touchDoSetAnno() { int n=touchWhichAnnoSel(); if (!n) return; touchWaitStatus=TWS_TOUCHED; if (n < 4) { sayTrain(tschedMarked, n-1); } drawTSched(TSM_ANNOUNCES, tschedStart, tschedMarked); } Teraz już wszystko powinno działać jak trzeba, wrócę więc do funkcji wyświetlania ekranu powitalnego: Wykorzystałem tu opisywane kiedyś metody wyświetlania obrazków kodowanych w RLE oraz przygotowywania danych. W folderze gfx znajdują się trzy obrazki PNG: logo Forbota, mój podpis oraz obrazek pociągu. Pociąg znalazłem na całkiem przyzwoitej licencji CC0, swój podpis ułożyłem lata temu w PETSCII, a logo Forbota po prostu ściągnąłem z nagłówka forum. Plik mkrle.py uruchamia się najprościej tak: > python3 ./mkrle.py obrazek.png Stworzony zostanie plik obrazek.h zawierający tablicę obrazek_pic typu uint8_t. Przykładowo początek pliku obrazka pociągu wygląda tak: static const uint8_t train_pic[]={ 0x1,0x40,0x0,0x70,0xfe,0x00,0x1f,0xfe,0x00,0x1f, 0xfe,0x00,0x1f,0xfe,0x00,0x1f,0xfe,0x00,0x1f,0xb6, 0x00,0x1f,0x01,0xce,0x5d,0x0e,0xe7,0x3c,0x01,0xd6, Przypomnę może funkcję rysowania obrazka: static void drawIconRLE(Adafruit_SPITFT *tft,int x, int y, const uint8_t *icon) { int w=*icon++; w=(w<<8) | *icon++; int h=*icon++; h=(h<<8) | *icon++; int cnt; uint16_t color; tft->startWrite(); tft->setAddrWindow(x,y,w,h); while (cnt = *icon++) { color=*icon++; color=(color << 8) | *icon++; while (cnt-- > 0) tft->SPI_WRITE16(color); } tft->endWrite(); } Dane do rysowania powinny być w formacie: 16 bit - Szerokość 16 bit - Wysokość // powtórzone odpowiednią ilość razy 8 bit - ilość pikseli w danym kolorze (>0) 16 bit - kolor RGB565 // marker końca 8 bit - zero Szasnastobitowe wartości w formacie BE (czyli najpierw starszy, później młodszy bajt). A sama funkcja rysowania ekranu powitalnego jest już naprawdę trywialna: #include "gfx/forbotlogo.h" #include "gfx/brainlogo.h" #include "gfx/train.h" #define INFOS_H (240-18) static const char *version="V17-Final"; static void drawInfoScreen() { drawMainMenu(0); touchScreenMode = TSM_INFO; tft.fillRect(0,18,320,INFOS_H/2,RGB565(0,0,255)); tft.fillRect(0,18+INFOS_H/2,320,INFOS_H/2,RGB565(0,255,0)); drawIconRLE(&tft,0,20,train_pic); drawIconRLE(&tft, 10,21+INFOS_H/2,forbotlogo_pic); drawIconRLE(&tft, 320-74,21+INFOS_H/2,brainlogo_pic); tft.setTextSize(3,3); tft.setTextColor(0); tft.setCursor(161-strlen(version) * 9,241-32); tft.print(version); tft.setTextColor(0xffff); tft.setCursor(160-strlen(version) * 9,240-32); tft.print(version); } I tu prośba: gdyby ktoś chciał wykorzystać program który tu powstał - proszę o pozostawienie zarówno logo Forbota, jak i mojego podpisu. Resztę można sobie zmieniać 🙂 I tym optymistycznym akcentem pozwolę sobie zakończyć coś, co można zatytułować "Historia jednego programu". Kod jak zwykle w załączniku:kolejpan12.zip Co prawda nie przewiduję kolejnych wersji, ale w razie gdyby ktoś miał jakieś pytania lub uwagi - zapraszam! 5 Cytuj Link do komentarza Share on other sites More sharing options...
prezesedi 4 stycznia Autor tematu Udostępnij 4 stycznia Dobry Wieczór. Kod ściągnięty i przetestowany. MISTRZOSTWO ŚWIATA. @ethanak twoje umiejętności i pomysły są niewyobrażalne. To jest niesamowite co stworzyłeś. Cytuj Link do komentarza Share on other sites More sharing options...
ethanak 5 stycznia Udostępnij 5 stycznia No tak - miał być koniec, ale gdzieś mi uciekł... Przede wszystkim po chwili zabawy stwierdziłem, że przy wygłaszaniu komunikatu dobrze by było, aby właściwa pozycja była podświetlona. Tu akurat sprawa okazała się prosta. Drobna zmiana w funkcji static void drawTMsgList(int selected=0) { touchScreenMode = TSM_MESSAGES; touchLastPos=0; if (!selected) tft.fillRect(0,18,320,240-18,0); Jak widać funkcja dostała parametr określający, który komunikat ma być podświetlony (domyślnie żaden). Jeśli komunikat ma być podświetlony nie trzeba wyczerniać fragmentu ekranu z listą komunikatów; takowa jest już wyświetlona, a wyczzernienie spowodowałoby brzydkie mignięcie, Dalej w pętli wyświetlania: tft.fillRect(0,20+24*i,320,22, (selected == i+1) ? RGB565(0,0,255) : GRAY565(200)); tft.setTextColor((selected == i+1) ? RGB565(255,255,0):0); Tło podświetlonego komunikatu będzie niebieskie, napis żółty. Jednocześnie wszędzie tam, gdzie wywoływana jest funkcja komunikatu, dodałem wywołanie drawTMsgList: if (key <= 9) { drawTMsgList(key); sayText(komunikaty[key-1].c_str(),true); } I tu mógłbym skończyć, ale popatrzyłem dokładniej na kod i stwierdziłem, że to koniecznie trzeba poprawić. Wyszukiwanie dotkniętego prostokąta na zasadzie siatki jest dobre, jeśli mamy ich niewiele i są w stałych miejscach ekranu (np. menu). Z drugiej strony tworzenie jakiejś klasy "touchButton" trochę mijałoby się z celem - trzeba by było tworzyć jakieś dynamiczne listy obiektów, co raczej nie pomogłoby w czytelności programu. Wybrałem więc inną drogę. Przede wszystkim stwierdziłem, że istnienie dwóch oddzielnych procedur obsługi dotyku i klawiatury to o jedna za dużo. W sumie reakcja na klawiaturę z wyjątkiem jednego przypadku (zapowiedź pociągu) jest identyczna... postanowiłem więc zrezygnować z owego dualizmu. Wyniknął z tego prosty wniosek: ponieważ każdy element interaktywny na ekranie ma przyporządkowany konkretny klawisz, warto stworzyć jakąś strukturę, która zawiera granice przycisku i kod przyporządkowanego klawisza. Po namyśle stworzyłem coś takiego: struct touchArea { int16_t tminx, tmaxx,tminy,tmaxy; const char *text; uint8_t key; }; static struct touchArea touchArea[16]; //więcej klwaiwzy nie ma static int buttonCount; // ilość aktualnie wyświetlanych pól Dodawanie pola do tablicy realizuje prosta funkcja: static void addTouchButton(int x, int y, int w, int h, uint8_t key,const char *text, bool selected) { touchArea[buttonCount].tminx=x?x+2:0; touchArea[buttonCount].tminy=y?y+2:0; touchArea[buttonCount].tmaxx=(x+w >=320)?0:(x+w-2); touchArea[buttonCount].tmaxy=(y+h >=240)?0:(y+h-2); touchArea[buttonCount++].key=key; if (text) { drawHButton(x,y,w,h,text,selected); } } Jak widać, pole dotyku jest dodatkowo zmniejszone (ekrany rezystancyjne nie grzeszą dokładnością). Dodatkowo można dodać albo narysowany klawisz, albo tylko dodać pole (gdy funkcja korzysta z własnych procedur rysowania). W tej sytuacji stwierdzenie, które pole zostało dotknięte realizuje prosta funkcja: static int touchedButton() { int i; for (i=0;i<buttonCount;i++) { if ((!touchArea[i].tminx || scrx >= touchArea[i].tminx) && (!touchArea[i].tmaxx || scrx < touchArea[i].tmaxx) && (!touchArea[i].tminy || scry >= touchArea[i].tminy) && (!touchArea[i].tmaxy || scry < touchArea[i].tmaxy)) { return touchArea[i].key; } } return 0; } Oczywiście ta funkcja nie może być bezpośrednio zastosowana do detekcji pola - pierwsze dotknięcie może błędnie podać pozycję, Dlatego właściwa funkcja zwraca kod klawisza dopiero wtedy, gdy kilka razy pod rząd otrzyma ten sam kod: static int lastTouchButton=0; static int getTouchedButton() { if (!getScreenXY(&scrx, &scry)) { if (touchScreenMode != TSM_SEMA && millis() - lastInputActivity > 20000UL) { drawMainMenu(1); drawTSemas(); } touchCtl=0; lastTouchButton=0; return 0; } lastInputActivity=millis(); int m=touchedButton(); if (!m) { touchCtl=0; lastTouchButton=0; return 0; } if (m != lastTouchButton) { lastTouchButton=m; touchCtl=0; } else { if (touchCtl++ >= 4) { touchWaitStatus = TWS_TOUCHED; touchCtl=0; return m; } } return 0; } Podobnie można uprościć kod dla klawiatury. Funkcja musi zwrócić kod klawisza o ile nie jest to KEY_ASTERISK (obsługiwany oddzielnie), a przy okazji zatrzymać komunikat: static int getTouchKey() { int key=getKey(); if (!key) return 0; lastInputActivity = millis(); if (key == KEY_ASTERISK) { if (touchScreenMode <= TSM_ERROR) drawInfoScreen(); if (touchScreenMode <= TSM_ERROR) drawInfoScreen(); return 0; } if (key == KEY_HASH) stopSpeech(); return key; } Teraz mogłem już uprościć do maksimum funkcję touchLoop(): void touchLoop() { int m=getTouchKey(); if (!m && have_calpos) m=getTouchedButton(); if (m) realizeKey(m); refreshTSema(); } a całą obsługę zawrzeć w jednej stosunkowo prostej funkcji realizeKey: void realizeKey(int key) { if (key >= KEY_A && key <=KEY_D) { drawMainMenu(key-KEY_A+1); switch(key) { case KEY_A: drawTSemas(); break; case KEY_B: drawTSched(TSM_SCHEDULE); break; case KEY_C: drawTMsgList(); break; case KEY_D: drawTSched(TSM_ANNOUNCES); break; } return; } switch(touchScreenMode) { case TSM_SEMA: if (key <= CNT_SEMA) { lastTouchSema = key; drawTSemaSet(); } break; case TSM_SETSEMA: if (key == KEY_HASH) { drawTSemas(); break; } printf("KEY %d, SK=%d, NUM=%d\n", key, touchSemaKeys[key-1], touchSemaKeyNum); if (key >= touchSemaKeyNum) break; semafor[lastTouchSema-1].set(touchSemaKeys[key-1]); drawTSemas(); break; case TSM_MESSAGES: if (key <= 9) { drawTMsgList(key); sayText(komunikaty[key-1].c_str(),true); } break; case TSM_ANNOUNCES: case TSM_SCHEDULE: keyboardDoSched(key); break; case TSM_SETANNO: if (key == KEY_HASH || key <= 3) { if (key != KEY_HASH) sayTrain(tschedMarked, key-1); drawTSched(TSM_ANNOUNCES, tschedStart, tschedMarked); } break; } } Funkcja jest praktycznie taka sama jak stosowane w poprzedniej wersji, tak więc pozwolę sobie nic na jej temat nie mówić. Oczywiście oprócz tego wszystkiego trzeba było dopisać wywołania addTouchButton wszędzie gdzie są rysowane jakieś elementy interaktywne. Nie będę może pokazywać wszystkich, a jedynie parę przykładów. Funkcja rysująca menu: static void drawMainMenu(int pos=0) { tft.fillRect(0,0,320,18,0); buttonCount=0; // reset indeksu tblicy touchArea int i; for (i=0;i<4;i++) { addTouchButton(80*i+1,0,78,18,KEY_A+i,mainMenu[i],pos == i+1); } } fragment funkcji rysującej klawisze nawigacji po rozkładzie: static void drawUDKey(int y, int nr) { static const uint8_t keys[]={KEY_PGUP, KEY_UP, KEY_DOWN, KEY_PGDN}; addTouchButton(292,y,28,33,keys[nr],NULL,false); Fragment funkcji wyświetlania pozycji komunikatu: static void drawTMsgList(int selected=0) { buttonCount=4; // reset do klawiszy menu ... for (i=0; i<9;i++) { // "niewidzialny" przycisk wielkości wyświetlanej pozycji addTouchButton(0,20+24*i,320,22,i+1, NULL, false); tft.fillRect(0,20+24*i,320,22, (selected == i+1) ? RGB565(0,0,255) : GRAY565(200)); Po skompilowaniu i załadowaniu programu wszystko pięknie ruszyło. Zadowolony z siebie postanowiłem zrobić sobie kawusię i usiąść do opisu zmian, ale nagle odezwał się kolega wątkotwórca: otóż okazało się, że możliwych ustawień tarczy jest nie trzy a pięć, w dodatku różne tarcze mają różne kolory. Na szczęście dodatkowe ustawienia nie wymagały praktycznie żadnych zmian w programie - wcześniej jakby coś mnie tknęło i poprawiłem w funkcji wyświetlania LED prog(4) na prog(9). Tak, że wystarczyło dodać dodatkowe linijki do tablicy tarczaTable: const SemaProg tarczaTable[16]={ {0}, {SEMLED(TAR_RED,0,0),"Ms1"}, {SEMLED(TAR_WHI,0,0),"Ms2"}, {SEMLED(TAR_RED,0,TAR_RED), "Ms3"}, // nowy {SEMLED(0,TAR_WHI,TAR_WHI), "Ms4"}, // nowy {0},{0},{0},{0},{0}, {SEMLED(TAR_RED,TAR_WHI,TAR_WHI),"Sz"} }; Dodanie informacji o kolorach wymagało jednak więcej zmian. Przede wszystkim w klasie Semafor należało uwzględnić dodatkową zmienną, przechowującą kolory danej tarczy: private: uint16_t shColor[2]; W związku z tym w konstruktorze klasy doszły dwa parametry: // fragment klasy public: Semafor(const SemaProg *program, uint8_t whereto, const char *name, uint16_t shColor1=0, uint16_t shColor2=0); // fragment konstruktora Semafor::Semafor(const SemaProg *program, uint8_t whereto, const char *name, uint16_t shColor1, uint16_t shColor2) { shColor[0]=shColor1; shColor[1]=shColor2; Trzeba było jeszcze przewidzieć metodę klasy udostępniającą owe kolory: // fragment klasy Semafor const uint16_t *shColors() {return shColor;}; Przy okazji stwierdziłem, że odwzorowanie kolorów na ekranie TFT jest takie raczej średnie, postanowiłem więc zdefiniować podstawowe kolory nieco inaczej niż typowe RGB. Dlatego makra RGB565 i GRAY565 przeszły do Makieta.h, a przy okazji wstawiłem tam definicje kolorów: #define RGB565(r,g,b) ((((r) & 0b11111000) << 8) | (((g) & 0b11111100) << 3) | ((b) >> 3)) #define GRAY565(n) RGB565(n,n,n) // kolory LED na ekranie ustawień semaforów #define CLED_WHITE 0xffff #define CLED_GREEN RGB565(0,200,0) #define CLED_RED RGB565(255,0,0) #define CLED_BLUE RGB565(100,100,255) #define CLED_ORA RGB565(255,200,0) Teraz już mogłem przygotować tablice inicjalizacji semaforów - oto jej fragment: Semafor semafor[]={ ... // tarcza pierwsza kolory domyślne Semafor(tarczaTable,WHERETO(0,5),"Tz"), // tarcza druga niebieski-biały Semafor(tarczaTable,WHERETO(1,5),"Tm",CLED_BLUE,CLED_WHITE), // tarcza trzecia zielony - pomarańczowy Semafor(tarczaTable,WHERETO(2,5),"To",CLED_GREEN,CLED_ORA), // tarcza czwarta czerwony - zielony Semafor(tarczaTable,EXTRSEM(0,1),"SBL",CLED_RED, CLED_GREEN) }; Pozostało jeszcze zmienić wyświetlanie kolorów LED na ekranie ustawień semaforów. Oto cała funkcja po zmianach: #define SEMAPIC_W 30 #define SEMAPIC_H 38 static void drawTSemaLed(int n) { static const uint16_t semaColor[]={ CLED_GREEN,CLED_ORA, CLED_RED,CLED_ORA,CLED_WHITE}; static const uint16_t shlColor[]={CLED_RED,CLED_WHITE}; int x=semaxy[2*n] * 8; int y=semaxy[2*n+1] * 8; Semafor *s=&semafor[n]; const uint16_t *scolor; uint8_t nled; if (s->prog(9)) { // dla semafora scolor=semaColor; nled=5; } else { // dla tarczy scolor = s->shColors(); // domyślny dla zapominalskich if (!scolor[0]) scolor=shlColor; nled=2; } uint16_t led[2]={0,0}; int aled=0,i; uint8_t mask=s->get(); oldSemaLed[n]=mask; uint8_t lmask=s->getcmd(); if (s->prog(9)) { // jeśli to semafor, trzeba wybrać diody do wyświetlenia for (i=0; i < nled && aled < 2; i++) { if (lmask & (1<<i)) led[aled++]= (mask & (1<<i)) ? scolor[i] : 0; } } else { // jeśli to tarcza, trzeba wyświewtlić obie diody na ich miejscach for (i=0; i < nled; i++) { if (lmask & (1<<i)) led[i]= (mask & (1<<i)) ? scolor[i] : 0; } } for (i=0; i<2;i++) { tft.fillRect(x-4,y+1,3,12,led[0]); tft.fillRect(x-4,y+18,3,12,led[1]); } } Teraz po skompilowaniu działa wszystko jak trzeba. I tu uwaga do kolegi wątkotwórcy: nie jestem pewien czy wszystko jest zgodne z tym czego oczekujesz. Jeśli istnieją różne typy tarcz z różnymi ustawieniami (np. jeden typ ma tylko trzy ustawienia, drugi pięć a trzeci trzy ale inne) to program bez żadnych zmian jest w stanie to obsłużyć, tylko muszę dokładnie wiedzieć jak to ma działać. Czekam na informacje, a kod jak zwykle w załączniku: kolejpan14.zip Cytuj Link do komentarza Share on other sites More sharing options...
ethanak 6 stycznia Udostępnij 6 stycznia Coś ten koniec jakoś dalej ucieka 🙂 Dziś nie będzie nowej wersji - za to pokażę, jak można zaimplementować w programie kilka różnych typów tarcz. Do tej pory program operował tylko dwoma typami sygnalizatorów: semafor (pięć lamp, ustalone kolory) i tarcza (dwie lampy, ustalone kolory). Okazało się, że to za mało: istnieje kilka różnych typów tarcz z różnymi zestawami kolorów, przy czym mało że kombinacje owych kolorów są różne dla każdego typu, to jeszcze inaczej się nazywają. Czyżby kolejna zmiana w programie? Ma szczęście nie! Klasa Semafor jako jeden z parametrów konstruktora przyjmuje tablicę kombinacji zapalonych lamp i nazwy owych kombinacji. Zamiast używać jednej wspólnej tabelki, można po prostu użyć kilku różnych! Przede wszystkim jednak aby uniknąć niejednoznaczności, zamiast kolorów zdefiniuję po prostu wartości górna-dolna lampa dla tarczy: #define TAR_UP 1 // górna lampa #define TAR_LO 2 // dolna lampa Teraz muszę zdefiniować tablice dla wszystkich rodzajów tarcz (mam informację od kol. @prezesedi że jest ich cztery): // Tarcza ostrzegawcza const SemaProg tarczaTable_TO[16] = { {0}, {SEMLED(0,TAR_LO,0),"Os1"}, {SEMLED(TAR_UP,0,0),"Os2"}, {SEMLED(TAR_UP,0,TAR_UP), "Os3"}, {SEMLED(0,TAR_LO,TAR_LO), "Os4"}, }; // Tarcza manewrowa const SemaProg tarczaTable_TM[16] = { {0}, {SEMLED(TAR_UP,0,0),"Os1"}, {SEMLED(TAR_LO,0,0),"Os2"}, }; // Tarcza zaporowa const SemaProg tarczaTable_TZ[16]={ {0}, {SEMLED(TAR_UP,0,0),"Ms1"}, {SEMLED(TAR_LO,0,0),"Ms2"}, {SEMLED(TAR_UP,TAR_LO,TAR_LO),"Sz"} 7}; // Samoczynna blokada const SemaProg tarczaTable_SBL[16]={ {0}, {SEMLED(TAR_UP,0,0),"S1"}, {SEMLED(TAR_LO,0,0),"S2"}, }; // a tego nie będę używać #if 0 const SemaProg tarczaTable[16]={ {0}, {SEMLED(TAR_UP,0,0),"Ms1"}, {SEMLED(TAR_LO,0,0),"Ms2"}, {SEMLED(TAR_UP,0,TAR_UP), "Ms3"}, {SEMLED(0,TAR_LO,TAR_LO), "Ms4"}, {0},{0},{0},{0},{0}, {SEMLED(TAR_UP,TAR_LO,TAR_LO),"Sz"} }; #endif Pozostaje jeszcze przyporządkowanie kolorów wyświetlanych na ekranie: Semafor semafor[]={ // tu przykładowe dane semaforów Semafor(semaTable,WHERETO(0,0),"Sem1"), // semafor pierwszy piny P0..P4 Semafor(semaTable,WHERETO(1,0),"Sem2"), // semafor drugi piny P10..P14 Semafor(semaTable,WHERETO(2,0),"Sem3"), // semafor trzeci piny P0..P4 Semafor(semaTable,EXTRSEM(0,0),"Sem4"), // semafor czwarty MAC 0x20 indeks 0 // tarcza zaporowa, kolory czerwony-biały (domyślne) Semafor(tarczaTable_TZ,WHERETO(0,5),"Tz"), // tarcza manewrowa, kolory niebieski-biały Semafor(tarczaTable_TM,WHERETO(1,5),"Tm",CLED_BLUE,CLED_WHITE), // tarcza ostrzegawcza, kolory zielony - pomarańczowy Semafor(tarczaTable_TO,WHERETO(2,5),"To",CLED_GREEN,CLED_ORA), // samoczynna blokada, kolory czerwony - zielony Semafor(tarczaTable_SBL,EXTRSEM(0,1),"SBL",CLED_RED, CLED_GREEN) }; Jest to oczywiście przykład - zastosowałem tu cztery różne tarcze, ale oczywistym jest, że można zastosować różne kombinacje. W podobny sposób można postąpić z semaforami, tam jednak nie ma możliwości zmiany kolorów wyświetlanych na ekranie (co nie powinno stanowić większej trudności, jako że cztero- i trzylampowe semafory nie wprowadzają nowych kolorów). Pamiętać jednak należy o ograniczeniach programu (projektowanego przecież dla konkretnej makiety): Maksymalna ilość podłączonych bezpośrednio sygnalizatorów (4 semafory i 4 tarcze) może być zwiększona, jednak trzeba do tego zmodyfikować program (większa ilość ekspanderów PCF8575); Maksymalna ilość połączonych urządzeń poprzez esp-now jest również ograniczona. Standardowo jest to 7 (chociaż nie jestem pewien, czy Arduinowy kompilat esp-idf nie dopuszcza maksymalnej sprzętowej ilości, czyli 10). Ponieważ odbiornik na ESP8266 może obsługiwać semafor i tarczę lub trzy tarcze, nie powinno stanowić to specjalnego ograniczenia. Istniejący sposób wyboru semafora do ustawienia pozwala na wybranie jednego z dziesięciu za pomocą klawiatury. Zwiększenie ilości semaforów może wymagać zwiększenia rozmiaru tablicy touchArea powyżej 16 (nie mniej niż 4 + ilość semaforów). Mam nadzieję że to wszystko... ale jak na początku stwierdziłem, ten koniec cały czas gdzieś ucieka 🙂 W każdym razie zostało jeszcze ok. 400 kB wolnej pamięci flash - a pamięć nieużywana to przecież pamięć zmarnowana... 2 Cytuj Link do komentarza Share on other sites More sharing options...
prezesedi 6 stycznia Autor tematu Udostępnij 6 stycznia Dzień dobry wszystkim. Kod już przetestowałem. Pozwoliłem sobie wprowadzić kosmetyczne poprawki w oznaczeniu wyświetlanych sygnałów. Powinno to wyglądać tak: // Tarcza ostrzegawcza const SemaProg tarczaTable_TO[16] = { {0}, {SEMLED(0,TAR_LO,0),"Os1"}, {SEMLED(TAR_UP,0,0),"Os2"}, {SEMLED(TAR_UP,0,TAR_UP), "Os3"}, {SEMLED(0,TAR_LO,TAR_LO), "Os4"}, }; // Tarcza manewrowa const SemaProg tarczaTable_TM[16] = { {0}, {SEMLED(TAR_UP,0,0),"Ms1"}, {SEMLED(TAR_LO,0,0),"Ms2"}, }; // Tarcza zaporowa const SemaProg tarczaTable_TZ[16]={ {0}, {SEMLED(TAR_UP,0,0),"S1"}, {SEMLED(TAR_LO,0,0),"Ms2"}, {SEMLED(TAR_UP,TAR_LO,TAR_LO),"Sz"}, }; // Samoczynna blokada const SemaProg tarczaTable_SBL[16]={ {0}, {SEMLED(TAR_UP,0,0),"S1"}, {SEMLED(TAR_LO,0,0),"S2"}, }; // a tego nie będę używać #if 0 const SemaProg tarczaTable[16]={ {0}, {SEMLED(TAR_UP,0,0),"Ms1"}, {SEMLED(TAR_LO,0,0),"Ms2"}, {SEMLED(TAR_UP,0,TAR_UP), "Ms3"}, {SEMLED(0,TAR_LO,TAR_LO), "Ms4"}, {0},{0},{0},{0},{0}, {SEMLED(TAR_UP,TAR_LO,TAR_LO),"Sz"} }; #endif Teraz, to jest to o czym rok temu mogłem jedynie śnić. Że znajdzie się ktoś, kto zechce poświęcić tyle swojego czasu i bawić się czymś, z czego nie będzie korzystać dając jednocześnie inne osobie jakże wielofunkcyjne urządzenie. Jeden powie "zabawka" a inny wykorzysta wszystkie jego możliwości i rozszerzy funkcjonalność tego, co do tej ma. @ethanak z mojej strony ogromne DZIĘKUJĘ. Cytuj Link do komentarza Share on other sites More sharing options...
ethanak 8 stycznia Udostępnij 8 stycznia Ech ten koniec... zamiast się zbliżać to ucieka 🙂 Przy okazji zabawy z wersjami definicji płytek natknąłem się na niemiłą rzecz: bez ekranu dotykowego nie chciały znikać komunikaty błędu i ekran powitalny. Po prostu w ferworze przenoszenia fragmentów kodu w inne miejsca jakoś te miejsca się niespecjalnie zechciały zgodzić z tym co zaplanowałem 🙂 Na szczęście poprawka jest prosta - dotyczy wyłącznie pliku display.cpp należy w funkcji getTouchedButton usunąć lub zakomentować kilka linii (poniżej zakomentowane): static int getTouchedButton() { if (!getScreenXY(&scrx, &scry)) { // if (touchScreenMode != TSM_SEMA && millis() - lastInputActivity > 20000UL) { // drawMainMenu(1); // drawTSemas(); // } touchCtl=0; lastTouchButton=0; return 0; } Analogiczny kawałek kodu należy wstawić do TouchLoop: void touchLoop() { int m=getTouchKey(); if (!m && have_calpos) m=getTouchedButton(); if (!m && touchScreenMode != TSM_SEMA && millis() - lastInputActivity > 20000UL) { drawMainMenu(1); drawTSemas(); } if (m) realizeKey(m); refreshTSema(); } Po tej operacji wszystko powinno ładnie działać„ nawet w sytuacji, kiedy podłączymy wyświetlacz nie obsługujący dotyku. Przy okazji stwierdziłem, że interfejs na smartfona wygląda nieco siermiężnie, postanowiłem go nieco podrasować: W załączniku fragment kodu - wystarczy podmienić pliki webdata.h i webserver.cpp (folder www zawiera nieskompresowane pliki do obejrzenia sobie): kolpanwww1.zip Ciekawe, czy to już koniec 🙂 Cytuj Link do komentarza Share on other sites More sharing options...
etet100 8 stycznia Udostępnij 8 stycznia Proponuję zapoznać się z narzędziami typu git i github. To naprawdę wygodniejsze niż kolej4213_final_ostateczna_z_poprawkami.zip. 1 Cytuj Link do komentarza Share on other sites More sharing options...
Treker (Damian Szymański) 9 stycznia Udostępnij 9 stycznia Cztery posty ukryłem, ponieważ dyskusja odchodziła od głównego tematu. Przypominam również wszystkim o polityce przyjaznego forum. Z góry dziękuję za zrozumienie. Mam nadzieję, że projekt będzie dalej kontynuowany w przyjaznej atmosferze. Nikt nie ma tu (chyba) złych intencji 🚆 2 Cytuj Link do komentarza Share on other sites More sharing options...
Popularny post prezesedi 15 stycznia Autor tematu Popularny post Udostępnij 15 stycznia Cześć, Żeby nie było, że się obijam i że @ethanak napisał kod i koniec, postanowiłem od samego początku opisać przygodę z urządzeniem, które roboczo nazwałem "sterownik urządzeń informacyjnych na makiecie" i opublikować to na forum modelarskim. Można sobie w wolnej chwili poczytać tu: https://forum.modelarstwo.info/threads/arduino-w-modelarstwie-kolejowym-sterownik-urządzeń-informacyjnych-na-makiecie.60643/ 3 Cytuj Link do komentarza Share on other sites More sharing options...
Pomocna odpowiedź
Dołącz do dyskusji, napisz odpowiedź!
Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!