Original SSD1306 [code] /* ------------------------------------------------------------------------------- This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ------------------------------------------------------------------------------- ******************************************************** * Arduino program to generate railway station displays * ******************************************************** This program is based the ideas and programs from Tobias, Klaus and Fredddy: - https://stummiforum.de/viewtopic.php?f=21&t=131472 - https://stummiforum.de/viewtopic.php?f=21&t=131472&start=50#p1849272 - https://stummiforum.de/viewtopic.php?f=21&t=131472&start=50#p1895237 List of changes compared to the basic program: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Several OLED displays could be connected to one Arduino - DCC commands could be used to control due displays - More than 100 displays could be stored in the FLASH - Buttons or other external signals could be used to change the display - Flexible adaptable display layout - Uses the new u8g2lib to access the OLED panel - Faster screen update - Saving memory by - using special fonts with only german "Umlauts" - using PROGMEM - optimizing the program - Serial port could be used to send the displays (Compatible to Fredddy's program) Several OLED panels ~~~~~~~~~~~~~~~~~~~~ The program could display different text messages on several OLED panels. For each panel a MOS-Fet (BS170) is used to switch the SDA line. It's tested with 3 panels, but it shouldn't be a problem to connect more than 8 panels. The limiting factor will be the current to drive the SCL line and the number of pins to switch the enable signal for the MOS-Fets (can be bypassed with an additional Demux: 74HCT238 or CD4514). Limitations: Only the active OLED could show a rolling text. DCC commands: ~~~~~~~~~~~~~ The displays could be controlled by DCC accessory commands. There are commands to: - show the previous/next text message on the actual display - change the actual display - show the next text message on a special display - select a special display as actual display - show a special text message The number of available DCC commands depends on the number of OLED panels an the number of text messages. The first used DCC command is defined with the constant FIRST_DCC_ADDR in "Configuration.h" It's also possible to limit the last used DCC with LAST_DCC_ADDR . For more details see below (search for "Used DCC addressees:") More than 100 displays ~~~~~~~~~~~~~~~~~~~~~~ Due to several memory optimizations over 100 displays could be stored in the FLASH of the Arduino (Nano/Uno). => There is no need to add an external SD-card to store the messages. If more displays are needed an Arduino Mega2560 could be used. This module has 256 KB flash instead of 32 KB => Over 2000 displays are possible. Serial commands: ~~~~~~~~~~~~~~~~ The displayed texts could be read out of the FLASH or send to the Arduino via the serial port (Over USB) The program accepts the same serial commands as introduced in the program from Fredddy. Some additional commands have been added and a "short" mode has been added. Beispiel: #LDas ist ein neuer Lauftext# definiert einen neuen Lauftext Die Felder haben folgende Kuerzel: L: Lauftext (Maximal 100 Zeichen) G: Gleisnummer W: Wagenstand 1: Zuglauf 1 2: Zuglauf 2 Z: Ziel U: Abfahrtszeit N: Zugnummer X: Loesche Zug Daten (zugnummer, uhrzeit, ziel, zuglauf1, zuglauf2, wagenstand) <: Gleis Links >: Gleis Rechts New commands: T: display text message with the given number: #T7 Also possible: #T+ and #T- a-z: Select OLED display: #b switch to the second OLED display. The next commands will control this panel. Short mode: The short mode uses one single character '|' to separate the entries. In this case the sequence of the fields must be fix. This mode is also used to store the text messages in the FLASH. The short mode could be mixed with the original mode. Test fuer serielle Monitor (Short Mode): 12:53|EC 172|Hamburg - Altona|Berlin Hbf - |Hamburg Hbf|-222211|Verspaetung ca 10 Min Normal Mode: #U12:53##NEC 172##ZHamburg - Altona##1Berlin Hbf - ##2Hamburg Hbf##W-222211##LVerspaetung ca 10 Min# Mixed: #1Zielzeile 1|Zielzeile 2 Mixed Mode mit Gleis: #G17#U12:53|EC 172|Hamburg - Altona|Berlin Hbf - |Hamburg Hbf|-222211|Verspaetung ca 10 Min Aufbau des Displays: 123456789012345678901234567 [ ] ABCDEFG Hardware: ~~~~~~~~~ The OLED Display is connected to the following ESP32 pins : (0.91 Inch 128x32 OLED Display SPI Series SSD1306) 21 (A4) = SDA () = Nano 22 (A5) = SCL If multiple OLED displays are used a FET (BS170) is used to switch the SDA line to the OLED displays See schematics for more details Ansteuerung mehrerer OLEDs ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nur ein OLED kann Lauftext anzeigen - Active_OLED bestimmt das OLED welches mit Tastern, DCC Kommandos oder serieller Schnittstelle angesprochen wird. - Nur das Active_OLED kann einen Lauftext anzeigen dadurch braucht man keinen Speicher fuer die anderen OLEDs. - Beim umschalten von einem auf das naechste OLED wird der Lauftext im alten abgeschaltet - Das aktive OLED wird nur bei einer Aenderung neu gezeichnet oder wenn der Lauftext aktiviert ist. Attention: ~~~~~~~~~~ If the number of used OLED panels is changed during tests sometimes the program doesn't start. This may be caused by a hanging OLED display. In this case the power supply has to be removed to reset the OLEDs. Low memory warning: ~~~~~~~~~~~~~~~~~~~ The warning "Low memory available, stability problems may occur." is generated if less than 512 bytes RAM are available. This is a safety warning because the compiler can't calculate the usage of dynamically allocated memory and the maximal used memory for the stack. German: "Wenig Arbeitsspeicher verfuegbar, es koennen Stabilitaetsprobleme auftreten." Alte Lib: Der Sketch verwendet 20824 Bytes (67%) des Programmspeicherplatzes. Das Maximum sind 30720 Bytes. Globale Variablen verwenden 1500 Bytes (73%) des dynamischen Speichers, 548 Bytes fuer lokale Variablen verbleiben. Das Maximum sind 2048 Bytes. ToDo: ~~~~~ - DCC auf zweiten Prozessor - Displays zu Gruppen zusammen fassen - Evtl. kommen die DCC Befehle zu schnell Man k nnte einen FiFo f r DispNr_Req einf hren. - Mehr als 200 Texte o.k. - Zahlen bei 1 und nicht 0 beginnen o.k. - Display Nummern (Debug, Tastatur, Hilfe) - Text Nummern - Einstellungen fuer Upload in Forum: - _PRINT_DEBUG_MESSAGES 0 - Nur ein OLED - Pruefen wie viele Text Messages moeglich sind (03.02.19: >130) - Deutsche Umlaute ersetzen - Schnellen Grafik Treiber aktivieren - ProcState_Outside_Disp() deaktivieren wenn alles getestet ist - RAM sparen: - 20 Leerzeichen am Anfang beim Lauftexts. Das wuerde auch die Display Updatezeit veringern - Nicht mehr den full frame buffer verwenden - Abfahrtszeit aus Moba Uhr bestimmen - Ausgabe wird schneller wenn Leerzeichen uebersprungen werden das sieht man mit dem Ossi bei der Laufschift. Es koennten vermutlich 6 ms von 60 ms gespart werden - Programm aufraeumen (Verstreute Konstanten, Variablen, ...) - Gleis Nummer Zentriert darstellen - Weitere DCC Befehle - Uhrzeit relativ zur Modelbahnzeit - CAN Bus Notizen: ~~~~~~~~ - Das Display gibt es auch ohne Platine: https://www.aliexpress.com/item/0-91-Inch-128x32-OLED-LCD-White-Display-Module-SPI-Series-SSD1306/32859420903.html?spm=2114.search0104.3.124.9ae71fd8F6M06v&ws_ab_test=searchweb0_0,searchweb201602_3_10320_10065_10068_10547_319_317_10548_10696_453_10084_454_10083_433_10618_431_10304_10307_10820_10821_537_10302_536_10902_10843_10059_10884_10887_100031_10319_321_322_10103,searchweb201603_6,ppcSwitch_0&algo_expid=d3e03323-63e4-4399-984e-81f3e7e31fa0-17&algo_pvid=d3e03323-63e4-4399-984e-81f3e7e31fa0&transAbTest=ae803_3 2W64284020BC 8 - Pin Namen und Abmessungen: https://www.aliexpress.com/item/0-91-0-91inch-White-OLED-Display-Module-OLED-Screen-Board-128x32-SSD1306/32787123531.html?spm=2114.10010108.1000014.4.33661312rWuNvP&gps-id=pcDetailBottomMoreOtherSeller&scm=1007.13338.80878.000000000000000&scm_id=1007.13338.80878.000000000000000&scm-url=1007.13338.80878.000000000000000&pvid=989eede7-35d8-466c-890d-cebdab413e79 - Chip: SSD1306 - Datenblatt (S.63 Anchluss): "..\SSD1306.pdf" - Bei der Original Version muss ein Befehl zum lesen von Texten immer mit einem '#' beginnen und mit einem # beendet werden. #LLauftext# Zeichen ohne einleitenden '#' werden ignoriert. - Bei den Kommandos '<', '>' und 'X' darf aber kein # kommen - Das '#' Zeichen kann nicht im Text verwendet werden Revision History: ~~~~~~~~~~~~~~~~~ 24.01.19: - Started 28.01.19: - Adapted to the new library u8g2lib 29.01.19: - Added font u8g2_font_tpss_tf from the old library 30.01.19: - Solved starnge crash which was caused by missing u8g.setDrawColor(1) call Unfortunately the drawBox() of the old lib function still generates crash. There is no setDrawColor() in the old Lib 08.02.19: Crash was caused by the wrong ProcState accessing Disp[ProcState] - DCC is working, but loosing messages ;-( 01.02.19: - Displays are only updated if necessary - Support for several OLED panels added 02.02.19: - Changed the DCC receive function. Now every signal triggers the action and not only a 0->1 edge. Now lost DCC signals are not a big problem. - Rail number and side is stored in individual arrays. - Commands to switch between the OLED panels by serial port and DCC added. 03.02.19: - Using special german fonts to save memory 05.02.19: - Saving RAM by not using an own buffer in Process_Character() - Found strange error which overwrites Active_OLED !!! Wrong value of ProcState in the following line: memset(ProcChrPtr, 0, PB(&Disp[ProcState].size)); in function Short_Mode_Next_ProcState() caused memory corruption ;-( 06.02.19: - Moving Rail_Cfg[] to PROGMEM to save RAM 07.02.19: - Moving Disp[] to PROGMEM to save RAM 09.02.19: - New commands to change the OLED and the displayed text - Random changing displays if RAND_CHANGE_MINTIME is enabled 22.08.25: - Start converted to ESP32 - Using PlatformIO to compile - Start Rolling Text an several displays (See: "Lauftext auf meheren Displays.txt") 24.08.25: - Debug Keys: TAB: Print variables - The function keys F1-F10 in the PIO serial console could be used like the hardware buttons F1 = Show next Text in OLED 1 F2 = Show next Text in OLED 2 : " " : F8 = Show next Text in OLED 8 F9 = Show next Text in active OLED F10 = Activate next OLED - Corrected problem when reading only the Lauftext: SL "#LDiese Zeile ver ndert nur den Lauftext" Prior the time was deleted if this line was read in because the next entry was cleared when a new field was read in in the short mode. (See USE_MEMSET_TO_CLEAR_ENTRIES) - Added DCC adresses to the help - Rail Nr Groups finished */ /* 09.02.19: Der Sketch verwendet 20782 Bytes (67%) des Programmspeicherplatzes. Das Maximum sind 30720 Bytes. Globale Variablen verwenden 1500 Bytes (73%) des dynamischen Speichers, 548 Bytes fuer lokale Variablen verbleiben. Das Maximum sind 2048 Bytes. Big memory consumers: (Get_MemUsage.bat (must be adapted to the location of Bahnsteiganzeige_V3.ino.elf)) 1156 t u8g2_font_6x13B_t_german 1122 t u8g2_font_tpss_t_german 0952 t u8g2_font_5x8_t_german 0792 t u8g2_font_4x6_t_german ===== 4022 Total = 13 % */ #define OLED_COUNT (sizeof(Rail_Cfg)/sizeof(Rail_Cfg_T)) typedef struct { union { uint8_t OLED_Enable_Pin; uint8_t MUX_Channel; }; char RailNr[4]; uint8_t RailSide; } Rail_Cfg_T; // Forward definition void Activate_OLED(uint8_t Nr); #if USE_WIRE_TIMEOUT // 09.08.25: #include #endif #include // Used for "Serial << ... ". Don't use any additional memory compared to the Serial.print https://github.com/janelia-arduino/Streaming #include "Configuration.h" // This file contains the configuration of the program (Pin numbers, Options, ...) #include "Text_Messages.h" // This file contains the messages for the displays // Set Default value for optional parameters in "Configuration.h" #if !defined(USE_FUNCTIONKEYS) #define USE_FUNCTIONKEYS 1 #endif #if !defined(DEBUG_PRINT_VAR_WITH_TAB) #define DEBUG_PRINT_VAR_WITH_TAB 1 #endif #if !defined(ACTIVATE_HELP) #define ACTIVATE_HELP 1 #endif #if !defined(USE_SERIAL) #define USE_SERIAL 1 // Saves 1068 bytes Flash & 177 bytes RAM if disabled // 07.08.25: #endif // If disabled the serial port is not used. // => It's not possible to send commands and messages via the serial console. Bit this is normally not used // Debug messages are not printed at all // // Old: 07.08.25: // Der Sketch verwendet 22182 Bytes (72%) des Programmspeicherplatzes. Das Maximum sind 30720 Bytes. // Globale Variablen verwenden 1548 Bytes (75%) des dynamischen Speichers, 500 Bytes für lokale Variablen verbleiben. Das Maximum sind 2048 Bytes. // // USE_SERIAL 0: // Der Sketch verwendet 21114 Bytes (68%) des Programmspeicherplatzes. Das Maximum sind 30720 Bytes. // Globale Variablen verwenden 1371 Bytes (66%) des dynamischen Speichers, 677 Bytes für lokale Variablen verbleiben. Das Maximum sind 2048 Bytes. // Saved: 1068 177 #if !defined(USE_WIRE_TIMEOUT) #define USE_WIRE_TIMEOUT 0 // 09.08.25: #endif // Sets the timeout for Wire transmissions in master mode. // // If a timeout is detected the Heartbeat LED flashes fast. // Could be checked be pulling SCL or SDA to ground. // The LED flashes slow while connection to GND is active. // // Leider bringt das nichts. Nach ~2 Stunden bleiben die Displays stehen, die Heartbeat LED blink langsam. // Allerdings haben SCL und SDA high Pegel. Die Anzeigen liefen weiter nachdem ich SDA kurz mit Masse verbunden habe. // Auf zwei der Anzeigen waren im oberen Bereich Ameisen. Das gesammte Display wurde nach unten verschoben (Siehe 2025-08-09_12h08_02.png) // // Note: these timeouts are almost always an indication of an underlying problem, // such as misbehaving devices, noise, insufficient shielding, or other electrical // problems. These timeouts will prevent your sketch from locking up, but not solve // these problems. In such situations there will often (also) be data corruption which // doesn t result in a timeout or other error and remains undetected. So when a timeout // happens, it is likely that some data previously read or written is also corrupted. // Additional measures might be needed to more reliably detect such issues (e.g. checksums // or reading back written values) and recover from them (e.g. full system reset). This // timeout and such additional measures should be seen as a last line of defence, when // possible the underlying cause should be fixed instead. #if !defined(RAIL_NR_GROUPS) #define RAIL_NR_GROUPS 0 #endif #if !defined(RT_SHIFT) #define RT_SHIFT 0 // In the original code the "lauftext" is shifted down => Letters like 'g' are cut of #endif // If this is a wanted "feature" set the the constant to 1 otherwise use 0 // This feature uses 30 additional bytes of flash. It does't matter if the NoFontDescent // variable is removed from the code or not if RT_SHIFT is set to 0. The compiler // is optimizing this #if USE_I2C_MUX #include //Click here to get the library: http://librarymanager/All#SparkFun_I2C_Mux QWIICMUX I2C_Mux[I2C_MUX_CHIPS]; #endif #include #include "4x6_t_german.h" // Special fonts which have only the german "umlauts" instead of all #include "5x8_t_german.h" // extendet characters (>127). Use "4x6_tf.h" if you need all ANSI characters >127 #include "6x13B_t_german.h" #include "tpss_t_german.h" #define FONT_4x6 u8g2_font_4x6_t_german #define FONT_5x8 u8g2_font_5x8_t_german #define FONT_6x13B u8g2_font_6x13B_t_german #define FONT_PS_11X17 u8g2_font_tpss_t_german #include "Dprintf.h" // Is enabled with _PRINT_DEBUG_MESSAGES Memory usage: FLASH:1844 27.01.19: // Attention: If to much debug messages are used the receiving of serial characters // may lose characters because of the limmited size of the input and // output buffer of the serial comunication // It's also a problem if the baud rate is to high. In this case the // received characters can't be processed fast enough. #if defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_ESP32) // #include "bootloader_random.h" // for bootloader_random_enable() #include #include #endif #ifdef LED_HEARTBEAT_PIN #include "LED_HeartBeat.h" LED_Heartbeat_C LED_HeartBeat(LED_HEARTBEAT_PIN); // Initialize the heartbeat LED which is flashing if the program runs. (FLASH usage: 116 Byte 02.02.19: ) #endif #if USE_DCC #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-variable" // Disable the warning: warning: 'EEPROM' defined but not used [-Wunused-variable] #include NmraDcc Dcc; // Instance of the NmraDcc class // #pragma GCC diagnostic pop // Unfortunately I can't turn on the warning again for the following code for some reasons ;-( // #pragma GCC diagnostic warning "-Wunused-variable" // Is also enabling the EEPROM warning #endif // Three possible display drivers. The differ in the used RAM. The first one uses a full frame buffer // => All data for the display fit into the RAM => The content for the display has to be drawn only once // The other drivers use less RAM => The display has to be drawn several times and written partly to the OLED // If several parts are used the OLED update time is longer ;-( // Attention: Problems with the arduino avr core library version 1.8.3 // 03.09.20: // There are some time out functions added to the wire part which generates compiler errors: // error: no 'void TwoWire::setWireTimeout(uint32_t, bool) // error: no 'bool TwoWire::getWireTimeoutFlag() // error: no 'void TwoWire::clearWireTimeoutFlag() // It's working with 1.8.2 /* #if 1 // 0.87" Display (New U8g2 library required 2.27.6) // 03.09.20: // ...1_HW must be used for fast scroll line together with U8G2_R0. // If U8G2_R2 is used any frame buffer size could be used U8G2_SSD1316_128X32_1_HW_I2C u8g(DISPLAY_ROT, U8X8_PIN_NONE); // frame buffer 1 tile row 19.379000ms //U8G2_SSD1316_128X32_2_HW_I2C u8g(DISPLAY_ROT, U8X8_PIN_NONE); // frame buffer 2 tile rows 18.458000ms //U8G2_SSD1316_128X32_F_HW_I2C u8g(DISPLAY_ROT, U8X8_PIN_NONE); // full frame buffer 17.992000ms #else if 1 // 0.87" Display SSD1312 (New U8g2 library required 2.27.6) // 03.09.20: U8G2_SSD1312_128X32_F_HW_I2C u8g(DISPLAY_ROT, U8X8_PIN_NONE); // Fuer 0.87" Display full frame buffer //U8G2_SSD1312_128X32_F_HW_I2C u8g(DISPLAY_ROT, U8X8_PIN_NONE); // Fuer 0.87" Display full frame buffer Rotate by 180 Deg //U8G2_SSD1312_128X32_1_HW_I2C u8g(DISPLAY_ROT, U8X8_PIN_NONE); // Fuer 0.87" Display #else // 0.91" Display // Rotate by 180 Deg: U8G2_R2 (Normal: U8G2_R0) See: https://github.com/olikraus/u8g2/wiki/u8g2setupc U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g(DISPLAY_ROT, U8X8_PIN_NONE); // 55-60 ms update time, full frame buffer: 512 bytes RAM //U8G2_SSD1306_128X32_UNIVISION_2_HW_I2C u8g(DISPLAY_ROT, U8X8_PIN_NONE); // 65-74 ms update time, 2 pages => 2* nextPage() 256 bytes RAM //U8G2_SSD1306_128X32_UNIVISION_1_HW_I2C u8g(DISPLAY_ROT, U8X8_PIN_NONE); // 88-94 ms update time, 4 pages => 4* nextPage() 128 bytes RAM #endif */ // Erster Wrapper erzwingt Makroexpansion #define EXPAND_DISPLAY(chip) MAKE_U8G2_NAME(chip) // Zweiter Makro führt das eigentliche Zusammenkleben aus #define MAKE_U8G2_NAME(chip) U8G2_##chip##_128X32_1_HW_I2C EXPAND_DISPLAY(SSD_DISPLAY) u8g(DISPLAY_ROT, U8X8_PIN_NONE); #include "Push_Button.h" Push_Button Button0(BUTTON0_PIN); Push_Button Button1(BUTTON1_PIN); Push_Button Button2(BUTTON2_PIN); Push_Button Button3(BUTTON3_PIN); Push_Button Button4(BUTTON4_PIN); #ifdef BUTTON5_PIN Push_Button Button5(BUTTON5_PIN); #endif #define GleisSeite_Links 0 // An enum uses 2 byte RAM and this uses more byte in the code also ;-( #define GleisSeite_Rechts 1 // Without enum we save 1 byte RAM and 18 bytes FLASH ! bool gleisSeite = GleisSeite_Links; // defines the position of the rail number. Links = Rail number on the left side //uint8_t offset = 0; // Rolling text //uint8_t subset = 0; typedef struct { uint8_t ColorIndex :1; // 1= Normal, 0 = Invers uint8_t Rightaligned :1; // Text is right alligned if gleisSeite = Rechts uint8_t DynInvLen :1; // Invers box has only the length of the text uint8_t NoFontDescent :1; // Pixel below baseline (in 'g') not shown in invers mode. Text shifted down uint8_t DelMark :1; // Delete this entry with the 'X' command uint8_t DispIfPrevious:1; // Display only if the previous line is also shown } Flags_T; typedef struct { uint8_t xR, xL, y; // x position if gleisSeite is R or L, y position uint8_t PixWidth; // output length in pixels uint8_t RolTextLen;// Visible characters of the rolling text Flags_T Flags; // single bits (see above) const uint8_t *font; // Pointer to the used font char ActivChar; // Char to activate the field when entered by the serial port or const. text uint8_t size; // Length of the Txt } Disp_T; // Numbers of the Disp[] entries #define LAUFTEXT 0 #define GLEIS 1 #define UHRZEIT 2 #define ZUGNUMMER 3 #define ZIEL 4 #define ZUGLAUF1 5 #define ZUGLAUF2 6 #define WAGENSTAND 7 #define ABSCHNITT 8 char lauftext[121]; char gleis[4] ; char uhrzeit[6] ; char zugnummer[8] ; char ziel[17] ; char zuglauf1[21] ; char zuglauf2[21] ; char wagenstand[8]; char abschnitt[8] = "ABCDEFG"; // This default value doesn't need additional RAM #define DISPL_ELEMENTS_CNT (sizeof(Disp)/sizeof(Disp_T)) #define DEFVAR(var) sizeof(var) // Attention: At the moment only one rolling text is possible ! Problem: offset must be individuel for each string // Changed layout ("ziel" is left alligned if "GleisSeite_Rechts" is active. Letters like 'g' are shown with descent pixel) // Attention: if the y-position of the scroll text is changed the fast scrolling has to be adapted (TileRow in Write_Display_to_OLED()) const PROGMEM Disp_T Disp[] = {// xR xL y Pix RolT. Color Right DynInv NoFont Del DispIf font Activ Define variable length // Width Len Index alig. Len Descent Mark Previ. Char { 30, 20, 5, 78, 20, { 0, 0, 0, RT_SHIFT, 0, 0 }, FONT_4x6, 'L', DEFVAR(lauftext ) }, // lauftext Should be the first entry because it overwrites the areas on the left and right side with Black pixels {128, 0, 13, 18, 0, { 1, 1, 0, 0, 0, 0 }, FONT_6x13B, 'G', DEFVAR(gleis ) }, // gleis { 0, 100, 6, 25, 0, { 1, 0, 0, 0, 1, 0 }, FONT_5x8, 'U', DEFVAR(uhrzeit ) }, // uhrzeit { 0, 100, 12, 20, 0, { 1, 0, 0, 0, 1, 0 }, FONT_4x6, 'N', DEFVAR(zugnummer ) }, // zugnummer { 30, 0, 28, 176, 0, { 1, 0, 0, 0, 1, 0 }, FONT_PS_11X17, 'Z', DEFVAR(ziel ) }, // ziel proportional font 11x17 ? https://github.com/olikraus/u8glib/wiki/fontgrouporgdot { 30, 20, 11, 80, 0, { 1, 0, 0, 0, 1, 0 }, FONT_4x6, '1', DEFVAR(zuglauf1 ) }, // zuglauf1 { 30, 20, 17, 80, 0, { 1, 0, 0, 0, 1, 0 }, FONT_4x6, '2', DEFVAR(zuglauf2 ) }, // zuglauf2 { 0, 100, 26, 28, 0, { 0, 0, 1, 0, 1, 0 }, FONT_4x6, 'W', DEFVAR(wagenstand) }, // wagenstand { 0, 100, 20, 28, 0, { 1, 0, 0, 0, 0, 1 }, FONT_4x6, 'A', DEFVAR(abschnitt ) }, // haltepositionen ABCDEFG }; // Disp_Txt[] is used to access the text variables with an array instead of using the variable names char *Disp_Txt[DISPL_ELEMENTS_CNT] = // Attention the sequence must be equal with the definition above ! { // size: 36 byte lauftext, gleis, uhrzeit, zugnummer, ziel, zuglauf1, zuglauf2, wagenstand, abschnitt }; typedef struct { char lauftext[121]; char zuglauf1[21]; char gleis[4]; char uhrzeit[6]; char zugnummer[8]; uint8_t subset; uint8_t offset; uint8_t UpdateDisplay; bool gleisSeite; // Summe: 164 } Disp_Cash_T; Disp_Cash_T Disp_Cash[OLED_COUNT]; //----------------------------------------------- void Copy_ActData_to_Cash(Disp_Cash_T &DispCash) //----------------------------------------------- { strcpy(DispCash.lauftext , lauftext ); strcpy(DispCash.zuglauf1 , zuglauf1 ); strcpy(DispCash.gleis , gleis ); strcpy(DispCash.uhrzeit , uhrzeit ); strcpy(DispCash.zugnummer, zugnummer); DispCash.gleisSeite = gleisSeite; //Dprintf("Copy2Cash: '%s' '%s' '%s' '%s' ", DispCash.gleis, DispCash.uhrzeit, DispCash.zugnummer, DispCash.zuglauf1); Dprintf("'%s'\n", DispCash.lauftext); } //----------------------------------------------- void Copy_Cash_to_ActData(Disp_Cash_T &DispCash) //----------------------------------------------- { strcpy(lauftext , DispCash.lauftext ); strcpy(zuglauf1 , DispCash.zuglauf1 ); strcpy(gleis , DispCash.gleis ); strcpy(uhrzeit , DispCash.uhrzeit ); strcpy(zugnummer, DispCash.zugnummer); gleisSeite = DispCash.gleisSeite; } #if defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_ESP32) #define PB(x) x #define PW(x) x #else #define PB(x) pgm_read_byte_near(&x) #define PW(x) pgm_read_word_near(&x) #endif // Constants for the UpdateDisplay variable: #define DONT_UPD_DISP 0 #define UPD_DISP_ONCE 1 // Update the display once. It has a rolling text UpdateDisplay is set to UPD_DISP_ROLL oterwise its set to DONT_UPD_DISP #define UPD_DISP_ROLL 2 // Display is updated preiodic because a rolling display is active #define UPD_DISP_STOP 3 // Stop the rolling display // Constants for DispNr_Req: #define DNR_PREV (0xFFFF - 4 - (2 * OLED_COUNT)) #define DNR_NEXT (DNR_PREV +1) #define DNR_PREV_OLED (DNR_NEXT +1) #define DNR_NEXT_OLED (DNR_PREV_OLED +1) #define DNR_OLED0_NEXT (DNR_NEXT_OLED +1) #define DNR_SEL_OLED0 (DNR_OLED0_NEXT+OLED_COUNT) #define DNR_NOTHING 0xFFFF volatile uint16_t DispNr_Req = DNR_NOTHING; volatile uint8_t Active_OLED = 0; // Number of the active OLED panel //uint8_t UpdateDisplay = UPD_DISP_ONCE; uint16_t DispNr = 0xFFFF; uint16_t Last_DispNr = 0xFFFF; // If Find_MsgNr() id called with an not existing number this variable contains the last available DispNr uint32_t UpdateTime; // Disply update time in the main loop uint16_t Last_DCC_Addr; #if RAIL_NR_GROUPS > 0 #define CALC_RAIL_NR_GROUPS 1 // Bin mir noch nicht sicher ob man die genaue Anzahl braucht oder ob RAIL_NR_GROUPS nur ein Schalter ist // Evtl. Braucht man die Anzahl f r DCC // Zun chst mal gehe ich davon aus, dass man die Anzahl dynamisch ermitteln kann #if CALC_RAIL_NR_GROUPS uint8_t Rail_Nr_Groups_Cnt; #else #define Rail_Nr_Groups_Cnt RAIL_NR_GROUPS #endif uint8_t Rail_Group_Array[OLED_COUNT]; // Array containing the Group number for each OLED display //-------------------------- void Fill_Rail_Group_Array() //-------------------------- { char OldRailNr[sizeof(Rail_Cfg[0].RailNr)] = ""; uint8_t GrpNr = 255; int8_t Err = 0; for (uint8_t i = 0; i < OLED_COUNT; i++) { if (strcmp(OldRailNr, Rail_Cfg[i].RailNr) != 0) { strcpy(OldRailNr, Rail_Cfg[i].RailNr); GrpNr++; if (GrpNr >= RAIL_NR_GROUPS) Err++; } #if CALC_RAIL_NR_GROUPS Rail_Group_Array[i] = GrpNr; #else Rail_Group_Array[i] = min(GrpNr, (uint8_t)(RAIL_NR_GROUPS-1)); #endif } #if CALC_RAIL_NR_GROUPS Rail_Nr_Groups_Cnt = GrpNr+1; #else if (GrpNr < RAIL_NR_GROUPS-1) Err = GrpNr - (RAIL_NR_GROUPS-1); if (Err) Dprintf("Error: RAIL_NR_GROUPS wrong defined!\n" "Correct to '#define RAIL_NR_GROUPS %i'\n", RAIL_NR_GROUPS+Err); #endif } //------------------------------------- uint8_t Get_Rail_Group(uint8_t OLED_Nr) //------------------------------------- { return Rail_Group_Array[OLED_Nr]; } #endif // RAIL_NR_GROUPS //---------------------------------------------------------------------------------- void Draw_Element(const Disp_T &Disp, Flags_T &F, char *Txt, Disp_Cash_T &Disp_Cash) //---------------------------------------------------------------------------------- // This function draws one element. // It's called in a loop to draw all // elements. { //Dprintf("'%s'\n", Txt); // Debug u8g.setFont(PW(Disp.font)); uint8_t x; if (gleisSeite == GleisSeite_Rechts) { x = PB(Disp.xR); if (F.Rightaligned) x -= u8g.getStrWidth(Txt); } else x = PB(Disp.xL); uint8_t y = PB(Disp.y); if (F.ColorIndex == 0) // Invers => draw a box { u8g.setColorIndex(1); if (*Txt != '\0' && (PB(Disp.RolTextLen) == 0 || Disp_Cash.UpdateDisplay != UPD_DISP_STOP)) // Don't draw box if text is empty or RolTextLen enabled and UPD_DISP_STOP { uint8_t PixWidth; if (F.DynInvLen) PixWidth = u8g.getStrWidth(Txt); else PixWidth = PB(Disp.PixWidth); u8g.drawBox(x, y - u8g.getFontAscent(), PixWidth, u8g.getFontAscent()+u8g.getFontDescent()+2); } #if RT_SHIFT if (F.NoFontDescent) y+= (u8g.getFontDescent()+2); // Was this a wanted "feature" in the original code ? #endif } u8g.setColorIndex(F.ColorIndex); if (PB(Disp.RolTextLen) && *Txt && Disp_Cash.UpdateDisplay != UPD_DISP_STOP) // rolling text used ? { uint8_t Len = strlen(Txt); if (Disp_Cash.offset > Len) Disp_Cash.offset = 0; int remaining = Len - Disp_Cash.offset; if (remaining > PB(Disp.RolTextLen)) remaining = PB(Disp.RolTextLen); if (Disp_Cash.offset < Len) { char Old; uint8_t End = Disp_Cash.offset+remaining; if (End < Len) { Old = Txt[End]; Txt[End] = '\0'; } u8g.drawStr(x - Disp_Cash.subset, y, Txt+Disp_Cash.offset); // Ein Buchstabe ist 4 Pixel breit. subset 0..3 if (End < Len) Txt[End] = Old; } } else u8g.drawStr(x, y, Txt); // Normal text } //-------------------------------------------- void Draw_All_Elements(Disp_Cash_T &Disp_Cash) //-------------------------------------------- { for (uint8_t i = 0; i < DISPL_ELEMENTS_CNT; i++) { Flags_T F = PB(Disp[i].Flags); if (i == 0 || F.DispIfPrevious == 0 || *Disp_Txt[i-1] != '\0') // Some lines are only shown if the previous line is also shown Draw_Element(Disp[i], F, Disp_Txt[i], Disp_Cash); } } //------------------------------- uint8_t Display_has_Roling_Text() //------------------------------- { for (uint8_t i = 0; i < DISPL_ELEMENTS_CNT; i++) if (PB(Disp[i].RolTextLen) && *Disp_Txt[i]) return i+1; return 0; } //----------------------------------------------- void Inc_Rolling_Text_Pos(Disp_Cash_T &Disp_Cash) //----------------------------------------------- { Disp_Cash.subset++; if (Disp_Cash.subset > 3) { Disp_Cash.subset = 0; Disp_Cash.offset++; if (Disp_Cash.offset > strlen(Disp_Cash.lauftext)) Disp_Cash.offset = 0; } } #if !USE_I2C_MUX //------------------------------ void Enable_OLED_Pin(uint8_t Nr) //------------------------------ // Activates the enable pin for one OLED. // The enable pin drives a FET (BS170) which // switches the SDA line to the OLED display. { for (uint8_t i = 0; i < OLED_COUNT; i++) digitalWrite(PB(Rail_Cfg[i].OLED_Enable_Pin), 0); digitalWrite(PB(Rail_Cfg[Nr].OLED_Enable_Pin), 1); } #endif #if USE_I2C_MUX //--------------------------------- void Enable_OLED_MUX_Ch(uint8_t Nr) //--------------------------------- { static uint8_t Old_OLED_MUX = 0; uint8_t MUX_Ch = PB(Rail_Cfg[Nr].MUX_Channel); if (Old_OLED_MUX != MUX_Ch) { I2C_Mux[Old_OLED_MUX/8].disablePort(Old_OLED_MUX%8); Old_OLED_MUX = MUX_Ch; I2C_Mux[MUX_Ch/8].setPort(MUX_Ch%8); } } #endif //---------------------------------- void Enable_OLED_Channel(uint8_t Nr) //---------------------------------- { #if USE_I2C_MUX //Serial << F("Enable_OLED_Channel:") << Nr+1 << endl; Enable_OLED_MUX_Ch(Nr); #else Enable_OLED_Pin(Nr); #endif //Active_OLED = Nr; } //---------------------------------------- void Write_Display_to_OLED(uint8_t OLED_Nr) //---------------------------------------- { Enable_OLED_Channel(OLED_Nr); //Dprintf("UpdateDisplay:%i OLED_Nr:%i\n", DispCash.UpdateDisplay, OLED_Nr); Disp_Cash_T *DispCash = &Disp_Cash[OLED_Nr]; if (DispCash->UpdateDisplay == UPD_DISP_ROLL) { // Update only the rolling text //uint32_t T0 = micros(); // Debug uint8_t TileRow = (DISPLAY_ROT == U8G2_R2) ? 3 : 0; u8g.setBufferCurrTileRow(TileRow); // One TileRow = 8 pixels height u8g.clearBuffer(); Copy_Cash_to_ActData(*DispCash); Draw_All_Elements(*DispCash); //uint32_t T1 = micros(); // Debug u8g.setBufferCurrTileRow(TileRow); // write the buffer to tile on the display u8g.sendBuffer(); //uint32_t T2 = micros(); Dprintf("T1: %fms T2: %fms Total %fms\n", (T1-T0)/1000.0, (T2-T1)/1000.0, (T2-T0)/1000.0); // Debug // T1: 0.774000ms T2: 4.292000ms Total 5.066000ms Inc_Rolling_Text_Pos(*DispCash); // Calculate position for rolling text } else { // Draw the whole screen Copy_ActData_to_Cash(*DispCash); //uint32_t T0 = micros(); // Debug u8g.firstPage(); do { Draw_All_Elements(*DispCash); } while( u8g.nextPage() ); //uint32_t T1 = micros(); Dprintf("T1: %fms\n", (T1-T0)/1000.0); // Debug } //Dprintf("End Active_OLED: %i\n", Active_OLED+1); if (Display_has_Roling_Text()) DispCash->UpdateDisplay = UPD_DISP_ROLL;// UPD_DISP_ONCE; else DispCash->UpdateDisplay = DONT_UPD_DISP; // Don't update the display any more } #define PC_READ_TM_NR 250 // Read the index number of the text message to be displayed #define PC_WAIT_START 251 #define PC_READ_TYP 252 #define PC_WAIT_END 253 #define PC_UNUSED 254 #define PC_ERROR 255 #define PC_UNKNOWN_CHAR PC_READ_TYP // Used only temporary in Process_Character() #define START_CHAR '#' #define END_CHAR '#' #define EXT_END_CHARS "#\r\n" NE // const char[] benoetigt genau so viel Speicher uint8_t ProcState = PC_WAIT_START; char *ProcChrPtr; char ShortBuf[6]; // Max: 65535 //------------------------------------------ void Debug_Print_DestVar_Name(uint8_t VarNr) //------------------------------------------ { switch (VarNr) { case LAUFTEXT : Dprintf("LAUFTEXT" ); break; case GLEIS : Dprintf("GLEIS" ); break; case UHRZEIT : Dprintf("UHRZEIT" ); break; case ZUGNUMMER : Dprintf("ZUGNUMMER" ); break; case ZIEL : Dprintf("ZIEL" ); break; case ZUGLAUF1 : Dprintf("ZUGLAUF1" ); break; case ZUGLAUF2 : Dprintf("ZUGLAUF2" ); break; case WAGENSTAND: Dprintf("WAGENSTAND"); break; case ABSCHNITT : Dprintf("ABSCHNITT" ); break; default: Dprintf("?"); } if (VarNr <= ABSCHNITT) Dprintf("\tOld: '%s'", Disp_Txt[VarNr]); } //--------------------------------- void Debug_Print_State_if_Changed() // Debug //--------------------------------- // Print the states for debugging { #if _PRINT_DEBUG_MESSAGES && 0 // use "&& 0" to disable, "&& 1" to enable the debug display static uint8_t Old_State = PC_UNUSED; if (Old_State != ProcState) { Old_State = ProcState; Dprintf("S: "); switch (ProcState) { case PC_WAIT_START: Dprintf("PC_WAIT_START"); break; case PC_READ_TYP: Dprintf("PC_READ_TYP"); break; case PC_WAIT_END: Dprintf("PC_WAIT_END"); break; case PC_READ_TM_NR: Dprintf("PC_READ_TM_NR"); break; case PC_UNUSED: Dprintf("PC_UNUSED"); case PC_ERROR: Dprintf("PC_ERROR"); default: Dprintf("Read Disp %i = ", ProcState); Debug_Print_DestVar_Name(ProcState); } Dprintf("\n"); } #endif } //-------------------------------------------------------- uint8_t Find_Activation_Character_in_Display_Array(char c) //-------------------------------------------------------- { for (uint8_t i = 0; i < DISPL_ELEMENTS_CNT; i++) { if (PB(Disp[i].ActivChar) == c) return i; } return PC_UNKNOWN_CHAR; } //----------------- void DeleteMarked() //----------------- // Delete the strings marked with an DelMark in the Disp[] array { for (uint8_t i = 0; i < DISPL_ELEMENTS_CNT; i++) { Flags_T F = PB(Disp[i].Flags); if (F.DelMark) *Disp_Txt[i] = '\0'; } } //-------------------------------- uint8_t Check_Special_Char(char c) //-------------------------------- { switch (c) { case '<' : gleisSeite = GleisSeite_Links; break; case '>' : gleisSeite = GleisSeite_Rechts; break; case 'X' : DeleteMarked(); break; // Clear all lines in Disp[] which are marked with the DelMark flag case START_CHAR: return PC_READ_TYP; break; // to allow multible start characters case 'T' : // Read the number of the text message to be displayed ProcChrPtr = ShortBuf; memset(ShortBuf, 0, sizeof(ShortBuf)); return PC_READ_TM_NR; default : #if RAIL_NR_GROUPS if ( c>= 'a' and c < (char)('a'+Rail_Nr_Groups_Cnt)) // a..z switch to an other OLED { Activate_Rail_Nr_Group(c-'a'); return PC_WAIT_START; // return without updating the display } #else if ( c>= 'a' and c < (char)('a'+OLED_COUNT)) // a..z switch to an other OLED { Activate_OLED(c-'a'); return PC_WAIT_START; // return without updating the display } #endif else Dprintf("Unknown command:'%c' 0x%02X\n", c, c); // Debug } Disp_Cash[Active_OLED].UpdateDisplay = UPD_DISP_ONCE; return PC_WAIT_START; } #define PCM_NORMAL 0 #define PCM_SHORT 1 uint8_t PC_Mode = PCM_NORMAL; #define START_DISP_NUMMER UHRZEIT // The first Entry in Text_Messages[] is the time #if 1 && USE_SERIAL // Set to 1 for debugging // 07.08.25: Added: USE_SERIAL //------------------------------------------------ uint8_t ProcState_Outside_Disp(uint8_t CheckPoint) // 294 Byte FLASH //------------------------------------------------ // Debug function to detect errors of the variable ProcState { if (ProcState >= DISPL_ELEMENTS_CNT) { Serial.print(F("ERROR Wrong ProcState:")); Serial.print(ProcState); Serial.print(" CP:"); Serial.println(CheckPoint); return 1; } return 0; } #else #define ProcState_Outside_Disp(Nr) 0 #endif #define USE_MEMSET_TO_CLEAR_ENTRIES 0 // Set to 0 to correct problem when reading only a lauftext // 24.08.25: // When enabled (Old) the Time entry was cleared ;-( #if !USE_MEMSET_TO_CLEAR_ENTRIES char oldFirstCharTime = 0; #endif //------------------------------ void Short_Mode_Next_ProcState() //------------------------------ { //Dprintf("Next_ProcState %i\n", ProcState); // Debug PC_Mode = PCM_SHORT; switch (ProcState) { case PC_WAIT_START: ProcState = START_DISP_NUMMER; break; case WAGENSTAND: ProcState = LAUFTEXT; break; case LAUFTEXT: ProcState = PC_WAIT_START; return; // 06.02.19: Old: break; FEHLER! Hier muss rausgesprungen werden werden sonst wird unten eine Falsche Groesse verwendet Disp[252] !!! default: ProcState++; if (ProcState >= DISPL_ELEMENTS_CNT) // Overflow ? { ProcState = PC_WAIT_START; return ; } } if (ProcState_Outside_Disp(1)) return; // Savety check ProcChrPtr = Disp_Txt[ProcState]; #if USE_MEMSET_TO_CLEAR_ENTRIES ///Dprintf("SM Going to clear "); Debug_Print_DestVar_Name(ProcState); Dprintf("\n"); // Debug memset(ProcChrPtr, 0, PB(Disp[ProcState].size)); #else if (ProcState == UHRZEIT) { // if (*ProcChrPtr) Dprintf("Stored first UHRZEIT character: '%c'\n", *ProcChrPtr); // Debug oldFirstCharTime = *ProcChrPtr; } else oldFirstCharTime = '\0'; *ProcChrPtr = '\0'; #endif } //------------------------------------------ void Change_Disp_from_Str(const char *NrStr) //------------------------------------------ { //Dprintf("Change_Disp_from_Str %s\n", NrStr); // Debug switch (*NrStr) { case '+': DispNr_Req = DNR_NEXT; return; case '-': DispNr_Req = DNR_PREV; return; } int Nr = atoi(NrStr); // sscanf() uses a lot of memory! if atoi() is used we save 1778 bytes ! if (Nr > 0) DispNr_Req = (uint16_t)Nr-1; } //---------------------------- void Process_Character(char c) //---------------------------- { Debug_Print_State_if_Changed(); // Debug switch (ProcState) { case PC_WAIT_START: if (c == START_CHAR) { PC_Mode = PCM_NORMAL; ProcState = PC_READ_TYP;} if (c == START_LINE_CHAR) Short_Mode_Next_ProcState(); // paragraph symbol break; case PC_READ_TYP: ProcState = Find_Activation_Character_in_Display_Array(c); if (ProcState != PC_UNKNOWN_CHAR) { if (ProcState_Outside_Disp(2)) { ProcState = PC_ERROR; return;} // Savety check #if USE_MEMSET_TO_CLEAR_ENTRIES ProcChrPtr = Disp_Txt[ProcState]; //Dprintf("PC Going to clear "); Debug_Print_DestVar_Name(ProcState); Dprintf("\n"); // Debug memset(ProcChrPtr, 0, PB(Disp[ProcState].size)); #else if (ProcChrPtr == Disp_Txt[UHRZEIT] && oldFirstCharTime && ProcState != UHRZEIT) // Repair Time { *ProcChrPtr = oldFirstCharTime; Dprintf("Repaired time field '%s'\n", Disp_Txt[UHRZEIT]); } ProcChrPtr = Disp_Txt[ProcState]; oldFirstCharTime = 0; *ProcChrPtr = '\0'; #endif } else ProcState = Check_Special_Char(c); break; case PC_WAIT_END: if (strchr(EXT_END_CHARS, c)) { if (c == END_CHAR) ProcState = PC_READ_TYP; // The # could also be the next start character else ProcState = PC_WAIT_START; } break; case PC_ERROR: break; default: // Reading the characters into a local temporary variable uint8_t MaxLen; if (ProcState == PC_READ_TM_NR) MaxLen = sizeof(ShortBuf)-1; else { if (ProcState_Outside_Disp(3)) { ProcState = PC_ERROR; return;} // Savety check MaxLen = PB(Disp[ProcState].size) - 1 - PB(Disp[ProcState].RolTextLen); //Dprintf(" MaxLen %i: ", MaxLen); Debug_Print_DestVar_Name(ProcState); Dprintf("\n"); // Debug } uint8_t ActLen = strlen(ProcChrPtr); if (strchr(EXT_END_CHARS, c)) // 25.08.25: Removed: ActLen >= MaxLen || { // End of the text detected //Dprintf("Disp[%i]='%s'\n", ProcState, ProcChrPtr); // Debug if (ProcState == PC_READ_TM_NR) { Change_Disp_from_Str(ShortBuf); ProcState = PC_WAIT_START; } else { if (ProcState_Outside_Disp(4)) { ProcState = PC_ERROR; return;}; // Savety check uint8_t RolTextLen = PB(Disp[ProcState].RolTextLen); if (RolTextLen > 0 && ActLen > 0) // Special treatement for rolling text { memmove(ProcChrPtr+RolTextLen, ProcChrPtr, ActLen+1); memset(ProcChrPtr, ' ', RolTextLen); // add leading space characters //23.08.25: ToDo: offset = subset = 0; // new start of the rolling text } switch (c) { case END_CHAR: ProcState = PC_READ_TYP; break; // The end could be the next start character case NEXT_ENTRY_CHAR: Short_Mode_Next_ProcState(); break; case '\r': case '\n': ProcState = PC_WAIT_START; break; default: Dprintf("End read Char c=%c (%02X)\n", c, c); // Debug ProcState = PC_WAIT_END; } } Disp_Cash[Active_OLED].UpdateDisplay = UPD_DISP_ONCE; } else { if (ActLen < MaxLen) // 25.08.25: { ProcChrPtr[ActLen] = c; #if !USE_MEMSET_TO_CLEAR_ENTRIES ProcChrPtr[ActLen+1] = '\0'; // Add a tailing '\0' to end the string #endif } } //Dprintf("ProcChrPtr = '%s' ActLen %i\n", ProcChrPtr, ActLen); // Debug } Debug_Print_State_if_Changed(); // Debug } //-------------------------------- void Set_Rail_Defaults(uint8_t Nr) //-------------------------------- { strcpy_P(Disp_Txt[GLEIS], Rail_Cfg[Nr].RailNr); gleisSeite = (PB(Rail_Cfg[Nr].RailSide) == 'L') ? GleisSeite_Links : GleisSeite_Rechts; } //------------------------------------- void Set_Default_Gleisseite(uint8_t Nr) //------------------------------------- { gleisSeite = (PB(Rail_Cfg[Nr].RailSide) == 'L') ? GleisSeite_Links : GleisSeite_Rechts; } //-------------------------------------------------------------------- void Activate_Display_from_Flash_Sub(uint8_t OLED_Nr, const char *Msg) //-------------------------------------------------------------------- // Activate the display by a text message located in the flash. // The message contains special characters which define the // destination in Disp[]. { Set_Rail_Defaults(OLED_Nr); // The rail number is defined by the array Rail_Cfg[OLED_Nr].RailNr, but this could be replaced in the text char c = pgm_read_byte_near(Msg); // message with the #G command. RailSide is replaced with #< or #>. do { Process_Character(c); c = pgm_read_byte_near(++Msg); } while (c != START_LINE_CHAR && c != '\0'); // START_LINE_CHAR = paragraph symbol Process_Character(NEXT_ENTRY_CHAR); //Copy_ActData_to_Cash(Disp_Cash[OLED_Nr]); Disp_Cash[OLED_Nr].UpdateDisplay = UPD_DISP_ONCE; Write_Display_to_OLED(OLED_Nr); } //--------------------------------- const char *Find_MsgNr(uint16_t Nr) //--------------------------------- { //Dprintf("Find_MsgNr %i\n", Nr); // Debug to check the search duration //uint32_t Start = micros(); // Debug const char *Msg = Text_Messages; char c = pgm_read_byte_near(Msg); Last_DispNr = 0xFFFF; do { if (c == START_LINE_CHAR) // paragraph symbol { if (Nr-- == 0) return Msg; Last_DispNr++; } c = pgm_read_byte_near(++Msg); } while (c != '\0'); //Dprintf("Search time %fms\n", (micros() - Start)/1000.0); // Searching the entry 150 took 8 ms :-) // Debug return NULL; } //--------------------------------------------------------------------- uint8_t Activate_Display_from_Flash_by_Nr(uint8_t OLED_Nr, uint16_t Nr) //--------------------------------------------------------------------- { //Dprintf("Activate_Display %i\n", Nr+1); // Debug Copy_Cash_to_ActData(Disp_Cash[OLED_Nr]); // Restore the actual data. They have been overwritten by // the last display update in the main loop //uint32_t Start = micros(); // Debug const char *Msg = Find_MsgNr(Nr); //Dprintf("Search time %fms\n", (micros() - Start)/1000.0); // Searching the entry 150 took 8 ms :-) // Debug if (Msg) { Activate_Display_from_Flash_Sub(OLED_Nr, Msg); #if RAIL_NR_GROUPS // Write the same content to all OLEDs in the Group uint8_t ONr = OLED_Nr+1; uint8_t ActGrp = Get_Rail_Group(OLED_Nr); while(Get_Rail_Group(ONr) == ActGrp) { Disp_Cash[ONr].UpdateDisplay = UPD_DISP_ONCE; Set_Default_Gleisseite(ONr); Write_Display_to_OLED(ONr); ONr++; } #endif } return Msg != NULL; } //------------------------------------- void Activate_Next_Display_from_Flash() //------------------------------------- { //uint32_t Start = micros(); // Debug if (!Activate_Display_from_Flash_by_Nr(Active_OLED, ++DispNr)) Activate_Display_from_Flash_by_Nr(Active_OLED, (DispNr = 0)); // Not found => activate display 0 //Dprintf("Activate_Display_from_Flash_by_Nr: %f ms\n", (micros() - Start)/1000.0); // Debug } //------------------------------------- void Activate_Prev_Display_from_Flash() //------------------------------------- { if (!Activate_Display_from_Flash_by_Nr(Active_OLED, --DispNr)) Activate_Display_from_Flash_by_Nr(Active_OLED, (DispNr = Last_DispNr)); // Not found => activate the last display } //---------------------------- void Activate_OLED(uint8_t Nr) //---------------------------- { #if RAIL_NR_GROUPS Dprintf("Activate OLED %i, Rail group %i (%c)\n", Nr+1, Get_Rail_Group(Nr)+1, 'a'+Get_Rail_Group(Nr)); #else Dprintf("Activate OLED %i\n", Nr+1); #endif uint8_t res; if (Nr >= OLED_COUNT) Nr = 0; Active_OLED = Nr; // if ((res = Display_has_Roling_Text())) // { // *Disp_Txt[res-1] = '\0'; // Clear the rolling text because otherwise the next display is overwritten when switched trough the OLEDs // Write_Display_to_OLED(Nr); // Update the display // } //Disp_Cash[Nr].UpdateDisplay = DONT_UPD_DISP; // Don't draw to the new display until we have a new content //Disp_Cash[Nr].UpdateDisplay = UPD_DISP_ONCE; ///Enable_OLED_Channel(Nr); } #if RAIL_NR_GROUPS //----------------------------------------- bool Activate_Rail_Nr_Group(uint8_t GrpNr) //----------------------------------------- { for (uint8_t i = 0; i < OLED_COUNT; i++) { if (Get_Rail_Group(i) == GrpNr) { Activate_OLED(i); return true; } } Dprintf("Error: Rail_Nr_Group %i not found\n", GrpNr+1); return false; } #endif //--------------------------------------------- bool Activate_OLED_or_Rail_Nr_Group(uint8_t Nr) //--------------------------------------------- { #if RAIL_NR_GROUPS return Activate_Rail_Nr_Group(Nr); #else Activate_OLED(Nr); return true; #endif } //------------------------- void Next_OLED_or_RailGrp() //------------------------- { #if RAIL_NR_GROUPS if (Rail_Nr_Groups_Cnt > 1) { uint8_t Act_Grp = Get_Rail_Group(Active_OLED); do { Active_OLED++; if (Active_OLED >= OLED_COUNT) Active_OLED = 0; } while (Act_Grp == Get_Rail_Group(Active_OLED)); } #else Active_OLED++; if (Active_OLED >= OLED_COUNT) Active_OLED = 0; #endif //Dprintf("Next_OLED_or_RailGrp %i\n", Active_OLED+1); Activate_OLED(Active_OLED); } //------------------------- void Prev_OLED_or_RailGrp() //------------------------- { #if RAIL_NR_GROUPS if (Rail_Nr_Groups_Cnt > 1) { uint8_t Act_Grp = Get_Rail_Group(Active_OLED); do { if (Active_OLED == 0) Active_OLED = OLED_COUNT-1; else Active_OLED--; } while (Act_Grp == Get_Rail_Group(Active_OLED)); Act_Grp = Get_Rail_Group(Active_OLED); while (Active_OLED > 0 && Act_Grp == Get_Rail_Group(Active_OLED-1)) Active_OLED--; // First entry in the group } #else if (Active_OLED == 0) Active_OLED = OLED_COUNT-1; else Active_OLED--; #endif //Dprintf("Next_OLED_or_RailGrp %i\n", Active_OLED+1); Activate_OLED(Active_OLED); } //-------------------------------------------------- void Next_Display_for_OLED_Nr_or_RailGrp(uint8_t Nr) //-------------------------------------------------- { if (Activate_OLED_or_Rail_Nr_Group(Nr)) Activate_Next_Display_from_Flash(); } //---------------- void Setup_OLEDs() //---------------- { pinMode(RESET_DISP_PIN, OUTPUT); // 02.10.20: delay(100); digitalWrite(RESET_DISP_PIN, 1); #if !USE_I2C_MUX for (uint8_t i = 0; i < OLED_COUNT; i++) // Configure all pins as output pinMode(PB(Rail_Cfg[i].OLED_Enable_Pin), OUTPUT); #endif uint8_t GrpNr = 255; DispNr = 0; for (uint8_t i = OLED_COUNT-1; i != 255; i--) // Initialize the OLEDs in reverse order to end with the first display { Enable_OLED_Channel(i); u8g.begin(); u8g.setContrast(150); // Helligkeit Anpassen // 23.01.19: Copied from Zugzielanzeiger_71.ino 22.08.25: Moved up if (i == OLED_COUNT-1) { u8g.setFontMode(1); // Fonts in the new library draw also the background pixels => The invers background is visible on the left side } // The display rotation is defined in the constructor in the new version if (i > 0) Disp_Cash[i].UpdateDisplay = UPD_DISP_STOP; // don't draw the rolling text Set_Rail_Defaults(i); #if RAIL_NR_GROUPS if (Get_Rail_Group(i) != GrpNr) { GrpNr = Get_Rail_Group(i); Activate_Display_from_Flash_by_Nr(i, DispNr++); } #else Activate_Display_from_Flash_by_Nr(i, DispNr++); #endif //Dprintf("Setup %i ", i); Write_Display_to_OLED(i); } } //************************************************** DCC ***************************************************** #if USE_DCC // Used DCC adresses: // ~~~~~~~~~~~~~~~~~~ // Address if FIRST_DCC_ADDR = 5 and three OLED panels are used: // ~~~~~~ // First DCC address: (5) Change the displayed text of the active OLED // Red = previous text message // Green = next " // // Second DCC address: (6) Change the active OLED panel (Only if more than one OLEDs are used) // Red = previous OLED panel // Green = next " // // Next n DCC addresses: Show the next text message on a certain OLED panel (Only if more than one OLEDs are used) // n = Number of OLED displays / 2 // (7) Red 1 = show next text message on OLED 1 // (7) Green 1 = " " " 2 // (8) Red 2 = " " " 3 // (-) Green 2 = " " " 4 (Invalid if 3 OLEDs are used => Selects OLED panel 0) // (-) : : : : : // (-) Green n = " " " n/2 // // Next n DCC addresses: Select a certain OLED panel (Only if more than one OLEDs are used) // (9) Red 1 = select OLED panel 1 // (9) Green 1 = " " " 2 // (10) Red 2 = " " " 3 // (-) Green 2 = " " " 4 (Invalid if 3 OLEDs are used => Does nothing) // (-) : : : : : // (-) Green n = " " " n/2 // // Next m DCC addresses: Show a certain text message on the actual OLED panel // (11) Red 1 = show text message 1 // (11) Green 1 = " " " 2 // (12) Red 2 = " " " 3 // (12) Green 2 = " " " 4 // : : : : : : #if RAIL_NR_GROUPS #define CHANGE_OLED_CMD (FIRST_DCC_ADDR + (Rail_Nr_Groups_Cnt > 1 ? 1 : 0)) // = 6 / 5 #define FIRST_SEL_NEXT_TXT (CHANGE_OLED_CMD + 1) // = 7 / 6 #define FIRST_SEL_OLED_CMD (FIRST_SEL_NEXT_TXT + (Rail_Nr_Groups_Cnt > 1 ? (Rail_Nr_Groups_Cnt+1)/2 : 0)) // 7 (3 +1)/2 = 9 / 6 #define FIRST_SEL_TXT_CMD (FIRST_SEL_OLED_CMD + (Rail_Nr_Groups_Cnt > 1 ? (Rail_Nr_Groups_Cnt+1)/2 : 0)) // 9+2 = 11 #else #define CHANGE_OLED_CMD (FIRST_DCC_ADDR + (OLED_COUNT > 1 ? 1 : 0)) // = 6 / 5 #define FIRST_SEL_NEXT_TXT (CHANGE_OLED_CMD + 1) // = 7 / 6 #define FIRST_SEL_OLED_CMD (FIRST_SEL_NEXT_TXT + (OLED_COUNT > 1 ? (OLED_COUNT+1)/2 : 0)) // 7 (3 +1)/2 = 9 / 6 #define FIRST_SEL_TXT_CMD (FIRST_SEL_OLED_CMD + (OLED_COUNT > 1 ? (OLED_COUNT+1)/2 : 0)) // 9+2 = 11 #endif //-------------------------- void Get_Last_DCC_Addr() //-------------------------- { //uint32_t Start = micros(); // Debug Find_MsgNr(0xFFF0); // Set Last_DispNr //Dprintf("Get_Last_DCC_Addr Search time %fms\n", (micros() - Start)/1000.0); // Searching through 238 entrys took 3 ms @ ESP32 // Debug Last_DCC_Addr = FIRST_SEL_TXT_CMD + (Last_DispNr+1)/2; } //------------------- void Print_DCC_Help() //------------------- { #if RAIL_NR_GROUPS #define OLED_OR_RAIL "Rail Nr Group" #define IN_OR_ON " in " #define MaxCnt Rail_Nr_Groups_Cnt #else #define OLED_OR_RAIL "OLED panel" #define IN_OR_ON " on " #define MaxCnt OLED_COUNT #endif Serial << F("\r\n"); Serial << F("DCC Addresses:\n"); Serial << F("==============\n"); Serial << F("Change the displayed text of the active OLED") << endl; Serial << F(" ") << FIRST_DCC_ADDR << F("\t Red: previous text message\n"); Serial << F(" ") << FIRST_DCC_ADDR << F("\t Green: next """"\n"); Serial << endl; Serial << F("Change the active " OLED_OR_RAIL "\n"); Serial << F(" ") << FIRST_DCC_ADDR +1 << F("\t Red: previous " OLED_OR_RAIL "\n"); Serial << F(" ") << FIRST_DCC_ADDR +1 << F("\t Green: next """"\n"); Serial << endl; Serial << F("Show the next text message" IN_OR_ON "a certain " OLED_OR_RAIL "\n"); uint16_t Nr = 1; for (uint16_t i = FIRST_SEL_NEXT_TXT; i < FIRST_SEL_OLED_CMD; i++, Nr+=2) { Serial << F(" ") << i << F("\t Red: show next text message" IN_OR_ON OLED_OR_RAIL " ") << Nr << endl; if (Nr+1 < MaxCnt) Serial << F(" ") << i << F("\t Green: show next text message" IN_OR_ON OLED_OR_RAIL " ") << Nr+1 << endl; } Serial << endl; Serial << F("Select a certain " OLED_OR_RAIL "\n"); Nr = 1; for (uint16_t i = FIRST_SEL_OLED_CMD; i < FIRST_SEL_TXT_CMD; i++, Nr+=2) { Serial << F(" ") << i << F("\t Red: select " OLED_OR_RAIL " ") << Nr << endl; if (Nr+1 < MaxCnt) Serial << F(" ") << i << F("\t Green: select " OLED_OR_RAIL " ") << Nr+1 << endl; } Serial << endl; Serial << F("Show a certain text message" IN_OR_ON "the actual " OLED_OR_RAIL "\n"); Nr = 1; for (uint16_t i = FIRST_SEL_TXT_CMD; i < FIRST_SEL_TXT_CMD+3; i++, Nr+=2) { Serial << F(" ") << i << F("\t Red: show text message ") << Nr << endl; Serial << F(" ") << i << F("\t Green: show text message ") << Nr+1 << endl; } Serial << F(" :\t : :\n"); Serial << F(" ") << Last_DCC_Addr << F("\t ") << (((Last_DispNr % 2) == 0) ? "Green:":"Red: ") << F(" show text message ") << Last_DispNr << endl; Serial << endl; } //------------------------------------------------------------------------------------- void notifyDccAccTurnoutOutput( uint16_t Addr, uint8_t Direction, uint8_t OutputPower ) //------------------------------------------------------------------------------------- // This function is called whenever a normal DCC Turnout Packet is received // // Unfortunately not all DCC commands are received if the I2C is used to update the OLED display. // => We don't allways get the "Button pressed" and "Button released" messages. // As a work arround a timeout is used. // The first message sets the timeout and triggers the action independant from the "Button" state. // The following messages within the timeout are ignored. // This methode is used because it's possible that we miss the "Button pressed" message but // receive the "Button released" signal. In this case we assume that the button must be pressed before. // The OutputPower parameter is ignored. // 6-7 DCC messages from a MS2 are received if a button is pressed and released again. This takes // about 300 ms (Depending on the time the button is pressed). Therefor the timeout is set to 500 ms. { const int DCC_BUTTON_TIMEOUT = 500; static uint32_t Timeout = 0; if (Addr >= FIRST_DCC_ADDR && Addr <= Last_DCC_Addr) { if (millis() > Timeout) { if (Direction>0) Direction = 1; // In case some controller sends an other value Timeout = millis() + DCC_BUTTON_TIMEOUT; // Commands to change the active OLED panel if (Addr == FIRST_DCC_ADDR) DispNr_Req=DNR_PREV +Direction; // Change the displayed text of the active OLED (-/+) else if (Addr == CHANGE_OLED_CMD) DispNr_Req=DNR_PREV_OLED +Direction; // Change the active OLED (-/+) else if (Addr < FIRST_SEL_OLED_CMD) DispNr_Req=DNR_OLED0_NEXT+(Addr-FIRST_SEL_NEXT_TXT)*2 +Direction; // Next text on a certain OLED else if (Addr < FIRST_SEL_TXT_CMD) DispNr_Req=DNR_SEL_OLED0 +(Addr-FIRST_SEL_OLED_CMD)*2 +Direction; // Select a sertain OLED else DispNr_Req= (Addr-FIRST_SEL_TXT_CMD) *2 +Direction; Dprintf("notifyDccAccTurnoutOutput: %4i, %i, %i\n", Addr, Direction, DispNr_Req); // Debug } //Dprintf("*"); // Display received messages. If no rolling text is active 6-7 * are shown otherwise messages get lost => 1-4 * are shown } } #if 0 // Not used at the moment //--------------------------------------------------------- void notifyDccSigOutputState( uint16_t Addr, uint8_t State) //--------------------------------------------------------- // notifyDccSigOutputState() Callback for a signal aspect accessory decoder. // Defined in S-9.2.1 as the Extended Accessory Decoder Control Packet. { Dprintf("notifyDccSigState: %4i, %02X\n", Addr, State) ; } #endif #endif // USE_DCC #if defined(RAND_CHANGE_MINTIME) && defined(RAND_CHANGE_MAXTIME) //---------------------------- void Change_Display_ramdomly() //---------------------------- { static uint32_t Next_Rand_Change = 0; if (Next_Rand_Change == 0) { /* #if defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_ESP32) bootloader_random_enable(); #else randomSeed(analogRead(UNUSED_AIN_PIN)); #endif */ #if defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_ESP32) // ESP32 hat eigene Zufallsquelle uint32_t seed = esp_random(); randomSeed(seed); #else randomSeed(analogRead(UNUSED_AIN_PIN)); #endif } if (Next_Rand_Change <= 1) Next_Rand_Change = millis() + random(RAND_CHANGE_MINTIME*1000L, RAND_CHANGE_MAXTIME*1000L); if (millis() >= Next_Rand_Change) { Next_Rand_Change = 1; #if RAIL_NR_GROUPS if (Rail_Nr_Groups_Cnt > 1) Activate_Rail_Nr_Group(random(Rail_Nr_Groups_Cnt)); #else if (OLED_COUNT > 1) Activate_OLED(random(OLED_COUNT)); #endif Activate_Next_Display_from_Flash(); } } #endif //--------------------------------- void Check_Display_Change_Request() //--------------------------------- // Check the variable DispNr_Req and activate the display { if (ProcState == PC_WAIT_START || ProcState == PC_WAIT_END) // Wait until all characters form the serial input (or a previous command) are processed { switch (DispNr_Req) { case DNR_NOTHING: break; case DNR_PREV : Dprintf("Prev txt %i\n", DispNr); Activate_Prev_Display_from_Flash(); break; case DNR_NEXT : Dprintf("Next txt %i\n", DispNr); Activate_Next_Display_from_Flash(); break; case DNR_PREV_OLED: Dprintf("Prev_OLED_or_RailGrp\n"); Prev_OLED_or_RailGrp(); break; case DNR_NEXT_OLED: Dprintf("Next_OLED_or_RailGrp\n"); Next_OLED_or_RailGrp(); break; default : if (DispNr_Req < DNR_OLED0_NEXT) { Dprintf("Txt %i\n", DispNr_Req+1); if (Activate_Display_from_Flash_by_Nr(Active_OLED, DispNr_Req) == 0) Dprintf("Error DispNr_Req %i doesn't exist\n", DispNr_Req+1); } else if (DispNr_Req < DNR_SEL_OLED0) Next_Display_for_OLED_Nr_or_RailGrp(DispNr_Req - DNR_OLED0_NEXT); #if RAIL_NR_GROUPS else Activate_Rail_Nr_Group(DispNr_Req - DNR_SEL_OLED0); #else else Activate_OLED(DispNr_Req - DNR_SEL_OLED0); #endif } DispNr_Req = DNR_NOTHING; } } //--------------------------------------------------- void Debug_Print_Cash_Sub(uint8_t i, Disp_Cash_T &dc) //--------------------------------------------------- { char Short[16]; char *p = dc.lauftext; while (*p == ' ') p++; uint8_t GrpNr = i; #if RAIL_NR_GROUPS GrpNr = Get_Rail_Group(i); #endif strncpy(Short, p, sizeof(Short)-1); Short[sizeof(Short)-1] = '\0'; Dprintf("%2i (%c): %-15s %-20s %-5s %-5s %-7s", i+1, 'a'+GrpNr, Short, dc.zuglauf1, dc.gleis, dc.uhrzeit, dc.zugnummer); Dprintf(" %c", dc.gleisSeite == GleisSeite_Links ? 'L':'R'); Dprintf(" UpdateDisplay:%i\n", dc.UpdateDisplay); } //--------------------- void Debug_Print_Cash() //--------------------- { Dprintf("\n" "Disp_Cash:\n" "Nr Lauftext Zuglauf1 Gleis Uhrz. Zugnum. S Var...\n"); for (int8_t i = OLED_COUNT-1; i >= 0; i--) Debug_Print_Cash_Sub(i, Disp_Cash[i]); } //---------------- void Debug_Print() //---------------- { #if DEBUG_PRINT_VAR_WITH_TAB Debug_Print_Cash(); Dprintf("Global Var:\n"); Dprintf("Active_OLED: %i\n", Active_OLED+1); Dprintf("UpdateTime: %i ms\n", UpdateTime); //Dprintf("gleisSeite: %c\n", gleisSeite == GleisSeite_Links ? 'L':'R'); #endif } //----------------- void Check_FN_Key() //----------------- #define KEY_F1 0x00504F62 #define KEY_F2 0x00514F62 #define KEY_F3 0x00524F62 #define KEY_F4 0x00534F62 #define KEY_F5 0x35315BE0 #define KEY_F6 0x37315BE0 #define KEY_F7 0x38315BE0 #define KEY_F8 0x39315BE0 #define KEY_F9 0x30325BE0 #define KEY_F10 0x31325BE0 { #if USE_FUNCTIONKEYS uint32_t Start = millis(); uint64_t Key = 0; uint8_t Shift = 0; while (millis()-Start < 2) { if (Serial.available() > 0) { uint32_t c = Serial.read(); if (Shift < 56) { Key += c << Shift; Shift += 8; } else Dprintf("Add char 0x%02X\n", c); } } Dprintf("\rFN Key "); // \r is used to overwrite the characters generated when the key is pressed switch (Key) { case KEY_F1: Dprintf("%i ", 1); Next_Display_for_OLED_Nr_or_RailGrp(0); break; case KEY_F2: Dprintf("%i ", 2); Next_Display_for_OLED_Nr_or_RailGrp(1); break; case KEY_F3: Dprintf("%i ", 3); Next_Display_for_OLED_Nr_or_RailGrp(2); break; case KEY_F4: Dprintf("%i ", 4); Next_Display_for_OLED_Nr_or_RailGrp(3); break; case KEY_F5: Dprintf("%i ", 5); Next_Display_for_OLED_Nr_or_RailGrp(4); break; case KEY_F6: Dprintf("%i ", 6); Next_Display_for_OLED_Nr_or_RailGrp(5); break; case KEY_F7: Dprintf("%i ", 7); Next_Display_for_OLED_Nr_or_RailGrp(6); break; case KEY_F8: Dprintf("%i ", 8); Next_Display_for_OLED_Nr_or_RailGrp(7); break; case KEY_F9: Dprintf("%i ", 9); Activate_Next_Display_from_Flash(); break; case KEY_F10: Dprintf("%i ", 10); Next_OLED_or_RailGrp(); break; default: Dprintf("0x%08llX ", Key); } Dprintf("\t\t\n"); // Erase the Time in the console #endif } //--------------- void Print_Help() //--------------- { #if ACTIVATE_HELP && USE_SERIAL Print_DCC_Help(); Serial << F("Serial commands:\n"); Serial << F("================\n"); Serial << F("The program accepts the same serial commands as introduced in the program from Fredddy.\n"); Serial << F("Some additional commands have been added and a 'short' mode has been added.\n"); Serial << F("All commands start with a '#'.\n"); Serial << F("\n"); Serial << F("Beispiel:\n"); // ToDo: bersetzen ins Englisch? Serial << F(" #LDas ist ein neuer Lauftext#\n"); Serial << F("definiert einen neuen Lauftext\n"); Serial << F("Die Felder haben folgende Kuerzel:\n"); Serial << F(" L: Lauftext (Maximal 100 Zeichen)\n"); Serial << F(" G: Gleisnummer\n"); Serial << F(" W: Wagenstand\n"); Serial << F(" 1: Zuglauf 1\n"); Serial << F(" 2: Zuglauf 2\n"); Serial << F(" Z: Ziel\n"); Serial << F(" U: Abfahrtszeit\n"); Serial << F(" N: Zugnummer\n"); Serial << F(" X: Loesche Zug Daten (zugnummer, uhrzeit, ziel, zuglauf1, zuglauf2, wagenstand)\n"); Serial << F(" <: Gleis Links\n"); Serial << F(" >: Gleis Rechts\n"); Serial << F("New commands:\n"); Serial << F(" T: display text message with the given number: #T7 Also possible: #T+ and #T-\n"); Serial << F(" a-z: Select OLED display: #b switch to the second OLED display. The next\n"); Serial << F(" commands will control this panel.\n"); Serial << F("\n"); Serial << F("The function keys F1-F10 in the PIO serial console could be used like the hardware buttons\n"); Serial << F(" F1 = Show next Text in OLED 1\n"); Serial << F(" F2 = Show next Text in OLED 2\n"); Serial << F(" : : : :\n"); Serial << F(" F8 = Show next Text in OLED 8\n"); Serial << F(" F9 = Show next Text in active OLED\n"); Serial << F(" F10 = Activate next OLED\n"); Serial << F("\n"); Serial << F("TAB: Show internal data\n"); Serial << F("\n"); #endif } //--------------------------- void Proc_SpecialKey(char &c) //--------------------------- // Function keys: // F1: 1 98 79 80 // F2: 1 98 79 81 { switch (c) { case 1: Check_FN_Key(); c = 0; break; case '\t': Debug_Print(); c = 0; break; case '?': Print_Help(); c = 0; break; } } //************************************************************************************************************ void setup() //************************************************************************************************************ { #if USE_SERIAL pinMode(LED_HEARTBEAT_PIN, OUTPUT); // 07.08.25: //digitalWrite(LED_HEARTBEAT_PIN, 1); Serial.begin(SERIAL_BAUDRATE); Serial.println(F("Bahnsteigsanzeige by Hardi")); //Serial << F("OLED_COUNT:") << OLED_COUNT << endl; #endif #if defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_ESP32) { WiFi.begin(ssid, password); Serial.print("Verbinde mit WLAN "); unsigned long startAttemptTime = millis(); // Startzeit merken const unsigned long wifiTimeout = 5000; // Timeout in Millisekunden (5s) // Versuche, WLAN zu verbinden – aber nicht länger als 5s while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < wifiTimeout) { delay(500); Serial.print("."); } // Prüfe, ob Timeout überschritten wurde if (WiFi.status() == WL_CONNECTED) { Serial.println("\nWLAN verbunden, IP-Adresse:"); Serial.println(WiFi.localIP()); ArduinoOTA.setHostname(hostname); // ArduinoOTA.setPassword("geheim"); // Optional ArduinoOTA.onStart([]() { Serial.println("Start OTA Update"); }); ArduinoOTA.onEnd([]() { Serial.println("\nEnde OTA Update"); }); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { Serial.printf("Fortschritt: %u%%\r", (progress / (total / 100))); }); ArduinoOTA.onError([](ota_error_t error) { Serial.printf("Fehler[%u]: ", error); if (error == OTA_AUTH_ERROR) Serial.println("Auth Fehler"); else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Fehler"); else if (error == OTA_CONNECT_ERROR) Serial.println("Verbindungsfehler"); else if (error == OTA_RECEIVE_ERROR) Serial.println("Empfangsfehler"); else if (error == OTA_END_ERROR) Serial.println("End Fehler"); }); ArduinoOTA.begin(); Serial.println("Bereit für OTA!"); } else { Serial.println("\nWLAN konnte nicht innerhalb von 5 Sekunden verbunden werden."); // Hier könntest du z. B. einen Offline-Modus starten oder später erneut versuchen. } } #endif Wire.begin(); // if not called we got the messege: "beginTransmission(): could not acquire lock" u8g.setBusClock(400000); // According to the documentation this command must be called before begin(), But then it has no effect ;-( // Update time for one page with 128 byte: (Measured with scope). Driver: U8G2_SSD1306_128X32_UNIVISION_1_HW_I2C // 400 kHz 5.4 ms = default frequency if line is disabled // 600 kHz 3.9 ms // 700 kHz 3.8 ms // 800 kHz 3.7 ms // 900 kHz => Display is not working (I2C Signals look o.k. on the skope) // // Old lib 100 kHz 26 ms (page size 256 byte) #if USE_WIRE_TIMEOUT // 09.08.25: Wire.setWireTimeout(); #endif #if USE_I2C_MUX // 22.08.25: for (uint8_t i = 0; i < I2C_MUX_CHIPS; i++) { if (I2C_Mux[i].begin(0x70+i) == false) // Disables all ports Serial << F("I2C Mux ") << i << F("not detected\n"); } #endif memset(Disp_Cash, 0, sizeof(Disp_Cash)); #if RAIL_NR_GROUPS Fill_Rail_Group_Array(); #endif Setup_OLEDs(); // Calls u8g.begin() for all displays and displays the first screens // 09.08.25: Moved down below to set the bus clock before #if USE_DCC Get_Last_DCC_Addr(); #if defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_ESP32) Dcc.pin( DCC_SIGNAL_PIN, 1); // Without interrupt specification #else Dcc.pin(0, DCC_SIGNAL_PIN, 1); // Setup which External Interrupt, the Pin it's associated with that we're using and enable the Pull-Up #endif // Call the main DCC Init function to enable the DCC Receiver Dcc.init( MAN_ID_DIY, 10, CV29_ACCESSORY_DECODER | CV29_OUTPUT_ADDRESS_MODE, 0 ); // ToDo: Was bedeuten die Konstanten ? // Dprintf("FIRST_SEL_NEXT_TXT:%i\n", FIRST_SEL_NEXT_TXT); // Debug // Dprintf("FIRST_SEL_OLED_CMD:%i\n", FIRST_SEL_OLED_CMD); // Dprintf("FIRST_SEL_TXT_CMD :%i\n", FIRST_SEL_TXT_CMD ); // Dprintf("DNR_PREV :%i\n", DNR_PREV ); // The numbers could be used with the #t command // Dprintf("DNR_SEL_OLED0 :%i\n", DNR_SEL_OLED0 ); #endif #if ACTIVATE_HELP && USE_SERIAL Serial.println(F("Enter ? to show the help\n")); #endif } //************************************************************************************************************ void loop() //************************************************************************************************************ { // Use Wifi for Over the air-Updates // #if USE_WIFI #if defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_ESP32) ArduinoOTA.handle(); #endif // #endif uint32_t EndWait = millis() + 100; // Screen update period [ms]. Important for the rolling text. (Minimum 55 ms) // The original program (#73) had an update rate of 95 ms without delay // If the update rate is too high, the DCC commands are recognized worse uint32_t start = millis(); for (uint8_t i = 0; i < OLED_COUNT; i++) { if (Disp_Cash[i].UpdateDisplay != DONT_UPD_DISP) { //Set_Rail_Defaults(i); // gleisSeite und Gleisnummer Write_Display_to_OLED(i); // Update the display and set UpdateDisplay } } UpdateTime = millis() - start; //Dprintf("%i\n", UpdateTime); // Debug // Use a button to change the displayed message if (Button0.Pressed()) Activate_Next_Display_from_Flash(); if (Button1.Pressed()) Next_OLED_or_RailGrp(); if (Button2.Pressed()) Next_Display_for_OLED_Nr_or_RailGrp(0); if (Button3.Pressed()) Next_Display_for_OLED_Nr_or_RailGrp(1); if (Button4.Pressed()) Next_Display_for_OLED_Nr_or_RailGrp(2); #ifdef BUTTON5_PIN if (Button5.Pressed()) Next_Display_for_OLED_Nr_or_RailGrp(3); #endif do { #if USE_DCC Dcc.process(); // You MUST call the NmraDcc.process() method frequently from the Arduino loop() function for correct library operation #endif #if USE_SERIAL // 07.08.25: // Process serial characters while (Serial.available() > 0) { char c = Serial.read(); if (ProcState == PC_WAIT_START) Proc_SpecialKey(c); uint8_t Act_OLED = Active_OLED; Copy_Cash_to_ActData(Disp_Cash[Act_OLED]); // To be able to change values Process_Character(c); Copy_ActData_to_Cash(Disp_Cash[Act_OLED]); } #endif } while (millis() < EndWait); Check_Display_Change_Request(); #if defined(RAND_CHANGE_MINTIME) && defined(RAND_CHANGE_MAXTIME) Change_Display_ramdomly(); #endif #ifdef LED_HEARTBEAT_PIN #if USE_WIRE_TIMEOUT // 09.08.25: if (digitalRead(4) == 0 || digitalRead(5) == 0) LED_HeartBeat.Update(1000); // SDA or SCL pulled to GND if (Wire.getWireTimeoutFlag()) LED_HeartBeat.Update(200); else #endif LED_HeartBeat.Update(); #endif } /* Arduino Nano: +-----+ +------------| USB |------------+ | +-----+ | LED HB | [ ]D13/SCK MISO/D12[ ] | | [ ]3.3V MOSI/D11[ ]~| | [ ]V.ref ___ SS/D10[ ]~| Enable OLED panel 2 DetectP | [ ]A0 / N \ D9[ ]~| Enable OLED panel 1 | [ ]A1 / A \ D8[ ] | Enable OLED panel 0 | [ ]A2 \ N / D7[ ] | Push button4: Next text message for OLED 2 RandNr | [ ]A3 \_0_/ D6[ ]~| Push button3: Next text message for OLED 1 OLED | [ ]A4/SDA D5[ ]~| Push button2: Next text message for OLED 0 OLED | [ ]A5/SCL D4[ ] | Push button1: Switch to the next OLED | [ ]A6 INT1/D3[ ]~| Push button0: Activate the next text message from Flash fo the current OLED | [ ]A7 INT0/D2[ ] | DCC Optocopler | [ ]5V GND[ ] | | [ ]RST RST[ ] | | [ ]GND 5V MOSI GND TX1[ ] | | [ ]Vin [ ] [ ] [ ] RX1[ ] | | [ ] [ ] [ ] | | MISO SCK RST | | NANO-V3 | +-------------------------------+ */[/code]