/* 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 */ #include "GwAppInfo.h" // #define GW_MESSAGE_DEBUG_ENABLED //#define FALLBACK_SERIAL //#define CAN_ESP_DEBUG const unsigned long HEAP_REPORT_TIME=2000; //set to 0 to disable heap reporting #include #include "GwApi.h" #include "GwHardware.h" #ifndef N2K_LOAD_LEVEL #define N2K_LOAD_LEVEL 0 #endif #ifndef N2K_CERTIFICATION_LEVEL #define N2K_CERTIFICATION_LEVEL 0xff #endif #include #include #include #include #include #include #include #include #include #include #include "esp_heap_caps.h" #include #include "GwJsonDocument.h" #include "N2kDataToNMEA0183.h" #include #include "GwLog.h" #include "GWConfig.h" #include "GWWifi.h" #include "GwSocketServer.h" #include "GwBoatData.h" #include "GwMessage.h" #include "GwSerial.h" #include "GwWebServer.h" #include "NMEA0183DataToN2K.h" #include "GwButtons.h" #include "GwLeds.h" #include "GwCounter.h" #include "GwXDRMappings.h" #include "GwSynchronized.h" #include "GwUserCode.h" #include "GwStatistics.h" #include "GwUpdate.h" #include "GwTcpClient.h" #include "GwChannel.h" #include "GwChannelList.h" #include "GwTimer.h" #ifdef FALLBACK_SERIAL #ifdef CAN_ESP_DEBUG #define CDBS &Serial #else #define CDBS NULL #endif #else #define CDBS NULL #endif #define MAX_NMEA2000_MESSAGE_SEASMART_SIZE 500 #define MAX_NMEA0183_MESSAGE_SIZE MAX_NMEA2000_MESSAGE_SEASMART_SIZE //https://curiouser.cheshireeng.com/2014/08/19/c-compile-time-assert/ #define CASSERT(predicate, text) _impl_CASSERT_LINE(predicate,__LINE__) #define _impl_PASTE(a,b) a##b #define _impl_CASSERT_LINE(predicate, line) typedef char _impl_PASTE(assertion_failed_CASSERT_,line)[(predicate)?1:-1]; //assert length of firmware name and version CASSERT(strlen(FIRMWARE_TYPE) <= 32, "environment name (FIRMWARE_TYPE) must not exceed 32 chars"); CASSERT(strlen(VERSION) <= 32, "VERSION must not exceed 32 chars"); //https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/app_image_format.html //and removed the bugs in the doc... __attribute__((section(".rodata_custom_desc"))) esp_app_desc_t custom_app_desc = { ESP_APP_DESC_MAGIC_WORD, 1, {0,0}, VERSION, FIRMWARE_TYPE, "00:00:00", "2021/12/13", "0000", {}, {} }; String firmwareType(FIRMWARE_TYPE); typedef std::map StringMap; GwLog logger(LOGLEVEL,NULL); GwConfigHandler config(&logger); #include "Nmea2kTwai.h" static const unsigned long CAN_RECOVERY_PERIOD=3000; //ms static const unsigned long NMEA2000_HEARTBEAT_INTERVAL=5000; class Nmea2kTwaiLog : public Nmea2kTwai{ private: GwLog* logger; public: Nmea2kTwaiLog(gpio_num_t _TxPin, gpio_num_t _RxPin, unsigned long recoveryPeriod,GwLog *l): Nmea2kTwai(_TxPin,_RxPin,recoveryPeriod,recoveryPeriod),logger(l){} virtual void logDebug(int level, const char *fmt,...){ va_list args; va_start(args,fmt); if (level > 2) level++; //error+info+debug are similar, map msg to 4 logger->logDebug(level,fmt,args); } }; #ifndef ESP32_CAN_TX_PIN #pragma message "WARNING: ESP32_CAN_TX_PIN not defined" #define ESP32_CAN_TX_PIN GPIO_NUM_NC #endif #ifndef ESP32_CAN_RX_PIN #pragma message "WARNING: ESP32_CAN_RX_PIN not defined" #define ESP32_CAN_RX_PIN GPIO_NUM_NC #endif Nmea2kTwai &NMEA2000=*(new Nmea2kTwaiLog(ESP32_CAN_TX_PIN,ESP32_CAN_RX_PIN,CAN_RECOVERY_PERIOD,&logger)); #ifdef GWBUTTON_PIN bool fixedApPass=false; #else #ifdef FORCE_AP_PWCHANGE bool fixedApPass=false; #else bool fixedApPass=true; #endif #endif GwWifi gwWifi(&config,&logger,fixedApPass); GwChannelList channels(&logger,&config); GwBoatData boatData(&logger); GwXDRMappings xdrMappings(&logger,&config); bool sendOutN2k=true; int NodeAddress; // To store last Node Address Preferences preferences; // Nonvolatile storage on ESP32 - To store LastDeviceAddress N2kDataToNMEA0183 *nmea0183Converter=NULL; NMEA0183DataToN2K *toN2KConverter=NULL; SemaphoreHandle_t mainLock; GwRequestQueue mainQueue(&logger,20); GwWebServer webserver(&logger,&mainQueue,80); GwCounter countNMEA2KIn("count2Kin"); GwCounter countNMEA2KOut("count2Kout"); GwIntervalRunner timers; bool checkPass(String hash){ return config.checkPass(hash); } GwUpdate updater(&logger,&webserver,&checkPass); GwConfigInterface *systemName=config.getConfigItem(config.systemName,true); void handleN2kMessage(const tN2kMsg &n2kMsg,int sourceId, bool isConverted=false) { logger.logDebug(GwLog::DEBUG + 1, "N2K: pgn %d, dir %d", n2kMsg.PGN,sourceId); if (sourceId == N2K_CHANNEL_ID){ countNMEA2KIn.add(n2kMsg.PGN); } char *buf=new char[MAX_NMEA2000_MESSAGE_SEASMART_SIZE+3]; std::unique_ptr bufDel(buf); bool messageCreated=false; channels.allChannels([&](GwChannel *c){ if (c->sendSeaSmart()){ if (! messageCreated){ size_t len; if ((len=N2kToSeasmart(n2kMsg, millis(), buf, MAX_NMEA2000_MESSAGE_SEASMART_SIZE)) != 0) { buf[len]=0x0d; len++; buf[len]=0x0a; len++; buf[len]=0; messageCreated=true; } } if (messageCreated){ c->sendToClients(buf,sourceId,true); } } }); channels.allChannels([&](GwChannel *c){ c->sendActisense(n2kMsg,sourceId); }); if (! isConverted){ nmea0183Converter->HandleMsg(n2kMsg,sourceId); } if (sourceId != N2K_CHANNEL_ID && sendOutN2k){ countNMEA2KOut.add(n2kMsg.PGN); if (NMEA2000.SendMsg(n2kMsg)){ countNMEA2KOut.add(n2kMsg.PGN); } else{ countNMEA2KOut.addFail(n2kMsg.PGN); } } }; //***************************************************************************** void SendNMEA0183Message(const tNMEA0183Msg &NMEA0183Msg, int sourceId,bool convert=false) { logger.logDebug(GwLog::DEBUG+2,"SendNMEA0183(1)"); char *buf=new char[MAX_NMEA0183_MESSAGE_SIZE+3]; std::unique_ptr bufDel(buf); if ( !NMEA0183Msg.GetMessage(buf, MAX_NMEA0183_MESSAGE_SIZE) ) return; logger.logDebug(GwLog::DEBUG+2,"SendNMEA0183: %s",buf); if (convert){ toN2KConverter->parseAndSend(buf,sourceId); } size_t len=strlen(buf); buf[len]=0x0d; buf[len+1]=0x0a; buf[len+2]=0; channels.allChannels([&](GwChannel *c){ c->sendToClients(buf,sourceId,false); }); } class ApiImpl : public GwApi { private: int sourceId = -1; public: ApiImpl(int sourceId) { this->sourceId = sourceId; } virtual GwRequestQueue *getQueue() { return &mainQueue; } virtual void sendN2kMessage(const tN2kMsg &msg,bool convert) { handleN2kMessage(msg,sourceId,!convert); } virtual void sendNMEA0183Message(const tNMEA0183Msg &msg, int sourceId,bool convert) { SendNMEA0183Message(msg, sourceId,convert); } virtual void sendNMEA0183Message(const tNMEA0183Msg &msg, bool convert) { SendNMEA0183Message(msg, sourceId,convert); } virtual int getSourceId() { return sourceId; }; virtual GwConfigHandler *getConfig() { return &config; } virtual GwLog* getLogger(){ return &logger; } virtual void getBoatDataValues(int numValues,BoatValue **list){ for (int i=0;igetName()); list[i]->changed=false; if (item){ bool newValid=item->isValid(); if (newValid != list[i]->valid) list[i]->changed=true; list[i]->valid=newValid; if (newValid){ double newValue=item->getDoubleValue(); if (newValue != list[i]->value) list[i]->changed=true; list[i]->value=newValue; int newSource=item->getLastSource(); if (newSource != list[i]->source){ list[i]->source=newSource; list[i]->changed=true; } } list[i]->setFormat(item->getFormat()); } else{ if (list[i]->valid) list[i]->changed=true; list[i]->valid=false; } } } virtual void getStatus(Status &status){ status.empty(); status.wifiApOn=gwWifi.isApActive(); status.wifiClientOn=gwWifi.isClientActive(); status.wifiClientConnected=gwWifi.clientConnected(); status.wifiApIp=gwWifi.apIP(); status.systemName=systemName->asString(); status.wifiApPass=config.getString(config.apPassword); status.wifiClientIp=WiFi.localIP().toString(); status.wifiClientSSID=config.getString(config.wifiSSID); status.n2kRx=countNMEA2KIn.getGlobal(); status.n2kTx=countNMEA2KOut.getGlobal(); channels.fillStatus(status); } virtual GwBoatData *getBoatData(){ return &boatData; } virtual const char* getTalkerId(){ return config.getString(config.talkerId,String("GP")).c_str(); } virtual ~ApiImpl(){} }; bool delayedRestart(){ return xTaskCreate([](void *p){ GwLog *logRef=(GwLog *)p; logRef->logDebug(GwLog::LOG,"delayed reset started"); delay(800); ESP.restart(); vTaskDelete(NULL); },"reset",2000,&logger,0,NULL) == pdPASS; } ApiImpl *apiImpl=new ApiImpl(MIN_USER_TASK); GwUserCode userCodeHandler(apiImpl,&mainLock); #define JSON_OK "{\"status\":\"OK\"}" #define JSON_INVALID_PASS F("{\"status\":\"invalid password\"}") //WebServer requests that should //be processed inside the main loop //this prevents us from the need to sync all the accesses class ResetRequest : public GwRequestMessage { String hash; public: ResetRequest(String hash) : GwRequestMessage(F("application/json"),F("reset")){ this->hash=hash; }; protected: virtual void processRequest() { logger.logDebug(GwLog::LOG, "Reset Button"); if (! checkPass(hash)){ result=JSON_INVALID_PASS; return; } result = JSON_OK; if (!delayedRestart()){ logger.logDebug(GwLog::ERROR,"cannot initiate restart"); } } }; class StatusRequest : public GwRequestMessage { public: StatusRequest() : GwRequestMessage(F("application/json"),F("status")){}; protected: virtual void processRequest() { GwJsonDocument status(290 + countNMEA2KIn.getJsonSize()+ countNMEA2KOut.getJsonSize() + channels.getJsonSize() ); status["version"] = VERSION; status["wifiConnected"] = gwWifi.clientConnected(); status["clientIP"] = WiFi.localIP().toString(); status["apIp"] = gwWifi.apIP(); size_t bsize=2*sizeof(unsigned long)+1; unsigned long base=config.getSaltBase() + ( millis()/1000UL & ~0x7UL); char buffer[bsize]; GwConfigHandler::toHex(base,buffer,bsize); status["salt"] = buffer; status["fwtype"]= firmwareType; status["heap"]=(long)xPortGetFreeHeapSize(); Nmea2kTwai::Status n2kState=NMEA2000.getStatus(); Nmea2kTwai::STATE driverState=n2kState.state; if (driverState == Nmea2kTwai::ST_RUNNING){ unsigned long lastRec=NMEA2000.getLastRecoveryStart(); if (lastRec > 0 && (lastRec+NMEA2000_HEARTBEAT_INTERVAL*2) > millis()){ //we still report bus off at least for 2 heartbeat intervals //this avoids always reporting BUS_OFF-RUNNING-BUS_OFF if the bus off condition //remains driverState=Nmea2kTwai::ST_BUS_OFF; } } status["n2kstate"]=NMEA2000.stateStr(driverState); status["n2knode"]=NodeAddress; //nmea0183Converter->toJson(status); countNMEA2KIn.toJson(status); countNMEA2KOut.toJson(status); channels.toJson(status); serializeJson(status, result); } }; class CheckPassRequest : public GwRequestMessage{ String hash; public: CheckPassRequest(String h): GwRequestMessage(F("application/json"),F("checkPass")){ this->hash=h; }; protected: virtual void processRequest(){ bool res=checkPass(hash); if (res) result=JSON_OK; else result=F("{\"status\":\"ERROR\"}"); } }; class CapabilitiesRequest : public GwRequestMessage{ public: CapabilitiesRequest() : GwRequestMessage(F("application/json"),F("capabilities")){}; protected: virtual void processRequest(){ int numCapabilities=userCodeHandler.getCapabilities()->size(); GwJsonDocument json(JSON_OBJECT_SIZE(numCapabilities*3+6)); for (auto it=userCodeHandler.getCapabilities()->begin(); it != userCodeHandler.getCapabilities()->end();it++){ json[it->first]=it->second; } #ifdef GWSERIAL_MODE String serial(F(GWSERIAL_MODE)); #else String serial(F("NONE")); #endif json["serialmode"]=serial; #ifdef GWBUTTON_PIN json["hardwareReset"]="true"; #endif json["apPwChange"] = fixedApPass?"false":"true"; serializeJson(json,result); } }; class ConverterInfoRequest : public GwRequestMessage{ public: ConverterInfoRequest() : GwRequestMessage(F("application/json"),F("converterInfo")){}; protected: virtual void processRequest(){ GwJsonDocument json(512); String keys=toN2KConverter->handledKeys(); logger.logDebug(GwLog::DEBUG,"handled nmea0183: %s",keys.c_str()); json["nmea0183"]=keys; keys=nmea0183Converter->handledKeys(); logger.logDebug(GwLog::DEBUG,"handled nmea2000: %s",keys.c_str()); json["nmea2000"]=keys; serializeJson(json,result); } }; class ConfigRequest : public GwRequestMessage { public: ConfigRequest() : GwRequestMessage(F("application/json"),F("config")){}; protected: virtual void processRequest() { result = config.toJson(); } }; class ResetConfigRequest : public GwRequestMessage { String hash; public: ResetConfigRequest(String hash) : GwRequestMessage(F("application/json"),F("resetConfig")){ this->hash=hash; }; virtual int getTimeout(){return 4000;} protected: virtual void processRequest() { if (! checkPass(hash)){ result=JSON_INVALID_PASS; return; } config.reset(); logger.logDebug(GwLog::ERROR,"reset config, restart"); result = JSON_OK; delayedRestart(); } }; class BoatDataRequest : public GwRequestMessage { public: BoatDataRequest() : GwRequestMessage(F("application/json"),F("boatData")){}; protected: virtual void processRequest() { result = boatData.toJson(); } }; class BoatDataStringRequest : public GwRequestMessage { public: BoatDataStringRequest() : GwRequestMessage(F("text/plain"),F("boatDataString")){}; protected: virtual void processRequest() { result = boatData.toString(); } }; class XdrExampleRequest : public GwRequestMessage { public: String mapping; double value; XdrExampleRequest(String mapping, double value) : GwRequestMessage(F("text/plain"),F("xdrExample")){ this->mapping=mapping; this->value=value; }; protected: virtual void processRequest() { String val=xdrMappings.getXdrEntry(mapping,value); if (val == "") { result=val; return; } tNMEA0183Msg msg; msg.Init("XDR",config.getString(config.talkerId,String("GP")).c_str()); msg.AddStrField(val.c_str()); char *buf=new char[MAX_NMEA0183_MSG_BUF_LEN+2]; std::unique_ptr bufDel(buf); msg.GetMessage(buf,MAX_NMEA0183_MSG_BUF_LEN); result=buf; } }; class XdrUnMappedRequest : public GwRequestMessage { public: XdrUnMappedRequest() : GwRequestMessage(F("text/plain"),F("boatData")){}; protected: virtual void processRequest() { result = xdrMappings.getUnMapped(); } }; void handleConfigRequestData(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){ typedef struct{ char notFirst; char hashChecked; char parsingValue; int bName; char name[33]; int bValue; char value[512]; }RequestNV; long long lastRecords=logger.getRecordCounter(); logger.logDebug(GwLog::DEBUG,"handleConfigRequestData len=%d,idx=%d,total=%d",(int)len,(int)index,(int)total); if (request->_tempObject == NULL){ logger.logDebug(GwLog::DEBUG,"handleConfigRequestData create receive struct"); //we cannot use new here as it will be deleted with free request->_tempObject=malloc(sizeof(RequestNV)); memset(request->_tempObject,0,sizeof(RequestNV)); } RequestNV *nv=(RequestNV*)(request->_tempObject); if (nv->notFirst && ! nv->hashChecked){ return; //ignore data } int parsed=0; while (parsed < len) { if (!nv->parsingValue) { int maxSize = sizeof(RequestNV::name) - 1; if (nv->bName >= maxSize) { nv->name[maxSize] = 0; logger.logDebug(GwLog::DEBUG, "parse error name too long %s", nv->name); nv->bName = 0; } while (nv->bName < maxSize && parsed < len) { bool endName = *data == '='; nv->name[nv->bName] = endName ? 0 : *data; nv->bName++; parsed++; data++; if (endName) { nv->parsingValue = 1; break; } } } bool valueDone = false; if (nv->parsingValue) { int maxSize = sizeof(RequestNV::value) - 1; if (nv->bValue >= maxSize) { nv->value[maxSize] = 0; logger.logDebug(GwLog::DEBUG, "parse error value too long %s:%s", nv->name, nv->value); nv->bValue = 0; } while (nv->bValue < maxSize && parsed < len) { valueDone = *data == '&'; nv->value[nv->bValue] = valueDone ? 0 : *data; nv->bValue++; parsed++; data++; if (valueDone) break; } if (! valueDone){ if (parsed >= len && (len+index) >= total){ //request ends here nv->value[nv->bValue]=0; valueDone=true; } } if (valueDone){ String name(nv->name); String value(nv->value); if (! nv->notFirst){ nv->notFirst=1; //we expect the _hash as first parameter if (name != String("_hash")){ logger.logDebug(GwLog::ERROR,"missing first parameter _hash in setConfig"); request->send(200,"application/json","{\"status\":\"missing _hash\"}"); return; } if (! config.checkPass(request->urlDecode(value))){ request->send(200,"application/json",JSON_INVALID_PASS); return; } else{ nv->hashChecked=1; } } else{ if (nv->hashChecked){ logger.logDebug(GwLog::DEBUG,"value ns=%d,n=%s,vs=%d,v=%s",nv->bName,nv->name,nv->bValue,nv->value); if ((logger.getRecordCounter() - lastRecords) > 20){ logger.flush(); lastRecords=logger.getRecordCounter(); } config.updateValue(request->urlDecode(name),request->urlDecode(value)); } } nv->parsingValue=0; nv->bName=0; nv->bValue=0; } } } if (parsed >= len && (len+index)>= total){ if (nv->notFirst){ if (nv->hashChecked){ request->send(200,"application/json",JSON_OK); logger.flush(); logger.logDebug(GwLog::DEBUG,"Heap free=%ld, minFree=%ld", (long)xPortGetFreeHeapSize(), (long)xPortGetMinimumEverFreeHeapSize() ); logger.flush(); delayedRestart(); } } else{ request->send(200,"application/json","{\"status\":\"missing _hash\"}"); } } } TimeMonitor monitor(20,0.2); void setup() { mainLock=xSemaphoreCreateMutex(); uint8_t chipid[6]; uint32_t id = 0; config.loadConfig(); int level=config.getInt(config.logLevel,LOGLEVEL); logger.setLevel(level); bool fallbackSerial=false; #ifdef FALLBACK_SERIAL fallbackSerial=true; //falling back to old style serial for logging Serial.begin(115200); Serial.printf("fallback serial enabled\n"); logger.prefix="FALLBACK:"; #endif userCodeHandler.startInitTasks(MIN_USER_TASK); config.stopChanges(); //maybe the user code changed the level level=config.getInt(config.logLevel,LOGLEVEL); logger.setLevel(level); sendOutN2k=config.getBool(config.sendN2k,true); logger.logDebug(GwLog::LOG,"send N2k=%s",(sendOutN2k?"true":"false")); gwWifi.setup(); MDNS.begin(config.getConfigItem(config.systemName)->asCString()); channels.begin(fallbackSerial); logger.flush(); webserver.registerMainHandler("/api/reset", [](AsyncWebServerRequest *request)->GwRequestMessage *{ return new ResetRequest(request->arg("_hash")); }); webserver.registerMainHandler("/api/capabilities", [](AsyncWebServerRequest *request)->GwRequestMessage *{ return new CapabilitiesRequest(); }); webserver.registerMainHandler("/api/converterInfo", [](AsyncWebServerRequest *request)->GwRequestMessage *{ return new ConverterInfoRequest(); }); webserver.registerMainHandler("/api/status", [](AsyncWebServerRequest *request)->GwRequestMessage * { return new StatusRequest(); }); webserver.registerMainHandler("/api/config", [](AsyncWebServerRequest *request)->GwRequestMessage * { return new ConfigRequest(); }); webserver.registerMainHandler("/api/resetConfig", [](AsyncWebServerRequest *request)->GwRequestMessage * { return new ResetConfigRequest(request->arg("_hash")); }); webserver.registerMainHandler("/api/boatData", [](AsyncWebServerRequest *request)->GwRequestMessage * { return new BoatDataRequest(); }); webserver.registerMainHandler("/api/boatDataString", [](AsyncWebServerRequest *request)->GwRequestMessage * { return new BoatDataStringRequest(); }); webserver.registerMainHandler("/api/xdrExample", [](AsyncWebServerRequest *request)->GwRequestMessage * { String mapping=request->arg("mapping"); double value=atof(request->arg("value").c_str()); return new XdrExampleRequest(mapping,value); }); webserver.registerMainHandler("/api/xdrUnmapped", [](AsyncWebServerRequest *request)->GwRequestMessage * { return new XdrUnMappedRequest(); }); webserver.registerMainHandler("/api/checkPass", [](AsyncWebServerRequest *request)->GwRequestMessage * { String hash=request->arg("hash"); return new CheckPassRequest(hash); }); webserver.registerPostHandler("/api/setConfig", [](AsyncWebServerRequest *request){ }, handleConfigRequestData); webserver.begin(); xdrMappings.begin(); logger.flush(); nmea0183Converter= N2kDataToNMEA0183::create(&logger, &boatData, [](const tNMEA0183Msg &msg, int sourceId){ SendNMEA0183Message(msg,sourceId,false); } , config.getString(config.talkerId,String("GP")), &xdrMappings, config.getInt(config.minXdrInterval,100) ); toN2KConverter= NMEA0183DataToN2K::create(&logger,&boatData,[](const tN2kMsg &msg, int sourceId)->bool{ logger.logDebug(GwLog::DEBUG+2,"send N2K %ld",msg.PGN); handleN2kMessage(msg,sourceId,true); return true; }, &xdrMappings, config.getInt(config.min2KInterval,50) ); NMEA2000.SetN2kCANMsgBufSize(8); NMEA2000.SetN2kCANReceiveFrameBufSize(250); NMEA2000.SetN2kCANSendFrameBufSize(250); esp_efuse_mac_get_default(chipid); for (int 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 systemName->asCString(), // Manufacturer's Model ID FIRMWARE_TYPE, // Manufacturer's Software version code VERSION, // Manufacturer's Model version, N2K_LOAD_LEVEL, 0xffff, //Version N2K_CERTIFICATION_LEVEL ); // 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(); logger.logDebug(GwLog::LOG,"NodeAddress=%d", NodeAddress); logger.flush(); NMEA2000.SetMode(tNMEA2000::N2km_ListenAndNode, NodeAddress); NMEA2000.SetForwardOwnMessages(false); NMEA2000.SetHeartbeatInterval(NMEA2000_HEARTBEAT_INTERVAL); if (sendOutN2k){ // Set the information for other bus devices, which messages we support unsigned long *pgns=toN2KConverter->handledPgns(); if (logger.isActive(GwLog::DEBUG)){ unsigned long *op=pgns; while (*op != 0){ logger.logDebug(GwLog::DEBUG,"add transmit pgn %ld",(long)(*op)); logger.flush(); op++; } } NMEA2000.ExtendTransmitMessages(pgns); } NMEA2000.ExtendReceiveMessages(nmea0183Converter->handledPgns()); NMEA2000.SetMsgHandler([](const tN2kMsg &n2kMsg){ handleN2kMessage(n2kMsg,N2K_CHANNEL_ID); }); NMEA2000.Open(); logger.logDebug(GwLog::LOG,"starting addon tasks"); logger.flush(); userCodeHandler.startAddonTask(F("handleButtons"),handleButtons,100); setLedMode(LED_GREEN); userCodeHandler.startAddonTask(F("handleLeds"),handleLeds,101); { GWSYNCHRONIZED(&mainLock); userCodeHandler.startUserTasks(MIN_USER_TASK); } timers.addAction(HEAP_REPORT_TIME,[](){ if (logger.isActive(GwLog::DEBUG)){ logger.logDebug(GwLog::DEBUG,"Heap free=%ld, minFree=%ld", (long)xPortGetFreeHeapSize(), (long)xPortGetMinimumEverFreeHeapSize() ); logger.logDebug(GwLog::DEBUG,"Main loop %s",monitor.getLog().c_str()); } }); logger.logString("wifi AP pass: %s",fixedApPass? gwWifi.AP_password:config.getString(config.apPassword).c_str()); logger.logString("admin pass: %s",config.getString(config.adminPassword).c_str()); logger.logDebug(GwLog::LOG,"setup done"); } //***************************************************************************** void handleSendAndRead(bool handleRead){ channels.allChannels([&](GwChannel *c){ c->loop(handleRead,true); }); } void loop() { monitor.reset(); GWSYNCHRONIZED(&mainLock); logger.flush(); monitor.setTime(1); gwWifi.loop(); unsigned long now=millis(); monitor.setTime(2); timers.loop(); NMEA2000.loop(); monitor.setTime(3); channels.allChannels([](GwChannel *c){ c->loop(true,false); }); //reads monitor.setTime(4); channels.allChannels([](GwChannel *c){ c->loop(false,true); }); //writes monitor.setTime(5); NMEA2000.ParseMessages(); monitor.setTime(6); 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(); logger.logDebug(GwLog::LOG,"Address Change: New Address=%d\n", SourceAddress); } nmea0183Converter->loop(); monitor.setTime(7); //read channels channels.allChannels([](GwChannel *c){ c->readMessages([&](const char * buffer, int sourceId){ bool isSeasmart=false; if (strlen(buffer) > 6 && strncmp(buffer,"$PCDIN",6) == 0){ isSeasmart=true; } channels.allChannels([&](GwChannel *oc){ oc->sendToClients(buffer,sourceId,isSeasmart); oc->loop(false,true); }); if (c->sendToN2K()){ if (isSeasmart){ tN2kMsg n2kMsg; uint32_t timestamp; if (SeasmartToN2k(buffer,timestamp,n2kMsg)){ handleN2kMessage(n2kMsg,sourceId); } } else{ toN2KConverter->parseAndSend(buffer, sourceId); } } }); }); monitor.setTime(8); channels.allChannels([](GwChannel *c){ c->parseActisense([](const tN2kMsg &msg,int source){ handleN2kMessage(msg,source); }); }); monitor.setTime(9); //handle message requests GwMessage *msg=mainQueue.fetchMessage(0); if (msg){ msg->process(); msg->unref(); } monitor.setTime(10); }