/* This code 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 code 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 St, Fifth Floor, Boston, MA 02110-1301 USA */ // Version 1.3, 04.08.2020, AK-Homberger //board specific pins #ifdef BOARD_M5ATOM #define ESP32_CAN_TX_PIN GPIO_NUM_22 #define ESP32_CAN_RX_PIN GPIO_NUM_19 #else #define ESP32_CAN_TX_PIN GPIO_NUM_5 // Set CAN TX port to 5 (Caution!!! Pin 2 before) #define ESP32_CAN_RX_PIN GPIO_NUM_4 // Set CAN RX port to 4 #endif #include #include // This will automatically choose right CAN library and create suitable NMEA2000 object #include #include #include #include #include #include #include "N2kDataToNMEA0183.h" #include "List.h" #include "index_html.h" #include "BoatData.h" #define ENABLE_DEBUG_LOG 0 // Debug log, set to 1 to enable AIS forward on USB-Serial / 2 for ADC voltage to support calibration #define UDP_Forwarding 0 // Set to 1 for forwarding AIS from serial2 to UDP brodcast #define HighTempAlarm 12 // Alarm level for fridge temperature (higher) #define LowVoltageAlarm 11 // Alarm level for battery voltage (lower) #define ADC_Calibration_Value 34.3 // The real value depends on the true resistor values for the ADC input (100K / 27 K) #define WLAN_CLIENT 0 // Set to 1 to enable client network. 0 to act as AP only // Wifi cofiguration Client and Access Point const char *AP_ssid = "MyESP32"; // ESP32 as AP const char *CL_ssid = "MyWLAN"; // ESP32 as client in network const char *AP_password = "appassw"; // AP password const char *CL_password = "clientpw"; // Client password // Put IP address details here IPAddress AP_local_ip(192, 168, 15, 1); // Static address for AP IPAddress AP_gateway(192, 168, 15, 1); IPAddress AP_subnet(255, 255, 255, 0); IPAddress CL_local_ip(192, 168, 1, 10); // Static address for Client Network. Please adjust to your AP IP and DHCP range! IPAddress CL_gateway(192, 168, 1, 1); IPAddress CL_subnet(255, 255, 255, 0); int wifiType = 0; // 0= Client 1= AP const uint16_t ServerPort = 2222; // Define the port, where server sends data. Use this e.g. on OpenCPN. Use 39150 for Navionis AIS // UPD broadcast for Navionics, OpenCPN, etc. const char * udpAddress = "192.168.15.255"; // UDP broadcast address. Should be the network of the ESP32 AP (please check!) const int udpPort = 2000; // port 2000 lets think Navionics it is an DY WLN10 device // Create UDP instance WiFiUDP udp; // Struct to update BoatData. See BoatData.h for content tBoatData BoatData; int NodeAddress; // To store last Node Address Preferences preferences; // Nonvolatile storage on ESP32 - To store LastDeviceAddress int alarmstate = false; // Alarm state (low voltage/temperature) int acknowledge = false; // Acknowledge for alarm, button pressed const size_t MaxClients = 10; bool SendNMEA0183Conversion = true; // Do we send NMEA2000 -> NMEA0183 conversion bool SendSeaSmart = false; // Do we send NMEA2000 messages in SeaSmart format WiFiServer server(ServerPort, MaxClients); WiFiServer json(90); using tWiFiClientPtr = std::shared_ptr; LinkedList clients; tN2kDataToNMEA0183 tN2kDataToNMEA0183(&NMEA2000, 0); // Set the information for other bus devices, which messages we support const unsigned long TransmitMessages[] PROGMEM = {127489L, // Engine dynamic 0 }; const unsigned long ReceiveMessages[] PROGMEM = {/*126992L,*/ // System time 127250L, // Heading 127258L, // Magnetic variation 128259UL,// Boat speed 128267UL,// Depth 129025UL,// Position 129026L, // COG and SOG 129029L, // GNSS 130306L, // Wind 128275UL,// Log 127245UL,// Rudder 0 }; // Forward declarations void HandleNMEA2000Msg(const tN2kMsg &N2kMsg); void SendNMEA0183Message(const tNMEA0183Msg &NMEA0183Msg); WebServer webserver(80); #define MiscSendOffset 120 #define SlowDataUpdatePeriod 1000 // Time between CAN Messages sent // Serial port 2 config (GPIO 16) const int baudrate = 38400; const int rs_config = SERIAL_8N1; // Buffer config #define MAX_NMEA0183_MESSAGE_SIZE 150 // For AIS char buff[MAX_NMEA0183_MESSAGE_SIZE]; // NMEA message for AIS receiving and multiplexing tNMEA0183Msg NMEA0183Msg; tNMEA0183 NMEA0183; void debug_log(char* str) { #if ENABLE_DEBUG_LOG == 1 Serial.println(str); #endif } void web_index() // Wenn "http:///" aufgerufen wurde { webserver.send(200, "text/html", indexHTML); //dann Index Webseite senden } void js_reset() // Wenn "http:///gauge.min.js" aufgerufen wurde { Serial.println("Reset Button"); ESP.restart(); } void handleNotFound() { webserver.send(404, "text/plain", "File Not Found\n\n"); } void setup() { uint8_t chipid[6]; uint32_t id = 0; int i = 0; int wifi_retry = 0; // Init USB serial port Serial.begin(115200); Serial.println("Starting..."); // Init AIS serial port 2 //Serial2.begin(baudrate, rs_config); //NMEA0183.Begin(&Serial2, 3, baudrate); if (WLAN_CLIENT == 1) { Serial.println("Start WLAN Client"); // WiFi Mode Client WiFi.config(CL_local_ip, CL_gateway, CL_subnet, CL_gateway); delay(100); WiFi.begin(CL_ssid, CL_password); while (WiFi.status() != WL_CONNECTED && wifi_retry < 20) { // Check connection, try 10 seconds wifi_retry++; delay(5000); Serial.print("."); } } if (WiFi.status() != WL_CONNECTED) { // No client connection start AP // Init wifi connection Serial.println("Start WLAN AP"); // WiFi Mode AP WiFi.mode(WIFI_AP); WiFi.softAP(AP_ssid, AP_password); delay(100); WiFi.softAPConfig(AP_local_ip, AP_gateway, AP_subnet); IPAddress IP = WiFi.softAPIP(); Serial.println(""); Serial.print("AP IP address: "); Serial.println(IP); wifiType = 1; } else { // Wifi Client connection was sucessfull Serial.println(""); Serial.println("WiFi client connected"); Serial.println("IP client address: "); Serial.println(WiFi.localIP()); } // Start TCP server server.begin(); // Start JSON server json.begin(); // Start Web Server webserver.on("/", web_index); webserver.on("/api/reset", js_reset); webserver.onNotFound(handleNotFound); webserver.begin(); Serial.println("HTTP server started"); // Reserve enough buffer for sending all messages. This does not work on small memory devices like Uno or Mega NMEA2000.SetN2kCANMsgBufSize(8); NMEA2000.SetN2kCANReceiveFrameBufSize(250); NMEA2000.SetN2kCANSendFrameBufSize(250); esp_efuse_read_mac(chipid); for (i = 0; i < 6; i++) id += (chipid[i] << (7 * i)); // Set product information NMEA2000.SetProductInformation("1", // Manufacturer's Model serial code 100, // Manufacturer's product code "NMEA 2000 WiFi Gateway", // Manufacturer's Model ID "1.0.2.25 (2019-07-07)", // Manufacturer's Software version code "1.0.2.0 (2019-07-07)" // Manufacturer's Model version ); // Set device information NMEA2000.SetDeviceInformation(id, // Unique number. Use e.g. Serial number. Id is generated from MAC-Address 130, // Device function=Analog to NMEA 2000 Gateway. See codes on http://www.nmea.org/Assets/20120726%20nmea%202000%20class%20&%20function%20codes%20v%202.00.pdf 25, // Device class=Inter/Intranetwork Device. See codes on http://www.nmea.org/Assets/20120726%20nmea%202000%20class%20&%20function%20codes%20v%202.00.pdf 2046 // Just choosen free from code list on http://www.nmea.org/Assets/20121020%20nmea%202000%20registration%20list.pdf ); // If you also want to see all traffic on the bus use N2km_ListenAndNode instead of N2km_NodeOnly below NMEA2000.SetForwardType(tNMEA2000::fwdt_Text); // Show in clear text. Leave uncommented for default Actisense format. preferences.begin("nvs", false); // Open nonvolatile storage (nvs) NodeAddress = preferences.getInt("LastNodeAddress", 32); // Read stored last NodeAddress, default 32 preferences.end(); Serial.printf("NodeAddress=%d\n", NodeAddress); NMEA2000.SetMode(tNMEA2000::N2km_ListenAndNode, NodeAddress); NMEA2000.ExtendTransmitMessages(TransmitMessages); NMEA2000.ExtendReceiveMessages(ReceiveMessages); NMEA2000.AttachMsgHandler(&tN2kDataToNMEA0183); // NMEA 2000 -> NMEA 0183 conversion NMEA2000.SetMsgHandler(HandleNMEA2000Msg); // Also send all NMEA2000 messages in SeaSmart format tN2kDataToNMEA0183.SetSendNMEA0183MessageCallback(SendNMEA0183Message); NMEA2000.Open(); } //***************************************************************************** //***************************************************************************** void SendBufToClients(const char *buf) { for (auto it = clients.begin() ; it != clients.end(); it++) { if ( (*it) != NULL && (*it)->connected() ) { (*it)->println(buf); } } } #define MAX_NMEA2000_MESSAGE_SEASMART_SIZE 500 //***************************************************************************** //NMEA 2000 message handler void HandleNMEA2000Msg(const tN2kMsg &N2kMsg) { if ( !SendSeaSmart ) return; char buf[MAX_NMEA2000_MESSAGE_SEASMART_SIZE]; if ( N2kToSeasmart(N2kMsg, millis(), buf, MAX_NMEA2000_MESSAGE_SEASMART_SIZE) == 0 ) return; SendBufToClients(buf); } //***************************************************************************** void SendNMEA0183Message(const tNMEA0183Msg &NMEA0183Msg) { if ( !SendNMEA0183Conversion ) return; char buf[MAX_NMEA0183_MESSAGE_SIZE]; if ( !NMEA0183Msg.GetMessage(buf, MAX_NMEA0183_MESSAGE_SIZE) ) return; SendBufToClients(buf); } bool IsTimeToUpdate(unsigned long NextUpdate) { return (NextUpdate < millis()); } unsigned long InitNextUpdate(unsigned long Period, unsigned long Offset = 0) { return millis() + Period + Offset; } void SetNextUpdate(unsigned long &NextUpdate, unsigned long Period) { while ( NextUpdate < millis() ) NextUpdate += Period; } void SendN2kEngine() { static unsigned long SlowDataUpdated = InitNextUpdate(SlowDataUpdatePeriod, MiscSendOffset); tN2kMsg N2kMsg; if ( IsTimeToUpdate(SlowDataUpdated) ) { SetNextUpdate(SlowDataUpdated, SlowDataUpdatePeriod); SetN2kEngineDynamicParam(N2kMsg, 0, N2kDoubleNA, N2kDoubleNA, N2kDoubleNA, N2kDoubleNA, N2kDoubleNA, N2kDoubleNA, N2kDoubleNA, N2kDoubleNA, N2kInt8NA, N2kInt8NA, true); NMEA2000.SendMsg(N2kMsg); } } //***************************************************************************** void AddClient(WiFiClient &client) { Serial.println("New Client."); clients.push_back(tWiFiClientPtr(new WiFiClient(client))); } //***************************************************************************** void StopClient(LinkedList::iterator &it) { Serial.println("Client Disconnected."); (*it)->stop(); it = clients.erase(it); } //***************************************************************************** void CheckConnections() { WiFiClient client = server.available(); // listen for incoming clients if ( client ) AddClient(client); for (auto it = clients.begin(); it != clients.end(); it++) { if ( (*it) != NULL ) { if ( !(*it)->connected() ) { StopClient(it); } else { if ( (*it)->available() ) { char c = (*it)->read(); if ( c == 0x03 ) StopClient(it); // Close connection by ctrl-c } } } else { it = clients.erase(it); // Should have been erased by StopClient } } } void handle_json() { WiFiClient client = json.available(); // Do we have a client? if (!client) return; // Serial.println(F("New client")); // Read the request (we ignore the content in this example) while (client.available()) client.read(); // Allocate JsonBuffer // Use arduinojson.org/assistant to compute the capacity. StaticJsonDocument<800> root; root["Latitude"] = BoatData.Latitude; root["Longitude"] = BoatData.Longitude; root["Heading"] = BoatData.Heading; root["COG"] = BoatData.COG; root["SOG"] = BoatData.SOG; root["STW"] = BoatData.STW; root["AWS"] = BoatData.AWS; root["TWS"] = BoatData.TWS; root["MaxAws"] = BoatData.MaxAws; root["MaxTws"] = BoatData.MaxTws; root["AWA"] = BoatData.AWA; root["TWA"] = BoatData.TWA; root["TWD"] = BoatData.TWD; root["TripLog"] = BoatData.TripLog; root["Log"] = BoatData.Log; root["RudderPosition"] = BoatData.RudderPosition; root["WaterTemperature"] = BoatData.WaterTemperature; root["WaterDepth"] = BoatData.WaterDepth; root["Variation"] = BoatData.Variation; root["Altitude"] = BoatData.Altitude; root["GPSTime"] = BoatData.GPSTime; root["DaysSince1970"] = BoatData.DaysSince1970; //Serial.print(F("Sending: ")); //serializeJson(root, Serial); //Serial.println(); // Write response headers client.println("HTTP/1.0 200 OK"); client.println("Content-Type: application/json"); client.println("Connection: close"); client.println(); // Write JSON document serializeJsonPretty(root, client); // Disconnect client.stop(); } void loop() { unsigned int size; int wifi_retry; webserver.handleClient(); handle_json(); if (NMEA0183.GetMessage(NMEA0183Msg)) { // Get AIS NMEA sentences from serial2 SendNMEA0183Message(NMEA0183Msg); // Send to TCP clients NMEA0183Msg.GetMessage(buff, MAX_NMEA0183_MESSAGE_SIZE); // send to buffer #if ENABLE_DEBUG_LOG == 1 Serial.println(buff); #endif #if UDP_Forwarding == 1 size = strlen(buff); udp.beginPacket(udpAddress, udpPort); // Send to UDP udp.write((byte*)buff, size); udp.endPacket(); #endif } SendN2kEngine(); CheckConnections(); NMEA2000.ParseMessages(); int SourceAddress = NMEA2000.GetN2kSource(); if (SourceAddress != NodeAddress) { // Save potentially changed Source Address to NVS memory NodeAddress = SourceAddress; // Set new Node Address (to save only once) preferences.begin("nvs", false); preferences.putInt("LastNodeAddress", SourceAddress); preferences.end(); Serial.printf("Address Change: New Address=%d\n", SourceAddress); } tN2kDataToNMEA0183.Update(&BoatData); // Dummy to empty input buffer to avoid board to stuck with e.g. NMEA Reader if ( Serial.available() ) { Serial.read(); } if (wifiType == 0) { // Check connection if working as client wifi_retry = 0; while (WiFi.status() != WL_CONNECTED && wifi_retry < 5 ) { // Connection lost, 5 tries to reconnect wifi_retry++; Serial.println("WiFi not connected. Try to reconnect"); WiFi.disconnect(); WiFi.mode(WIFI_OFF); WiFi.mode(WIFI_STA); WiFi.begin(CL_ssid, CL_password); delay(1000); } if (wifi_retry >= 5) { Serial.println("\nReboot"); // Did not work -> restart ESP32 ESP.restart(); } } }