diff --git a/CommandStation-EX.ino b/CommandStation-EX.ino index 3a0e5ca..31b3b21 100644 --- a/CommandStation-EX.ino +++ b/CommandStation-EX.ino @@ -141,44 +141,73 @@ void setup() CommandDistributor::broadcastPower(); } +void looptimer(unsigned long timeout, const FSH* message) +{ + static unsigned long lasttimestamp = 0; + unsigned long now = micros(); + if (timeout != 0) { + unsigned long diff = now - lasttimestamp; + if (diff > timeout) { + DIAG(message); + DIAG(F("DeltaT=%L"), diff); + lasttimestamp = micros(); + return; + } + } + lasttimestamp = now; +} + void loop() { // The main sketch has responsibilities during loop() // Responsibility 1: Handle DCC background processes // (loco reminders and power checks) + looptimer(0, F("")); DCC::loop(); + looptimer(5000, F("DCC")); // got warnings up to 3884 during prog track read // Responsibility 2: handle any incoming commands on USB connection SerialManager::loop(); + looptimer(2000, F("Serial")); // got warnings up to 1900 during start // Responsibility 3: Optionally handle any incoming WiFi traffic #ifndef ARDUINO_ARCH_ESP32 #if WIFI_ON WifiInterface::loop(); + looptimer(9000, F("Wifi")); // got warnings up to 8000 + #endif //WIFI_ON #else //ARDUINO_ARCH_ESP32 #ifndef WIFI_TASK_ON_CORE0 WifiESP::loop(); + looptimer(1000, F("WifiESP")); + #endif #endif //ARDUINO_ARCH_ESP32 #if ETHERNET_ON EthernetInterface::loop(); + looptimer(10000, F("Ethernet")); #endif RMFT::loop(); // ignored if no automation + looptimer(1000, F("RMFT")); #if defined(LCN_SERIAL) LCN::loop(); + looptimer(1000, F("LCN")); #endif // Display refresh DisplayInterface::loop(); + looptimer(2000, F("Display")); // got warnings around 1150 // Handle/update IO devices. IODevice::loop(); + looptimer(1000, F("IODevice")); Sensor::checkAll(); // Update and print changes + looptimer(1000, F("Sensor")); // Report any decrease in memory (will automatically trigger on first call) static int ramLowWatermark = __INT_MAX__; // replaced on first loop diff --git a/EthernetInterface.cpp b/EthernetInterface.cpp index 34e209a..4b83f6d 100644 --- a/EthernetInterface.cpp +++ b/EthernetInterface.cpp @@ -29,77 +29,96 @@ #include "CommandDistributor.h" #include "WiThrottle.h" #include "DCCTimer.h" +#if __has_include ( "MDNS_Generic.h") + #include "MDNS_Generic.h" + #define DO_MDNS + EthernetUDP udp; + MDNS mdns(udp); +#endif + + +extern void looptimer(unsigned long timeout, const FSH* message); + +bool EthernetInterface::connected=false; +EthernetServer * EthernetInterface::server= nullptr; +EthernetClient EthernetInterface::clients[MAX_SOCK_NUM]; // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield +uint8_t EthernetInterface::buffer[MAX_ETH_BUFFER+1]; // buffer used by TCP for the recv +RingStream * EthernetInterface::outboundRing = nullptr; -EthernetInterface * EthernetInterface::singleton=NULL; /** * @brief Setup Ethernet Connection * */ -void EthernetInterface::setup() + +void EthernetInterface::setup() // STM32 VERSION { - if (singleton!=NULL) { - DIAG(F("Prog Error!")); - return; - } - if ((singleton=new EthernetInterface())) - return; - DIAG(F("Ethernet not initialized")); -}; + DIAG(F("Ethernet begin" + #ifdef DO_MDNS + " with mDNS" + #endif + )); + + #ifdef STM32_ETHERNET + // Set a HOSTNAME for the DHCP request - a nice to have, but hard it seems on LWIP for STM32 + // The default is "lwip", which is **always** set in STM32Ethernet/src/utility/ethernetif.cpp + // for some reason. One can edit it to instead read: + // #if LWIP_NETIF_HOSTNAME + // /* Initialize interface hostname */ + // if (netif->hostname == NULL) + // netif->hostname = "lwip"; + // #endif /* LWIP_NETIF_HOSTNAME */ + // Which seems more useful! We should propose the patch... so the following line actually works! + netif_set_hostname(&gnetif, WIFI_HOSTNAME); // Should probably be passed in the contructor... + #endif - -#ifdef IP_ADDRESS -static IPAddress myIP(IP_ADDRESS); -#endif - -/** - * @brief Aquire IP Address from DHCP and start server - * - * @return true - * @return false - */ -EthernetInterface::EthernetInterface() -{ byte mac[6]; DCCTimer::getSimulatedMacAddress(mac); - connected=false; - -#ifdef IP_ADDRESS - Ethernet.begin(mac, myIP); -#else - if (Ethernet.begin(mac) == 0) - { - DIAG(F("Ethernet.begin FAILED")); - return; - } -#endif - if (Ethernet.hardwareStatus() == EthernetNoHardware) { - DIAG(F("Ethernet shield not found or W5100")); - } - unsigned long startmilli = millis(); - while ((millis() - startmilli) < 5500) { // Loop to give time to check for cable connection - if (Ethernet.linkStatus() == LinkON) - break; - DIAG(F("Ethernet waiting for link (1sec) ")); - delay(1000); - } - // now we either do have link of we have a W5100 - // where we do not know if we have link. That's - // the reason to now run checkLink. - // CheckLinks sets up outboundRing if it does - // not exist yet as well. - checkLink(); + #ifdef IP_ADDRESS + static IPAddress myIP(IP_ADDRESS); + Ethernet.begin(mac,myIP); + #else + if (Ethernet.begin(mac)==0) + { + LCD(4,F("IP: No DHCP")); + return; + } + #endif + + auto ip = Ethernet.localIP(); // look what IP was obtained (dynamic or static) + if (!ip) { + LCD(4,F("IP: None")); + return; + } + server = new EthernetServer(IP_PORT); // Ethernet Server listening on default port IP_PORT + server->begin(); + + // Arrange display of IP address and port + #ifdef LCD_DRIVER + const byte lcdData[]={LCD_DRIVER}; + const bool wideDisplay=lcdData[1]>=24; // data[1] is cols. + #else + const bool wideDisplay=true; + #endif + if (wideDisplay) { + // OLEDS or just usb diag is ok on one line. + LCD(4,F("IP %d.%d.%d.%d:%d"), ip[0], ip[1], ip[2], ip[3], IP_PORT); + } + else { // LCDs generally too narrow, so take 2 lines + LCD(4,F("IP %d.%d.%d.%d"), ip[0], ip[1], ip[2], ip[3]); + LCD(5,F("Port %d"), IP_PORT); + } + + outboundRing=new RingStream(OUTBOUND_RING_SIZE); + #ifdef DO_MDNS + mdns.begin(Ethernet.localIP(), WIFI_HOSTNAME); // hostname + mdns.addServiceRecord(WIFI_HOSTNAME "._withrottle", IP_PORT, MDNSServiceTCP); + // Not sure if we need to run it once, but just in case! + mdns.run(); + #endif + connected=true; } -/** - * @brief Cleanup any resources - * - * @return none - */ -EthernetInterface::~EthernetInterface() { - delete server; - delete outboundRing; -} /** * @brief Main loop for the EthernetInterface @@ -107,134 +126,138 @@ EthernetInterface::~EthernetInterface() { */ void EthernetInterface::loop() { - if (!singleton || (!singleton->checkLink())) - return; + if (!connected) return; + looptimer(5000, F("E.loop")); + + static bool warnedAboutLink=false; + if (Ethernet.linkStatus() == LinkOFF){ + if (warnedAboutLink) return; + DIAG(F("Ethernet link OFF")); + warnedAboutLink=true; + return; + } + looptimer(5000, F("E.loop warn")); + + // link status must be ok here + if (warnedAboutLink) { + DIAG(F("Ethernet link RESTORED")); + warnedAboutLink=false; + } + #ifdef DO_MDNS + // Always do this because we don't want traffic to intefere with being found! + mdns.run(); + looptimer(5000, F("E.mdns")); + + #endif + + // switch (Ethernet.maintain()) { case 1: //renewed fail DIAG(F("Ethernet Error: renewed fail")); - singleton=NULL; + connected=false; return; case 3: //rebind fail DIAG(F("Ethernet Error: rebind fail")); - singleton=NULL; + connected=false; return; default: //nothing happened + //DIAG(F("maintained")); break; } - singleton->loop2(); -} - -/** - * @brief Checks ethernet link cable status and detects when it connects / disconnects - * - * @return true when cable is connected, false otherwise - */ -bool EthernetInterface::checkLink() { - if (Ethernet.linkStatus() != LinkOFF) { // check for not linkOFF instead of linkON as the W5100 does return LinkUnknown - //if we are not connected yet, setup a new server - if(!connected) { - DIAG(F("Ethernet cable connected")); - connected=true; - #ifdef IP_ADDRESS - Ethernet.setLocalIP(myIP); // for static IP, set it again - #endif - IPAddress ip = Ethernet.localIP(); // look what IP was obtained (dynamic or static) - server = new EthernetServer(IP_PORT); // Ethernet Server listening on default port IP_PORT - server->begin(); - LCD(4,F("IP: %d.%d.%d.%d"), ip[0], ip[1], ip[2], ip[3]); - LCD(5,F("Port:%d"), IP_PORT); - // only create a outboundRing it none exists, this may happen if the cable - // gets disconnected and connected again - if(!outboundRing) - outboundRing=new RingStream(OUTBOUND_RING_SIZE); - } - return true; - } else { // connected - DIAG(F("Ethernet cable disconnected")); - connected=false; - //clean up any client - for (byte socket = 0; socket < MAX_SOCK_NUM; socket++) { - if(clients[socket].connected()) - clients[socket].stop(); - } - // tear down server - delete server; - server = nullptr; - LCD(4,F("IP: None")); - } - return false; -} - -void EthernetInterface::loop2() { - if (!outboundRing) { // no idea to call loop2() if we can't handle outgoing data in it - if (Diag::ETHERNET) DIAG(F("No outboundRing")); - return; - } - // get client from the server - EthernetClient client = server->accept(); - - // check for new client - if (client) - { - if (Diag::ETHERNET) DIAG(F("Ethernet: New client ")); - byte socket; + looptimer(5000, F("E.maintain")); + + // get client from the server + #if defined (STM32_ETHERNET) + // STM32Ethernet doesn't use accept(), just available() + auto client = server->available(); + if (client) { + // check for new client + byte socket; + bool sockfound = false; + for (socket = 0; socket < MAX_SOCK_NUM; socket++) + { + if (client == clients[socket]) + { + sockfound = true; + break; + } + } + if (!sockfound) + { // new client for (socket = 0; socket < MAX_SOCK_NUM; socket++) { - if (!clients[socket]) - { - // On accept() the EthernetServer doesn't track the client anymore - // so we store it in our client array - if (Diag::ETHERNET) DIAG(F("Socket %d"),socket); - clients[socket] = client; - break; - } + if (!clients[socket]) + { + clients[socket] = client; + sockfound=true; + if (Diag::ETHERNET) + DIAG(F("Ethernet: New client socket %d"), socket); + break; + } } - if (socket==MAX_SOCK_NUM) DIAG(F("new Ethernet OVERFLOW")); + } + if (!sockfound) DIAG(F("new Ethernet OVERFLOW")); } + + #else + auto client = server->accept(); + if (client) clients[client.getSocketNumber()]=client; + #endif + // check for incoming data from all possible clients for (byte socket = 0; socket < MAX_SOCK_NUM; socket++) { - if (clients[socket]) { - - int available=clients[socket].available(); - if (available > 0) { - if (Diag::ETHERNET) DIAG(F("Ethernet: available socket=%d,avail=%d"), socket, available); - // read bytes from a client - int count = clients[socket].read(buffer, MAX_ETH_BUFFER); - buffer[count] = '\0'; // terminate the string properly - if (Diag::ETHERNET) DIAG(F(",count=%d:%e"), socket,buffer); - // execute with data going directly back - CommandDistributor::parse(socket,buffer,outboundRing); - return; // limit the amount of processing that takes place within 1 loop() cycle. - } - } - } - - // stop any clients which disconnect - for (int socket = 0; socket 0) { // we have incoming data + buffer[count] = '\0'; // terminate the string properly + if (Diag::ETHERNET) DIAG(F("Ethernet s=%d, c=%d b=:%e"), socket, count, buffer); + // execute with data going directly back + CommandDistributor::parse(socket,buffer,outboundRing); + //looptimer(5000, F("Ethloop2 parse")); + return; // limit the amount of processing that takes place within 1 loop() cycle. + } + + // count=0 The client has disconnected + clients[socket].stop(); + CommandDistributor::forget(socket); + if (Diag::ETHERNET) DIAG(F("Ethernet: disconnect %d "), socket); + } + WiThrottle::loop(outboundRing); - + // handle at most 1 outbound transmission - int socketOut=outboundRing->read(); + auto socketOut=outboundRing->read(); + if (socketOut<0) return; // no outbound pending + if (socketOut >= MAX_SOCK_NUM) { - DIAG(F("Ethernet outboundRing socket=%d error"), socketOut); - } else if (socketOut >= 0) { - int count=outboundRing->count(); - if (Diag::ETHERNET) DIAG(F("Ethernet reply socket=%d, count=:%d"), socketOut,count); - for(;count>0;count--) clients[socketOut].write(outboundRing->read()); - clients[socketOut].flush(); //maybe + // This is a catastrophic code failure and unrecoverable. + DIAG(F("Ethernet outboundRing s=%d error"), socketOut); + connected=false; + return; + } + + auto count=outboundRing->count(); + { + char tmpbuf[count+1]; // one extra for '\0' + for(int i=0;iread(); + } + tmpbuf[count]=0; + if (Diag::ETHERNET) DIAG(F("Ethernet reply s=%d, c=%d, b:%e"), + socketOut,count,tmpbuf); + clients[socketOut].write(tmpbuf,count); } + } #endif diff --git a/EthernetInterface.h b/EthernetInterface.h index 8078c3f..9ea2718 100644 --- a/EthernetInterface.h +++ b/EthernetInterface.h @@ -35,6 +35,14 @@ #if defined (ARDUINO_TEENSY41) #include //TEENSY Ethernet Treiber #include +#elif defined (ARDUINO_NUCLEO_F429ZI) || defined (ARDUINO_NUCLEO_F439ZI) || defined (ARDUINO_NUCLEO_F4X9ZI) + #include +// #include "STM32lwipopts.h" + #include + #include + extern "C" struct netif gnetif; + #define STM32_ETHERNET + #define MAX_SOCK_NUM 8 #else #include "Ethernet.h" #endif @@ -45,7 +53,7 @@ * */ -#define MAX_ETH_BUFFER 512 +#define MAX_ETH_BUFFER 128 #define OUTBOUND_RING_SIZE 2048 class EthernetInterface { @@ -56,16 +64,11 @@ class EthernetInterface { static void loop(); private: - static EthernetInterface * singleton; - bool connected; - EthernetInterface(); - ~EthernetInterface(); - void loop2(); - bool checkLink(); - EthernetServer * server = NULL; - EthernetClient clients[MAX_SOCK_NUM]; // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield - uint8_t buffer[MAX_ETH_BUFFER+1]; // buffer used by TCP for the recv - RingStream * outboundRing = NULL; + static bool connected; + static EthernetServer * server; + static EthernetClient clients[MAX_SOCK_NUM]; // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield + static uint8_t buffer[MAX_ETH_BUFFER+1]; // buffer used by TCP for the recv + static RingStream * outboundRing; }; #endif diff --git a/platformio.ini b/platformio.ini index e1d6ed9..2de9bed 100644 --- a/platformio.ini +++ b/platformio.ini @@ -104,10 +104,35 @@ lib_deps = ${env.lib_deps} arduino-libraries/Ethernet SPI + MDNS_Generic + +lib_ignore = WiFi101 + WiFi101_Generic + WiFiEspAT + WiFiMulti_Generic + WiFiNINA_Generic + monitor_speed = 115200 monitor_echo = yes build_flags = +[env:mega2560-eth] +platform = atmelavr +board = megaatmega2560 +framework = arduino +lib_deps = + ${env.lib_deps} + arduino-libraries/Ethernet + MDNS_Generic + SPI +lib_ignore = WiFi101 + WiFi101_Generic + WiFiEspAT + WiFiMulti_Generic + WiFiNINA_Generic +monitor_speed = 115200 +monitor_echo = yes + [env:mega328] platform = atmelavr board = uno @@ -246,6 +271,24 @@ monitor_echo = yes ; Experimental - Ethernet work still in progress ; +[env:Nucleo-F429ZI] +platform = ststm32 +board = nucleo_f429zi +framework = arduino +lib_deps = ${env.lib_deps} + stm32duino/STM32Ethernet @ ^1.3.0 + stm32duino/STM32duino LwIP @ ^2.1.2 + MDNS_Generic +lib_ignore = WiFi101 + WiFi101_Generic + WiFiEspAT + WiFiMulti_Generic + WiFiNINA_Generic +build_flags = -std=c++17 -Os -g2 -Wunused-variable +monitor_speed = 115200 +monitor_echo = yes +upload_protocol = stlink + ; [env:Nucleo-F429ZI] ; platform = ststm32 ; board = nucleo_f429zi