// ESP8266_NMEA_BRIDGE // V1.1.0 // 2022/01/11 V1.1.0 added DHCP server to AccessPoint /* (c)2021-2022 Forward Computing and Control Pty. Ltd. NSW Australia, www.forward.com.au This code is not warranted to be fit for any purpose. You may only use it at your own risk. This code may be freely used for both private and commercial use Provide this copyright is maintained. */ #include #include #include #include #include #include #include "SafeString.h" #include "millisDelay.h" // Arduino Monitor input must be set to Newline (or CR and NL) !!! // comment this out to remove UDP broadcast #define UDP_BROADCAST // this connects debugPtr I/O only use while testing leave commented out in final code //#define DEBUG #ifdef DEBUG // uncomment this to connect / disconnect messages on debugPtr out. #define CONNECTION_MESSAGES #endif //forward declarations void sendUDP(const char* str); void setupAP(const char* ssid_wifi, const char* password_wifi); Stream* debugPtr = NULL; // these are only used to setup your own network SSID / pw // when the config pin GPIO2 is grounded on startup. const char ssidPrefix[] = "NMEA_"; createSafeString(NMEA_AP, 32); // NMEA_xxxxxxxxxxxx createSafeString(NMEA_PASSWORD, 32, "NMEA_WiFi_Bridge"); int NMEAport = 10110; // Official NMEA TCP/UDP port https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers // NMEA 0183 https://en.wikipedia.org/wiki/NMEA_0183 // Messages have a maximum length of 82 characters, including the $ or ! starting character and the ending ('\n') // The start character for each message can be either a $ (For conventional field delimited messages) or ! (for messages that have special encapsulation in them) // ends the message. // The next five characters identify the talker (two characters) and the type of message (three characters). createSafeString(NMEAstr, 255); // max NMEA msg length is 82 so 255 is plenty WiFiServer tcpServer(NMEAport); // telnet #define MAX_SRV_CLIENTS 5 WiFiClient tcpServerClients[MAX_SRV_CLIENTS]; bool tcpClientConnected[MAX_SRV_CLIENTS]; pfodESP8266BufferedClient* bufferedClients[MAX_SRV_CLIENTS]; // buffers for each client pfodESP8266BufferedClient bufferedClient0; pfodESP8266BufferedClient bufferedClient1; pfodESP8266BufferedClient bufferedClient2; pfodESP8266BufferedClient bufferedClient3; pfodESP8266BufferedClient bufferedClient4; // must match MAX_SRV_CLIENTS-1 IPAddress local_ip = IPAddress(10, 1, 1, 1); IPAddress gateway_ip = IPAddress(10, 1, 1, 1); IPAddress subnet_ip = IPAddress(255, 255, 255, 0); IPAddress broadcastUDP(230, 1, 1, 1); #ifdef UDP_BROADCAST WiFiUDP udp; #endif // UDP_BROADCAST #define SerialGPS Serial // GPS/AIS or other device connected to the ESP UART #define SerialDebug Serial1 // Debug goes out on GPIO02 millisDelay restartDelay; const unsigned long RESTART_DELAY_MS = 3000; bool submittedCalled = false; // =============== end of pfodWifiWebConfig settings ============== // On ESP8266-01 and Adafruit HAZZAH ESP8266, connect LED + 270ohm resistor from D2 (GPIO2) to +3V3 to indicate when in config mode // start the eeprom address from 20 for Config const uint8_t webConfigEEPROMStartAddress = 20; // set the EEPROM structure struct EEPROM_storage { uint32_t baudRate; uint16_t tcpPortNo; uint16_t udpPortNo; uint16_t txPower; byte init; } storage; const int EEPROM_storageSize = sizeof(EEPROM_storage); ESP8266WebServer webserver(80); // this just sets portNo nothing else happens until begin() is called void restart() { if (debugPtr) { debugPtr->println(F("restart()")); } wdt_enable(WDTO_15MS); while (1) {} } // use these setting if EEPROM not initialized void initializeEEPROM() { if (debugPtr) { debugPtr->println(F("Initializing EEPROM")); debugPtr->print(F(" storage.init = 0x")); debugPtr->println(storage.init, HEX); } storage.init = 1; storage.baudRate = 38400; storage.tcpPortNo = 10110; storage.udpPortNo = 10110; storage.txPower = 60; uint8_t * byteStorage = (uint8_t *)&storage; for (size_t i = 0; i < EEPROM_storageSize; i++) { EEPROM.write(webConfigEEPROMStartAddress + i, byteStorage[i]); } delay(0); bool committed = EEPROM.commit(); #ifdef DEBUG if (debugPtr) { debugPtr->print(F("EEPROM.commit() = ")); debugPtr->println(committed ? "true" : "false"); } #endif if (debugPtr) { uint8_t * byteStorageRead = (uint8_t *)&storage; for (size_t i = 0; i < EEPROM_storageSize; i++) { byteStorageRead[i] = EEPROM.read(webConfigEEPROMStartAddress + i); } debugPtr->println(F("Storage retrieved")); printStorage(&storage); } } void SSIDmacAddress(SafeString& ssidStr) { uint8_t mac[6]; WiFi.macAddress(mac); char macStr[18] = { 0 }; sprintf(macStr, "%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); ssidStr = ssidPrefix; ssidStr += macStr; // add mac address to NMEA } void setup() { EEPROM.begin(512); // WiFi.mode(WIFI_STA); // WiFi.persistent(false); delay(10); float tx_dB = float(storage.txPower) / 4.0f; WiFi.setOutputPower(tx_dB); SSIDmacAddress(NMEA_AP); // else button was not pressed continue to load the stored network settings //else use configured setttings from EEPROM uint8_t * byteStorageRead = (uint8_t *)&storage; for (size_t i = 0; i < EEPROM_storageSize; i++) { byteStorageRead[i] = EEPROM.read(webConfigEEPROMStartAddress + i); } #ifdef DEBUG SerialDebug.begin(38400); // start another serial if you have separate debug out debugPtr = &SerialDebug; #endif if (debugPtr) { SafeString::setOutput(SerialDebug); } if (debugPtr) { debugPtr->println(); for (int i = 10; i > 0; i--) { delay(500); debugPtr->print(i); debugPtr->print(' '); } debugPtr->println(); debugPtr->println(F("Starting NMEA to WiFi Bridge")); } if (storage.init == 0xff) { // eeprom not set yet initializeEEPROM(); } //start UART. Be sure to set the speed to match the speed of whatever is //connected to the UART. SerialGPS.begin(storage.baudRate); for (int i = 0; i < MAX_SRV_CLIENTS ; i++) { tcpClientConnected[i] = false; } bufferedClients[0] = &bufferedClient0; bufferedClients[1] = &bufferedClient1; bufferedClients[2] = &bufferedClient2; bufferedClients[3] = &bufferedClient3; bufferedClients[4] = &bufferedClient4; // must match MAX_SRV_CLIENTS -1 if (debugPtr) { debugPtr->print(F("Tx Power set to ")); debugPtr->println(storage.txPower); } setupAP(NMEA_AP.c_str(), NMEA_PASSWORD.c_str()); // starts config webserver tcpServer.begin(storage.tcpPortNo); // overrides port setting above tcpServer.setNoDelay(true); // does not do much if anything if (debugPtr) { debugPtr->println(F("TCP Server Started")); // Print the IP address debugPtr->print(local_ip); debugPtr->print(':'); debugPtr->println(storage.tcpPortNo); } #ifdef UDP_BROADCAST if (debugPtr) { debugPtr->println(F("UDP Server Started")); // Print the IP address debugPtr->print(broadcastUDP); debugPtr->print(':'); debugPtr->println(storage.udpPortNo); } #endif // UDP_BROADCAST if (debugPtr) { debugPtr->print(F("Input baud rate is ")); debugPtr->println(storage.baudRate); } } void loop() { webserver.handleClient(); delay(0); if (restartDelay.justFinished()) { restart(); } // check for full buffers to send to TCP for (int i = 0; i < MAX_SRV_CLIENTS; i++) { bufferedClients[i]->available(); // checks for send timeout } // check for still connected for (int i = 0; i < MAX_SRV_CLIENTS; i++) { //find free/disconnected spot if (tcpClientConnected[i] && (!tcpServerClients[i].connected())) { if (debugPtr) { debugPtr->print("closed client:"); debugPtr->println(i); } tcpServerClients[i].stop(); bufferedClients[i]->stop(); tcpClientConnected[i] = false; } } // look for new clients connecting if (tcpServer.hasClient()) { #ifdef CONNECTION_MESSAGES if (debugPtr) { debugPtr->println("Have client"); } #endif bool foundSlot = false; for (int i = 0; i < MAX_SRV_CLIENTS; i++) { //find free/disconnected spot if (!tcpServerClients[i] || !tcpServerClients[i].connected()) { foundSlot = true; if (tcpServerClients[i]) { tcpServerClients[i].stop(); } tcpServerClients[i] = tcpServer.available(); tcpServerClients[i].setNoDelay(true); // does not do much if anything bufferedClients[i]->stop(); // clean up bufferedClients[i]->connect(&(tcpServerClients[i])); tcpClientConnected[i] = true; #ifdef CONNECTION_MESSAGES if (debugPtr) { debugPtr->print("New client: "); debugPtr->println(i); } #endif break; } } if (!foundSlot) { // drop some other client?? perhaps have half closed connection problem if (debugPtr) { debugPtr->println("No free slots for this client close last one."); } WiFiClient tcpClient = tcpServer.available(); tcpServerClients[MAX_SRV_CLIENTS - 1].stop(); bufferedClients[MAX_SRV_CLIENTS - 1]->stop(); // clean up tcpServerClients[MAX_SRV_CLIENTS - 1] = tcpClient; tcpServerClients[MAX_SRV_CLIENTS - 1].setNoDelay(true); // does not do much if anything bufferedClients[MAX_SRV_CLIENTS - 1]->connect(&(tcpServerClients[MAX_SRV_CLIENTS - 1])); if (debugPtr) { debugPtr->print("Closed "); debugPtr->print(MAX_SRV_CLIENTS - 1); debugPtr->println(" and reused"); } } } // check for incoming data to reset timer for (int i = 0; i < MAX_SRV_CLIENTS; i++) { if (tcpClientConnected[i] && tcpServerClients[i].connected()) { //get data from the telnet client and push it to the UART while ((bufferedClients[i]->available()) && (SerialGPS.availableForWrite() > 0)) { // use SerialGPS.availableForWrite to prevent loosing incoming data SerialGPS.write(bufferedClients[i]->read()); // NMEA I/O delay(0); } } } // handle input // read into SafeString until get terminator // filter (to be done) // write out to TCP and UDP if (NMEAstr.readUntil(SerialGPS, "\n")) { // non-blocking read // found \n in input so process line bool endsWithNL = NMEAstr.endsWith("\n"); NMEAstr.trim(); // remove leading and trailing white space if (!NMEAstr.isEmpty()) { // skip empty lines if (endsWithNL) { NMEAstr += "\r\n"; // add back terminators } // else just buffer full // add filtering here !!! // ... // if line passes add to TCP connections const char* str = NMEAstr.c_str(); size_t str_len = NMEAstr.length(); for (int i = 0; i < MAX_SRV_CLIENTS; i++) { if (tcpServerClients[i] && tcpServerClients[i].connected()) { //get data from the telnet client and push it to the UART bufferedClients[i]->write((const uint8_t*)str, str_len); delay(0); } } // send to UDP broadcast if enabled sendUDP(str); // does nothing if not enabled NMEAstr.clear(); // finished with this line } } } void setupAP(const char* ssid_wifi, const char* password_wifi) { WiFi.softAPConfig(local_ip, gateway_ip, subnet_ip); WiFi.softAP(ssid_wifi, password_wifi); if (debugPtr) { debugPtr->print(F("Access Point setup ")); debugPtr->print(ssid_wifi); debugPtr->print(" pw:"); debugPtr->print(password_wifi); debugPtr->println(); } if (debugPtr) { IPAddress myIP = WiFi.softAPIP(); debugPtr->print(F("AP/Sever IP address: ")); debugPtr->println(myIP); } delay(10); webserver.on("/", handleRoot ); webserver.on("/config", handleConfig ); webserver.on("/restart", handleRestart ); webserver.onNotFound( handleNotFound ); webserver.begin(); #ifdef DEBUG if (debugPtr) { debugPtr->println ( "Config webserver started" ); } #endif } void sendUDP(const char* str) { #ifdef UDP_BROADCAST if (debugPtr) { debugPtr->print("sendUDP : "); debugPtr->println(str); } udp.beginPacketMulticast(broadcastUDP, storage.udpPortNo, local_ip); udp.write(str); udp.endPacket(); #endif // UDP_BROADCAST } void handleRestart() { if (!submittedCalled) { handleRoot(); return; // if user refreshes restart } createSafeString(rtnMsg, 2048); rtnMsg = "" "" "" "" "" "" "
" "
" "
" "
" "

" "


" "
" "
" "" ""; webserver.send ( 200, "text/html", rtnMsg.c_str() ); restartDelay.start(RESTART_DELAY_MS); } void handleConfig() { // set defaults uint16_t portNo = 10110; uint32_t baudRate = 38400; createSafeString(tmpStr, 50); // for rate,port,timeout if (webserver.args() > 0) { #ifdef DEBUG if (debugPtr) { createSafeString(message, 512); message = "Config results\n\n"; message += "URI: "; message += webserver.uri().c_str(); message += "\nMethod: "; message += ( webserver.method() == HTTP_GET ) ? "GET" : "POST"; message += "\nArguments: "; message += webserver.args(); message += "\n"; for ( uint8_t i = 0; i < webserver.args(); i++ ) { message += " "; message += webserver.argName(i).c_str(); message += ": "; message += webserver.arg(i).c_str(); message += "\n"; } debugPtr->println(message); debugPtr->println(); } #endif createSafeString(argName, 30); createSafeString(argValue, 128); uint8_t numOfArgs = webserver.args(); const char *strPtr; uint8_t i = 0; for (; (i < numOfArgs); i++ ) { // check field numbers argName = webserver.argName(i).c_str(); // do this to avoid creating more String is cmps below argValue = webserver.arg(i).c_str(); argValue.trim(); // trim spaces if (argName == "TxPwr") { #ifdef DEBUG if (debugPtr) { debugPtr->print(F("txPwr:")); debugPtr->println(argValue); } #endif long txPower = 0; if (!argValue.toLong(txPower)) { txPower = 60; } // else if (txPower < 0) { txPower = 0; } if (txPower > 82) { txPower = 82; } storage.txPower = (uint16_t)txPower; } else if (argName == "UDPport") { // convert portNo to uint16_6 #ifdef DEBUG if (debugPtr) { debugPtr->print(F("udpPortNoStr:")); debugPtr->println(argValue); } #endif long longPort = 0; if (!argValue.toLong(longPort)) { longPort = 10110; } // else storage.udpPortNo = (uint16_t)longPort; } else if (argName == "TCPport") { // convert portNo to uint16_6 #ifdef DEBUG if (debugPtr) { debugPtr->print(F("tcpPortNoStr:")); debugPtr->println(argValue); } #endif long longPort = 0; if (!argValue.toLong(longPort)) { longPort = 10110; } // else storage.tcpPortNo = (uint16_t)longPort; } else if (argName == "baud") { // convert baud rate to int32_t #ifdef DEBUG if (debugPtr) { debugPtr->print(F("baudStr:")); debugPtr->println(argValue); } #endif long baud = 0; if (!argValue.toLong(baud)) { baud = 38400; } // else storage.baudRate = (uint32_t)(baud); } } #ifdef DEBUG if (debugPtr) { debugPtr->println(); printStorage(&storage); } #endif uint8_t * byteStorage = (uint8_t *)&storage; for (size_t i = 0; i < EEPROM_storageSize; i++) { EEPROM.write(webConfigEEPROMStartAddress + i, byteStorage[i]); } delay(0); bool committed = EEPROM.commit(); #ifdef DEBUG if (debugPtr) { debugPtr->print(F("EEPROM.commit() = ")); debugPtr->println(committed ? "true" : "false"); } #endif } // else if no args just return current settings delay(0); struct EEPROM_storage storageRead; uint8_t * byteStorageRead = (uint8_t *)&storageRead; for (size_t i = 0; i < EEPROM_storageSize; i++) { byteStorageRead[i] = EEPROM.read(webConfigEEPROMStartAddress + i); } #ifdef DEBUG if (debugPtr) { debugPtr->println(); printStorage(&storageRead); } #endif createSafeString(rtnMsg, 2048); rtnMsg = "" "" "" "" "
" "
" " NMEA Bridge Config" "
" "
" "" "
" "

" "

" "Use the button below to apply these settings
otherwise they will be applied on next power up of the NMEA to WiFi Bridge
"; rtnMsg += "SSID: "; rtnMsg += ""; rtnMsg += NMEA_AP; rtnMsg += "
"; rtnMsg += "password: "; rtnMsg += ""; rtnMsg += NMEA_PASSWORD; rtnMsg += "

"; rtnMsg += "A NMEA TCP server will be started on IP 10.1.1.1 port "; rtnMsg += storage.tcpPortNo; rtnMsg += "
A NMEA UDP Broadcast Group will be started on IP 230.1.1.1 port "; rtnMsg += storage.udpPortNo; rtnMsg += ""; rtnMsg += "

" "The Tx Power is set to "; rtnMsg += storage.txPower; rtnMsg += ""; rtnMsg += "

" "The NMEA Input Serial Baud Rate is set to "; rtnMsg += storage.baudRate; rtnMsg += ""; rtnMsg += "


" "
" "

" "
" "
" "" ""; webserver.send ( 200, "text/html", rtnMsg.c_str() ); submittedCalled = true; // enables reset handling } createSafeString(configMsg, 3000); void handleRoot() { configMsg = "" "" "" "" "
" "
" "
" "
" "" "
" "

" "This NEMA to WiFi Bridge sets up a Local Network (Access Point/Router) for your device/computer to connect to.
" "Both a TCP server and a UDP mulitcast broadcaster are started on this local network.
" "Up to 4 clients can connect to the TCP server and/or multiple UDP clients can join the UDP multicast group
" "If TCP clients do not disconnect cleanly, the 4 TCP slots can become filled.
" "In that case Power Cycle the NEMA to WiFi Bridge " "
" "" "
" "
" "" ""; webserver.send ( 200, "text/html", configMsg.c_str() ); } void handleNotFound() { handleRoot(); } void printStorage(struct EEPROM_storage* storagePtr) { #ifdef DEBUG if (debugPtr) { debugPtr->print("init:0x"); if (storagePtr->init < 16) { debugPtr->print('0'); } debugPtr->println(storagePtr->init, HEX); debugPtr->print("baudRate:"); debugPtr->println(storagePtr->baudRate); debugPtr->print("tcpPortNo:"); debugPtr->println(storagePtr->tcpPortNo); debugPtr->print("udpPortNo:"); debugPtr->println(storagePtr->udpPortNo); debugPtr->print("txPower:"); debugPtr->println(storagePtr->txPower); } #endif }